Files
ic10emu/www/src/ts/virtualMachine/device/deviceList.ts
2024-08-24 16:38:07 -07:00

183 lines
5.2 KiB
TypeScript

import { html, css, HTMLTemplateResult, PropertyValueMap } 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 { isSome, structuralEqual } from "utils";
import { repeat } from "lit/directives/repeat.js";
import { default as uFuzzy } from "@leeoniya/ufuzzy";
import { VMSlotAddDialog } from "./slotAddDialog";
import "./addDevice"
import { SlotModifyEvent } from "./slot";
import { computed, ReadonlySignal, Signal, signal, SignalWatcher, watch } from "@lit-labs/preact-signals";
import { ObjectID } from "ic10emu_wasm";
import { VMObjectMixin } from "virtualMachine/baseDevice";
@customElement("vm-device-list")
export class VMDeviceList extends VMObjectMixin(BaseElement) {
static styles = [
...defaultCss,
css`
.header {
margin-bottom: 1rem;
padding: 0.25rem 0.25rem;
align-items: center;
display: flex;
flex-direction: row;
width: 100%;
box-sizing: border-box;
}
.device-list {
display: flex;
flex-direction: column;
box-sizing: border-box;
}
.device-list-card {
width: 100%;
}
.device-filter-input {
margin-left: auto;
}
`,
];
constructor() {
super();
}
devices: ReadonlySignal<ObjectID[]> = (() => {
let last: ObjectID[] = null;
return computed(() => {
const vm = this.vm.value;
const next: ObjectID[] = vm?.state.vm.value?.objects.flatMap((obj): ObjectID[] => {
if (!isSome(obj.obj_info.parent_slot) && !isSome(obj.obj_info.root_parent_human)) {
return [obj.obj_info.id]
}
return [];
})
if (structuralEqual(last, next)) {
return last;
}
last = next;
return next;
});
})();
private _filter: Signal<string> = signal("");
private _filteredDeviceIds: ReadonlySignal<ObjectID[]> = (() => {
let last: ObjectID[] = null;
return computed(() => {
const vm = this.vm.value;
let next = this.devices.value;
if (this._filter.value) {
const datapoints: [string, number][] = [];
for (const device_id of this.devices.value) {
const name = vm?.state.getObjectName(device_id).value;
const prefab = vm?.state.getObjectPrefabName(device_id).value;
if (name != null) {
datapoints.push([name, device_id]);
}
if (prefab != null) {
datapoints.push([prefab, device_id]);
}
}
const haystack: string[] = datapoints.map((data) => data[0]);
const uf = new uFuzzy({});
const [_idxs, info, order] = uf.search(haystack, this._filter.value, 0, 1e3);
const filtered = order?.map((infoIdx) => datapoints[info.idx[infoIdx]]);
const deviceIds: number[] =
filtered
?.map((data) => data[1])
?.filter((val, index, arr) => arr.indexOf(val) === index) ?? [];
next = deviceIds;
}
if (structuralEqual(last, next)) {
return last;
}
last = next;
return next;
});
})();
protected firstUpdated(_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {
this.renderRoot.querySelector(".device-list").addEventListener(
"device-modify-slot",
this._showDeviceSlotDialog.bind(this),
);
}
protected render(): HTMLTemplateResult {
const deviceCards = computed(() => repeat(
this.filteredDeviceIds.value,
(id) => id,
(id) =>
html`<vm-device-card .objectID=${id} class="device-list-card">
</vm-device-card>`,
));
const numDevices = computed(() => this.devices.value.length);
const result = html`
<div class="header">
<span>
Devices:
<sl-badge variant="neutral" pill>${watch(numDevices)}</sl-badge>
</span>
<sl-input
class="device-filter-input"
placeholder="Filter Devices"
clearable
@sl-input=${this._handleFilterInput}
>
<sl-icon slot="suffix" name="search"></sl-icon>"
</sl-input>
<vm-add-device-button class="ms-auto"></vm-add-device-button>
</div>
<div class="device-list">${watch(deviceCards)}</div>
<vm-slot-add-dialog></vm-slot-add-dialog>
`;
return result;
}
@query("vm-slot-add-dialog") slotDialog: VMSlotAddDialog;
_showDeviceSlotDialog(
e: CustomEvent<SlotModifyEvent>,
) {
this.slotDialog.show(e.detail.objectID, e.detail.slotIndex);
}
get filteredDeviceIds() {
if (typeof this._filteredDeviceIds !== "undefined") {
return this._filteredDeviceIds;
} else {
return this.devices;
}
}
@query(".device-filter-input") filterInput: SlInput;
get filter() {
return this._filter.value;
}
@state()
set filter(val: string) {
this._filter.value = val;
}
private filterTimeout: number | undefined;
_handleFilterInput(_e: CustomEvent) {
if (this.filterTimeout) {
clearTimeout(this.filterTimeout);
}
const that = this;
this.filterTimeout = setTimeout(() => {
that.filter = that.filterInput.value;
that.filterTimeout = undefined;
}, 500);
}
}