| /** |
| * @module module/query-sk |
| * @description <h2><code>query-sk</code></h2> |
| * |
| * Starting from a serialized paramtools.ParamSet, this control allows the user |
| * to build up a query, suitable for passing to query.New. |
| * |
| * @evt query-change - The 'query-sk' element will produce 'query-change' events when the query |
| * parameters chosen have changed. The event contains the current selections formatted as a URL query, found in e.detail.q. |
| * |
| * @evt query-change-delayed - The same exact event as query-change, but with a 500ms delay after |
| * the query stops changing. |
| * |
| * @attr {string} current_query - The current query formatted as a URL formatted query string. |
| * @attr {boolean} hide_invert - If the option to invert a query should be made available to |
| * the user. |
| * @attr {boolean} hide_regex - If the option to include regex in the query should be made |
| * available to the user. |
| */ |
| import { define } from 'elements-sk/define'; |
| import { html } from 'lit-html'; |
| import { toParamSet, fromParamSet } from 'common-sk/modules/query'; |
| import { ElementSk } from '../ElementSk'; |
| |
| import '../query-values-sk'; |
| import 'elements-sk/select-sk'; |
| import 'elements-sk/styles/buttons'; |
| |
| const _keys = (ele) => ele._keys.map((k) => html`<div>${k}</div>`); |
| |
| const template = (ele) => html` |
| <div> |
| <label>Filter <input id=fast @input=${ele._fastFilter}></label> |
| <button @click=${ele._clearFilter}>Clear Filter</button> |
| </div> |
| <div class=bottom> |
| <div class=selection> |
| <select-sk @selection-changed=${ele._keyChange}> |
| ${_keys(ele)} |
| </select-sk> |
| <button @click=${ele._clear}>Clear Selections</button> |
| </div> |
| <query-values-sk id=values @query-values-changed=${ele._valuesChanged} |
| ?hide_invert=${ele.hide_invert} ?hide_regex=${ele.hide_regex}></query-values-sk> |
| </div> |
| `; |
| |
| // The delay in ms before sending a delayed query-change event. |
| const DELAY_MS = 500; |
| |
| define('query-sk', class extends ElementSk { |
| constructor() { |
| super(template); |
| this._paramset = {}; |
| this._originalParamset = {}; |
| this._key_order = []; |
| |
| // We keep the current_query as an object. |
| this._query = {}; |
| |
| // The full set of keys in the desired order. |
| this._keys = []; |
| |
| // The id of a pending timeout func that will send a delayed query-change event. |
| this._delayedTimeout = null; |
| } |
| |
| connectedCallback() { |
| super.connectedCallback(); |
| this._upgradeProperty('paramset'); |
| this._upgradeProperty('key_order'); |
| this._upgradeProperty('current_query'); |
| this._upgradeProperty('hide_invert'); |
| this._upgradeProperty('hide_regex'); |
| this._render(); |
| this._values = this.querySelector('#values'); |
| this._keySelect = this.querySelector('select-sk'); |
| this._fast = this.querySelector('#fast'); |
| } |
| |
| _valuesChanged(e) { |
| const key = this._keys[this._keySelect.selection]; |
| if (this._fast.value.trim() !== '') { |
| // Things get complicated if the user has entered a filter. The user may |
| // have selections in this._query[key] which don't appear in e.detail |
| // because they have been filtered out, so we should only add/remove |
| // values from this._query[key] that appear in this._paramset[key]. |
| |
| // Make everything into Sets to make our lives easier. |
| const valuesDisplayed = new Set(this._paramset[key]); |
| const currentQueryForKey = new Set(this._query[key]); |
| const selectionsFromEvent = new Set(e.detail); |
| // Loop over valuesDisplayed, if the value appears in selectionsFromEvent |
| // then add it to currentQueryForKey, otherwise remove it from |
| // currentQueryForKey. |
| valuesDisplayed.forEach((value) => { |
| if (selectionsFromEvent.has(value)) { |
| currentQueryForKey.add(value); |
| } else { |
| currentQueryForKey.delete(value); |
| } |
| }); |
| this._query[key] = [...currentQueryForKey]; |
| } else { |
| this._query[key] = e.detail; |
| } |
| this._queryChanged(); |
| } |
| |
| _keyChange() { |
| if (this._keySelect.selection === -1) { |
| return; |
| } |
| const key = this._keys[this._keySelect.selection]; |
| this._values.options = this._paramset[key] || []; |
| this._values.selected = this._query[key] || []; |
| this._render(); |
| } |
| |
| _recalcKeys() { |
| const keys = Object.keys(this._paramset); |
| keys.sort(); |
| // Pull out all the keys that appear in _key_order to be pushed to the front of the list. |
| const pre = this._key_order.filter((ordered) => keys.indexOf(ordered) > -1); |
| const post = keys.filter((key) => pre.indexOf(key) === -1); |
| this._keys = pre.concat(post); |
| } |
| |
| _queryChanged() { |
| const prev_query = this.current_query; |
| // Rationalize the _query, i.e. remove keys that don't exist in the |
| // paramset. |
| const originalKeys = Object.keys(this._originalParamset); |
| Object.keys(this._query).forEach((key) => { |
| if (originalKeys.indexOf(key) === -1) { |
| delete this._query[key]; |
| } |
| }); |
| this.current_query = fromParamSet(this._query); |
| if (prev_query !== this.current_query) { |
| this.dispatchEvent(new CustomEvent('query-change', { |
| detail: { q: this.current_query }, |
| bubbles: true, |
| })); |
| clearTimeout(this._delayedTimeout); |
| this._delayedTimeout = setTimeout(() => { |
| this.dispatchEvent(new CustomEvent('query-change-delayed', { |
| detail: { q: this.current_query }, |
| bubbles: true, |
| })); |
| }, DELAY_MS); |
| } |
| } |
| |
| _clear() { |
| this._query = {}; |
| this._recalcKeys(); |
| this._queryChanged(); |
| this._keyChange(); |
| this._render(); |
| } |
| |
| _fastFilter() { |
| const filters = this._fast.value.trim().toLowerCase().split(/\s+/); |
| |
| // Create a closure that returns true if the given label matches the filter. |
| const matches = (s) => { |
| s = s.toLowerCase(); |
| return filters.filter((f) => s.indexOf(f) > -1).length > 0; |
| }; |
| |
| // Loop over this._originalParamset. |
| const filtered = {}; |
| Object.keys(this._originalParamset).forEach((paramkey) => { |
| // If the param key matches, then all the values go over. |
| if (matches(paramkey)) { |
| filtered[paramkey] = this._originalParamset[paramkey]; |
| } else { |
| // Look for matches in the param values. |
| const valueMatches = []; |
| this._originalParamset[paramkey].forEach((paramvalue) => { |
| if (matches(paramvalue)) { |
| valueMatches.push(paramvalue); |
| } |
| }); |
| if (valueMatches.length > 0) { |
| filtered[paramkey] = valueMatches; |
| } |
| } |
| }); |
| |
| this._paramset = filtered; |
| this._recalcKeys(); |
| this._keyChange(); |
| this._render(); |
| } |
| |
| _clearFilter() { |
| this._fast.value = ''; |
| this.paramset = this._originalParamset; |
| this._queryChanged(); |
| } |
| |
| /** @prop paramset {Object} A serialized paramtools.ParamSet. */ |
| get paramset() { return this._paramset; } |
| |
| set paramset(val) { |
| // Record the current key so we can restore it later. |
| let prevSelectKey = ''; |
| if (this._keySelect && this._keySelect.selection) { |
| prevSelectKey = this._keys[this._keySelect.selection]; |
| } |
| |
| this._paramset = val; |
| this._originalParamset = val; |
| this._recalcKeys(); |
| if (this._fast && this._fast.value.trim() !== '') { |
| this._fastFilter(); |
| } |
| this._render(); |
| |
| // Now re-select the current key if it still exists post-filtering. |
| if (this._keySelect && prevSelectKey && this._keys.indexOf(prevSelectKey) !== -1) { |
| this._keySelect.selection = this._keys.indexOf(prevSelectKey); |
| this._keyChange(); |
| } |
| } |
| |
| /** @prop key_order {string} An array of strings, the keys in the order they |
| * should appear. All keys not in the key order will be present after and in |
| * alphabetical order. |
| */ |
| get key_order() { return this._key_order; } |
| |
| set key_order(val) { |
| this._key_order = val; |
| this._recalcKeys(); |
| this._render(); |
| } |
| |
| /** @prop hide_invert {boolean} Mirrors the hide_invert attribute. */ |
| get hide_invert() { return this.hasAttribute('hide_invert'); } |
| |
| set hide_invert(val) { |
| if (val) { |
| this.setAttribute('hide_invert', ''); |
| } else { |
| this.removeAttribute('hide_invert'); |
| } |
| this._render(); |
| } |
| |
| /** @prop hide_regex {boolean} Mirrors the hide_regex attribute. */ |
| get hide_regex() { return this.hasAttribute('hide_regex'); } |
| |
| set hide_regex(val) { |
| if (val) { |
| this.setAttribute('hide_regex', ''); |
| } else { |
| this.removeAttribute('hide_regex'); |
| } |
| this._render(); |
| } |
| |
| static get observedAttributes() { |
| return ['current_query', 'hide_invert', 'hide_regex']; |
| } |
| |
| /** @prop current_query {string} Mirrors the current_query attribute. */ |
| get current_query() { return this.getAttribute('current_query'); } |
| |
| set current_query(val) { this.setAttribute('current_query', val); } |
| |
| attributeChangedCallback(name, oldValue, newValue) { |
| if (name === 'current_query') { |
| // Convert the current_query string into an object. |
| this._query = toParamSet(newValue); |
| } |
| |
| this._render(); |
| } |
| }); |