335 lines
		
	
	
		
			8.4 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			335 lines
		
	
	
		
			8.4 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/**
 | 
						|
 * @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-dropdown/iron-dropdown.js';
 | 
						|
import '../gr-button/gr-button.js';
 | 
						|
import '../gr-cursor-manager/gr-cursor-manager.js';
 | 
						|
import '../gr-rest-api-interface/gr-rest-api-interface.js';
 | 
						|
import '../gr-tooltip-content/gr-tooltip-content.js';
 | 
						|
import '../../../styles/shared-styles.js';
 | 
						|
import {flush, dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 | 
						|
import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
 | 
						|
import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 | 
						|
import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 | 
						|
import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 | 
						|
import {htmlTemplate} from './gr-dropdown_html.js';
 | 
						|
import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
 | 
						|
import {KeyboardShortcutBehavior} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
 | 
						|
 | 
						|
const REL_NOOPENER = 'noopener';
 | 
						|
const REL_EXTERNAL = 'external';
 | 
						|
 | 
						|
/**
 | 
						|
 * @extends PolymerElement
 | 
						|
 */
 | 
						|
class GrDropdown extends mixinBehaviors( [
 | 
						|
  BaseUrlBehavior,
 | 
						|
  KeyboardShortcutBehavior,
 | 
						|
], GestureEventListeners(
 | 
						|
    LegacyElementMixin(
 | 
						|
        PolymerElement))) {
 | 
						|
  static get template() { return htmlTemplate; }
 | 
						|
 | 
						|
  static get is() { return 'gr-dropdown'; }
 | 
						|
  /**
 | 
						|
   * Fired when a non-link dropdown item with the given ID is tapped.
 | 
						|
   *
 | 
						|
   * @event tap-item-<id>
 | 
						|
   */
 | 
						|
 | 
						|
  /**
 | 
						|
   * Fired when a non-link dropdown item is tapped.
 | 
						|
   *
 | 
						|
   * @event tap-item
 | 
						|
   */
 | 
						|
 | 
						|
  static get properties() {
 | 
						|
    return {
 | 
						|
      items: {
 | 
						|
        type: Array,
 | 
						|
        observer: '_resetCursorStops',
 | 
						|
      },
 | 
						|
      downArrow: Boolean,
 | 
						|
      topContent: Object,
 | 
						|
      horizontalAlign: {
 | 
						|
        type: String,
 | 
						|
        value: 'left',
 | 
						|
      },
 | 
						|
 | 
						|
      /**
 | 
						|
       * Style the dropdown trigger as a link (rather than a button).
 | 
						|
       */
 | 
						|
      link: {
 | 
						|
        type: Boolean,
 | 
						|
        value: false,
 | 
						|
      },
 | 
						|
 | 
						|
      verticalOffset: {
 | 
						|
        type: Number,
 | 
						|
        value: 40,
 | 
						|
      },
 | 
						|
 | 
						|
      /**
 | 
						|
       * List the IDs of dropdown buttons to be disabled. (Note this only
 | 
						|
       * diisables bittons and not link entries.)
 | 
						|
       */
 | 
						|
      disabledIds: {
 | 
						|
        type: Array,
 | 
						|
        value() { return []; },
 | 
						|
      },
 | 
						|
 | 
						|
      /**
 | 
						|
       * The elements of the list.
 | 
						|
       */
 | 
						|
      _listElements: {
 | 
						|
        type: Array,
 | 
						|
        value() { return []; },
 | 
						|
      },
 | 
						|
    };
 | 
						|
  }
 | 
						|
 | 
						|
  get keyBindings() {
 | 
						|
    return {
 | 
						|
      'down': '_handleDown',
 | 
						|
      'enter space': '_handleEnter',
 | 
						|
      'tab': '_handleTab',
 | 
						|
      'up': '_handleUp',
 | 
						|
    };
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Handle the up key.
 | 
						|
   *
 | 
						|
   * @param {!Event} e
 | 
						|
   */
 | 
						|
  _handleUp(e) {
 | 
						|
    if (this.$.dropdown.opened) {
 | 
						|
      e.preventDefault();
 | 
						|
      e.stopPropagation();
 | 
						|
      this.$.cursor.previous();
 | 
						|
    } else {
 | 
						|
      this._open();
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Handle the down key.
 | 
						|
   *
 | 
						|
   * @param {!Event} e
 | 
						|
   */
 | 
						|
  _handleDown(e) {
 | 
						|
    if (this.$.dropdown.opened) {
 | 
						|
      e.preventDefault();
 | 
						|
      e.stopPropagation();
 | 
						|
      this.$.cursor.next();
 | 
						|
    } else {
 | 
						|
      this._open();
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Handle the tab key.
 | 
						|
   *
 | 
						|
   * @param {!Event} e
 | 
						|
   */
 | 
						|
  _handleTab(e) {
 | 
						|
    if (this.$.dropdown.opened) {
 | 
						|
      // Tab in a native select is a no-op. Emulate this.
 | 
						|
      e.preventDefault();
 | 
						|
      e.stopPropagation();
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Handle the enter key.
 | 
						|
   *
 | 
						|
   * @param {!Event} e
 | 
						|
   */
 | 
						|
  _handleEnter(e) {
 | 
						|
    e.preventDefault();
 | 
						|
    e.stopPropagation();
 | 
						|
    if (this.$.dropdown.opened) {
 | 
						|
      // TODO(milutin): This solution is not particularly robust in general.
 | 
						|
      // Since gr-tooltip-content click on shadow dom is not propagated down,
 | 
						|
      // we have to target `a` inside it.
 | 
						|
      const el = this.$.cursor.target.querySelector(':not([hidden]) a');
 | 
						|
      if (el) { el.click(); }
 | 
						|
    } else {
 | 
						|
      this._open();
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Handle a click on the iron-dropdown element.
 | 
						|
   *
 | 
						|
   * @param {!Event} e
 | 
						|
   */
 | 
						|
  _handleDropdownClick(e) {
 | 
						|
    this._close();
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Handle a click on the button to open the dropdown.
 | 
						|
   *
 | 
						|
   * @param {!Event} e
 | 
						|
   */
 | 
						|
  _dropdownTriggerTapHandler(e) {
 | 
						|
    e.preventDefault();
 | 
						|
    e.stopPropagation();
 | 
						|
    if (this.$.dropdown.opened) {
 | 
						|
      this._close();
 | 
						|
    } else {
 | 
						|
      this._open();
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Open the dropdown and initialize the cursor.
 | 
						|
   */
 | 
						|
  _open() {
 | 
						|
    this.$.dropdown.open();
 | 
						|
    this._resetCursorStops();
 | 
						|
    this.$.cursor.setCursorAtIndex(0);
 | 
						|
    this.$.cursor.target.focus();
 | 
						|
  }
 | 
						|
 | 
						|
  _close() {
 | 
						|
    // async is needed so that that the click event is fired before the
 | 
						|
    // dropdown closes (This was a bug for touch devices).
 | 
						|
    this.async(() => {
 | 
						|
      this.$.dropdown.close();
 | 
						|
    }, 1);
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Get the class for a top-content item based on the given boolean.
 | 
						|
   *
 | 
						|
   * @param {boolean} bold Whether the item is bold.
 | 
						|
   * @return {string} The class for the top-content item.
 | 
						|
   */
 | 
						|
  _getClassIfBold(bold) {
 | 
						|
    return bold ? 'bold-text' : '';
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Build a URL for the given host and path. The base URL will be only added,
 | 
						|
   * if it is not already included in the path.
 | 
						|
   *
 | 
						|
   * @param {!string} host
 | 
						|
   * @param {!string} path
 | 
						|
   * @return {!string} The scheme-relative URL.
 | 
						|
   */
 | 
						|
  _computeURLHelper(host, path) {
 | 
						|
    const base = path.startsWith(this.getBaseUrl()) ?
 | 
						|
      '' : this.getBaseUrl();
 | 
						|
    return '//' + host + base + path;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Build a scheme-relative URL for the current host. Will include the base
 | 
						|
   * URL if one is present. Note: the URL will be scheme-relative but absolute
 | 
						|
   * with regard to the host.
 | 
						|
   *
 | 
						|
   * @param {!string} path The path for the URL.
 | 
						|
   * @return {!string} The scheme-relative URL.
 | 
						|
   */
 | 
						|
  _computeRelativeURL(path) {
 | 
						|
    const host = window.location.host;
 | 
						|
    return this._computeURLHelper(host, path);
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Compute the URL for a link object.
 | 
						|
   *
 | 
						|
   * @param {!Object} link The object describing the link.
 | 
						|
   * @return {!string} The URL.
 | 
						|
   */
 | 
						|
  _computeLinkURL(link) {
 | 
						|
    if (typeof link.url === 'undefined') {
 | 
						|
      return '';
 | 
						|
    }
 | 
						|
    if (link.target || !link.url.startsWith('/')) {
 | 
						|
      return link.url;
 | 
						|
    }
 | 
						|
    return this._computeRelativeURL(link.url);
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Compute the value for the rel attribute of an anchor for the given link
 | 
						|
   * object. If the link has a target value, then the rel must be "noopener"
 | 
						|
   * for security reasons.
 | 
						|
   *
 | 
						|
   * @param {!Object} link The object describing the link.
 | 
						|
   * @return {?string} The rel value for the link.
 | 
						|
   */
 | 
						|
  _computeLinkRel(link) {
 | 
						|
    // Note: noopener takes precedence over external.
 | 
						|
    if (link.target) { return REL_NOOPENER; }
 | 
						|
    if (link.external) { return REL_EXTERNAL; }
 | 
						|
    return null;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Handle a click on an item of the dropdown.
 | 
						|
   *
 | 
						|
   * @param {!Event} e
 | 
						|
   */
 | 
						|
  _handleItemTap(e) {
 | 
						|
    const id = e.target.getAttribute('data-id');
 | 
						|
    const item = this.items.find(item => item.id === id);
 | 
						|
    if (id && !this.disabledIds.includes(id)) {
 | 
						|
      if (item) {
 | 
						|
        this.dispatchEvent(new CustomEvent('tap-item',
 | 
						|
            {detail: item, bubbles: true, composed: true}));
 | 
						|
      }
 | 
						|
      this.dispatchEvent(new CustomEvent('tap-item-' + id));
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * If a dropdown item is shown as a button, get the class for the button.
 | 
						|
   *
 | 
						|
   * @param {string} id
 | 
						|
   * @param {!Object} disabledIdsRecord The change record for the disabled IDs
 | 
						|
   *     list.
 | 
						|
   * @return {!string} The class for the item button.
 | 
						|
   */
 | 
						|
  _computeDisabledClass(id, disabledIdsRecord) {
 | 
						|
    return disabledIdsRecord.base.includes(id) ? 'disabled' : '';
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Recompute the stops for the dropdown item cursor.
 | 
						|
   */
 | 
						|
  _resetCursorStops() {
 | 
						|
    if (this.items && this.items.length > 0 && this.$.dropdown.opened) {
 | 
						|
      flush();
 | 
						|
      this._listElements = Array.from(
 | 
						|
          dom(this.root).querySelectorAll('li'));
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  _computeHasTooltip(tooltip) {
 | 
						|
    return !!tooltip;
 | 
						|
  }
 | 
						|
 | 
						|
  _computeIsDownload(link) {
 | 
						|
    return !!link.download;
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
customElements.define(GrDropdown.is, GrDropdown);
 |