| /** |
| * @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 '../../../scripts/bundled-polymer.js'; |
| |
| import '../gr-diff-highlight/gr-annotation.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-ranged-comment-layer_html.js'; |
| |
| // Polymer 1 adds # before array's key, while Polymer 2 doesn't |
| const HOVER_PATH_PATTERN = /^(commentRanges\.\#?\d+)\.hovering$/; |
| |
| const RANGE_HIGHLIGHT = 'style-scope gr-diff range'; |
| const HOVER_HIGHLIGHT = 'style-scope gr-diff rangeHighlight'; |
| |
| /** @extends Polymer.Element */ |
| class GrRangedCommentLayer extends GestureEventListeners( |
| LegacyElementMixin( |
| PolymerElement)) { |
| static get template() { return htmlTemplate; } |
| |
| static get is() { return 'gr-ranged-comment-layer'; } |
| /** |
| * Fired when the range in a range comment was malformed and had to be |
| * normalized. |
| * |
| * It's `detail` has a `lineNum` and `side` parameter. |
| * |
| * @event normalize-range |
| */ |
| |
| static get properties() { |
| return { |
| /** @type {!Array<!Gerrit.HoveredRange>} */ |
| commentRanges: Array, |
| _listeners: { |
| type: Array, |
| value() { return []; }, |
| }, |
| _rangesMap: { |
| type: Object, |
| value() { return {left: {}, right: {}}; }, |
| }, |
| }; |
| } |
| |
| static get observers() { |
| return [ |
| '_handleCommentRangesChange(commentRanges.*)', |
| ]; |
| } |
| |
| get styleModuleName() { |
| return 'gr-ranged-comment-styles'; |
| } |
| |
| /** |
| * Layer method to add annotations to a line. |
| * |
| * @param {!HTMLElement} el The DIV.contentText element to apply the |
| * annotation to. |
| * @param {!HTMLElement} lineNumberEl |
| * @param {!Object} line The line object. (GrDiffLine) |
| */ |
| annotate(el, lineNumberEl, line) { |
| let ranges = []; |
| if (line.type === GrDiffLine.Type.REMOVE || ( |
| line.type === GrDiffLine.Type.BOTH && |
| el.getAttribute('data-side') !== 'right')) { |
| ranges = ranges.concat(this._getRangesForLine(line, 'left')); |
| } |
| if (line.type === GrDiffLine.Type.ADD || ( |
| line.type === GrDiffLine.Type.BOTH && |
| el.getAttribute('data-side') !== 'left')) { |
| ranges = ranges.concat(this._getRangesForLine(line, 'right')); |
| } |
| |
| for (const range of ranges) { |
| GrAnnotation.annotateElement(el, range.start, |
| range.end - range.start, |
| range.hovering ? HOVER_HIGHLIGHT : RANGE_HIGHLIGHT); |
| } |
| } |
| |
| /** |
| * Register a listener for layer updates. |
| * |
| * @param {function(number, number, string)} fn The update handler function. |
| * Should accept as arguments the line numbers for the start and end of |
| * the update and the side as a string. |
| */ |
| addListener(fn) { |
| this._listeners.push(fn); |
| } |
| |
| /** |
| * Notify Layer listeners of changes to annotations. |
| * |
| * @param {number} start The line where the update starts. |
| * @param {number} end The line where the update ends. |
| * @param {string} side The side of the update. ('left' or 'right') |
| */ |
| _notifyUpdateRange(start, end, side) { |
| for (const listener of this._listeners) { |
| listener(start, end, side); |
| } |
| } |
| |
| /** |
| * Handle change in the ranges by updating the ranges maps and by |
| * emitting appropriate update notifications. |
| * |
| * @param {Object} record The change record. |
| */ |
| _handleCommentRangesChange(record) { |
| if (!record) return; |
| |
| // If the entire set of comments was changed. |
| if (record.path === 'commentRanges') { |
| this._rangesMap = {left: {}, right: {}}; |
| for (const {side, range, hovering} of record.value) { |
| this._updateRangesMap( |
| side, range, hovering, (forLine, start, end, hovering) => { |
| forLine.push({start, end, hovering}); |
| }); |
| } |
| } |
| |
| // If the change only changed the `hovering` property of a comment. |
| const match = record.path.match(HOVER_PATH_PATTERN); |
| if (match) { |
| // The #number indicates the key of that item in the array |
| // not the index, especially in polymer 1. |
| const {side, range, hovering} = this.get(match[1]); |
| |
| this._updateRangesMap( |
| side, range, hovering, (forLine, start, end, hovering) => { |
| const index = forLine.findIndex(lineRange => |
| lineRange.start === start && lineRange.end === end); |
| forLine[index].hovering = hovering; |
| }); |
| } |
| |
| // If comments were spliced in or out. |
| if (record.path === 'commentRanges.splices') { |
| for (const indexSplice of record.value.indexSplices) { |
| const removed = indexSplice.removed; |
| for (const {side, range, hovering} of removed) { |
| this._updateRangesMap( |
| side, range, hovering, (forLine, start, end) => { |
| const index = forLine.findIndex(lineRange => |
| lineRange.start === start && lineRange.end === end); |
| forLine.splice(index, 1); |
| }); |
| } |
| const added = indexSplice.object.slice( |
| indexSplice.index, indexSplice.index + indexSplice.addedCount); |
| for (const {side, range, hovering} of added) { |
| this._updateRangesMap( |
| side, range, hovering, (forLine, start, end, hovering) => { |
| forLine.push({start, end, hovering}); |
| }); |
| } |
| } |
| } |
| } |
| |
| _updateRangesMap(side, range, hovering, operation) { |
| const forSide = this._rangesMap[side] || (this._rangesMap[side] = {}); |
| for (let line = range.start_line; line <= range.end_line; line++) { |
| const forLine = forSide[line] || (forSide[line] = []); |
| const start = line === range.start_line ? range.start_character : 0; |
| const end = line === range.end_line ? range.end_character : -1; |
| operation(forLine, start, end, hovering); |
| } |
| this._notifyUpdateRange(range.start_line, range.end_line, side); |
| } |
| |
| _getRangesForLine(line, side) { |
| const lineNum = side === 'left' ? line.beforeNumber : line.afterNumber; |
| const ranges = this.get(['_rangesMap', side, lineNum]) || []; |
| return ranges |
| .map(range => { |
| // Make a copy, so that the normalization below does not mess with |
| // our map. |
| range = Object.assign({}, range); |
| range.end = range.end === -1 ? line.text.length : range.end; |
| |
| // Normalize invalid ranges where the start is after the end but the |
| // start still makes sense. Set the end to the end of the line. |
| // @see Issue 5744 |
| if (range.start >= range.end && range.start < line.text.length) { |
| range.end = line.text.length; |
| this.dispatchEvent(new CustomEvent('normalize-range', { |
| bubbles: true, |
| composed: true, |
| detail: {lineNum, side}, |
| })); |
| } |
| |
| return range; |
| }) |
| // Sort the ranges so that hovering highlights are on top. |
| .sort((a, b) => (a.hovering && !b.hovering ? 1 : 0)); |
| } |
| } |
| |
| customElements.define(GrRangedCommentLayer.is, GrRangedCommentLayer); |