
The previous code has a bug where when you navigate to the next file using keyboard shortcuts, and the next file does not have changes, the cursor checks all regions of interest (comments and such) and because they are not diffs, moves past them to the bottom of the diff. This change allows us to specify the intention to go back to top whenever we get out of range to prevent this. Change-Id: I7d00a43115ca0f926fb7a7f1b93084ff5ef0e334
419 lines
11 KiB
JavaScript
419 lines
11 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 DiffSides = {
|
|
LEFT: 'left',
|
|
RIGHT: 'right',
|
|
};
|
|
|
|
const DiffViewMode = {
|
|
SIDE_BY_SIDE: 'SIDE_BY_SIDE',
|
|
UNIFIED: 'UNIFIED_DIFF',
|
|
};
|
|
|
|
const ScrollBehavior = {
|
|
KEEP_VISIBLE: 'keep-visible',
|
|
NEVER: 'never',
|
|
};
|
|
|
|
const LEFT_SIDE_CLASS = 'target-side-left';
|
|
const RIGHT_SIDE_CLASS = 'target-side-right';
|
|
|
|
Polymer({
|
|
is: 'gr-diff-cursor',
|
|
_legacyUndefinedCheck: true,
|
|
|
|
properties: {
|
|
/**
|
|
* Either DiffSides.LEFT or DiffSides.RIGHT.
|
|
*/
|
|
side: {
|
|
type: String,
|
|
value: DiffSides.RIGHT,
|
|
},
|
|
/** @type {!HTMLElement|undefined} */
|
|
diffRow: {
|
|
type: Object,
|
|
notify: true,
|
|
observer: '_rowChanged',
|
|
},
|
|
|
|
/**
|
|
* The diff views to cursor through and listen to.
|
|
*/
|
|
diffs: {
|
|
type: Array,
|
|
value() { return []; },
|
|
},
|
|
|
|
/**
|
|
* If set, the cursor will attempt to move to the line number (instead of
|
|
* the first chunk) the next time the diff renders. It is set back to null
|
|
* when used.
|
|
*
|
|
* @type (?number)
|
|
*/
|
|
initialLineNumber: {
|
|
type: Number,
|
|
value: null,
|
|
},
|
|
|
|
/**
|
|
* The scroll behavior for the cursor. Values are 'never' and
|
|
* 'keep-visible'. 'keep-visible' will only scroll if the cursor is beyond
|
|
* the viewport.
|
|
*/
|
|
_scrollBehavior: {
|
|
type: String,
|
|
value: ScrollBehavior.KEEP_VISIBLE,
|
|
},
|
|
|
|
_focusOnMove: {
|
|
type: Boolean,
|
|
value: true,
|
|
},
|
|
|
|
_listeningForScroll: Boolean,
|
|
},
|
|
|
|
observers: [
|
|
'_updateSideClass(side)',
|
|
'_diffsChanged(diffs.splices)',
|
|
],
|
|
|
|
attached() {
|
|
// Catch when users are scrolling as the view loads.
|
|
this.listen(window, 'scroll', '_handleWindowScroll');
|
|
},
|
|
|
|
detached() {
|
|
this.unlisten(window, 'scroll', '_handleWindowScroll');
|
|
},
|
|
|
|
moveLeft() {
|
|
this.side = DiffSides.LEFT;
|
|
if (this._isTargetBlank()) {
|
|
this.moveUp();
|
|
}
|
|
},
|
|
|
|
moveRight() {
|
|
this.side = DiffSides.RIGHT;
|
|
if (this._isTargetBlank()) {
|
|
this.moveUp();
|
|
}
|
|
},
|
|
|
|
moveDown() {
|
|
if (this._getViewMode() === DiffViewMode.SIDE_BY_SIDE) {
|
|
this.$.cursorManager.next(this._rowHasSide.bind(this));
|
|
} else {
|
|
this.$.cursorManager.next();
|
|
}
|
|
},
|
|
|
|
moveUp() {
|
|
if (this._getViewMode() === DiffViewMode.SIDE_BY_SIDE) {
|
|
this.$.cursorManager.previous(this._rowHasSide.bind(this));
|
|
} else {
|
|
this.$.cursorManager.previous();
|
|
}
|
|
},
|
|
|
|
moveToNextChunk(opt_clipToTop) {
|
|
this.$.cursorManager.next(this._isFirstRowOfChunk.bind(this),
|
|
target => {
|
|
return target.parentNode.scrollHeight;
|
|
}, opt_clipToTop);
|
|
this._fixSide();
|
|
},
|
|
|
|
moveToPreviousChunk() {
|
|
this.$.cursorManager.previous(this._isFirstRowOfChunk.bind(this));
|
|
this._fixSide();
|
|
},
|
|
|
|
moveToNextCommentThread() {
|
|
this.$.cursorManager.next(this._rowHasThread.bind(this));
|
|
this._fixSide();
|
|
},
|
|
|
|
moveToPreviousCommentThread() {
|
|
this.$.cursorManager.previous(this._rowHasThread.bind(this));
|
|
this._fixSide();
|
|
},
|
|
|
|
/**
|
|
* @param {number} number
|
|
* @param {string} side
|
|
* @param {string=} opt_path
|
|
*/
|
|
moveToLineNumber(number, side, opt_path) {
|
|
const row = this._findRowByNumberAndFile(number, side, opt_path);
|
|
if (row) {
|
|
this.side = side;
|
|
this.$.cursorManager.setCursor(row);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Get the line number element targeted by the cursor row and side.
|
|
* @return {?Element|undefined}
|
|
*/
|
|
getTargetLineElement() {
|
|
let lineElSelector = '.lineNum';
|
|
|
|
if (!this.diffRow) {
|
|
return;
|
|
}
|
|
|
|
if (this._getViewMode() === DiffViewMode.SIDE_BY_SIDE) {
|
|
lineElSelector += this.side === DiffSides.LEFT ? '.left' : '.right';
|
|
}
|
|
|
|
return this.diffRow.querySelector(lineElSelector);
|
|
},
|
|
|
|
getTargetDiffElement() {
|
|
// Find the parent diff element of the cursor row.
|
|
for (let diff = this.diffRow; diff; diff = diff.parentElement) {
|
|
if (diff.tagName === 'GR-DIFF') { return diff; }
|
|
}
|
|
return null;
|
|
},
|
|
|
|
moveToFirstChunk() {
|
|
this.$.cursorManager.moveToStart();
|
|
this.moveToNextChunk(true);
|
|
},
|
|
|
|
reInitCursor() {
|
|
this._updateStops();
|
|
if (this.initialLineNumber) {
|
|
this.moveToLineNumber(this.initialLineNumber, this.side);
|
|
this.initialLineNumber = null;
|
|
} else {
|
|
this.moveToFirstChunk();
|
|
}
|
|
},
|
|
|
|
_handleWindowScroll() {
|
|
if (this._listeningForScroll) {
|
|
this._scrollBehavior = ScrollBehavior.NEVER;
|
|
this._focusOnMove = false;
|
|
this._listeningForScroll = false;
|
|
}
|
|
},
|
|
|
|
handleDiffUpdate() {
|
|
this._updateStops();
|
|
|
|
if (!this.diffRow) {
|
|
this.reInitCursor();
|
|
}
|
|
this._scrollBehavior = ScrollBehavior.KEEP_VISIBLE;
|
|
this._focusOnMove = true;
|
|
this._listeningForScroll = false;
|
|
},
|
|
|
|
_handleDiffRenderStart() {
|
|
this._listeningForScroll = true;
|
|
},
|
|
|
|
/**
|
|
* Get an object describing the location of the cursor. Such as
|
|
* {leftSide: false, number: 123} for line 123 of the revision, or
|
|
* {leftSide: true, number: 321} for line 321 of the base patch.
|
|
* Returns null if an address is not available.
|
|
* @return {?Object}
|
|
*/
|
|
getAddress() {
|
|
if (!this.diffRow) { return null; }
|
|
|
|
// Get the line-number cell targeted by the cursor. If the mode is unified
|
|
// then prefer the revision cell if available.
|
|
let cell;
|
|
if (this._getViewMode() === DiffViewMode.UNIFIED) {
|
|
cell = this.diffRow.querySelector('.lineNum.right');
|
|
if (!cell) {
|
|
cell = this.diffRow.querySelector('.lineNum.left');
|
|
}
|
|
} else {
|
|
cell = this.diffRow.querySelector('.lineNum.' + this.side);
|
|
}
|
|
if (!cell) { return null; }
|
|
|
|
const number = cell.getAttribute('data-value');
|
|
if (!number || number === 'FILE') { return null; }
|
|
|
|
return {
|
|
leftSide: cell.matches('.left'),
|
|
number: parseInt(number, 10),
|
|
};
|
|
},
|
|
|
|
_getViewMode() {
|
|
if (!this.diffRow) {
|
|
return null;
|
|
}
|
|
|
|
if (this.diffRow.classList.contains('side-by-side')) {
|
|
return DiffViewMode.SIDE_BY_SIDE;
|
|
} else {
|
|
return DiffViewMode.UNIFIED;
|
|
}
|
|
},
|
|
|
|
_rowHasSide(row) {
|
|
const selector = (this.side === DiffSides.LEFT ? '.left' : '.right') +
|
|
' + .content';
|
|
return !!row.querySelector(selector);
|
|
},
|
|
|
|
_isFirstRowOfChunk(row) {
|
|
const parentClassList = row.parentNode.classList;
|
|
return parentClassList.contains('section') &&
|
|
parentClassList.contains('delta') &&
|
|
!row.previousSibling;
|
|
},
|
|
|
|
_rowHasThread(row) {
|
|
return row.querySelector('.comment-thread');
|
|
},
|
|
|
|
/**
|
|
* If we jumped to a row where there is no content on the current side then
|
|
* switch to the alternate side.
|
|
*/
|
|
_fixSide() {
|
|
if (this._getViewMode() === DiffViewMode.SIDE_BY_SIDE &&
|
|
this._isTargetBlank()) {
|
|
this.side = this.side === DiffSides.LEFT ?
|
|
DiffSides.RIGHT : DiffSides.LEFT;
|
|
}
|
|
},
|
|
|
|
_isTargetBlank() {
|
|
if (!this.diffRow) {
|
|
return false;
|
|
}
|
|
|
|
const actions = this._getActionsForRow();
|
|
return (this.side === DiffSides.LEFT && !actions.left) ||
|
|
(this.side === DiffSides.RIGHT && !actions.right);
|
|
},
|
|
|
|
_rowChanged(newRow, oldRow) {
|
|
if (oldRow) {
|
|
oldRow.classList.remove(LEFT_SIDE_CLASS, RIGHT_SIDE_CLASS);
|
|
}
|
|
this._updateSideClass();
|
|
},
|
|
|
|
_updateSideClass() {
|
|
if (!this.diffRow) {
|
|
return;
|
|
}
|
|
this.toggleClass(LEFT_SIDE_CLASS, this.side === DiffSides.LEFT,
|
|
this.diffRow);
|
|
this.toggleClass(RIGHT_SIDE_CLASS, this.side === DiffSides.RIGHT,
|
|
this.diffRow);
|
|
},
|
|
|
|
_isActionType(type) {
|
|
return type !== 'blank' && type !== 'contextControl';
|
|
},
|
|
|
|
_getActionsForRow() {
|
|
const actions = {left: false, right: false};
|
|
if (this.diffRow) {
|
|
actions.left = this._isActionType(
|
|
this.diffRow.getAttribute('left-type'));
|
|
actions.right = this._isActionType(
|
|
this.diffRow.getAttribute('right-type'));
|
|
}
|
|
return actions;
|
|
},
|
|
|
|
_getStops() {
|
|
return this.diffs.reduce(
|
|
(stops, diff) => {
|
|
return stops.concat(diff.getCursorStops());
|
|
}, []);
|
|
},
|
|
|
|
_updateStops() {
|
|
this.$.cursorManager.stops = this._getStops();
|
|
},
|
|
|
|
/**
|
|
* Setup and tear down on-render listeners for any diffs that are added or
|
|
* removed from the cursor.
|
|
* @private
|
|
*/
|
|
_diffsChanged(changeRecord) {
|
|
if (!changeRecord) { return; }
|
|
|
|
this._updateStops();
|
|
|
|
let splice;
|
|
let i;
|
|
for (let spliceIdx = 0;
|
|
changeRecord.indexSplices &&
|
|
spliceIdx < changeRecord.indexSplices.length;
|
|
spliceIdx++) {
|
|
splice = changeRecord.indexSplices[spliceIdx];
|
|
|
|
for (i = splice.index;
|
|
i < splice.index + splice.addedCount;
|
|
i++) {
|
|
this.listen(this.diffs[i], 'render-start', '_handleDiffRenderStart');
|
|
this.listen(this.diffs[i], 'render-content', 'handleDiffUpdate');
|
|
}
|
|
|
|
for (i = 0;
|
|
i < splice.removed && splice.removed.length;
|
|
i++) {
|
|
this.unlisten(splice.removed[i],
|
|
'render-start', '_handleDiffRenderStart');
|
|
this.unlisten(splice.removed[i],
|
|
'render-content', 'handleDiffUpdate');
|
|
}
|
|
}
|
|
},
|
|
|
|
_findRowByNumberAndFile(targetNumber, side, opt_path) {
|
|
let stops;
|
|
if (opt_path) {
|
|
const diff = this.diffs.filter(diff => diff.path === opt_path)[0];
|
|
stops = diff.getCursorStops();
|
|
} else {
|
|
stops = this.$.cursorManager.stops;
|
|
}
|
|
let selector;
|
|
for (let i = 0; i < stops.length; i++) {
|
|
selector = '.lineNum.' + side + '[data-value="' + targetNumber + '"]';
|
|
if (stops[i].querySelector(selector)) {
|
|
return stops[i];
|
|
}
|
|
}
|
|
},
|
|
});
|
|
})();
|