| <!-- |
| Copyright (C) 2015 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="gr-diff-comment-thread.html"> |
| |
| <dom-module id="gr-diff-view"> |
| <template> |
| <style> |
| :host { |
| background-color: var(--view-background-color); |
| display: block; |
| margin: 1em 1.25rem; |
| } |
| h3 { |
| border-bottom: 1px solid #eee; |
| padding: .75em 1em; |
| } |
| .mainContainer { |
| max-width: 100%; |
| overflow: auto; |
| } |
| .diffContainer { |
| display: flex; |
| font-family: 'Source Code Pro', monospace; |
| white-space: pre; |
| } |
| .diffNumbers { |
| background-color: #ddd; |
| color: #666; |
| padding: 0 .75em; |
| text-align: right; |
| } |
| .diffContent { |
| border-right: 1px solid #ddd; |
| min-width: calc(80ch + 2px); |
| overflow-x: auto; |
| padding-left: 2px; |
| width: calc(80ch + 2px); |
| } |
| .diffContainer.leftOnly .diffContent, |
| .diffContainer.rightOnly .diffContent { |
| overflow: visible; |
| } |
| .diffContainer.leftOnly .right { |
| display: none; |
| } |
| .diffContainer.rightOnly .left { |
| display: none; |
| } |
| .ruler { |
| display: block; |
| background-color: #ddd; |
| height: 1.3em; |
| position: absolute; |
| top: 0; |
| width: 1px; |
| } |
| .lineNum:before, |
| .content:before { |
| /* To ensure the height is non-zero in these elements, a |
| zero-width space is set as its content. The character |
| itself doesn't matter. Just that there is something |
| there. */ |
| content: '\200B'; |
| } |
| .content { |
| position: relative; |
| } |
| .lineNum.blank { |
| border-right: 2px solid #F34D4D; |
| margin-right: 3px; |
| } |
| .lineNum:not(.blank) { |
| cursor: pointer; |
| } |
| .lineNum:hover { |
| text-decoration: underline; |
| } |
| .lightRed { |
| background-color: #ffecec; |
| } |
| .darkRed { |
| background-color: #faa; |
| } |
| .lightGreen { |
| background-color: #eaffea; |
| } |
| .darkGreen { |
| background-color: #9f9; |
| } |
| </style> |
| <iron-ajax id="changeDetailXHR" |
| auto |
| url="[[_computeChangeDetailPath(_changeNum)]]" |
| params="[[_computeChangeDetailQueryParams()]]" |
| json-prefix=")]}'" |
| last-response="{{_change}}" |
| debounce-duration="300"></iron-ajax> |
| <iron-ajax |
| id="diffXHR" |
| url="[[_computeDiffPath(_changeNum, _patchNum, _path)]]" |
| json-prefix=")]}'" |
| on-response="_handleDiffResponse"></iron-ajax> |
| <iron-ajax |
| id="leftCommentsXHR" |
| url="[[_computeCommentsPath(_changeNum, _basePatchNum)]]" |
| json-prefix=")]}'" |
| on-response="_handleLeftCommentsResponse"></iron-ajax> |
| <iron-ajax |
| id="rightCommentsXHR" |
| url="[[_computeCommentsPath(_changeNum, _patchNum)]]" |
| json-prefix=")]}'" |
| on-response="_handleRightCommentsResponse"></iron-ajax> |
| <h3> |
| <a href$="[[_computeChangePath(_changeNum)]]">[[_changeNum]]</a><span>:</span> |
| <span>[[_change.subject]]</span> — <span>[[params.path]]</span> |
| </h3> |
| <div class="mainContainer"> |
| <div class="diffContainer" id="diffContainer"> |
| <div class="diffNumbers left" id="leftDiffNumbers"></div> |
| <div class="diffContent left" id="leftDiffContent"></div> |
| <div class="diffNumbers right" id="rightDiffNumbers"></div> |
| <div class="diffContent right" id="rightDiffContent"></div> |
| </div> |
| </div> |
| </template> |
| <script> |
| (function() { |
| 'use strict'; |
| |
| Polymer({ |
| is: 'gr-diff-view', |
| |
| properties: { |
| /** |
| * URL params passed from the router. |
| */ |
| params: { |
| type: Object, |
| observer: '_paramsChanged', |
| }, |
| rulerWidth: { |
| type: Number, |
| value: 80, |
| observer: '_rulerWidthChanged', |
| }, |
| _change: Object, |
| _changeNum: String, |
| _diff: Object, |
| _basePatchNum: String, |
| _patchNum: String, |
| _path: String, |
| _leftComments: Array, |
| _rightComments: Array, |
| _rendered: Boolean, |
| }, |
| |
| listeners: { |
| 'diffContainer.tap': '_diffContainerTapHandler', |
| }, |
| |
| _paramsChanged: function(value) { |
| this._changeNum = value.changeNum; |
| this._patchNum = value.patchNum; |
| this._basePatchNum = value.basePatchNum; |
| this._path = value.path; |
| if (!this._patchNum) { |
| this._change = null; |
| this._basePatchNum = null; |
| this._patchNum = null; |
| this._diff = null; |
| this._path = null; |
| this._leftComments = null; |
| this._rightComments = null; |
| this._rendered = false; |
| return; |
| } |
| // Assign the params here since a computed binding relying on |
| // `_basePatchNum` won't fire in the case where it's not defined. |
| this.$.diffXHR.params = this._diffQueryParams(this._basePatchNum); |
| this.$.diffXHR.generateRequest(); |
| |
| if (this._basePatchNum) { |
| this.$.leftCommentsXHR.generateRequest(); |
| } |
| this.$.rightCommentsXHR.generateRequest(); |
| }, |
| |
| _rulerWidthChanged: function(newValue, oldValue) { |
| if (newValue < 0) { |
| throw Error('ruler width must be greater than zero.'); |
| } |
| if (oldValue == 0) { |
| this._renderRulerElements(); |
| } |
| var remove = newValue == 0; |
| var rulerEls = document.querySelectorAll('.ruler'); |
| for (var i = 0; i < rulerEls.length; i++) { |
| if (remove) { |
| rulerEls[i].parentNode.removeChild(rulerEls[i]); |
| } else { |
| rulerEls[i].style.left = newValue + 'ch'; |
| } |
| } |
| }, |
| |
| _computeChangePath: function(changeNum) { |
| return '/c/' + changeNum; |
| }, |
| |
| _computeChangeDetailPath: function(changeNum) { |
| return '/changes/' + changeNum + '/detail'; |
| }, |
| |
| _computeChangeDetailQueryParams: function() { |
| var options = Changes.listChangesOptionsToHex( |
| Changes.ListChangesOption.ALL_REVISIONS |
| ); |
| return { O: options }; |
| }, |
| |
| _computeDiffPath: function(changeNum, patchNum, path) { |
| return '/changes/' + changeNum + '/revisions/' + patchNum + '/files/' + |
| encodeURIComponent(path) + '/diff'; |
| }, |
| |
| _computeCommentsPath: function(changeNum, patchNum) { |
| return '/changes/' + changeNum + '/revisions/' + patchNum + '/comments'; |
| }, |
| |
| _diffQueryParams: function(basePatchNum) { |
| var params = { |
| context: 'ALL', |
| intraline: null |
| }; |
| if (!!basePatchNum) { |
| params.base = basePatchNum; |
| } |
| return params; |
| }, |
| |
| _diffContainerTapHandler: function(e) { |
| var el = e.detail.sourceEvent.target; |
| if (el.classList.contains('lineNum')) { |
| // TODO: Implement adding draft comments. |
| } |
| }, |
| |
| _handleLeftCommentsResponse: function(e, req) { |
| this._leftComments = e.detail.response[this._path] || []; |
| this._maybeRenderDiff(this._diff, this._leftComments, |
| this._rightComments); |
| }, |
| |
| _handleRightCommentsResponse: function(e, req) { |
| this._rightComments = e.detail.response[this._path] || []; |
| this._maybeRenderDiff(this._diff, this._leftComments, |
| this._rightComments); |
| }, |
| |
| _handleDiffResponse: function(e, req) { |
| this._diff = e.detail.response; |
| this._maybeRenderDiff(this._diff, this._leftComments, |
| this._rightComments); |
| }, |
| |
| _threadID: function(patchNum, lineNum) { |
| return 'thread-' + patchNum + '-' + lineNum; |
| }, |
| |
| _renderComments: function(comments, patchNum) { |
| // Group the comments by line number. Absense of a line number indicates |
| // a top-level file comment. |
| var threads = {}; |
| |
| for (var i = 0; i < comments.length; i++) { |
| var line = comments[i].line || 'FILE'; |
| if (threads[line] == null) { |
| threads[line] = [] |
| } |
| threads[line].push(comments[i]); |
| } |
| for (var lineNum in threads) { |
| this._addThread(threads[lineNum], patchNum, lineNum); |
| } |
| }, |
| |
| _addThread: function(comments, patchNum, lineNum) { |
| var el = document.createElement('gr-diff-comment-thread'); |
| el.comments = comments; |
| var threadID = this._threadID(patchNum, lineNum); |
| el.setAttribute('data-thread-id', threadID); |
| |
| // Find the element that the thread should be appended after. In the |
| // case of a file comment, it will be appended after the first line. |
| // TODO: Show file comment above the file itself. |
| var fileComment = lineNum == 'FILE'; |
| if (fileComment) { |
| lineNum = 1; |
| } |
| var contentEl = this.$$('.content' + |
| '[data-patch-num="' + patchNum + '"]' + |
| '[data-line-num="' + lineNum + '"]'); |
| var rowNum = contentEl.getAttribute('data-row-num'); |
| el.addEventListener('gr-diff-comment-thread-height-changed', |
| this._handleCommentThreadHeightChange.bind(this, rowNum, threadID)); |
| |
| Polymer.dom(contentEl.parentNode).insertBefore( |
| el, contentEl.nextSibling); |
| }, |
| |
| _handleCommentThreadHeightChange: function(rowNum, threadID, e) { |
| // Adjust the filler element heights if they're present in the DOM. |
| var els = this.querySelectorAll( |
| '.js-threadFiller[data-thread-id="' + threadID + '"]'); |
| if (els.length > 0) { |
| for (var i = 0; i < els.length; i++) { |
| els[i].style.height = e.detail.height + 'px'; |
| } |
| return; |
| } |
| |
| // Create the filler elements if they're not already present. |
| var els = this.querySelectorAll('[data-row-num="' + rowNum + '"]'); |
| for (var i = 0; i < els.length; i++) { |
| // Is this is the side with the comment? Skip if so. |
| if (els[i].nextSibling && |
| els[i].nextSibling.tagName == 'GR-DIFF-COMMENT-THREAD') { |
| continue; |
| } |
| var fillerEl = document.createElement('div'); |
| fillerEl.setAttribute('data-thread-id', threadID); |
| fillerEl.className = 'js-threadFiller'; |
| fillerEl.style.height = e.detail.height + 'px'; |
| Polymer.dom(els[i].parentNode).insertBefore( |
| fillerEl, els[i].nextSibling); |
| } |
| }, |
| |
| _maybeRenderDiff: function(diff, leftComments, rightComments) { |
| if (this._rendered) { |
| throw Error('diff has already been rendered'); |
| } |
| if (!diff || !diff.content) { return; } |
| if (this._basePatchNum && leftComments == null) { return; } |
| if (rightComments == null) { return; } |
| |
| this.$.diffContainer.classList.toggle('rightOnly', |
| diff.change_type == Changes.DiffType.ADDED); |
| this.$.diffContainer.classList.toggle('leftOnly', |
| diff.change_type == Changes.DiffType.DELETED); |
| |
| var initialLineNum = 0 + (diff.content.skip || 0); |
| var ctx = { |
| rowNum: 0, |
| left: { |
| lineNum: initialLineNum, |
| content: '', |
| cssClass: '', |
| }, |
| right: { |
| lineNum: initialLineNum, |
| content: '', |
| cssClass: '', |
| } |
| }; |
| for (var i = 0; i < diff.content.length; i++) { |
| this._addDiffChunk(ctx, diff.content[i]); |
| } |
| |
| if (leftComments) { |
| this._renderComments(leftComments, this._basePatchNum); |
| } |
| if (rightComments) { |
| this._renderComments(rightComments, this._patchNum); |
| } |
| |
| if (this.rulerWidth) { |
| this._renderRulerElements(); |
| } |
| |
| this._rendered = true; |
| }, |
| |
| _addDiffChunk: function(ctx, diffChunk) { |
| // Simplest case where both sides have the same content. |
| if (diffChunk.ab) { |
| for (var i = 0; i < diffChunk.ab.length; i++) { |
| ctx.left.lineNum++; |
| ctx.right.lineNum++; |
| ctx.left.content = ctx.right.content = diffChunk.ab[i]; |
| ctx.left.cssClass = ctx.right.cssClass = null; |
| this._addRow(ctx); |
| } |
| return; |
| } |
| |
| if (diffChunk.a) { |
| ctx.left.cssClass = 'lightRed'; |
| } else { |
| delete(ctx.left.cssClass); |
| } |
| if (diffChunk.b) { |
| ctx.right.cssClass = 'lightGreen'; |
| } else { |
| delete(ctx.right.cssClass); |
| } |
| |
| var aLen = (diffChunk.a && diffChunk.a.length) || 0; |
| var bLen = (diffChunk.b && diffChunk.b.length) || 0; |
| var maxLen = Math.max(aLen, bLen); |
| for (var i = 0; i < maxLen; i++) { |
| if (diffChunk.a && i < diffChunk.a.length) { |
| ctx.left.lineNum++; |
| ctx.left.content = diffChunk.a[i]; |
| } else { |
| delete(ctx.left.content); |
| } |
| if (diffChunk.b && i < diffChunk.b.length) { |
| ctx.right.lineNum++; |
| ctx.right.content = diffChunk.b[i]; |
| } else { |
| delete(ctx.right.content); |
| } |
| this._addRow(ctx); |
| } |
| }, |
| |
| _addRow: function(ctx) { |
| var leftLineNumEl = this._createElement('div', 'lineNum'); |
| var leftColEl = this._createElement('div', 'content'); |
| var rightLineNumEl = this._createElement('div', 'lineNum'); |
| var rightColEl = this._createElement('div', 'content'); |
| |
| [leftColEl, |
| rightColEl, |
| leftLineNumEl, |
| rightLineNumEl].forEach(function(el) { |
| el.setAttribute('data-row-num', ctx.rowNum); |
| }); |
| |
| var self = this; |
| if (this._basePatchNum) { |
| [leftLineNumEl, leftColEl].forEach(function(el) { |
| el.setAttribute('data-patch-num', self._basePatchNum); |
| }); |
| } |
| [rightLineNumEl, rightColEl].forEach(function(el) { |
| el.setAttribute('data-patch-num', self._patchNum); |
| }); |
| |
| if (ctx.left.content != null) { |
| leftLineNumEl.textContent = ctx.left.lineNum; |
| [leftLineNumEl, leftColEl].forEach(function(el) { |
| el.setAttribute('data-line-num', ctx.left.lineNum); |
| }); |
| } else { |
| leftLineNumEl.classList.add('blank'); |
| } |
| if (ctx.right.content != null) { |
| rightLineNumEl.textContent = ctx.right.lineNum; |
| [rightLineNumEl, rightColEl].forEach(function(el) { |
| el.setAttribute('data-line-num', ctx.right.lineNum); |
| }); |
| } else { |
| rightLineNumEl.classList.add('blank'); |
| } |
| |
| // Content must be defined to prevent the HTML from showing 'undefined'. |
| // Additionally, a newline is used place of an empty string to ensure |
| // copy/paste works correctly. |
| ctx.left.content = ctx.left.content || '\n'; |
| ctx.right.content = ctx.right.content || '\n'; |
| |
| if (!!ctx.left.cssClass) { |
| leftColEl.classList.add(ctx.left.cssClass); |
| } |
| if (!!ctx.right.cssClass) { |
| rightColEl.classList.add(ctx.right.cssClass); |
| } |
| |
| var leftHTML = util.escapeHTML(ctx.left.content); |
| var rightHTML = util.escapeHTML(ctx.right.content); |
| |
| // If the html is equivalent to the text then it didn't get highlighted |
| // or escaped. Use textContent which is faster than innerHTML. |
| if (ctx.left.content == leftHTML) { |
| leftColEl.textContent = ctx.left.content; |
| } else { |
| leftColEl.innerHTML = leftHTML; |
| } |
| if (ctx.right.content == rightHTML) { |
| rightColEl.textContent = ctx.right.content; |
| } else { |
| rightColEl.innerHTML = rightHTML; |
| } |
| |
| this.$.leftDiffNumbers.appendChild(leftLineNumEl); |
| this.$.leftDiffContent.appendChild(leftColEl); |
| this.$.rightDiffNumbers.appendChild(rightLineNumEl); |
| this.$.rightDiffContent.appendChild(rightColEl); |
| |
| ctx.rowNum++; |
| }, |
| |
| _renderRulerElements: function() { |
| var contentEls = this.querySelectorAll('.content'); |
| for (var i = 0; i < contentEls.length; i++) { |
| var rulerEl = this._createElement('i', 'ruler'); |
| rulerEl.style.left = this.rulerWidth + 'ch'; |
| contentEls[i].appendChild(rulerEl); |
| } |
| }, |
| |
| _createElement: function(tagName, className) { |
| var el = document.createElement(tagName); |
| // When Shady DOM is being used, these classes are added to account for |
| // Polymer's polyfill behavior. In order to guarantee sufficient |
| // specificity within the CSS rules, these are added to every element. |
| // Since the Polymer DOM utility functions (which would do this |
| // automatically) are not being used for performance reasons, this is |
| // done manually. |
| el.className = 'style-scope gr-diff-view ' + className; |
| return el; |
| }, |
| }); |
| })(); |
| </script> |
| </dom-module> |