The diff cursor suspends the underlying cursor's scroll behavior while the diff is rendering if the user scrolls the page before the render is complete. However, when focus-on-move is enabled, the underlying cursor also sets focus and this causes a scroll anyway. With this change the cursor manager suspends the focus-on-move in the same way that it suspends the scroll behavior to prevent this unwanted scroll/focus. Bug: Issue 7390 Change-Id: I2bc7a06e940475b6b3c94995494f4207902acb40
		
			
				
	
	
		
			415 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			415 lines
		
	
	
		
			11 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';
 | 
						|
 | 
						|
  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];
 | 
						|
        }
 | 
						|
      }
 | 
						|
    },
 | 
						|
  });
 | 
						|
})();
 |