diff options
Diffstat (limited to 'polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-legacy.ts')
-rw-r--r-- | polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-legacy.ts | 505 |
1 files changed, 505 insertions, 0 deletions
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-legacy.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-legacy.ts new file mode 100644 index 0000000000..ceadc94e45 --- /dev/null +++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-legacy.ts @@ -0,0 +1,505 @@ +/** + * @license + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the 'License'); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an 'AS IS' BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + MovedLinkClickedEventDetail, + RenderPreferences, +} from '../../../api/diff'; +import {fire} from '../../../utils/event-util'; +import {GrDiffLine, GrDiffLineType, LineNumber} from '../gr-diff/gr-diff-line'; +import {GrDiffGroup, GrDiffGroupType} from '../gr-diff/gr-diff-group'; +import '../gr-context-controls/gr-context-controls'; +import { + GrContextControls, + GrContextControlsShowConfig, +} from '../gr-context-controls/gr-context-controls'; +import {DiffInfo, DiffPreferencesInfo} from '../../../types/diff'; +import {DiffViewMode, Side} from '../../../constants/constants'; +import {DiffLayer} from '../../../types/types'; +import { + createBlameElement, + createElementDiff, + createElementDiffWithText, + formatText, + getResponsiveMode, +} from '../gr-diff/gr-diff-utils'; +import {GrDiffBuilder} from './gr-diff-builder'; +import {BlameInfo} from '../../../types/common'; + +function lineTdSelector(lineNumber: LineNumber, side?: Side): string { + const sideSelector = side ? `.${side}` : ''; + return `td.lineNum[data-value="${lineNumber}"]${sideSelector}`; +} +/** + * Base class for builders that are creating the DOM elements programmatically + * by calling `document.createElement()` and such. We are calling such builders + * "legacy", because we want to create (Lit) component based diff elements. + * + * TODO: Do not subclass `GrDiffBuilder`. Use composition and interfaces. + */ +export abstract class GrDiffBuilderLegacy extends GrDiffBuilder { + constructor( + diff: DiffInfo, + prefs: DiffPreferencesInfo, + outputEl: HTMLElement, + layers: DiffLayer[] = [], + renderPrefs?: RenderPreferences + ) { + super(diff, prefs, outputEl, layers, renderPrefs); + } + + override getContentTdByLine( + lineNumber: LineNumber, + side?: Side, + root: Element = this.outputEl + ): HTMLTableCellElement | null { + return root.querySelector<HTMLTableCellElement>( + `${lineTdSelector(lineNumber, side)} ~ td.content` + ); + } + + override getLineElByNumber( + lineNumber: LineNumber, + side?: Side + ): HTMLTableCellElement | null { + return this.outputEl.querySelector<HTMLTableCellElement>( + lineTdSelector(lineNumber, side) + ); + } + + override getLineNumberRows() { + return Array.from( + this.outputEl.querySelectorAll<HTMLTableRowElement>( + ':not(.contextControl) > .diff-row' + ) ?? [] + ).filter(tr => tr.querySelector('button')); + } + + override getLineNumEls(side: Side): HTMLTableCellElement[] { + return Array.from( + this.outputEl.querySelectorAll<HTMLTableCellElement>( + `td.lineNum.${side}` + ) ?? [] + ); + } + + override getBlameTdByLine(lineNum: number): Element | undefined { + return ( + this.outputEl.querySelector(`td.blame[data-line-number="${lineNum}"]`) ?? + undefined + ); + } + + override getContentByLine( + lineNumber: LineNumber, + side?: Side, + root?: HTMLElement + ): HTMLElement | null { + const td = this.getContentTdByLine(lineNumber, side, root); + return td ? td.querySelector('.contentText') : null; + } + + override renderContentByRange( + start: LineNumber, + end: LineNumber, + side: Side + ) { + const lines: GrDiffLine[] = []; + const elements: HTMLElement[] = []; + let line; + let el; + this.findLinesByRange(start, end, side, lines, elements); + for (let i = 0; i < lines.length; i++) { + line = lines[i]; + el = elements[i]; + if (!el || !el.parentElement) { + // Cannot re-render an element if it does not exist. This can happen + // if lines are collapsed and not visible on the page yet. + continue; + } + const lineNumberEl = this.getLineNumberEl(el, side); + el.parentElement.replaceChild( + this.createTextEl(lineNumberEl, line, side).firstChild!, + el + ); + } + } + + override renderBlameByRange(blame: BlameInfo, start: number, end: number) { + for (let i = start; i <= end; i++) { + // TODO(wyatta): this query is expensive, but, when traversing a + // range, the lines are consecutive, and given the previous blame + // cell, the next one can be reached cheaply. + const blameCell = this.getBlameTdByLine(i); + if (!blameCell) continue; + + // Remove the element's children (if any). + while (blameCell.hasChildNodes()) { + blameCell.removeChild(blameCell.lastChild!); + } + const blameEl = createBlameElement(i, blame); + if (blameEl) blameCell.appendChild(blameEl); + } + } + + /** + * Finds the line number element given the content element by walking up the + * DOM tree to the diff row and then querying for a .lineNum element on the + * requested side. + * + * TODO(brohlfs): Consolidate this with getLineEl... methods in html file. + */ + private getLineNumberEl( + content: HTMLElement, + side: Side + ): HTMLElement | null { + let row: HTMLElement | null = content; + while (row && !row.classList.contains('diff-row')) row = row.parentElement; + return row ? (row.querySelector('.lineNum.' + side) as HTMLElement) : null; + } + + /** + * Adds <tr> table rows to a <tbody> section for allowing the user to expand + * collapsed of lines. Called by subclasses. + */ + protected createContextControls( + section: HTMLElement, + group: GrDiffGroup, + viewMode: DiffViewMode + ) { + const leftStart = group.lineRange.left.start_line; + const leftEnd = group.lineRange.left.end_line; + const firstGroupIsSkipped = !!group.contextGroups[0].skip; + const lastGroupIsSkipped = + !!group.contextGroups[group.contextGroups.length - 1].skip; + + const containsWholeFile = this.numLinesLeft === leftEnd - leftStart + 1; + const showAbove = + (leftStart > 1 && !firstGroupIsSkipped) || containsWholeFile; + const showBelow = leftEnd < this.numLinesLeft && !lastGroupIsSkipped; + + if (showAbove) { + const paddingRow = this.createContextControlPaddingRow(viewMode); + paddingRow.classList.add('above'); + section.appendChild(paddingRow); + } + section.appendChild( + this.createContextControlRow(group, showAbove, showBelow, viewMode) + ); + if (showBelow) { + const paddingRow = this.createContextControlPaddingRow(viewMode); + paddingRow.classList.add('below'); + section.appendChild(paddingRow); + } + } + + /** + * Creates a context control <tr> table row for with buttons the allow the + * user to expand collapsed lines. Buttons extend from the gap created by this + * method up or down into the area of code that they affect. + */ + private createContextControlRow( + group: GrDiffGroup, + showAbove: boolean, + showBelow: boolean, + viewMode: DiffViewMode + ): HTMLElement { + const row = createElementDiff('tr', 'dividerRow'); + let showConfig: GrContextControlsShowConfig; + if (showAbove && !showBelow) { + showConfig = 'above'; + } else if (!showAbove && showBelow) { + showConfig = 'below'; + } else { + // Note that !showAbove && !showBelow also intentionally creates + // "show-both". This means the file is completely collapsed, which is + // unusual, but at least happens in one test. + showConfig = 'both'; + } + row.classList.add(`show-${showConfig}`); + + row.appendChild(this.createBlameCell(0)); + if (viewMode === DiffViewMode.SIDE_BY_SIDE) { + row.appendChild(createElementDiff('td')); + } + + const cell = createElementDiff('td', 'dividerCell'); + const colspan = this.renderPrefs?.show_sign_col ? '5' : '3'; + cell.setAttribute('colspan', colspan); + row.appendChild(cell); + + const contextControls = createElementDiff( + 'gr-context-controls' + ) as GrContextControls; + contextControls.diff = this._diff; + contextControls.renderPreferences = this.renderPrefs; + contextControls.group = group; + contextControls.showConfig = showConfig; + cell.appendChild(contextControls); + return row; + } + + /** + * Creates a table row to serve as padding between code and context controls. + * Blame column, line gutters, and content area will continue visually, but + * context controls can render over this background to map more clearly to + * the area of code they expand. + */ + private createContextControlPaddingRow(viewMode: DiffViewMode) { + const row = createElementDiff('tr', 'contextBackground'); + + if (viewMode === DiffViewMode.SIDE_BY_SIDE) { + row.classList.add('side-by-side'); + row.setAttribute('left-type', GrDiffGroupType.CONTEXT_CONTROL); + row.setAttribute('right-type', GrDiffGroupType.CONTEXT_CONTROL); + } else { + row.classList.add('unified'); + } + + row.appendChild(this.createBlameCell(0)); + row.appendChild(createElementDiff('td', 'contextLineNum')); + if (viewMode === DiffViewMode.SIDE_BY_SIDE) { + row.appendChild(createElementDiff('td', 'sign')); + row.appendChild(createElementDiff('td')); + } + row.appendChild(createElementDiff('td', 'contextLineNum')); + if (viewMode === DiffViewMode.SIDE_BY_SIDE) { + row.appendChild(createElementDiff('td', 'sign')); + } + row.appendChild(createElementDiff('td')); + + return row; + } + + protected createLineEl( + line: GrDiffLine, + number: LineNumber, + type: GrDiffLineType, + side: Side + ) { + const td = createElementDiff('td'); + td.classList.add(side); + if (line.type === GrDiffLineType.BLANK) { + td.classList.add('blankLineNum'); + return td; + } + if (line.type === GrDiffLineType.BOTH || line.type === type) { + td.classList.add('lineNum'); + td.dataset['value'] = number.toString(); + + if ( + ((this._prefs.show_file_comment_button === false || + this.renderPrefs?.show_file_comment_button === false) && + number === 'FILE') || + number === 'LOST' + ) { + return td; + } + + const button = createElementDiff('button'); + td.appendChild(button); + button.tabIndex = -1; + button.classList.add('lineNumButton'); + button.classList.add(side); + button.dataset['value'] = number.toString(); + button.textContent = number === 'FILE' ? 'File' : number.toString(); + if (number === 'FILE') { + button.setAttribute('aria-label', 'Add file comment'); + } + + // Add aria-labels for valid line numbers. + // For unified diff, this method will be called with number set to 0 for + // the empty line number column for added/removed lines. This should not + // be announced to the screenreader. + if (number > 0) { + if (line.type === GrDiffLineType.REMOVE) { + button.setAttribute('aria-label', `${number} removed`); + } else if (line.type === GrDiffLineType.ADD) { + button.setAttribute('aria-label', `${number} added`); + } + } + this.addLineNumberMouseEvents(td, number, side); + } + return td; + } + + private addLineNumberMouseEvents( + el: HTMLElement, + number: LineNumber, + side: Side + ) { + el.addEventListener('mouseenter', () => { + fire(el, 'line-mouse-enter', {lineNum: number, side}); + }); + el.addEventListener('mouseleave', () => { + fire(el, 'line-mouse-leave', {lineNum: number, side}); + }); + } + + protected createTextEl( + lineNumberEl: HTMLElement | null, + line: GrDiffLine, + side?: Side + ) { + const td = createElementDiff('td'); + if (line.type !== GrDiffLineType.BLANK) { + td.classList.add('content'); + } + if (side) { + td.classList.add(side); + } + + // If intraline info is not available, the entire line will be + // considered as changed and marked as dark red / green color + if (!line.hasIntralineInfo) { + td.classList.add('no-intraline-info'); + } + td.classList.add(line.type); + + const {beforeNumber, afterNumber} = line; + if (beforeNumber !== 'FILE' && beforeNumber !== 'LOST') { + const responsiveMode = getResponsiveMode(this._prefs, this.renderPrefs); + const contentText = formatText( + line.text, + responsiveMode, + this._prefs.tab_size, + this._prefs.line_length + ); + + if (side) { + contentText.setAttribute('data-side', side); + const number = side === Side.LEFT ? beforeNumber : afterNumber; + this.addLineNumberMouseEvents(td, number, side); + } + + if (lineNumberEl && side) { + for (const layer of this.layers) { + if (typeof layer.annotate === 'function') { + layer.annotate(contentText, lineNumberEl, line, side); + } + } + } else { + console.error('lineNumberEl or side not set, skipping layer.annotate'); + } + + td.appendChild(contentText); + } else if (line.beforeNumber === 'FILE') td.classList.add('file'); + else if (line.beforeNumber === 'LOST') td.classList.add('lost'); + + return td; + } + + private createMovedLineAnchor(line: number, side: Side) { + const anchor = createElementDiffWithText('a', `${line}`); + + // href is not actually used but important for Screen Readers + anchor.setAttribute('href', `#${line}`); + anchor.addEventListener('click', e => { + e.preventDefault(); + anchor.dispatchEvent( + new CustomEvent<MovedLinkClickedEventDetail>('moved-link-clicked', { + detail: { + lineNum: line, + side, + }, + composed: true, + bubbles: true, + }) + ); + }); + return anchor; + } + + private createMoveDescriptionDiv(movedIn: boolean, group: GrDiffGroup) { + const div = createElementDiff('div'); + if (group.moveDetails?.range) { + const {changed, range} = group.moveDetails; + const otherSide = movedIn ? Side.LEFT : Side.RIGHT; + const andChangedLabel = changed ? 'and changed ' : ''; + const direction = movedIn ? 'from' : 'to'; + const textLabel = `Moved ${andChangedLabel}${direction} lines `; + div.appendChild(createElementDiffWithText('span', textLabel)); + div.appendChild(this.createMovedLineAnchor(range.start, otherSide)); + div.appendChild(createElementDiffWithText('span', ' - ')); + div.appendChild(this.createMovedLineAnchor(range.end, otherSide)); + } else { + div.appendChild( + createElementDiffWithText('span', movedIn ? 'Moved in' : 'Moved out') + ); + } + return div; + } + + protected buildMoveControls(group: GrDiffGroup) { + const movedIn = group.adds.length > 0; + const { + numberOfCells, + movedOutIndex, + movedInIndex, + lineNumberCols, + signCols, + } = this.getMoveControlsConfig(); + + let controlsClass; + let descriptionIndex; + const descriptionTextDiv = this.createMoveDescriptionDiv(movedIn, group); + if (movedIn) { + controlsClass = 'movedIn'; + descriptionIndex = movedInIndex; + } else { + controlsClass = 'movedOut'; + descriptionIndex = movedOutIndex; + } + + const controls = createElementDiff('tr', `moveControls ${controlsClass}`); + const cells = [...Array(numberOfCells).keys()].map(() => + createElementDiff('td') + ); + lineNumberCols.forEach(index => { + cells[index].classList.add('moveControlsLineNumCol'); + }); + + if (signCols) { + cells[signCols.left].classList.add('sign', 'left'); + cells[signCols.right].classList.add('sign', 'right'); + } + const moveRangeHeader = createElementDiff('gr-range-header'); + moveRangeHeader.setAttribute('icon', 'gr-icons:move-item'); + moveRangeHeader.appendChild(descriptionTextDiv); + cells[descriptionIndex].classList.add('moveHeader'); + cells[descriptionIndex].appendChild(moveRangeHeader); + cells.forEach(c => { + controls.appendChild(c); + }); + return controls; + } + + /** + * Create a blame cell for the given base line. Blame information will be + * included in the cell if available. + */ + protected createBlameCell(lineNumber: LineNumber): HTMLTableCellElement { + const blameTd = createElementDiff('td', 'blame') as HTMLTableCellElement; + blameTd.setAttribute('data-line-number', lineNumber.toString()); + if (!lineNumber) return blameTd; + + const blameInfo = this.getBlameCommitForBaseLine(lineNumber); + if (!blameInfo) return blameTd; + + blameTd.appendChild(createBlameElement(lineNumber, blameInfo)); + return blameTd; + } +} |