
With native Shadow DOM, the selections returned by document.getSelection() cannot reference the elements actually making up the diff because those live in the Shadow DOM of gr-diff. Instead, a shadowRoot.getSelection() method is offered that works as the pre-Shadow DOM Selection used to, but that one is only available from gr-diff. This change moves listening for the selectionchange event and retrieving the selection to gr-diff, which then delegates the extraction and normalization of the ranges to gr-diff-highlight (it's current location). This change also fixes an incompatibility with native Shadow DOM in gr-selection-action-box, where a call to parentElement cannot cross the Shadow DOM boundary and needs special handling to retrieve the Shadow Host. Together these changes fix creating range comments for native Shadow DOM. Bug: Issue 6372 Change-Id: I4bce21025ab5a4c06d77db096d6aa56e7840987b
872 lines
26 KiB
JavaScript
872 lines
26 KiB
JavaScript
/**
|
|
* @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.
|
|
*/
|
|
(function() {
|
|
'use strict';
|
|
|
|
const ERR_COMMENT_ON_EDIT = 'You cannot comment on an edit.';
|
|
const ERR_COMMENT_ON_EDIT_BASE = 'You cannot comment on the base patch set ' +
|
|
'of an edit.';
|
|
const ERR_INVALID_LINE = 'Invalid line number: ';
|
|
|
|
const NO_NEWLINE_BASE = 'No newline at end of base file.';
|
|
const NO_NEWLINE_REVISION = 'No newline at end of revision file.';
|
|
|
|
const DiffViewMode = {
|
|
SIDE_BY_SIDE: 'SIDE_BY_SIDE',
|
|
UNIFIED: 'UNIFIED_DIFF',
|
|
};
|
|
|
|
const DiffSide = {
|
|
LEFT: 'left',
|
|
RIGHT: 'right',
|
|
};
|
|
|
|
const Defs = {};
|
|
|
|
/**
|
|
* Special line number which should not be collapsed into a shared region.
|
|
*
|
|
* @typedef {{
|
|
* number: number,
|
|
* leftSide: boolean
|
|
* }}
|
|
*/
|
|
Defs.LineOfInterest;
|
|
|
|
const LARGE_DIFF_THRESHOLD_LINES = 10000;
|
|
const FULL_CONTEXT = -1;
|
|
const LIMITED_CONTEXT = 10;
|
|
|
|
/** @typedef {{start_line: number, start_character: number,
|
|
* end_line: number, end_character: number}} */
|
|
Gerrit.Range;
|
|
|
|
/**
|
|
* Compare two ranges. Either argument may be falsy, but will only return
|
|
* true if both are falsy or if neither are falsy and have the same position
|
|
* values.
|
|
*
|
|
* @param {Gerrit.Range=} a range 1
|
|
* @param {Gerrit.Range=} b range 2
|
|
* @return {boolean}
|
|
*/
|
|
Gerrit.rangesEqual = function(a, b) {
|
|
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;
|
|
};
|
|
|
|
function isThreadEl(node) {
|
|
return node.nodeType === Node.ELEMENT_NODE &&
|
|
node.classList.contains('comment-thread');
|
|
}
|
|
|
|
/**
|
|
* Turn a slot element into the corresponding content element.
|
|
* Slots are only fully supported in Polymer 2 - in Polymer 1, they are
|
|
* replaced with content elements during template parsing. This conversion is
|
|
* not applied for imperatively created slot elements, so this method
|
|
* implements the same behavior as the template parsing for imperative slots.
|
|
*/
|
|
Gerrit.slotToContent = function(slot) {
|
|
const content = document.createElement('content');
|
|
content.name = slot.name;
|
|
content.setAttribute('select', `[slot='${slot.name}']`);
|
|
return content;
|
|
};
|
|
|
|
Polymer({
|
|
is: 'gr-diff',
|
|
|
|
/**
|
|
* Fired when the user selects a line.
|
|
* @event line-selected
|
|
*/
|
|
|
|
/**
|
|
* Fired if being logged in is required.
|
|
*
|
|
* @event show-auth-required
|
|
*/
|
|
|
|
/**
|
|
* Fired when a comment is created
|
|
*
|
|
* @event create-comment
|
|
*/
|
|
|
|
properties: {
|
|
changeNum: String,
|
|
noAutoRender: {
|
|
type: Boolean,
|
|
value: false,
|
|
},
|
|
/** @type {?} */
|
|
patchRange: Object,
|
|
path: String,
|
|
prefs: {
|
|
type: Object,
|
|
observer: '_prefsObserver',
|
|
},
|
|
projectName: String,
|
|
displayLine: {
|
|
type: Boolean,
|
|
value: false,
|
|
},
|
|
isImageDiff: {
|
|
type: Boolean,
|
|
},
|
|
commitRange: Object,
|
|
hidden: {
|
|
type: Boolean,
|
|
reflectToAttribute: true,
|
|
},
|
|
noRenderOnPrefsChange: Boolean,
|
|
/** @type {!Array<!Gerrit.HoveredRange>} */
|
|
_commentRanges: {
|
|
type: Array,
|
|
value: () => [],
|
|
},
|
|
lineWrapping: {
|
|
type: Boolean,
|
|
value: false,
|
|
observer: '_lineWrappingObserver',
|
|
},
|
|
viewMode: {
|
|
type: String,
|
|
value: DiffViewMode.SIDE_BY_SIDE,
|
|
observer: '_viewModeObserver',
|
|
},
|
|
|
|
/** @type ?Defs.LineOfInterest */
|
|
lineOfInterest: Object,
|
|
|
|
/**
|
|
* The key locations based on the comments and line of interests,
|
|
* where lines should not be collapsed.
|
|
*
|
|
* @type {{left: Object<(string|number), number>,
|
|
* right: Object<(string|number), number>}}
|
|
*/
|
|
_keyLocations: {
|
|
type: Object,
|
|
value: () => ({
|
|
left: {},
|
|
right: {},
|
|
}),
|
|
},
|
|
|
|
loading: {
|
|
type: Boolean,
|
|
value: false,
|
|
observer: '_loadingChanged',
|
|
},
|
|
|
|
loggedIn: {
|
|
type: Boolean,
|
|
value: false,
|
|
},
|
|
diff: {
|
|
type: Object,
|
|
observer: '_diffChanged',
|
|
},
|
|
_diffHeaderItems: {
|
|
type: Array,
|
|
value: [],
|
|
computed: '_computeDiffHeaderItems(diff.*)',
|
|
},
|
|
_diffTableClass: {
|
|
type: String,
|
|
value: '',
|
|
},
|
|
/** @type {?Object} */
|
|
baseImage: Object,
|
|
/** @type {?Object} */
|
|
revisionImage: Object,
|
|
|
|
/**
|
|
* Whether the safety check for large diffs when whole-file is set has
|
|
* been bypassed. If the value is null, then the safety has not been
|
|
* bypassed. If the value is a number, then that number represents the
|
|
* context preference to use when rendering the bypassed diff.
|
|
*
|
|
* @type (number|null)
|
|
*/
|
|
_safetyBypass: {
|
|
type: Number,
|
|
value: null,
|
|
},
|
|
|
|
_showWarning: Boolean,
|
|
|
|
/** @type {?string} */
|
|
errorMessage: {
|
|
type: String,
|
|
value: null,
|
|
},
|
|
|
|
/** @type {?Object} */
|
|
blame: {
|
|
type: Object,
|
|
value: null,
|
|
observer: '_blameChanged',
|
|
},
|
|
|
|
parentIndex: Number,
|
|
|
|
_newlineWarning: {
|
|
type: String,
|
|
computed: '_computeNewlineWarning(diff)',
|
|
},
|
|
|
|
_diffLength: Number,
|
|
|
|
/**
|
|
* Observes comment nodes added or removed after the initial render.
|
|
* Can be used to unregister when the entire diff is (re-)rendered or upon
|
|
* detachment.
|
|
* @type {?PolymerDomApi.ObserveHandle}
|
|
*/
|
|
_incrementalNodeObserver: Object,
|
|
|
|
/**
|
|
* Observes comment nodes added or removed at any point.
|
|
* Can be used to unregister upon detachment.
|
|
* @type {?PolymerDomApi.ObserveHandle}
|
|
*/
|
|
_nodeObserver: Object,
|
|
|
|
/** Set by Polymer. */
|
|
isAttached: Boolean,
|
|
},
|
|
|
|
behaviors: [
|
|
Gerrit.PatchSetBehavior,
|
|
],
|
|
|
|
listeners: {
|
|
'create-range-comment': '_handleCreateRangeComment',
|
|
'render-content': '_handleRenderContent',
|
|
},
|
|
|
|
observers: [
|
|
'_enableSelectionObserver(loggedIn, isAttached)',
|
|
],
|
|
|
|
attached() {
|
|
this._observeNodes();
|
|
},
|
|
|
|
detached() {
|
|
this._unobserveIncrementalNodes();
|
|
this._unobserveNodes();
|
|
},
|
|
|
|
_enableSelectionObserver(loggedIn, isAttached) {
|
|
if (loggedIn && isAttached) {
|
|
this.listen(document, 'selectionchange', '_handleSelectionChange');
|
|
} else {
|
|
this.unlisten(document, 'selectionchange', '_handleSelectionChange');
|
|
}
|
|
},
|
|
|
|
_handleSelectionChange() {
|
|
// When using native shadow DOM, the selection returned by
|
|
// document.getSelection() cannot reference the actual DOM elements making
|
|
// up the diff, because they are in the shadow DOM of the gr-diff element.
|
|
// For this reason, we handle the selectionchange here, and pass the
|
|
// shadow DOM selection into gr-diff-highlight, where the corresponding
|
|
// range is determined and normalized.
|
|
const selection = this.root.getSelection ?
|
|
this.root.getSelection() :
|
|
document.getSelection();
|
|
this.$.highlights.handleSelectionChange(selection);
|
|
},
|
|
|
|
_observeNodes() {
|
|
this._nodeObserver = Polymer.dom(this).observeNodes(info => {
|
|
const addedThreadEls = info.addedNodes.filter(isThreadEl);
|
|
const removedThreadEls = info.removedNodes.filter(isThreadEl);
|
|
this._updateRanges(addedThreadEls, removedThreadEls);
|
|
this._updateKeyLocations(addedThreadEls, removedThreadEls);
|
|
this._redispatchHoverEvents(addedThreadEls);
|
|
});
|
|
},
|
|
|
|
_updateRanges(addedThreadEls, removedThreadEls) {
|
|
function commentRangeFromThreadEl(threadEl) {
|
|
const side = threadEl.getAttribute('comment-side');
|
|
const range = JSON.parse(threadEl.getAttribute('range'));
|
|
return {side, range, hovering: false};
|
|
}
|
|
|
|
const addedCommentRanges = addedThreadEls
|
|
.map(commentRangeFromThreadEl)
|
|
.filter(({range}) => range);
|
|
const removedCommentRanges = removedThreadEls
|
|
.map(commentRangeFromThreadEl)
|
|
.filter(({range}) => range);
|
|
for (const removedCommentRange of removedCommentRanges) {
|
|
const i = this._commentRanges.findIndex(commentRange => {
|
|
return commentRange.side === removedCommentRange.side &&
|
|
Gerrit.rangesEqual(commentRange.range, removedCommentRange.range);
|
|
});
|
|
this.splice('_commentRanges', i, 1);
|
|
}
|
|
this.push('_commentRanges', ...addedCommentRanges);
|
|
},
|
|
|
|
_updateKeyLocations(addedThreadEls, removedThreadEls) {
|
|
for (const threadEl of addedThreadEls) {
|
|
const commentSide = threadEl.getAttribute('comment-side');
|
|
const lineNum = threadEl.getAttribute('line-num') || GrDiffLine.FILE;
|
|
this._keyLocations[commentSide][lineNum] = true;
|
|
}
|
|
for (const threadEl of removedThreadEls) {
|
|
const commentSide = threadEl.getAttribute('comment-side');
|
|
const lineNum = threadEl.getAttribute('line-num') || GrDiffLine.FILE;
|
|
this._keyLocations[commentSide][lineNum] = false;
|
|
}
|
|
},
|
|
|
|
// Dispatch events that are handled by the gr-diff-highlight.
|
|
_redispatchHoverEvents(addedThreadEls) {
|
|
for (const threadEl of addedThreadEls) {
|
|
threadEl.addEventListener('mouseenter', () => {
|
|
threadEl.dispatchEvent(
|
|
new CustomEvent('comment-thread-mouseenter', {bubbles: true}));
|
|
});
|
|
threadEl.addEventListener('mouseleave', () => {
|
|
threadEl.dispatchEvent(
|
|
new CustomEvent('comment-thread-mouseleave', {bubbles: true}));
|
|
});
|
|
}
|
|
},
|
|
|
|
/** Cancel any remaining diff builder rendering work. */
|
|
cancel() {
|
|
this.$.diffBuilder.cancel();
|
|
},
|
|
|
|
/** @return {!Array<!HTMLElement>} */
|
|
getCursorStops() {
|
|
if (this.hidden && this.noAutoRender) {
|
|
return [];
|
|
}
|
|
|
|
return Polymer.dom(this.root).querySelectorAll('.diff-row');
|
|
},
|
|
|
|
/** @return {boolean} */
|
|
isRangeSelected() {
|
|
return this.$.highlights.isRangeSelected();
|
|
},
|
|
|
|
toggleLeftDiff() {
|
|
this.toggleClass('no-left');
|
|
},
|
|
|
|
_blameChanged(newValue) {
|
|
this.$.diffBuilder.setBlame(newValue);
|
|
if (newValue) {
|
|
this.classList.add('showBlame');
|
|
} else {
|
|
this.classList.remove('showBlame');
|
|
}
|
|
},
|
|
|
|
/** @return {string} */
|
|
_computeContainerClass(loggedIn, viewMode, displayLine) {
|
|
const classes = ['diffContainer'];
|
|
switch (viewMode) {
|
|
case DiffViewMode.UNIFIED:
|
|
classes.push('unified');
|
|
break;
|
|
case DiffViewMode.SIDE_BY_SIDE:
|
|
classes.push('sideBySide');
|
|
break;
|
|
default:
|
|
throw Error('Invalid view mode: ', viewMode);
|
|
}
|
|
if (Gerrit.hiddenscroll) {
|
|
classes.push('hiddenscroll');
|
|
}
|
|
if (loggedIn) {
|
|
classes.push('canComment');
|
|
}
|
|
if (displayLine) {
|
|
classes.push('displayLine');
|
|
}
|
|
return classes.join(' ');
|
|
},
|
|
|
|
_handleTap(e) {
|
|
const el = Polymer.dom(e).localTarget;
|
|
|
|
if (el.classList.contains('showContext')) {
|
|
this.$.diffBuilder.showContext(e.detail.groups, e.detail.section);
|
|
} else if (el.classList.contains('lineNum')) {
|
|
this.addDraftAtLine(el);
|
|
} else if (el.tagName === 'HL' ||
|
|
el.classList.contains('content') ||
|
|
el.classList.contains('contentText')) {
|
|
const target = this.$.diffBuilder.getLineElByChild(el);
|
|
if (target) { this._selectLine(target); }
|
|
}
|
|
},
|
|
|
|
_selectLine(el) {
|
|
this.fire('line-selected', {
|
|
side: el.classList.contains('left') ? DiffSide.LEFT : DiffSide.RIGHT,
|
|
number: el.getAttribute('data-value'),
|
|
path: this.path,
|
|
});
|
|
},
|
|
|
|
addDraftAtLine(el) {
|
|
this._selectLine(el);
|
|
if (!this._isValidElForComment(el)) { return; }
|
|
|
|
const value = el.getAttribute('data-value');
|
|
let lineNum;
|
|
if (value !== GrDiffLine.FILE) {
|
|
lineNum = parseInt(value, 10);
|
|
if (isNaN(lineNum)) {
|
|
this.fire('show-alert', {message: ERR_INVALID_LINE + value});
|
|
return;
|
|
}
|
|
}
|
|
this._createComment(el, lineNum);
|
|
},
|
|
|
|
_handleCreateRangeComment(e) {
|
|
const range = e.detail.range;
|
|
const side = e.detail.side;
|
|
const lineNum = range.end_line;
|
|
const lineEl = this.$.diffBuilder.getLineElByNumber(lineNum, side);
|
|
|
|
if (this._isValidElForComment(lineEl)) {
|
|
this._createComment(lineEl, lineNum, side, range);
|
|
}
|
|
},
|
|
|
|
/** @return {boolean} */
|
|
_isValidElForComment(el) {
|
|
if (!this.loggedIn) {
|
|
this.fire('show-auth-required');
|
|
return false;
|
|
}
|
|
const patchNum = el.classList.contains(DiffSide.LEFT) ?
|
|
this.patchRange.basePatchNum :
|
|
this.patchRange.patchNum;
|
|
|
|
const isEdit = this.patchNumEquals(patchNum, this.EDIT_NAME);
|
|
const isEditBase = this.patchNumEquals(patchNum, this.PARENT_NAME) &&
|
|
this.patchNumEquals(this.patchRange.patchNum, this.EDIT_NAME);
|
|
|
|
if (isEdit) {
|
|
this.fire('show-alert', {message: ERR_COMMENT_ON_EDIT});
|
|
return false;
|
|
} else if (isEditBase) {
|
|
this.fire('show-alert', {message: ERR_COMMENT_ON_EDIT_BASE});
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
|
|
/**
|
|
* @param {!Object} lineEl
|
|
* @param {number=} lineNum
|
|
* @param {string=} side
|
|
* @param {!Object=} range
|
|
*/
|
|
_createComment(lineEl, lineNum=undefined, side=undefined, range=undefined) {
|
|
const contentText = this.$.diffBuilder.getContentByLineEl(lineEl);
|
|
const contentEl = contentText.parentElement;
|
|
side = side ||
|
|
this._getCommentSideByLineAndContent(lineEl, contentEl);
|
|
const patchForNewThreads = this._getPatchNumByLineAndContent(
|
|
lineEl, contentEl);
|
|
const isOnParent =
|
|
this._getIsParentCommentByLineAndContent(lineEl, contentEl);
|
|
this.dispatchEvent(new CustomEvent('create-comment', {
|
|
bubbles: true,
|
|
detail: {
|
|
lineNum,
|
|
side,
|
|
patchNum: patchForNewThreads,
|
|
isOnParent,
|
|
range,
|
|
},
|
|
}));
|
|
},
|
|
|
|
_getThreadGroupForLine(contentEl) {
|
|
return contentEl.querySelector('.thread-group');
|
|
},
|
|
|
|
/**
|
|
* Gets or creates a comment thread group for a specific line and side on a
|
|
* diff.
|
|
* @param {!Object} contentEl
|
|
* @param {!Gerrit.DiffSide} commentSide
|
|
* @return {!Node}
|
|
*/
|
|
_getOrCreateThreadGroup(contentEl, commentSide) {
|
|
// Check if thread group exists.
|
|
let threadGroupEl = this._getThreadGroupForLine(contentEl);
|
|
if (!threadGroupEl) {
|
|
threadGroupEl = document.createElement('div');
|
|
threadGroupEl.className = 'thread-group';
|
|
threadGroupEl.setAttribute('data-side', commentSide);
|
|
contentEl.appendChild(threadGroupEl);
|
|
}
|
|
return threadGroupEl;
|
|
},
|
|
|
|
/**
|
|
* The value to be used for the patch number of new comments created at the
|
|
* given line and content elements.
|
|
*
|
|
* In two cases of creating a comment on the left side, the patch number to
|
|
* be used should actually be right side of the patch range:
|
|
* - When the patch range is against the parent comment of a normal change.
|
|
* Such comments declare themmselves to be on the left using side=PARENT.
|
|
* - If the patch range is against the indexed parent of a merge change.
|
|
* Such comments declare themselves to be on the given parent by
|
|
* specifying the parent index via parent=i.
|
|
*
|
|
* @return {number}
|
|
*/
|
|
_getPatchNumByLineAndContent(lineEl, contentEl) {
|
|
let patchNum = this.patchRange.patchNum;
|
|
|
|
if ((lineEl.classList.contains(DiffSide.LEFT) ||
|
|
contentEl.classList.contains('remove')) &&
|
|
this.patchRange.basePatchNum !== 'PARENT' &&
|
|
!this.isMergeParent(this.patchRange.basePatchNum)) {
|
|
patchNum = this.patchRange.basePatchNum;
|
|
}
|
|
return patchNum;
|
|
},
|
|
|
|
/** @return {boolean} */
|
|
_getIsParentCommentByLineAndContent(lineEl, contentEl) {
|
|
if ((lineEl.classList.contains(DiffSide.LEFT) ||
|
|
contentEl.classList.contains('remove')) &&
|
|
(this.patchRange.basePatchNum === 'PARENT' ||
|
|
this.isMergeParent(this.patchRange.basePatchNum))) {
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
|
|
/** @return {string} */
|
|
_getCommentSideByLineAndContent(lineEl, contentEl) {
|
|
let side = 'right';
|
|
if (lineEl.classList.contains(DiffSide.LEFT) ||
|
|
contentEl.classList.contains('remove')) {
|
|
side = 'left';
|
|
}
|
|
return side;
|
|
},
|
|
|
|
_prefsObserver(newPrefs, oldPrefs) {
|
|
// Scan the preference objects one level deep to see if they differ.
|
|
let differ = !oldPrefs;
|
|
if (newPrefs && oldPrefs) {
|
|
for (const key in newPrefs) {
|
|
if (newPrefs[key] !== oldPrefs[key]) {
|
|
differ = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (differ) {
|
|
this._prefsChanged(newPrefs);
|
|
}
|
|
},
|
|
|
|
_viewModeObserver() {
|
|
this._prefsChanged(this.prefs);
|
|
},
|
|
|
|
/** @param {boolean} newValue */
|
|
_loadingChanged(newValue) {
|
|
if (newValue) {
|
|
this.cancel();
|
|
this._blame = null;
|
|
this._safetyBypass = null;
|
|
this._showWarning = false;
|
|
this.clearDiffContent();
|
|
}
|
|
},
|
|
|
|
_lineWrappingObserver() {
|
|
this._prefsChanged(this.prefs);
|
|
},
|
|
|
|
_prefsChanged(prefs) {
|
|
if (!prefs) { return; }
|
|
|
|
this._blame = null;
|
|
|
|
const stylesToUpdate = {};
|
|
|
|
if (prefs.line_wrapping) {
|
|
this._diffTableClass = 'full-width';
|
|
if (this.viewMode === 'SIDE_BY_SIDE') {
|
|
stylesToUpdate['--content-width'] = 'none';
|
|
stylesToUpdate['--line-limit'] = prefs.line_length + 'ch';
|
|
}
|
|
} else {
|
|
this._diffTableClass = '';
|
|
stylesToUpdate['--content-width'] = prefs.line_length + 'ch';
|
|
}
|
|
|
|
if (prefs.font_size) {
|
|
stylesToUpdate['--font-size'] = prefs.font_size + 'px';
|
|
}
|
|
|
|
this.updateStyles(stylesToUpdate);
|
|
|
|
if (this.diff && !this.noRenderOnPrefsChange) {
|
|
this._renderDiffTable();
|
|
}
|
|
},
|
|
|
|
_diffChanged(newValue) {
|
|
if (newValue) {
|
|
this._diffLength = this.$.diffBuilder.getDiffLength();
|
|
this._renderDiffTable();
|
|
}
|
|
},
|
|
|
|
_renderDiffTable() {
|
|
this._unobserveIncrementalNodes();
|
|
if (!this.prefs) {
|
|
this.dispatchEvent(new CustomEvent('render', {bubbles: true}));
|
|
return;
|
|
}
|
|
if (this.prefs.context === -1 &&
|
|
this._diffLength >= LARGE_DIFF_THRESHOLD_LINES &&
|
|
this._safetyBypass === null) {
|
|
this._showWarning = true;
|
|
this.dispatchEvent(new CustomEvent('render', {bubbles: true}));
|
|
return;
|
|
}
|
|
|
|
this._showWarning = false;
|
|
|
|
if (this.lineOfInterest) {
|
|
const side = this.lineOfInterest.leftSide ? 'left' : 'right';
|
|
this._keyLocations[side][this.lineOfInterest.number] = true;
|
|
}
|
|
this.$.diffBuilder.render(this._keyLocations, this._getBypassPrefs());
|
|
},
|
|
|
|
_handleRenderContent() {
|
|
this._incrementalNodeObserver = Polymer.dom(this).observeNodes(info => {
|
|
const addedThreadEls = info.addedNodes.filter(isThreadEl);
|
|
// Removed nodes do not need to be handled because all this code does is
|
|
// adding a slot for the added thread elements, and the extra slots do
|
|
// not hurt. It's probably a bigger performance cost to remove them than
|
|
// to keep them around. Medium term we can even consider to add one slot
|
|
// for each line from the start.
|
|
for (const threadEl of addedThreadEls) {
|
|
const lineNumString = threadEl.getAttribute('line-num') || 'FILE';
|
|
const commentSide = threadEl.getAttribute('comment-side');
|
|
const lineEl = this.$.diffBuilder.getLineElByNumber(
|
|
lineNumString, commentSide);
|
|
const contentText = this.$.diffBuilder.getContentByLineEl(lineEl);
|
|
const contentEl = contentText.parentElement;
|
|
const threadGroupEl = this._getOrCreateThreadGroup(
|
|
contentEl, commentSide);
|
|
// Create a slot for the thread and attach it to the thread group.
|
|
// The Polyfill has some bugs and this only works if the slot is
|
|
// attached to the group after the group is attached to the DOM.
|
|
// The thread group may already have a slot with the right name, but
|
|
// that is okay because the first matching slot is used and the rest
|
|
// are ignored.
|
|
const slot = document.createElement('slot');
|
|
slot.name = threadEl.slot;
|
|
Polymer.dom(threadGroupEl).appendChild(Gerrit.slotToContent(slot));
|
|
}
|
|
});
|
|
},
|
|
|
|
_unobserveIncrementalNodes() {
|
|
if (this._incrementalNodeObserver) {
|
|
Polymer.dom(this).unobserveNodes(this._incrementalNodeObserver);
|
|
}
|
|
},
|
|
|
|
_unobserveNodes() {
|
|
if (this._nodeObserver) {
|
|
Polymer.dom(this).unobserveNodes(this._nodeObserver);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Get the preferences object including the safety bypass context (if any).
|
|
*/
|
|
_getBypassPrefs() {
|
|
if (this._safetyBypass !== null) {
|
|
return Object.assign({}, this.prefs, {context: this._safetyBypass});
|
|
}
|
|
return this.prefs;
|
|
},
|
|
|
|
clearDiffContent() {
|
|
this._unobserveIncrementalNodes();
|
|
this.$.diffTable.innerHTML = null;
|
|
},
|
|
|
|
/** @return {!Array} */
|
|
_computeDiffHeaderItems(diffInfoRecord) {
|
|
const diffInfo = diffInfoRecord.base;
|
|
if (!diffInfo || !diffInfo.diff_header) { return []; }
|
|
return diffInfo.diff_header.filter(item => {
|
|
return !(item.startsWith('diff --git ') ||
|
|
item.startsWith('index ') ||
|
|
item.startsWith('+++ ') ||
|
|
item.startsWith('--- ') ||
|
|
item === 'Binary files differ');
|
|
});
|
|
},
|
|
|
|
/** @return {boolean} */
|
|
_computeDiffHeaderHidden(items) {
|
|
return items.length === 0;
|
|
},
|
|
|
|
_handleFullBypass() {
|
|
this._safetyBypass = FULL_CONTEXT;
|
|
this._renderDiffTable();
|
|
},
|
|
|
|
_handleLimitedBypass() {
|
|
this._safetyBypass = LIMITED_CONTEXT;
|
|
this._renderDiffTable();
|
|
},
|
|
|
|
/** @return {string} */
|
|
_computeWarningClass(showWarning) {
|
|
return showWarning ? 'warn' : '';
|
|
},
|
|
|
|
/**
|
|
* @param {string} errorMessage
|
|
* @return {string}
|
|
*/
|
|
_computeErrorClass(errorMessage) {
|
|
return errorMessage ? 'showError' : '';
|
|
},
|
|
|
|
expandAllContext() {
|
|
this._handleFullBypass();
|
|
},
|
|
|
|
/**
|
|
* Find the last chunk for the given side.
|
|
* @param {!Object} diff
|
|
* @param {boolean} leftSide true if checking the base of the diff,
|
|
* false if testing the revision.
|
|
* @return {Object|null} returns the chunk object or null if there was
|
|
* no chunk for that side.
|
|
*/
|
|
_lastChunkForSide(diff, leftSide) {
|
|
if (!diff.content.length) { return null; }
|
|
|
|
let chunkIndex = diff.content.length;
|
|
let chunk;
|
|
|
|
// Walk backwards until we find a chunk for the given side.
|
|
do {
|
|
chunkIndex--;
|
|
chunk = diff.content[chunkIndex];
|
|
} while (
|
|
// We haven't reached the beginning.
|
|
chunkIndex >= 0 &&
|
|
|
|
// The chunk doesn't have both sides.
|
|
!chunk.ab &&
|
|
|
|
// The chunk doesn't have the given side.
|
|
((leftSide && !chunk.a) || (!leftSide && !chunk.b)));
|
|
|
|
// If we reached the beginning of the diff and failed to find a chunk
|
|
// with the given side, return null.
|
|
if (chunkIndex === -1) { return null; }
|
|
|
|
return chunk;
|
|
},
|
|
|
|
/**
|
|
* Check whether the specified side of the diff has a trailing newline.
|
|
* @param {!Object} diff
|
|
* @param {boolean} leftSide true if checking the base of the diff,
|
|
* false if testing the revision.
|
|
* @return {boolean|null} Return true if the side has a trailing newline.
|
|
* Return false if it doesn't. Return null if not applicable (for
|
|
* example, if the diff has no content on the specified side).
|
|
*/
|
|
_hasTrailingNewlines(diff, leftSide) {
|
|
const chunk = this._lastChunkForSide(diff, leftSide);
|
|
if (!chunk) { return null; }
|
|
let lines;
|
|
if (chunk.ab) {
|
|
lines = chunk.ab;
|
|
} else {
|
|
lines = leftSide ? chunk.a : chunk.b;
|
|
}
|
|
return lines[lines.length - 1] === '';
|
|
},
|
|
|
|
/**
|
|
* @param {!Object} diff
|
|
* @return {string|null}
|
|
*/
|
|
_computeNewlineWarning(diff) {
|
|
const hasLeft = this._hasTrailingNewlines(diff, true);
|
|
const hasRight = this._hasTrailingNewlines(diff, false);
|
|
const messages = [];
|
|
if (hasLeft === false) {
|
|
messages.push(NO_NEWLINE_BASE);
|
|
}
|
|
if (hasRight === false) {
|
|
messages.push(NO_NEWLINE_REVISION);
|
|
}
|
|
if (!messages.length) { return null; }
|
|
return messages.join(' — ');
|
|
},
|
|
|
|
/**
|
|
* @param {string} warning
|
|
* @param {boolean} loading
|
|
* @return {string}
|
|
*/
|
|
_computeNewlineWarningClass(warning, loading) {
|
|
if (loading || !warning) { return 'newlineWarning hidden'; }
|
|
return 'newlineWarning';
|
|
},
|
|
});
|
|
})();
|