// 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', 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() { this.$.cursorManager.next(this._isFirstRowOfChunk.bind(this), target => { return target.parentNode.scrollHeight; }); 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(); }, 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('gr-diff-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]; } } }, }); })();