diff --git a/ui/autocomplete-compnent.ts b/ui/autocomplete-compnent.ts index c32d99b..644c63d 100644 --- a/ui/autocomplete-compnent.ts +++ b/ui/autocomplete-compnent.ts @@ -51,11 +51,8 @@ export default class Autocomplete { el.addEventListener("focus", () => { this.element = el; - const domRect = el.getBoundingClientRect(); - this.container.style.left = `${domRect.left + window.pageXOffset}px`; - this.container.style.width = `${domRect.width}px`; - this.container.style.top = `${domRect.bottom + window.pageYOffset}px`; this.update(); + this.reposition(); }); el.addEventListener("blur", () => { @@ -71,7 +68,8 @@ export default class Autocomplete { } else if (e.key === "Enter") { if (this.default != null) { el.value = this.default; - e.preventDefault(); + e.stopImmediatePropagation(); + el.dispatchEvent(new InputEvent("input")); this.update(); } } else if (e.key === "ArrowDown") { @@ -93,6 +91,19 @@ export default class Autocomplete { }); } + public reposition(): void { + if (!this.element) return; + const domRect = this.element.getBoundingClientRect(); + if (!domRect.width) { + // Element has been removed + if (this.visible) this.hide(); + return; + } + this.container.style.left = `${domRect.left + window.pageXOffset}px`; + this.container.style.width = `${domRect.width}px`; + this.container.style.top = `${domRect.bottom + window.pageYOffset}px`; + } + private hide(): void { this.container.style.opacity = "0"; this.visible = false; @@ -159,6 +170,7 @@ export default class Autocomplete { e.addEventListener("mousedown", (ev) => { ev.preventDefault(); el.value = suggestion.value; + el.dispatchEvent(new InputEvent("input")); if (this.element === el) this.update(); }); this.container.appendChild(e); diff --git a/ui/devices-page.ts b/ui/devices-page.ts index bf3d61a..bb1d2fa 100644 --- a/ui/devices-page.ts +++ b/ui/devices-page.ts @@ -328,10 +328,11 @@ export const component: ClosureComponent = (): Component => { if (window.authorizer.hasAccess("devices", 3)) attrs["actionsCallback"] = renderActions; - const filterAttrs = {}; - filterAttrs["resource"] = "devices"; - filterAttrs["filter"] = vnode.attrs["filter"]; - filterAttrs["onChange"] = onFilterChanged; + const filterAttrs = { + resource: "devices", + filter: vnode.attrs["filter"], + onChange: onFilterChanged, + }; return [ m("h1", "Listing devices"), diff --git a/ui/faults-page.ts b/ui/faults-page.ts index 9cae35f..63cd97c 100644 --- a/ui/faults-page.ts +++ b/ui/faults-page.ts @@ -200,10 +200,11 @@ export const component: ClosureComponent = (): Component => { }; } - const filterAttrs = {}; - filterAttrs["resource"] = "faults"; - filterAttrs["filter"] = vnode.attrs["filter"]; - filterAttrs["onChange"] = onFilterChanged; + const filterAttrs = { + resource: "faults", + filter: vnode.attrs["filter"], + onChange: onFilterChanged, + }; return [ m("h1", "Listing faults"), diff --git a/ui/files-page.ts b/ui/files-page.ts index 37ae82b..0531357 100644 --- a/ui/files-page.ts +++ b/ui/files-page.ts @@ -316,10 +316,11 @@ export const component: ClosureComponent = (): Component => { }; } - const filterAttrs = {}; - filterAttrs["resource"] = "files"; - filterAttrs["filter"] = vnode.attrs["filter"]; - filterAttrs["onChange"] = onFilterChanged; + const filterAttrs = { + resource: "files", + filter: vnode.attrs["filter"], + onChange: onFilterChanged, + }; return [ m("h1", "Listing files"), diff --git a/ui/filter-component.ts b/ui/filter-component.ts index d375bd6..f3b2385 100644 --- a/ui/filter-component.ts +++ b/ui/filter-component.ts @@ -17,7 +17,7 @@ * along with GenieACS. If not, see . */ -import m, { ClosureComponent, Component } from "mithril"; +import m, { ClosureComponent } from "mithril"; import { parse, stringify, map } from "../lib/common/expression-parser"; import memoize from "../lib/common/memoize"; import Autocomplete from "./autocomplete-compnent"; @@ -82,9 +82,9 @@ function stringifyFilter(f: Expression): string { return stringify(f); } -const splitFilter = memoize((filter) => { +function splitFilter(filter: string): string[] { if (!filter) return [""]; - const list = []; + const list: string[] = []; const f = parse(filter); if (Array.isArray(f) && f[0] === "AND") for (const ff of f.slice(1)) list.push(stringifyFilter(ff)); @@ -92,69 +92,82 @@ const splitFilter = memoize((filter) => { list.push(""); return list; -}); +} + +interface Attrs { + resource: string; + filter: string; + onChange: (filter: string) => void; +} + +const component: ClosureComponent = (initialVnode) => { + let filterList = splitFilter(initialVnode.attrs.filter); + let filterInvalid = 0; + let filterTouched = false; + let attrs: Attrs = initialVnode.attrs; + + function onChange(): void { + filterTouched = false; + filterInvalid = 0; + filterList = filterList.filter((f) => f); + const list = filterList.map((f, idx) => { + try { + return parseFilter(attrs.resource, f); + } catch (err) { + filterInvalid |= 1 << idx; + } + return null; + }); + filterList.push(""); + + if (filterInvalid) { + m.redraw(); + return; + } + if (list.length === 0) attrs.onChange(""); + else if (list.length > 1) attrs.onChange(stringify(["AND", ...list])); + else attrs.onChange(stringify(list[0])); + } -const component: ClosureComponent = (): Component => { return { + onupdate: (vnode) => { + getAutocomplete(vnode.attrs.resource).reposition(); + }, view: (vnode) => { - if ( - !vnode.state["filterList"] || - vnode.attrs["filter"] !== vnode.state["filter"] - ) { - vnode.state["filterInvalid"] = 0; - vnode.state["filter"] = vnode.attrs["filter"]; - vnode.state["filterList"] = splitFilter(vnode.attrs["filter"]); + if (attrs.filter !== vnode.attrs.filter) { + filterInvalid = 0; + filterList = splitFilter(vnode.attrs.filter); } - function onChange(): void { - vnode.state["filterInvalid"] = 0; - vnode.state["filterList"] = vnode.state["filterList"].filter((f) => f); - let filter = vnode.state["filterList"].map((f, idx) => { - try { - return parseFilter(vnode.attrs["resource"], f); - } catch (err) { - vnode.state["filterInvalid"] |= 1 << idx; - } - return null; - }); - vnode.state["filterList"].push(""); + attrs = vnode.attrs; - if (!vnode.state["filterInvalid"]) { - delete vnode.state["filter"]; - if (filter.length === 0) { - vnode.attrs["onChange"](""); - } else { - if (filter.length > 1) filter = ["AND"].concat(filter); - else filter = filter[0]; - vnode.attrs["onChange"](stringify(filter)); - } - } - } + return m("div.filter", [ + m("b", "Filter"), + ...filterList.map((fltr, idx) => { + return m("input", { + type: "text", + class: `${(filterInvalid >> idx) & 1 ? "error" : ""}`, + value: fltr, + oninput: (e) => { + e.redraw = false; + filterList[idx] = e.target.value; + filterTouched = true; + }, + oncreate: (vn) => { + const el = vn.dom as HTMLInputElement; + getAutocomplete(vnode.attrs.resource).attach(el); - return m( - "div.filter", - [m("b", "Filter")].concat( - vnode.state["filterList"].map((fltr, idx) => { - return m("input", { - type: "text", - class: `${ - (vnode.state["filterInvalid"] >> idx) & 1 ? "error" : "" - }`, - value: fltr, - onchange: (e) => { - vnode.state["filterList"] = vnode.state["filterList"].slice(); - vnode.state["filterList"][idx] = e.target.value.trim(); - onChange(); - }, - oncreate: (vn) => { - getAutocomplete(vnode.attrs["resource"]).attach( - vn.dom as HTMLInputElement - ); - }, - }); - }) - ) - ); + el.addEventListener("blur", () => { + if (filterTouched) onChange(); + }); + + el.addEventListener("keydown", (e) => { + if (e.key === "Enter" && filterTouched) onChange(); + }); + }, + }); + }), + ]); }, }; }; diff --git a/ui/permissions-page.ts b/ui/permissions-page.ts index a791bdc..25e2f50 100644 --- a/ui/permissions-page.ts +++ b/ui/permissions-page.ts @@ -365,10 +365,11 @@ export const component: ClosureComponent = (): Component => { }; } - const filterAttrs = {}; - filterAttrs["resource"] = "permissions"; - filterAttrs["filter"] = vnode.attrs["filter"]; - filterAttrs["onChange"] = onFilterChanged; + const filterAttrs = { + resource: "permissions", + filter: vnode.attrs["filter"], + onChange: onFilterChanged, + }; return [ m("h1", "Listing permissions"), diff --git a/ui/presets-page.ts b/ui/presets-page.ts index 9f7c07f..f150167 100644 --- a/ui/presets-page.ts +++ b/ui/presets-page.ts @@ -427,10 +427,11 @@ export const component: ClosureComponent = (): Component => { }; } - const filterAttrs = {}; - filterAttrs["resource"] = "presets"; - filterAttrs["filter"] = vnode.attrs["filter"]; - filterAttrs["onChange"] = onFilterChanged; + const filterAttrs = { + resource: "presets", + filter: vnode.attrs["filter"], + onChange: onFilterChanged, + }; return [ m("h1", "Listing presets"), diff --git a/ui/provisions-page.ts b/ui/provisions-page.ts index 687283f..176a3bc 100644 --- a/ui/provisions-page.ts +++ b/ui/provisions-page.ts @@ -346,10 +346,11 @@ export const component: ClosureComponent = (): Component => { }; } - const filterAttrs = {}; - filterAttrs["resource"] = "provisions"; - filterAttrs["filter"] = vnode.attrs["filter"]; - filterAttrs["onChange"] = onFilterChanged; + const filterAttrs = { + resource: "provisions", + filter: vnode.attrs["filter"], + onChange: onFilterChanged, + }; return [ m("h1", "Listing provisions"), diff --git a/ui/users-page.ts b/ui/users-page.ts index 49c99c3..b74de30 100644 --- a/ui/users-page.ts +++ b/ui/users-page.ts @@ -401,10 +401,11 @@ export const component: ClosureComponent = (): Component => { }; } - const filterAttrs = {}; - filterAttrs["resource"] = "users"; - filterAttrs["filter"] = vnode.attrs["filter"]; - filterAttrs["onChange"] = onFilterChanged; + const filterAttrs = { + resource: "users", + filter: vnode.attrs["filter"], + onChange: onFilterChanged, + }; return [ m("h1", "Listing users"), diff --git a/ui/virtual-parameters-page.ts b/ui/virtual-parameters-page.ts index 44532b3..7192a41 100644 --- a/ui/virtual-parameters-page.ts +++ b/ui/virtual-parameters-page.ts @@ -346,10 +346,11 @@ export const component: ClosureComponent = (): Component => { }; } - const filterAttrs = {}; - filterAttrs["resource"] = "virtualParameters"; - filterAttrs["filter"] = vnode.attrs["filter"]; - filterAttrs["onChange"] = onFilterChanged; + const filterAttrs = { + resource: "virtualParameters", + filter: vnode.attrs["filter"], + onChange: onFilterChanged, + }; return [ m("h1", "Listing virtual parameters"),