
At the moment, gr-diff-highlight react to selection change event, so if there are multiple instances on the page, for example in gr-change-view, all instances react to selection. This change makes gr-diff-highlight to ignore selections for other instances, thus reacting only its own selection. Bug: Issue 5504 Change-Id: I9f0e1dc7fe9f316400d9c96fb0c8c5f7ff78d779
326 lines
10 KiB
JavaScript
326 lines
10 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';
|
|
|
|
Polymer({
|
|
is: 'gr-diff-highlight',
|
|
|
|
properties: {
|
|
comments: Object,
|
|
loggedIn: Boolean,
|
|
_cachedDiffBuilder: Object,
|
|
isAttached: Boolean,
|
|
},
|
|
|
|
listeners: {
|
|
'comment-mouse-out': '_handleCommentMouseOut',
|
|
'comment-mouse-over': '_handleCommentMouseOver',
|
|
'create-comment': '_createComment',
|
|
},
|
|
|
|
observers: [
|
|
'_enableSelectionObserver(loggedIn, isAttached)',
|
|
],
|
|
|
|
get diffBuilder() {
|
|
if (!this._cachedDiffBuilder) {
|
|
this._cachedDiffBuilder =
|
|
Polymer.dom(this).querySelector('gr-diff-builder');
|
|
}
|
|
return this._cachedDiffBuilder;
|
|
},
|
|
|
|
_enableSelectionObserver: function(loggedIn, isAttached) {
|
|
if (loggedIn && isAttached) {
|
|
this.listen(document, 'selectionchange', '_handleSelectionChange');
|
|
} else {
|
|
this.unlisten(document, 'selectionchange', '_handleSelectionChange');
|
|
}
|
|
},
|
|
|
|
isRangeSelected: function() {
|
|
return !!this.$$('gr-selection-action-box');
|
|
},
|
|
|
|
_handleSelectionChange: function() {
|
|
// Can't use up or down events to handle selection started and/or ended in
|
|
// in comment threads or outside of diff.
|
|
// Debounce removeActionBox to give it a chance to react to click/tap.
|
|
this._removeActionBoxDebounced();
|
|
this.debounce('selectionChange', this._handleSelection, 200);
|
|
},
|
|
|
|
_handleCommentMouseOver: function(e) {
|
|
var comment = e.detail.comment;
|
|
if (!comment.range) { return; }
|
|
var lineEl = this.diffBuilder.getLineElByChild(e.target);
|
|
var side = this.diffBuilder.getSideByLineEl(lineEl);
|
|
var index = this._indexOfComment(side, comment);
|
|
if (index !== undefined) {
|
|
this.set(['comments', side, index, '__hovering'], true);
|
|
}
|
|
},
|
|
|
|
_handleCommentMouseOut: function(e) {
|
|
var comment = e.detail.comment;
|
|
if (!comment.range) { return; }
|
|
var lineEl = this.diffBuilder.getLineElByChild(e.target);
|
|
var side = this.diffBuilder.getSideByLineEl(lineEl);
|
|
var index = this._indexOfComment(side, comment);
|
|
if (index !== undefined) {
|
|
this.set(['comments', side, index, '__hovering'], false);
|
|
}
|
|
},
|
|
|
|
_indexOfComment: function(side, comment) {
|
|
var idProp = comment.id ? 'id' : '__draftID';
|
|
for (var i = 0; i < this.comments[side].length; i++) {
|
|
if (comment[idProp] &&
|
|
this.comments[side][i][idProp] === comment[idProp]) {
|
|
return i;
|
|
}
|
|
}
|
|
},
|
|
|
|
_normalizeRange: function(domRange) {
|
|
var range = GrRangeNormalizer.normalize(domRange);
|
|
return this._fixTripleClickSelection({
|
|
start: this._normalizeSelectionSide(
|
|
range.startContainer, range.startOffset),
|
|
end: this._normalizeSelectionSide(
|
|
range.endContainer, range.endOffset),
|
|
}, domRange);
|
|
},
|
|
|
|
/**
|
|
* Adjust triple click selection for the whole line.
|
|
* domRange.endContainer may be one of the following:
|
|
* 1) 0 offset at right column's line number cell, or
|
|
* 2) 0 offset at left column's line number at the next line.
|
|
* Case 1 means left column was triple clicked.
|
|
* Case 2 means right column or unified view triple clicked.
|
|
* @param {!Object} range Normalized range, ie column/line numbers
|
|
* @param {!Range} domRange DOM Range object
|
|
* @return {!Object} fixed normalized range
|
|
*/
|
|
_fixTripleClickSelection: function(range, domRange) {
|
|
if (!range.start) {
|
|
// Selection outside of current diff.
|
|
return range;
|
|
}
|
|
var start = range.start;
|
|
var end = range.end;
|
|
var endsAtOtherSideLineNum =
|
|
domRange.endOffset === 0 &&
|
|
domRange.endContainer.nodeName === 'TD' &&
|
|
(domRange.endContainer.classList.contains('left') ||
|
|
domRange.endContainer.classList.contains('right'));
|
|
var endsOnOtherSideStart = endsAtOtherSideLineNum ||
|
|
end &&
|
|
end.column === 0 &&
|
|
end.line === start.line &&
|
|
end.side != start.side;
|
|
if (endsOnOtherSideStart || endsAtOtherSideLineNum) {
|
|
// Selection ends at the beginning of the next line.
|
|
// Move the selection to the end of the previous line.
|
|
range.end = {
|
|
node: start.node,
|
|
column: this._getLength(
|
|
domRange.cloneContents().querySelector('.contentText')),
|
|
side: start.side,
|
|
line: start.line,
|
|
};
|
|
}
|
|
return range;
|
|
},
|
|
|
|
/**
|
|
* Convert DOM Range selection to concrete numbers (line, column, side).
|
|
* Moves range end if it's not inside td.content.
|
|
* Returns null if selection end is not valid (outside of diff).
|
|
*
|
|
* @param {Node} node td.content child
|
|
* @param {number} offset offset within node
|
|
* @return {{
|
|
* node: Node,
|
|
* side: string,
|
|
* line: Number,
|
|
* column: Number
|
|
* }}
|
|
*/
|
|
_normalizeSelectionSide: function(node, offset) {
|
|
var column;
|
|
if (!this.contains(node)) {
|
|
return;
|
|
}
|
|
var lineEl = this.diffBuilder.getLineElByChild(node);
|
|
if (!lineEl) {
|
|
return;
|
|
}
|
|
var side = this.diffBuilder.getSideByLineEl(lineEl);
|
|
if (!side) {
|
|
return;
|
|
}
|
|
var line = this.diffBuilder.getLineNumberByChild(lineEl);
|
|
if (!line) {
|
|
return;
|
|
}
|
|
var contentText = this.diffBuilder.getContentByLineEl(lineEl);
|
|
if (!contentText) {
|
|
return;
|
|
}
|
|
var contentTd = contentText.parentElement;
|
|
if (!contentTd.contains(node)) {
|
|
node = contentText;
|
|
column = 0;
|
|
} else {
|
|
var thread = contentTd.querySelector('gr-diff-comment-thread');
|
|
if (thread && thread.contains(node)) {
|
|
column = this._getLength(contentText);
|
|
node = contentText;
|
|
} else {
|
|
column = this._convertOffsetToColumn(node, offset);
|
|
}
|
|
}
|
|
|
|
return {
|
|
node: node,
|
|
side: side,
|
|
line: line,
|
|
column: column,
|
|
};
|
|
},
|
|
|
|
_handleSelection: function() {
|
|
var selection = window.getSelection();
|
|
if (selection.rangeCount != 1) {
|
|
return;
|
|
}
|
|
var range = selection.getRangeAt(0);
|
|
if (range.collapsed) {
|
|
return;
|
|
}
|
|
var normalizedRange = this._normalizeRange(range);
|
|
var start = normalizedRange.start;
|
|
if (!start) {
|
|
return;
|
|
}
|
|
var end = normalizedRange.end;
|
|
if (!end) {
|
|
return;
|
|
}
|
|
if (start.side !== end.side ||
|
|
end.line < start.line ||
|
|
(start.line === end.line && start.column === end.column)) {
|
|
return;
|
|
}
|
|
|
|
// TODO (viktard): Drop empty first and last lines from selection.
|
|
|
|
var actionBox = document.createElement('gr-selection-action-box');
|
|
Polymer.dom(this.root).appendChild(actionBox);
|
|
actionBox.range = {
|
|
startLine: start.line,
|
|
startChar: start.column,
|
|
endLine: end.line,
|
|
endChar: end.column,
|
|
};
|
|
actionBox.side = start.side;
|
|
if (start.line === end.line) {
|
|
actionBox.placeAbove(range);
|
|
} else if (start.node instanceof Text) {
|
|
actionBox.placeAbove(start.node.splitText(start.column));
|
|
start.node.parentElement.normalize(); // Undo splitText from above.
|
|
} else if (start.node.classList.contains('content') &&
|
|
start.node.firstChild) {
|
|
actionBox.placeAbove(start.node.firstChild);
|
|
} else {
|
|
actionBox.placeAbove(start.node);
|
|
}
|
|
},
|
|
|
|
_createComment: function(e) {
|
|
this._removeActionBox();
|
|
},
|
|
|
|
_removeActionBoxDebounced: function() {
|
|
this.debounce('removeActionBox', this._removeActionBox, 10);
|
|
},
|
|
|
|
_removeActionBox: function() {
|
|
var actionBox = this.$$('gr-selection-action-box');
|
|
if (actionBox) {
|
|
Polymer.dom(this.root).removeChild(actionBox);
|
|
}
|
|
},
|
|
|
|
_convertOffsetToColumn: function(el, offset) {
|
|
if (el instanceof Element && el.classList.contains('content')) {
|
|
return offset;
|
|
}
|
|
while (el.previousSibling ||
|
|
!el.parentElement.classList.contains('content')) {
|
|
if (el.previousSibling) {
|
|
el = el.previousSibling;
|
|
offset += this._getLength(el);
|
|
} else {
|
|
el = el.parentElement;
|
|
}
|
|
}
|
|
return offset;
|
|
},
|
|
|
|
/**
|
|
* Traverse Element from right to left, call callback for each node.
|
|
* Stops if callback returns true.
|
|
*
|
|
* @param {!Node} startNode
|
|
* @param {function(Node):boolean} callback
|
|
* @param {Object=} opt_flags If flags.left is true, traverse left.
|
|
*/
|
|
_traverseContentSiblings: function(startNode, callback, opt_flags) {
|
|
var travelLeft = opt_flags && opt_flags.left;
|
|
var node = startNode;
|
|
while (node) {
|
|
if (node instanceof Element &&
|
|
node.tagName !== 'HL' &&
|
|
node.tagName !== 'SPAN') {
|
|
break;
|
|
}
|
|
var nextNode = travelLeft ? node.previousSibling : node.nextSibling;
|
|
if (callback(node)) {
|
|
break;
|
|
}
|
|
node = nextNode;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Get length of a node. If the node is a content node, then only give the
|
|
* length of its .contentText child.
|
|
*
|
|
* @param {!Node} node
|
|
* @return {number}
|
|
*/
|
|
_getLength: function(node) {
|
|
if (node instanceof Element && node.classList.contains('content')) {
|
|
return this._getLength(node.querySelector('.contentText'));
|
|
} else {
|
|
return GrAnnotation.getLength(node);
|
|
}
|
|
},
|
|
});
|
|
})();
|