UNPKG

@blinkk/editor

Version:

Structured content editor with live previews.

277 lines 9.67 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.AutoCompleteUIItem = exports.AutoCompleteUi = exports.AutoCompleteMixin = void 0; const mixins_1 = require("@blinkk/selective-edit/dist/src/mixins"); const listeners_1 = require("./listeners"); const selective_edit_1 = require("@blinkk/selective-edit"); const events_1 = require("../editor/events"); function AutoCompleteMixin(Base) { return class AutoCompleteClass extends Base { get autoCompleteUi() { if (!this._autoCompleteUi) { this._autoCompleteUi = new AutoCompleteUi(); } return this._autoCompleteUi; } }; } exports.AutoCompleteMixin = AutoCompleteMixin; class AutoCompleteUi extends listeners_1.ListenersMixin(mixins_1.Base) { filter(value) { this.currentFilter = value; this.filteredItems = this.items?.filter(item => item.matchesFilter(value)); // If it is an exact match show all the items instead. if (this.filteredItems?.length === 1 && value === this.filteredItems[0].value) { this.filteredItems = this.items; } this.currentIndex = undefined; } handleFocus(evt) { // Store the parent container of the input to detect when clicking // outside of the container to close the auto complete. // This means that the autocomplete UI should be a sibling of the // input field. this.container = evt.target .parentElement; // Bind once to the document click to detect when the focis is // lost from both the input and the autocomplete. if (!this.hasBoundDocument) { this.hasBoundDocument = true; document.addEventListener('click', (clickEvt) => { if (!this.container || !this.isVisible) { return; } let target = clickEvt.target; while (target) { if (target === this.container) { return; } target = target.parentElement; } this.isVisible = false; this.render(); }); } // Filter using the current field value. this.filter(evt.target.value || ''); this.isVisible = true; this.render(); } get items() { return this._items; } set items(values) { this._items = values; if (this.currentFilter) { this.filter(this.currentFilter); } else { this.filteredItems = [...(values || [])]; } } // eslint-disable-next-line @typescript-eslint/no-unused-vars handleIconClick(evt) { this.isVisible = !this.isVisible; this.render(); } handleInputKeyDown(evt) { // Detect when tabbing away from the input to close the autocomplete. switch (evt.key) { case 'Tab': this.isVisible = false; this.render(); break; } } handleInputKeyUp(evt) { switch (evt.key) { case 'ArrowDown': this.isVisible = true; this.nextItem(); this.scrollToItem(); break; case 'ArrowLeft': case 'ArrowRight': case 'Shift': return; case 'ArrowUp': this.isVisible = true; this.previousItem(); this.scrollToItem(); break; case 'Enter': case ' ': if (this.currentIndex !== undefined && this.filteredItems) { this.selectItem(this.filteredItems[this.currentIndex]); return; } break; case 'Escape': this.isVisible = false; break; default: this.isVisible = true; // Trigger the listener for a keyup event. // This allows the field to handle updating the filter list options. this.triggerListener('keyup', evt.target.value); } this.render(); } nextItem() { // Check for empty items. if (!this.filteredItems?.length) { this.currentIndex = undefined; return; } // Loop the options. if (this.currentIndex === this.filteredItems.length - 1) { this.currentIndex = 0; return; } this.currentIndex = this.currentIndex !== undefined ? this.currentIndex + 1 : 0; } previousItem() { // Check for empty items. if (!this.filteredItems?.length) { this.currentIndex = undefined; return; } // Loop the options. if (this.currentIndex === 0) { this.currentIndex = this.filteredItems.length - 1; return; } this.currentIndex = this.currentIndex !== undefined ? this.currentIndex - 1 : this.filteredItems.length - 1; } /** * Signal for the editor to re-render. */ render() { document.dispatchEvent(new CustomEvent(events_1.EVENT_RENDER)); } /** * Make sure that the current index item is visible in the list. * If it is not, scroll to the item. */ scrollToItem() { if (!this.container) { return; } const listElement = this.container.querySelector('.selective__autocomplete__list'); if (!listElement) { console.error('Unable to find the autocomplete list.'); return; } const itemElement = listElement?.querySelector(`[data-index="${this.currentIndex}"]`); if (!itemElement) { console.error('Unable to find the autocomplete item.'); return; } // Center scroll to the item. listElement.scrollTo({ top: itemElement.offsetTop + itemElement.offsetHeight / 2 - listElement.offsetHeight / 2, left: 0, behavior: 'smooth', }); } selectItem(item) { this.triggerListener('select', item.value); this.filter(item.value); this.isVisible = false; this.render(); } // eslint-disable-next-line @typescript-eslint/no-unused-vars templateIcons(editor) { return selective_edit_1.html `<div class="selective__field__actions"> <div class="selective__action selective__tooltip--left" @click=${this.handleIconClick.bind(this)} aria-label="Toggle list" data-tip="Toggle list" > <span class="material-icons">list_alt</span> </div> </div>`; } templateList(editor, value) { if (!this.isVisible) { return selective_edit_1.html ``; } // Certain fields allow values that are not in the list but do not need // to show the empty items status and should hide the entire list. if ((this.filteredItems || []).length === 0) { if (this.shouldShowEmpty && !this.shouldShowEmpty(value)) { return selective_edit_1.html ``; } } return selective_edit_1.html `<div class="selective__autocomplete"> <div class="selective__autocomplete__list" role="listbox"> ${this.templateStatus(editor, this.filteredItems || [])} ${selective_edit_1.repeat(this.filteredItems || [], item => item.uid, (item, index) => item.templateItem(editor, index, this.currentIndex === index, () => { this.selectItem(item); }))} </div> </div>`; } templateStatus(editor, items) { let statusString = this.labels?.resultsMultiple ? this.labels.resultsMultiple.replace('${items.length}', `${items.length}`) : `${items.length} results available.`; if (items.length === 0) { statusString = this.labels?.resultsNone || 'No results available.'; } else if (items.length === 1) { statusString = this.labels?.resultsSingle || '1 result available.'; } return selective_edit_1.html `<div class="selective__autocomplete__list__status" aria-live="polite" role="status" > ${statusString} </div>`; } } exports.AutoCompleteUi = AutoCompleteUi; class AutoCompleteUIItem extends selective_edit_1.UuidMixin(mixins_1.Base) { constructor(value, label) { super(); this.value = value; this.label = label; } /** * Used for filtering down the items in the items list. * * Uses case-insensitive matching for the value in the label or value. * * @param value Value input in the field. * @returns True if the value matches the value in some way. */ matchesFilter(value) { value = value.toLowerCase(); return (this.value.toLocaleLowerCase().includes(value) || this.label.toLowerCase().includes(value)); } templateItem(editor, index, isSelected, handleClick) { return selective_edit_1.html ` <div aria-selected=${isSelected ? 'true' : 'false'} class="selective__autocomplete__list__item" role="option" tabindex="-1" data-index=${index} data-value=${this.value} @click=${handleClick} > ${this.label} </div>`; } } exports.AutoCompleteUIItem = AutoCompleteUIItem; //# sourceMappingURL=autocomplete.js.map