- Moving show all bar to gr-editable-content - Adding Save, Cancel buttons to show all bar - textarea to block, so no space between textarea and show all bar - Set min-height for editor to 160px same as viewer The code will be cleaned up, when feature flag will be removed. Change-Id: I303e57672c88c29bce757365e2b48722458f7695
		
			
				
	
	
		
			261 lines
		
	
	
		
			7.5 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			261 lines
		
	
	
		
			7.5 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
/**
 | 
						|
 * @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';
 | 
						|
import {fireAlert, fireEvent} from '../../../utils/event-util';
 | 
						|
import {appContext} from '../../../services/app-context';
 | 
						|
import {KnownExperimentId} from '../../../services/flags/flags';
 | 
						|
 | 
						|
const RESTORED_MESSAGE = 'Content restored from a previous edit.';
 | 
						|
const STORAGE_DEBOUNCE_INTERVAL_MS = 400;
 | 
						|
 | 
						|
declare global {
 | 
						|
  interface HTMLElementTagNameMap {
 | 
						|
    'gr-editable-content': GrEditableContent;
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
@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', notify: true})
 | 
						|
  editing = false;
 | 
						|
 | 
						|
  @property({type: Boolean})
 | 
						|
  removeZeroWidthSpace?: boolean;
 | 
						|
 | 
						|
  // If no storage key is provided, content is not stored.
 | 
						|
  @property({type: String})
 | 
						|
  storageKey?: string;
 | 
						|
 | 
						|
  /** If false, then the "Show more" button was used to expand. */
 | 
						|
  @property({type: Boolean})
 | 
						|
  _commitCollapsed = true;
 | 
						|
 | 
						|
  @property({type: Boolean})
 | 
						|
  commitCollapsible = true;
 | 
						|
 | 
						|
  @property({
 | 
						|
    type: Boolean,
 | 
						|
    computed:
 | 
						|
      '_computeHideShowAllContainer(hideEditCommitMessage, _hideShowAllButton, editing)',
 | 
						|
  })
 | 
						|
  _hideShowAllContainer = false;
 | 
						|
 | 
						|
  @property({
 | 
						|
    type: Boolean,
 | 
						|
    computed: '_computeHideShowAllButton(commitCollapsible, editing)',
 | 
						|
  })
 | 
						|
  _hideShowAllButton = false;
 | 
						|
 | 
						|
  @property({type: Boolean})
 | 
						|
  hideEditCommitMessage?: boolean;
 | 
						|
 | 
						|
  @property({
 | 
						|
    type: Boolean,
 | 
						|
    computed: '_computeSaveDisabled(disabled, content, _newContent)',
 | 
						|
  })
 | 
						|
  _saveDisabled!: boolean;
 | 
						|
 | 
						|
  @property({type: String, observer: '_newContentChanged'})
 | 
						|
  _newContent?: string;
 | 
						|
 | 
						|
  @property({type: Boolean})
 | 
						|
  _isNewChangeSummaryUiEnabled = false;
 | 
						|
 | 
						|
  private readonly storage = new GrStorage();
 | 
						|
 | 
						|
  private readonly flagsService = appContext.flagsService;
 | 
						|
 | 
						|
  /** @override */
 | 
						|
  ready() {
 | 
						|
    super.ready();
 | 
						|
    this._isNewChangeSummaryUiEnabled = this.flagsService.isEnabled(
 | 
						|
      KnownExperimentId.NEW_CHANGE_SUMMARY_UI
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  _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 GrStorage 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;
 | 
						|
        fireAlert(this, RESTORED_MESSAGE);
 | 
						|
      }
 | 
						|
    }
 | 
						|
    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;
 | 
						|
    fireEvent(this, 'editable-content-cancel');
 | 
						|
  }
 | 
						|
 | 
						|
  _computeCollapseText(collapsed: boolean) {
 | 
						|
    return collapsed ? 'Show all' : 'Show less';
 | 
						|
  }
 | 
						|
 | 
						|
  _toggleCommitCollapsed() {
 | 
						|
    this._commitCollapsed = !this._commitCollapsed;
 | 
						|
    if (this._commitCollapsed) {
 | 
						|
      window.scrollTo(0, 0);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  _computeHideShowAllContainer(
 | 
						|
    hideEditCommitMessage?: boolean,
 | 
						|
    _hideShowAllButton?: boolean,
 | 
						|
    editing?: boolean
 | 
						|
  ) {
 | 
						|
    if (editing) return false;
 | 
						|
    return _hideShowAllButton && hideEditCommitMessage;
 | 
						|
  }
 | 
						|
 | 
						|
  _computeHideShowAllButton(commitCollapsible?: boolean, editing?: boolean) {
 | 
						|
    return !commitCollapsible || editing;
 | 
						|
  }
 | 
						|
 | 
						|
  _computeCommitMessageCollapsed(collapsed?: boolean, collapsible?: boolean) {
 | 
						|
    return collapsible && collapsed;
 | 
						|
  }
 | 
						|
 | 
						|
  _handleEditCommitMessage() {
 | 
						|
    this.editing = true;
 | 
						|
    this.focusTextarea();
 | 
						|
  }
 | 
						|
}
 |