UNPKG

spincycle

Version:

A reactive message router and object manager that lets clients subscribe to object property changes on the server

598 lines (518 loc) 19.4 kB
<!-- @license Copyright (c) 2016 The Polymer Project Authors. All rights reserved. This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as part of the polymer project is also subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt --> <link rel="import" href="../polymer/polymer.html"> <link rel="import" href="../iron-a11y-keys-behavior/iron-a11y-keys-behavior.html"> <link rel="import" href="../iron-behaviors/iron-button-state.html"> <link rel="import" href="../iron-behaviors/iron-control-state.html"> <link rel="import" href="../iron-form-element-behavior/iron-form-element-behavior.html"> <link rel="import" href="../iron-icon/iron-icon.html"> <link rel="import" href="../iron-validatable-behavior/iron-validatable-behavior.html"> <link rel="import" href="../paper-behaviors/paper-ripple-behavior.html"> <link rel="import" href="../paper-menu-button/paper-menu-button.html"> <link rel="import" href="../paper-styles/default-theme.html"> <link rel="import" href="paper-dropdown-menu-icons.html"> <link rel="import" href="paper-dropdown-menu-shared-styles.html"> <!-- Material design: [Dropdown menus](https://www.google.com/design/spec/components/buttons.html#buttons-dropdown-buttons) This is a faster, lighter version of `paper-dropdown-menu`, that does not use a `<paper-input>` internally. Use this element if you're concerned about the performance of this element, i.e., if you plan on using many dropdowns on the same page. Note that this element has a slightly different styling API than `paper-dropdown-menu`. `paper-dropdown-menu-light` is similar to a native browser select element. `paper-dropdown-menu-light` works with selectable content. The currently selected item is displayed in the control. If no item is selected, the `label` is displayed instead. Example: <paper-dropdown-menu-light label="Your favourite pastry"> <paper-listbox class="dropdown-content"> <paper-item>Croissant</paper-item> <paper-item>Donut</paper-item> <paper-item>Financier</paper-item> <paper-item>Madeleine</paper-item> </paper-listbox> </paper-dropdown-menu-light> This example renders a dropdown menu with 4 options. The child element with the class `dropdown-content` is used as the dropdown menu. This can be a [`paper-listbox`](paper-listbox), or any other or element that acts like an [`iron-selector`](iron-selector). Specifically, the menu child must fire an [`iron-select`](iron-selector#event-iron-select) event when one of its children is selected, and an [`iron-deselect`](iron-selector#event-iron-deselect) event when a child is deselected. The selected or deselected item must be passed as the event's `detail.item` property. Applications can listen for the `iron-select` and `iron-deselect` events to react when options are selected and deselected. ### Styling The following custom properties and mixins are also available for styling: Custom property | Description | Default ----------------|-------------|---------- `--paper-dropdown-menu` | A mixin that is applied to the element host | `{}` `--paper-dropdown-menu-disabled` | A mixin that is applied to the element host when disabled | `{}` `--paper-dropdown-menu-ripple` | A mixin that is applied to the internal ripple | `{}` `--paper-dropdown-menu-button` | A mixin that is applied to the internal menu button | `{}` `--paper-dropdown-menu-icon` | A mixin that is applied to the internal icon | `{}` `--paper-dropdown-menu-disabled-opacity` | The opacity of the dropdown when disabled | `0.33` `--paper-dropdown-menu-color` | The color of the input/label/underline when the dropdown is unfocused | `--primary-text-color` `--paper-dropdown-menu-focus-color` | The color of the label/underline when the dropdown is focused | `--primary-color` `--paper-dropdown-error-color` | The color of the label/underline when the dropdown is invalid | `--error-color` `--paper-dropdown-menu-label` | Mixin applied to the label | `{}` `--paper-dropdown-menu-input` | Mixin appled to the input | `{}` Note that in this element, the underline is just the bottom border of the "input". To style it: <style is=custom-style> paper-dropdown-menu-light.custom { --paper-dropdown-menu-input: { border-bottom: 2px dashed lavender; }; </style> @group Paper Elements @element paper-dropdown-menu-light @hero hero.svg @demo demo/index.html --> <dom-module id="paper-dropdown-menu-light"> <template> <style include="paper-dropdown-menu-shared-styles"> :host(:focus) { outline: none; } :host { width: 200px; /* Default size of an <input> */ } /** * All of these styles below are for styling the fake-input display */ .dropdown-trigger { box-sizing: border-box; position: relative; width: 100%; padding: 16px 0 8px 0; } :host([disabled]) .dropdown-trigger { pointer-events: none; opacity: var(--paper-dropdown-menu-disabled-opacity, 0.33); } :host([no-label-float]) .dropdown-trigger { padding-top: 8px; /* If there's no label, we need less space up top. */ } #input { @apply(--paper-font-subhead); @apply(--paper-font-common-nowrap); line-height: 1.5; border-bottom: 1px solid var(--paper-dropdown-menu-color, --secondary-text-color); color: var(--paper-dropdown-menu-color, --primary-text-color); width: 100%; box-sizing: border-box; padding: 12px 20px 0 0; /* Right padding so that text doesn't overlap the icon */ outline: none; @apply(--paper-dropdown-menu-input); } :host-context([dir="rtl"]) #input { padding-right: 0px; padding-left: 20px; } :host([disabled]) #input { border-bottom: 1px dashed var(--paper-dropdown-menu-color, --secondary-text-color); } :host([invalid]) #input { border-bottom: 2px solid var(--paper-dropdown-error-color, --error-color); } :host([no-label-float]) #input { padding-top: 0; /* If there's no label, we need less space up top. */ } label { @apply(--paper-font-subhead); @apply(--paper-font-common-nowrap); display: block; position: absolute; bottom: 0; left: 0; right: 0; /** * The container has a 16px top padding, and there's 12px of padding * between the input and the label (from the input's padding-top) */ top: 28px; box-sizing: border-box; width: 100%; padding-right: 20px; /* Right padding so that text doesn't overlap the icon */ text-align: left; transition-duration: .2s; transition-timing-function: cubic-bezier(.4,0,.2,1); color: var(--paper-dropdown-menu-color, --secondary-text-color); @apply(--paper-dropdown-menu-label); } :host-context([dir="rtl"]) label { padding-right: 0px; padding-left: 20px; } :host([no-label-float]) label { top: 8px; /* Since the label doesn't need to float, remove the animation duration which slows down visibility changes (i.e. when a selection is made) */ transition-duration: 0s; } label.label-is-floating { font-size: 12px; top: 8px; } label.label-is-hidden { visibility: hidden; } :host([focused]) label.label-is-floating { color: var(--paper-dropdown-menu-focus-color, --primary-color); } :host([invalid]) label.label-is-floating { color: var(--paper-dropdown-error-color, --error-color); } /** * Sets up the focused underline. It's initially hidden, and becomes * visible when it's focused. */ label:after { background-color: var(--paper-dropdown-menu-focus-color, --primary-color); bottom: 7px; /* The container has an 8px bottom padding */ content: ''; height: 2px; left: 45%; position: absolute; transition-duration: .2s; transition-timing-function: cubic-bezier(.4,0,.2,1); visibility: hidden; width: 8px; z-index: 10; } :host([invalid]) label:after { background-color: var(--paper-dropdown-error-color, --error-color); } :host([no-label-float]) label:after { bottom: 7px; /* The container has a 8px bottom padding */ } :host([focused]:not([disabled])) label:after { left: 0; visibility: visible; width: 100%; } iron-icon { position: absolute; right: 0px; bottom: 8px; /* The container has an 8px bottom padding */ @apply(--paper-font-subhead); color: var(--disabled-text-color); @apply(--paper-dropdown-menu-icon); } :host-context([dir="rtl"]) iron-icon { left: 0; right: auto; } :host([no-label-float]) iron-icon { margin-top: 0px; } .error { display: inline-block; visibility: hidden; color: var(--paper-dropdown-error-color, --error-color); @apply(--paper-font-caption); position: absolute; left:0; right:0; bottom: -12px; } :host([invalid]) .error { visibility: visible; } </style> <!-- this div fulfills an a11y requirement for combobox, do not remove --> <span role="button"></span> <paper-menu-button id="menuButton" vertical-align="[[verticalAlign]]" horizontal-align="[[horizontalAlign]]" vertical-offset="[[_computeMenuVerticalOffset(noLabelFloat)]]" disabled="[[disabled]]" no-animations="[[noAnimations]]" on-iron-select="_onIronSelect" on-iron-deselect="_onIronDeselect" opened="{{opened}}" close-on-activate allow-outside-scroll="[[allowOutsideScroll]]"> <div class="dropdown-trigger"> <label class$="[[_computeLabelClass(noLabelFloat,alwaysFloatLabel,hasContent)]]"> [[label]] </label> <div id="input" tabindex="-1">&nbsp;</div> <iron-icon icon="paper-dropdown-menu:arrow-drop-down"></iron-icon> <span class="error">[[errorMessage]]</span> </div> <content id="content" select=".dropdown-content"></content> </paper-menu-button> </template> <script> (function() { 'use strict'; Polymer({ is: 'paper-dropdown-menu-light', behaviors: [ Polymer.IronButtonState, Polymer.IronControlState, Polymer.PaperRippleBehavior, Polymer.IronFormElementBehavior, Polymer.IronValidatableBehavior ], properties: { /** * The derived "label" of the currently selected item. This value * is the `label` property on the selected item if set, or else the * trimmed text content of the selected item. */ selectedItemLabel: { type: String, notify: true, readOnly: true }, /** * The last selected item. An item is selected if the dropdown menu has * a child with class `dropdown-content`, and that child triggers an * `iron-select` event with the selected `item` in the `detail`. * * @type {?Object} */ selectedItem: { type: Object, notify: true, readOnly: true }, /** * The value for this element that will be used when submitting in * a form. It is read only, and will always have the same value * as `selectedItemLabel`. */ value: { type: String, notify: true, readOnly: true, observer: '_valueChanged', }, /** * The label for the dropdown. */ label: { type: String }, /** * The placeholder for the dropdown. */ placeholder: { type: String }, /** * True if the dropdown is open. Otherwise, false. */ opened: { type: Boolean, notify: true, value: false, observer: '_openedChanged' }, /** * By default, the dropdown will constrain scrolling on the page * to itself when opened. * Set to true in order to prevent scroll from being constrained * to the dropdown when it opens. */ allowOutsideScroll: { type: Boolean, value: false }, /** * Set to true to disable the floating label. Bind this to the * `<paper-input-container>`'s `noLabelFloat` property. */ noLabelFloat: { type: Boolean, value: false, reflectToAttribute: true }, /** * Set to true to always float the label. Bind this to the * `<paper-input-container>`'s `alwaysFloatLabel` property. */ alwaysFloatLabel: { type: Boolean, value: false }, /** * Set to true to disable animations when opening and closing the * dropdown. */ noAnimations: { type: Boolean, value: false }, /** * The orientation against which to align the menu dropdown * horizontally relative to the dropdown trigger. */ horizontalAlign: { type: String, value: 'right' }, /** * The orientation against which to align the menu dropdown * vertically relative to the dropdown trigger. */ verticalAlign: { type: String, value: 'top' }, hasContent: { type: Boolean, readOnly: true } }, listeners: { 'tap': '_onTap' }, keyBindings: { 'up down': 'open', 'esc': 'close' }, hostAttributes: { tabindex: 0, role: 'combobox', 'aria-autocomplete': 'none', 'aria-haspopup': 'true' }, observers: [ '_selectedItemChanged(selectedItem)' ], attached: function() { // NOTE(cdata): Due to timing, a preselected value in a `IronSelectable` // child will cause an `iron-select` event to fire while the element is // still in a `DocumentFragment`. This has the effect of causing // handlers not to fire. So, we double check this value on attached: var contentElement = this.contentElement; if (contentElement && contentElement.selectedItem) { this._setSelectedItem(contentElement.selectedItem); } }, /** * The content element that is contained by the dropdown menu, if any. */ get contentElement() { return Polymer.dom(this.$.content).getDistributedNodes()[0]; }, /** * Show the dropdown content. */ open: function() { this.$.menuButton.open(); }, /** * Hide the dropdown content. */ close: function() { this.$.menuButton.close(); }, /** * A handler that is called when `iron-select` is fired. * * @param {CustomEvent} event An `iron-select` event. */ _onIronSelect: function(event) { this._setSelectedItem(event.detail.item); }, /** * A handler that is called when `iron-deselect` is fired. * * @param {CustomEvent} event An `iron-deselect` event. */ _onIronDeselect: function(event) { this._setSelectedItem(null); }, /** * A handler that is called when the dropdown is tapped. * * @param {CustomEvent} event A tap event. */ _onTap: function(event) { if (Polymer.Gestures.findOriginalTarget(event) === this) { this.open(); } }, /** * Compute the label for the dropdown given a selected item. * * @param {Element} selectedItem A selected Element item, with an * optional `label` property. */ _selectedItemChanged: function(selectedItem) { var value = ''; if (!selectedItem) { value = ''; } else { value = selectedItem.label || selectedItem.getAttribute('label') || selectedItem.textContent.trim(); } this._setValue(value); this._setSelectedItemLabel(value); }, /** * Compute the vertical offset of the menu based on the value of * `noLabelFloat`. * * @param {boolean} noLabelFloat True if the label should not float * above the input, otherwise false. */ _computeMenuVerticalOffset: function(noLabelFloat) { // NOTE(cdata): These numbers are somewhat magical because they are // derived from the metrics of elements internal to `paper-input`'s // template. The metrics will change depending on whether or not the // input has a floating label. return noLabelFloat ? -4 : 8; }, /** * Returns false if the element is required and does not have a selection, * and true otherwise. * @param {*=} _value Ignored. * @return {boolean} true if `required` is false, or if `required` is true * and the element has a valid selection. */ _getValidity: function(_value) { return this.disabled || !this.required || (this.required && !!this.value); }, _openedChanged: function() { var openState = this.opened ? 'true' : 'false'; var e = this.contentElement; if (e) { e.setAttribute('aria-expanded', openState); } }, _computeLabelClass: function(noLabelFloat, alwaysFloatLabel, hasContent) { var cls = ''; if (noLabelFloat === true) { return hasContent ? 'label-is-hidden' : ''; } if (hasContent || alwaysFloatLabel === true) { cls += ' label-is-floating'; } return cls; }, _valueChanged: function() { // Only update if it's actually different. if (this.$.input && this.$.input.textContent !== this.value) { this.$.input.textContent = this.value; } this._setHasContent(!!this.value); }, }); })(); </script> </dom-module>