259 lines
9.2 KiB
JavaScript
259 lines
9.2 KiB
JavaScript
import { IgniteHtml } from '../ignite-html/ignite-html.js';
|
|
import { IgniteElement } from "../ignite-html/ignite-element.js";
|
|
import { IgniteTemplate, button, ul, slot, span, div, i, html, list, converter } from "../ignite-html/ignite-template.js";
|
|
import { IgniteProperty } from "../ignite-html/ignite-html.js";
|
|
import { Popper } from "./popper.js";
|
|
|
|
class SearchSelect extends IgniteElement {
|
|
constructor() {
|
|
super();
|
|
}
|
|
|
|
get properties() {
|
|
return {
|
|
inputElement: null,
|
|
placeholder: null,
|
|
options: null,
|
|
optionConverter: null,
|
|
results: null,
|
|
value: null,
|
|
valueConverter: null,
|
|
searchSelector: null,
|
|
searchCallback: null,
|
|
searchDelay: 200,
|
|
searching: false,
|
|
documentListener: null,
|
|
maxResults: -1,
|
|
resultsMaxHeight: "15em",
|
|
onEnter: null
|
|
};
|
|
}
|
|
|
|
render() {
|
|
return this.template
|
|
.class("w-100 position-relative form-control form-control-lg d-flex flex-row justify-content-between align-items-center")
|
|
.attribute("tabindex", "0")
|
|
.onFocus(() => this.onFocus())
|
|
.child(
|
|
//Search icon
|
|
new button()
|
|
.class("btn btn-none ps-0")
|
|
.child(new i().class("fa-solid fa-magnifying-glass")),
|
|
|
|
//Placeholder
|
|
new div()
|
|
.hide([this.value, this.searching], (value, searching) => value || searching)
|
|
.class("cursor-pointer")
|
|
.style("flex", 1)
|
|
.style("opacity", "0.5")
|
|
.style("overflow", "auto")
|
|
.innerText(this.placeholder),
|
|
|
|
//Search input
|
|
new div()
|
|
.show(this.searching)
|
|
.style("flex", 1)
|
|
.style("border", "none")
|
|
.style("outline", "none")
|
|
.style("overflow", "auto")
|
|
.attribute("contenteditable", true)
|
|
.ref(this.inputElement)
|
|
.onFocus(() => this.onFocus())
|
|
.on("keydown", (e) => {
|
|
//If the enter key is pressed, prevent it from doing anything.
|
|
if (e.key == "Enter") {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
if (this.onEnter) {
|
|
this.onEnter(e.target.textContent);
|
|
}
|
|
|
|
this.searching = false;
|
|
this.inputElement.blur();
|
|
this.inputElement.textContent = null;
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
return;
|
|
}
|
|
|
|
//If the escape key is pressed stop searching until something else happens.
|
|
if (e.key == "Escape") {
|
|
this.searching = false;
|
|
this.inputElement.blur();
|
|
this.inputElement.textContent = null;
|
|
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.inputElement.blur();
|
|
this.inputElement.textContent = null;
|
|
return;
|
|
}
|
|
|
|
//If we are not searching and a key was pressed, open the search box.
|
|
if (!this.searching && (e.key !== "Backspace" || (e.key == "Backspace" && e.target.textContent.length > 1)) && this.options && this.options.length > 0) {
|
|
this.searching = true;
|
|
}
|
|
|
|
//If we are searching and we have a on search function invoke it.
|
|
if (this.searching) {
|
|
if (this.searchCallback) {
|
|
clearTimeout(this.searchCallback);
|
|
}
|
|
|
|
this.searchCallback = setTimeout(() => this.search(e.target.textContent.trim()), this.searchDelay);
|
|
}
|
|
}),
|
|
|
|
//Value
|
|
new div().show(this.value).child(
|
|
new converter(this.value, this.valueConverter)
|
|
),
|
|
|
|
//Clear value button
|
|
new button().show(this.value)
|
|
.class("btn btn-none")
|
|
.child(new i().class("fa-solid fa-times"))
|
|
.onClick(() => {
|
|
this.value = null;
|
|
|
|
this.dispatchEvent(new Event("change"));
|
|
|
|
this.onFocus();
|
|
}),
|
|
|
|
//Search results popper
|
|
new Popper()
|
|
.class("form-control form-control-lg shadow d-flex flex-column gap-3 overflow-auto p-3")
|
|
.style("max-height", this.resultsMaxHeight)
|
|
.property("show", this.searching)
|
|
.child(
|
|
new span().class("text-muted").innerText(this.results, results => results && results.length > 0 ? `Showing ${results.length} ${results.length == 1 ? "result" : "results"}` : "No results"),
|
|
|
|
new list(this.results, option =>
|
|
new div()
|
|
.class("cursor-pointer")
|
|
.child(
|
|
new converter(option, this.optionConverter)
|
|
)
|
|
.onClick(e => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
this.value = option;
|
|
|
|
this.searching = false;
|
|
|
|
//Dispatch native change event so this acts like a native control
|
|
this.dispatchEvent(new Event("change"));
|
|
})
|
|
)
|
|
)
|
|
)
|
|
.on("keydown", e => {
|
|
if (!this.searching && e.key == "Backspace" && this.value) {
|
|
this.value = null;
|
|
|
|
this.dispatchEvent(new Event("change"));
|
|
|
|
this.onFocus();
|
|
}
|
|
});
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
onBlur(e) {
|
|
//Only blur if we are editing and the target is not ourself or any of our children.
|
|
if (this.searching) {
|
|
if (e.target != this && !this.contains(e.target)) {
|
|
this.searching = false;
|
|
|
|
this.inputElement.blur();
|
|
|
|
this.inputElement.textContent = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
onFocus() {
|
|
if (!this.searching && !this.value) {
|
|
this.searching = true;
|
|
|
|
this.inputElement.focus();
|
|
|
|
this.inputElement.textContent = null;
|
|
|
|
this.search(this.inputElement.textContent.trim());
|
|
}
|
|
}
|
|
|
|
search(text) {
|
|
var results = [];
|
|
|
|
if (this.options) {
|
|
results = results.concat(this.options);
|
|
}
|
|
|
|
if (text && text != "" && text != " ") {
|
|
var regex = new RegExp(text, 'i');
|
|
|
|
results = results.filter(result => {
|
|
if (this.searchSelector) {
|
|
var values = this.searchSelector(result);
|
|
|
|
if (values) {
|
|
if (Array.isArray(values)) {
|
|
for (var i = 0; i < values.length; i++) {
|
|
if (values[i] && values[i].toString().match(regex)) {
|
|
return true;
|
|
}
|
|
}
|
|
} else if (values.toString().match(regex)) {
|
|
return true;
|
|
}
|
|
}
|
|
} else if (result && result.toString().match(regex)) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
});
|
|
}
|
|
|
|
//Limit the results if needed
|
|
if (this.maxResults != -1) {
|
|
this.results = results.slice(0, this.maxResults);
|
|
} else {
|
|
this.results = results;
|
|
}
|
|
}
|
|
}
|
|
|
|
class Template extends IgniteTemplate {
|
|
constructor(...children) {
|
|
super("mt-search-select", children);
|
|
}
|
|
}
|
|
|
|
IgniteHtml.register("mt-search-select", SearchSelect);
|
|
|
|
export {
|
|
Template as SearchSelect
|
|
} |