component()

in src/components/Selection/MultiselectFilter.ts [198:339]


    component(filtersByLabel: Map<string, MultiselectFilterSpec>) {
        const props = this.props;
        const field = () => props.field;
        const createValue = (value: FieldValue, label: string) => props.createValue ? props.createValue(value, label) : { value, label };
        const prettyName = () => props.prettyName ?? field();

        if (props.requires) {
            // Resolve requires strings to dependencies and FieldValues.
            type Require = { dep: MultiselectFilterSpec, value: FieldValue, pings: Set<Ping> };
            let resolvedRequires: Require[] | undefined = [];
            for (const [label, value] of Object.entries(props.requires)) {
                const dep = filtersByLabel.get(label);
                if (!dep) {
                    resolvedRequires = undefined;
                    break;
                }
                const si = dep.pingValues!.strings.indexOf(value);
                if (si === -1) {
                    resolvedRequires = undefined;
                    break;
                }
                const p = new Set<Ping>();
                let i = -1;
                while ((i = dep.pingValues!.values.indexOf(si, i + 1)) != -1) {
                    p.add(i);
                }
                resolvedRequires.push({ dep, value: si, pings: p });
            }

            if (resolvedRequires === undefined) {
                this.#disabled = () => true;
            } else {
                this.#disabled = () => resolvedRequires.some(req => {
                    return !req.dep.selectedValues().has(req.value);
                });
                this.#limitTo = resolvedRequires.map(req => req.pings)
                    .reduce((a, b) => a.intersection(b));
            }
        }

        let valueSubset: Set<number>;
        if (this.#limitTo) {
            valueSubset = new Set();
            for (const ping of this.#limitTo) {
                valueSubset.add(this.pingValues!.values[ping]);
            }
        } else {
            valueSubset = new Set(this.pingValues!.strings.keys());
        }
        const values = valueSubset.keys().map(i => {
            const s = this.pingValues!.strings[i];
            if (s === null) {
                console.assert(getTypeDescriptor(this.props.field).nullable, "expected nullable field", this.props.field);
                return new MultiselectFilterOption({ value: i, label: "(none)" });
            }
            return new MultiselectFilterOption(createValue(i, s))
        }).toArray();
        mildlySmartSort(values, v => this.pingValues!.strings[v.value]);
        this.#fieldValueOptions = new Map(values.map(v => [v.value, v]));

        let groupToggle;
        let groupedOptions: Map<string, MultiselectFilterOption[]> | undefined;
        {
            const hasGroups = values.some(v => v.hasGroup);
            if (hasGroups) {
                const [grouped, setGrouped] = createSignal(false);
                this.#grouped = grouped;
                this.#setGrouped = setGrouped;
                setGrouped(true);
                groupedOptions = new Map();
                for (const v of values) {
                    if (!groupedOptions.get(v.group)) {
                        groupedOptions.set(v.group, []);
                    }
                    groupedOptions.get(v.group)!.push(v);
                }

                const toggleGrouped = (_: Event) => {
                    if (this.#disabled()) return;
                    setGrouped(v => !v);
                };

                groupToggle = html`<span
                    onClick=${toggleGrouped}
                    title="Toggle groups"
                    class="group-toggle icon fas fa-plus"
                    classList=${() => { return { "fa-minus": !grouped(), "fa-plus": grouped() } }}
                > </span>`;
            }
        }

        const options = () => {
            let opts = this.#grouped() ? groupedOptions!.keys().map(k => { return { label: k, value: k } }).toArray() : values;
            return opts.map(v => html`<option value=${v.value}>${v.label}</option>`);
        };

        let selectEl: HTMLSelectElement;

        const selectAll = () => {
            if (this.#disabled()) return;
            for (const o of selectEl.options) o.selected = true;
            setTimeout(() => selectEl.dispatchEvent(new Event('change')), 0);
        };

        // Update options when `selectedValues` changes (this will only have a
        // meaningful effect when settings are loaded).
        createEffect(() => {
            const values = this.selectedValues();
            let optionValues: Set<string> | undefined;
            if (this.#grouped()) {
                optionValues = new Set();
                for (const [k, v] of groupedOptions!) {
                    if (v.every(opt => values.has(opt.value))) {
                        optionValues.add(k);
                    }
                }
            } else {
                optionValues = new Set(values.keys().map(n => n.toString()));
            }
            for (const o of selectEl.options) {
                o.selected = optionValues.size === 0 || optionValues.has(o.value);
            }
        });

        const changed = (_: Event) => {
            let values = Array.from(selectEl.selectedOptions);
            let result: Set<FieldValue>;

            // We rely on `selectedOptions` changing when `grouped()` changes.
            if (untrack(() => this.#grouped())) {
                result = new Set(values.flatMap(o => groupedOptions!.get(o.value)!.map(v => v.value)));
            } else {
                result = new Set(values.map(o => parseInt(o.value)));
            }
            this.#setSelectedValues(result);
        };
        onMount(() => selectEl.dispatchEvent(new Event('change')));

        const fieldid = () => `filter-${prettyName().replaceAll(" ", "-")}`;

        return html`<div class="filter">
            <label onClick=${(_: Event) => selectAll()} for=${fieldid} title="Click to select all" style="cursor:pointer">