1b3d9536eb
Some additional cleanup of event names and fixing of the bubbles property not being placed in the right place. TODO in a follow-up change: + Comments are not considered in context, yet. So if there is a comment made in a span of common lines that are hidden, it will be there underneath the context control but wont show the code. Feature: Issue 3649 Change-Id: I8117b243a7d8d529e0c1cea155a4b6b436c0c768
593 lines
20 KiB
HTML
593 lines
20 KiB
HTML
<!--
|
|
Copyright (C) 2015 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.
|
|
-->
|
|
|
|
<link rel="import" href="../bower_components/polymer/polymer.html">
|
|
<link rel="import" href="gr-diff-comment-thread.html">
|
|
|
|
<dom-module id="gr-diff-side">
|
|
<template>
|
|
<style>
|
|
:host,
|
|
.container {
|
|
display: flex;
|
|
}
|
|
.content {
|
|
width: 80ch;
|
|
}
|
|
.lineNum:before,
|
|
.code:before {
|
|
/* To ensure the height is non-zero in these elements, a
|
|
zero-width space is set as its content. The character
|
|
itself doesn't matter. Just that there is something
|
|
there. */
|
|
content: '\200B';
|
|
}
|
|
.lineNum {
|
|
background-color: #eee;
|
|
color: #666;
|
|
padding: 0 .75em;
|
|
text-align: right;
|
|
}
|
|
.canComment .lineNum {
|
|
cursor: pointer;
|
|
}
|
|
.canComment .lineNum:hover {
|
|
background-color: #ccc;
|
|
}
|
|
.code {
|
|
white-space: pre;
|
|
}
|
|
.lightHighlight {
|
|
background-color: var(--light-highlight-color);
|
|
}
|
|
hl,
|
|
.darkHighlight {
|
|
background-color: var(--dark-highlight-color);
|
|
}
|
|
.br:after {
|
|
/* Line feed */
|
|
content: '\A';
|
|
}
|
|
.tab:before {
|
|
color: #C62828;
|
|
/* >> character */
|
|
content: '\00BB';
|
|
}
|
|
.numbers .filler {
|
|
background-color: #eee;
|
|
}
|
|
.contextControl {
|
|
background-color: #fef;
|
|
}
|
|
.contextControl a:link,
|
|
.contextControl a:visited {
|
|
display: block;
|
|
text-decoration: none;
|
|
}
|
|
.numbers .contextControl {
|
|
padding: 0 .75em;
|
|
text-align: right;
|
|
}
|
|
.content .contextControl {
|
|
text-align: center;
|
|
}
|
|
</style>
|
|
<div class$="[[_computeContainerClass(canComment)]]">
|
|
<div class="numbers" id="numbers"></div>
|
|
<div class="content" id="content"></div>
|
|
</div>
|
|
</template>
|
|
<script>
|
|
(function() {
|
|
'use strict';
|
|
|
|
var CharCode = {
|
|
LESS_THAN: '<'.charCodeAt(0),
|
|
GREATER_THAN: '>'.charCodeAt(0),
|
|
AMPERSAND: '&'.charCodeAt(0),
|
|
SEMICOLON: ';'.charCodeAt(0),
|
|
};
|
|
|
|
var TAB_REGEX = /\t/g;
|
|
|
|
Polymer({
|
|
is: 'gr-diff-side',
|
|
|
|
/**
|
|
* Fired when an expand context control is clicked.
|
|
*
|
|
* @event expand-context
|
|
*/
|
|
|
|
/**
|
|
* Fired when a thread's height is changed.
|
|
*
|
|
* @event thread-height-change
|
|
*/
|
|
|
|
/**
|
|
* Fired when a draft should be added.
|
|
*
|
|
* @event add-draft
|
|
*/
|
|
|
|
/**
|
|
* Fired when a thread is removed.
|
|
*
|
|
* @event remove-thread
|
|
*/
|
|
|
|
properties: {
|
|
canComment: {
|
|
type: Boolean,
|
|
value: false,
|
|
},
|
|
content: {
|
|
type: Array,
|
|
notify: true,
|
|
observer: '_contentChanged',
|
|
},
|
|
width: {
|
|
type: Number,
|
|
observer: '_widthChanged',
|
|
},
|
|
changeNum: String,
|
|
patchNum: String,
|
|
path: String,
|
|
|
|
_lineFeedHTML: {
|
|
type: String,
|
|
value: '<span class="style-scope gr-diff-side br"></span>',
|
|
readOnly: true,
|
|
},
|
|
_highlightStartTag: {
|
|
type: String,
|
|
value: '<hl class="style-scope gr-diff-side">',
|
|
readOnly: true,
|
|
},
|
|
_highlightEndTag: {
|
|
type: String,
|
|
value: '</hl>',
|
|
readOnly: true,
|
|
},
|
|
},
|
|
|
|
listeners: {
|
|
'tap': '_tapHandler',
|
|
},
|
|
|
|
rowInserted: function(index) {
|
|
this.renderLineIndexRange(index, index);
|
|
this._updateDOMIndices();
|
|
},
|
|
|
|
rowRemoved: function(index) {
|
|
var removedEls = Polymer.dom(this.root).querySelectorAll(
|
|
'[data-index="' + index + '"]');
|
|
for (var i = 0; i < removedEls.length; i++) {
|
|
removedEls[i].parentNode.removeChild(removedEls[i]);
|
|
}
|
|
this._updateDOMIndices();
|
|
},
|
|
|
|
rowUpdated: function(index) {
|
|
var removedEls = Polymer.dom(this.root).querySelectorAll(
|
|
'[data-index="' + index + '"]');
|
|
for (var i = 0; i < removedEls.length; i++) {
|
|
removedEls[i].parentNode.removeChild(removedEls[i]);
|
|
}
|
|
this.renderLineIndexRange(index, index);
|
|
},
|
|
|
|
scrollToLine: function(lineNum) {
|
|
if (isNaN(lineNum) || lineNum < 1) { return; }
|
|
|
|
var el = this.$$('.numbers .lineNum[data-line-num="' + lineNum + '"]');
|
|
if (!el) { return; }
|
|
|
|
// Calculate where the line is relative to the window.
|
|
var top = el.offsetTop;
|
|
for (var offsetParent = el.offsetParent;
|
|
offsetParent;
|
|
offsetParent = offsetParent.offsetParent) {
|
|
top += offsetParent.offsetTop;
|
|
}
|
|
|
|
// Scroll the element to the middle of the window. Dividing by a third
|
|
// instead of half the inner height feels a bit better otherwise the
|
|
// element appears to be below the center of the window even when it
|
|
// isn't.
|
|
window.scrollTo(0, top - (window.innerHeight / 3) - el.offsetHeight);
|
|
},
|
|
|
|
renderLineIndexRange: function(startIndex, endIndex) {
|
|
this._render(this.content, startIndex, endIndex);
|
|
},
|
|
|
|
hideElementsWithIndex: function(index) {
|
|
var els = Polymer.dom(this.root).querySelectorAll(
|
|
'[data-index="' + index + '"]');
|
|
for (var i = 0; i < els.length; i++) {
|
|
els[i].setAttribute('hidden', true);
|
|
}
|
|
},
|
|
|
|
getRowHeight: function(index) {
|
|
var row = this.content[index];
|
|
// Filler elements should not be taken into account when determining
|
|
// height calculations.
|
|
if (row.type == 'FILLER') {
|
|
return 0;
|
|
}
|
|
if (row.height != null) {
|
|
return row.height;
|
|
}
|
|
|
|
var selector = '[data-index="' + index + '"]'
|
|
var els = Polymer.dom(this.root).querySelectorAll(selector);
|
|
if (els.length != 2) {
|
|
throw Error('Rows should only consist of two elements');
|
|
}
|
|
return Math.max(els[0].offsetHeight, els[1].offsetHeight);
|
|
},
|
|
|
|
getRowNaturalHeight: function(index) {
|
|
var contentEl = this.$$('.content [data-index="' + index + '"]');
|
|
return contentEl.naturalHeight || contentEl.offsetHeight;
|
|
},
|
|
|
|
setRowNaturalHeight: function(index) {
|
|
var lineEl = this.$$('.numbers [data-index="' + index + '"]');
|
|
var contentEl = this.$$('.content [data-index="' + index + '"]');
|
|
contentEl.style.height = null;
|
|
var height = contentEl.offsetHeight;
|
|
lineEl.style.height = height + 'px';
|
|
this.content[index].height = height;
|
|
return height;
|
|
},
|
|
|
|
setRowHeight: function(index, height) {
|
|
var selector = '[data-index="' + index + '"]';
|
|
var els = Polymer.dom(this.root).querySelectorAll(selector);
|
|
for (var i = 0; i < els.length; i++) {
|
|
els[i].style.height = height + 'px';
|
|
}
|
|
this.content[index].height = height;
|
|
},
|
|
|
|
_updateDOMIndices: function() {
|
|
// There is no way to select elements with a data-index greater than a
|
|
// given value. For now, just update all DOM elements.
|
|
var lineEls = Polymer.dom(this.root).querySelectorAll(
|
|
'.numbers [data-index]');
|
|
var contentEls = Polymer.dom(this.root).querySelectorAll(
|
|
'.content [data-index]');
|
|
if (lineEls.length != contentEls.length) {
|
|
throw Error(
|
|
'There must be the same number of line and content elements');
|
|
}
|
|
var index = 0;
|
|
for (var i = 0; i < this.content.length; i++) {
|
|
if (this.content[i].hidden) { continue; }
|
|
|
|
lineEls[index].setAttribute('data-index', i);
|
|
contentEls[index].setAttribute('data-index', i);
|
|
index++;
|
|
}
|
|
},
|
|
|
|
_widthChanged: function(width) {
|
|
this.$.content.style.width = width + 'ch';
|
|
},
|
|
|
|
_contentChanged: function(diff) {
|
|
this._clearChildren(this.$.numbers);
|
|
this._clearChildren(this.$.content);
|
|
this._render(diff, 0, diff.length - 1);
|
|
},
|
|
|
|
_computeContainerClass: function(canComment) {
|
|
return 'container' + (canComment ? ' canComment' : '');
|
|
},
|
|
|
|
_tapHandler: function(e) {
|
|
var lineEl = Polymer.dom(e).rootTarget;
|
|
if (!this.canComment || !lineEl.classList.contains('lineNum')) {
|
|
return;
|
|
}
|
|
|
|
e.preventDefault();
|
|
var index = parseInt(lineEl.getAttribute('data-index'), 10);
|
|
var line = parseInt(lineEl.getAttribute('data-line-num'), 10);
|
|
this.fire('add-draft', {
|
|
index: index,
|
|
line: line
|
|
}, {bubbles: false});
|
|
},
|
|
|
|
_clearChildren: function(el) {
|
|
while (el.firstChild) {
|
|
el.removeChild(el.firstChild);
|
|
}
|
|
},
|
|
|
|
_handleContextControlClick: function(context, e) {
|
|
e.preventDefault();
|
|
this.fire('expand-context', {context: context}, {bubbles: false});
|
|
},
|
|
|
|
_render: function(diff, startIndex, endIndex) {
|
|
var beforeLineEl;
|
|
var beforeContentEl;
|
|
if (endIndex != diff.length - 1) {
|
|
beforeLineEl = this.$$('.numbers [data-index="' + endIndex + '"]');
|
|
beforeContentEl = this.$$('.content [data-index="' + endIndex + '"]');
|
|
if (!beforeLineEl && !beforeContentEl) {
|
|
// `endIndex` may be present within the model, but not in the DOM.
|
|
// Insert it before its successive element.
|
|
beforeLineEl = this.$$(
|
|
'.numbers [data-index="' + (endIndex + 1) + '"]');
|
|
beforeContentEl = this.$$(
|
|
'.content [data-index="' + (endIndex + 1) + '"]');
|
|
}
|
|
}
|
|
|
|
for (var i = startIndex; i <= endIndex; i++) {
|
|
if (diff[i].hidden) { continue; }
|
|
|
|
switch (diff[i].type) {
|
|
case 'CODE':
|
|
this._renderCode(diff[i], i, beforeLineEl, beforeContentEl);
|
|
break;
|
|
case 'FILLER':
|
|
this._renderFiller(diff[i], i, beforeLineEl, beforeContentEl);
|
|
break;
|
|
case 'CONTEXT_CONTROL':
|
|
this._renderContextControl(diff[i], i, beforeLineEl,
|
|
beforeContentEl);
|
|
break;
|
|
case 'COMMENT_THREAD':
|
|
this._renderCommentThread(diff[i], i, beforeLineEl,
|
|
beforeContentEl);
|
|
break;
|
|
}
|
|
}
|
|
},
|
|
|
|
_handleCommentThreadHeightChange: function(e) {
|
|
var threadEl = Polymer.dom(e).rootTarget;
|
|
var index = parseInt(threadEl.getAttribute('data-index'), 10);
|
|
this.content[index].height = e.detail.height;
|
|
var lineEl = this.$$('.numbers [data-index="' + index + '"]');
|
|
lineEl.style.height = e.detail.height + 'px';
|
|
this.fire('thread-height-change', {
|
|
index: index,
|
|
height: e.detail.height,
|
|
}, {bubbles: false});
|
|
},
|
|
|
|
_handleCommentThreadDiscard: function(e) {
|
|
var threadEl = Polymer.dom(e).rootTarget;
|
|
var index = parseInt(threadEl.getAttribute('data-index'), 10);
|
|
this.fire('remove-thread', {index: index}, {bubbles: false});
|
|
},
|
|
|
|
_renderCommentThread: function(thread, index, beforeLineEl,
|
|
beforeContentEl) {
|
|
var lineEl = this._createElement('div', 'commentThread');
|
|
lineEl.classList.add('filler');
|
|
lineEl.setAttribute('data-index', index);
|
|
var threadEl = document.createElement('gr-diff-comment-thread');
|
|
threadEl.addEventListener('height-change',
|
|
this._handleCommentThreadHeightChange.bind(this));
|
|
threadEl.addEventListener('discard',
|
|
this._handleCommentThreadDiscard.bind(this));
|
|
threadEl.setAttribute('data-index', index);
|
|
threadEl.changeNum = this.changeNum;
|
|
threadEl.patchNum = this.patchNum;
|
|
threadEl.path = this.path;
|
|
threadEl.comments = thread.comments;
|
|
threadEl.showActions = this.canComment;
|
|
|
|
this.$.numbers.insertBefore(lineEl, beforeLineEl);
|
|
this.$.content.insertBefore(threadEl, beforeContentEl);
|
|
},
|
|
|
|
_renderContextControl: function(control, index, beforeLineEl,
|
|
beforeContentEl) {
|
|
var lineEl = this._createElement('div', 'contextControl');
|
|
lineEl.setAttribute('data-index', index);
|
|
lineEl.textContent = '@@';
|
|
var contentEl = this._createElement('div', 'contextControl');
|
|
contentEl.setAttribute('data-index', index);
|
|
var a = this._createElement('a');
|
|
a.href = '#';
|
|
a.textContent = 'Show ' + control.numLines + ' common ' +
|
|
(control.numLines == 1 ? 'line' : 'lines') + '...';
|
|
a.addEventListener('click',
|
|
this._handleContextControlClick.bind(this, control));
|
|
contentEl.appendChild(a);
|
|
|
|
this.$.numbers.insertBefore(lineEl, beforeLineEl);
|
|
this.$.content.insertBefore(contentEl, beforeContentEl);
|
|
},
|
|
|
|
_renderFiller: function(filler, index, beforeLineEl, beforeContentEl) {
|
|
var lineFillerEl = this._createElement('div', 'filler');
|
|
lineFillerEl.setAttribute('data-index', index);
|
|
var fillerEl = this._createElement('div', 'filler');
|
|
fillerEl.setAttribute('data-index', index);
|
|
var numLines = filler.numLines || 1;
|
|
|
|
lineFillerEl.textContent = '\n'.repeat(numLines);
|
|
for (var i = 0; i < numLines; i++) {
|
|
var newlineEl = this._createElement('span', 'br');
|
|
fillerEl.appendChild(newlineEl);
|
|
}
|
|
|
|
this.$.numbers.insertBefore(lineFillerEl, beforeLineEl);
|
|
this.$.content.insertBefore(fillerEl, beforeContentEl);
|
|
},
|
|
|
|
_renderCode: function(code, index, beforeLineEl,
|
|
beforeContentEl) {
|
|
var lineNumEl = this._createElement('div', 'lineNum');
|
|
lineNumEl.setAttribute('data-line-num', code.lineNum);
|
|
lineNumEl.setAttribute('data-index', index);
|
|
var numLines = code.numLines || 1;
|
|
lineNumEl.textContent = code.lineNum + '\n'.repeat(numLines);
|
|
|
|
var contentEl = this._createElement('div', 'code');
|
|
contentEl.setAttribute('data-line-num', code.lineNum);
|
|
contentEl.setAttribute('data-index', index);
|
|
|
|
if (code.highlight) {
|
|
contentEl.classList.add(code.intraline.length > 0 ?
|
|
'lightHighlight' : 'darkHighlight');
|
|
}
|
|
|
|
var html = util.escapeHTML(code.content);
|
|
if (code.highlight && code.intraline.length > 0) {
|
|
html = this._addIntralineHighlights(code.content, html,
|
|
code.intraline);
|
|
}
|
|
if (numLines > 1) {
|
|
html = this._addNewLines(code.content, html, numLines);
|
|
}
|
|
html = this._addTabIndicators(code.content, html);
|
|
|
|
// If the html is equivalent to the text then it didn't get highlighted
|
|
// or escaped. Use textContent which is faster than innerHTML.
|
|
if (code.content == html) {
|
|
contentEl.textContent = code.content;
|
|
} else {
|
|
contentEl.innerHTML = html;
|
|
}
|
|
|
|
this.$.numbers.insertBefore(lineNumEl, beforeLineEl);
|
|
this.$.content.insertBefore(contentEl, beforeContentEl);
|
|
},
|
|
|
|
// Advance `index` by the appropriate number of characters that would
|
|
// represent one source code character and return that index. For
|
|
// example, for source code '<span>' the escaped html string is
|
|
// '<span>'. Advancing from index 0 on the prior html string would
|
|
// return 4, since < maps to one source code character ('<').
|
|
_advanceChar: function(html, index) {
|
|
// Any tags don't count as characters
|
|
while (index < html.length &&
|
|
html.charCodeAt(index) == CharCode.LESS_THAN) {
|
|
while (index < html.length &&
|
|
html.charCodeAt(index) != CharCode.GREATER_THAN) {
|
|
index++;
|
|
}
|
|
index++; // skip the ">" itself
|
|
}
|
|
// An HTML entity (e.g., <) counts as one character.
|
|
if (index < html.length &&
|
|
html.charCodeAt(index) == CharCode.AMPERSAND) {
|
|
while (index < html.length &&
|
|
html.charCodeAt(index) != CharCode.SEMICOLON) {
|
|
index++;
|
|
}
|
|
}
|
|
return index + 1;
|
|
},
|
|
|
|
_addIntralineHighlights: function(content, html, highlights) {
|
|
var startTag = this._highlightStartTag;
|
|
var endTag = this._highlightEndTag;
|
|
|
|
for (var i = 0; i < highlights.length; i++) {
|
|
var hl = highlights[i];
|
|
|
|
var htmlStartIndex = 0;
|
|
for (var j = 0; j < hl.startIndex; j++) {
|
|
htmlStartIndex = this._advanceChar(html, htmlStartIndex);
|
|
}
|
|
|
|
var htmlEndIndex = 0;
|
|
if (hl.endIndex != null) {
|
|
for (var j = 0; j < hl.endIndex; j++) {
|
|
htmlEndIndex = this._advanceChar(html, htmlEndIndex);
|
|
}
|
|
} else {
|
|
// If endIndex isn't present, continue to the end of the line.
|
|
htmlEndIndex = html.length;
|
|
}
|
|
// The start and end indices could be the same if a highlight is meant
|
|
// to start at the end of a line and continue onto the next one.
|
|
// Ignore it.
|
|
if (htmlStartIndex != htmlEndIndex) {
|
|
html = html.slice(0, htmlStartIndex) + startTag +
|
|
html.slice(htmlStartIndex, htmlEndIndex) + endTag +
|
|
html.slice(htmlEndIndex);
|
|
}
|
|
}
|
|
return html;
|
|
},
|
|
|
|
_addNewLines: function(content, html, numLines) {
|
|
var htmlIndex = 0;
|
|
var indices = [];
|
|
for (var i = 0; i < content.length; i++) {
|
|
if (i > 0 && i % this.width == 0) {
|
|
indices.push(htmlIndex);
|
|
}
|
|
htmlIndex = this._advanceChar(html, htmlIndex)
|
|
}
|
|
var result = html;
|
|
var linesLeft = numLines;
|
|
// Since the result string is being altered in place, start from the end
|
|
// of the string so that the insertion indices are not affected as the
|
|
// result string changes.
|
|
for (var i = indices.length - 1; i >= 0; i--) {
|
|
result = result.slice(0, indices[i]) + this._lineFeedHTML +
|
|
result.slice(indices[i]);
|
|
linesLeft--;
|
|
}
|
|
// numLines is the total number of lines this code block should take up.
|
|
// Fill in the remaining ones.
|
|
for (var i = 0; i < linesLeft; i++) {
|
|
result += this._lineFeedHTML;
|
|
}
|
|
return result;
|
|
},
|
|
|
|
_addTabIndicators: function(content, html) {
|
|
return html.replace(TAB_REGEX,
|
|
'<span class="style-scope gr-diff-side tab">\t</span>');
|
|
},
|
|
|
|
_createElement: function(tagName, className) {
|
|
var el = document.createElement(tagName);
|
|
// When Shady DOM is being used, these classes are added to account for
|
|
// Polymer's polyfill behavior. In order to guarantee sufficient
|
|
// specificity within the CSS rules, these are added to every element.
|
|
// Since the Polymer DOM utility functions (which would do this
|
|
// automatically) are not being used for performance reasons, this is
|
|
// done manually.
|
|
el.classList.add('style-scope', 'gr-diff-side');
|
|
if (!!className) {
|
|
el.classList.add(className);
|
|
}
|
|
return el;
|
|
},
|
|
});
|
|
})();
|
|
</script>
|
|
</dom-module>
|