mirror of
https://github.com/go-gitea/gitea.git
synced 2026-08-27 00:13:50 +00:00
feat(diff): Add search and extension filter to diff sidebar (#37068)
Adds a search box and a file-extension filter to the pull request diff sidebar, so reviewers can narrow a large diff down to the files they care about. Both filters apply to the file tree and to the diff itself. The extension menu follows GitHub: extensions sorted alphabetically, dotfiles and extension-less files in their own buckets, and the selection kept in the same `file-filters[]` query parameter, so a filtered view is shareable and survives a reload. The menu can list every extension in a diff, so `createTippy` gains an opt-in `limitSizeToViewport` option that caps a popup to the space left in the viewport and scrolls its content. Popups that do not ask for it are unchanged. Closes https://github.com/go-gitea/gitea/issues/27256 Signed-off-by: silverwind <me@silverwind.io> Signed-off-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: silverwind <me@silverwind.io> Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com> Co-authored-by: Copilot <copilot@github.com> Co-authored-by: Nicolas <bircni@icloud.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
@@ -7,8 +7,11 @@ import {stripTags} from '../utils.ts';
|
||||
type TippyOpts = {
|
||||
role?: string,
|
||||
theme?: 'default' | 'tooltip' | 'menu' | 'box-with-header' | 'bare',
|
||||
limitSizeToViewport?: {horizontal?: boolean, vertical?: boolean}, // cap to the viewport and scroll the content
|
||||
} & Partial<Props>;
|
||||
|
||||
type PopperModifier = NonNullable<NonNullable<Props['popperOptions']>['modifiers']>[number];
|
||||
|
||||
const visibleInstances = new Set<Instance>();
|
||||
const arrowSvg = html`<svg width="16" height="7"><path d="m0 7 8-7 8 7Z" class="tippy-svg-arrow-outer"/><path d="m0 8 8-7 8 7Z" class="tippy-svg-arrow-inner"/></svg>`;
|
||||
|
||||
@@ -20,10 +23,46 @@ function arrowPadding({placement, reference}: {placement: Placement, reference:
|
||||
return Math.max(0, Math.min(3, referenceLength / 2 - 8)); // 8 = half of arrow width
|
||||
}
|
||||
|
||||
const viewportPadding = 8;
|
||||
|
||||
// space left on the side the popup opens towards, the viewport on the other axis
|
||||
export function availableSizeForPlacement(referenceRect: DOMRect, placement: string, offset: number): {width: number, height: number} {
|
||||
const gap = Math.abs(offset) + viewportPadding;
|
||||
const spanWidth = window.innerWidth - viewportPadding * 2;
|
||||
const spanHeight = window.innerHeight - viewportPadding * 2;
|
||||
const side = placement.split('-')[0];
|
||||
if (side === 'top') return {width: spanWidth, height: referenceRect.top - gap};
|
||||
if (side === 'bottom') return {width: spanWidth, height: window.innerHeight - referenceRect.bottom - gap};
|
||||
if (side === 'left') return {width: referenceRect.left - gap, height: spanHeight};
|
||||
return {width: window.innerWidth - referenceRect.right - gap, height: spanHeight};
|
||||
}
|
||||
|
||||
// popper does not constrain size, publish it for the styles. Replaced by floating-ui's "size" on migration
|
||||
function sizeModifier(limit: {horizontal?: boolean, vertical?: boolean}): PopperModifier {
|
||||
return {
|
||||
name: 'tippyLimitSize',
|
||||
enabled: true,
|
||||
phase: 'beforeWrite',
|
||||
requires: ['computeStyles'], // runs after "flip" and "offset", so both are final
|
||||
fn({state}) {
|
||||
const offset = state.modifiersData.offset?.[state.placement];
|
||||
const isVertical = state.placement.startsWith('top') || state.placement.startsWith('bottom');
|
||||
const available = availableSizeForPlacement(
|
||||
state.elements.reference.getBoundingClientRect(),
|
||||
state.placement,
|
||||
(isVertical ? offset?.y : offset?.x) ?? 0,
|
||||
);
|
||||
const {style} = state.elements.popper;
|
||||
if (limit.horizontal) style.setProperty('--tippy-max-width', `${Math.max(0, Math.floor(available.width))}px`);
|
||||
if (limit.vertical) style.setProperty('--tippy-max-height', `${Math.max(0, Math.floor(available.height))}px`);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function createTippy(target: Element, opts: TippyOpts = {}): Instance {
|
||||
// the callback functions should be destructured from opts,
|
||||
// because we should use our own wrapper functions to handle them, do not let the user override them
|
||||
const {onHide, onShow, onDestroy, role, theme, arrow, ...other} = opts;
|
||||
const {onHide, onShow, onDestroy, role, theme, arrow, limitSizeToViewport, ...other} = opts;
|
||||
// CSS theme, either "default", "tooltip", "menu", "box-with-header" or "bare"
|
||||
const resolvedTheme = theme || role || 'default';
|
||||
const resolvedArrow = arrow ?? (resolvedTheme === 'bare' ? false : arrowSvg);
|
||||
@@ -56,7 +95,10 @@ export function createTippy(target: Element, opts: TippyOpts = {}): Instance {
|
||||
return onShow?.(instance);
|
||||
},
|
||||
arrow: resolvedArrow,
|
||||
popperOptions: {modifiers: [{name: 'arrow', options: {padding: arrowPadding}}]},
|
||||
popperOptions: {modifiers: [
|
||||
{name: 'arrow', options: {padding: arrowPadding}},
|
||||
...limitSizeToViewport ? [sizeModifier(limitSizeToViewport)] : [],
|
||||
]},
|
||||
// HTML role attribute, ideally the default role would be "popover" but it does not exist
|
||||
role: role || 'menu',
|
||||
theme: resolvedTheme,
|
||||
@@ -68,6 +110,7 @@ export function createTippy(target: Element, opts: TippyOpts = {}): Instance {
|
||||
if (instance.props.role === 'menu') {
|
||||
target.setAttribute('aria-haspopup', 'true');
|
||||
}
|
||||
if (limitSizeToViewport) instance.popper.setAttribute('data-tippy-limit-size', '');
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user