512 lines
17 KiB
JavaScript
512 lines
17 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';
|
|
|
|
// Astral code point as per https://mathiasbynens.be/notes/javascript-unicode
|
|
var REGEX_ASTRAL_SYMBOL = /[\uD800-\uDBFF][\uDC00-\uDFFF]/;
|
|
var RANGE_HIGHLIGHT = 'range';
|
|
var HOVER_HIGHLIGHT = 'rangeHighlight';
|
|
|
|
Polymer({
|
|
is: 'gr-diff-highlight',
|
|
|
|
properties: {
|
|
comments: Object,
|
|
enabled: {
|
|
type: Boolean,
|
|
observer: '_enabledChanged',
|
|
},
|
|
loggedIn: Boolean,
|
|
_cachedDiffBuilder: Object,
|
|
_enabledListeners: {
|
|
type: Object,
|
|
value: function() {
|
|
return {
|
|
'comment-discard': '_handleCommentDiscard',
|
|
'comment-mouse-out': '_handleCommentMouseOut',
|
|
'comment-mouse-over': '_handleCommentMouseOver',
|
|
'create-comment': '_createComment',
|
|
'render': '_handleRender',
|
|
'show-context': '_handleShowContext',
|
|
'thread-discard': '_handleThreadDiscard',
|
|
};
|
|
},
|
|
},
|
|
},
|
|
|
|
get diffBuilder() {
|
|
if (!this._cachedDiffBuilder) {
|
|
this._cachedDiffBuilder =
|
|
Polymer.dom(this).querySelector('gr-diff-builder');
|
|
}
|
|
return this._cachedDiffBuilder;
|
|
},
|
|
|
|
detached: function() {
|
|
this.enabled = false;
|
|
},
|
|
|
|
_enabledChanged: function() {
|
|
for (var eventName in this._enabledListeners) {
|
|
var methodName = this._enabledListeners[eventName];
|
|
if (this.enabled) {
|
|
this.listen(this, eventName, methodName);
|
|
} else {
|
|
this.unlisten(this, eventName, methodName);
|
|
}
|
|
}
|
|
},
|
|
|
|
isRangeSelected: function() {
|
|
return !!this.$$('gr-selection-action-box');
|
|
},
|
|
|
|
_handleThreadDiscard: function(e) {
|
|
var comment = e.detail.lastComment;
|
|
// Comment Element was removed from DOM already.
|
|
if (comment.range) {
|
|
this._renderCommentRange(comment, e.target);
|
|
}
|
|
},
|
|
|
|
_handleCommentDiscard: function(e) {
|
|
var comment = e.detail.comment;
|
|
if (comment.range) {
|
|
this._renderCommentRange(comment, e.target);
|
|
}
|
|
},
|
|
|
|
_handleRender: function() {
|
|
this._applyAllHighlights();
|
|
},
|
|
|
|
_handleShowContext: function() {
|
|
// TODO (viktard): Re-render expanded sections only.
|
|
this._applyAllHighlights();
|
|
},
|
|
|
|
_handleCommentMouseOver: function(e) {
|
|
var comment = e.detail.comment;
|
|
var range = comment.range;
|
|
if (!range) {
|
|
return;
|
|
}
|
|
var lineEl = this.diffBuilder.getLineElByChild(e.target);
|
|
var side = this.diffBuilder.getSideByLineEl(lineEl);
|
|
this._applyRangedHighlight(
|
|
HOVER_HIGHLIGHT, range.start_line, range.start_character,
|
|
range.end_line, range.end_character, side);
|
|
},
|
|
|
|
_handleCommentMouseOut: function(e) {
|
|
var comment = e.detail.comment;
|
|
var range = comment.range;
|
|
if (!range) {
|
|
return;
|
|
}
|
|
var lineEl = this.diffBuilder.getLineElByChild(e.target);
|
|
var side = this.diffBuilder.getSideByLineEl(lineEl);
|
|
var contentEls = this.diffBuilder.getContentsByLineRange(
|
|
range.start_line, range.end_line, side);
|
|
contentEls.forEach(function(content) {
|
|
Polymer.dom(content).querySelectorAll('.' + HOVER_HIGHLIGHT).forEach(
|
|
function(el) {
|
|
el.classList.remove(HOVER_HIGHLIGHT);
|
|
el.classList.add(RANGE_HIGHLIGHT);
|
|
});
|
|
}, this);
|
|
},
|
|
|
|
_renderCommentRange: function(comment, el) {
|
|
var lineEl = this.diffBuilder.getLineElByChild(el);
|
|
if (!lineEl) {
|
|
return;
|
|
}
|
|
var side = this.diffBuilder.getSideByLineEl(lineEl);
|
|
this._rerenderByLines(
|
|
comment.range.start_line, comment.range.end_line, side);
|
|
},
|
|
|
|
_createComment: function(e) {
|
|
this._removeActionBox();
|
|
var side = e.detail.side;
|
|
var range = e.detail.range;
|
|
if (!range) {
|
|
return;
|
|
}
|
|
var lineEl = this.diffBuilder.getLineElByChild(e.target);
|
|
var side = this.diffBuilder.getSideByLineEl(lineEl);
|
|
var contentEls = this.diffBuilder.getContentsByLineRange(
|
|
range.start_line, range.end_line, side);
|
|
contentEls.forEach(function(content) {
|
|
Polymer.dom(content).querySelectorAll('.' + HOVER_HIGHLIGHT).forEach(
|
|
function(el) {
|
|
el.classList.remove(HOVER_HIGHLIGHT);
|
|
el.classList.add(RANGE_HIGHLIGHT);
|
|
});
|
|
}, this);
|
|
},
|
|
|
|
_renderCommentRange: function(comment, el) {
|
|
var lineEl = this.diffBuilder.getLineElByChild(el);
|
|
if (!lineEl) {
|
|
return;
|
|
}
|
|
var side = this.diffBuilder.getSideByLineEl(lineEl);
|
|
this._rerenderByLines(
|
|
comment.range.start_line, comment.range.end_line, side);
|
|
},
|
|
|
|
_createComment: function(e) {
|
|
this._removeActionBox();
|
|
var side = e.detail.side;
|
|
var range = e.detail.range;
|
|
if (!range) {
|
|
return;
|
|
}
|
|
this._applyRangedHighlight(
|
|
RANGE_HIGHLIGHT, range.startLine, range.startChar,
|
|
range.endLine, range.endChar, side);
|
|
},
|
|
|
|
_removeActionBox: function() {
|
|
var actionBox = this.$$('gr-selection-action-box');
|
|
if (actionBox) {
|
|
Polymer.dom(this.root).removeChild(actionBox);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Traverse diff content from right to left, call callback for each node.
|
|
* Stops if callback returns true.
|
|
*
|
|
* @param {!Node} startNode
|
|
* @param {function(Node):boolean} callback
|
|
* @param {Object=} 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') {
|
|
break;
|
|
}
|
|
var nextNode = travelLeft ? node.previousSibling : node.nextSibling;
|
|
if (callback(node)) {
|
|
break;
|
|
}
|
|
node = nextNode;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Get length of a node. Traverses diff content siblings if required.
|
|
*
|
|
* @param {!Node} node
|
|
* @return {number}
|
|
*/
|
|
_getLength: function(node) {
|
|
if (node instanceof Element && node.classList.contains('content')) {
|
|
node = node.firstChild;
|
|
var length = 0;
|
|
while (node) {
|
|
// Only measure Text nodes and <hl>
|
|
if (node instanceof Text || node.tagName == 'HL') {
|
|
length += this._getLength(node);
|
|
}
|
|
node = node.nextSibling;
|
|
}
|
|
return length;
|
|
} else {
|
|
// DOM API for textContent.length is broken for Unicode:
|
|
// https://mathiasbynens.be/notes/javascript-unicode
|
|
return node.textContent.replace(REGEX_ASTRAL_SYMBOL, '_').length;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Wraps node in hl tag with cssClass, replacing the node in DOM.
|
|
*
|
|
* @return {!Element} Wrapped node.
|
|
*/
|
|
_wrapInHighlight: function(node, cssClass) {
|
|
var hl = document.createElement('hl');
|
|
hl.className = cssClass;
|
|
Polymer.dom(node.parentElement).replaceChild(hl, node);
|
|
hl.appendChild(node);
|
|
return hl;
|
|
},
|
|
|
|
/**
|
|
* Node.prototype.splitText Unicode-valid alternative.
|
|
*
|
|
* @param {!Text} node
|
|
* @param {number} offset
|
|
* @return {!Text} Trailing Text Node.
|
|
*/
|
|
_splitText: function(node, offset) {
|
|
if (node.textContent.match(REGEX_ASTRAL_SYMBOL)) {
|
|
// DOM Api for splitText() is broken for Unicode:
|
|
// https://mathiasbynens.be/notes/javascript-unicode
|
|
// TODO (viktard): Polyfill Array.from for IE10.
|
|
var head = Array.from(node.textContent);
|
|
var tail = head.splice(offset);
|
|
var parent = node.parentElement;
|
|
var headNode = document.createTextNode(head.join(''));
|
|
parent.replaceChild(headNode, node);
|
|
var tailNode = document.createTextNode(tail.join(''));
|
|
parent.insertBefore(tailNode, headNode.nextSibling);
|
|
return tailNode;
|
|
} else {
|
|
return node.splitText(offset);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Split Text Node and wrap it in hl with cssClass.
|
|
* Wraps trailing part after split, tailing one if opt_firstPart is true.
|
|
*
|
|
* @param {!Text} node
|
|
* @param {number} offset
|
|
* @param {string} cssClass
|
|
* @param {boolean=} opt_firstPart
|
|
*/
|
|
_splitAndWrapInHighlight: function(node, offset, cssClass, opt_firstPart) {
|
|
if (this._getLength(node) === offset || offset === 0) {
|
|
return this._wrapInHighlight(node, cssClass);
|
|
} else {
|
|
if (opt_firstPart) {
|
|
this._splitText(node, offset);
|
|
// Node points to first part of the Text, second one is sibling.
|
|
} else {
|
|
node = this._splitText(node, offset);
|
|
}
|
|
return this._wrapInHighlight(node, cssClass);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Creates hl tag with cssClass for starting side of range highlight.
|
|
*
|
|
* @param {!Element} startContent Range start diff content aka td.content.
|
|
* @param {!Element} endContent Range end diff content aka td.content.
|
|
* @param {number} startOffset Range start within start content.
|
|
* @param {number} endOffset Range end within end content.
|
|
* @param {string} cssClass
|
|
* @return {!Element} Range start node.
|
|
*/
|
|
_normalizeStart: function(
|
|
startContent, endContent, startOffset, endOffset, cssClass) {
|
|
var isOneLine = startContent === endContent;
|
|
var startNode = startContent.firstChild;
|
|
var length = endOffset - startOffset;
|
|
|
|
if (!startNode) {
|
|
return startNode;
|
|
}
|
|
|
|
// Skip nodes before startOffset.
|
|
while (startNode &&
|
|
this._getLength(startNode) <= startOffset ||
|
|
this._getLength(startNode) === 0) {
|
|
startOffset -= this._getLength(startNode);
|
|
startNode = startNode.nextSibling;
|
|
}
|
|
|
|
// Split Text node.
|
|
if (startNode instanceof Text) {
|
|
startNode =
|
|
this._splitAndWrapInHighlight(startNode, startOffset, cssClass);
|
|
startContent.insertBefore(startNode, startNode.nextSibling);
|
|
// Edge case: single line, text node wraps the highlight.
|
|
if (isOneLine && this._getLength(startNode) > length) {
|
|
var extra = this._splitText(startNode.firstChild, length);
|
|
startContent.insertBefore(extra, startNode.nextSibling);
|
|
startContent.normalize();
|
|
}
|
|
} else if (startNode.tagName == 'HL') {
|
|
if (!startNode.classList.contains(cssClass)) {
|
|
var hl = startNode;
|
|
startNode = this._splitAndWrapInHighlight(
|
|
startNode.firstChild, startOffset, cssClass);
|
|
startContent.insertBefore(startNode, hl.nextSibling);
|
|
// Edge case: single line, <hl> wraps the highlight.
|
|
if (isOneLine && this._getLength(startNode) > length) {
|
|
var trailingHl = hl.cloneNode(false);
|
|
trailingHl.appendChild(
|
|
this._splitText(startNode.firstChild, length));
|
|
startContent.insertBefore(trailingHl, startNode.nextSibling);
|
|
}
|
|
if (hl.textContent.length === 0) {
|
|
hl.remove();
|
|
}
|
|
}
|
|
} else {
|
|
startNode = null;
|
|
}
|
|
return startNode;
|
|
},
|
|
|
|
/**
|
|
* Creates hl tag with cssClass for ending side of range highlight.
|
|
*
|
|
* @param {!Element} startContent Range start diff content aka td.content.
|
|
* @param {!Element} endContent Range end diff content aka td.content.
|
|
* @param {number} startOffset Range start within start content.
|
|
* @param {number} endOffset Range end within end content.
|
|
* @param {string} cssClass
|
|
* @return {!Element} Range start node.
|
|
*/
|
|
_normalizeEnd: function(
|
|
startContent, endContent, startOffset, endOffset, cssClass) {
|
|
var endNode = endContent.firstChild;
|
|
|
|
if (!endNode) {
|
|
return endNode;
|
|
}
|
|
|
|
// Find the node where endOffset points at.
|
|
while (endNode &&
|
|
this._getLength(endNode) < endOffset ||
|
|
this._getLength(endNode) === 0) {
|
|
endOffset -= this._getLength(endNode);
|
|
endNode = endNode.nextSibling;
|
|
}
|
|
|
|
if (endNode instanceof Text) {
|
|
endNode =
|
|
this._splitAndWrapInHighlight(endNode, endOffset, cssClass, true);
|
|
} else if (endNode.tagName == 'HL') {
|
|
if (!endNode.classList.contains(cssClass)) {
|
|
// Split text inside HL.
|
|
var hl = endNode;
|
|
endNode = this._splitAndWrapInHighlight(
|
|
endNode.firstChild, endOffset, cssClass, true);
|
|
endContent.insertBefore(endNode, hl);
|
|
if (hl.textContent.length === 0) {
|
|
hl.remove();
|
|
}
|
|
}
|
|
} else {
|
|
endNode = null;
|
|
}
|
|
return endNode;
|
|
},
|
|
|
|
/**
|
|
* Applies highlight to first and last lines in range.
|
|
*
|
|
* @param {!Element} startContent Range start diff content aka td.content.
|
|
* @param {!Element} endContent Range end diff content aka td.content.
|
|
* @param {number} startOffset Range start within start content.
|
|
* @param {number} endOffset Range end within end content.
|
|
* @param {string} cssClass
|
|
*/
|
|
_highlightSides: function(
|
|
startContent, endContent, startOffset, endOffset, cssClass) {
|
|
var isOneLine = startContent === endContent;
|
|
var startNode = this._normalizeStart(
|
|
startContent, endContent, startOffset, endOffset, cssClass);
|
|
var endNode = this._normalizeEnd(
|
|
startContent, endContent, startOffset, endOffset, cssClass);
|
|
|
|
// Grow starting highlight until endNode or end of line.
|
|
if (startNode && startNode != endNode) {
|
|
this._traverseContentSiblings(startNode.nextSibling, function(node) {
|
|
startNode.textContent += node.textContent;
|
|
node.remove();
|
|
return node == endNode;
|
|
});
|
|
}
|
|
|
|
if (!isOneLine && endNode) {
|
|
// Prepend text up to line start to the ending highlight.
|
|
this._traverseContentSiblings(endNode.previousSibling, function(node) {
|
|
endNode.textContent = node.textContent + endNode.textContent;
|
|
node.remove();
|
|
}, {left: true});
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @param {string} cssClass
|
|
* @param {number} startLine Range start code line number.
|
|
* @param {number} startCol Range start column number.
|
|
* @param {number} endCol Range end column number.
|
|
* @param {number} endOffset Range end within end content.
|
|
* @param {string=} opt_side Side selector (right or left).
|
|
*/
|
|
_applyRangedHighlight: function(
|
|
cssClass, startLine, startCol, endLine, endCol, opt_side) {
|
|
var side = opt_side;
|
|
var startEl = this.diffBuilder.getContentByLine(startLine, opt_side);
|
|
var endEl = this.diffBuilder.getContentByLine(endLine, opt_side);
|
|
this._highlightSides(startEl, endEl, startCol, endCol, cssClass);
|
|
if (endLine - startLine > 1) {
|
|
// There is at least one line in between.
|
|
var contents = this.diffBuilder.getContentsByLineRange(
|
|
startLine + 1, endLine - 1, opt_side);
|
|
// Wrap contents in highlight.
|
|
contents.forEach(function(content) {
|
|
if (content.textContent.length === 0) {
|
|
return;
|
|
}
|
|
var lineEl = this.diffBuilder.getLineElByChild(content);
|
|
var line = lineEl.getAttribute('data-value');
|
|
var threadEl =
|
|
this.diffBuilder.getCommentThreadByContentEl(content);
|
|
if (threadEl) {
|
|
threadEl.remove();
|
|
}
|
|
var text = document.createTextNode(content.textContent);
|
|
while (content.firstChild) {
|
|
content.removeChild(content.firstChild);
|
|
}
|
|
content.appendChild(text);
|
|
if (threadEl) {
|
|
content.appendChild(threadEl);
|
|
}
|
|
this._wrapInHighlight(text, cssClass);
|
|
}, this);
|
|
}
|
|
},
|
|
|
|
_applyAllHighlights: function() {
|
|
var rangedLeft =
|
|
this.comments.left.filter(function(item) { return !!item.range; });
|
|
var rangedRight =
|
|
this.comments.right.filter(function(item) { return !!item.range; });
|
|
rangedLeft.forEach(function(item) {
|
|
var range = item.range;
|
|
this._applyRangedHighlight(
|
|
RANGE_HIGHLIGHT, range.start_line, range.start_character,
|
|
range.end_line, range.end_character, 'left');
|
|
}, this);
|
|
rangedRight.forEach(function(item) {
|
|
var range = item.range;
|
|
this._applyRangedHighlight(
|
|
RANGE_HIGHLIGHT, range.start_line, range.start_character,
|
|
range.end_line, range.end_character, 'right');
|
|
}, this);
|
|
},
|
|
|
|
_rerenderByLines: function(startLine, endLine, opt_side) {
|
|
this.async(function() {
|
|
this.diffBuilder.renderLineRange(startLine, endLine, opt_side);
|
|
}, 1);
|
|
},
|
|
});
|
|
})();
|