@blinkk/editor
Version:
Structured content editor with live previews.
277 lines • 9.67 kB
JavaScript
"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"
=${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}
=${handleClick}
>
${this.label}
</div>`;
}
}
exports.AutoCompleteUIItem = AutoCompleteUIItem;
//# sourceMappingURL=autocomplete.js.map