import { html, css } from "lit"; import { customElement, query, state } from "lit/decorators.js"; import { BaseElement, defaultCss } from "components"; import SlInput from "@shoelace-style/shoelace/dist/components/input/input.js"; import SlDrawer from "@shoelace-style/shoelace/dist/components/drawer/drawer.js"; import { repeat } from "lit/directives/repeat.js"; import { cache } from "lit/directives/cache.js"; import { default as uFuzzy } from "@leeoniya/ufuzzy"; import { when } from "lit/directives/when.js"; import { unsafeHTML } from "lit/directives/unsafe-html.js"; import { VMTemplateDBMixin } from "virtual_machine/base_device"; import { LogicInfo, ObjectTemplate, StructureInfo } from "ic10emu_wasm"; type LogicableStrucutureTemplate = Extract< ObjectTemplate, { structure: StructureInfo; logic: LogicInfo } >; @customElement("vm-add-device-button") export class VMAddDeviceButton extends VMTemplateDBMixin(BaseElement) { static styles = [ ...defaultCss, css` .add-device-drawer { --size: 36rem; --footer-spacing: var(--sl-spacing-small); } .card { margin-top: var(--sl-spacing-small); margin-right: var(--sl-spacing-small); } `, ]; @query("sl-drawer") drawer: SlDrawer; @query(".device-search-input") searchInput: SlInput; private _structures: Map = new Map(); private _datapoints: [string, string][] = []; private _haystack: string[] = []; postDBSetUpdate(): void { this._structures = new Map( Array.from(this.templateDB.values()).flatMap((template) => { if ("structure" in template && "logic" in template) { return [[template.prefab.prefab_name, template]] as [ string, LogicableStrucutureTemplate, ][]; } else { return [] as [string, LogicableStrucutureTemplate][]; } }), ); const datapoints: [string, string][] = []; for (const entry of this._structures.values()) { datapoints.push( [entry.prefab.name, entry.prefab.prefab_name], [entry.prefab.prefab_name, entry.prefab.prefab_name], [entry.prefab.desc, entry.prefab.prefab_name], ); } const haystack: string[] = datapoints.map((data) => data[0]); this._datapoints = datapoints; this._haystack = haystack; this.performSearch(); } private _filter: string = ""; get filter() { return this._filter; } @state() set filter(val: string) { this._filter = val; this.page = 0; this.performSearch(); } private _searchResults: { entry: LogicableStrucutureTemplate; haystackEntry: string; ranges: number[]; }[] = []; private filterTimeout: number | undefined; performSearch() { if (this._filter) { const uf = new uFuzzy({}); const [_idxs, info, order] = uf.search( this._haystack, this._filter, 0, 1e3, ); const filtered = order?.map((infoIdx) => ({ name: this._datapoints[info.idx[infoIdx]][1], haystackEntry: this._haystack[info.idx[infoIdx]], ranges: info.ranges[infoIdx], })); const unique = [...new Set(filtered.map((obj) => obj.name))].map( (result) => { return filtered.find((obj) => obj.name === result); }, ); this._searchResults = unique.map(({ name, haystackEntry, ranges }) => ({ entry: this._structures.get(name)!, haystackEntry, ranges, })); } else { // return everything this._searchResults = [...this._structures.values()].map((st) => ({ entry: st, haystackEntry: st.prefab.prefab_name, ranges: [], })); } } connectedCallback(): void { super.connectedCallback(); window.VM.get().then((vm) => vm.addEventListener( "vm-device-db-loaded", this._handleDeviceDBLoad.bind(this), ), ); } _handleDeviceDBLoad(e: CustomEvent) { this.templateDB = e.detail; } @state() private page = 0; renderSearchResults() { const perPage = 40; const totalPages = Math.ceil((this._searchResults?.length ?? 0) / perPage); let pageKeys = Array.from({ length: totalPages }, (_, index) => index); const extra: { entry: { prefab: { name: string; prefab_name: string } }; haystackEntry: string; ranges: number[]; }[] = []; if (this.page < totalPages - 1) { extra.push({ entry: { prefab: { name: "", prefab_name: this.filter } }, haystackEntry: "...", ranges: [], }); } return when( typeof this._searchResults !== "undefined" && this._searchResults.length < 20, () => repeat( this._searchResults ?? [], (result) => result.entry.prefab.prefab_name, (result) => cache(html` `), ), () => html`

results, filter more to get cards

Page: ${pageKeys.map( (key, index) => html` ${key + 1}${index < totalPages - 1 ? "," : ""} `, )}
${[ ...this._searchResults.slice( perPage * this.page, perPage * this.page + perPage, ), ...extra, ].map((result) => { let hay = result.haystackEntry.slice(0, 15); if (result.haystackEntry.length > 15) hay += "..."; const ranges = result.ranges.filter((pos) => pos < 20); const key = result.entry.prefab.prefab_name; return html`
${result.entry.prefab.name} ( ${ranges.length ? unsafeHTML(uFuzzy.highlight(hay, ranges)) : hay} )
`; })}
`, ); } _handlePageChange(e: Event) { const span = e.currentTarget as HTMLSpanElement; const key = parseInt(span.getAttribute("key")); this.page = key; } _handleHaystackClick(e: Event) { const div = e.currentTarget as HTMLDivElement; const key = div.getAttribute("key"); if (key === this.filter) { this.page += 1; } else { this.filter = key; this.searchInput.value = key; } } _handleDeviceAdd() { this.drawer.hide(); } render() { return html` Add Device Search Structures
${this.renderSearchResults()}
{ this.drawer.hide(); }} > Close
`; } _handleSearchInput(e: CustomEvent) { if (this.filterTimeout) { clearTimeout(this.filterTimeout); } const that = this; this.filterTimeout = setTimeout(() => { that.filter = that.searchInput.value; that.filterTimeout = undefined; }, 200); } _handleAddButtonClick() { this.drawer.show(); this.searchInput.select(); } }