Merge changes I23e4f19f,I2eb64e18,Ia8f1ca47,I9090d834 into stable-3.6
* changes:
Migrate gr-diff-highlight from PolymerElement to plain class
Simplify and clean up gr-diff-highlight
Migrate gr-diff-highlight_test from js to ts
Migrate gr-diff-selection from PolymerElement to plain class
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-cursor/gr-diff-cursor_test.js b/polygerrit-ui/app/embed/diff/gr-diff-cursor/gr-diff-cursor_test.js
index 9b093af..c64f484 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-cursor/gr-diff-cursor_test.js
+++ b/polygerrit-ui/app/embed/diff/gr-diff-cursor/gr-diff-cursor_test.js
@@ -548,7 +548,7 @@
end_line: 6,
end_character: 1,
};
- diffElement.$.highlights.selectedRange = {
+ diffElement.highlights.selectedRange = {
side: 'right',
range: someRange,
};
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-diff-highlight.ts b/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-diff-highlight.ts
index 2ee6c9f..0714645 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-diff-highlight.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-diff-highlight.ts
@@ -1,43 +1,25 @@
/**
* @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.
+ * Copyright 2016 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
*/
import '../../../styles/shared-styles';
import '../gr-selection-action-box/gr-selection-action-box';
-import {dom, EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-diff-highlight_html';
import {GrAnnotation} from './gr-annotation';
import {normalize} from './gr-range-normalizer';
import {strToClassName} from '../../../utils/dom-util';
-import {customElement, property} from '@polymer/decorators';
import {Side} from '../../../constants/constants';
import {CommentRange} from '../../../types/common';
import {GrSelectionActionBox} from '../gr-selection-action-box/gr-selection-action-box';
-import {GrDiffBuilderElement} from '../gr-diff-builder/gr-diff-builder-element';
import {FILE} from '../gr-diff/gr-diff-line';
import {
getLineElByChild,
getLineNumberByChild,
- getRange,
- getSide,
getSideByLineEl,
GrDiffThreadElement,
} from '../gr-diff/gr-diff-utils';
import {debounce, DelayedTask} from '../../../utils/async-util';
-import {queryAndAssert} from '../../../utils/common-util';
+import {assertIsDefined, queryAndAssert} from '../../../utils/common-util';
interface SidedRange {
side: Side;
@@ -56,51 +38,65 @@
end: NormalizedPosition | null;
}
-@customElement('gr-diff-highlight')
-export class GrDiffHighlight extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
+/**
+ * The methods that we actually want to call on the builder. We don't want a
+ * fully blown dependency on GrDiffBuilderElement.
+ */
+export interface DiffBuilderInterface {
+ getContentTdByLineEl(lineEl?: Element): Element | null;
+}
- @property({type: Array, notify: true})
- commentRanges: SidedRange[] = [];
-
- @property({type: Boolean})
- loggedIn?: boolean;
-
- @property({type: Object})
- _cachedDiffBuilder?: GrDiffBuilderElement;
-
- @property({type: Object, notify: true})
+/**
+ * Handles showing, positioning and interacting with <gr-selection-action-box>.
+ *
+ * Toggles a css class for highlighting comment ranges when the mouse leaves or
+ * enters a comment thread element.
+ */
+export class GrDiffHighlight {
selectedRange?: SidedRange;
+ private diffBuilder?: DiffBuilderInterface;
+
+ private diffTable?: HTMLElement;
+
private selectionChangeTask?: DelayedTask;
- constructor() {
- super();
- this.addEventListener('comment-thread-mouseleave', e =>
- this._handleCommentThreadMouseleave(e)
+ init(diffTable: HTMLElement, diffBuilder: DiffBuilderInterface) {
+ this.cleanup();
+
+ this.diffTable = diffTable;
+ this.diffBuilder = diffBuilder;
+
+ diffTable.addEventListener(
+ 'comment-thread-mouseleave',
+ this.handleCommentThreadMouseleave
);
- this.addEventListener('comment-thread-mouseenter', e =>
- this._handleCommentThreadMouseenter(e)
+ diffTable.addEventListener(
+ 'comment-thread-mouseenter',
+ this.handleCommentThreadMouseenter
);
- this.addEventListener('create-comment-requested', e =>
- this._handleRangeCommentRequest(e)
+ diffTable.addEventListener(
+ 'create-comment-requested',
+ this.handleRangeCommentRequest
);
}
- override disconnectedCallback() {
+ cleanup() {
this.selectionChangeTask?.cancel();
- super.disconnectedCallback();
- }
-
- get diffBuilder() {
- if (!this._cachedDiffBuilder) {
- this._cachedDiffBuilder = this.querySelector(
- 'gr-diff-builder'
- ) as GrDiffBuilderElement;
+ if (this.diffTable) {
+ this.diffTable.removeEventListener(
+ 'comment-thread-mouseleave',
+ this.handleCommentThreadMouseleave
+ );
+ this.diffTable.removeEventListener(
+ 'comment-thread-mouseenter',
+ this.handleCommentThreadMouseenter
+ );
+ this.diffTable.removeEventListener(
+ 'create-comment-requested',
+ this.handleRangeCommentRequest
+ );
}
- return this._cachedDiffBuilder;
}
/**
@@ -129,18 +125,17 @@
// removed.
// If you wait longer than 50 ms, then you don't properly catch a very
// quick 'c' press after the selection change. If you wait less than 10
- // ms, then you will have about 50 _handleSelection calls when doing a
+ // ms, then you will have about 50 handleSelection() calls when doing a
// simple drag for select.
this.selectionChangeTask = debounce(
this.selectionChangeTask,
- () => this._handleSelection(selection, isMouseUp),
+ () => this.handleSelection(selection, isMouseUp),
10
);
}
- _getThreadEl(e: Event): GrDiffThreadElement | null {
- const path = (dom(e) as EventApi).path || [];
- for (const pathEl of path) {
+ private getThreadEl(e: Event): GrDiffThreadElement | null {
+ for (const pathEl of e.composedPath()) {
if (
pathEl instanceof HTMLElement &&
pathEl.classList.contains('comment-thread')
@@ -151,130 +146,74 @@
return null;
}
- _toggleRangeElHighlight(
- threadEl: GrDiffThreadElement,
+ private toggleRangeElHighlight(
+ threadEl: GrDiffThreadElement | null,
highlightRange = false
) {
- // We don't want to re-create the line just for highlighting the range which
- // is creating annoying bugs: @see Issue 12934
- // As gr-ranged-comment-layer now does not notify the layer re-render and
- // lack of access to the thread or the lineEl from the ranged-comment-layer,
- // need to update range class for styles here.
- let curNode: HTMLElement | null = threadEl.assignedSlot;
- while (curNode) {
- if (curNode.nodeName === 'TABLE') break;
- curNode = curNode.parentElement;
- }
- if (curNode?.querySelectorAll) {
- if (highlightRange) {
- const rangeNodes = curNode.querySelectorAll(
- `.range.${strToClassName(threadEl.rootId)}`
- );
- rangeNodes.forEach(rangeNode => {
- rangeNode.classList.add('rangeHoverHighlight');
- });
- const hintNode = threadEl.parentElement?.querySelector(
- `gr-ranged-comment-hint[threadElRootId="${threadEl.rootId}"]`
- );
- if (hintNode) {
- hintNode.shadowRoot
- ?.querySelectorAll('.rangeHighlight')
- .forEach(highlightNode =>
- highlightNode.classList.add('rangeHoverHighlight')
- );
- }
- } else {
- const rangeNodes = curNode.querySelectorAll(
- `.rangeHoverHighlight.${strToClassName(threadEl.rootId)}`
- );
- rangeNodes.forEach(rangeNode => {
- rangeNode.classList.remove('rangeHoverHighlight');
- });
- const hintNode = threadEl.parentElement?.querySelector(
- `gr-ranged-comment-hint[threadElRootId="${threadEl.rootId}"]`
- );
- if (hintNode) {
- hintNode.shadowRoot
- ?.querySelectorAll('.rangeHoverHighlight')
- .forEach(highlightNode =>
- highlightNode.classList.remove('rangeHoverHighlight')
- );
- }
- }
- }
- }
-
- _handleCommentThreadMouseenter(e: Event) {
- const threadEl = this._getThreadEl(e)!;
- const index = this._indexForThreadEl(threadEl);
-
- if (index !== undefined) {
- this.set(['commentRanges', index, 'hovering'], true);
- }
-
- this._toggleRangeElHighlight(threadEl, /* highlightRange= */ true);
- }
-
- _handleCommentThreadMouseleave(e: Event) {
- const threadEl = this._getThreadEl(e)!;
- const index = this._indexForThreadEl(threadEl);
-
- if (index !== undefined) {
- this.set(['commentRanges', index, 'hovering'], false);
- }
-
- this._toggleRangeElHighlight(threadEl, /* highlightRange= */ false);
- }
-
- _indexForThreadEl(threadEl: HTMLElement) {
- const side = getSide(threadEl);
- const range = getRange(threadEl);
- if (!side || !range) return undefined;
- return this._indexOfCommentRange(side, range);
- }
-
- _indexOfCommentRange(side: Side, range: CommentRange) {
- function rangesEqual(a: CommentRange, b: CommentRange) {
- if (!a && !b) {
- return true;
- }
- if (!a || !b) {
- return false;
- }
- return (
- a.start_line === b.start_line &&
- a.start_character === b.start_character &&
- a.end_line === b.end_line &&
- a.end_character === b.end_character
+ const rootId = threadEl?.rootId;
+ if (!rootId) return;
+ if (!this.diffTable) return;
+ if (highlightRange) {
+ const selector = `.range.${strToClassName(rootId)}`;
+ const rangeNodes = this.diffTable.querySelectorAll(selector);
+ rangeNodes.forEach(rangeNode => {
+ rangeNode.classList.add('rangeHoverHighlight');
+ });
+ const hintNode = this.diffTable.querySelector(
+ `gr-ranged-comment-hint[threadElRootId="${rootId}"]`
);
+ hintNode?.shadowRoot
+ ?.querySelectorAll('.rangeHighlight')
+ .forEach(highlightNode =>
+ highlightNode.classList.add('rangeHoverHighlight')
+ );
+ } else {
+ const selector = `.rangeHoverHighlight.${strToClassName(rootId)}`;
+ const rangeNodes = this.diffTable.querySelectorAll(selector);
+ rangeNodes.forEach(rangeNode => {
+ rangeNode.classList.remove('rangeHoverHighlight');
+ });
+ const hintNode = this.diffTable.querySelector(
+ `gr-ranged-comment-hint[threadElRootId="${rootId}"]`
+ );
+ hintNode?.shadowRoot
+ ?.querySelectorAll('.rangeHoverHighlight')
+ .forEach(highlightNode =>
+ highlightNode.classList.remove('rangeHoverHighlight')
+ );
}
-
- return this.commentRanges.findIndex(
- commentRange =>
- commentRange.side === side && rangesEqual(commentRange.range, range)
- );
}
+ private handleCommentThreadMouseenter = (e: Event) => {
+ const threadEl = this.getThreadEl(e);
+ this.toggleRangeElHighlight(threadEl, /* highlightRange= */ true);
+ };
+
+ private handleCommentThreadMouseleave = (e: Event) => {
+ const threadEl = this.getThreadEl(e);
+ this.toggleRangeElHighlight(threadEl, /* highlightRange= */ false);
+ };
+
/**
* Get current normalized selection.
* Merges multiple ranges, accounts for triple click, accounts for
* syntax highligh, convert native DOM Range objects to Gerrit concepts
* (line, side, etc).
*/
- _getNormalizedRange(selection: Selection | Range) {
+ private getNormalizedRange(selection: Selection | Range) {
/* On Safari the ShadowRoot.getSelection() isn't there and the only thing
we can get is a single Range */
if (selection instanceof Range) {
- return this._normalizeRange(selection);
+ return this.normalizeRange(selection);
}
const rangeCount = selection.rangeCount;
if (rangeCount === 0) {
return null;
} else if (rangeCount === 1) {
- return this._normalizeRange(selection.getRangeAt(0));
+ return this.normalizeRange(selection.getRangeAt(0));
} else {
- const startRange = this._normalizeRange(selection.getRangeAt(0));
- const endRange = this._normalizeRange(
+ const startRange = this.normalizeRange(selection.getRangeAt(0));
+ const endRange = this.normalizeRange(
selection.getRangeAt(rangeCount - 1)
);
return {
@@ -289,15 +228,15 @@
*
* @return fixed normalized range
*/
- _normalizeRange(domRange: Range): NormalizedRange {
+ private normalizeRange(domRange: Range): NormalizedRange {
const range = normalize(domRange);
- return this._fixTripleClickSelection(
+ return this.fixTripleClickSelection(
{
- start: this._normalizeSelectionSide(
+ start: this.normalizeSelectionSide(
range.startContainer,
range.startOffset
),
- end: this._normalizeSelectionSide(range.endContainer, range.endOffset),
+ end: this.normalizeSelectionSide(range.endContainer, range.endOffset),
},
domRange
);
@@ -313,7 +252,7 @@
* @param domRange DOM Range object
* @return fixed normalized range
*/
- _fixTripleClickSelection(range: NormalizedRange, domRange: Range) {
+ private fixTripleClickSelection(range: NormalizedRange, domRange: Range) {
if (!range.start) {
// Selection outside of current diff.
return range;
@@ -334,7 +273,7 @@
end.column === 0 &&
end.line === start.line + 1;
const content = domRange.cloneContents().querySelector('.contentText');
- const lineLength = (content && this._getLength(content)) || 0;
+ const lineLength = (content && this.getLength(content)) || 0;
if (lineLength && (endsAtBeginningOfNextLine || endsAtOtherEmptySide)) {
// Move the selection to the end of the previous line.
range.end = {
@@ -355,12 +294,14 @@
* @param node td.content child
* @param offset offset within node
*/
- _normalizeSelectionSide(
+ private normalizeSelectionSide(
node: Node | null,
offset: number
): NormalizedPosition | null {
let column;
- if (!node || !this.contains(node)) return null;
+ if (!this.diffTable) return null;
+ if (!this.diffBuilder) return null;
+ if (!node || !this.diffTable.contains(node)) return null;
const lineEl = getLineElByChild(node);
if (!lineEl) return null;
const side = getSideByLineEl(lineEl);
@@ -376,10 +317,10 @@
} else {
const thread = contentTd.querySelector('.comment-thread');
if (thread?.contains(node)) {
- column = this._getLength(contentText);
+ column = this.getLength(contentText);
node = contentText;
} else {
- column = this._convertOffsetToColumn(node, offset);
+ column = this.convertOffsetToColumn(node, offset);
}
}
@@ -398,7 +339,8 @@
* collapsed section, so don't need to worry about this case for
* positioning the tooltip.
*/
- _positionActionBox(
+ // visible for testing
+ positionActionBox(
actionBox: GrSelectionActionBox,
startLine: number,
range: Text | Element | Range
@@ -412,7 +354,7 @@
actionBox.placeBelow(range);
}
- _isRangeValid(range: NormalizedRange | null) {
+ private isRangeValid(range: NormalizedRange | null) {
if (!range || !range.start || !range.start.node || !range.end) {
return false;
}
@@ -425,15 +367,16 @@
);
}
- _handleSelection(selection: Selection | Range, isMouseUp: boolean) {
+ // visible for testing
+ handleSelection(selection: Selection | Range, isMouseUp: boolean) {
/* On Safari, the selection events may return a null range that should
be ignored */
- if (!selection) {
- return;
- }
- const normalizedRange = this._getNormalizedRange(selection);
- if (!this._isRangeValid(normalizedRange)) {
- this._removeActionBox();
+ if (!selection) return;
+ if (!this.diffTable) return;
+
+ const normalizedRange = this.getNormalizedRange(selection);
+ if (!this.isRangeValid(normalizedRange)) {
+ this.removeActionBox();
return;
}
/* On Safari the ShadowRoot.getSelection() isn't there and the only thing
@@ -463,8 +406,8 @@
// start.column with the content length), we just check if the selection
// is empty to see that it's at the end of a line.
const content = domRange.cloneContents().querySelector('.contentText');
- if (isMouseUp && this._getLength(content) === 0) {
- this._fireCreateRangeComment(start.side, {
+ if (isMouseUp && this.getLength(content) === 0) {
+ this.fireCreateRangeComment(start.side, {
start_line: start.line,
start_character: 0,
end_line: start.line,
@@ -474,10 +417,10 @@
return;
}
- let actionBox = this.shadowRoot!.querySelector('gr-selection-action-box');
+ let actionBox = this.diffTable.querySelector('gr-selection-action-box');
if (!actionBox) {
actionBox = document.createElement('gr-selection-action-box');
- this.root!.insertBefore(actionBox, this.root!.firstElementChild);
+ this.diffTable.appendChild(actionBox);
}
this.selectedRange = {
range: {
@@ -489,10 +432,10 @@
side: start.side,
};
if (start.line === end.line) {
- this._positionActionBox(actionBox, start.line, domRange);
+ this.positionActionBox(actionBox, start.line, domRange);
} else if (start.node instanceof Text) {
if (start.column) {
- this._positionActionBox(
+ this.positionActionBox(
actionBox,
start.line,
start.node.splitText(start.column)
@@ -505,44 +448,41 @@
(start.node.firstChild instanceof Element ||
start.node.firstChild instanceof Text)
) {
- this._positionActionBox(actionBox, start.line, start.node.firstChild);
+ this.positionActionBox(actionBox, start.line, start.node.firstChild);
} else if (start.node instanceof Element || start.node instanceof Text) {
- this._positionActionBox(actionBox, start.line, start.node);
+ this.positionActionBox(actionBox, start.line, start.node);
} else {
console.warn('Failed to position comment action box.');
- this._removeActionBox();
+ this.removeActionBox();
}
}
- _fireCreateRangeComment(side: Side, range: CommentRange) {
- this.dispatchEvent(
+ private fireCreateRangeComment(side: Side, range: CommentRange) {
+ this.diffTable?.dispatchEvent(
new CustomEvent('create-range-comment', {
detail: {side, range},
composed: true,
bubbles: true,
})
);
- this._removeActionBox();
+ this.removeActionBox();
}
- _handleRangeCommentRequest(e: Event) {
+ private handleRangeCommentRequest = (e: Event) => {
e.stopPropagation();
- if (!this.selectedRange) {
- throw Error('Selected Range is needed for new range comment!');
- }
+ assertIsDefined(this.selectedRange, 'selectedRange');
const {side, range} = this.selectedRange;
- this._fireCreateRangeComment(side, range);
- }
+ this.fireCreateRangeComment(side, range);
+ };
- _removeActionBox() {
+ // visible for testing
+ removeActionBox() {
this.selectedRange = undefined;
- const actionBox = this.shadowRoot!.querySelector('gr-selection-action-box');
- if (actionBox) {
- this.root!.removeChild(actionBox);
- }
+ const actionBox = this.diffTable?.querySelector('gr-selection-action-box');
+ if (actionBox) actionBox.remove();
}
- _convertOffsetToColumn(el: Node, offset: number) {
+ private convertOffsetToColumn(el: Node, offset: number) {
if (el instanceof Element && el.classList.contains('content')) {
return offset;
}
@@ -552,7 +492,7 @@
) {
if (el.previousSibling) {
el = el.previousSibling;
- offset += this._getLength(el);
+ offset += this.getLength(el);
} else {
el = el.parentElement!;
}
@@ -566,18 +506,24 @@
*
* @param node this is sometimes passed as null.
*/
- _getLength(node: Node | null): number {
+ // visible for testing
+ getLength(node: Node | null): number {
if (node === null) return 0;
if (node instanceof Element && node.classList.contains('content')) {
- return this._getLength(queryAndAssert(node, '.contentText'));
+ return this.getLength(queryAndAssert(node, '.contentText'));
} else {
return GrAnnotation.getLength(node);
}
}
}
+export interface CreateRangeCommentEventDetail {
+ side: Side;
+ range: CommentRange;
+}
+
declare global {
- interface HTMLElementTagNameMap {
- 'gr-diff-highlight': GrDiffHighlight;
+ interface HTMLElementEventMap {
+ 'create-range-comment': CustomEvent<CreateRangeCommentEventDetail>;
}
}
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-diff-highlight_html.ts b/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-diff-highlight_html.ts
deleted file mode 100644
index 5a6cb1c..0000000
--- a/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-diff-highlight_html.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 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 {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="shared-styles">
- :host {
- position: relative;
- }
- gr-selection-action-box {
- /**
- * Needs z-index to appear above wrapped content, since it's inserted
- * into DOM before it.
- */
- z-index: 10;
- }
- </style>
- <div class="contentWrapper">
- <slot></slot>
- </div>
-`;
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-diff-highlight_test.js b/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-diff-highlight_test.js
deleted file mode 100644
index 4c1295f..0000000
--- a/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-diff-highlight_test.js
+++ /dev/null
@@ -1,589 +0,0 @@
-/**
- * @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 '../../../test/common-test-setup-karma.js';
-import './gr-diff-highlight.js';
-import {_getTextOffset} from './gr-range-normalizer.js';
-import {html} from '@polymer/polymer/lib/utils/html-tag.js';
-
-// Splitting long lines in html into shorter rows breaks tests:
-// zero-length text nodes and new lines are not expected in some places
-/* eslint-disable max-len */
-const basicFixture = fixtureFromTemplate(html`
-<style>
- .tab-indicator:before {
- color: #C62828;
- /* >> character */
- content: '\\00BB';
- }
- </style>
- <gr-diff-highlight>
- <table id="diffTable">
-
- <tbody class="section both">
- <tr class="diff-row side-by-side" left-type="both" right-type="both">
- <td class="left lineNum" data-value="1"></td>
- <td class="content both"><div class="contentText">[1] Nam cum ad me in Cumanum salutandi causa uterque venisset,</div></td>
- <td class="right lineNum" data-value="1"></td>
- <td class="content both"><div class="contentText">[1] Nam cum ad me in Cumanum salutandi causa uterque</div></td>
- </tr>
- </tbody>
-
- <tbody class="section delta">
- <tr class="diff-row side-by-side" left-type="remove" right-type="add">
- <td class="left lineNum" data-value="2"></td>
- <!-- Next tag is formatted to eliminate zero-length text nodes. -->
- <td class="content remove"><div class="contentText">na💢ti <hl class="foo">te, inquit</hl>, sumus <hl class="bar">aliquando</hl> otiosum, <hl>certe</hl> a <hl><span class="tab-indicator" style="tab-size:8;">\u0009</span></hl>udiam, <hl>quid</hl> sit, <span class="tab-indicator" style="tab-size:8;">\u0009</span>quod <hl>Epicurum</hl></div></td>
- <td class="right lineNum" data-value="2"></td>
- <!-- Next tag is formatted to eliminate zero-length text nodes. -->
- <td class="content add"><div class="contentText">nacti , <hl>,</hl> sumus <hl><span class="tab-indicator" style="tab-size:8;">\u0009</span></hl> otiosum, <span class="tab-indicator" style="tab-size:8;">\u0009</span> audiam, sit, quod</div></td>
- </tr>
- </tbody>
-
-<tbody class="section both">
- <tr class="diff-row side-by-side" left-type="both" right-type="both">
- <td class="left lineNum" data-value="138"></td>
- <td class="content both"><div class="contentText">[14] Nam cum ad me in Cumanum salutandi causa uterque venisset,</div></td>
- <td class="right lineNum" data-value="119"></td>
- <td class="content both"><div class="contentText">[14] Nam cum ad me in Cumanum salutandi causa uterque venisset,</div></td>
- </tr>
- </tbody>
-
- <tbody class="section delta">
- <tr class="diff-row side-by-side" left-type="remove" right-type="add">
- <td class="left lineNum" data-value="140"></td>
- <!-- Next tag is formatted to eliminate zero-length text nodes. -->
- <td class="content remove"><div class="contentText">na💢ti <hl class="foo">te, inquit</hl>, sumus <hl class="bar">aliquando</hl> otiosum, <hl>certe</hl> a <hl><span class="tab-indicator" style="tab-size:8;">\u0009</span></hl>udiam, <hl>quid</hl> sit, <span class="tab-indicator" style="tab-size:8;">\u0009</span>quod <hl>Epicurum</hl></div><div class="comment-thread">
- [Yet another random diff thread content here]
- </div></td>
- <td class="right lineNum" data-value="120"></td>
- <!-- Next tag is formatted to eliminate zero-length text nodes. -->
- <td class="content add"><div class="contentText">nacti , <hl>,</hl> sumus <hl><span class="tab-indicator" style="tab-size:8;">\u0009</span></hl> otiosum, <span class="tab-indicator" style="tab-size:8;">\u0009</span> audiam, sit, quod</div></td>
- </tr>
- </tbody>
-
- <tbody class="section both">
- <tr class="diff-row side-by-side" left-type="both" right-type="both">
- <td class="left lineNum" data-value="141"></td>
- <td class="content both"><div class="contentText">nam et<hl><span class="tab-indicator" style="tab-size:8;">\u0009</span></hl>complectitur<span class="tab-indicator" style="tab-size:8;">\u0009</span>verbis, quod vult, et dicit plane, quod intellegam;</div></td>
- <td class="right lineNum" data-value="130"></td>
- <td class="content both"><div class="contentText">nam et complectitur verbis, quod vult, et dicit plane, quod intellegam;</div></td>
- </tr>
- </tbody>
-
- <tbody class="section contextControl">
- <tr class="diff-row side-by-side" left-type="contextControl" right-type="contextControl">
- <td class="left contextLineNum"></td>
- <td>
- <gr-button>+10↑</gr-button>
- -
- <gr-button>Show 21 common lines</gr-button>
- -
- <gr-button>+10↓</gr-button>
- </td>
- <td class="right contextLineNum"></td>
- <td>
- <gr-button>+10↑</gr-button>
- -
- <gr-button>Show 21 common lines</gr-button>
- -
- <gr-button>+10↓</gr-button>
- </td>
- </tr>
- </tbody>
-
- <tbody class="section delta total">
- <tr class="diff-row side-by-side" left-type="blank" right-type="add">
- <td class="left"></td>
- <td class="blank"></td>
- <td class="right lineNum" data-value="146"></td>
- <td class="content add"><div class="contentText">[17] Quid igitur est? inquit; audire enim cupio, quid non probes. Principio, inquam,</div></td>
- </tr>
- </tbody>
-
- <tbody class="section both">
- <tr class="diff-row side-by-side" left-type="both" right-type="both">
- <td class="left lineNum" data-value="165"></td>
- <td class="content both"><div class="contentText"></div></td>
- <td class="right lineNum" data-value="147"></td>
- <td class="content both"><div class="contentText">in physicis, <hl><span class="tab-indicator" style="tab-size:8;">\u0009</span></hl> quibus maxime gloriatur, primum totus est alienus. Democritea dicit</div></td>
- </tr>
- </tbody>
-
- </table>
- </gr-diff-highlight>
-`);
-/* eslint-enable max-len */
-
-suite('gr-diff-highlight', () => {
- let element;
-
- setup(() => {
- element = basicFixture.instantiate()[1];
- });
-
- suite('comment events', () => {
- let builder;
-
- setup(() => {
- builder = {
- getContentsByLineRange: sinon.stub().returns([]),
- getLineElByChild: sinon.stub().returns({}),
- getSideByLineEl: sinon.stub().returns('other-side'),
- };
- element._cachedDiffBuilder = builder;
- });
-
- test('comment-thread-mouseenter from line comments is ignored', () => {
- const threadEl = document.createElement('div');
- threadEl.className = 'comment-thread';
- threadEl.setAttribute('diff-side', 'right');
- threadEl.setAttribute('line-num', 3);
- element.appendChild(threadEl);
- element.commentRanges = [{side: 'right'}];
-
- sinon.stub(element, 'set');
- threadEl.dispatchEvent(new CustomEvent(
- 'comment-thread-mouseenter', {bubbles: true, composed: true}));
- assert.isFalse(element.set.called);
- });
-
- test('comment-thread-mouseenter from ranged comment causes set', () => {
- const threadEl = document.createElement('div');
- threadEl.className = 'comment-thread';
- threadEl.setAttribute('diff-side', 'right');
- threadEl.setAttribute('line-num', 3);
- threadEl.setAttribute('range', JSON.stringify({
- start_line: 3,
- start_character: 4,
- end_line: 5,
- end_character: 6,
- }));
- element.appendChild(threadEl);
- element.commentRanges = [{side: 'right', range: {
- start_line: 3,
- start_character: 4,
- end_line: 5,
- end_character: 6,
- }}];
-
- sinon.stub(element, 'set');
- threadEl.dispatchEvent(new CustomEvent(
- 'comment-thread-mouseenter', {bubbles: true, composed: true}));
- assert.isTrue(element.set.called);
- const args = element.set.lastCall.args;
- assert.deepEqual(args[0], ['commentRanges', 0, 'hovering']);
- assert.deepEqual(args[1], true);
- });
-
- test('comment-thread-mouseleave from line comments is ignored', () => {
- const threadEl = document.createElement('div');
- threadEl.className = 'comment-thread';
- threadEl.setAttribute('diff-side', 'right');
- threadEl.setAttribute('line-num', 3);
- element.appendChild(threadEl);
- element.commentRanges = [{side: 'right'}];
-
- sinon.stub(element, 'set');
- threadEl.dispatchEvent(new CustomEvent(
- 'comment-thread-mouseleave', {bubbles: true, composed: true}));
- assert.isFalse(element.set.called);
- });
-
- test(`create-range-comment for range when create-comment-requested
- is fired`, () => {
- sinon.stub(element, '_removeActionBox');
- element.selectedRange = {
- side: 'left',
- range: {
- start_line: 7,
- start_character: 11,
- end_line: 24,
- end_character: 42,
- },
- };
- const requestEvent = new CustomEvent('create-comment-requested');
- let createRangeEvent;
- element.addEventListener('create-range-comment', e => {
- createRangeEvent = e;
- });
- element.dispatchEvent(requestEvent);
- assert.deepEqual(element.selectedRange, createRangeEvent.detail);
- assert.isTrue(element._removeActionBox.called);
- });
- });
-
- suite('selection', () => {
- let diff;
- let builder;
- let contentStubs;
-
- const stubContent = (line, side, opt_child) => {
- const contentTd = diff.querySelector(
- `.${side}.lineNum[data-value="${line}"] ~ .content`);
- const contentText = contentTd.querySelector('.contentText');
- const lineEl = diff.querySelector(
- `.${side}.lineNum[data-value="${line}"]`);
- contentStubs.push({
- lineEl,
- contentTd,
- contentText,
- });
- builder.getContentTdByLineEl.withArgs(lineEl).returns(contentTd);
- builder.getLineNumberByChild.withArgs(lineEl).returns(line);
- builder.getContentTdByLine.withArgs(line, side).returns(contentTd);
- builder.getSideByLineEl.withArgs(lineEl).returns(side);
- return contentText;
- };
-
- const emulateSelection = (startNode, startOffset, endNode, endOffset) => {
- const selection = document.getSelection();
- const range = document.createRange();
- range.setStart(startNode, startOffset);
- range.setEnd(endNode, endOffset);
- selection.addRange(range);
- element._handleSelection(selection);
- };
-
- const getLineElByChild = node => {
- const stubs = contentStubs.find(stub => stub.contentTd.contains(node));
- return stubs && stubs.lineEl;
- };
-
- setup(() => {
- contentStubs = [];
- stub('gr-selection-action-box', 'placeAbove');
- stub('gr-selection-action-box', 'placeBelow');
- diff = element.querySelector('#diffTable');
- builder = {
- getContentTdByLine: sinon.stub(),
- getContentTdByLineEl: sinon.stub(),
- getLineElByChild,
- getLineNumberByChild: sinon.stub(),
- getSideByLineEl: sinon.stub(),
- };
- element._cachedDiffBuilder = builder;
- });
-
- teardown(() => {
- contentStubs = null;
- document.getSelection().removeAllRanges();
- });
-
- test('single first line', () => {
- const content = stubContent(1, 'right');
- sinon.spy(element, '_positionActionBox');
- emulateSelection(content.firstChild, 5, content.firstChild, 12);
- const actionBox = element.shadowRoot
- .querySelector('gr-selection-action-box');
- assert.isTrue(actionBox.positionBelow);
- });
-
- test('multiline starting on first line', () => {
- const startContent = stubContent(1, 'right');
- const endContent = stubContent(2, 'right');
- sinon.spy(element, '_positionActionBox');
- emulateSelection(
- startContent.firstChild, 10, endContent.lastChild, 7);
- const actionBox = element.shadowRoot
- .querySelector('gr-selection-action-box');
- assert.isTrue(actionBox.positionBelow);
- });
-
- test('single line', () => {
- const content = stubContent(138, 'left');
- sinon.spy(element, '_positionActionBox');
- emulateSelection(content.firstChild, 5, content.firstChild, 12);
- const actionBox = element.shadowRoot
- .querySelector('gr-selection-action-box');
- const {range, side} = element.selectedRange;
- assert.deepEqual(range, {
- start_line: 138,
- start_character: 5,
- end_line: 138,
- end_character: 12,
- });
- assert.equal(side, 'left');
- assert.notOk(actionBox.positionBelow);
- });
-
- test('multiline', () => {
- const startContent = stubContent(119, 'right');
- const endContent = stubContent(120, 'right');
- sinon.spy(element, '_positionActionBox');
- emulateSelection(
- startContent.firstChild, 10, endContent.lastChild, 7);
- const actionBox = element.shadowRoot
- .querySelector('gr-selection-action-box');
- const {range, side} = element.selectedRange;
- assert.deepEqual(range, {
- start_line: 119,
- start_character: 10,
- end_line: 120,
- end_character: 36,
- });
- assert.equal(side, 'right');
- assert.notOk(actionBox.positionBelow);
- });
-
- test('multiple ranges aka firefox implementation', () => {
- const startContent = stubContent(119, 'right');
- const endContent = stubContent(120, 'right');
-
- const startRange = document.createRange();
- startRange.setStart(startContent.firstChild, 10);
- startRange.setEnd(startContent.firstChild, 11);
-
- const endRange = document.createRange();
- endRange.setStart(endContent.lastChild, 6);
- endRange.setEnd(endContent.lastChild, 7);
-
- const getRangeAtStub = sinon.stub();
- getRangeAtStub
- .onFirstCall().returns(startRange)
- .onSecondCall()
- .returns(endRange);
- const selection = {
- rangeCount: 2,
- getRangeAt: getRangeAtStub,
- removeAllRanges: sinon.stub(),
- };
- element._handleSelection(selection);
- const {range} = element.selectedRange;
- assert.deepEqual(range, {
- start_line: 119,
- start_character: 10,
- end_line: 120,
- end_character: 36,
- });
- });
-
- test('multiline grow end highlight over tabs', () => {
- const startContent = stubContent(119, 'right');
- const endContent = stubContent(120, 'right');
- emulateSelection(startContent.firstChild, 10, endContent.firstChild, 2);
- const {range, side} = element.selectedRange;
- assert.deepEqual(range, {
- start_line: 119,
- start_character: 10,
- end_line: 120,
- end_character: 2,
- });
- assert.equal(side, 'right');
- });
-
- test('collapsed', () => {
- const content = stubContent(138, 'left');
- emulateSelection(content.firstChild, 5, content.firstChild, 5);
- assert.isOk(document.getSelection().getRangeAt(0).startContainer);
- assert.isFalse(!!element.selectedRange);
- });
-
- test('starts inside hl', () => {
- const content = stubContent(140, 'left');
- const hl = content.querySelector('.foo');
- emulateSelection(hl.firstChild, 2, hl.nextSibling, 7);
- const {range, side} = element.selectedRange;
- assert.deepEqual(range, {
- start_line: 140,
- start_character: 8,
- end_line: 140,
- end_character: 23,
- });
- assert.equal(side, 'left');
- });
-
- test('ends inside hl', () => {
- const content = stubContent(140, 'left');
- const hl = content.querySelector('.bar');
- emulateSelection(hl.previousSibling, 2, hl.firstChild, 3);
- const {range} = element.selectedRange;
- assert.deepEqual(range, {
- start_line: 140,
- start_character: 18,
- end_line: 140,
- end_character: 27,
- });
- });
-
- test('multiple hl', () => {
- const content = stubContent(140, 'left');
- const hl = content.querySelectorAll('hl')[4];
- emulateSelection(content.firstChild, 2, hl.firstChild, 2);
- const {range, side} = element.selectedRange;
- assert.deepEqual(range, {
- start_line: 140,
- start_character: 2,
- end_line: 140,
- end_character: 61,
- });
- assert.equal(side, 'left');
- });
-
- test('starts outside of diff', () => {
- const contentText = stubContent(140, 'left');
- const contentTd = contentText.parentElement;
-
- emulateSelection(contentTd.parentElement, 0,
- contentText.firstChild, 2);
- assert.isFalse(!!element.selectedRange);
- });
-
- test('ends outside of diff', () => {
- const content = stubContent(140, 'left');
- emulateSelection(content.nextElementSibling.firstChild, 2,
- content.firstChild, 2);
- assert.isFalse(!!element.selectedRange);
- });
-
- test('starts and ends on different sides', () => {
- const startContent = stubContent(140, 'left');
- const endContent = stubContent(130, 'right');
- emulateSelection(startContent.firstChild, 2, endContent.firstChild, 2);
- assert.isFalse(!!element.selectedRange);
- });
-
- test('starts in comment thread element', () => {
- const startContent = stubContent(140, 'left');
- const comment = startContent.parentElement.querySelector(
- '.comment-thread');
- const endContent = stubContent(141, 'left');
- emulateSelection(comment.firstChild, 2, endContent.firstChild, 4);
- const {range, side} = element.selectedRange;
- assert.deepEqual(range, {
- start_line: 140,
- start_character: 83,
- end_line: 141,
- end_character: 4,
- });
- assert.equal(side, 'left');
- });
-
- test('ends in comment thread element', () => {
- const content = stubContent(140, 'left');
- const comment = content.parentElement.querySelector(
- '.comment-thread');
- emulateSelection(content.firstChild, 4, comment.firstChild, 1);
- const {range, side} = element.selectedRange;
- assert.deepEqual(range, {
- start_line: 140,
- start_character: 4,
- end_line: 140,
- end_character: 83,
- });
- assert.equal(side, 'left');
- });
-
- test('starts in context element', () => {
- const contextControl =
- diff.querySelector('.contextControl').querySelector('gr-button');
- const content = stubContent(146, 'right');
- emulateSelection(contextControl, 0, content.firstChild, 7);
- // TODO (viktard): Select nearest line.
- assert.isFalse(!!element.selectedRange);
- });
-
- test('ends in context element', () => {
- const contextControl =
- diff.querySelector('.contextControl').querySelector('gr-button');
- const content = stubContent(141, 'left');
- emulateSelection(content.firstChild, 2, contextControl, 1);
- // TODO (viktard): Select nearest line.
- assert.isFalse(!!element.selectedRange);
- });
-
- test('selection containing context element', () => {
- const startContent = stubContent(130, 'right');
- const endContent = stubContent(146, 'right');
- emulateSelection(startContent.firstChild, 3, endContent.firstChild, 14);
- const {range, side} = element.selectedRange;
- assert.deepEqual(range, {
- start_line: 130,
- start_character: 3,
- end_line: 146,
- end_character: 14,
- });
- assert.equal(side, 'right');
- });
-
- test('ends at a tab', () => {
- const content = stubContent(140, 'left');
- emulateSelection(
- content.firstChild, 1, content.querySelector('span'), 0);
- const {range, side} = element.selectedRange;
- assert.deepEqual(range, {
- start_line: 140,
- start_character: 1,
- end_line: 140,
- end_character: 51,
- });
- assert.equal(side, 'left');
- });
-
- test('starts at a tab', () => {
- const content = stubContent(140, 'left');
- emulateSelection(
- content.querySelectorAll('hl')[3], 0,
- content.querySelectorAll('span')[1].nextSibling, 1);
- const {range, side} = element.selectedRange;
- assert.deepEqual(range, {
- start_line: 140,
- start_character: 51,
- end_line: 140,
- end_character: 71,
- });
- assert.equal(side, 'left');
- });
-
- test('properly accounts for syntax highlighting', () => {
- const content = stubContent(140, 'left');
- const spy = sinon.spy(element, '_normalizeRange');
- emulateSelection(
- content.querySelectorAll('hl')[3], 0,
- content.querySelectorAll('span')[1], 0);
- const spyCall = spy.getCall(0);
- const range = document.getSelection().getRangeAt(0);
- assert.notDeepEqual(spyCall.returnValue, range);
- });
-
- test('GrRangeNormalizer._getTextOffset computes text offset', () => {
- let content = stubContent(140, 'left');
- let child = content.lastChild.lastChild;
- let result = _getTextOffset(content, child);
- assert.equal(result, 75);
- content = stubContent(146, 'right');
- child = content.lastChild;
- result = _getTextOffset(content, child);
- assert.equal(result, 0);
- });
-
- test('_fixTripleClickSelection', () => {
- const startContent = stubContent(119, 'right');
- const endContent = stubContent(120, 'right');
- emulateSelection(startContent.firstChild, 0, endContent.firstChild, 0);
- const {range, side} = element.selectedRange;
- assert.deepEqual(range, {
- start_line: 119,
- start_character: 0,
- end_line: 119,
- end_character: element._getLength(startContent),
- });
- assert.equal(side, 'right');
- });
- });
-});
-
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-diff-highlight_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-diff-highlight_test.ts
new file mode 100644
index 0000000..b819754
--- /dev/null
+++ b/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-diff-highlight_test.ts
@@ -0,0 +1,713 @@
+/**
+ * @license
+ * Copyright 2016 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup-karma';
+import './gr-diff-highlight';
+import {_getTextOffset} from './gr-range-normalizer';
+import {fixture, fixtureCleanup, html} from '@open-wc/testing-helpers';
+import {
+ GrDiffHighlight,
+ DiffBuilderInterface,
+ CreateRangeCommentEventDetail,
+} from './gr-diff-highlight';
+import {Side} from '../../../api/diff';
+import {SinonStubbedMember} from 'sinon';
+import {queryAndAssert} from '../../../utils/common-util';
+import {GrDiffThreadElement} from '../gr-diff/gr-diff-utils';
+import {waitQueryAndAssert, waitUntil} from '../../../test/test-utils';
+import {GrSelectionActionBox} from '../gr-selection-action-box/gr-selection-action-box';
+
+// Splitting long lines in html into shorter rows breaks tests:
+// zero-length text nodes and new lines are not expected in some places
+/* eslint-disable max-len, lit/prefer-static-styles */
+/* prettier-ignore */
+const diffTable = html`
+ <table id="diffTable">
+ <tbody class="section both">
+ <tr class="diff-row side-by-side" left-type="both" right-type="both">
+ <td class="left lineNum" data-value="1"></td>
+ <td class="content both"><div class="contentText">[1] Nam cum ad me in Cumanum salutandi causa uterque venisset,</div></td>
+ <td class="right lineNum" data-value="1"></td>
+ <td class="content both"><div class="contentText">[1] Nam cum ad me in Cumanum salutandi causa uterque</div></td>
+ </tr>
+ </tbody>
+
+ <tbody class="section delta">
+ <tr class="diff-row side-by-side" left-type="remove" right-type="add">
+ <td class="left lineNum" data-value="2"></td>
+ <!-- Next tag is formatted to eliminate zero-length text nodes. -->
+ <td class="content remove"><div class="contentText">na💢ti <hl class="foo range generated_id314">te, inquit</hl>, sumus<hl class="bar">aliquando</hl> otiosum, <hl>certe</hl> a<hl><span class="tab-indicator" style="tab-size:8;"> </span></hl>udiam, <hl>quid</hl> sit,<span class="tab-indicator" style="tab-size:8;"> </span>quod<hl>Epicurum</hl></div></td>
+ <td class="right lineNum" data-value="2"></td>
+ <!-- Next tag is formatted to eliminate zero-length text nodes. -->
+ <td class="content add"><div class="contentText">nacti , <hl>,</hl> sumus<hl><span class="tab-indicator" style="tab-size:8;"> </span></hl>otiosum,<span class="tab-indicator" style="tab-size:8;"> </span> audiam,sit, quod</div></td>
+ </tr>
+ </tbody>
+
+ <tbody class="section both">
+ <tr class="diff-row side-by-side" left-type="both" right-type="both">
+ <td class="left lineNum" data-value="138"></td>
+ <td class="content both"><div class="contentText">[14] Nam cum ad me in Cumanum salutandi causa uterque venisset,</div></td>
+ <td class="right lineNum" data-value="119"></td>
+ <td class="content both"><div class="contentText">[14] Nam cum ad me in Cumanum salutandi causa uterque venisset,</div></td>
+ </tr>
+ </tbody>
+
+ <tbody class="section delta">
+ <tr class="diff-row side-by-side" left-type="remove" right-type="add">
+ <td class="left lineNum" data-value="140"></td>
+ <!-- Next tag is formatted to eliminate zero-length text nodes. -->
+ <td class="content remove"><div class="contentText">na💢ti <hl class="foo">te, inquit</hl>, sumus <hl class="bar">aliquando</hl> otiosum, <hl>certe</hl> a <hl><span class="tab-indicator" style="tab-size:8;">\u0009</span></hl>udiam, <hl>quid</hl> sit, <span class="tab-indicator" style="tab-size:8;">\u0009</span>quod <hl>Epicurum</hl></div><div class="comment-thread">
+ [Yet another random diff thread content here]
+ </div></td>
+ <td class="right lineNum" data-value="120"></td>
+ <!-- Next tag is formatted to eliminate zero-length text nodes. -->
+ <td class="content add"><div class="contentText">nacti , <hl>,</hl> sumus <hl><span class="tab-indicator" style="tab-size:8;">\u0009</span></hl> otiosum, <span class="tab-indicator" style="tab-size:8;">\u0009</span> audiam, sit, quod</div></td>
+ </tr>
+ </tbody>
+
+ <tbody class="section both">
+ <tr class="diff-row side-by-side" left-type="both" right-type="both">
+ <td class="left lineNum" data-value="141"></td>
+ <td class="content both"><div class="contentText">nam et<hl><span class="tab-indicator" style="tab-size:8;"> </span></hl>complectitur<span class="tab-indicator" style="tab-size:8;"></span>verbis, quod vult, et dicit plane, quod intellegam;</div></td>
+ <td class="right lineNum" data-value="130"></td>
+ <td class="content both"><div class="contentText">nam et complectitur verbis, quod vult, et dicit plane, quodintellegam;</div></td>
+ </tr>
+ </tbody>
+
+ <tbody class="section contextControl">
+ <tr
+ class="diff-row side-by-side"
+ left-type="contextControl"
+ right-type="contextControl"
+ >
+ <td class="left contextLineNum"></td>
+ <td>
+ <gr-button>+10↑</gr-button>
+ -
+ <gr-button>Show 21 common lines</gr-button>
+ -
+ <gr-button>+10↓</gr-button>
+ </td>
+ <td class="right contextLineNum"></td>
+ <td>
+ <gr-button>+10↑</gr-button>
+ -
+ <gr-button>Show 21 common lines</gr-button>
+ -
+ <gr-button>+10↓</gr-button>
+ </td>
+ </tr>
+ </tbody>
+
+ <tbody class="section delta total">
+ <tr class="diff-row side-by-side" left-type="blank" right-type="add">
+ <td class="left"></td>
+ <td class="blank"></td>
+ <td class="right lineNum" data-value="146"></td>
+ <td class="content add"><div class="contentText">[17] Quid igitur est? inquit; audire enim cupio, quid non probes. Principio, inquam,</div></td>
+ </tr>
+ </tbody>
+
+ <tbody class="section both">
+ <tr class="diff-row side-by-side" left-type="both" right-type="both">
+ <td class="left lineNum" data-value="165"></td>
+ <td class="content both"><div class="contentText"></div></td>
+ <td class="right lineNum" data-value="147"></td>
+ <td class="content both"><div class="contentText">in physicis, <hl><span class="tab-indicator" style="tab-size:8;"> </span></hl>quibus maxime gloriatur, primum totus est alienus. Democritea dicit</div></td>
+ </tr>
+ </tbody>
+ </table>
+`;
+/* eslint-enable max-len */
+
+suite('gr-diff-highlight', () => {
+ suite('comment events', () => {
+ let threadEl: GrDiffThreadElement;
+ let hlRange: HTMLElement;
+ let element: GrDiffHighlight;
+ let diff: HTMLElement;
+ let builder: {
+ getContentTdByLineEl: SinonStubbedMember<
+ DiffBuilderInterface['getContentTdByLineEl']
+ >;
+ };
+
+ setup(async () => {
+ diff = await fixture<HTMLTableElement>(diffTable);
+ builder = {
+ getContentTdByLineEl: sinon.stub(),
+ };
+ element = new GrDiffHighlight();
+ element.init(diff, builder);
+ hlRange = queryAndAssert(diff, 'hl.range.generated_id314');
+
+ threadEl = document.createElement(
+ 'div'
+ ) as unknown as GrDiffThreadElement;
+ threadEl.className = 'comment-thread';
+ threadEl.rootId = 'id314';
+ diff.appendChild(threadEl);
+ });
+
+ teardown(() => {
+ element.cleanup();
+ threadEl.remove();
+ });
+
+ test('comment-thread-mouseenter toggles rangeHoverHighlight class', async () => {
+ assert.isFalse(hlRange.classList.contains('rangeHoverHighlight'));
+ threadEl.dispatchEvent(
+ new CustomEvent('comment-thread-mouseenter', {
+ bubbles: true,
+ composed: true,
+ })
+ );
+ await waitUntil(() => hlRange.classList.contains('rangeHoverHighlight'));
+ assert.isTrue(hlRange.classList.contains('rangeHoverHighlight'));
+ });
+
+ test('comment-thread-mouseleave toggles rangeHoverHighlight class', async () => {
+ hlRange.classList.add('rangeHoverHighlight');
+ threadEl.dispatchEvent(
+ new CustomEvent('comment-thread-mouseleave', {
+ bubbles: true,
+ composed: true,
+ })
+ );
+ await waitUntil(() => !hlRange.classList.contains('rangeHoverHighlight'));
+ assert.isFalse(hlRange.classList.contains('rangeHoverHighlight'));
+ });
+
+ test(`create-range-comment for range when create-comment-requested
+ is fired`, () => {
+ const removeActionBoxStub = sinon.stub(element, 'removeActionBox');
+ element.selectedRange = {
+ side: Side.LEFT,
+ range: {
+ start_line: 7,
+ start_character: 11,
+ end_line: 24,
+ end_character: 42,
+ },
+ };
+ const requestEvent = new CustomEvent('create-comment-requested');
+ let createRangeEvent: CustomEvent<CreateRangeCommentEventDetail>;
+ diff.addEventListener('create-range-comment', e => {
+ createRangeEvent = e;
+ });
+ diff.dispatchEvent(requestEvent);
+ if (!createRangeEvent!) assert.fail('event not set');
+ assert.deepEqual(element.selectedRange, createRangeEvent.detail);
+ assert.isTrue(removeActionBoxStub.called);
+ });
+ });
+
+ suite('selection', () => {
+ let element: GrDiffHighlight;
+ let diff: HTMLElement;
+ let builder: {
+ getContentTdByLineEl: SinonStubbedMember<
+ DiffBuilderInterface['getContentTdByLineEl']
+ >;
+ };
+ let contentStubs;
+
+ setup(async () => {
+ diff = await fixture<HTMLTableElement>(diffTable);
+ builder = {
+ getContentTdByLineEl: sinon.stub(),
+ };
+ element = new GrDiffHighlight();
+ element.init(diff, builder);
+ contentStubs = [];
+ stub('gr-selection-action-box', 'placeAbove');
+ stub('gr-selection-action-box', 'placeBelow');
+ });
+
+ teardown(() => {
+ fixtureCleanup();
+ element.cleanup();
+ contentStubs = null;
+ document.getSelection()!.removeAllRanges();
+ });
+
+ const stubContent = (line: number, side: Side) => {
+ const contentTd = diff.querySelector(
+ `.${side}.lineNum[data-value="${line}"] ~ .content`
+ );
+ if (!contentTd) assert.fail('content td not found');
+ const contentText = contentTd.querySelector('.contentText');
+ const lineEl =
+ diff.querySelector(`.${side}.lineNum[data-value="${line}"]`) ??
+ undefined;
+ contentStubs.push({
+ lineEl,
+ contentTd,
+ contentText,
+ });
+ builder.getContentTdByLineEl.withArgs(lineEl).returns(contentTd);
+ return contentText;
+ };
+
+ const emulateSelection = (
+ startNode: Node,
+ startOffset: number,
+ endNode: Node,
+ endOffset: number
+ ) => {
+ const selection = document.getSelection();
+ if (!selection) assert.fail('no selection');
+ selection.removeAllRanges();
+ const range = document.createRange();
+ range.setStart(startNode, startOffset);
+ range.setEnd(endNode, endOffset);
+ selection.addRange(range);
+ element.handleSelection(selection, false);
+ };
+
+ test('single first line', () => {
+ const content = stubContent(1, Side.RIGHT);
+ sinon.spy(element, 'positionActionBox');
+ if (!content?.firstChild) assert.fail('content first child not found');
+ emulateSelection(content.firstChild, 5, content.firstChild, 12);
+ const actionBox = diff.querySelector('gr-selection-action-box');
+ if (!actionBox) assert.fail('action box not found');
+ assert.isTrue(actionBox.positionBelow);
+ });
+
+ test('multiline starting on first line', () => {
+ const startContent = stubContent(1, Side.RIGHT);
+ const endContent = stubContent(2, Side.RIGHT);
+ sinon.spy(element, 'positionActionBox');
+ if (!startContent?.firstChild) {
+ assert.fail('first child of start content not found');
+ }
+ if (!endContent?.lastChild) {
+ assert.fail('last child of end content not found');
+ }
+ emulateSelection(startContent.firstChild, 10, endContent.lastChild, 7);
+ const actionBox = diff.querySelector('gr-selection-action-box');
+ if (!actionBox) assert.fail('action box not found');
+ assert.isTrue(actionBox.positionBelow);
+ });
+
+ test('single line', async () => {
+ const content = stubContent(138, Side.LEFT);
+ sinon.spy(element, 'positionActionBox');
+ if (!content?.firstChild) assert.fail('content first child not found');
+ emulateSelection(content.firstChild, 5, content.firstChild, 12);
+ const actionBox = await waitQueryAndAssert<GrSelectionActionBox>(
+ diff,
+ 'gr-selection-action-box'
+ );
+ if (!element.selectedRange) assert.fail('no range selected');
+ const {range, side} = element.selectedRange;
+ assert.deepEqual(range, {
+ start_line: 138,
+ start_character: 5,
+ end_line: 138,
+ end_character: 12,
+ });
+ assert.equal(side, Side.LEFT);
+ assert.notOk(actionBox.positionBelow);
+ });
+
+ test('multiline', () => {
+ const startContent = stubContent(119, Side.RIGHT);
+ const endContent = stubContent(120, Side.RIGHT);
+ sinon.spy(element, 'positionActionBox');
+ if (!startContent?.firstChild) {
+ assert.fail('first child of start content not found');
+ }
+ if (!endContent?.lastChild) {
+ assert.fail('last child of end content');
+ }
+ emulateSelection(startContent.firstChild, 10, endContent.lastChild, 7);
+ const actionBox = diff.querySelector('gr-selection-action-box');
+ if (!actionBox) assert.fail('action box not found');
+ if (!element.selectedRange) assert.fail('no range selected');
+ const {range, side} = element.selectedRange;
+ assert.deepEqual(range, {
+ start_line: 119,
+ start_character: 10,
+ end_line: 120,
+ end_character: 36,
+ });
+ assert.equal(side, Side.RIGHT);
+ assert.notOk(actionBox.positionBelow);
+ });
+
+ test('multiple ranges aka firefox implementation', () => {
+ const startContent = stubContent(119, Side.RIGHT);
+ const endContent = stubContent(120, Side.RIGHT);
+ if (!startContent?.firstChild) {
+ assert.fail('first child of start content not found');
+ }
+ if (!endContent?.lastChild) {
+ assert.fail('last child of end content');
+ }
+
+ const startRange = document.createRange();
+ startRange.setStart(startContent.firstChild, 10);
+ startRange.setEnd(startContent.firstChild, 11);
+
+ const endRange = document.createRange();
+ endRange.setStart(endContent.lastChild, 6);
+ endRange.setEnd(endContent.lastChild, 7);
+
+ const getRangeAtStub = sinon.stub();
+ getRangeAtStub
+ .onFirstCall()
+ .returns(startRange)
+ .onSecondCall()
+ .returns(endRange);
+ const selection = {
+ rangeCount: 2,
+ getRangeAt: getRangeAtStub,
+ removeAllRanges: sinon.stub(),
+ } as unknown as Selection;
+ element.handleSelection(selection, false);
+ if (!element.selectedRange) assert.fail('no range selected');
+ const {range} = element.selectedRange;
+ assert.deepEqual(range, {
+ start_line: 119,
+ start_character: 10,
+ end_line: 120,
+ end_character: 36,
+ });
+ });
+
+ test('multiline grow end highlight over tabs', () => {
+ const startContent = stubContent(119, Side.RIGHT);
+ const endContent = stubContent(120, Side.RIGHT);
+ if (!startContent?.firstChild) {
+ assert.fail('first child of start content not found');
+ }
+ if (!endContent?.firstChild) {
+ assert.fail('first child of end content not found');
+ }
+ emulateSelection(startContent.firstChild, 10, endContent.firstChild, 2);
+ if (!element.selectedRange) assert.fail('no range selected');
+ const {range, side} = element.selectedRange;
+ assert.deepEqual(range, {
+ start_line: 119,
+ start_character: 10,
+ end_line: 120,
+ end_character: 2,
+ });
+ assert.equal(side, Side.RIGHT);
+ });
+
+ test('collapsed', () => {
+ const content = stubContent(138, Side.LEFT);
+ if (!content?.firstChild) {
+ assert.fail('first child of content not found');
+ }
+ emulateSelection(content.firstChild, 5, content.firstChild, 5);
+ const sel = document.getSelection();
+ if (!sel) assert.fail('no selection');
+ assert.isOk(sel.getRangeAt(0).startContainer);
+ assert.isFalse(!!element.selectedRange);
+ });
+
+ test('starts inside hl', () => {
+ const content = stubContent(140, Side.LEFT);
+ if (!content) {
+ assert.fail('content not found');
+ }
+ const hl = content.querySelector('.foo');
+ if (!hl?.firstChild) {
+ assert.fail('first child of hl element not found');
+ }
+ if (!hl?.nextSibling) {
+ assert.fail('next sibling of hl element not found');
+ }
+ emulateSelection(hl.firstChild, 2, hl.nextSibling, 7);
+ if (!element.selectedRange) assert.fail('no range selected');
+ const {range, side} = element.selectedRange;
+ assert.deepEqual(range, {
+ start_line: 140,
+ start_character: 8,
+ end_line: 140,
+ end_character: 23,
+ });
+ assert.equal(side, Side.LEFT);
+ });
+
+ test('ends inside hl', () => {
+ const content = stubContent(140, Side.LEFT);
+ if (!content) assert.fail('content not found');
+ const hl = content.querySelector('.bar');
+ if (!hl) assert.fail('hl inside content not found');
+ if (!hl.previousSibling) assert.fail('previous sibling not found');
+ if (!hl.firstChild) assert.fail('first child not found');
+ emulateSelection(hl.previousSibling, 2, hl.firstChild, 3);
+ if (!element.selectedRange) assert.fail('no range selected');
+ const {range} = element.selectedRange;
+ assert.deepEqual(range, {
+ start_line: 140,
+ start_character: 18,
+ end_line: 140,
+ end_character: 27,
+ });
+ });
+
+ test('multiple hl', () => {
+ const content = stubContent(140, Side.LEFT);
+ if (!content) assert.fail('content not found');
+ if (!content.firstChild) assert.fail('first child not found');
+ const hl = content.querySelectorAll('hl')[4];
+ if (!hl) assert.fail('hl not found');
+ if (!hl.firstChild) assert.fail('first child of hl not found');
+ emulateSelection(content.firstChild, 2, hl.firstChild, 2);
+ if (!element.selectedRange) assert.fail('no range selected');
+ const {range, side} = element.selectedRange;
+ assert.deepEqual(range, {
+ start_line: 140,
+ start_character: 2,
+ end_line: 140,
+ end_character: 61,
+ });
+ assert.equal(side, Side.LEFT);
+ });
+
+ test('starts outside of diff', () => {
+ const contentText = stubContent(140, Side.LEFT);
+ if (!contentText) assert.fail('content not found');
+ if (!contentText.firstChild) assert.fail('child not found');
+ const contentTd = contentText.parentElement;
+ if (!contentTd) assert.fail('content td not found');
+ if (!contentTd.parentElement) assert.fail('parent of td not found');
+
+ emulateSelection(contentTd.parentElement, 0, contentText.firstChild, 2);
+ assert.isFalse(!!element.selectedRange);
+ });
+
+ test('ends outside of diff', () => {
+ const content = stubContent(140, Side.LEFT);
+ if (!content) assert.fail('content not found');
+ if (!content.firstChild) assert.fail('child not found');
+ if (!content.nextElementSibling) assert.fail('sibling not found');
+ if (!content.nextElementSibling.firstChild) {
+ assert.fail('sibling child not found');
+ }
+ emulateSelection(
+ content.nextElementSibling.firstChild,
+ 2,
+ content.firstChild,
+ 2
+ );
+ assert.isFalse(!!element.selectedRange);
+ });
+
+ test('starts and ends on different sides', () => {
+ const startContent = stubContent(140, Side.LEFT);
+ const endContent = stubContent(130, Side.RIGHT);
+ if (!startContent?.firstChild) {
+ assert.fail('first child of start content not found');
+ }
+ if (!endContent?.firstChild) {
+ assert.fail('first child of end content not found');
+ }
+ emulateSelection(startContent.firstChild, 2, endContent.firstChild, 2);
+ assert.isFalse(!!element.selectedRange);
+ });
+
+ test('starts in comment thread element', () => {
+ const startContent = stubContent(140, Side.LEFT);
+ if (!startContent?.parentElement) {
+ assert.fail('parent el of start content not found');
+ }
+ const comment =
+ startContent.parentElement.querySelector('.comment-thread');
+ if (!comment?.firstChild) {
+ assert.fail('first child of comment not found');
+ }
+ const endContent = stubContent(141, Side.LEFT);
+ if (!endContent?.firstChild) {
+ assert.fail('first child of end content not found');
+ }
+ emulateSelection(comment.firstChild, 2, endContent.firstChild, 4);
+ if (!element.selectedRange) assert.fail('no range selected');
+ const {range, side} = element.selectedRange;
+ assert.deepEqual(range, {
+ start_line: 140,
+ start_character: 83,
+ end_line: 141,
+ end_character: 4,
+ });
+ assert.equal(side, Side.LEFT);
+ });
+
+ test('ends in comment thread element', () => {
+ const content = stubContent(140, Side.LEFT);
+ if (!content?.firstChild) {
+ assert.fail('first child of content not found');
+ }
+ if (!content?.parentElement) {
+ assert.fail('parent element of content not found');
+ }
+ const comment = content.parentElement.querySelector('.comment-thread');
+ if (!comment?.firstChild) {
+ assert.fail('first child of comment element not found');
+ }
+ emulateSelection(content.firstChild, 4, comment.firstChild, 1);
+ if (!element.selectedRange) assert.fail('no range selected');
+ const {range, side} = element.selectedRange;
+ assert.deepEqual(range, {
+ start_line: 140,
+ start_character: 4,
+ end_line: 140,
+ end_character: 83,
+ });
+ assert.equal(side, Side.LEFT);
+ });
+
+ test('starts in context element', () => {
+ const contextControl = diff
+ .querySelector('.contextControl')!
+ .querySelector('gr-button');
+ if (!contextControl) assert.fail('context control not found');
+ const content = stubContent(146, Side.RIGHT);
+ if (!content) assert.fail('content not found');
+ if (!content.firstChild) assert.fail('content child not found');
+ emulateSelection(contextControl, 0, content.firstChild, 7);
+ // TODO (viktard): Select nearest line.
+ assert.isFalse(!!element.selectedRange);
+ });
+
+ test('ends in context element', () => {
+ const contextControl = diff
+ .querySelector('.contextControl')!
+ .querySelector('gr-button');
+ if (!contextControl) {
+ assert.fail('context control element not found');
+ }
+ const content = stubContent(141, Side.LEFT);
+ if (!content?.firstChild) {
+ assert.fail('first child of content element not found');
+ }
+ emulateSelection(content.firstChild, 2, contextControl, 1);
+ // TODO (viktard): Select nearest line.
+ assert.isFalse(!!element.selectedRange);
+ });
+
+ test('selection containing context element', () => {
+ const startContent = stubContent(130, Side.RIGHT);
+ const endContent = stubContent(146, Side.RIGHT);
+ if (!startContent?.firstChild) {
+ assert.fail('first child of start content not found');
+ }
+ if (!endContent?.firstChild) {
+ assert.fail('first child of end content not found');
+ }
+ emulateSelection(startContent.firstChild, 3, endContent.firstChild, 14);
+ if (!element.selectedRange) assert.fail('no range selected');
+ const {range, side} = element.selectedRange;
+ assert.deepEqual(range, {
+ start_line: 130,
+ start_character: 3,
+ end_line: 146,
+ end_character: 14,
+ });
+ assert.equal(side, Side.RIGHT);
+ });
+
+ test('ends at a tab', () => {
+ const content = stubContent(140, Side.LEFT);
+ if (!content?.firstChild) {
+ assert.fail('first child of content element not found');
+ }
+ const span = content.querySelector('span');
+ if (!span) assert.fail('span element not found');
+ emulateSelection(content.firstChild, 1, span, 0);
+ if (!element.selectedRange) assert.fail('no range selected');
+ const {range, side} = element.selectedRange;
+ assert.deepEqual(range, {
+ start_line: 140,
+ start_character: 1,
+ end_line: 140,
+ end_character: 51,
+ });
+ assert.equal(side, Side.LEFT);
+ });
+
+ test('starts at a tab', () => {
+ const content = stubContent(140, Side.LEFT);
+ if (!content) assert.fail('content element not found');
+ emulateSelection(
+ content.querySelectorAll('hl')[3],
+ 0,
+ content.querySelectorAll('span')[1].nextSibling!,
+ 1
+ );
+ if (!element.selectedRange) assert.fail('no range selected');
+ const {range, side} = element.selectedRange;
+ assert.deepEqual(range, {
+ start_line: 140,
+ start_character: 51,
+ end_line: 140,
+ end_character: 71,
+ });
+ assert.equal(side, Side.LEFT);
+ });
+
+ test('properly accounts for syntax highlighting', () => {
+ const content = stubContent(140, Side.LEFT);
+ if (!content) assert.fail('content element not found');
+ emulateSelection(
+ content.querySelectorAll('hl')[3],
+ 0,
+ content.querySelectorAll('span')[1],
+ 0
+ );
+ if (!element.selectedRange) assert.fail('no range selected');
+ const {range, side} = element.selectedRange;
+ assert.deepEqual(range, {
+ start_line: 140,
+ start_character: 51,
+ end_line: 140,
+ end_character: 69,
+ });
+ assert.equal(side, Side.LEFT);
+ });
+
+ test('GrRangeNormalizer.getTextOffset computes text offset', () => {
+ let content = stubContent(140, Side.LEFT);
+ if (!content) assert.fail('content element not found');
+ if (!content.lastChild) assert.fail('last child of content not found');
+ let child = content.lastChild.lastChild;
+ if (!child) assert.fail('last child of last child of content not found');
+ let result = _getTextOffset(content, child);
+ assert.equal(result, 75);
+ content = stubContent(146, Side.RIGHT);
+ if (!content) assert.fail('content element not found');
+ child = content.lastChild;
+ if (!child) assert.fail('child element not found');
+ result = _getTextOffset(content, child);
+ assert.equal(result, 0);
+ });
+
+ test('fixTripleClickSelection', () => {
+ const startContent = stubContent(119, Side.RIGHT);
+ const endContent = stubContent(120, Side.RIGHT);
+ if (!startContent?.firstChild) {
+ assert.fail('first child of start content not found');
+ }
+ if (!endContent) assert.fail('end content not found');
+ if (!endContent.firstChild) assert.fail('first child not found');
+ emulateSelection(startContent.firstChild, 0, endContent.firstChild, 0);
+ if (!element.selectedRange) assert.fail('no range selected');
+ const {range, side} = element.selectedRange;
+ assert.deepEqual(range, {
+ start_line: 119,
+ start_character: 0,
+ end_line: 119,
+ end_character: element.getLength(startContent),
+ });
+ assert.equal(side, Side.RIGHT);
+ });
+ });
+});
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-selection/gr-diff-selection.ts b/polygerrit-ui/app/embed/diff/gr-diff-selection/gr-diff-selection.ts
index 2665ef0..4bb8cc3 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-selection/gr-diff-selection.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-selection/gr-diff-selection.ts
@@ -1,39 +1,23 @@
/**
* @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.
+ * Copyright 2016 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
*/
import '../../../styles/shared-styles';
-import {addListener} from '@polymer/polymer/lib/utils/gestures';
-import {dom, EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-diff-selection_html';
import {
normalize,
NormalizedRange,
} from '../gr-diff-highlight/gr-range-normalizer';
import {descendedFromClass, querySelectorAll} from '../../../utils/dom-util';
-import {customElement, property, observe} from '@polymer/decorators';
import {DiffInfo} from '../../../types/diff';
import {Side} from '../../../constants/constants';
-import {GrDiffBuilderElement} from '../gr-diff-builder/gr-diff-builder-element';
import {
getLineElByChild,
getSide,
getSideByLineEl,
isThreadEl,
} from '../gr-diff/gr-diff-utils';
+import {assertIsDefined} from '../../../utils/common-util';
/**
* Possible CSS classes indicating the state of selection. Dynamically added/
@@ -55,49 +39,35 @@
return {left: null, right: null};
}
-@customElement('gr-diff-selection')
-export class GrDiffSelection extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
-
- @property({type: Object})
+export class GrDiffSelection {
+ // visible for testing
diff?: DiffInfo;
- @property({type: Object})
- _cachedDiffBuilder?: GrDiffBuilderElement;
+ // visible for testing
+ diffTable?: HTMLElement;
- @property({type: Object})
- _linesCache: LinesCache = {left: null, right: null};
+ // visible for testing
+ linesCache: LinesCache = getNewCache();
- constructor() {
- super();
- this.addEventListener('copy', e => this._handleCopy(e));
- addListener(this, 'down', e => this._handleDown(e));
+ init(diff: DiffInfo, diffTable: HTMLElement) {
+ this.cleanup();
+ this.diff = diff;
+ this.diffTable = diffTable;
+ this.diffTable.classList.add(SelectionClass.RIGHT);
+ this.diffTable.addEventListener('copy', this.handleCopy);
+ this.diffTable.addEventListener('mousedown', this.handleDown);
+ this.linesCache = getNewCache();
}
- override connectedCallback() {
- super.connectedCallback();
- this.classList.add(SelectionClass.RIGHT);
+ cleanup() {
+ if (!this.diffTable) return;
+ this.diffTable.removeEventListener('copy', this.handleCopy);
+ this.diffTable.removeEventListener('mousedown', this.handleDown);
}
- get diffBuilder() {
- if (!this._cachedDiffBuilder) {
- this._cachedDiffBuilder = this.querySelector(
- 'gr-diff-builder'
- ) as GrDiffBuilderElement;
- }
- return this._cachedDiffBuilder;
- }
-
- @observe('diff')
- _diffChanged() {
- this._linesCache = getNewCache();
- }
-
- _handleDownOnRangeComment(node: Element) {
+ handleDownOnRangeComment(node: Element) {
if (isThreadEl(node)) {
- this._setClasses([
+ this.setClasses([
SelectionClass.COMMENT,
getSide(node) === Side.LEFT
? SelectionClass.LEFT
@@ -108,14 +78,13 @@
return false;
}
- _handleDown(e: Event) {
+ handleDown = (e: Event) => {
const target = e.target;
if (!(target instanceof Element)) return;
- // Handle the down event on comment thread in Polymer 2
- const handled = this._handleDownOnRangeComment(target);
+ const handled = this.handleDownOnRangeComment(target);
if (handled) return;
const lineEl = getLineElByChild(target);
- const blameSelected = this._elementDescendedFromClass(target, 'blame');
+ const blameSelected = descendedFromClass(target, 'blame', this.diffTable);
if (!lineEl && !blameSelected) {
return;
}
@@ -125,9 +94,10 @@
if (blameSelected) {
targetClasses.push(SelectionClass.BLAME);
} else if (lineEl) {
- const commentSelected = this._elementDescendedFromClass(
+ const commentSelected = descendedFromClass(
target,
- 'gr-comment'
+ 'gr-comment',
+ this.diffTable
);
const side = getSideByLineEl(lineEl);
@@ -140,60 +110,50 @@
}
}
- this._setClasses(targetClasses);
- }
+ this.setClasses(targetClasses);
+ };
/**
* Set the provided list of classes on the element, to the exclusion of all
* other SelectionClass values.
*/
- _setClasses(targetClasses: string[]) {
+ setClasses(targetClasses: string[]) {
+ if (!this.diffTable) return;
// Remove any selection classes that do not belong.
for (const className of Object.values(SelectionClass)) {
if (!targetClasses.includes(className)) {
- this.classList.remove(className);
+ this.diffTable.classList.remove(className);
}
}
// Add new selection classes iff they are not already present.
- for (const _class of targetClasses) {
- if (!this.classList.contains(_class)) {
- this.classList.add(_class);
+ for (const targetClass of targetClasses) {
+ if (!this.diffTable.classList.contains(targetClass)) {
+ this.diffTable.classList.add(targetClass);
}
}
}
- _getCopyEventTarget(e: Event) {
- return (dom(e) as EventApi).rootTarget;
- }
-
- /**
- * Utility function to determine whether an element is a descendant of
- * another element with the particular className.
- */
- _elementDescendedFromClass(element: Element, className: string) {
- return descendedFromClass(element, className, this.diffBuilder.diffElement);
- }
-
- _handleCopy(e: ClipboardEvent) {
+ handleCopy = (e: ClipboardEvent) => {
let commentSelected = false;
- const target = this._getCopyEventTarget(e);
+ const target = e.composedPath()[0];
if (!(target instanceof Element)) return;
if (target instanceof HTMLTextAreaElement) return;
- if (!this._elementDescendedFromClass(target, 'diff-row')) return;
- if (this.classList.contains(SelectionClass.COMMENT)) {
+ if (!descendedFromClass(target, 'diff-row', this.diffTable)) return;
+ if (!this.diffTable) return;
+ if (this.diffTable.classList.contains(SelectionClass.COMMENT)) {
commentSelected = true;
}
const lineEl = getLineElByChild(target);
if (!lineEl) return;
const side = getSideByLineEl(lineEl);
- const text = this._getSelectedText(side, commentSelected);
+ const text = this.getSelectedText(side, commentSelected);
if (text && e.clipboardData) {
e.clipboardData.setData('Text', text);
e.preventDefault();
}
- }
+ };
- _getSelection() {
+ getSelection() {
const diffHosts = querySelectorAll(document.body, 'gr-diff');
if (!diffHosts.length) return document.getSelection();
@@ -219,13 +179,13 @@
* @param commentSelected Whether or not a comment is selected.
* @return The selected text.
*/
- _getSelectedText(side: Side, commentSelected: boolean) {
- const sel = this._getSelection();
+ getSelectedText(side: Side, commentSelected: boolean) {
+ const sel = this.getSelection();
if (!sel || sel.rangeCount !== 1) {
return ''; // No multi-select support yet.
}
if (commentSelected) {
- return this._getCommentLines(sel, side);
+ return this.getCommentLines(sel, side);
}
const range = normalize(sel.getRangeAt(0));
const startLineEl = getLineElByChild(range.startContainer);
@@ -250,7 +210,7 @@
if (endLineDataValue) endLineNum = Number(endLineDataValue);
}
- return this._getRangeFromDiff(
+ return this.getRangeFromDiff(
startLineNum,
range.startOffset,
endLineNum,
@@ -262,7 +222,7 @@
/**
* Query the diff object for the selected lines.
*/
- _getRangeFromDiff(
+ getRangeFromDiff(
startLineNum: number,
startOffset: number,
endLineNum: number | undefined,
@@ -274,7 +234,7 @@
startLineNum -= skipChunk.skip!;
if (endLineNum) endLineNum -= skipChunk.skip!;
}
- const lines = this._getDiffLines(side).slice(startLineNum - 1, endLineNum);
+ const lines = this.getDiffLines(side).slice(startLineNum - 1, endLineNum);
if (lines.length) {
lines[lines.length - 1] = lines[lines.length - 1].substring(0, endOffset);
lines[0] = lines[0].substring(startOffset);
@@ -288,9 +248,9 @@
* @param side The side that is currently selected.
* @return An array of strings indexed by line number.
*/
- _getDiffLines(side: Side): string[] {
- if (this._linesCache[side]) {
- return this._linesCache[side]!;
+ getDiffLines(side: Side): string[] {
+ if (this.linesCache[side]) {
+ return this.linesCache[side]!;
}
if (!this.diff) return [];
let lines: string[] = [];
@@ -303,7 +263,7 @@
lines = lines.concat(chunk.b);
}
}
- this._linesCache[side] = lines;
+ this.linesCache[side] = lines;
return lines;
}
@@ -315,11 +275,11 @@
* @param side The side that is currently selected.
* @return The selected comment text.
*/
- _getCommentLines(sel: Selection, side: Side) {
+ getCommentLines(sel: Selection, side: Side) {
const range = normalize(sel.getRangeAt(0));
const content = [];
- // Query the diffElement for comments.
- const messages = this.diffBuilder.diffElement.querySelectorAll(
+ assertIsDefined(this.diffTable, 'diffTable');
+ const messages = this.diffTable.querySelectorAll(
`.side-by-side [data-side="${side}"] .message *, .unified .message *`
);
@@ -339,9 +299,9 @@
if (
el.id === 'output' &&
- !this._elementDescendedFromClass(el, 'collapsed')
+ !descendedFromClass(el, 'collapsed', this.diffTable)
) {
- content.push(this._getTextContentForRange(el, sel, range));
+ content.push(this.getTextContentForRange(el, sel, range));
}
}
}
@@ -359,7 +319,7 @@
* @param range The normalized selection range.
* @return The text within the selection.
*/
- _getTextContentForRange(
+ getTextContentForRange(
domNode: Node,
sel: Selection,
range: NormalizedRange
@@ -379,15 +339,9 @@
}
} else {
for (const childNode of domNode.childNodes) {
- text += this._getTextContentForRange(childNode, sel, range);
+ text += this.getTextContentForRange(childNode, sel, range);
}
}
return text;
}
}
-
-declare global {
- interface HTMLElementTagNameMap {
- 'gr-diff-selection': GrDiffSelection;
- }
-}
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-selection/gr-diff-selection_html.ts b/polygerrit-ui/app/embed/diff/gr-diff-selection/gr-diff-selection_html.ts
deleted file mode 100644
index bd0e034..0000000
--- a/polygerrit-ui/app/embed/diff/gr-diff-selection/gr-diff-selection_html.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 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 {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <div class="contentWrapper">
- <slot></slot>
- </div>
-`;
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-selection/gr-diff-selection_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-selection/gr-diff-selection_test.ts
index dc7f6a2..b44114a 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-selection/gr-diff-selection_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-selection/gr-diff-selection_test.ts
@@ -5,121 +5,115 @@
*/
import '../../../test/common-test-setup-karma';
import './gr-diff-selection';
-import {html} from '@polymer/polymer/lib/utils/html-tag';
import {GrDiffSelection} from './gr-diff-selection';
-import {GrDiffBuilderElement} from '../gr-diff-builder/gr-diff-builder-element';
import {createDiff} from '../../../test/test-data-generators';
-import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
-import {Side} from '../../../api/diff';
+import {DiffInfo, Side} from '../../../api/diff';
import {GrFormattedText} from '../../../elements/shared/gr-formatted-text/gr-formatted-text';
+import {fixture, html} from '@open-wc/testing-helpers';
+import {mouseDown} from '../../../test/test-utils';
-const basicFixture = fixtureFromTemplate(html`
- <gr-diff-selection>
- <table id="diffTable" class="side-by-side">
- <tr class="diff-row">
- <td class="blame" data-line-number="1"></td>
- <td class="lineNum left" data-value="1">1</td>
- <td class="content">
- <div class="contentText" data-side="left">ba ba</div>
- <div data-side="left">
- <div class="comment-thread">
- <div class="gr-formatted-text message">
- <span id="output" class="gr-linked-text"
- >This is a comment</span
- >
- </div>
+const diffTableTemplate = html`
+ <table id="diffTable" class="side-by-side">
+ <tr class="diff-row">
+ <td class="blame" data-line-number="1"></td>
+ <td class="lineNum left" data-value="1">1</td>
+ <td class="content">
+ <div class="contentText" data-side="left">ba ba</div>
+ <div data-side="left">
+ <div class="comment-thread">
+ <div class="gr-formatted-text message">
+ <span id="output" class="gr-linked-text">This is a comment</span>
</div>
</div>
- </td>
- <td class="lineNum right" data-value="1">1</td>
- <td class="content">
- <div class="contentText" data-side="right">some other text</div>
- </td>
- </tr>
- <tr class="diff-row">
- <td class="blame" data-line-number="2"></td>
- <td class="lineNum left" data-value="2">2</td>
- <td class="content">
- <div class="contentText" data-side="left">zin</div>
- </td>
- <td class="lineNum right" data-value="2">2</td>
- <td class="content">
- <div class="contentText" data-side="right">more more more</div>
- <div data-side="right">
- <div class="comment-thread">
- <div class="gr-formatted-text message">
- <span id="output" class="gr-linked-text"
- >This is a comment on the right</span
- >
- </div>
+ </div>
+ </td>
+ <td class="lineNum right" data-value="1">1</td>
+ <td class="content">
+ <div class="contentText" data-side="right">some other text</div>
+ </td>
+ </tr>
+ <tr class="diff-row">
+ <td class="blame" data-line-number="2"></td>
+ <td class="lineNum left" data-value="2">2</td>
+ <td class="content">
+ <div class="contentText" data-side="left">zin</div>
+ </td>
+ <td class="lineNum right" data-value="2">2</td>
+ <td class="content">
+ <div class="contentText" data-side="right">more more more</div>
+ <div data-side="right">
+ <div class="comment-thread">
+ <div class="gr-formatted-text message">
+ <span id="output" class="gr-linked-text"
+ >This is a comment on the right</span
+ >
</div>
</div>
- </td>
- </tr>
- <tr class="diff-row">
- <td class="blame" data-line-number="3"></td>
- <td class="lineNum left" data-value="3">3</td>
- <td class="content">
- <div class="contentText" data-side="left">ga ga</div>
- <div data-side="left">
- <div class="comment-thread">
- <div class="gr-formatted-text message">
- <span id="output" class="gr-linked-text"
- >This is <a>a</a> different comment 💩 unicode is fun</span
- >
- </div>
+ </div>
+ </td>
+ </tr>
+ <tr class="diff-row">
+ <td class="blame" data-line-number="3"></td>
+ <td class="lineNum left" data-value="3">3</td>
+ <td class="content">
+ <div class="contentText" data-side="left">ga ga</div>
+ <div data-side="left">
+ <div class="comment-thread">
+ <div class="gr-formatted-text message">
+ <span id="output" class="gr-linked-text"
+ >This is <a>a</a> different comment 💩 unicode is fun</span
+ >
</div>
</div>
- </td>
- <td class="lineNum right" data-value="3">3</td>
- </tr>
- <tr class="diff-row">
- <td class="blame" data-line-number="4"></td>
- <td class="lineNum left" data-value="4">4</td>
- <td class="content">
- <div class="contentText" data-side="left">ga ga</div>
- <div data-side="left">
- <div class="comment-thread">
- <textarea data-side="right">test for textarea copying</textarea>
- </div>
+ </div>
+ </td>
+ <td class="lineNum right" data-value="3">3</td>
+ </tr>
+ <tr class="diff-row">
+ <td class="blame" data-line-number="4"></td>
+ <td class="lineNum left" data-value="4">4</td>
+ <td class="content">
+ <div class="contentText" data-side="left">ga ga</div>
+ <div data-side="left">
+ <div class="comment-thread">
+ <textarea data-side="right">test for textarea copying</textarea>
</div>
- </td>
- <td class="lineNum right" data-value="4">4</td>
- </tr>
- <tr class="not-diff-row">
- <td class="other">
- <div class="contentText" data-side="right">some other text</div>
- </td>
- </tr>
- </table>
- </gr-diff-selection>
-`);
+ </div>
+ </td>
+ <td class="lineNum right" data-value="4">4</td>
+ </tr>
+ <tr class="not-diff-row">
+ <td class="other">
+ <div class="contentText" data-side="right">some other text</div>
+ </td>
+ </tr>
+ </table>
+`;
suite('gr-diff-selection', () => {
let element: GrDiffSelection;
- let getCopyEventTargetStub: sinon.SinonStub;
+ let diffTable: HTMLTableElement;
const emulateCopyOn = function (target: HTMLElement | null) {
const fakeEvent = {
target,
preventDefault: sinon.stub(),
+ composedPath() {
+ return [target];
+ },
clipboardData: {
setData: sinon.stub(),
},
};
- getCopyEventTargetStub.returns(target);
- element._handleCopy(fakeEvent as unknown as ClipboardEvent);
+ element.handleCopy(fakeEvent as unknown as ClipboardEvent);
return fakeEvent;
};
- setup(() => {
- element = basicFixture.instantiate() as GrDiffSelection;
+ setup(async () => {
+ element = new GrDiffSelection();
+ diffTable = await fixture<HTMLTableElement>(diffTableTemplate);
- getCopyEventTargetStub = sinon.stub(element, '_getCopyEventTarget');
- element._cachedDiffBuilder = {
- diffElement: element.querySelector('#diffTable')!,
- } as GrDiffBuilderElement;
- element.diff = {
+ const diff: DiffInfo = {
...createDiff(),
content: [
{
@@ -136,228 +130,231 @@
},
],
};
+ element.init(diff, diffTable);
});
test('applies selected-left on left side click', () => {
- element.classList.add('selected-right');
- const lineNumberEl = element.querySelector('.lineNum.left');
- assert.isOk(lineNumberEl);
- MockInteractions.down(lineNumberEl!);
+ element.diffTable!.classList.add('selected-right');
+ const lineNumberEl = diffTable.querySelector<HTMLElement>('.lineNum.left');
+ if (!lineNumberEl) assert.fail('line number element missing');
+ mouseDown(lineNumberEl);
assert.isTrue(
- element.classList.contains('selected-left'),
+ element.diffTable!.classList.contains('selected-left'),
'adds selected-left'
);
assert.isFalse(
- element.classList.contains('selected-right'),
+ element.diffTable!.classList.contains('selected-right'),
'removes selected-right'
);
});
test('applies selected-right on right side click', () => {
- element.classList.add('selected-left');
- const lineNumberEl = element.querySelector('.lineNum.right');
- assert.isOk(lineNumberEl);
- MockInteractions.down(lineNumberEl!);
+ element.diffTable!.classList.add('selected-left');
+ const lineNumberEl = diffTable.querySelector<HTMLElement>('.lineNum.right');
+ if (!lineNumberEl) assert.fail('line number element missing');
+ mouseDown(lineNumberEl);
assert.isTrue(
- element.classList.contains('selected-right'),
+ element.diffTable!.classList.contains('selected-right'),
'adds selected-right'
);
assert.isFalse(
- element.classList.contains('selected-left'),
+ element.diffTable!.classList.contains('selected-left'),
'removes selected-left'
);
});
test('applies selected-blame on blame click', () => {
- element.classList.add('selected-left');
- sinon
- .stub(element, '_elementDescendedFromClass')
- .callsFake((_: Element, className: string) => className === 'blame');
- MockInteractions.down(element);
+ element.diffTable!.classList.add('selected-left');
+ const blameDiv = document.createElement('div');
+ blameDiv.classList.add('blame');
+ element.diffTable!.appendChild(blameDiv);
+ mouseDown(blameDiv);
assert.isTrue(
- element.classList.contains('selected-blame'),
+ element.diffTable!.classList.contains('selected-blame'),
'adds selected-right'
);
assert.isFalse(
- element.classList.contains('selected-left'),
+ element.diffTable!.classList.contains('selected-left'),
'removes selected-left'
);
});
test('ignores copy for non-content Element', () => {
- const getSelectedTextStub = sinon.stub(element, '_getSelectedText');
- emulateCopyOn(element.querySelector('.not-diff-row'));
+ const getSelectedTextStub = sinon.stub(element, 'getSelectedText');
+ emulateCopyOn(diffTable.querySelector('.not-diff-row'));
assert.isFalse(getSelectedTextStub.called);
});
test('asks for text for left side Elements', () => {
- const getSelectedTextStub = sinon.stub(element, '_getSelectedText');
- emulateCopyOn(element.querySelector('div.contentText'));
+ const getSelectedTextStub = sinon.stub(element, 'getSelectedText');
+ emulateCopyOn(diffTable.querySelector('div.contentText'));
assert.deepEqual([Side.LEFT, false], getSelectedTextStub.lastCall.args);
});
test('reacts to copy for content Elements', () => {
- const getSelectedTextStub = sinon.stub(element, '_getSelectedText');
- emulateCopyOn(element.querySelector('div.contentText'));
+ const getSelectedTextStub = sinon.stub(element, 'getSelectedText');
+ emulateCopyOn(diffTable.querySelector('div.contentText'));
assert.isTrue(getSelectedTextStub.called);
});
test('copy event is prevented for content Elements', () => {
- const getSelectedTextStub = sinon.stub(element, '_getSelectedText');
+ const getSelectedTextStub = sinon.stub(element, 'getSelectedText');
getSelectedTextStub.returns('test');
- const event = emulateCopyOn(element.querySelector('div.contentText'));
+ const event = emulateCopyOn(diffTable.querySelector('div.contentText'));
assert.isTrue(event.preventDefault.called);
});
test('inserts text into clipboard on copy', () => {
- sinon.stub(element, '_getSelectedText').returns('the text');
- const event = emulateCopyOn(element.querySelector('div.contentText'));
+ sinon.stub(element, 'getSelectedText').returns('the text');
+ const event = emulateCopyOn(diffTable.querySelector('div.contentText'));
assert.deepEqual(
['Text', 'the text'],
event.clipboardData.setData.lastCall.args
);
});
- test('_setClasses adds given SelectionClass values, removes others', () => {
- element.classList.add('selected-right');
- element._setClasses(['selected-comment', 'selected-left']);
- assert.isTrue(element.classList.contains('selected-comment'));
- assert.isTrue(element.classList.contains('selected-left'));
- assert.isFalse(element.classList.contains('selected-right'));
- assert.isFalse(element.classList.contains('selected-blame'));
+ test('setClasses adds given SelectionClass values, removes others', () => {
+ element.diffTable!.classList.add('selected-right');
+ element.setClasses(['selected-comment', 'selected-left']);
+ assert.isTrue(element.diffTable!.classList.contains('selected-comment'));
+ assert.isTrue(element.diffTable!.classList.contains('selected-left'));
+ assert.isFalse(element.diffTable!.classList.contains('selected-right'));
+ assert.isFalse(element.diffTable!.classList.contains('selected-blame'));
- element._setClasses(['selected-blame']);
- assert.isFalse(element.classList.contains('selected-comment'));
- assert.isFalse(element.classList.contains('selected-left'));
- assert.isFalse(element.classList.contains('selected-right'));
- assert.isTrue(element.classList.contains('selected-blame'));
+ element.setClasses(['selected-blame']);
+ assert.isFalse(element.diffTable!.classList.contains('selected-comment'));
+ assert.isFalse(element.diffTable!.classList.contains('selected-left'));
+ assert.isFalse(element.diffTable!.classList.contains('selected-right'));
+ assert.isTrue(element.diffTable!.classList.contains('selected-blame'));
});
- test('_setClasses removes before it ads', () => {
- element.classList.add('selected-right');
- const addStub = sinon.stub(element.classList, 'add');
- const removeStub = sinon.stub(element.classList, 'remove').callsFake(() => {
- assert.isFalse(addStub.called);
- });
- element._setClasses(['selected-comment', 'selected-left']);
+ test('setClasses removes before it ads', () => {
+ element.diffTable!.classList.add('selected-right');
+ const addStub = sinon.stub(element.diffTable!.classList, 'add');
+ const removeStub = sinon
+ .stub(element.diffTable!.classList, 'remove')
+ .callsFake(() => {
+ assert.isFalse(addStub.called);
+ });
+ element.setClasses(['selected-comment', 'selected-left']);
assert.isTrue(addStub.called);
assert.isTrue(removeStub.called);
});
test('copies content correctly', () => {
- element.classList.add('selected-left');
- element.classList.remove('selected-right');
+ element.diffTable!.classList.add('selected-left');
+ element.diffTable!.classList.remove('selected-right');
const selection = document.getSelection();
if (selection === null) assert.fail('no selection');
selection.removeAllRanges();
const range = document.createRange();
- range.setStart(element.querySelector('div.contentText')!.firstChild!, 3);
+ range.setStart(diffTable.querySelector('div.contentText')!.firstChild!, 3);
range.setEnd(
- element.querySelectorAll('div.contentText')[4]!.firstChild!,
+ diffTable.querySelectorAll('div.contentText')[4]!.firstChild!,
2
);
selection.addRange(range);
- assert.equal(element._getSelectedText(Side.LEFT, false), 'ba\nzin\nga');
+ assert.equal(element.getSelectedText(Side.LEFT, false), 'ba\nzin\nga');
});
test('copies comments', () => {
- element.classList.add('selected-left');
- element.classList.add('selected-comment');
- element.classList.remove('selected-right');
+ element.diffTable!.classList.add('selected-left');
+ element.diffTable!.classList.add('selected-comment');
+ element.diffTable!.classList.remove('selected-right');
const selection = document.getSelection();
if (selection === null) assert.fail('no selection');
selection.removeAllRanges();
const range = document.createRange();
range.setStart(
- element.querySelector('.gr-formatted-text *')!.firstChild!,
+ diffTable.querySelector('.gr-formatted-text *')!.firstChild!,
3
);
range.setEnd(
- element.querySelectorAll('.gr-formatted-text *')[2].childNodes[2],
+ diffTable.querySelectorAll('.gr-formatted-text *')[2].childNodes[2],
7
);
selection.addRange(range);
assert.equal(
's is a comment\nThis is a differ',
- element._getSelectedText(Side.LEFT, true)
+ element.getSelectedText(Side.LEFT, true)
);
});
test('respects astral chars in comments', () => {
- element.classList.add('selected-left');
- element.classList.add('selected-comment');
- element.classList.remove('selected-right');
+ element.diffTable!.classList.add('selected-left');
+ element.diffTable!.classList.add('selected-comment');
+ element.diffTable!.classList.remove('selected-right');
const selection = document.getSelection();
if (selection === null) assert.fail('no selection');
selection.removeAllRanges();
const range = document.createRange();
- const nodes = element.querySelectorAll('.gr-formatted-text *');
+ const nodes = diffTable.querySelectorAll('.gr-formatted-text *');
range.setStart(nodes[2].childNodes[2], 13);
range.setEnd(nodes[2].childNodes[2], 23);
selection.addRange(range);
- assert.equal('mment 💩 u', element._getSelectedText(Side.LEFT, true));
+ assert.equal('mment 💩 u', element.getSelectedText(Side.LEFT, true));
});
test('defers to default behavior for textarea', () => {
- element.classList.add('selected-left');
- element.classList.remove('selected-right');
- const selectedTextSpy = sinon.spy(element, '_getSelectedText');
- emulateCopyOn(element.querySelector('textarea'));
+ element.diffTable!.classList.add('selected-left');
+ element.diffTable!.classList.remove('selected-right');
+ const selectedTextSpy = sinon.spy(element, 'getSelectedText');
+ emulateCopyOn(diffTable.querySelector('textarea'));
assert.isFalse(selectedTextSpy.called);
});
test('regression test for 4794', () => {
- element.classList.add('selected-right');
- element.classList.remove('selected-left');
+ element.diffTable!.classList.add('selected-right');
+ element.diffTable!.classList.remove('selected-left');
const selection = document.getSelection();
if (!selection) assert.fail('no selection');
selection.removeAllRanges();
const range = document.createRange();
range.setStart(
- element.querySelectorAll('div.contentText')[1]!.firstChild!,
+ diffTable.querySelectorAll('div.contentText')[1]!.firstChild!,
4
);
range.setEnd(
- element.querySelectorAll('div.contentText')[1]!.firstChild!,
+ diffTable.querySelectorAll('div.contentText')[1]!.firstChild!,
10
);
selection.addRange(range);
- assert.equal(element._getSelectedText(Side.RIGHT, false), ' other');
+ assert.equal(element.getSelectedText(Side.RIGHT, false), ' other');
});
test('copies to end of side (issue 7895)', () => {
- element.classList.add('selected-left');
- element.classList.remove('selected-right');
+ element.diffTable!.classList.add('selected-left');
+ element.diffTable!.classList.remove('selected-right');
const selection = document.getSelection();
if (selection === null) assert.fail('no selection');
selection.removeAllRanges();
const range = document.createRange();
- range.setStart(element.querySelector('div.contentText')!.firstChild!, 3);
+ range.setStart(diffTable.querySelector('div.contentText')!.firstChild!, 3);
range.setEnd(
- element.querySelectorAll('div.contentText')[4]!.firstChild!,
+ diffTable.querySelectorAll('div.contentText')[4]!.firstChild!,
2
);
selection.addRange(range);
- assert.equal(element._getSelectedText(Side.LEFT, false), 'ba\nzin\nga');
+ assert.equal(element.getSelectedText(Side.LEFT, false), 'ba\nzin\nga');
});
- suite('_getTextContentForRange', () => {
+ suite('getTextContentForRange', () => {
let selection: Selection;
let range: Range;
let nodes: NodeListOf<GrFormattedText>;
setup(() => {
- element.classList.add('selected-left');
- element.classList.add('selected-comment');
- element.classList.remove('selected-right');
+ element.diffTable!.classList.add('selected-left');
+ element.diffTable!.classList.add('selected-comment');
+ element.diffTable!.classList.remove('selected-right');
const s = document.getSelection();
if (s === null) assert.fail('no selection');
selection = s;
selection.removeAllRanges();
range = document.createRange();
- nodes = element.querySelectorAll('.gr-formatted-text *');
+ nodes = diffTable.querySelectorAll('.gr-formatted-text *');
});
test('multi level element contained in range', () => {
@@ -365,7 +362,7 @@
range.setEnd(nodes[2].childNodes[2], 7);
selection.addRange(range);
assert.equal(
- element._getTextContentForRange(element, selection, range),
+ element.getTextContentForRange(diffTable, selection, range),
'his is a differ'
);
});
@@ -375,7 +372,7 @@
range.setEnd(nodes[2].childNodes[2], 7);
selection.addRange(range);
assert.equal(
- element._getTextContentForRange(element, selection, range),
+ element.getTextContentForRange(diffTable, selection, range),
'a differ'
);
});
@@ -385,16 +382,9 @@
range.setEnd(nodes[0].firstChild!, 12);
selection.addRange(range);
assert.equal(
- element._getTextContentForRange(element, selection, range),
+ element.getTextContentForRange(diffTable, selection, range),
'is is a co'
);
});
});
-
- test('cache is reset when diff changes', () => {
- element._linesCache = {left: ['test'], right: ['test']};
- element.diff = createDiff();
- flush();
- assert.deepEqual(element._linesCache, {left: null, right: null});
- });
});
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
index a38ec91..34c2a33 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
@@ -80,6 +80,7 @@
import {isSafari, toggleClass} from '../../../utils/dom-util';
import {assertIsDefined} from '../../../utils/common-util';
import {debounce, DelayedTask} from '../../../utils/async-util';
+import {GrDiffSelection} from '../gr-diff-selection/gr-diff-selection';
const NO_NEWLINE_LEFT = 'No newline at end of left file.';
const NO_NEWLINE_RIGHT = 'No newline at end of right file.';
@@ -99,7 +100,6 @@
export interface GrDiff {
$: {
- highlights: GrDiffHighlight;
diffBuilder: GrDiffBuilderElement;
diffTable: HTMLTableElement;
};
@@ -294,6 +294,10 @@
private renderDiffTableTask?: DelayedTask;
+ private diffSelection = new GrDiffSelection();
+
+ private highlights = new GrDiffHighlight();
+
constructor() {
super();
this._setLoading(true);
@@ -315,6 +319,8 @@
this.renderDiffTableTask?.cancel();
this._unobserveIncrementalNodes();
this._unobserveNodes();
+ this.diffSelection.cleanup();
+ this.highlights.cleanup();
super.disconnectedCallback();
}
@@ -357,7 +363,7 @@
// and pass the shadow DOM selection into gr-diff-highlight, where the
// corresponding range is determined and normalized.
const selection = this._getShadowOrDocumentSelection();
- this.$.highlights.handleSelectionChange(selection, false);
+ this.highlights.handleSelectionChange(selection, false);
};
private readonly handleMouseUp = () => {
@@ -365,7 +371,7 @@
// mouse-up if there's a selection that just covers a line change. We
// can't do that on selection change since the user may still be dragging.
const selection = this._getShadowOrDocumentSelection();
- this.$.highlights.handleSelectionChange(selection, true);
+ this.highlights.handleSelectionChange(selection, true);
};
/** Gets the current selection, preferring the shadow DOM selection. */
@@ -404,7 +410,7 @@
const range = getRange(threadEl);
if (!range) return undefined;
- return {side, range, hovering: false, rootId: threadEl.rootId};
+ return {side, range, rootId: threadEl.rootId};
}
// TODO(brohlfs): Rewrite `.map().filter() as ...` with `.reduce()` instead.
@@ -430,7 +436,6 @@
this.push('_commentRanges', {
side: Side.RIGHT,
range: this.highlightRange,
- hovering: true,
rootId: '',
});
}
@@ -498,7 +503,7 @@
}
isRangeSelected() {
- return !!this.$.highlights.selectedRange;
+ return !!this.highlights.selectedRange;
}
toggleLeftDiff() {
@@ -590,7 +595,7 @@
if (!this.isRangeSelected()) {
throw Error('Selection is needed for new range comment');
}
- const selectedRange = this.$.highlights.selectedRange;
+ const selectedRange = this.highlights.selectedRange;
if (!selectedRange) throw Error('selected range not set');
const {side, range} = selectedRange;
this._createCommentForSelection(side, range);
@@ -813,6 +818,10 @@
this._diffLength = this.getDiffLength(newValue);
this._debounceRenderDiffTable();
}
+ if (this.diff) {
+ this.diffSelection.init(this.diff, this.$.diffTable);
+ this.highlights.init(this.$.diffTable, this.$.diffBuilder);
+ }
}
/**
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_html.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_html.ts
index e05e85a..6d36b89 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_html.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_html.ts
@@ -487,6 +487,10 @@
color: var(--link-color);
padding: var(--spacing-m) 0 var(--spacing-m) 48px;
}
+ #diffTable {
+ /* for gr-selection-action-box positioning */
+ position: relative;
+ }
#diffTable:focus {
outline: none;
}
@@ -670,6 +674,14 @@
.token-highlight {
background-color: var(--token-highlighting-color, #fffd54);
}
+
+ gr-selection-action-box {
+ /**
+ * Needs z-index to appear above wrapped content, since it's inserted
+ * into DOM before it.
+ */
+ z-index: 10;
+ }
</style>
<style include="gr-syntax-theme">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
@@ -686,44 +698,36 @@
class$="[[_computeContainerClass(loggedIn, viewMode, displayLine)]]"
on-click="_handleTap"
>
- <gr-diff-selection diff="[[diff]]">
- <gr-diff-highlight
- id="highlights"
- logged-in="[[loggedIn]]"
- comment-ranges="{{_commentRanges}}"
- >
- <gr-diff-builder
- id="diffBuilder"
- comment-ranges="[[_commentRanges]]"
- coverage-ranges="[[coverageRanges]]"
- diff="[[diff]]"
- path="[[path]]"
- view-mode="[[viewMode]]"
- is-image-diff="[[isImageDiff]]"
- base-image="[[baseImage]]"
- layers="[[layers]]"
- revision-image="[[revisionImage]]"
- use-new-image-diff-ui="[[useNewImageDiffUi]]"
- >
- <table
- id="diffTable"
- class$="[[_diffTableClass]]"
- role="presentation"
- contenteditable$="[[isContentEditable]]"
- ></table>
+ <gr-diff-builder
+ id="diffBuilder"
+ comment-ranges="[[_commentRanges]]"
+ coverage-ranges="[[coverageRanges]]"
+ diff="[[diff]]"
+ path="[[path]]"
+ view-mode="[[viewMode]]"
+ is-image-diff="[[isImageDiff]]"
+ base-image="[[baseImage]]"
+ layers="[[layers]]"
+ revision-image="[[revisionImage]]"
+ use-new-image-diff-ui="[[useNewImageDiffUi]]"
+ >
+ <table
+ id="diffTable"
+ class$="[[_diffTableClass]]"
+ role="presentation"
+ contenteditable$="[[isContentEditable]]"
+ ></table>
- <template
- is="dom-if"
- if="[[showNoChangeMessage(_loading, prefs, _diffLength, diff)]]"
- >
- <div class="whitespace-change-only-message">
- This file only contains whitespace changes. Modify the whitespace
- setting to see the changes.
- </div>
- </template>
- </gr-diff-builder>
- </gr-diff-highlight>
- </gr-diff-selection>
+ <template
+ is="dom-if"
+ if="[[showNoChangeMessage(_loading, prefs, _diffLength, diff)]]"
+ >
+ <div class="whitespace-change-only-message">
+ This file only contains whitespace changes. Modify the whitespace
+ setting to see the changes.
+ </div>
+ </template>
+ </gr-diff-builder>
</div>
<div class$="[[_computeNewlineWarningClass(_newlineWarning, _loading)]]">
[[_newlineWarning]]
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.js b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.js
index ba4fa9f..c8d8a2f 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.js
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.js
@@ -50,21 +50,21 @@
setup(() => {
element = basicFixture.instantiate();
- sinon.stub(element.$.highlights, 'handleSelectionChange');
+ sinon.stub(element.highlights, 'handleSelectionChange');
});
test('enabled if logged in', async () => {
element.loggedIn = true;
emulateSelection();
await flush();
- assert.isTrue(element.$.highlights.handleSelectionChange.called);
+ assert.isTrue(element.highlights.handleSelectionChange.called);
});
test('ignored if logged out', async () => {
element.loggedIn = false;
emulateSelection();
await flush();
- assert.isFalse(element.$.highlights.handleSelectionChange.called);
+ assert.isFalse(element.highlights.handleSelectionChange.called);
});
});
diff --git a/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.ts b/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.ts
index 6c8a5e9..70cec64 100644
--- a/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.ts
+++ b/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.ts
@@ -20,7 +20,6 @@
export interface CommentRangeLayer {
side: Side;
range: CommentRange;
- hovering: boolean;
// New drafts don't have a rootId.
rootId?: string;
}
@@ -40,7 +39,6 @@
* highlights.
*/
interface CommentRangeLineLayer {
- hovering: boolean;
longRange: boolean;
id: string;
// start char (0-based)
@@ -59,7 +57,7 @@
const RANGE_BASE_ONLY = 'style-scope gr-diff range';
const RANGE_HIGHLIGHT = 'style-scope gr-diff range rangeHighlight';
-const HOVER_HIGHLIGHT = 'style-scope gr-diff range rangeHoverHighlight';
+// Note that there is also `rangeHoverHighlight` being set by GrDiffHighlight.
export class GrRangedCommentLayer implements DiffLayer {
private knownRanges: CommentRangeLayer[] = [];
@@ -95,11 +93,8 @@
el,
range.start,
range.end - range.start,
- (range.hovering
- ? HOVER_HIGHLIGHT
- : range.longRange
- ? RANGE_BASE_ONLY
- : RANGE_HIGHLIGHT) + ` ${strToClassName(range.id)}`
+ (range.longRange ? RANGE_BASE_ONLY : RANGE_HIGHLIGHT) +
+ ` ${strToClassName(range.id)}`
);
}
}
@@ -139,17 +134,15 @@
}
private addRange(commentRange: CommentRangeLayer) {
- const {side, range, hovering} = commentRange;
+ const {side, range} = commentRange;
const longRange = isLongCommentRange(range);
this.updateRangesMap({
side,
range,
- hovering,
- operation: (forLine, startChar, endChar, hovering) => {
+ operation: (forLine, startChar, endChar) => {
forLine.push({
start: startChar,
end: endChar,
- hovering,
id: id(commentRange),
longRange,
});
@@ -158,11 +151,10 @@
}
private removeRange(commentRange: CommentRangeLayer) {
- const {side, range, hovering} = commentRange;
+ const {side, range} = commentRange;
this.updateRangesMap({
side,
range,
- hovering,
operation: forLine => {
const index = forLine.findIndex(
lineRange => id(commentRange) === lineRange.id
@@ -175,21 +167,19 @@
private updateRangesMap(options: {
side: Side;
range: CommentRange;
- hovering: boolean;
operation: (
forLine: CommentRangeLineLayer[],
start: number,
- end: number,
- hovering: boolean
+ end: number
) => void;
}) {
- const {side, range, hovering, operation} = options;
+ const {side, range, operation} = options;
const forSide = this.rangesMap[side] || (this.rangesMap[side] = {});
for (let line = range.start_line; line <= range.end_line; line++) {
const forLine = forSide[line] || (forSide[line] = []);
const start = line === range.start_line ? range.start_character : 0;
const end = line === range.end_line ? range.end_character : -1;
- operation(forLine, start, end, hovering);
+ operation(forLine, start, end);
}
this.notifyUpdateRange(range.start_line, range.end_line, side);
}
@@ -199,25 +189,20 @@
const lineNum = side === Side.LEFT ? line.beforeNumber : line.afterNumber;
if (lineNum === 'FILE' || lineNum === 'LOST') return [];
const ranges: CommentRangeLineLayer[] = this.rangesMap[side][lineNum] || [];
- return (
- ranges
- .map(range => {
- // Make a copy, so that the normalization below does not mess with
- // our map.
- range = {...range};
- range.end = range.end === -1 ? line.text.length : range.end;
+ return ranges.map(range => {
+ // Make a copy, so that the normalization below does not mess with
+ // our map.
+ range = {...range};
+ range.end = range.end === -1 ? line.text.length : range.end;
- // Normalize invalid ranges where the start is after the end but the
- // start still makes sense. Set the end to the end of the line.
- // @see Issue 5744
- if (range.start >= range.end && range.start < line.text.length) {
- range.end = line.text.length;
- }
+ // Normalize invalid ranges where the start is after the end but the
+ // start still makes sense. Set the end to the end of the line.
+ // @see Issue 5744
+ if (range.start >= range.end && range.start < line.text.length) {
+ range.end = line.text.length;
+ }
- return range;
- })
- // Sort the ranges so that hovering highlights are on top.
- .sort((a, b) => (a.hovering && !b.hovering ? 1 : 0))
- );
+ return range;
+ });
}
}
diff --git a/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.ts b/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.ts
index 4e35645..15d14e3 100644
--- a/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.ts
@@ -24,7 +24,6 @@
start_line: 36,
},
rootId: 'a',
- hovering: false,
};
const rangeB: CommentRangeLayer = {
@@ -36,7 +35,6 @@
start_line: 10,
},
rootId: 'b',
- hovering: false,
};
const rangeC: CommentRangeLayer = {
@@ -47,7 +45,6 @@
start_character: 5,
start_line: 100,
},
- hovering: false,
};
const rangeD: CommentRangeLayer = {
@@ -59,7 +56,6 @@
start_line: 55,
},
rootId: 'd',
- hovering: false,
};
const rangeE: CommentRangeLayer = {
@@ -70,7 +66,6 @@
start_character: 1,
start_line: 60,
},
- hovering: false,
};
suite('gr-ranged-comment-layer', () => {
diff --git a/polygerrit-ui/app/test/test-data-generators.ts b/polygerrit-ui/app/test/test-data-generators.ts
index cc292e6..2fffc9a 100644
--- a/polygerrit-ui/app/test/test-data-generators.ts
+++ b/polygerrit-ui/app/test/test-data-generators.ts
@@ -33,6 +33,7 @@
CommentInfo,
CommentLinkInfo,
CommentLinks,
+ CommentRange,
CommitId,
CommitInfo,
ConfigInfo,
@@ -652,6 +653,15 @@
};
}
+export function createRange(): CommentRange {
+ return {
+ start_line: 1,
+ start_character: 0,
+ end_line: 1,
+ end_character: 1,
+ };
+}
+
export function createComment(
extra: Partial<CommentInfo | DraftInfo> = {}
): CommentInfo {
diff --git a/polygerrit-ui/app/test/test-utils.ts b/polygerrit-ui/app/test/test-utils.ts
index 0c63de0..985bec1 100644
--- a/polygerrit-ui/app/test/test-utils.ts
+++ b/polygerrit-ui/app/test/test-utils.ts
@@ -296,6 +296,19 @@
element.dispatchEvent(new KeyboardEvent('keydown', eventOptions));
}
+export function mouseDown(element: HTMLElement) {
+ const rect = element.getBoundingClientRect();
+ const eventOptions = {
+ bubbles: true,
+ composed: true,
+ clientX: (rect.left + rect.right) / 2,
+ clientY: (rect.top + rect.bottom) / 2,
+ screenX: (rect.left + rect.right) / 2,
+ screenY: (rect.top + rect.bottom) / 2,
+ };
+ element.dispatchEvent(new MouseEvent('mousedown', eventOptions));
+}
+
export function assertFails(promise: Promise<unknown>, error?: unknown) {
promise
.then((_v: unknown) => {