UNPKG

api-console-assets

Version:

This repo only exists to publish api console components to npm

532 lines (499 loc) 16.5 kB
<!-- @license Copyright 2016 Pawel Psztyc, The ARC team 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. --> <link rel="import" href="../polymer/polymer.html"> <link rel="import" href="../paper-item/paper-item.html"> <link rel="import" href="../paper-ripple/paper-ripple.html"> <link rel="import" href="../paper-material/paper-material.html"> <link rel="import" href="../iron-overlay-behavior/iron-overlay-behavior.html"> <link rel="import" href="../iron-selector/iron-selector.html"> <link rel="import" href="../iron-a11y-keys/iron-a11y-keys.html"> <link rel="import" href="../iron-scroll-target-behavior/iron-scroll-target-behavior.html"> <link rel="import" href="../paper-progress/paper-progress.html"> <!-- # `<paper-autocomplete>` Use `paper-autocomplete` to add autocomplete functionality to the input elements. It also works wilt polymer inputs. The element works with static list of suggestions or with dynamic (asynchronous) operation that require calling te backend or local datastore. In second case you should set `loader` property which will display a loader animation while results are loaded. You must associate suggestions with the input field. This can be done by passing an element reference to the `target` property. ## Example: ### Static suggestions ``` <paper-input label="Enter fruit name" id="fruits"></paper-input> <paper-autocomplete id="fruitsSuggestions" target="[[fruits]]" on-selected="_fruitSelected"></paper-input-autocomplete> <script> document.querySelector('#fruitsSuggestions').source = ['Apple', 'Orange', 'Bananas']; </script> ``` ### Dynamic suggestions ``` <paper-input-container> <label>Enter friut name</label> <input is="iron-input" type="text" value="{{async::input}}" id="asyncField" /> </paper-input-container> <paper-autocomplete loader id="fruitAsync" on-query="_asyncSuggestions"></paper-input-autocomplete> <script> document.querySelector('#fruitAsync').target = document.querySelector('#asyncField'); document.querySelector('#fruitAsync').addEventListener('query', (e) => { var query = e.detail.value; asyncQuery(query, (suggestions) => { document.querySelector('#fruitAsync').source = suggestions; }); }); </script> ``` ## Displaying the suggestions Suggestions array can be either an array of strings or objects. For strings, displayed in the list and inserted to the input field value is the same item. You can set different list item display value and value inserted into the field when the array contains onject. Each object must contain `value` and `display` properties where `value` property will be inserted into the text field and `display` will be used to display description inside the list. ## Query event The `query` event is fired when the user query change in the way so the element is not able to display suggestions properly. This means if the user add a letter to previously entered value the query event will not fire since it already have list of suggestion that should be used to filter suggestions from. And again when the user will delete a letter the element will still have list of source suggestions to filter suggestions from. However, if the user change the query entirely it will fire `query` event and the app will expect to `source` to change. Setting source is not mandatory. ## Preventing from changing the input value To prevent the element to update the value of the target input, listent for `selected` event and cancel it by calling `event.preventDefault()` function. ## Styling Suggestions are positioned absolutely! You must include relative positioned parent to contain the suggestion display in the same area. Use CSS properties to position the display in the left bottom corner of the input field. `<paper-autocomplete>` provides the following custom properties and mixins for styling: | Custom property | Description | Default | ----------------|-------------|---------- | `--paper-autocomplete` | Mixin applied to the display | `{}` | @group UI Elements @element paper-autocomplete @demo demo/index.html @hero hero.svg --> <dom-module id="paper-autocomplete"> <style> :host { position: absolute !important; left: 0px; top: 52px; @apply(--paper-autocomplete); } paper-material { background-color: white; } #container { overflow: auto; } </style> <template> <paper-material elevation="2" id="container"> <paper-progress hidden$="[[!_showLoader]]" indeterminate></paper-progress> <iron-selector selected="{{selectedItem}}" id="selector"> <template is="dom-repeat" items="{{suggestions}}" id="repeater"> <paper-item> <div>{{_suggestionDisplay(item)}}</div> <paper-ripple></paper-ripple> </paper-item> </template> </iron-selector> </paper-material> <iron-a11y-keys id="a11y" target="[[target]]" keys="up" on-keys-pressed="selectPrevious"></iron-a11y-keys> <iron-a11y-keys id="a11y" target="[[target]]" keys="down" on-keys-pressed="selectNext"></iron-a11y-keys> <iron-a11y-keys id="a11y" target="[[target]]" keys="enter" on-keys-pressed="acceptSelection"></iron-a11y-keys> <iron-a11y-keys id="a11y" target="[[_keyTarget]]" keys="up" on-keys-pressed="selectPrevious"></iron-a11y-keys> <iron-a11y-keys id="a11y" target="[[_keyTarget]]" keys="down" on-keys-pressed="selectNext"></iron-a11y-keys> <iron-a11y-keys id="a11y" target="[[_keyTarget]]" keys="enter" on-keys-pressed="acceptSelection"></iron-a11y-keys> </template> </dom-module> <script> Polymer({ is: 'paper-autocomplete', behaviors: [ Polymer.IronOverlayBehavior, Polymer.IronScrollTargetBehavior ], /** * Fired when user entered some text into the input. * It is a time to query external datastore for suggestions and update "source" property. * Source should be updated event if the backend result with empty values and should set * the list to empty array. * * Nore that setting up source in response to this event after the user has closed * the dropdown it will have no effect at the moment. * * @event query * @param {String} value An entered phrase in text field. */ /** * Fired when the item was selected by the user. * At the time of receiving this event new value is already set in target input field. * * @event selected * @param {String} value Selected value */ properties: { /** * List of suggestions to display. * If the array items are strings they will be used for display a suggestions and * to insert a value. * If the list is an object the each object must contain `value` and `display` * properties. * The `display` property will be used in the suggestions list and the * `value` property will be used to insert the value to the referenced text field. * * @type {Array<Object>|Array<String>} */ source: { type: Array }, /** * `value` Selected object from the suggestions */ value: { type: Object, notify: true }, /** * List of suggestion that are displayed. */ suggestions: { type: Array, value: [], readOnly: true }, /** * A target input field to observe. * This may be an ID if the field or an element itself. * * If this is set the element will not display it's own input field. * @type {HTMLElement} */ target: HTMLElement, /** * Currently selected item on a suggestions list. * @type {Number} */ selectedItem: { type: Number, value: 0 }, scrollTarget: { type: Object, value: function() { return this.$.container; } }, sizingTarget: { type: HTMLElement, value: function() { return this.$.container; } }, /** * True when user query changed and waiting for `source` property update */ loading: { type: Boolean, value: false, readOnly: true, notify: true }, /** * Set this to true if you use async operation in response for query event. * This will display a loader when querying for more suggestions. * Do not use it it you do not handle suggestions asynchronously. */ loader: { type: Boolean, value: false }, _showLoader: { type: Boolean, computed: '_computeShowLoader(loader, loading)' }, isAttached: Boolean, // If true it will opend suggestions on input field focus. openOnFocus: { type: Boolean, value: false }, // this property is set when the `target` changes. It is used to remove // listeners. _oldTarget: HTMLElement, // An event target for key down event. _keyTarget: { type: HTMLElement, value: function() { return this; } } }, observers: [ '_targetChanged(target, isAttached)', '_filterSuggestions(source, _oldTarget, isAttached)' ], listeners: { 'tap': 'acceptSelection' }, /* Handler for target property change. */ _targetChanged: function(target, isAttached) { if (!isAttached) { return; } this.resetFit(); if (this._oldTarget) { this.unlisten(this._oldTarget, 'input', '_valueChanged'); this.unlisten(this._oldTarget, 'focus', '_targetFocus'); this.unlisten(this._oldTarget, 'click', '_targetClick'); this._oldTarget = null; } if (!target) { return; } if (typeof target === 'string') { this.target = this.domHost ? this.domHost.$[target] : Polymer.dom(this.ownerDocument).querySelector('#' + target); //this will call the function again with the element attached. } else if (target) { this.listen(target, 'input', '_valueChanged'); this.listen(target, 'focus', '_targetFocus'); this.listen(target, 'click', '_targetClick'); this._oldTarget = target; if (target === document.activeElement) { this._targetFocus(); } } }, /** * Handler to be called when input of the text field changes. */ _valueChanged: function() { if (!this.isAttached || !this._oldTarget) { return; } var value = this._oldTarget.value; if (this._previousQuery) { if (value.indexOf(this._previousQuery) === 0) { this._previousQuery = value; this._filterSuggestions(); return; } else { //this is a new query this._previousQuery = null; this._setSuggestions([]); } } else if (!value && this._previousQuery === undefined) { // First query without the value means initialization. return; } this.fire('query', { value: value }); this._previousQuery = value; if (!this.opened) { this.selectedItem = 0; } this._filterSuggestions(); this._setLoading(true); }, /** * Filter `source` array for current value. */ _filterSuggestions: function() { //source, _oldTarget, isAttached if (!this.isAttached || !this._oldTarget) { return; } if (this._previousQuery === undefined) { return; } this._setLoading(false); var source = this.source; if (!source) { this._setSuggestions([]); return; } var query = this._previousQuery ? this._previousQuery.toLowerCase() : ''; var filter = function(item) { var value = (typeof item === 'string') ? item : item.value; return value.toLowerCase().indexOf(query) !== -1; }; var filtered = query ? source.filter(filter) : source; if (filtered.length === 0) { this.close(); return; } filtered.sort(function(a, b) { var valueA = (typeof a === 'string') ? a : a.value; var valueB = (typeof b === 'string') ? b : b.value; var aIndex = valueA.indexOf(query); var bIndex = valueB.indexOf(query); if (aIndex === 0 && bIndex !== 0) { return 1; } if (aIndex !== 0 && bIndex === 0) { return -1; } if (valueA > valueB) { return 1; } if (valueA < valueB) { return -1; } return 0; }); this._setSuggestions(filtered); this.notifyResize(); this._ensureSelection(); this.opened = true; }, /* Compute suggestion display value */ _suggestionDisplay: function(item) { return item.value || item; }, /** * Highlight previous suggestion */ selectPrevious: function() { if (!this.suggestions || !this.suggestions.length) { return; } if (!this.opened) { this.opened = true; } this.$.selector.selectPrevious(); this.ensureItemVisible(false); }, /** * Highlight next suggestion */ selectNext: function() { if (!this.suggestions || !this.suggestions.length) { return; } if (!this.opened) { this.opened = true; } this.$.selector.selectNext(); this.ensureItemVisible(true); }, /** * Accepts currently selected suggestion and enters it into a text field. */ acceptSelection: function() { if (!this.opened || !this.suggestions || !this.suggestions.length || !this.$.selector.selectedItem) { return; } var value = this.$.repeater.itemForElement(this.$.selector.selectedItem); if (typeof value !== 'string') { value = value.value; } this.async(function() { this._inform(value); }, 1); }, _inform: function(value) { var e = this.fire('selected', { value: value }, { cancelable: true }); if (!e.defaultPrevented) { this.target.value = value; } this.close(); }, /** * Ensure that the selected item is visible in the scroller. * When there is more elements to show than space available (height) * then some elements will be hidden. When the user use arrows to navigate * the selection may get out from the screen. This function ensures that * currently selected element is visible. * * @param {Boolean} bottom If trully it will ensure that the element is * visible at the bottom of the container. On the top otherwise. */ ensureItemVisible: function(bottom) { if (!this.opened || !this.suggestions || !this.suggestions.length) { return; } var container = this.scrollTarget; var index = this.$.selector.selected; if (bottom && index === 0) { this.scroll(0); return; } var toMove; if (!bottom && index === this.suggestions.length - 1) { toMove = container.scrollHeight - container.offsetHeight; this.scroll(0, toMove); return; } var item = this.$.selector.selectedItem; var containerOffsetHeight = bottom ? container.offsetHeight : 0; var itemOffsetHeight = bottom ? item.offsetHeight : 0; var visible = containerOffsetHeight + container.scrollTop; var treshold = item.offsetTop + itemOffsetHeight; if (bottom && treshold > visible) { toMove = item.offsetHeight + item.offsetTop - container.offsetHeight; this.scroll(0, toMove); } else if (!bottom && visible > treshold) { this.scroll(0, treshold); } }, _computeShowLoader: function(loader, loading) { return !!loader && !!loading; }, _targetFocus: function() { if (!this.openOnFocus || this.opened) { return; } this._previousQuery = this._previousQuery || ''; this.debounce('autocomplete-focus', this._valueChanged, 20); }, /** * Prohibits click event propagation when the overlay is opened so * the overlay manager won't close it immidietly after focusing (with click * event included) in the target field. */ _targetClick: function(e) { if (!this.opened) { return; } e.stopPropagation(); e.stopImmediatePropagation(); }, /** * Selects a first available item after filtering results and missing * selection. */ _ensureSelection: function() { if (this.$.selector.selectedItem) { return; } this.$.selector.selected = 0; } }); </script>