|  | /** | 
|  | * @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 '../../../scripts/bundled-polymer.js'; | 
|  | 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 Polymer.Element | 
|  | */ | 
|  | 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(); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Hanlde 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})); | 
|  | } | 
|  | 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); |