Cleaned up chip list further and simplified it.

This commit is contained in:
2025-09-24 17:24:22 -07:00
parent 449a5eb896
commit 061bd5a3b6

View File

@@ -1,6 +1,6 @@
import { IgniteHtml } from '../ignite-html/ignite-html.js'; import { IgniteHtml } from '../ignite-html/ignite-html.js';
import { IgniteElement } from "../ignite-html/ignite-element.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 { Chip } from "./chip.js";
import { Popper } from "./popper.js"; import { Popper } from "./popper.js";
import { LinearProgress } from "./linear-progress.js"; import { LinearProgress } from "./linear-progress.js";
@@ -47,123 +47,119 @@ class ChipList extends IgniteElement {
items: [], items: [],
itemsMax: Number.MAX_SAFE_INTEGER, itemsMax: Number.MAX_SAFE_INTEGER,
placeholder: null, placeholder: null,
stopEditingOnBlur: true,
editing: false, editing: false,
input: null, input: null,
searchBox: null,
search: true, search: true,
searching: false, searching: false,
showSearchResults: true, showSearchResults: true,
searchLoading: false, searchLoading: false,
searchResults: null, searchResults: null,
searchPlaceholder: "No results found.", searchResultConverter: (result) => new text(result),
searchFooter: null,
onSearch: null, onSearch: null,
onSearchDelay: 200, onSearchDelay: 200,
onSearchCallback: null, onSearchCallback: null,
blurTimeout: null,
documentListener: null, documentListener: null,
freeForm: true, freeForm: true,
chipBackground: null, chipBackground: null,
chipColor: null, chipColor: null,
changed: false, changed: false,
border: false,
readOnly: false, readOnly: false,
showClearButton: true
}; };
} }
render() { render() {
return this.template 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) .class(this.editing, value => value ? "editing" : null)
.attribute("tabindex", "0") .attribute("tabindex", "0")
.onFocus(e => this.onFocus()) .onFocus(e => this.onFocus())
.child( .child(
//Placeholder new div().class("d-flex flex-row flex-wrap gap-2 align-items-center").child(
new span().class("text-muted").child(this.placeholder).hide([this.editing, this.items], (editing, items) => editing || (items != null && items.length > 0)), //Placeholder
new span().class("text-muted").child(this.placeholder).hide([this.editing, this.items], (editing, items) => editing || (items != null && items.length > 0)),
//Chips //Chips
new list(this.items, (item) => { new list(this.items, (item) => {
return new Chip() return new Chip()
.id(item.id) .id(item.id)
.property("color", item.chipColor ? item.chipColor : this.chipColor) .property("color", item.chipColor ? item.chipColor : this.chipColor)
.property("background", item.chipBackground ? item.chipBackground : this.chipBackground) .property("background", item.chipBackground ? item.chipBackground : this.chipBackground)
.property("readOnly", this.readOnly) .property("readOnly", this.readOnly)
.property("onDelete", () => { .property("onDelete", () => {
this.items = this.items.filter(needle => needle != item); this.items = this.items.filter(needle => needle != item);
this.changed = true; //Make sure changed flag was set. 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.
}
}) })
.onBackspace((e) => { .child(item.content);
//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. //Text input
if (this.input.textContent.length == 0 || (this.input.textContent.length == 1 && this.input.textContent[0] == " ")) { new div()
.class("input-container")
.show(this.editing)
.child(
new div()
.class("input")
.attribute("contenteditable", "true")
.ref(this.input)
.onEnter((e) => {
e.preventDefault(); e.preventDefault();
if (this.items) { //If we are read only don't do anything.
this.items.pop(); 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. 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; //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();
.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 (this.items) {
if (e.key == "Escape") { this.items.pop();
this.searching = false; this.changed = true; //Make sure changed flag was set.
e.preventDefault(); }
e.stopPropagation();
return;
}
//Reset the searching and input if we get a tab, since the browser this.searching = false;
//will focus the next avaiable element. }
if (e.key == "Tab") { })
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; this.editing = false;
//Fire a change event if there was a change. //Fire a change event if there was a change.
@@ -171,48 +167,71 @@ class ChipList extends IgniteElement {
this.changed = false; this.changed = false;
this.dispatchEvent(new Event("change")); this.dispatchEvent(new Event("change"));
} }
this.input.innerHTML = "";
return;
} }
this.input.innerHTML = ""; //If we are not searching and a key was pressed, open the search box.
return; 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;
//If we are not searching and a key was pressed, open the search box. } else if (this.items != null && this.items.length >= this.itemsMax && e.key !== "Backspace") {
if (!this.searching && this.search && (e.key !== "Backspace" || (e.key == "Backspace" && e.target.textContent.length > 1)) && (this.items == null || this.items.length < this.itemsMax)) { //Don't allow input if we reached the max number of items.
this.searching = true; e.preventDefault();
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);
} //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 //Search results popper
new Popper() new Popper()
.property("show", this.searching) .property("show", this.searching)
.child( .child(
new div() new div()
.class("d-flex flex-column justify-content-center p-2 shadow bg-white") .class("d-flex flex-column justify-content-center p-2 shadow bg-white border rounded-3")
.style("border-radius", "0.4em")
.child( .child(
new LinearProgress().class("my-2").property("loading", this.searchLoading), //Progress bar
new span(this.searchPlaceholder).class("mt-2").hide([this.searchResults, this.showSearchResults, this.searchLoading], (searchResults, showSearchResults, searchLoading) => { new LinearProgress()
//Dont show the placeholder if we have search results, or if we are not showing search results. .class("my-2")
return (searchResults != null && searchResults.length > 0 && !searchLoading) || (!showSearchResults || searchLoading); .property("loading", this.searchLoading)
}), .property("color", "var(--bs-primary)"),
new list(this.searchResults, item => {
return new div(item.content).class("search-result p-2").onClick(() => this.searchResultClick(item)); //Showing results
}).hide([this.showSearchResults, this.searchLoading], (showSearchResults, searchLoading) => !showSearchResults || searchLoading), 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"),
this.searchFooter
//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. //Only blur if we are editing and the target is not ourself or any of our children.
if (this.editing) { if (this.editing) {
if (e.target != this && !this.contains(e.target)) { if (e.target != this && !this.contains(e.target)) {
if (this.stopEditingOnBlur) { this.editing = false;
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.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.blur();
this.input.innerHTML = ""; this.input.innerHTML = "";
} }