// 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'; var DiffViewMode = { SIDE_BY_SIDE: 'SIDE_BY_SIDE', UNIFIED: 'UNIFIED_DIFF', }; var DiffSide = { LEFT: 'left', RIGHT: 'right', }; Polymer({ is: 'gr-diff', /** * Fired when the diff is rendered. * * @event render */ properties: { changeNum: String, patchRange: Object, path: String, prefs: Object, projectConfig: { type: Object, observer: '_projectConfigChanged', }, project: String, commit: String, isImageDiff: { type: Boolean, computed: '_computeIsImageDiff(_diff)', notify: true, }, _loggedIn: { type: Boolean, value: false, }, viewMode: { type: String, value: DiffViewMode.SIDE_BY_SIDE, }, _diff: Object, _diffBuilder: Object, _selectionSide: { type: String, observer: '_selectionSideChanged', }, _comments: Object, }, observers: [ '_prefsChanged(prefs.*, viewMode)', ], listeners: { 'thread-discard': '_handleThreadDiscard', 'comment-discard': '_handleCommentDiscard', 'comment-update': '_handleCommentUpdate', 'comment-save': '_handleCommentSave', }, attached: function() { this._getLoggedIn().then(function(loggedIn) { this._loggedIn = loggedIn; }.bind(this)); }, reload: function() { this._clearDiffContent(); var promises = []; promises.push(this._getDiff().then(function(diff) { this._diff = diff; return this._loadDiffAssets(); }.bind(this))); promises.push(this._getDiffCommentsAndDrafts().then(function(comments) { this._comments = comments; }.bind(this))); return Promise.all(promises).then(function() { if (this.prefs) { this._render(); } }.bind(this)); }, scrollToLine: function(lineNum) { if (isNaN(lineNum) || lineNum < 1) { return; } var lineEls = Polymer.dom(this.root).querySelectorAll( '.lineNum[data-value="' + lineNum + '"]'); // Always choose the right side. var el = lineEls.length === 2 ? lineEls[1] : lineEls[0]; this._scrollToElement(el); }, getCursorStops: function() { if (this.hidden) { return []; } return Polymer.dom(this.root).querySelectorAll('.diff-row'); }, addDraftAtLine: function(el) { this._getLoggedIn().then(function(loggedIn) { if (!loggedIn) { return; } var value = el.getAttribute('data-value'); if (value === GrDiffLine.FILE) { this._addDraft(el); return; } var lineNum = parseInt(value, 10); if (isNaN(lineNum)) { throw Error('Invalid line number: ' + value); } this._addDraft(el, lineNum); }.bind(this)); }, _advanceElementWithinNodeList: function(els, curIndex, direction) { var idx = Math.max(0, Math.min(els.length - 1, curIndex + direction)); if (curIndex !== idx) { this._scrollToElement(els[idx]); return idx; } return curIndex; }, _getCommentThreads: function() { return Polymer.dom(this.root).querySelectorAll('gr-diff-comment-thread'); }, _scrollToElement: function(el) { if (!el) { return; } // Calculate where the element is relative to the window. var top = el.offsetTop; for (var offsetParent = el.offsetParent; offsetParent; offsetParent = offsetParent.offsetParent) { top += offsetParent.offsetTop; } // Scroll the element to the middle of the window. Dividing by a third // instead of half the inner height feels a bit better otherwise the // element appears to be below the center of the window even when it // isn't. window.scrollTo(0, top - (window.innerHeight / 3) + (el.offsetHeight / 2)); }, _computeContainerClass: function(loggedIn, viewMode) { var classes = ['diffContainer']; switch (viewMode) { case DiffViewMode.UNIFIED: classes.push('unified'); break; case DiffViewMode.SIDE_BY_SIDE: classes.push('sideBySide'); break; default: throw Error('Invalid view mode: ', viewMode); } if (loggedIn) { classes.push('canComment'); } return classes.join(' '); }, _handleTap: function(e) { var el = Polymer.dom(e).rootTarget; if (el.classList.contains('showContext')) { this._showContext(e.detail.group, e.detail.section); } else if (el.classList.contains('lineNum')) { this.addDraftAtLine(el); } }, _addDraft: function(lineEl, opt_lineNum) { var threadEl; // Does a thread already exist at this line? var contentEl = lineEl.nextSibling; while (contentEl && !contentEl.classList.contains('content')) { contentEl = contentEl.nextSibling; } if (contentEl.childNodes.length > 0 && contentEl.lastChild.nodeName === 'GR-DIFF-COMMENT-THREAD') { threadEl = contentEl.lastChild; } else { var patchNum = this.patchRange.patchNum; var side = 'REVISION'; if (contentEl.classList.contains(DiffSide.LEFT) || contentEl.classList.contains('remove')) { if (this.patchRange.basePatchNum === 'PARENT') { side = 'PARENT'; } else { patchNum = this.patchRange.basePatchNum; } } threadEl = this._builder.createCommentThread(this.changeNum, patchNum, this.path, side, this.projectConfig); contentEl.appendChild(threadEl); } threadEl.addDraft(opt_lineNum); }, _handleThreadDiscard: function(e) { var el = Polymer.dom(e).rootTarget; el.parentNode.removeChild(el); }, _handleCommentDiscard: function(e) { var comment = e.detail.comment; this._removeComment(comment, e.target.patchNum); }, _removeComment: function(comment, opt_patchNum) { var side = this._findCommentSide(comment, opt_patchNum); this._removeCommentFromSide(comment, side); }, _findCommentSide: function(comment, opt_patchNum) { if (comment.side === 'PARENT') { return DiffSide.LEFT; } else { return this._comments.meta.patchRange.basePatchNum === opt_patchNum ? DiffSide.LEFT : DiffSide.RIGHT; } }, _handleCommentSave: function(e) { var comment = e.detail.comment; var side = this._findCommentSide(comment, e.target.patchNum); var idx = this._findDraftIndex(comment, side); this.set(['_comments', side, idx], comment); }, _handleCommentUpdate: function(e) { var comment = e.detail.comment; var side = this._findCommentSide(comment, e.target.patchNum); var idx = this._findCommentIndex(comment, side); if (idx === -1) { idx = this._findDraftIndex(comment, side); } if (idx !== -1) { // Update draft or comment. this.set(['_comments', side, idx], comment); } else { // Create new draft. this.push(['_comments', side], comment); } }, _removeCommentFromSide: function(comment, side) { var idx = this._findCommentIndex(comment, side); if (idx === -1) { idx = this._findDraftIndex(comment, side); } if (idx !== -1) { this.splice('_comments.' + side, idx, 1); } }, _findCommentIndex: function(comment, side) { if (!comment.id || !this._comments[side]) { return -1; } return this._comments[side].findIndex(function(item) { return item.id === comment.id; }); }, _findDraftIndex: function(comment, side) { if (!comment.__draftID || !this._comments[side]) { return -1; } return this._comments[side].findIndex(function(item) { return item.__draftID === comment.__draftID; }); }, _handleMouseDown: function(e) { var el = Polymer.dom(e).rootTarget; var side; for (var node = el; node != null; node = node.parentNode) { if (!node.classList) { continue; } if (node.classList.contains(DiffSide.LEFT)) { side = DiffSide.LEFT; break; } else if (node.classList.contains(DiffSide.RIGHT)) { side = DiffSide.RIGHT; break; } } this._selectionSide = side; }, _selectionSideChanged: function(side) { if (side) { var oppositeSide = side === DiffSide.RIGHT ? DiffSide.LEFT : DiffSide.RIGHT; this.customStyle['--' + side + '-user-select'] = 'text'; this.customStyle['--' + oppositeSide + '-user-select'] = 'none'; } else { this.customStyle['--left-user-select'] = 'text'; this.customStyle['--right-user-select'] = 'text'; } this.updateStyles(); }, _handleCopy: function(e) { if (!e.target.classList.contains('content')) { return; } var text = this._getSelectedText(this._selectionSide); e.clipboardData.setData('Text', text); e.preventDefault(); }, _getSelectedText: function(opt_side) { var sel = window.getSelection(); var range = sel.getRangeAt(0); var doc = range.cloneContents(); var selector = '.content'; if (opt_side) { selector += '.' + opt_side; } var contentEls = Polymer.dom(doc).querySelectorAll(selector); if (contentEls.length === 0) { return doc.textContent; } var text = ''; for (var i = 0; i < contentEls.length; i++) { text += contentEls[i].textContent + '\n'; } return text; }, _showContext: function(group, sectionEl) { this._builder.emitGroup(group, sectionEl); sectionEl.parentNode.removeChild(sectionEl); this.async(function() { this.fire('render', null, {bubbles: false}); }.bind(this), 1); }, _prefsChanged: function(prefsChangeRecord) { var prefs = prefsChangeRecord.base; this.customStyle['--content-width'] = prefs.line_length + 'ch'; this.updateStyles(); if (this._diff && this._comments) { this._render(); } }, _render: function() { this._builder = this._getDiffBuilder(this._diff, this._comments, this.prefs); this._renderDiff(); }, _renderDiff: function() { this._clearDiffContent(); this._builder.emitDiff(); this.async(function() { this.fire('render', null, {bubbles: false}); }, 1); }, _clearDiffContent: function() { this.$.diffTable.innerHTML = null; }, _handleGetDiffError: function(response) { this.fire('page-error', {response: response}); }, _getDiff: function() { return this.$.restAPI.getDiff( this.changeNum, this.patchRange.basePatchNum, this.patchRange.patchNum, this.path, this._handleGetDiffError.bind(this)); }, _getDiffComments: function() { return this.$.restAPI.getDiffComments( this.changeNum, this.patchRange.basePatchNum, this.patchRange.patchNum, this.path); }, _getDiffDrafts: function() { return this._getLoggedIn().then(function(loggedIn) { if (!loggedIn) { return Promise.resolve({baseComments: [], comments: []}); } return this.$.restAPI.getDiffDrafts( this.changeNum, this.patchRange.basePatchNum, this.patchRange.patchNum, this.path); }.bind(this)); }, _getDiffCommentsAndDrafts: function() { var promises = []; promises.push(this._getDiffComments()); promises.push(this._getDiffDrafts()); return Promise.all(promises).then(function(results) { return Promise.resolve({ comments: results[0], drafts: results[1], }); }).then(this._normalizeDiffCommentsAndDrafts.bind(this)); }, _normalizeDiffCommentsAndDrafts: function(results) { function markAsDraft(d) { d.__draft = true; return d; } var baseDrafts = results.drafts.baseComments.map(markAsDraft); var drafts = results.drafts.comments.map(markAsDraft); return Promise.resolve({ meta: { path: this.path, changeNum: this.changeNum, patchRange: this.patchRange, projectConfig: this.projectConfig, }, left: results.comments.baseComments.concat(baseDrafts), right: results.comments.comments.concat(drafts), }); }, _getLoggedIn: function() { return this.$.restAPI.getLoggedIn(); }, _computeIsImageDiff: function() { if (!this._diff) { return false; } var isA = this._diff.meta_a && this._diff.meta_a.content_type.indexOf('image/') === 0; var isB = this._diff.meta_b && this._diff.meta_b.content_type.indexOf('image/') === 0; return this._diff.binary && (isA || isB); }, _loadDiffAssets: function() { if (this.isImageDiff) { return this._getImages().then(function(images) { this._baseImage = images.baseImage; this._revisionImage = images.revisionImage; }.bind(this)); } else { this._baseImage = null; this._revisionImage = null; return Promise.resolve(); } }, _getImages: function() { return this.$.restAPI.getImagesForDiff(this.project, this.commit, this.changeNum, this._diff, this.patchRange); }, _getDiffBuilder: function(diff, comments, prefs) { if (this.isImageDiff) { return new GrDiffBuilderImage(diff, comments, prefs, this.$.diffTable, this._baseImage, this._revisionImage); } else if (this.viewMode === DiffViewMode.SIDE_BY_SIDE) { return new GrDiffBuilderSideBySide(diff, comments, prefs, this.$.diffTable); } else if (this.viewMode === DiffViewMode.UNIFIED) { return new GrDiffBuilderUnified(diff, comments, prefs, this.$.diffTable); } throw Error('Unsupported diff view mode: ' + this.viewMode); }, _projectConfigChanged: function(projectConfig) { var threadEls = this._getCommentThreads(); for (var i = 0; i < threadEls.length; i++) { threadEls[i].projectConfig = projectConfig; } }, }); })();