217 lines
5.3 KiB
JavaScript
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);
|