394 lines
9.3 KiB
JavaScript
394 lines
9.3 KiB
JavaScript
/**
|
|
@license
|
|
Copyright (c) 2024 trading_peter
|
|
This program is available under Apache License Version 2.0
|
|
*/
|
|
|
|
import '@tp/tp-input/tp-input.js';
|
|
import '@tp/tp-icon/tp-icon.js';
|
|
import './tp-tag-input-popup.js';
|
|
import { LitElement, html, css, svg } from 'lit';
|
|
import { FormElement } from '@tp/helpers/form-element.js';
|
|
import { EventHelpers } from '@tp/helpers/event-helpers.js';
|
|
import { DomQuery } from '@tp/helpers/dom-query.js';
|
|
import { reach } from '@tp/helpers/reach.js';
|
|
import { debounce } from '@tp/helpers/debounce.js';
|
|
import { clone } from '@tp/helpers/clone.js';
|
|
import { closest } from '@tp/helpers/closest.js';
|
|
|
|
const mixins = [
|
|
FormElement,
|
|
EventHelpers,
|
|
DomQuery
|
|
];
|
|
|
|
/* @litElement */
|
|
const BaseElement = mixins.reduce((baseClass, mixin) => {
|
|
return mixin(baseClass);
|
|
}, LitElement);
|
|
|
|
class TpTagInput extends BaseElement {
|
|
static get styles() {
|
|
return [
|
|
css`
|
|
:host {
|
|
display: block;
|
|
border-radius: 2px;
|
|
border: solid 1px #BDBDBD;
|
|
}
|
|
|
|
:host([invalid]) {
|
|
border: solid 1px #B71C1C;
|
|
}
|
|
|
|
[hidden] {
|
|
display: none;
|
|
}
|
|
|
|
.wrap {
|
|
display: flex;
|
|
flex-direction: row;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
tp-input {
|
|
min-height: 15px;
|
|
outline: none;
|
|
margin: 0;
|
|
padding: 0;
|
|
display: flex;
|
|
flex: 1;
|
|
}
|
|
|
|
tp-input::part(wrap) {
|
|
border: none;
|
|
flex: 1;
|
|
}
|
|
|
|
:host(.has-items) tp-input {
|
|
margin-top: 10px;
|
|
}
|
|
|
|
.item {
|
|
display: inline-block;
|
|
margin: 5px;
|
|
border-radius: 2px;
|
|
border: solid 1px #9E9E9E;
|
|
background: #FFFFFF;
|
|
font-size: 14px;
|
|
padding: 3px 5px 3px 3px;
|
|
margin-right: 5px;
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
}
|
|
|
|
.item tp-icon {
|
|
--tp-icon-width: 12px;
|
|
--tp-icon-height: 12px;
|
|
margin-right: 2px;
|
|
}
|
|
`
|
|
];
|
|
}
|
|
|
|
static get icon() {
|
|
return svg`<path fill="var(--tp-icon-color)" d="M20 6.91L17.09 4L12 9.09L6.91 4L4 6.91L9.09 12L4 17.09L6.91 20L12 14.91L17.09 20L20 17.09L14.91 12L20 6.91Z" />`;
|
|
}
|
|
|
|
render() {
|
|
const value = this.value || [];
|
|
|
|
return html`
|
|
<div class="wrap" part="wrap" @click=${this._onClick}>
|
|
${value.map(item => html`
|
|
<div class="item" .item=${item}>
|
|
<tp-icon class="remove-icon" .icon=${TpTagInput.icon}></tp-icon>
|
|
${item.label}
|
|
</div>
|
|
`)}
|
|
<tp-input id="input" .validator=${this.validator} @keydown=${e => this._inputChanged(e)} required>
|
|
<input type="text" id="innerInput" .pattern=${this.pattern} autocomplete="off">
|
|
</tp-input>
|
|
</div>
|
|
<tp-tag-input-popup id="autoCpl" .items=${this.items} @selection-changed=${e => this._itemSelected(e.detail)}></tp-tag-input-popup>
|
|
`;
|
|
}
|
|
|
|
static get properties() {
|
|
return {
|
|
value: { type: Array },
|
|
items: { type: Array },
|
|
validator: { type: Object },
|
|
allowUnknown: { type: Boolean },
|
|
allowDuplicates: { type: Boolean },
|
|
tooltipRemove: { type: String },
|
|
min: { type: Number },
|
|
max: { type: Number },
|
|
invalid: { type: Boolean },
|
|
filter: { type: String },
|
|
pattern: { type: String },
|
|
triggerKeys: { type: String },
|
|
_input: { type: String },
|
|
};
|
|
}
|
|
|
|
constructor() {
|
|
super();
|
|
this.allowUnknown = false;
|
|
this.allowDuplicates = false;
|
|
this.triggerKeys = 'enter';
|
|
this.tooltipRemove = 'Remove';
|
|
this._input = '';
|
|
|
|
this._callFilterApiDebounced = debounce(this._callFilterApi.bind(this), 300, true);
|
|
}
|
|
|
|
firstUpdated() {
|
|
this.listen(this.$.innerInput, 'keydown', '_inputKeyDown');
|
|
this.listen(this.$.input, 'blur', '_inputBlur');
|
|
this.listen(this, 'focus', '_onFocus');
|
|
}
|
|
|
|
disconnectedCallback() {
|
|
super.disconnectedCallback();
|
|
|
|
this.unlisten(this.$.innerInput, 'keydown', '_inputKeyDown');
|
|
this.unlisten(this.$.innerInput, 'blur', '_inputBlur');
|
|
this.unlisten(this, 'focus', '_onFocus');
|
|
}
|
|
|
|
// shouldUpdate(changes) {
|
|
// if (changes.has('_input')) {
|
|
// this._inputChanged();
|
|
// }
|
|
// }
|
|
|
|
validate() {
|
|
const val = Array.isArray(this.value) ? this.value : [];
|
|
|
|
if ((this.required && val.length === 0) ||
|
|
(this.min > 0 && val.length < this.min) ||
|
|
(this.max > 0 && val.length > this.max)) {
|
|
this.invalid = true;
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
_onClick(e) {
|
|
if (closest(e.composedPath()[0], '.remove-icon', true)) {
|
|
const item = closest(e.composedPath()[0], '.item', true);
|
|
if (!item) return;
|
|
this.value = this.value.filter(i => i.value !== item.item.value);
|
|
}
|
|
}
|
|
|
|
_inputChanged(e) {
|
|
this._input = this.$.input.value;
|
|
|
|
if (this.api && this._input.length > 2) {
|
|
this._callFilterApiDebounced();
|
|
return;
|
|
}
|
|
|
|
if (this._input.length > 0) {
|
|
this.$.autoCpl.show();
|
|
} else {
|
|
this.$.autoCpl.hide();
|
|
}
|
|
}
|
|
|
|
async _callFilterApi() {
|
|
const apiFunc = typeof this.api === 'function' ? this.api : reach(this.api, window);
|
|
if (typeof apiFunc !== 'function') return;
|
|
|
|
this.items = [];
|
|
|
|
const apiResp = apiFunc({ value: this._input });
|
|
const promisedResult = apiResp.completes ? apiResp.completes : apiResp;
|
|
|
|
const result = await promisedResult;
|
|
this.$.autoCpl.loading = false;
|
|
|
|
if (Array.isArray(result)) {
|
|
this.items = result;
|
|
this.$.autoCpl.show();
|
|
return;
|
|
}
|
|
|
|
const resp = result.response;
|
|
if (resp && resp.statusCode === 200) {
|
|
this.items = resp.data;
|
|
this.$.autoCpl.show();
|
|
}
|
|
}
|
|
|
|
_itemSelected(item) {
|
|
if (item !== null) {
|
|
if (this.allowDuplicates || !this._isAlreadySelected(item.label)) {
|
|
if (!Array.isArray(this.value)) {
|
|
this.value = [ clone(item) ];
|
|
} else {
|
|
this.value = [...this.value, clone(item)];
|
|
}
|
|
}
|
|
this._clearInput();
|
|
this.$.autoCpl.selection = null;
|
|
this.$.autoCpl.value = null;
|
|
}
|
|
}
|
|
|
|
_inputBlur() {
|
|
if (!this._tryToAddItem()) {
|
|
this._clearInput();
|
|
this.$.autoCpl.selection = null;
|
|
this.$.autoCpl.value = null;
|
|
this.$.autoCpl.hide();
|
|
}
|
|
}
|
|
|
|
_prepFilter(filter, api) {
|
|
if (api) {
|
|
return ''; // No filter because items come from api called and should already be filtered by a server.
|
|
} else {
|
|
return filter;
|
|
}
|
|
}
|
|
|
|
_maxLengthReached() {
|
|
return (this.value || []).length === this.max && this.max > 0;
|
|
}
|
|
|
|
_inputKeyDown(e) {
|
|
if (this._keyboardEventMatchesKeys(e, this.triggerKeys)) {
|
|
e.preventDefault();
|
|
this._tryToAddItem();
|
|
return;
|
|
}
|
|
|
|
if (this._keyboardEventMatchesKeys(e, 'backspace')) {
|
|
if (this._input === '' && this.value) {
|
|
this.value = this.value.slice(0, -1);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (this._maxLengthReached() && !this._keyboardEventMatchesKeys(e, 'tab')) {
|
|
e.preventDefault();
|
|
}
|
|
}
|
|
|
|
_tryToAddItem() {
|
|
if (this._maxLengthReached()) {
|
|
return;
|
|
}
|
|
|
|
if (typeof this.$.autoCpl.selection === 'number') {
|
|
return;
|
|
}
|
|
|
|
if (this.allowUnknown) {
|
|
if (this.$.input.validate()) {
|
|
var item = this._getItemByLabel(this._input);
|
|
if (!item) {
|
|
item = {
|
|
value: this._getFreeValue(),
|
|
label: this._input,
|
|
custom: true
|
|
};
|
|
|
|
this.push('items', item);
|
|
}
|
|
|
|
if (!this.allowDuplicates && this._isAlreadySelected(this._input)) {
|
|
return false;
|
|
}
|
|
|
|
this._itemSelected(item);
|
|
return true;
|
|
}
|
|
} else {
|
|
if (this.$.autoCpl.filteredItems.length > 0 && this.$.autoCpl.visible) {
|
|
if (this.allowDuplicates || !this._isAlreadySelected(this.$.autoCpl.filteredItems[0].label)) {
|
|
this._itemSelected(this.$.autoCpl.filteredItems[0]);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
_keyboardEventMatchesKeys(e, keys) {
|
|
const keyList = keys.split(' ');
|
|
|
|
for (const key of keyList) {
|
|
if (key.toLowerCase() === e.key.toLowerCase()) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
_valueChanged() {
|
|
this.toggleClass('has-items', Array.isArray(this.value) && this.value.length > 0);
|
|
}
|
|
|
|
_removeItem(e) {
|
|
this.splice('value', e.model.index, 1)[0];
|
|
}
|
|
|
|
_isAlreadySelected(label) {
|
|
if (!Array.isArray(this.value)) return false;
|
|
|
|
for (let i = 0, li = this.value.length; i < li; ++i) {
|
|
const item = this.value[i];
|
|
if (item.label.toLocaleLowerCase() === label.toLocaleLowerCase()) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
_getItemByLabel(label) {
|
|
for (var i = 0, li = this.items.length; i < li; ++i) {
|
|
var item = this.items[i];
|
|
if (item.label === label) {
|
|
return item;
|
|
}
|
|
}
|
|
}
|
|
|
|
_getFreeValue() {
|
|
var newValue = 0;
|
|
var _this = this;
|
|
|
|
while(!isFree(newValue)) {
|
|
newValue++;
|
|
}
|
|
|
|
function isFree() {
|
|
for (var i = 0, li = _this.items.length; i < li; ++i) {
|
|
if (_this.items[i].value == newValue) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return newValue;
|
|
}
|
|
|
|
_onFocus() {
|
|
this.$.input.focus();
|
|
}
|
|
|
|
_clearInput() {
|
|
this._input = '';
|
|
this.$.input.value = '';
|
|
}
|
|
}
|
|
|
|
window.customElements.define('tp-tag-input', TpTagInput);
|