compote-ui
Version:
An opinionated UI component library for Svelte, built on top of [Ark UI](https://ark-ui.com) with additional components and features not available in the core Ark UI library.
89 lines (88 loc) • 3.84 kB
JavaScript
import { DebouncedFilterEdits } from '../../utils/debounced-filter-edit.svelte';
import { mergeRangeFilter, rangeBounds } from '../../utils/range-filter';
import { getFilterRevisions } from './filter-revisions.svelte';
/**
* The debounced edits pending on one table's filters. Shared by every component that edits
* them (the toolbar filter, each header filter), so the latest edit to a column wins no
* matter where it was made, and cancelling reaches edits made from any of them.
*/
class PendingFilterEdits {
text = new DebouncedFilterEdits();
range = new DebouncedFilterEdits();
}
const registry = new WeakMap();
function pendingEdits(table) {
let edits = registry.get(table);
if (!edits) {
edits = new PendingFilterEdits();
registry.set(table, edits);
}
return edits;
}
/** Drops every pending filter edit on `table` without committing, whichever host made it. */
export function cancelPendingFilterEdits(table) {
const edits = registry.get(table);
edits?.text.cancelAll();
edits?.range.cancelAll();
}
/**
* One host's access to a table's pending filter edits (see PendingFilterEdits). The edits
* live with the table, not with the host or its editors, so an edit still commits after its
* editor unmounts with a closing popover.
*
* A control shows its pending edit if there is one, otherwise the column's live filter
* value — so a filter changed from outside (the app calling setFilterValue /
* setColumnFilters / resetColumnFilters) never leaves a stale value in the inputs, an edit
* pending during that change is dropped rather than re-applied, and committing one range
* bound never restores the other's old value.
*/
export class ColumnFilterEditing {
#table;
#isActive;
constructor(options) {
this.#table = options.table;
this.#isActive = options.isActive ?? (() => true);
}
get #edits() {
return pendingEdits(this.#table());
}
/**
* What an edit is based on: the column's filter revision, which only ever goes up, so a
* filter cleared and set back to its old value still makes the edit stale. Tables not
* made by compote's createTable have no revisions; fall back to the filter value.
*/
#base(column) {
const revisions = getFilterRevisions(this.#table());
return revisions ? revisions.get(column.id) : column.getFilterValue();
}
textValue(column) {
const value = column.getFilterValue();
return (this.#edits.text.get(column.id, this.#base(column)) ??
(typeof value === 'string' ? value : ''));
}
editText(column, value) {
if (!this.#isActive(column.id))
return;
this.#edits.text.edit(column.id, this.#base(column), () => value, () => this.#base(column), (text) => column.setFilterValue(text || undefined));
}
rangeBound(column, which) {
const pending = this.#edits.range.get(column.id, this.#base(column));
if (pending && which in pending)
return pending[which] ?? null;
return rangeBounds(column.getFilterValue())[which === 'min' ? 0 : 1] ?? null;
}
editRange(column, which, value) {
if (!this.#isActive(column.id))
return;
this.#edits.range.edit(column.id, this.#base(column), (pending) => ({ ...pending, [which]: value }), () => this.#base(column), (pending) => column.setFilterValue(mergeRangeFilter(column.getFilterValue(), pending)));
}
/** Drops the pending edits for one column without committing them. */
cancel(columnId) {
this.#edits.text.cancel(columnId);
this.#edits.range.cancel(columnId);
}
/** Drops every pending edit on the table without committing — including other hosts'. */
cancelAll() {
cancelPendingFilterEdits(this.#table());
}
}