diff --git a/chip-list.js b/chip-list.js index d2df528..11e599b 100644 --- a/chip-list.js +++ b/chip-list.js @@ -1,6 +1,6 @@ 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 { IgniteTemplate, list, div, input, button, h4, span, i, converter, text } from "../ignite-html/ignite-template.js"; import { Chip } from "./chip.js"; import { Popper } from "./popper.js"; import { LinearProgress } from "./linear-progress.js"; @@ -47,123 +47,119 @@ class ChipList extends IgniteElement { 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, + searchResultConverter: (result) => new text(result), onSearch: null, onSearchDelay: 200, onSearchCallback: null, - blurTimeout: null, documentListener: null, freeForm: true, chipBackground: null, chipColor: null, changed: false, - border: false, readOnly: false, + showClearButton: true }; } render() { return this.template - .class("form-control form-control-lg position-relative d-flex flex-row flex-wrap gap-2 align-items-center") + .class("form-control form-control-lg position-relative d-flex flex-row gap-2 align-items-center justify-content-between") .class(this.editing, value => value ? "editing" : null) .attribute("tabindex", "0") .onFocus(e => this.onFocus()) .child( - //Placeholder - new span().class("text-muted").child(this.placeholder).hide([this.editing, this.items], (editing, items) => editing || (items != null && items.length > 0)), + new div().class("d-flex flex-row flex-wrap gap-2 align-items-center").child( + //Placeholder + new span().class("text-muted").child(this.placeholder).hide([this.editing, this.items], (editing, items) => editing || (items != null && items.length > 0)), - //Chips - 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); - }), - - //Text input - 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. - } + //Chips + 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. }) - .onBackspace((e) => { - //If we are read only don't do anything. - if (this.readOnly) { - return; - } + .child(item.content); + }), - //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] == " ")) { + //Text input + new div() + .class("input-container") + .show(this.editing) + .child( + new div() + .class("input") + .attribute("contenteditable", "true") + .ref(this.input) + .onEnter((e) => { e.preventDefault(); - if (this.items) { - this.items.pop(); + //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.input.focus(); //Refocus to ensure we still have it. + 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; + } - this.searching = false; - } - }) - .on("keydown", (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 the escape key is pressed stop searching until something else happens. - if (e.key == "Escape") { - this.searching = false; - e.preventDefault(); - e.stopPropagation(); - return; - } + if (this.items) { + this.items.pop(); + this.changed = true; //Make sure changed flag was set. + } - //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; + this.searching = false; + } + }) + .on("keydown", (e) => { + //If we are read only don't do anything. + if (this.readOnly) { + return; + } - if (this.stopEditingOnBlur) { + //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; this.editing = false; //Fire a change event if there was a change. @@ -171,48 +167,71 @@ class ChipList extends IgniteElement { this.changed = false; this.dispatchEvent(new Event("change")); } + + this.input.innerHTML = ""; + return; } - 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); + //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(); } - this.onSearchCallback = setTimeout(() => this.onSearch(this, this.input.textContent.trim()), this.onSearchDelay); - } - }) + + //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); + } + }) + ) ), + //Clear button + new button() + .class("btn btn-link text-muted fs-4") + .title("Clear") + .show([this.showClearButton, this.items], (show, items) => show && items && items.length > 0) + .child(new i().class('fa fa-times')) + .onClick(() => { + this.items = null; + this.editing = false; + this.searching = false; + this.input.blur(); + this.input.innerHTML = ""; + }), + //Search results popper 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") + .class("d-flex flex-column justify-content-center p-2 shadow bg-white border rounded-3") .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 + //Progress bar + new LinearProgress() + .class("my-2") + .property("loading", this.searchLoading) + .property("color", "var(--bs-primary)"), + + //Showing results + new span().class("text-muted").hide(this.searchLoading).innerText(this.searchResults, results => results && results.length > 0 ? `Showing ${results.length} ${results.length == 1 ? "result" : "results"}` : "No results"), + + //Results + new div().class("d-flex flex-column gap-2").hide([this.showSearchResults, this.searchLoading], (showSearchResults, searchLoading) => !showSearchResults || searchLoading).child( + new list( + this.searchResults, + result => new div() + .class("search-result") + .onClick(() => this.searchResultClick(result)) + .child(new converter(result, this.searchResultConverter)) + ) + ) ) ), ) @@ -245,16 +264,15 @@ class ChipList extends IgniteElement { //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.editing = false; this.searching = false; + + //Fire a change event if there was a change. + if (this.changed) { + this.changed = false; + this.dispatchEvent(new Event("change")); + } + this.input.blur(); this.input.innerHTML = ""; }