Files
tp-tag-input/tp-tag-input-popup.js
2025-11-08 18:49:13 +01:00

217 lines
5.3 KiB
JavaScript

/**
@license
Copyright (c) 2024 trading_peter
This program is available under Apache License Version 2.0
*/
import { LitElement, html, css } from 'lit';
import { DomQuery } from '@tp/helpers/dom-query.js';
import { EventHelpers } from '@tp/helpers/event-helpers.js';
import { closest } from '@tp/helpers/closest.js';
class TpTagInputPopup extends DomQuery(EventHelpers(LitElement)) {
static get styles() {
return [
css`
:host {
display: none;
position: fixed;
border-radius: 2px;
background: #ffffff;
z-index: 1;
left: 0;
width: 100%;
margin-top: 5px;
box-shadow: var(--tp-tag-input-popup-shadow, none);
}
#list {
max-height: 150px;
overflow-y: auto;
}
.item {
line-height: 25px;
padding: 3px 10px;
white-space: nowrap;
overflow: hidden;
cursor: pointer;
text-overflow: ellipsis;
}
.item[selected] {
background: #e4e4e4;
}
`
];
}
render() {
const { selection } = this;
return html`
<div class="wrap">
<div id="list" @click=${this._onItemClick}>
${this.filteredItems.map((item, idx) => html`
<div class="item" .item=${item} ?selected=${item.value === this.value || idx === selection}>${item.label}</div>
`)}
</div>
</div>
`;
}
static get is() { return 'era-autocomplete-popup'; }
static get properties() {
return {
/**
* The choosen item.
*/
value: { type: String },
/**
* List of items available for auto complete.
*/
items: { type: Array },
filteredItems: { type: Array },
/**
* The filter string to narrow down the items.
*/
filter: { type: String },
selection: { type: Number },
visible: { type: Boolean }
};
}
constructor() {
super();
this.visible = false;
this.filteredItems = [];
}
get target() {
return closest(this, 'tp-tag-input', true);
}
shouldUpdate(changes) {
if (changes.has('filter') || changes.has('items')) {
this._filterItemsByTerm();
}
return true;
}
firstUpdated() {
this.listen(this.$.list, 'mousedown', '_onItemClick');
}
disconnectedCallback() {
super.disconnectedCallback();
this.unlisten(this.$.list, 'mousedown', '_onItemClick');
}
show() {
if (this.visible) {
return;
}
var targetRect = this.target.getBoundingClientRect();
this.style.top = (targetRect.top + targetRect.height) + 'px';
this.style.left = targetRect.left + 'px';
this.style.width = Math.max(100, targetRect.width) + 'px';
this.listen(this.target, 'keydown', '_onHostKeydown');
this.style.display = 'block';
this.visible = true;
}
hide() {
this.style.display = 'none';
this.unlisten(this.target, 'keydown', '_onHostKeydown');
this.visible = false;
}
_onItemClick(e) {
const item = closest(e.composedPath()[0], '.item', true);
if (!item) return;
this.value = item.item;
this.dispatchEvent(new CustomEvent('selection-changed', { detail: this.value, bubbles: true, composed: true }));
this.hide();
this.target.focus();
}
_filterItemsByTerm() {
if (!Array.isArray(this.items)) return;
if (!this.filter) {
this.filteredItems = this.items;
} else {
const term = this.filter.toLowerCase();
this.filteredItems = this.items.filter(item => item.label.toLowerCase().includes(term));
}
}
_onHostKeydown(e) {
const count = this.$.list.querySelectorAll('.item').length;
let el;
switch (e.keyCode) {
case 38: // Up
this.selection = this.selection - 1 < 0 ? count - 1 : this.selection - 1;
el = this.$.list.querySelector('.item[selected]');
this._scrollIntoView(el, this.$.list);
e.preventDefault();
e.stopPropagation();
return;
case 40: // Down
if (isNaN(this.selection) || this.selection === null) {
this.selection = 0;
} else {
this.selection = (this.selection + 1) % count;
}
el = this.$.list.querySelector('.item[selected]');
this._scrollIntoView(el, this.$.list);
e.preventDefault();
e.stopPropagation();
return;
case 13: // Enter
const item = this.$.list.querySelector('.item[selected]');
if (item) {
this.value = item.item;
this.dispatchEvent(new CustomEvent('selection-changed', { detail: this.value, bubbles: true, composed: true }));
}
this.hide();
e.preventDefault();
e.stopPropagation();
return;
case 27: // Esc
this.hide();
e.preventDefault();
e.stopPropagation();
return;
}
}
_scrollIntoView(element, container) {
if (!element) {
return;
}
const containerTop = container.scrollTop;
const containerBottom = containerTop + container.offsetHeight;
const elTop = element.offsetTop;
const elBottom = elTop + element.offsetHeight;
if (elTop < containerTop) {
container.scrollTop = elTop;
} else if (elBottom > containerBottom) {
container.scrollTop = elBottom - container.offsetHeight;
}
}
}
window.customElements.define('tp-tag-input-popup', TpTagInputPopup);