| <!-- |
| 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="../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html"> |
| <link rel="import" href="gr-date-formatter.html"> |
| <link rel="import" href="gr-linked-text.html"> |
| <link rel="import" href="gr-request.html"> |
| |
| <dom-module id="gr-diff-comment"> |
| <template> |
| <style> |
| :host { |
| background-color: #ffd; |
| display: block; |
| --iron-autogrow-textarea: { |
| padding: 2px; |
| }; |
| } |
| :host([disabled]) { |
| pointer-events: none; |
| } |
| :host([disabled]) .container { |
| opacity: .5; |
| } |
| .header, |
| .message, |
| .actions { |
| padding: .5em .7em; |
| } |
| .header { |
| display: flex; |
| padding-bottom: 0; |
| font-family: 'Open Sans', sans-serif; |
| } |
| .headerLeft { |
| flex: 1; |
| } |
| .authorName, |
| .draftLabel { |
| font-weight: bold; |
| } |
| .draftLabel { |
| color: #999; |
| display: none; |
| } |
| .date { |
| justify-content: flex-end; |
| margin-left: 5px; |
| } |
| a.date:link, |
| a.date:visited { |
| color: #666; |
| } |
| .actions { |
| display: flex; |
| padding-top: 0; |
| } |
| .action { |
| cursor: pointer; |
| margin-right: .5em; |
| padding: .2em .6em .3em; |
| } |
| .danger { |
| display: flex; |
| flex: 1; |
| justify-content: flex-end; |
| } |
| .editMessage { |
| display: none; |
| margin: .5em .7em; |
| width: calc(100% - 1.4em - 2px); |
| } |
| .danger .action { |
| margin-right: 0; |
| } |
| .container:not(.draft) .actions :not(.reply):not(.quote):not(.done) { |
| display: none; |
| } |
| .draft .reply, |
| .draft .quote, |
| .draft .done { |
| display: none; |
| } |
| .draft .draftLabel { |
| display: inline; |
| } |
| .draft:not(.editing) .save, |
| .draft:not(.editing) .cancel { |
| display: none; |
| } |
| .editing .message, |
| .editing .reply, |
| .editing .quote, |
| .editing .done, |
| .editing .edit { |
| display: none; |
| } |
| .editing .editMessage { |
| background-color: #fff; |
| display: block; |
| } |
| </style> |
| <div class="container" id="container"> |
| <div class="header" id="header"> |
| <div class="headerLeft"> |
| <span class="authorName">[[comment.author.name]]</span> |
| <span class="draftLabel">DRAFT</span> |
| </div> |
| <a class="date" href$="[[_computeLinkToComment(comment)]]" on-tap="_handleLinkTap"> |
| <gr-date-formatter date-str="[[comment.updated]]"></gr-date-formatter> |
| </a> |
| </div> |
| <iron-autogrow-textarea |
| id="editTextarea" |
| class="editMessage" |
| disabled="{{disabled}}" |
| rows="4" |
| bind-value="{{_editDraft}}" |
| on-keyup="_handleTextareaKeyup" |
| on-keydown="_handleTextareaKeydown"></iron-autogrow-textarea> |
| <gr-linked-text class="message" |
| pre |
| content="[[comment.message]]" |
| config="[[projectConfig.commentlinks]]"></gr-linked-text> |
| <div class="actions" hidden$="[[!showActions]]"> |
| <button class="action reply" on-tap="_handleReply">Reply</button> |
| <button class="action quote" on-tap="_handleQuote">Quote</button> |
| <button class="action done" on-tap="_handleDone">Done</button> |
| <button class="action edit" on-tap="_handleEdit">Edit</button> |
| <button class="action save" on-tap="_handleSave" |
| disabled$="[[_computeSaveDisabled(_editDraft)]]">Save</button> |
| <button class="action cancel" on-tap="_handleCancel" hidden>Cancel</button> |
| <div class="danger"> |
| <button class="action discard" on-tap="_handleDiscard">Discard</button> |
| </div> |
| </div> |
| </div> |
| </template> |
| <script> |
| (function() { |
| 'use strict'; |
| |
| Polymer({ |
| is: 'gr-diff-comment', |
| |
| /** |
| * Fired when the height of the comment changes. |
| * |
| * @event height-change |
| */ |
| |
| /** |
| * Fired when the Reply action is triggered. |
| * |
| * @event reply |
| */ |
| |
| /** |
| * Fired when the Done action is triggered. |
| * |
| * @event done |
| */ |
| |
| /** |
| * Fired when this comment is discarded. |
| * |
| * @event discard |
| */ |
| |
| properties: { |
| changeNum: String, |
| comment: { |
| type: Object, |
| notify: true, |
| }, |
| disabled: { |
| type: Boolean, |
| value: false, |
| reflectToAttribute: true, |
| }, |
| draft: { |
| type: Boolean, |
| value: false, |
| observer: '_draftChanged', |
| }, |
| editing: { |
| type: Boolean, |
| value: false, |
| observer: '_editingChanged', |
| }, |
| patchNum: String, |
| showActions: Boolean, |
| projectConfig: Object, |
| |
| _xhrPromise: Object, // Used for testing. |
| _editDraft: String, |
| }, |
| |
| ready: function() { |
| this._editDraft = (this.comment && this.comment.message) || ''; |
| this.editing = this._editDraft.length == 0; |
| }, |
| |
| attached: function() { |
| this._heightChanged(); |
| }, |
| |
| save: function() { |
| this.comment.message = this._editDraft; |
| this.disabled = true; |
| var endpoint = this._restEndpoint(this.comment.id); |
| this._send('PUT', endpoint).then(function(req) { |
| this.disabled = false; |
| var comment = req.response; |
| comment.__draft = true; |
| // Maintain the ephemeral draft ID for identification by other |
| // elements. |
| if (this.comment.__draftID) { |
| comment.__draftID = this.comment.__draftID; |
| } |
| this.comment = comment; |
| this.editing = false; |
| }.bind(this)).catch(function(err) { |
| alert('Your draft couldn’t be saved. Check the console and contact ' + |
| 'the PolyGerrit team for assistance.'); |
| this.disabled = false; |
| }.bind(this)); |
| }, |
| |
| _heightChanged: function() { |
| this.async(function() { |
| this.fire('height-change', {height: this.offsetHeight}, |
| {bubbles: false}); |
| }.bind(this)); |
| }, |
| |
| _draftChanged: function(draft) { |
| this.$.container.classList.toggle('draft', draft); |
| }, |
| |
| _editingChanged: function(editing) { |
| this.$.container.classList.toggle('editing', editing); |
| if (editing) { |
| var textarea = this.$.editTextarea.textarea; |
| // Put the cursor at the end always. |
| textarea.selectionStart = textarea.value.length; |
| textarea.selectionEnd = textarea.selectionStart; |
| this.async(function() { |
| textarea.focus(); |
| }.bind(this)); |
| } |
| if (this.comment && this.comment.id) { |
| this.$$('.cancel').hidden = !editing; |
| } |
| this._heightChanged(); |
| }, |
| |
| _computeLinkToComment: function(comment) { |
| return '#' + comment.line; |
| }, |
| |
| _computeSaveDisabled: function(draft) { |
| return draft == null || draft.trim() == ''; |
| }, |
| |
| _handleTextareaKeyup: function(e) { |
| // TODO(andybons): This isn't always true, but I can't currently think |
| // of a better metric. |
| this._heightChanged(); |
| }, |
| |
| _handleTextareaKeydown: function(e) { |
| if (e.keyCode == 27) { // 'esc' |
| this._handleCancel(e); |
| } |
| }, |
| |
| _handleLinkTap: function(e) { |
| e.preventDefault(); |
| var hash = this._computeLinkToComment(this.comment); |
| // Don't add the hash to the window history if it's already there. |
| // Otherwise you mess up expected back button behavior. |
| if (window.location.hash == hash) { return; } |
| // Change the URL but don’t trigger a nav event. Otherwise it will |
| // reload the page. |
| page.show(window.location.pathname + hash, null, false); |
| }, |
| |
| _handleReply: function(e) { |
| this._preventDefaultAndBlur(e); |
| this.fire('reply', {comment: this.comment}, {bubbles: false}); |
| }, |
| |
| _handleQuote: function(e) { |
| this._preventDefaultAndBlur(e); |
| this.fire('reply', {comment: this.comment, quote: true}, |
| {bubbles: false}); |
| }, |
| |
| _handleDone: function(e) { |
| this._preventDefaultAndBlur(e); |
| this.fire('done', {comment: this.comment}, {bubbles: false}); |
| }, |
| |
| _handleEdit: function(e) { |
| this._preventDefaultAndBlur(e); |
| this._editDraft = this.comment.message; |
| this.editing = true; |
| }, |
| |
| _handleSave: function(e) { |
| this._preventDefaultAndBlur(e); |
| this.save(); |
| }, |
| |
| _handleCancel: function(e) { |
| this._preventDefaultAndBlur(e); |
| if (this.comment.message == null || this.comment.message.length == 0) { |
| this.fire('discard', null, {bubbles: false}); |
| return; |
| } |
| this._editDraft = this.comment.message; |
| this.editing = false; |
| }, |
| |
| _handleDiscard: function(e) { |
| this._preventDefaultAndBlur(e); |
| if (!this.comment.__draft) { |
| throw Error('Cannot discard a non-draft comment.'); |
| } |
| this.disabled = true; |
| var commentID = this.comment.id; |
| if (!commentID) { |
| this.fire('discard', null, {bubbles: false}); |
| return; |
| } |
| this._send('DELETE', this._restEndpoint(commentID)).then(function(req) { |
| this.fire('discard', null, {bubbles: false}); |
| }.bind(this)).catch(function(err) { |
| alert('Your draft couldn’t be deleted. Check the console and ' + |
| 'contact the PolyGerrit team for assistance.'); |
| this.disabled = false; |
| }.bind(this)); |
| }, |
| |
| _preventDefaultAndBlur: function(e) { |
| e.preventDefault(); |
| Polymer.dom(e).rootTarget.blur(); |
| }, |
| |
| _send: function(method, url) { |
| var xhr = document.createElement('gr-request'); |
| var opts = { |
| method: method, |
| url: url, |
| }; |
| if (method == 'PUT' || method == 'POST') { |
| opts.body = this.comment; |
| } |
| this._xhrPromise = xhr.send(opts); |
| return this._xhrPromise; |
| }, |
| |
| _restEndpoint: function(id) { |
| var path = '/changes/' + this.changeNum + '/revisions/' + |
| this.patchNum + '/drafts'; |
| if (id) { |
| path += '/' + id; |
| } |
| return path; |
| }, |
| |
| }); |
| })(); |
| </script> |
| </dom-module> |