Also replace existing usages and add more tests on all util methods. Change-Id: I89e0d9413153bfc115cd989ca7c66893b9709cc2
		
			
				
	
	
		
			375 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			375 lines
		
	
	
		
			12 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.
 | 
						|
 */
 | 
						|
import '../../../styles/shared-styles.js';
 | 
						|
import {addListener} from '@polymer/polymer/lib/utils/gestures.js';
 | 
						|
import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 | 
						|
import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
 | 
						|
import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 | 
						|
import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 | 
						|
import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 | 
						|
import {htmlTemplate} from './gr-diff-selection_html.js';
 | 
						|
import {DomUtilBehavior} from '../../../behaviors/dom-util-behavior/dom-util-behavior.js';
 | 
						|
import {GrRangeNormalizer} from '../gr-diff-highlight/gr-range-normalizer.js';
 | 
						|
import {querySelectorAll} from '../../../utils/dom-util.js';
 | 
						|
 | 
						|
/**
 | 
						|
 * Possible CSS classes indicating the state of selection. Dynamically added/
 | 
						|
 * removed based on where the user clicks within the diff.
 | 
						|
 */
 | 
						|
const SelectionClass = {
 | 
						|
  COMMENT: 'selected-comment',
 | 
						|
  LEFT: 'selected-left',
 | 
						|
  RIGHT: 'selected-right',
 | 
						|
  BLAME: 'selected-blame',
 | 
						|
};
 | 
						|
 | 
						|
const getNewCache = () => { return {left: null, right: null}; };
 | 
						|
 | 
						|
/**
 | 
						|
 * @extends PolymerElement
 | 
						|
 */
 | 
						|
class GrDiffSelection extends mixinBehaviors( [
 | 
						|
  DomUtilBehavior,
 | 
						|
], GestureEventListeners(
 | 
						|
    LegacyElementMixin(
 | 
						|
        PolymerElement))) {
 | 
						|
  static get template() { return htmlTemplate; }
 | 
						|
 | 
						|
  static get is() { return 'gr-diff-selection'; }
 | 
						|
 | 
						|
  static get properties() {
 | 
						|
    return {
 | 
						|
      diff: Object,
 | 
						|
      /** @type {?Object} */
 | 
						|
      _cachedDiffBuilder: Object,
 | 
						|
      _linesCache: {
 | 
						|
        type: Object,
 | 
						|
        value: getNewCache(),
 | 
						|
      },
 | 
						|
    };
 | 
						|
  }
 | 
						|
 | 
						|
  static get observers() {
 | 
						|
    return [
 | 
						|
      '_diffChanged(diff)',
 | 
						|
    ];
 | 
						|
  }
 | 
						|
 | 
						|
  /** @override */
 | 
						|
  created() {
 | 
						|
    super.created();
 | 
						|
    this.addEventListener('copy',
 | 
						|
        e => this._handleCopy(e));
 | 
						|
    addListener(this, 'down',
 | 
						|
        e => this._handleDown(e));
 | 
						|
  }
 | 
						|
 | 
						|
  /** @override */
 | 
						|
  attached() {
 | 
						|
    super.attached();
 | 
						|
    this.classList.add(SelectionClass.RIGHT);
 | 
						|
  }
 | 
						|
 | 
						|
  get diffBuilder() {
 | 
						|
    if (!this._cachedDiffBuilder) {
 | 
						|
      this._cachedDiffBuilder =
 | 
						|
          dom(this).querySelector('gr-diff-builder');
 | 
						|
    }
 | 
						|
    return this._cachedDiffBuilder;
 | 
						|
  }
 | 
						|
 | 
						|
  _diffChanged() {
 | 
						|
    this._linesCache = getNewCache();
 | 
						|
  }
 | 
						|
 | 
						|
  _handleDownOnRangeComment(node) {
 | 
						|
    if (node &&
 | 
						|
        node.nodeName &&
 | 
						|
        node.nodeName.toLowerCase() === 'gr-comment-thread') {
 | 
						|
      this._setClasses([
 | 
						|
        SelectionClass.COMMENT,
 | 
						|
        node.commentSide === 'left' ?
 | 
						|
          SelectionClass.LEFT :
 | 
						|
          SelectionClass.RIGHT,
 | 
						|
      ]);
 | 
						|
      return true;
 | 
						|
    }
 | 
						|
    return false;
 | 
						|
  }
 | 
						|
 | 
						|
  _handleDown(e) {
 | 
						|
    // Handle the down event on comment thread in Polymer 2
 | 
						|
    const handled = this._handleDownOnRangeComment(e.target);
 | 
						|
    if (handled) return;
 | 
						|
 | 
						|
    const lineEl = this.diffBuilder.getLineElByChild(e.target);
 | 
						|
    const blameSelected = this._elementDescendedFromClass(e.target, 'blame');
 | 
						|
    if (!lineEl && !blameSelected) { return; }
 | 
						|
 | 
						|
    const targetClasses = [];
 | 
						|
 | 
						|
    if (blameSelected) {
 | 
						|
      targetClasses.push(SelectionClass.BLAME);
 | 
						|
    } else {
 | 
						|
      const commentSelected =
 | 
						|
          this._elementDescendedFromClass(e.target, 'gr-comment');
 | 
						|
      const side = this.diffBuilder.getSideByLineEl(lineEl);
 | 
						|
 | 
						|
      targetClasses.push(side === 'left' ?
 | 
						|
        SelectionClass.LEFT :
 | 
						|
        SelectionClass.RIGHT);
 | 
						|
 | 
						|
      if (commentSelected) {
 | 
						|
        targetClasses.push(SelectionClass.COMMENT);
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    this._setClasses(targetClasses);
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Set the provided list of classes on the element, to the exclusion of all
 | 
						|
   * other SelectionClass values.
 | 
						|
   *
 | 
						|
   * @param {!Array<!string>} targetClasses
 | 
						|
   */
 | 
						|
  _setClasses(targetClasses) {
 | 
						|
    // Remove any selection classes that do not belong.
 | 
						|
    for (const key in SelectionClass) {
 | 
						|
      if (SelectionClass.hasOwnProperty(key)) {
 | 
						|
        const className = SelectionClass[key];
 | 
						|
        if (!targetClasses.includes(className)) {
 | 
						|
          this.classList.remove(SelectionClass[key]);
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
    // Add new selection classes iff they are not already present.
 | 
						|
    for (const _class of targetClasses) {
 | 
						|
      if (!this.classList.contains(_class)) {
 | 
						|
        this.classList.add(_class);
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  _getCopyEventTarget(e) {
 | 
						|
    return dom(e).rootTarget;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Utility function to determine whether an element is a descendant of
 | 
						|
   * another element with the particular className.
 | 
						|
   *
 | 
						|
   * @param {!Element} element
 | 
						|
   * @param {!string} className
 | 
						|
   * @return {boolean}
 | 
						|
   */
 | 
						|
  _elementDescendedFromClass(element, className) {
 | 
						|
    return this.descendedFromClass(element, className,
 | 
						|
        this.diffBuilder.diffElement);
 | 
						|
  }
 | 
						|
 | 
						|
  _handleCopy(e) {
 | 
						|
    let commentSelected = false;
 | 
						|
    const target = this._getCopyEventTarget(e);
 | 
						|
    if (target.type === 'textarea') { return; }
 | 
						|
    if (!this._elementDescendedFromClass(target, 'diff-row')) { return; }
 | 
						|
    if (this.classList.contains(SelectionClass.COMMENT)) {
 | 
						|
      commentSelected = true;
 | 
						|
    }
 | 
						|
    const lineEl = this.diffBuilder.getLineElByChild(target);
 | 
						|
    if (!lineEl) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    const side = this.diffBuilder.getSideByLineEl(lineEl);
 | 
						|
    const text = this._getSelectedText(side, commentSelected);
 | 
						|
    if (text) {
 | 
						|
      e.clipboardData.setData('Text', text);
 | 
						|
      e.preventDefault();
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  _getSelection() {
 | 
						|
    const diffHosts = querySelectorAll(document.body, 'gr-diff');
 | 
						|
    if (!diffHosts.length) return window.getSelection();
 | 
						|
 | 
						|
    const curDiffHost = diffHosts.find(diffHost => {
 | 
						|
      if (!diffHost || !diffHost.shadowRoot) return false;
 | 
						|
      const selection = diffHost.shadowRoot.getSelection();
 | 
						|
      // Pick the one with valid selection:
 | 
						|
      // https://developer.mozilla.org/en-US/docs/Web/API/Selection/type
 | 
						|
      return selection && selection.type !== 'None';
 | 
						|
    });
 | 
						|
 | 
						|
    return curDiffHost ?
 | 
						|
      curDiffHost.shadowRoot.getSelection(): window.getSelection();
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Get the text of the current selection. If commentSelected is
 | 
						|
   * true, it returns only the text of comments within the selection.
 | 
						|
   * Otherwise it returns the text of the selected diff region.
 | 
						|
   *
 | 
						|
   * @param {!string} side The side that is selected.
 | 
						|
   * @param {boolean} commentSelected Whether or not a comment is selected.
 | 
						|
   * @return {string} The selected text.
 | 
						|
   */
 | 
						|
  _getSelectedText(side, commentSelected) {
 | 
						|
    const sel = this._getSelection();
 | 
						|
    if (sel.rangeCount != 1) {
 | 
						|
      return ''; // No multi-select support yet.
 | 
						|
    }
 | 
						|
    if (commentSelected) {
 | 
						|
      return this._getCommentLines(sel, side);
 | 
						|
    }
 | 
						|
    const range = GrRangeNormalizer.normalize(sel.getRangeAt(0));
 | 
						|
    const startLineEl =
 | 
						|
        this.diffBuilder.getLineElByChild(range.startContainer);
 | 
						|
    const endLineEl = this.diffBuilder.getLineElByChild(range.endContainer);
 | 
						|
    // Happens when triple click in side-by-side mode with other side empty.
 | 
						|
    const endsAtOtherEmptySide = !endLineEl &&
 | 
						|
        range.endOffset === 0 &&
 | 
						|
        range.endContainer.nodeName === 'TD' &&
 | 
						|
        (range.endContainer.classList.contains('left') ||
 | 
						|
         range.endContainer.classList.contains('right'));
 | 
						|
    const startLineNum = parseInt(startLineEl.getAttribute('data-value'), 10);
 | 
						|
    let endLineNum;
 | 
						|
    if (endsAtOtherEmptySide) {
 | 
						|
      endLineNum = startLineNum + 1;
 | 
						|
    } else if (endLineEl) {
 | 
						|
      endLineNum = parseInt(endLineEl.getAttribute('data-value'), 10);
 | 
						|
    }
 | 
						|
 | 
						|
    return this._getRangeFromDiff(startLineNum, range.startOffset, endLineNum,
 | 
						|
        range.endOffset, side);
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Query the diff object for the selected lines.
 | 
						|
   *
 | 
						|
   * @param {number} startLineNum
 | 
						|
   * @param {number} startOffset
 | 
						|
   * @param {number|undefined} endLineNum Use undefined to get the range
 | 
						|
   *     extending to the end of the file.
 | 
						|
   * @param {number} endOffset
 | 
						|
   * @param {!string} side The side that is currently selected.
 | 
						|
   * @return {string} The selected diff text.
 | 
						|
   */
 | 
						|
  _getRangeFromDiff(startLineNum, startOffset, endLineNum, endOffset, side) {
 | 
						|
    const lines =
 | 
						|
        this._getDiffLines(side).slice(startLineNum - 1, endLineNum);
 | 
						|
    if (lines.length) {
 | 
						|
      lines[lines.length - 1] = lines[lines.length - 1]
 | 
						|
          .substring(0, endOffset);
 | 
						|
      lines[0] = lines[0].substring(startOffset);
 | 
						|
    }
 | 
						|
    return lines.join('\n');
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Query the diff object for the lines from a particular side.
 | 
						|
   *
 | 
						|
   * @param {!string} side The side that is currently selected.
 | 
						|
   * @return {!Array<string>} An array of strings indexed by line number.
 | 
						|
   */
 | 
						|
  _getDiffLines(side) {
 | 
						|
    if (this._linesCache[side]) {
 | 
						|
      return this._linesCache[side];
 | 
						|
    }
 | 
						|
    let lines = [];
 | 
						|
    const key = side === 'left' ? 'a' : 'b';
 | 
						|
    for (const chunk of this.diff.content) {
 | 
						|
      if (chunk.ab) {
 | 
						|
        lines = lines.concat(chunk.ab);
 | 
						|
      } else if (chunk[key]) {
 | 
						|
        lines = lines.concat(chunk[key]);
 | 
						|
      }
 | 
						|
    }
 | 
						|
    this._linesCache[side] = lines;
 | 
						|
    return lines;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Query the diffElement for comments and check whether they lie inside the
 | 
						|
   * selection range.
 | 
						|
   *
 | 
						|
   * @param {!Selection} sel The selection of the window.
 | 
						|
   * @param {!string} side The side that is currently selected.
 | 
						|
   * @return {string} The selected comment text.
 | 
						|
   */
 | 
						|
  _getCommentLines(sel, side) {
 | 
						|
    const range = GrRangeNormalizer.normalize(sel.getRangeAt(0));
 | 
						|
    const content = [];
 | 
						|
    // Query the diffElement for comments.
 | 
						|
    const messages = this.diffBuilder.diffElement.querySelectorAll(
 | 
						|
        `.side-by-side [data-side="${side
 | 
						|
        }"] .message *, .unified .message *`);
 | 
						|
 | 
						|
    for (let i = 0; i < messages.length; i++) {
 | 
						|
      const el = messages[i];
 | 
						|
      // Check if the comment element exists inside the selection.
 | 
						|
      if (sel.containsNode(el, true)) {
 | 
						|
        // Padded elements require newlines for accurate spacing.
 | 
						|
        if (el.parentElement.id === 'container' ||
 | 
						|
            el.parentElement.nodeName === 'BLOCKQUOTE') {
 | 
						|
          if (content.length && content[content.length - 1] !== '') {
 | 
						|
            content.push('');
 | 
						|
          }
 | 
						|
        }
 | 
						|
 | 
						|
        if (el.id === 'output' &&
 | 
						|
            !this._elementDescendedFromClass(el, 'collapsed')) {
 | 
						|
          content.push(this._getTextContentForRange(el, sel, range));
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    return content.join('\n');
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Given a DOM node, a selection, and a selection range, recursively get all
 | 
						|
   * of the text content within that selection.
 | 
						|
   * Using a domNode that isn't in the selection returns an empty string.
 | 
						|
   *
 | 
						|
   * @param {!Node} domNode The root DOM node.
 | 
						|
   * @param {!Selection} sel The selection.
 | 
						|
   * @param {!Range} range The normalized selection range.
 | 
						|
   * @return {string} The text within the selection.
 | 
						|
   */
 | 
						|
  _getTextContentForRange(domNode, sel, range) {
 | 
						|
    if (!sel.containsNode(domNode, true)) { return ''; }
 | 
						|
 | 
						|
    let text = '';
 | 
						|
    if (domNode instanceof Text) {
 | 
						|
      text = domNode.textContent;
 | 
						|
      if (domNode === range.endContainer) {
 | 
						|
        text = text.substring(0, range.endOffset);
 | 
						|
      }
 | 
						|
      if (domNode === range.startContainer) {
 | 
						|
        text = text.substring(range.startOffset);
 | 
						|
      }
 | 
						|
    } else {
 | 
						|
      for (const childNode of domNode.childNodes) {
 | 
						|
        text += this._getTextContentForRange(childNode, sel, range);
 | 
						|
      }
 | 
						|
    }
 | 
						|
    return text;
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
customElements.define(GrDiffSelection.is, GrDiffSelection);
 |