Wyatt Allen f93f1c24ee Add toggle to PolyGerrit diff view to switch between diff styles
The gr-diff element was already capable of rendering diffs in either
side-by-side or unified style, but was configured by default to use
side-by-side. The GWT diff UI included an icon button toggle to switch
between these two styles above and to the right of the diff near the
"Diff View Preferences" button. This change introduces a selector in the
PolyGerrit UI to allow similar switching behavior.

Previously, the diff mode was encoded as a private property of the
gr-diff element. Now that the switcher is introduced at the same level
as the "Diff View Preferences" button (one level up from the gr-diff
element) it is changed to be a public property of the element so that it
can be changed through the parent view (in this case the gr-diff-view
element).

A dropdown is added to the gr-diff-view based in style on the
gr-patch-range-select element (which plays a similar role). This
dropdown has options for either of the two diff modes and controls the
diff display through a property on the view.

The view gets a new, computed property called _diffMode which encodes
which diff state should be displayed based on the changeViewState and
the user's preferences. The user's choice of diff view is persisted
across diff views, but is reverted to the preference when the change
view is visited.

A test is added to the gr-diff-view element.

Bug: Issue 3909
Change-Id: I00d93500f8d210394acc9c8f3398c9406ae74276
2016-05-09 15:56:23 -07:00

460 lines
13 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';
var DiffViewMode = {
SIDE_BY_SIDE: 'SIDE_BY_SIDE',
UNIFIED: 'UNIFIED_DIFF',
};
var DiffSide = {
LEFT: 'left',
RIGHT: 'right',
};
Polymer({
is: 'gr-diff',
/**
* Fired when the diff is rendered.
*
* @event render
*/
properties: {
changeNum: String,
patchRange: Object,
path: String,
prefs: Object,
projectConfig: {
type: Object,
observer: '_projectConfigChanged',
},
_loggedIn: {
type: Boolean,
value: false,
},
viewMode: {
type: String,
value: DiffViewMode.SIDE_BY_SIDE,
},
_diff: Object,
_diffBuilder: Object,
_selectionSide: {
type: String,
observer: '_selectionSideChanged',
},
_comments: Object,
_focusedSection: {
type: Number,
value: -1,
},
_focusedThread: {
type: Number,
value: -1,
},
},
observers: [
'_prefsChanged(prefs.*, viewMode)',
],
attached: function() {
this._getLoggedIn().then(function(loggedIn) {
this._loggedIn = loggedIn;
}.bind(this));
this.addEventListener('thread-discard',
this._handleThreadDiscard.bind(this));
this.addEventListener('comment-discard',
this._handleCommentDiscard.bind(this));
},
reload: function() {
this._clearDiffContent();
var promises = [];
promises.push(this._getDiff().then(function(diff) {
this._diff = diff;
}.bind(this)));
promises.push(this._getDiffCommentsAndDrafts().then(function(comments) {
this._comments = comments;
}.bind(this)));
return Promise.all(promises).then(function() {
if (this.prefs) {
this._render();
}
}.bind(this));
},
showDiffPreferences: function() {
this.$.prefsOverlay.open();
},
scrollToLine: function(lineNum) {
if (isNaN(lineNum) || lineNum < 1) { return; }
var lineEls = Polymer.dom(this.root).querySelectorAll(
'.lineNum[data-value="' + lineNum + '"]');
// Always choose the right side.
var el = lineEls.length === 2 ? lineEls[1] : lineEls[0];
this._scrollToElement(el);
},
scrollToNextDiffChunk: function() {
this._focusedSection = this._advanceElementWithinNodeList(
this._getDeltaSections(), this._focusedSection, 1);
},
scrollToPreviousDiffChunk: function() {
this._focusedSection = this._advanceElementWithinNodeList(
this._getDeltaSections(), this._focusedSection, -1);
},
scrollToNextCommentThread: function() {
this._focusedThread = this._advanceElementWithinNodeList(
this._getCommentThreads(), this._focusedThread, 1);
},
scrollToPreviousCommentThread: function() {
this._focusedThread = this._advanceElementWithinNodeList(
this._getCommentThreads(), this._focusedThread, -1);
},
_advanceElementWithinNodeList: function(els, curIndex, direction) {
var idx = Math.max(0, Math.min(els.length - 1, curIndex + direction));
if (curIndex !== idx) {
this._scrollToElement(els[idx]);
return idx;
}
return curIndex;
},
_getCommentThreads: function() {
return Polymer.dom(this.root).querySelectorAll('gr-diff-comment-thread');
},
_getDeltaSections: function() {
return Polymer.dom(this.root).querySelectorAll('.section.delta');
},
_scrollToElement: function(el) {
if (!el) { return; }
// Calculate where the element 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 / 2));
},
_computeContainerClass: function(loggedIn, viewMode) {
var 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 (loggedIn) {
classes.push('canComment');
}
return classes.join(' ');
},
_handleTap: function(e) {
var el = Polymer.dom(e).rootTarget;
if (el.classList.contains('showContext')) {
this._showContext(e.detail.group, e.detail.section);
} else if (el.classList.contains('lineNum')) {
this._handleLineTap(el);
}
},
_handleLineTap: function(el) {
this._getLoggedIn().then(function(loggedIn) {
if (!loggedIn) { return; }
var value = el.getAttribute('data-value');
if (value === GrDiffLine.FILE) {
this._addDraft(el);
return;
}
var lineNum = parseInt(value, 10);
if (isNaN(lineNum)) {
throw Error('Invalid line number: ' + value);
}
this._addDraft(el, lineNum);
}.bind(this));
},
_addDraft: function(lineEl, opt_lineNum) {
var threadEl;
// Does a thread already exist at this line?
var contentEl = lineEl.nextSibling;
while (contentEl && !contentEl.classList.contains('content')) {
contentEl = contentEl.nextSibling;
}
if (contentEl.childNodes.length > 0 &&
contentEl.lastChild.nodeName === 'GR-DIFF-COMMENT-THREAD') {
threadEl = contentEl.lastChild;
} else {
var patchNum = this.patchRange.patchNum;
var side = 'REVISION';
if (contentEl.classList.contains(DiffSide.LEFT) ||
contentEl.classList.contains('remove')) {
if (this.patchRange.basePatchNum === 'PARENT') {
side = 'PARENT';
} else {
patchNum = this.patchRange.basePatchNum;
}
}
threadEl = this._builder.createCommentThread(this.changeNum, patchNum,
this.path, side, this.projectConfig);
contentEl.appendChild(threadEl);
}
threadEl.addDraft(opt_lineNum);
},
_handleThreadDiscard: function(e) {
var el = Polymer.dom(e).rootTarget;
el.parentNode.removeChild(el);
},
_handleCommentDiscard: function(e) {
var comment = Polymer.dom(e).rootTarget.comment;
this._removeComment(comment);
},
_removeComment: function(comment) {
if (!comment.id) { return; }
this._removeCommentFromSide(comment, DiffSide.LEFT) ||
this._removeCommentFromSide(comment, DiffSide.RIGHT);
},
_removeCommentFromSide: function(comment, side) {
var idx = -1;
for (var i = 0; i < this._comments[side].length; i++) {
if (this._comments[side][i].id === comment.id) {
idx = i;
break;
}
}
if (idx !== -1) {
this.splice('_comments.' + side, idx, 1);
return true;
}
return false;
},
_handleMouseDown: function(e) {
var el = Polymer.dom(e).rootTarget;
var side;
for (var node = el; node != null; node = node.parentNode) {
if (!node.classList) { continue; }
if (node.classList.contains(DiffSide.LEFT)) {
side = DiffSide.LEFT;
break;
} else if (node.classList.contains(DiffSide.RIGHT)) {
side = DiffSide.RIGHT;
break;
}
}
this._selectionSide = side;
},
_selectionSideChanged: function(side) {
if (side) {
var oppositeSide = side === DiffSide.RIGHT ?
DiffSide.LEFT : DiffSide.RIGHT;
this.customStyle['--' + side + '-user-select'] = 'text';
this.customStyle['--' + oppositeSide + '-user-select'] = 'none';
} else {
this.customStyle['--left-user-select'] = 'text';
this.customStyle['--right-user-select'] = 'text';
}
this.updateStyles();
},
_handleCopy: function(e) {
if (!e.target.classList.contains('content')) {
return;
}
var text = this._getSelectedText(this._selectionSide);
e.clipboardData.setData('Text', text);
e.preventDefault();
},
_getSelectedText: function(opt_side) {
var sel = window.getSelection();
var range = sel.getRangeAt(0);
var doc = range.cloneContents();
var selector = '.content';
if (opt_side) {
selector += '.' + opt_side;
}
var contentEls = Polymer.dom(doc).querySelectorAll(selector);
if (contentEls.length === 0) {
return doc.textContent;
}
var text = '';
for (var i = 0; i < contentEls.length; i++) {
text += contentEls[i].textContent + '\n';
}
return text;
},
_showContext: function(group, sectionEl) {
this._builder.emitGroup(group, sectionEl);
sectionEl.parentNode.removeChild(sectionEl);
},
_prefsChanged: function(prefsChangeRecord) {
var prefs = prefsChangeRecord.base;
this.customStyle['--content-width'] = prefs.line_length + 'ch';
this.updateStyles();
if (this._diff && this._comments) {
this._render();
}
},
_render: function() {
this._clearDiffContent();
this._builder = this._getDiffBuilder(this._diff, this._comments,
this.prefs);
this._builder.emitDiff(this._diff.content);
this.async(function() {
this.fire('render', null, {bubbles: false});
}.bind(this), 1);
},
_clearDiffContent: function() {
this.$.diffTable.innerHTML = null;
},
_handleGetDiffError: function(response) {
this.fire('page-error', {response: response});
},
_getDiff: function() {
return this.$.restAPI.getDiff(
this.changeNum,
this.patchRange.basePatchNum,
this.patchRange.patchNum,
this.path,
this._handleGetDiffError.bind(this));
},
_getDiffComments: function() {
return this.$.restAPI.getDiffComments(
this.changeNum,
this.patchRange.basePatchNum,
this.patchRange.patchNum,
this.path);
},
_getDiffDrafts: function() {
return this._getLoggedIn().then(function(loggedIn) {
if (!loggedIn) {
return Promise.resolve({baseComments: [], comments: []});
}
return this.$.restAPI.getDiffDrafts(
this.changeNum,
this.patchRange.basePatchNum,
this.patchRange.patchNum,
this.path);
}.bind(this));
},
_getDiffCommentsAndDrafts: function() {
var promises = [];
promises.push(this._getDiffComments());
promises.push(this._getDiffDrafts());
return Promise.all(promises).then(function(results) {
return Promise.resolve({
comments: results[0],
drafts: results[1],
});
}).then(this._normalizeDiffCommentsAndDrafts.bind(this));
},
_normalizeDiffCommentsAndDrafts: function(results) {
function markAsDraft(d) {
d.__draft = true;
return d;
}
var baseDrafts = results.drafts.baseComments.map(markAsDraft);
var drafts = results.drafts.comments.map(markAsDraft);
return Promise.resolve({
meta: {
path: this.path,
changeNum: this.changeNum,
patchRange: this.patchRange,
projectConfig: this.projectConfig,
},
left: results.comments.baseComments.concat(baseDrafts),
right: results.comments.comments.concat(drafts),
});
},
_getLoggedIn: function() {
return this.$.restAPI.getLoggedIn();
},
_getDiffBuilder: function(diff, comments, prefs) {
if (this.viewMode === DiffViewMode.SIDE_BY_SIDE) {
return new GrDiffBuilderSideBySide(diff, comments, prefs,
this.$.diffTable);
} else if (this.viewMode === DiffViewMode.UNIFIED) {
return new GrDiffBuilderUnified(diff, comments, prefs,
this.$.diffTable);
}
throw Error('Unsupported diff view mode: ' + this.viewMode);
},
_projectConfigChanged: function(projectConfig) {
var threadEls = this._getCommentThreads();
for (var i = 0; i < threadEls.length; i++) {
threadEls[i].projectConfig = projectConfig;
}
},
});
})();