| // 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 DiffSides = { |
| LEFT: 'left', |
| RIGHT: 'right', |
| }; |
| |
| var DiffViewMode = { |
| SIDE_BY_SIDE: 'SIDE_BY_SIDE', |
| UNIFIED: 'UNIFIED_DIFF', |
| }; |
| |
| var LEFT_SIDE_CLASS = 'target-side-left'; |
| var RIGHT_SIDE_CLASS = 'target-side-right'; |
| |
| Polymer({ |
| is: 'gr-diff-cursor', |
| |
| properties: { |
| /** |
| * Either DiffSides.LEFT or DiffSides.RIGHT. |
| */ |
| side: { |
| type: String, |
| value: DiffSides.RIGHT, |
| }, |
| diffRow: { |
| type: Object, |
| notify: true, |
| observer: '_rowChanged', |
| }, |
| |
| /** |
| * The diff views to cursor through and listen to. |
| */ |
| diffs: { |
| type: Array, |
| value: function() { |
| return []; |
| }, |
| }, |
| |
| foldOffsetTop: { |
| type: Number, |
| value: 0, |
| }, |
| |
| /** |
| * 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. |
| */ |
| initialLineNumber: { |
| type: Number, |
| value: null, |
| }, |
| }, |
| |
| observers: [ |
| '_updateSideClass(side)', |
| '_diffsChanged(diffs.splices)', |
| ], |
| |
| moveLeft: function() { |
| this.side = DiffSides.LEFT; |
| if (this._isTargetBlank()) { |
| this.moveUp(); |
| } |
| }, |
| |
| moveRight: function() { |
| this.side = DiffSides.RIGHT; |
| if (this._isTargetBlank()) { |
| this.moveUp(); |
| } |
| }, |
| |
| moveDown: function() { |
| if (this._getViewMode() === DiffViewMode.SIDE_BY_SIDE) { |
| this.$.cursorManager.next(this._rowHasSide.bind(this)); |
| } else { |
| this.$.cursorManager.next(); |
| } |
| }, |
| |
| moveUp: function() { |
| if (this._getViewMode() === DiffViewMode.SIDE_BY_SIDE) { |
| this.$.cursorManager.previous(this._rowHasSide.bind(this)); |
| } else { |
| this.$.cursorManager.previous(); |
| } |
| }, |
| |
| moveToNextChunk: function() { |
| this.$.cursorManager.next(this._isFirstRowOfChunk.bind(this)); |
| this._fixSide(); |
| }, |
| |
| moveToPreviousChunk: function() { |
| this.$.cursorManager.previous(this._isFirstRowOfChunk.bind(this)); |
| this._fixSide(); |
| }, |
| |
| moveToNextCommentThread: function() { |
| this.$.cursorManager.next(this._rowHasThread.bind(this)); |
| this._fixSide(); |
| }, |
| |
| moveToPreviousCommentThread: function() { |
| this.$.cursorManager.previous(this._rowHasThread.bind(this)); |
| this._fixSide(); |
| }, |
| |
| moveToLineNumber: function(number, side) { |
| var row = this._findRowByNumber(number, side); |
| if (row) { |
| this.side = side; |
| this.$.cursorManager.setCursor(row); |
| } |
| }, |
| |
| /** |
| * Get the line number element targeted by the cursor row and side. |
| * @return {DOMElement} |
| */ |
| getTargetLineElement: function() { |
| var 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: function() { |
| // Find the parent diff element of the cursor row. |
| for (var diff = this.diffRow; diff; diff = diff.parentElement) { |
| if (diff.tagName === 'GR-DIFF') { return diff; } |
| } |
| return null; |
| }, |
| |
| moveToFirstChunk: function() { |
| this.$.cursorManager.moveToStart(); |
| this.moveToNextChunk(); |
| }, |
| |
| reInitCursor: function() { |
| this._updateStops(); |
| if (this.initialLineNumber) { |
| this.moveToLineNumber(this.initialLineNumber, this.side); |
| this.initialLineNumber = null; |
| } else { |
| this.moveToFirstChunk(); |
| } |
| }, |
| |
| handleDiffUpdate: function() { |
| this._updateStops(); |
| |
| if (!this.diffRow) { |
| this.reInitCursor(); |
| } |
| }, |
| |
| /** |
| * Get a short address for the location of the cursor. Such as '123' for |
| * line 123 of the revision, or 'b321' for line 321 of the base patch. |
| * Returns an empty string if an address is not available. |
| * @return {String} |
| */ |
| getAddress: function() { |
| if (!this.diffRow) { return ''; } |
| |
| // Get the line-number cell targeted by the cursor. If the mode is unified |
| // then prefer the revision cell if available. |
| var 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 ''; } |
| |
| var number = cell.getAttribute('data-value'); |
| if (!number || number === 'FILE') { return ''; } |
| |
| return (cell.matches('.left') ? 'b' : '') + number; |
| }, |
| |
| _getViewMode: function() { |
| if (!this.diffRow) { |
| return null; |
| } |
| |
| if (this.diffRow.classList.contains('side-by-side')) { |
| return DiffViewMode.SIDE_BY_SIDE; |
| } else { |
| return DiffViewMode.UNIFIED; |
| } |
| }, |
| |
| _rowHasSide: function(row) { |
| var selector = (this.side === DiffSides.LEFT ? '.left' : '.right') + |
| ' + .content'; |
| return !!row.querySelector(selector); |
| }, |
| |
| _isFirstRowOfChunk: function(row) { |
| var parentClassList = row.parentNode.classList; |
| return parentClassList.contains('section') && |
| parentClassList.contains('delta') && |
| !row.previousSibling; |
| }, |
| |
| _rowHasThread: function(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: function() { |
| if (this._getViewMode() === DiffViewMode.SIDE_BY_SIDE && |
| this._isTargetBlank()) { |
| this.side = this.side === DiffSides.LEFT ? |
| DiffSides.RIGHT : DiffSides.LEFT; |
| } |
| }, |
| |
| _isTargetBlank: function() { |
| if (!this.diffRow) { |
| return false; |
| } |
| |
| var actions = this._getActionsForRow(); |
| return (this.side === DiffSides.LEFT && !actions.left) || |
| (this.side === DiffSides.RIGHT && !actions.right); |
| }, |
| |
| _rowChanged: function(newRow, oldRow) { |
| if (oldRow) { |
| oldRow.classList.remove(LEFT_SIDE_CLASS, RIGHT_SIDE_CLASS); |
| } |
| this._updateSideClass(); |
| }, |
| |
| _updateSideClass: function() { |
| 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: function(type) { |
| return type !== 'blank' && type !== 'contextControl'; |
| }, |
| |
| _getActionsForRow: function() { |
| var 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: function() { |
| return this.diffs.reduce( |
| function(stops, diff) { |
| return stops.concat(diff.getCursorStops()); |
| }, []); |
| }, |
| |
| _updateStops: function() { |
| 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: function(changeRecord) { |
| if (!changeRecord) { return; } |
| |
| this._updateStops(); |
| |
| var splice; |
| var i; |
| for (var 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', 'handleDiffUpdate'); |
| } |
| |
| for (i = 0; |
| i < splice.removed && splice.removed.length; |
| i++) { |
| this.unlisten(splice.removed[i], 'render', 'handleDiffUpdate'); |
| } |
| } |
| }, |
| |
| _findRowByNumber: function(targetNumber, side) { |
| var stops = this.$.cursorManager.stops; |
| var selector; |
| for (var i = 0; i < stops.length; i++) { |
| selector = '.lineNum.' + side + '[data-value="' + targetNumber + '"]'; |
| if (stops[i].querySelector(selector)) { |
| return stops[i]; |
| } |
| } |
| }, |
| }); |
| })(); |