| /** |
| * @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 '@polymer/iron-autogrow-textarea/iron-autogrow-textarea'; |
| import '../../../styles/shared-styles'; |
| import '../gr-storage/gr-storage'; |
| import '../gr-button/gr-button'; |
| import {GrStorage} from '../gr-storage/gr-storage'; |
| import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners'; |
| import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin'; |
| import {PolymerElement} from '@polymer/polymer/polymer-element'; |
| import {customElement, property} from '@polymer/decorators'; |
| import {htmlTemplate} from './gr-editable-content_html'; |
| |
| const RESTORED_MESSAGE = 'Content restored from a previous edit.'; |
| const STORAGE_DEBOUNCE_INTERVAL_MS = 400; |
| |
| declare global { |
| interface HTMLElementTagNameMap { |
| 'gr-editable-content': GrEditableContent; |
| } |
| } |
| |
| export interface GrEditableContent { |
| $: { |
| storage: GrStorage; |
| }; |
| } |
| |
| @customElement('gr-editable-content') |
| export class GrEditableContent extends GestureEventListeners( |
| LegacyElementMixin(PolymerElement) |
| ) { |
| static get template() { |
| return htmlTemplate; |
| } |
| |
| /** |
| * Fired when the save button is pressed. |
| * |
| * @event editable-content-save |
| */ |
| |
| /** |
| * Fired when the cancel button is pressed. |
| * |
| * @event editable-content-cancel |
| */ |
| |
| /** |
| * Fired when content is restored from storage. |
| * |
| * @event show-alert |
| */ |
| |
| @property({type: String, notify: true, observer: '_contentChanged'}) |
| content?: string; |
| |
| @property({type: Boolean, reflectToAttribute: true}) |
| disabled = false; |
| |
| @property({type: Boolean, observer: '_editingChanged'}) |
| editing = false; |
| |
| @property({type: Boolean}) |
| removeZeroWidthSpace?: boolean; |
| |
| // If no storage key is provided, content is not stored. |
| @property({type: String}) |
| storageKey?: string; |
| |
| @property({ |
| type: Boolean, |
| computed: '_computeSaveDisabled(disabled, content, _newContent)', |
| }) |
| _saveDisabled!: boolean; |
| |
| @property({type: String, observer: '_newContentChanged'}) |
| _newContent?: string; |
| |
| _contentChanged() { |
| /* A changed content means that either a different change has been loaded |
| * or new content was saved. Either way, let's reset the component. |
| */ |
| this.editing = false; |
| this._newContent = ''; |
| } |
| |
| focusTextarea() { |
| this.shadowRoot!.querySelector('iron-autogrow-textarea')!.textarea.focus(); |
| } |
| |
| _newContentChanged(newContent: string) { |
| if (!this.storageKey) return; |
| const storageKey = this.storageKey; |
| |
| this.debounce( |
| 'store', |
| () => { |
| if (newContent.length) { |
| this.$.storage.setEditableContentItem(storageKey, newContent); |
| } else { |
| // This does not really happen, because we don't clear newContent |
| // after saving (see below). So this only occurs when the user clears |
| // all the content in the editable textarea. But <gr-storage> cleans |
| // up itself after one day, so we are not so concerned about leaving |
| // some garbage behind. |
| this.$.storage.eraseEditableContentItem(storageKey); |
| } |
| }, |
| STORAGE_DEBOUNCE_INTERVAL_MS |
| ); |
| } |
| |
| _editingChanged(editing: boolean) { |
| // This method is for initializing _newContent when you start editing. |
| // Restoring content from local storage is not perfect and has |
| // some issues: |
| // |
| // 1. When you start editing in multiple tabs, then we are vulnerable to |
| // race conditions between the tabs. |
| // 2. The stored content is keyed by revision, so when you upload a new |
| // patchset and click "reload" and then click "cancel" on the content- |
| // editable, then you won't be able to recover the content anymore. |
| // |
| // Because of these issues we believe that it is better to only recover |
| // content from local storage when you enter editing mode for the first |
| // time. Otherwise it is better to just keep the last editing state from |
| // the same session. |
| if (!editing || this._newContent) return; |
| |
| let content; |
| if (this.storageKey) { |
| const storedContent = this.$.storage.getEditableContentItem( |
| this.storageKey |
| ); |
| if (storedContent?.message) { |
| content = storedContent.message; |
| this.dispatchEvent( |
| new CustomEvent('show-alert', { |
| detail: {message: RESTORED_MESSAGE}, |
| bubbles: true, |
| composed: true, |
| }) |
| ); |
| } |
| } |
| if (!content) { |
| content = this.content || ''; |
| } |
| |
| // TODO(wyatta) switch linkify sequence, see issue 5526. |
| this._newContent = this.removeZeroWidthSpace |
| ? content.replace(/^R=\u200B/gm, 'R=') |
| : content; |
| } |
| |
| _computeSaveDisabled( |
| disabled?: boolean, |
| content?: string, |
| newContent?: string |
| ): boolean { |
| return disabled || !newContent || content === newContent; |
| } |
| |
| _handleSave(e: Event) { |
| e.preventDefault(); |
| this.dispatchEvent( |
| new CustomEvent('editable-content-save', { |
| detail: {content: this._newContent}, |
| composed: true, |
| bubbles: true, |
| }) |
| ); |
| // It would be nice, if we would set this._newContent = undefined here, |
| // but we can only do that when we are sure that the save operation has |
| // succeeded. |
| } |
| |
| _handleCancel(e: Event) { |
| e.preventDefault(); |
| this.editing = false; |
| this.dispatchEvent( |
| new CustomEvent('editable-content-cancel', { |
| composed: true, |
| bubbles: true, |
| }) |
| ); |
| } |
| } |