
1. add `@extends` for all polymer elements 2. add `@override` for all lifecycle methods 3. fix a wrong type in gr-auth Change-Id: Id9dea76b169197084d2e0b5f267459cdc4aaec3e
472 lines
13 KiB
JavaScript
472 lines
13 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';
|
|
|
|
/** @extends Polymer.Element */
|
|
class GrDiffCursor extends Polymer.mixinBehaviors([Gerrit.FireBehavior],
|
|
Polymer.GestureEventListeners(
|
|
Polymer.LegacyElementMixin(Polymer.Element))) {
|
|
static get is() { return 'gr-diff-cursor'; }
|
|
|
|
static get properties() {
|
|
return {
|
|
/**
|
|
* 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. It should be only used if you want the line to be focused
|
|
* after initialization of the component and page should scroll
|
|
* to that position. This parameter should be set at most for one gr-diff
|
|
* element in the page.
|
|
*
|
|
* @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,
|
|
};
|
|
}
|
|
|
|
static get observers() {
|
|
return [
|
|
'_updateSideClass(side)',
|
|
'_diffsChanged(diffs.splices)',
|
|
];
|
|
}
|
|
|
|
/** @override */
|
|
ready() {
|
|
super.ready();
|
|
Polymer.RenderStatus.afterNextRender(this, () => {
|
|
/*
|
|
This represents the diff cursor is ready for interaction coming from
|
|
client components. It is more then Polymer "ready" lifecycle, as no
|
|
"ready" events are automatically fired by Polymer, it means
|
|
the cursor is completely interactable - in this case attached and
|
|
painted on the page. We name it "ready" instead of "rendered" as the
|
|
long-term goal is to make gr-diff-cursor a javascript class - not a DOM
|
|
element with an actual lifecycle. This will be triggered only once
|
|
per element.
|
|
*/
|
|
this.fire('ready', null, {bubbles: false});
|
|
});
|
|
}
|
|
|
|
/** @override */
|
|
attached() {
|
|
super.attached();
|
|
// Catch when users are scrolling as the view loads.
|
|
this.listen(window, 'scroll', '_handleWindowScroll');
|
|
}
|
|
|
|
/** @override */
|
|
detached() {
|
|
super.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 => 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() {
|
|
if (!this.diffRow) return null;
|
|
|
|
const hostOwner = Polymer.dom(/** @type {Node} */ (this.diffRow))
|
|
.getOwnerRoot();
|
|
if (hostOwner && hostOwner.host &&
|
|
hostOwner.host.tagName === 'GR-DIFF') {
|
|
return hostOwner.host;
|
|
}
|
|
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) {
|
|
// does not scroll during init unless requested
|
|
const scrollingBehaviorForInit = this.initialLineNumber ?
|
|
ScrollBehavior.KEEP_VISIBLE :
|
|
ScrollBehavior.NEVER;
|
|
this._scrollBehavior = scrollingBehaviorForInit;
|
|
this.reInitCursor();
|
|
}
|
|
this._scrollBehavior = ScrollBehavior.KEEP_VISIBLE;
|
|
this._focusOnMove = true;
|
|
this._listeningForScroll = false;
|
|
}
|
|
|
|
_handleDiffRenderStart() {
|
|
this._listeningForScroll = true;
|
|
}
|
|
|
|
createCommentInPlace() {
|
|
const diffWithRangeSelected = this.diffs
|
|
.find(diff => diff.isRangeSelected());
|
|
if (diffWithRangeSelected) {
|
|
diffWithRangeSelected.createRangeComment();
|
|
} else {
|
|
const line = this.getTargetLineElement();
|
|
if (line) {
|
|
this.getTargetDiffElement().addDraftAtLine(line);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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('.thread-group');
|
|
}
|
|
|
|
/**
|
|
* 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) => 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];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
customElements.define(GrDiffCursor.is, GrDiffCursor);
|
|
})();
|