302 lines
9.5 KiB
JavaScript
302 lines
9.5 KiB
JavaScript
// 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.
|
|
(function() {
|
|
'use strict';
|
|
|
|
/**
|
|
* Possible CSS classes indicating the state of selection. Dynamically added/
|
|
* removed based on where the user clicks within the diff.
|
|
*/
|
|
const SelectionClass = {
|
|
COMMENT: 'selected-comment',
|
|
LEFT: 'selected-left',
|
|
RIGHT: 'selected-right',
|
|
BLAME: 'selected-blame',
|
|
};
|
|
|
|
const getNewCache = () => { return {left: null, right: null}; };
|
|
|
|
Polymer({
|
|
is: 'gr-diff-selection',
|
|
|
|
properties: {
|
|
diff: Object,
|
|
/** @type {?Object} */
|
|
_cachedDiffBuilder: Object,
|
|
_linesCache: {
|
|
type: Object,
|
|
value: getNewCache(),
|
|
},
|
|
},
|
|
|
|
observers: [
|
|
'_diffChanged(diff)',
|
|
],
|
|
|
|
listeners: {
|
|
copy: '_handleCopy',
|
|
down: '_handleDown',
|
|
},
|
|
|
|
behaviors: [
|
|
Gerrit.DomUtilBehavior,
|
|
],
|
|
|
|
attached() {
|
|
this.classList.add(SelectionClass.RIGHT);
|
|
},
|
|
|
|
get diffBuilder() {
|
|
if (!this._cachedDiffBuilder) {
|
|
this._cachedDiffBuilder =
|
|
Polymer.dom(this).querySelector('gr-diff-builder');
|
|
}
|
|
return this._cachedDiffBuilder;
|
|
},
|
|
|
|
_diffChanged() {
|
|
this._linesCache = getNewCache();
|
|
},
|
|
|
|
_handleDown(e) {
|
|
const lineEl = this.diffBuilder.getLineElByChild(e.target);
|
|
const blameSelected = this._elementDescendedFromClass(e.target, 'blame');
|
|
if (!lineEl && !blameSelected) { return; }
|
|
|
|
const targetClasses = [];
|
|
|
|
if (blameSelected) {
|
|
targetClasses.push(SelectionClass.BLAME);
|
|
} else {
|
|
const commentSelected =
|
|
this._elementDescendedFromClass(e.target, 'gr-diff-comment');
|
|
const side = this.diffBuilder.getSideByLineEl(lineEl);
|
|
|
|
targetClasses.push(side === 'left' ?
|
|
SelectionClass.LEFT :
|
|
SelectionClass.RIGHT);
|
|
|
|
if (commentSelected) {
|
|
targetClasses.push(SelectionClass.COMMENT);
|
|
}
|
|
}
|
|
|
|
this._setClasses(targetClasses);
|
|
},
|
|
|
|
/**
|
|
* Set the provided list of classes on the element, to the exclusion of all
|
|
* other SelectionClass values.
|
|
* @param {!Array<!string>} targetClasses
|
|
*/
|
|
_setClasses(targetClasses) {
|
|
// Remove any selection classes that do not belong.
|
|
for (const key in SelectionClass) {
|
|
if (SelectionClass.hasOwnProperty(key)) {
|
|
const className = SelectionClass[key];
|
|
if (!targetClasses.includes(className)) {
|
|
this.classList.remove(SelectionClass[key]);
|
|
}
|
|
}
|
|
}
|
|
// Add new selection classes iff they are not already present.
|
|
for (const _class of targetClasses) {
|
|
if (!this.classList.contains(_class)) {
|
|
this.classList.add(_class);
|
|
}
|
|
}
|
|
},
|
|
|
|
_getCopyEventTarget(e) {
|
|
return Polymer.dom(e).rootTarget;
|
|
},
|
|
|
|
/**
|
|
* Utility function to determine whether an element is a descendant of
|
|
* another element with the particular className.
|
|
*
|
|
* @param {!Element} element
|
|
* @param {!string} className
|
|
* @return {boolean}
|
|
*/
|
|
_elementDescendedFromClass(element, className) {
|
|
return this.descendedFromClass(element, className,
|
|
this.diffBuilder.diffElement);
|
|
},
|
|
|
|
_handleCopy(e) {
|
|
let commentSelected = false;
|
|
const target = this._getCopyEventTarget(e);
|
|
if (target.type === 'textarea') { return; }
|
|
if (!this._elementDescendedFromClass(target, 'diff-row')) { return; }
|
|
if (this.classList.contains(SelectionClass.COMMENT)) {
|
|
commentSelected = true;
|
|
}
|
|
const lineEl = this.diffBuilder.getLineElByChild(target);
|
|
if (!lineEl) {
|
|
return;
|
|
}
|
|
const side = this.diffBuilder.getSideByLineEl(lineEl);
|
|
const text = this._getSelectedText(side, commentSelected);
|
|
if (text) {
|
|
e.clipboardData.setData('Text', text);
|
|
e.preventDefault();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Get the text of the current window selection. If commentSelected is
|
|
* true, it returns only the text of comments within the selection.
|
|
* Otherwise it returns the text of the selected diff region.
|
|
*
|
|
* @param {!string} side The side that is selected.
|
|
* @param {boolean} commentSelected Whether or not a comment is selected.
|
|
* @return {string} The selected text.
|
|
*/
|
|
_getSelectedText(side, commentSelected) {
|
|
const sel = window.getSelection();
|
|
if (sel.rangeCount != 1) {
|
|
return ''; // No multi-select support yet.
|
|
}
|
|
if (commentSelected) {
|
|
return this._getCommentLines(sel, side);
|
|
}
|
|
const range = GrRangeNormalizer.normalize(sel.getRangeAt(0));
|
|
const startLineEl =
|
|
this.diffBuilder.getLineElByChild(range.startContainer);
|
|
const endLineEl = this.diffBuilder.getLineElByChild(range.endContainer);
|
|
const startLineNum = parseInt(startLineEl.getAttribute('data-value'), 10);
|
|
const endLineNum = endLineEl === null ?
|
|
undefined :
|
|
parseInt(endLineEl.getAttribute('data-value'), 10);
|
|
|
|
return this._getRangeFromDiff(startLineNum, range.startOffset, endLineNum,
|
|
range.endOffset, side);
|
|
},
|
|
|
|
/**
|
|
* Query the diff object for the selected lines.
|
|
*
|
|
* @param {number} startLineNum
|
|
* @param {number} startOffset
|
|
* @param {number|undefined} endLineNum Use undefined to get the range
|
|
* extending to the end of the file.
|
|
* @param {number} endOffset
|
|
* @param {!string} side The side that is currently selected.
|
|
* @return {string} The selected diff text.
|
|
*/
|
|
_getRangeFromDiff(startLineNum, startOffset, endLineNum, endOffset, side) {
|
|
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);
|
|
}
|
|
return lines.join('\n');
|
|
},
|
|
|
|
/**
|
|
* Query the diff object for the lines from a particular side.
|
|
*
|
|
* @param {!string} side The side that is currently selected.
|
|
* @return {!Array<string>} An array of strings indexed by line number.
|
|
*/
|
|
_getDiffLines(side) {
|
|
if (this._linesCache[side]) {
|
|
return this._linesCache[side];
|
|
}
|
|
let lines = [];
|
|
const key = side === 'left' ? 'a' : 'b';
|
|
for (const chunk of this.diff.content) {
|
|
if (chunk.ab) {
|
|
lines = lines.concat(chunk.ab);
|
|
} else if (chunk[key]) {
|
|
lines = lines.concat(chunk[key]);
|
|
}
|
|
}
|
|
this._linesCache[side] = lines;
|
|
return lines;
|
|
},
|
|
|
|
/**
|
|
* Query the diffElement for comments and check whether they lie inside the
|
|
* selection range.
|
|
*
|
|
* @param {!Selection} sel The selection of the window.
|
|
* @param {!string} side The side that is currently selected.
|
|
* @return {string} The selected comment text.
|
|
*/
|
|
_getCommentLines(sel, side) {
|
|
const range = GrRangeNormalizer.normalize(sel.getRangeAt(0));
|
|
const content = [];
|
|
// Query the diffElement for comments.
|
|
const messages = this.diffBuilder.diffElement.querySelectorAll(
|
|
`.side-by-side [data-side="${side
|
|
}"] .message *, .unified .message *`);
|
|
|
|
for (let i = 0; i < messages.length; i++) {
|
|
const el = messages[i];
|
|
// Check if the comment element exists inside the selection.
|
|
if (sel.containsNode(el, true)) {
|
|
// Padded elements require newlines for accurate spacing.
|
|
if (el.parentElement.id === 'container' ||
|
|
el.parentElement.nodeName === 'BLOCKQUOTE') {
|
|
if (content.length && content[content.length - 1] !== '') {
|
|
content.push('');
|
|
}
|
|
}
|
|
|
|
if (el.id === 'output' &&
|
|
!this._elementDescendedFromClass(el, 'collapsed')) {
|
|
content.push(this._getTextContentForRange(el, sel, range));
|
|
}
|
|
}
|
|
}
|
|
|
|
return content.join('\n');
|
|
},
|
|
|
|
/**
|
|
* Given a DOM node, a selection, and a selection range, recursively get all
|
|
* of the text content within that selection.
|
|
* Using a domNode that isn't in the selection returns an empty string.
|
|
*
|
|
* @param {!Node} domNode The root DOM node.
|
|
* @param {!Selection} sel The selection.
|
|
* @param {!Range} range The normalized selection range.
|
|
* @return {string} The text within the selection.
|
|
*/
|
|
_getTextContentForRange(domNode, sel, range) {
|
|
if (!sel.containsNode(domNode, true)) { return ''; }
|
|
|
|
let text = '';
|
|
if (domNode instanceof Text) {
|
|
text = domNode.textContent;
|
|
if (domNode === range.endContainer) {
|
|
text = text.substring(0, range.endOffset);
|
|
}
|
|
if (domNode === range.startContainer) {
|
|
text = text.substring(range.startOffset);
|
|
}
|
|
} else {
|
|
for (const childNode of domNode.childNodes) {
|
|
text += this._getTextContentForRange(childNode, sel, range);
|
|
}
|
|
}
|
|
return text;
|
|
},
|
|
});
|
|
})();
|