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">