import { IgniteHtml } from '../ignite-html/ignite-html.js'; import { IgniteElement } from "../ignite-html/ignite-element.js"; import { IgniteTemplate, list, div, input, button, h4, span } from "../ignite-html/ignite-template.js"; import { Chip } from "./chip.js"; import { Popper } from "./popper.js"; import { LinearProgress } from "./linear-progress.js"; class ChipList extends IgniteElement { constructor() { super(); } get styles() { return /*css*/` mt-chip-list { display: flex; flex-direction: row; flex-wrap: wrap; gap: 12px; border: solid 0.13rem #ced4da; border-radius: 0.3rem; padding: 0.4rem; } mt-chip-list.no-border { border-color: transparent; } mt-chip-list:focus { outline: none; } mt-chip-list:hover { cursor: pointer; } mt-chip-list.editing { border: solid 0.13rem rgba(0,0,0,0.1); border-radius: 0.3rem; } mt-chip-list.placeholder { border: solid 0.13rem #ced4da; border-radius: 0.3rem; color: rgba(0,0,0,0.6); } mt-chip-list > .input-container { display: flex; flex-direction: column; justify-content: center; flex: 1; flex-basis: auto; flex-shrink: 1; } mt-chip-list > .input-container > .input { outline: none; } mt-chip-list .search-result:hover { background-color: #e0e0e0; border-radius: 0.3em; } `; } get properties() { return { items: [], itemsMax: Number.MAX_SAFE_INTEGER, placeholder: null, stopEditingOnBlur: true, editing: false, input: null, searchBox: null, search: true, searching: false, showSearchResults: true, searchLoading: false, searchResults: null, searchPlaceholder: "No results found.", searchFooter: null, onSearch: null, onSearchDelay: 200, onSearchCallback: null, blurTimeout: null, documentListener: null, freeForm: true, chipBackground: null, chipColor: null, changed: false, border: false, readOnly: false, }; } render() { return this.template .style("position", "relative") .class(this.border, value => value ? null : "no-border") .class(this.editing, value => value ? "editing" : null) .attribute("tabindex", "0") .onFocus(e => this.onFocus()) .class([this.editing, this.items], (editing, items) => { return !editing && (items == null || items.length == 0) ? "placeholder" : null; }) .child( new span(this.placeholder).hide([this.editing, this.items], (editing, items) => { return editing || (items != null && items.length > 0); }), new list(this.items, (item) => { return new Chip() .id(item.id) .property("color", item.chipColor ? item.chipColor : this.chipColor) .property("background", item.chipBackground ? item.chipBackground : this.chipBackground) .property("readOnly", this.readOnly) .property("onDelete", () => { this.items = this.items.filter(needle => needle != item); this.changed = true; //Make sure changed flag was set. }) .child(item.content); }), new div() .class("input-container") .child( new div() .class("input") .attribute("contenteditable", "true") .hide(this.editing, value => { return !value; }) .ref(this.input) .onEnter((e) => { e.preventDefault(); //If we are read only don't do anything. if (this.readOnly) { return; } //If this chip allows free form input then add a new item. if (this.freeForm && this.input.textContent.trim().length >= 1) { if (this.items == null) { this.items = []; } //Add a new item to the chip list. this.items.push({ content: this.input.textContent.trim() }); this.input.innerHTML = ""; this.searching = false; //Reset searching since we just added a item. this.changed = true; //Make sure changed flag was set. } }) .onBackspace((e) => { //If we are read only don't do anything. if (this.readOnly) { return; } //If the backspace key is pressed and there is no content, try to remove the last item from the list. if (this.input.textContent.length == 0 || (this.input.textContent.length == 1 && this.input.textContent[0] == " ")) { e.preventDefault(); if (this.items) { this.items.pop(); this.changed = true; //Make sure changed flag was set. } this.searching = false; } }) .on("keydown", (e) => { //If we are read only don't do anything. if (this.readOnly) { return; } //If the escape key is pressed stop searching until something else happens. if (e.key == "Escape") { this.searching = false; e.preventDefault(); e.stopPropagation(); return; } //Reset the searching and input if we get a tab, since the browser //will focus the next avaiable element. if (e.key == "Tab") { this.searching = false; if (this.stopEditingOnBlur) { this.editing = false; //Fire a change event if there was a change. if (this.changed) { this.changed = false; this.dispatchEvent(new Event("change")); } } this.input.innerHTML = ""; return; } //If we are not searching and a key was pressed, open the search box. if (!this.searching && this.search && (e.key !== "Backspace" || (e.key == "Backspace" && e.target.textContent.length > 1)) && (this.items == null || this.items.length < this.itemsMax)) { this.searching = true; this.showSearchResults = true; } else if (this.items != null && this.items.length >= this.itemsMax && e.key !== "Backspace") { //Don't allow input if we reached the max number of items. e.preventDefault(); } //If we are searching and we have a on search function invoke it. if (this.searching && this.onSearch) { if (this.onSearchCallback) { clearTimeout(this.onSearchCallback); } this.onSearchCallback = setTimeout(() => this.onSearch(this, this.input.textContent.trim()), this.onSearchDelay); } }) ), new Popper() .property("show", this.searching) .child( new div() .class("d-flex flex-column justify-content-center p-2 shadow bg-white") .style("border-radius", "0.4em") .child( new LinearProgress().class("my-2").property("loading", this.searchLoading), new span(this.searchPlaceholder).class("mt-2").hide([this.searchResults, this.showSearchResults, this.searchLoading], (searchResults, showSearchResults, searchLoading) => { //Dont show the placeholder if we have search results, or if we are not showing search results. return (searchResults != null && searchResults.length > 0 && !searchLoading) || (!showSearchResults || searchLoading); }), new list(this.searchResults, item => { return new div(item.content).class("search-result p-2").onClick(() => this.searchResultClick(item)); }).hide([this.showSearchResults, this.searchLoading], (showSearchResults, searchLoading) => !showSearchResults || searchLoading), this.searchFooter ) ), ) } ready() { //Add a listener to the document click to blur our element. this.documentListener = (e) => this.onBlur(e); window.document.addEventListener("click", this.documentListener); } cleanup() { window.document.removeEventListener("click", this.documentListener); } onFocus() { if (!this.readOnly) { if (!this.editing) { this.editing = true; this.input.focus(); this.input.textContent = " "; } else { this.input.focus(); } } } onBlur(e) { //Only blur if we are editing and the target is not ourself or any of our children. if (this.editing) { if (e.target != this && !this.contains(e.target)) { if (this.stopEditingOnBlur) { this.editing = false; //Fire a change event if there was a change. if (this.changed) { this.changed = false; this.dispatchEvent(new Event("change")); } } this.searching = false; this.input.blur(); this.input.innerHTML = ""; } } } searchResultClick(item) { if (this.items == null) { this.items = []; } this.changed = true; //Make sure changed flag is set. this.items.push(item); this.searching = false; this.input.innerHTML = ""; this.input.focus(); } } class ChipListTemplate extends IgniteTemplate { constructor(...children) { super("mt-chip-list", children); } } IgniteHtml.register("mt-chip-list", ChipList); export { ChipListTemplate as ChipList }