| <!-- |
| 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. |
| --> |
| <link rel="import" href="../../../bower_components/polymer/polymer.html"> |
| <link rel="import" href="../../core/gr-reporting/gr-reporting.html"> |
| <link rel="import" href="../gr-diff-comment-thread/gr-diff-comment-thread.html"> |
| <link rel="import" href="../gr-diff-processor/gr-diff-processor.html"> |
| <link rel="import" href="../gr-ranged-comment-layer/gr-ranged-comment-layer.html"> |
| <link rel="import" href="../gr-syntax-layer/gr-syntax-layer.html"> |
| |
| <dom-module id="gr-diff-builder"> |
| <template> |
| <div class="contentWrapper"> |
| <content></content> |
| </div> |
| <gr-ranged-comment-layer |
| id="rangeLayer" |
| comments="[[comments]]"></gr-ranged-comment-layer> |
| <gr-syntax-layer |
| id="syntaxLayer" |
| diff="[[diff]]"></gr-syntax-layer> |
| <gr-diff-processor |
| id="processor" |
| groups="{{_groups}}"></gr-diff-processor> |
| <gr-reporting id="reporting"></gr-reporting> |
| </template> |
| <script src="../gr-diff/gr-diff-line.js"></script> |
| <script src="../gr-diff/gr-diff-group.js"></script> |
| <script src="../gr-diff-highlight/gr-annotation.js"></script> |
| <script src="gr-diff-builder.js"></script> |
| <script src="gr-diff-builder-side-by-side.js"></script> |
| <script src="gr-diff-builder-unified.js"></script> |
| <script src="gr-diff-builder-image.js"></script> |
| <script> |
| (function() { |
| 'use strict'; |
| |
| var DiffViewMode = { |
| SIDE_BY_SIDE: 'SIDE_BY_SIDE', |
| UNIFIED: 'UNIFIED_DIFF', |
| }; |
| |
| var TimingLabel = { |
| TOTAL: 'Diff Total Render', |
| CONTENT: 'Diff Content Render', |
| SYNTAX: 'Diff Syntax Render', |
| }; |
| |
| Polymer({ |
| is: 'gr-diff-builder', |
| |
| /** |
| * Fired when the diff is rendered. |
| * |
| * @event render |
| */ |
| |
| properties: { |
| diff: Object, |
| viewMode: String, |
| comments: Object, |
| isImageDiff: Boolean, |
| baseImage: Object, |
| revisionImage: Object, |
| _builder: Object, |
| _groups: Array, |
| _layers: Array, |
| _showTabs: Boolean, |
| }, |
| |
| get diffElement() { |
| return this.queryEffectiveChildren('#diffTable'); |
| }, |
| |
| observers: [ |
| '_groupsChanged(_groups.splices)', |
| ], |
| |
| attached: function() { |
| // Setup annotation layers. |
| this._layers = [ |
| this.$.syntaxLayer, |
| this._createIntralineLayer(), |
| this._createTabIndicatorLayer(), |
| this.$.rangeLayer, |
| ]; |
| |
| this.async(function() { |
| this._preRenderThread(); |
| }); |
| }, |
| |
| render: function(comments, prefs) { |
| this.$.syntaxLayer.enabled = prefs.syntax_highlighting; |
| this._showTabs = !!prefs.show_tabs; |
| |
| // Stop the processor (if it's running). |
| this.$.processor.cancel(); |
| this.$.syntaxLayer.cancel(); |
| |
| this._builder = this._getDiffBuilder(this.diff, comments, prefs); |
| |
| this.$.processor.context = prefs.context; |
| this.$.processor.keyLocations = this._getCommentLocations(comments); |
| |
| this._clearDiffContent(); |
| |
| var reporting = this.$.reporting; |
| |
| reporting.time(TimingLabel.TOTAL); |
| reporting.time(TimingLabel.CONTENT); |
| return this.$.processor.process(this.diff.content).then(function() { |
| if (this.isImageDiff) { |
| this._builder.renderDiffImages(); |
| } |
| reporting.timeEnd(TimingLabel.CONTENT); |
| reporting.time(TimingLabel.SYNTAX); |
| this.$.syntaxLayer.process().then(function() { |
| reporting.timeEnd(TimingLabel.SYNTAX); |
| reporting.timeEnd(TimingLabel.TOTAL); |
| }); |
| this.fire('render'); |
| }.bind(this)); |
| }, |
| |
| getLineElByChild: function(node) { |
| while (node) { |
| if (node instanceof Element) { |
| if (node.classList.contains('lineNum')) { |
| return node; |
| } |
| if (node.classList.contains('section')) { |
| return null; |
| } |
| } |
| node = node.previousSibling || node.parentElement; |
| } |
| return null; |
| }, |
| |
| getLineNumberByChild: function(node) { |
| var lineEl = this.getLineElByChild(node); |
| return lineEl ? |
| parseInt(lineEl.getAttribute('data-value'), 10) : null; |
| }, |
| |
| renderLineRange: function(startLine, endLine, opt_side) { |
| var groups = |
| this._builder.getGroupsByLineRange(startLine, endLine, opt_side); |
| groups.forEach(function(group) { |
| var newElement = this._builder.buildSectionElement(group); |
| var oldElement = group.element; |
| |
| // Transfer comment threads from existing section to new one. |
| var threads = Polymer.dom(newElement).querySelectorAll( |
| 'gr-diff-comment-thread'); |
| threads.forEach(function(threadEl) { |
| var lineEl = this.getLineElByChild(threadEl, oldElement); |
| if (!lineEl) { // New comment thread. |
| return; |
| } |
| var side = this.getSideByLineEl(lineEl); |
| var line = lineEl.getAttribute('data-value'); |
| var oldThreadEl = |
| this.getCommentThreadByLine(line, side, oldElement); |
| threadEl.parentNode.replaceChild(oldThreadEl, threadEl); |
| }, this); |
| |
| // Replace old group elements with new ones. |
| group.element.parentNode.replaceChild(newElement, group.element); |
| group.element = newElement; |
| }, this); |
| |
| this.async(function() { |
| this.fire('render'); |
| }, 1); |
| }, |
| |
| getContentByLine: function(lineNumber, opt_side, opt_root) { |
| return this._builder.getContentByLine(lineNumber, opt_side, opt_root); |
| }, |
| |
| getContentByLineEl: function(lineEl) { |
| var root = Polymer.dom(lineEl.parentElement); |
| var side = this.getSideByLineEl(lineEl); |
| var line = lineEl.getAttribute('data-value'); |
| return this.getContentByLine(line, side, root); |
| }, |
| |
| getLineElByNumber: function(lineNumber, opt_side) { |
| var sideSelector = !!opt_side ? ('.' + opt_side) : ''; |
| return this.diffElement.querySelector( |
| '.lineNum[data-value="' + lineNumber + '"]' + sideSelector); |
| }, |
| |
| getContentsByLineRange: function(startLine, endLine, opt_side) { |
| var result = []; |
| this._builder.findLinesByRange(startLine, endLine, opt_side, null, |
| result); |
| return result; |
| }, |
| |
| getCommentThreadByLine: function(lineNumber, opt_side, opt_root) { |
| var content = this.getContentByLine(lineNumber, opt_side, opt_root); |
| return this.getCommentThreadByContentEl(content); |
| }, |
| |
| getCommentThreadByContentEl: function(contentEl) { |
| if (contentEl.classList.contains('contentText')) { |
| contentEl = contentEl.parentElement; |
| } |
| return contentEl.querySelector('gr-diff-comment-thread'); |
| }, |
| |
| getSideByLineEl: function(lineEl) { |
| return lineEl.classList.contains(GrDiffBuilder.Side.RIGHT) ? |
| GrDiffBuilder.Side.RIGHT : GrDiffBuilder.Side.LEFT; |
| }, |
| |
| createCommentThread: function(changeNum, patchNum, path, side, |
| projectConfig) { |
| return this._builder.createCommentThread(changeNum, patchNum, path, |
| side, projectConfig); |
| }, |
| |
| emitGroup: function(group, sectionEl) { |
| this._builder.emitGroup(group, sectionEl); |
| }, |
| |
| showContext: function(newGroups, sectionEl) { |
| var groups = this._builder.groups; |
| // TODO(viktard): Polyfill findIndex for IE10. |
| var contextIndex = groups.findIndex(function(group) { |
| return group.element == sectionEl; |
| }); |
| groups.splice.apply(groups, [contextIndex, 1].concat(newGroups)); |
| |
| newGroups.forEach(function(newGroup) { |
| this._builder.emitGroup(newGroup, sectionEl); |
| }, this); |
| sectionEl.parentNode.removeChild(sectionEl); |
| |
| this.async(function() { |
| this.fire('render'); |
| }, 1); |
| }, |
| |
| _getDiffBuilder: function(diff, comments, prefs) { |
| if (this.isImageDiff) { |
| return new GrDiffBuilderImage(diff, comments, prefs, |
| this.diffElement, this.baseImage, this.revisionImage); |
| } else if (this.viewMode === DiffViewMode.SIDE_BY_SIDE) { |
| return new GrDiffBuilderSideBySide( |
| diff, comments, prefs, this.diffElement, this._layers); |
| } else if (this.viewMode === DiffViewMode.UNIFIED) { |
| return new GrDiffBuilderUnified( |
| diff, comments, prefs, this.diffElement, this._layers); |
| } |
| throw Error('Unsupported diff view mode: ' + this.viewMode); |
| }, |
| |
| _clearDiffContent: function() { |
| this.diffElement.innerHTML = null; |
| }, |
| |
| _getCommentLocations: function(comments) { |
| var result = { |
| left: {}, |
| right: {}, |
| }; |
| for (var side in comments) { |
| if (side !== GrDiffBuilder.Side.LEFT && |
| side !== GrDiffBuilder.Side.RIGHT) { |
| continue; |
| } |
| comments[side].forEach(function(c) { |
| result[side][c.line || GrDiffLine.FILE] = true; |
| }); |
| } |
| return result; |
| }, |
| |
| _groupsChanged: function(changeRecord) { |
| if (!changeRecord) { return; } |
| changeRecord.indexSplices.forEach(function(splice) { |
| var group; |
| for (var i = 0; i < splice.addedCount; i++) { |
| group = splice.object[splice.index + i]; |
| this._builder.groups.push(group); |
| this._builder.emitGroup(group); |
| } |
| }, this); |
| }, |
| |
| _createIntralineLayer: function() { |
| return { |
| addListener: function() {}, |
| |
| // Take a DIV.contentText element and a line object with intraline |
| // differences to highlight and apply them to the element as |
| // annotations. |
| annotate: function(el, line) { |
| var HL_CLASS = 'style-scope gr-diff intraline'; |
| line.highlights.forEach(function(highlight) { |
| // The start and end indices could be the same if a highlight is |
| // meant to start at the end of a line and continue onto the |
| // next one. Ignore it. |
| if (highlight.startIndex === highlight.endIndex) { return; } |
| |
| // If endIndex isn't present, continue to the end of the line. |
| var endIndex = highlight.endIndex === undefined ? |
| line.text.length : highlight.endIndex; |
| |
| GrAnnotation.annotateElement( |
| el, |
| highlight.startIndex, |
| endIndex - highlight.startIndex, |
| HL_CLASS); |
| }); |
| }, |
| }; |
| }, |
| |
| _createTabIndicatorLayer: function() { |
| var show = (function() { return this._showTabs; }).bind(this); |
| return { |
| addListener: function() {}, |
| annotate: function(el, line) { |
| // If visible tabs are disabled, do nothing. |
| if (!show()) { return; } |
| |
| // Find and annotate the locations of tabs. |
| var split = line.text.split('\t'); |
| if (!split) { return; } |
| for (var i = 0, pos = 0; i < split.length - 1; i++) { |
| // Skip forward by the length of the content |
| pos += split[i].length; |
| |
| GrAnnotation.annotateElement(el, pos, 1, |
| 'style-scope gr-diff tab-indicator'); |
| |
| // Skip forward by one tab character. |
| pos++; |
| } |
| }, |
| }; |
| }, |
| |
| /** |
| * In pages with large diffs, creating the first comment thread can be |
| * slow because nested Polymer elements (particularly |
| * iron-autogrow-textarea) add style elements to the document head, |
| * which, in turn, triggers a reflow on the page. Create a hidden |
| * thread, attach it to the page, and remove it so the stylesheet will |
| * already exist and the user's comment will be quick to load. |
| * @see https://gerrit-review.googlesource.com/c/82213/ |
| */ |
| _preRenderThread: function() { |
| var thread = document.createElement('gr-diff-comment-thread'); |
| thread.setAttribute('hidden', true); |
| thread.addDraft(); |
| var parent = Polymer.dom(this.root); |
| parent.appendChild(thread); |
| Polymer.dom.flush(); |
| parent.removeChild(thread); |
| }, |
| }); |
| })(); |
| </script> |
| </dom-module> |