| // 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'; |
| |
| /** |
| * 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}; }; |
| |
| Polymer({ |
| is: 'gr-diff-selection', |
| |
| properties: { |
| diff: Object, |
| /** @type {?Object} */ |
| _cachedDiffBuilder: Object, |
| _linesCache: { |
| type: Object, |
| value: getNewCache(), |
| }, |
| }, |
| |
| observers: [ |
| '_diffChanged(diff)', |
| ], |
| |
| listeners: { |
| copy: '_handleCopy', |
| down: '_handleDown', |
| }, |
| |
| attached() { |
| this.classList.add(SelectionClass.RIGHT); |
| }, |
| |
| get diffBuilder() { |
| if (!this._cachedDiffBuilder) { |
| this._cachedDiffBuilder = |
| Polymer.dom(this).querySelector('gr-diff-builder'); |
| } |
| return this._cachedDiffBuilder; |
| }, |
| |
| _diffChanged() { |
| this._linesCache = getNewCache(); |
| }, |
| |
| _handleDown(e) { |
| 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-diff-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 Polymer.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) { |
| while (!element.classList.contains(className)) { |
| if (!element.parentElement || |
| element === this.diffBuilder.diffElement) { |
| return false; |
| } |
| element = element.parentElement; |
| } |
| return true; |
| }, |
| |
| _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(); |
| } |
| }, |
| |
| /** |
| * Get the text of the current window 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 = window.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); |
| const startLineNum = parseInt(startLineEl.getAttribute('data-value'), 10); |
| const 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} endLineNum |
| * @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; |
| }, |
| }); |
| })(); |