mirror of
https://github.com/go-gitea/gitea.git
synced 2026-02-22 04:35:44 +01:00
Add "Expand all" / "Collapse expanded" toggle button in diff file boxes
Add a per-file toggle button that expands all collapsed sections at once and can revert to the original collapsed state. The expand mechanism uses direct JS fetch instead of htmx, replacing the hx-get/hx-target attributes on code expander buttons with data-url. Individual expand buttons are also handled via JS delegated click handlers. The expand-all uses direction=full to make the backend expand entire gaps in a single response instead of chunk-by-chunk, significantly reducing the number of network round-trips for large files. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
318cb85037
commit
faa7680555
@ -2562,6 +2562,8 @@
|
||||
"repo.diff.load": "Load Diff",
|
||||
"repo.diff.generated": "Generated",
|
||||
"repo.diff.vendored": "Vendored",
|
||||
"repo.diff.expand_all": "Expand all",
|
||||
"repo.diff.collapse_expanded": "Collapse expanded",
|
||||
"repo.diff.comment.add_line_comment": "Add line comment",
|
||||
"repo.diff.comment.placeholder": "Leave a comment",
|
||||
"repo.diff.comment.add_single_comment": "Add single comment",
|
||||
|
||||
@ -228,7 +228,7 @@ func (d *DiffLine) RenderBlobExcerptButtons(fileNameHash string, data *DiffBlobE
|
||||
link += fmt.Sprintf("&pull_issue_index=%d", data.PullIssueIndex)
|
||||
}
|
||||
return htmlutil.HTMLFormat(
|
||||
`<button class="code-expander-button" hx-target="closest tr" hx-get="%s" data-hidden-comment-ids=",%s,">%s</button>`,
|
||||
`<button class="code-expander-button" data-url="%s" data-hidden-comment-ids=",%s,">%s</button>`,
|
||||
link, dataHiddenCommentIDs, svg.RenderHTML(svgName),
|
||||
)
|
||||
}
|
||||
|
||||
@ -98,6 +98,9 @@
|
||||
</a>
|
||||
</div>
|
||||
<button class="btn interact-fg tw-p-2 tw-shrink-0" data-clipboard-text="{{$file.Name}}" data-tooltip-content="{{ctx.Locale.Tr "copy_path"}}">{{svg "octicon-copy" 14}}</button>
|
||||
{{if and $.DiffBlobExcerptData (not $file.IsSubmodule) (not $file.IsBin) (not $file.IsIncomplete)}}
|
||||
<button class="btn interact-fg tw-p-2 tw-shrink-0 diff-expand-all" data-tooltip-content="{{ctx.Locale.Tr "repo.diff.expand_all"}}" data-expand-tooltip="{{ctx.Locale.Tr "repo.diff.expand_all"}}" data-collapse-tooltip="{{ctx.Locale.Tr "repo.diff.collapse_expanded"}}">{{svg "octicon-unfold" 14}}</button>
|
||||
{{end}}
|
||||
{{if $file.IsLFSFile}}
|
||||
<span class="ui label">LFS</span>
|
||||
{{end}}
|
||||
|
||||
@ -147,12 +147,12 @@ func TestCompareCodeExpand(t *testing.T) {
|
||||
req := NewRequest(t, "GET", "/user1/test_blob_excerpt/compare/main...user2/test_blob_excerpt-fork:forked-branch")
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
els := htmlDoc.Find(`button.code-expander-button[hx-get]`)
|
||||
els := htmlDoc.Find(`button.code-expander-button[data-url]`)
|
||||
|
||||
// all the links in the comparison should be to the forked repo&branch
|
||||
assert.NotZero(t, els.Length())
|
||||
for i := 0; i < els.Length(); i++ {
|
||||
link := els.Eq(i).AttrOr("hx-get", "")
|
||||
link := els.Eq(i).AttrOr("data-url", "")
|
||||
assert.True(t, strings.HasPrefix(link, "/user2/test_blob_excerpt-fork/blob_excerpt/"))
|
||||
}
|
||||
})
|
||||
|
||||
@ -9,8 +9,9 @@ import {submitEventSubmitter, queryElemSiblings, hideElem, showElem, animateOnce
|
||||
import {POST, GET} from '../modules/fetch.ts';
|
||||
import {createTippy} from '../modules/tippy.ts';
|
||||
import {invertFileFolding} from './file-fold.ts';
|
||||
import {parseDom, sleep} from '../utils.ts';
|
||||
import {parseDom} from '../utils.ts';
|
||||
import {registerGlobalSelectorFunc} from '../modules/observer.ts';
|
||||
import {svg} from '../svg.ts';
|
||||
|
||||
function initRepoDiffFileBox(el: HTMLElement) {
|
||||
// switch between "rendered" and "source", for image and CSV files
|
||||
@ -24,6 +25,12 @@ function initRepoDiffFileBox(el: HTMLElement) {
|
||||
hideElem(queryElemSiblings(target));
|
||||
showElem(target);
|
||||
}));
|
||||
|
||||
// Hide "expand all" button when there are no expandable sections
|
||||
const expandAllBtn = el.querySelector('.diff-expand-all');
|
||||
if (expandAllBtn && !el.querySelector('.code-expander-button')) {
|
||||
hideElem(expandAllBtn);
|
||||
}
|
||||
}
|
||||
|
||||
function initRepoDiffConversationForm() {
|
||||
@ -247,12 +254,13 @@ async function onLocationHashChange() {
|
||||
const commentId = currentHash.substring(issueCommentPrefix.length);
|
||||
const expandButton = document.querySelector<HTMLElement>(`.code-expander-button[data-hidden-comment-ids*=",${commentId},"]`);
|
||||
if (expandButton) {
|
||||
// avoid infinite loop, do not re-click the button if already clicked
|
||||
// avoid infinite loop, do not re-expand the same button
|
||||
const attrAutoLoadClicked = 'data-auto-load-clicked';
|
||||
if (expandButton.hasAttribute(attrAutoLoadClicked)) return;
|
||||
expandButton.setAttribute(attrAutoLoadClicked, 'true');
|
||||
expandButton.click();
|
||||
await sleep(500); // Wait for HTMX to load the content. FIXME: need to drop htmx in the future
|
||||
const tr = expandButton.closest('tr');
|
||||
const url = expandButton.getAttribute('data-url');
|
||||
if (tr && url) await fetchBlobExcerpt(tr, url);
|
||||
continue; // Try again to find the element
|
||||
}
|
||||
}
|
||||
@ -274,6 +282,105 @@ function initRepoDiffHashChangeListener() {
|
||||
onLocationHashChange();
|
||||
}
|
||||
|
||||
const expandAllSavedState = new WeakMap<HTMLElement, HTMLElement>();
|
||||
|
||||
async function fetchBlobExcerpt(tr: Element, url: string): Promise<void> {
|
||||
const resp = await GET(url);
|
||||
const text = await resp.text();
|
||||
// Parse <tr> elements in proper table context
|
||||
const tempTbody = document.createElement('tbody');
|
||||
tempTbody.innerHTML = text;
|
||||
const nodes = Array.from(tempTbody.children);
|
||||
tr.replaceWith(...nodes);
|
||||
}
|
||||
|
||||
async function expandAllLines(btn: HTMLElement, fileBox: HTMLElement) {
|
||||
const fileBody = fileBox.querySelector('.diff-file-body');
|
||||
if (!fileBody) return;
|
||||
|
||||
// Save original state for later collapse
|
||||
const tbody = fileBody.querySelector('table.chroma tbody');
|
||||
if (!tbody) return;
|
||||
expandAllSavedState.set(fileBox, tbody.cloneNode(true) as HTMLElement);
|
||||
|
||||
btn.classList.add('disabled');
|
||||
try {
|
||||
// Loop: expand all collapsed sections until none remain.
|
||||
// Each round fetches all current expander URLs in parallel, replaces their
|
||||
// target <tr> rows with the response rows, then rescans for new expanders
|
||||
// that may have appeared in the inserted content.
|
||||
while (true) {
|
||||
const expanders = collectExpanderButtons(fileBody, true);
|
||||
if (expanders.length === 0) break;
|
||||
await Promise.all(expanders.map(({tr, url}) => fetchBlobExcerpt(tr, url)));
|
||||
}
|
||||
} finally {
|
||||
btn.classList.remove('disabled');
|
||||
}
|
||||
|
||||
// Update button to "collapse" state
|
||||
btn.innerHTML = svg('octicon-fold', 14);
|
||||
btn.setAttribute('data-tooltip-content', btn.getAttribute('data-collapse-tooltip') ?? '');
|
||||
}
|
||||
|
||||
function collapseExpandedLines(btn: HTMLElement, fileBox: HTMLElement) {
|
||||
const savedTbody = expandAllSavedState.get(fileBox);
|
||||
if (!savedTbody) return;
|
||||
|
||||
const tbody = fileBox.querySelector('.diff-file-body table.chroma tbody');
|
||||
if (tbody) {
|
||||
tbody.replaceWith(savedTbody.cloneNode(true));
|
||||
}
|
||||
|
||||
expandAllSavedState.delete(fileBox);
|
||||
|
||||
// Update button to "expand" state
|
||||
btn.innerHTML = svg('octicon-unfold', 14);
|
||||
btn.setAttribute('data-tooltip-content', btn.getAttribute('data-expand-tooltip') ?? '');
|
||||
}
|
||||
|
||||
// Collect one expander button per <tr> (skip duplicates from "updown" rows
|
||||
// that have both up/down buttons targeting the same row).
|
||||
// When fullExpand is true, the direction parameter is rewritten so the backend
|
||||
// expands the entire gap in a single response instead of chunk-by-chunk.
|
||||
function collectExpanderButtons(container: Element, fullExpand?: boolean): {tr: Element, url: string}[] {
|
||||
const seen = new Set<Element>();
|
||||
const expanders: {tr: Element, url: string}[] = [];
|
||||
for (const btn of container.querySelectorAll<HTMLElement>('.code-expander-button')) {
|
||||
const tr = btn.closest('tr');
|
||||
let url = btn.getAttribute('data-url');
|
||||
if (tr && url && !seen.has(tr)) {
|
||||
seen.add(tr);
|
||||
if (fullExpand) url = url.replace(/direction=[^&]*/, 'direction=full');
|
||||
expanders.push({tr, url});
|
||||
}
|
||||
}
|
||||
return expanders;
|
||||
}
|
||||
|
||||
function initDiffExpandAllLines() {
|
||||
addDelegatedEventListener(document, 'click', '.diff-expand-all', (btn, e) => {
|
||||
e.preventDefault();
|
||||
if (btn.classList.contains('disabled')) return;
|
||||
const fileBox = btn.closest<HTMLElement>('.diff-file-box');
|
||||
if (!fileBox) return;
|
||||
|
||||
if (expandAllSavedState.has(fileBox)) {
|
||||
collapseExpandedLines(btn, fileBox);
|
||||
} else {
|
||||
expandAllLines(btn, fileBox);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle individual expand button clicks
|
||||
addDelegatedEventListener(document, 'click', '.code-expander-button', (btn, e) => {
|
||||
e.preventDefault();
|
||||
const tr = btn.closest('tr');
|
||||
const url = btn.getAttribute('data-url');
|
||||
if (tr && url) fetchBlobExcerpt(tr, url);
|
||||
});
|
||||
}
|
||||
|
||||
export function initRepoDiffView() {
|
||||
initRepoDiffConversationForm(); // such form appears on the "conversation" page and "diff" page
|
||||
|
||||
@ -285,6 +392,7 @@ export function initRepoDiffView() {
|
||||
initDiffHeaderPopup();
|
||||
initViewedCheckboxListenerFor();
|
||||
initExpandAndCollapseFilesButton();
|
||||
initDiffExpandAllLines();
|
||||
initRepoDiffHashChangeListener();
|
||||
|
||||
registerGlobalSelectorFunc('#diff-file-boxes .diff-file-box', initRepoDiffFileBox);
|
||||
|
||||
@ -34,6 +34,7 @@ import octiconFileDirectoryOpenFill from '../../public/assets/img/svg/octicon-fi
|
||||
import octiconFileSubmodule from '../../public/assets/img/svg/octicon-file-submodule.svg';
|
||||
import octiconFileSymlinkFile from '../../public/assets/img/svg/octicon-file-symlink-file.svg';
|
||||
import octiconFilter from '../../public/assets/img/svg/octicon-filter.svg';
|
||||
import octiconFold from '../../public/assets/img/svg/octicon-fold.svg';
|
||||
import octiconGear from '../../public/assets/img/svg/octicon-gear.svg';
|
||||
import octiconGitBranch from '../../public/assets/img/svg/octicon-git-branch.svg';
|
||||
import octiconGitCommit from '../../public/assets/img/svg/octicon-git-commit.svg';
|
||||
@ -78,6 +79,7 @@ import octiconTable from '../../public/assets/img/svg/octicon-table.svg';
|
||||
import octiconTag from '../../public/assets/img/svg/octicon-tag.svg';
|
||||
import octiconTrash from '../../public/assets/img/svg/octicon-trash.svg';
|
||||
import octiconTriangleDown from '../../public/assets/img/svg/octicon-triangle-down.svg';
|
||||
import octiconUnfold from '../../public/assets/img/svg/octicon-unfold.svg';
|
||||
import octiconX from '../../public/assets/img/svg/octicon-x.svg';
|
||||
import octiconXCircleFill from '../../public/assets/img/svg/octicon-x-circle-fill.svg';
|
||||
import octiconZoomIn from '../../public/assets/img/svg/octicon-zoom-in.svg';
|
||||
@ -117,6 +119,7 @@ const svgs = {
|
||||
'octicon-file-submodule': octiconFileSubmodule,
|
||||
'octicon-file-symlink-file': octiconFileSymlinkFile,
|
||||
'octicon-filter': octiconFilter,
|
||||
'octicon-fold': octiconFold,
|
||||
'octicon-gear': octiconGear,
|
||||
'octicon-git-branch': octiconGitBranch,
|
||||
'octicon-git-commit': octiconGitCommit,
|
||||
@ -161,6 +164,7 @@ const svgs = {
|
||||
'octicon-tag': octiconTag,
|
||||
'octicon-trash': octiconTrash,
|
||||
'octicon-triangle-down': octiconTriangleDown,
|
||||
'octicon-unfold': octiconUnfold,
|
||||
'octicon-x': octiconX,
|
||||
'octicon-x-circle-fill': octiconXCircleFill,
|
||||
'octicon-zoom-in': octiconZoomIn,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user