argus-table 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/argus-column-group.js +66 -0
- package/dist/argus-column-group.js.map +1 -0
- package/dist/argus-column.js +4 -25
- package/dist/argus-column.js.map +1 -1
- package/dist/argus-table.js +553 -200
- package/dist/argus-table.js.map +1 -1
- package/dist/attributes.js +21 -0
- package/dist/attributes.js.map +1 -0
- package/dist/cdn/argus-table.js +92 -57
- package/dist/cdn/argus-table.js.map +1 -1
- package/dist/column-registry.js +23 -0
- package/dist/column-registry.js.map +1 -0
- package/dist/controllers/auto-width.js +6 -5
- package/dist/controllers/auto-width.js.map +1 -1
- package/dist/controllers/column-children.js +32 -9
- package/dist/controllers/column-children.js.map +1 -1
- package/dist/controllers/column-window.js +0 -4
- package/dist/controllers/column-window.js.map +1 -1
- package/dist/controllers/plugins.js +25 -6
- package/dist/controllers/plugins.js.map +1 -1
- package/dist/controllers/reorder-drag.js +12 -9
- package/dist/controllers/reorder-drag.js.map +1 -1
- package/dist/controllers/row-measure.js +2 -1
- package/dist/controllers/row-measure.js.map +1 -1
- package/dist/controllers/virtual-controller.js +25 -32
- package/dist/controllers/virtual-controller.js.map +1 -1
- package/dist/core/src/columns/column-groups.js +129 -0
- package/dist/core/src/columns/column-groups.js.map +1 -0
- package/dist/core/src/columns/column-order.js +125 -31
- package/dist/core/src/columns/column-order.js.map +1 -1
- package/dist/core/src/columns/resolve-columns.js +0 -1
- package/dist/core/src/columns/resolve-columns.js.map +1 -1
- package/dist/core/src/columns/spread-width.js +43 -0
- package/dist/core/src/columns/spread-width.js.map +1 -0
- package/dist/core/src/filters/filter.js +6 -8
- package/dist/core/src/filters/filter.js.map +1 -1
- package/dist/core/src/grid/row-layout.js +19 -11
- package/dist/core/src/grid/row-layout.js.map +1 -1
- package/dist/core/src/state/selection.js +2 -1
- package/dist/core/src/state/selection.js.map +1 -1
- package/dist/core/src/table/create-table.js +57 -22
- package/dist/core/src/table/create-table.js.map +1 -1
- package/dist/core/src/table/sorted-row-model.js.map +1 -1
- package/dist/core/src/virtual/row-sizes.js +0 -4
- package/dist/core/src/virtual/row-sizes.js.map +1 -1
- package/dist/custom-elements.json +489 -245
- package/dist/events.js +16 -16
- package/dist/events.js.map +1 -1
- package/dist/index.d.ts +273 -127
- package/dist/index.js +3 -2
- package/dist/render/body.js +2 -2
- package/dist/render/body.js.map +1 -1
- package/dist/render/cells.js +11 -3
- package/dist/render/cells.js.map +1 -1
- package/dist/render/content-directive.js +1 -1
- package/dist/render/content-directive.js.map +1 -1
- package/dist/render/content.js +15 -3
- package/dist/render/content.js.map +1 -1
- package/dist/render/header.js +82 -19
- package/dist/render/header.js.map +1 -1
- package/dist/styles/base.js +1 -1
- package/dist/styles/base.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// src/column-registry.ts
|
|
2
|
+
/**
|
|
3
|
+
* Elements watching their `<argus-column>` and `<argus-column-group>` children,
|
|
4
|
+
* by element: tables, and groups (which pass it on to their own parent). Columns
|
|
5
|
+
* and groups call their parent's entry when they connect, disconnect or change,
|
|
6
|
+
* so the table needs no MutationObserver on its own children (which would fire on
|
|
7
|
+
* every light-DOM cell render while scrolling).
|
|
8
|
+
*/
|
|
9
|
+
var watchers = /* @__PURE__ */ new WeakMap();
|
|
10
|
+
/** Calls `onChange` whenever a column or group child of `host` is added, removed, moved or changed. */
|
|
11
|
+
function watchColumns(host, onChange) {
|
|
12
|
+
watchers.set(host, onChange);
|
|
13
|
+
}
|
|
14
|
+
function unwatchColumns(host) {
|
|
15
|
+
watchers.delete(host);
|
|
16
|
+
}
|
|
17
|
+
/** Tells a column's or group's parent that it changed. */
|
|
18
|
+
function notify(parent) {
|
|
19
|
+
if (parent) watchers.get(parent)?.();
|
|
20
|
+
}
|
|
21
|
+
export { notify, unwatchColumns, watchColumns };
|
|
22
|
+
|
|
23
|
+
//# sourceMappingURL=column-registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"column-registry.js","names":[],"sources":["../src/column-registry.ts"],"sourcesContent":["/**\n * Elements watching their `<argus-column>` and `<argus-column-group>` children,\n * by element: tables, and groups (which pass it on to their own parent). Columns\n * and groups call their parent's entry when they connect, disconnect or change,\n * so the table needs no MutationObserver on its own children (which would fire on\n * every light-DOM cell render while scrolling).\n */\nconst watchers = new WeakMap<Element, () => void>();\n\n/** Calls `onChange` whenever a column or group child of `host` is added, removed, moved or changed. */\nexport function watchColumns(host: Element, onChange: () => void): void {\n watchers.set(host, onChange);\n}\n\nexport function unwatchColumns(host: Element): void {\n watchers.delete(host);\n}\n\n/** Tells a column's or group's parent that it changed. */\nexport function notify(parent: Element | null): void {\n if (parent) watchers.get(parent)?.();\n}\n"],"mappings":";;;;;;;;AAOA,IAAM,2BAAW,IAAI,QAA6B;;AAGlD,SAAgB,aAAa,MAAe,UAA4B;CACtE,SAAS,IAAI,MAAM,QAAQ;AAC7B;AAEA,SAAgB,eAAe,MAAqB;CAClD,SAAS,OAAO,IAAI;AACtB;;AAGA,SAAgB,OAAO,QAA8B;CACnD,IAAI,QAAQ,SAAS,IAAI,MAAM,CAAC,GAAG;AACrC"}
|
|
@@ -53,10 +53,14 @@ var AutoWidths = class {
|
|
|
53
53
|
* With `virtualizeColumns` they render out of view until they are.
|
|
54
54
|
*/
|
|
55
55
|
pending(columns) {
|
|
56
|
+
return this.#due(columns, true);
|
|
57
|
+
}
|
|
58
|
+
/** The auto columns never measured, or measured without loaded rows when there are some now. */
|
|
59
|
+
#due(columns, hasLoadedRows) {
|
|
56
60
|
return columns.filter((c) => {
|
|
57
61
|
if (!c.auto) return false;
|
|
58
62
|
const state = this.#state.get(c.id);
|
|
59
|
-
return state === void 0 || state === "provisional";
|
|
63
|
+
return state === void 0 || state === "provisional" && hasLoadedRows;
|
|
60
64
|
}).map((c) => c.id);
|
|
61
65
|
}
|
|
62
66
|
/**
|
|
@@ -64,10 +68,7 @@ var AutoWidths = class {
|
|
|
64
68
|
* the caller then renders again (the one extra pass of the render cycle).
|
|
65
69
|
*/
|
|
66
70
|
measure(root, columns, hasLoadedRows) {
|
|
67
|
-
const due = columns
|
|
68
|
-
const state = this.#state.get(id);
|
|
69
|
-
return state === void 0 || state === "provisional" && hasLoadedRows;
|
|
70
|
-
});
|
|
71
|
+
const due = this.#due(columns, hasLoadedRows);
|
|
71
72
|
if (due.length === 0) return false;
|
|
72
73
|
const measured = measureColumns(root, due);
|
|
73
74
|
const fontsLoading = typeof document !== "undefined" && document.fonts.status === "loading";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auto-width.js","names":[],"sources":["../../src/controllers/auto-width.ts"],"sourcesContent":["import type { ColumnSizingState } from '@argus-table/core';\n\n/**\n * The width each column's content needs, in px, from the cells rendered now:\n * the header, the pinned rows and the rendered body rows. Rows that haven't\n * loaded are skipped (their skeleton bars say nothing about the content).\n *\n * Every cell of the columns gets `data-measure`, which sizes it to its\n * `max-content` (base styles), so the browser accounts for padding, the sort\n * indicator and whatever the content is. One forced layout, no paint: the\n * attribute is gone before the frame ends. A column with nothing laid out\n * (the table is hidden) is left out.\n */\nexport function measureColumns(root: ParentNode, ids: Iterable<string>): Map<string, number> {\n const wanted = new Set(ids);\n const widths = new Map<string, number>();\n if (wanted.size === 0) return widths;\n const cells = [...root.querySelectorAll<HTMLElement>('[data-col]')].filter(\n (cell) =>\n wanted.has(cell.dataset.col ?? '') && !cell.parentElement?.matches('[part~=\"row-loading\"]'),\n );\n for (const cell of cells) cell.setAttribute('data-measure', '');\n for (const cell of cells) {\n const id = cell.dataset.col ?? '';\n const px = Math.ceil(cell.getBoundingClientRect().width);\n if (px > 0 && px > (widths.get(id) ?? 0)) widths.set(id, px);\n }\n for (const cell of cells) cell.removeAttribute('data-measure');\n return widths;\n}\n\n/**\n * - `provisional`: measured without loaded body rows (the header and pinned rows only).\n * - `final`: measured with loaded rows; kept until the column goes away.\n * - `fonts`: measured while web fonts were loading; measured again once they are ready.\n */\ntype State = 'provisional' | 'final' | 'fonts';\n\n/**\n * Widths of `width: 'auto'` columns. A column measures once its first loaded\n * rows render, and before that fits the header and pinned rows. So each column\n * is measured at most twice (three times if fonts were still loading), and a\n * table that stays empty doesn't measure on every update. State is keyed by\n * column id, so new `columns` arrays with the same ids don't measure again.\n */\nexport class AutoWidths {\n /** Measured widths by column id; a new object whenever one changes. */\n widths: ColumnSizingState = {};\n readonly #state = new Map<string, State>();\n #fontsPending = false;\n readonly #requestUpdate: () => void;\n\n constructor(requestUpdate: () => void) {\n this.#requestUpdate = requestUpdate;\n }\n\n /** Forgets columns that are gone or no longer `'auto'`, given the ids of the auto columns. */\n sync(ids: Iterable<string>): void {\n const auto = new Set(ids);\n for (const id of this.#state.keys()) {\n if (!auto.has(id)) this.#state.delete(id);\n }\n const kept = Object.entries(this.widths).filter(([id]) => auto.has(id));\n if (kept.length !== Object.keys(this.widths).length) this.widths = Object.fromEntries(kept);\n }\n\n /**\n * The auto columns still to be measured: never, or without loaded rows yet.\n * With `virtualizeColumns` they render out of view until they are.\n */\n pending(columns: readonly { id: string; auto: boolean }[]): string[] {\n return columns\n .filter((c) => {\n if (!c.auto) return false;\n const state = this.#state.get(c.id);\n return state === undefined || state === 'provisional';\n })\n .map((c) => c.id);\n }\n\n /**\n * Measures the auto columns that need it. Returns `true` if a width changed;\n * the caller then renders again (the one extra pass of the render cycle).\n */\n measure(\n root: ParentNode,\n columns: readonly { id: string; auto: boolean }[],\n hasLoadedRows: boolean,\n ): boolean {\n const due =
|
|
1
|
+
{"version":3,"file":"auto-width.js","names":[],"sources":["../../src/controllers/auto-width.ts"],"sourcesContent":["import type { ColumnSizingState } from '@argus-table/core';\n\n/**\n * The width each column's content needs, in px, from the cells rendered now:\n * the header, the pinned rows and the rendered body rows. Rows that haven't\n * loaded are skipped (their skeleton bars say nothing about the content).\n *\n * Every cell of the columns gets `data-measure`, which sizes it to its\n * `max-content` (base styles), so the browser accounts for padding, the sort\n * indicator and whatever the content is. One forced layout, no paint: the\n * attribute is gone before the frame ends. A column with nothing laid out\n * (the table is hidden) is left out.\n */\nexport function measureColumns(root: ParentNode, ids: Iterable<string>): Map<string, number> {\n const wanted = new Set(ids);\n const widths = new Map<string, number>();\n if (wanted.size === 0) return widths;\n const cells = [...root.querySelectorAll<HTMLElement>('[data-col]')].filter(\n (cell) =>\n wanted.has(cell.dataset.col ?? '') && !cell.parentElement?.matches('[part~=\"row-loading\"]'),\n );\n for (const cell of cells) cell.setAttribute('data-measure', '');\n for (const cell of cells) {\n const id = cell.dataset.col ?? '';\n const px = Math.ceil(cell.getBoundingClientRect().width);\n if (px > 0 && px > (widths.get(id) ?? 0)) widths.set(id, px);\n }\n for (const cell of cells) cell.removeAttribute('data-measure');\n return widths;\n}\n\n/**\n * - `provisional`: measured without loaded body rows (the header and pinned rows only).\n * - `final`: measured with loaded rows; kept until the column goes away.\n * - `fonts`: measured while web fonts were loading; measured again once they are ready.\n */\ntype State = 'provisional' | 'final' | 'fonts';\n\n/**\n * Widths of `width: 'auto'` columns. A column measures once its first loaded\n * rows render, and before that fits the header and pinned rows. So each column\n * is measured at most twice (three times if fonts were still loading), and a\n * table that stays empty doesn't measure on every update. State is keyed by\n * column id, so new `columns` arrays with the same ids don't measure again.\n */\nexport class AutoWidths {\n /** Measured widths by column id; a new object whenever one changes. */\n widths: ColumnSizingState = {};\n readonly #state = new Map<string, State>();\n #fontsPending = false;\n readonly #requestUpdate: () => void;\n\n constructor(requestUpdate: () => void) {\n this.#requestUpdate = requestUpdate;\n }\n\n /** Forgets columns that are gone or no longer `'auto'`, given the ids of the auto columns. */\n sync(ids: Iterable<string>): void {\n const auto = new Set(ids);\n for (const id of this.#state.keys()) {\n if (!auto.has(id)) this.#state.delete(id);\n }\n const kept = Object.entries(this.widths).filter(([id]) => auto.has(id));\n if (kept.length !== Object.keys(this.widths).length) this.widths = Object.fromEntries(kept);\n }\n\n /**\n * The auto columns still to be measured: never, or without loaded rows yet.\n * With `virtualizeColumns` they render out of view until they are.\n */\n pending(columns: readonly { id: string; auto: boolean }[]): string[] {\n return this.#due(columns, true);\n }\n\n /** The auto columns never measured, or measured without loaded rows when there are some now. */\n #due(columns: readonly { id: string; auto: boolean }[], hasLoadedRows: boolean): string[] {\n return columns\n .filter((c) => {\n if (!c.auto) return false;\n const state = this.#state.get(c.id);\n return state === undefined || (state === 'provisional' && hasLoadedRows);\n })\n .map((c) => c.id);\n }\n\n /**\n * Measures the auto columns that need it. Returns `true` if a width changed;\n * the caller then renders again (the one extra pass of the render cycle).\n */\n measure(\n root: ParentNode,\n columns: readonly { id: string; auto: boolean }[],\n hasLoadedRows: boolean,\n ): boolean {\n const due = this.#due(columns, hasLoadedRows);\n if (due.length === 0) return false;\n const measured = measureColumns(root, due);\n const fontsLoading = typeof document !== 'undefined' && document.fonts.status === 'loading';\n let next: ColumnSizingState | undefined;\n for (const [id, px] of measured) {\n this.#state.set(id, fontsLoading ? 'fonts' : hasLoadedRows ? 'final' : 'provisional');\n if (this.widths[id] !== px) (next ??= { ...this.widths })[id] = px;\n }\n if (fontsLoading && measured.size > 0) this.#remeasureAfterFonts();\n if (next) this.widths = next;\n return next !== undefined;\n }\n\n /** Once web fonts are ready, columns measured meanwhile measure again. */\n #remeasureAfterFonts(): void {\n if (this.#fontsPending) return;\n this.#fontsPending = true;\n void document.fonts.ready.then(() => {\n this.#fontsPending = false;\n let any = false;\n for (const [id, state] of this.#state) {\n if (state === 'fonts') {\n this.#state.delete(id);\n any = true;\n }\n }\n if (any) this.#requestUpdate();\n });\n }\n}\n"],"mappings":";;;;;;;;;;;;AAaA,SAAgB,eAAe,MAAkB,KAA4C;CAC3F,MAAM,SAAS,IAAI,IAAI,GAAG;CAC1B,MAAM,yBAAS,IAAI,IAAoB;CACvC,IAAI,OAAO,SAAS,GAAG,OAAO;CAC9B,MAAM,QAAQ,CAAC,GAAG,KAAK,iBAA8B,YAAY,CAAC,CAAC,CAAC,QACjE,SACC,OAAO,IAAI,KAAK,QAAQ,OAAO,EAAE,KAAK,CAAC,KAAK,eAAe,QAAQ,yBAAuB,CAC9F;CACA,KAAK,MAAM,QAAQ,OAAO,KAAK,aAAa,gBAAgB,EAAE;CAC9D,KAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,KAAK,KAAK,QAAQ,OAAO;EAC/B,MAAM,KAAK,KAAK,KAAK,KAAK,sBAAsB,CAAC,CAAC,KAAK;EACvD,IAAI,KAAK,KAAK,MAAM,OAAO,IAAI,EAAE,KAAK,IAAI,OAAO,IAAI,IAAI,EAAE;CAC7D;CACA,KAAK,MAAM,QAAQ,OAAO,KAAK,gBAAgB,cAAc;CAC7D,OAAO;AACT;;;;;;;;AAgBA,IAAa,aAAb,MAAwB;CAGtB;CACA;CACA;CAEA,YAAY,eAA2B;EALX,KAAA,SAAA,CAAC;EACX,KAAA,yBAAA,IAAI,IAAmB;EACzB,KAAA,gBAAA;EAId,KAAK,iBAAiB;CACxB;;CAGA,KAAK,KAA6B;EAChC,MAAM,OAAO,IAAI,IAAI,GAAG;EACxB,KAAK,MAAM,MAAM,KAAK,OAAO,KAAK,GAChC,IAAI,CAAC,KAAK,IAAI,EAAE,GAAG,KAAK,OAAO,OAAO,EAAE;EAE1C,MAAM,OAAO,OAAO,QAAQ,KAAK,MAAM,CAAC,CAAC,QAAQ,CAAC,QAAQ,KAAK,IAAI,EAAE,CAAC;EACtE,IAAI,KAAK,WAAW,OAAO,KAAK,KAAK,MAAM,CAAC,CAAC,QAAQ,KAAK,SAAS,OAAO,YAAY,IAAI;CAC5F;;;;;CAMA,QAAQ,SAA6D;EACnE,OAAO,KAAK,KAAK,SAAS,IAAI;CAChC;;CAGA,KAAK,SAAmD,eAAkC;EACxF,OAAO,QACJ,QAAQ,MAAM;GACb,IAAI,CAAC,EAAE,MAAM,OAAO;GACpB,MAAM,QAAQ,KAAK,OAAO,IAAI,EAAE,EAAE;GAClC,OAAO,UAAU,KAAA,KAAc,UAAU,iBAAiB;EAC5D,CAAC,CAAC,CACD,KAAK,MAAM,EAAE,EAAE;CACpB;;;;;CAMA,QACE,MACA,SACA,eACS;EACT,MAAM,MAAM,KAAK,KAAK,SAAS,aAAa;EAC5C,IAAI,IAAI,WAAW,GAAG,OAAO;EAC7B,MAAM,WAAW,eAAe,MAAM,GAAG;EACzC,MAAM,eAAe,OAAO,aAAa,eAAe,SAAS,MAAM,WAAW;EAClF,IAAI;EACJ,KAAK,MAAM,CAAC,IAAI,OAAO,UAAU;GAC/B,KAAK,OAAO,IAAI,IAAI,eAAe,UAAU,gBAAgB,UAAU,aAAa;GACpF,IAAI,KAAK,OAAO,QAAQ,IAAI,CAAC,SAAS,EAAE,GAAG,KAAK,OAAO,EAAA,CAAG,MAAM;EAClE;EACA,IAAI,gBAAgB,SAAS,OAAO,GAAG,KAAK,qBAAqB;EACjE,IAAI,MAAM,KAAK,SAAS;EACxB,OAAO,SAAS,KAAA;CAClB;;CAGA,uBAA6B;EAC3B,IAAI,KAAK,eAAe;EACxB,KAAK,gBAAgB;EACrB,SAAc,MAAM,MAAM,WAAW;GACnC,KAAK,gBAAgB;GACrB,IAAI,MAAM;GACV,KAAK,MAAM,CAAC,IAAI,UAAU,KAAK,QAC7B,IAAI,UAAU,SAAS;IACrB,KAAK,OAAO,OAAO,EAAE;IACrB,MAAM;GACR;GAEF,IAAI,KAAK,KAAK,eAAe;EAC/B,CAAC;CACH;AACF"}
|
|
@@ -1,13 +1,27 @@
|
|
|
1
|
-
import { ArgusColumn
|
|
1
|
+
import { ArgusColumn } from "../argus-column.js";
|
|
2
|
+
import { ArgusColumnGroup } from "../argus-column-group.js";
|
|
3
|
+
import { unwatchColumns, watchColumns } from "../column-registry.js";
|
|
2
4
|
// src/controllers/column-children.ts
|
|
5
|
+
var NO_GROUPS = [];
|
|
6
|
+
/** The `<argus-column>`s among an element's children and in its groups, in document order. */
|
|
7
|
+
function columnsIn(parent) {
|
|
8
|
+
return [...parent.children].flatMap((c) => {
|
|
9
|
+
if (c instanceof ArgusColumn) return [c];
|
|
10
|
+
if (c instanceof ArgusColumnGroup) return columnsIn(c);
|
|
11
|
+
return [];
|
|
12
|
+
});
|
|
13
|
+
}
|
|
3
14
|
/**
|
|
4
|
-
* Reads the host's `<argus-column>` children
|
|
5
|
-
*
|
|
6
|
-
* `
|
|
15
|
+
* Reads the host's `<argus-column>` children, and those in its
|
|
16
|
+
* `<argus-column-group>` children, into `ColumnDef`s, and the groups into
|
|
17
|
+
* `ColumnGroup`s. Columns and groups tell the host when they change
|
|
18
|
+
* (`watchColumns`); the lists are rebuilt only then, so they keep their identity
|
|
19
|
+
* between changes.
|
|
7
20
|
*/
|
|
8
21
|
var ColumnChildrenController = class {
|
|
9
22
|
#host;
|
|
10
23
|
#defs = [];
|
|
24
|
+
#groups = NO_GROUPS;
|
|
11
25
|
#dirty = true;
|
|
12
26
|
constructor(host) {
|
|
13
27
|
this.#host = host;
|
|
@@ -27,14 +41,23 @@ var ColumnChildrenController = class {
|
|
|
27
41
|
get present() {
|
|
28
42
|
return this.defs.length > 0;
|
|
29
43
|
}
|
|
30
|
-
/** The children's columns, in document order. */
|
|
44
|
+
/** The children's columns, in document order, those in groups included. */
|
|
31
45
|
get defs() {
|
|
32
|
-
|
|
33
|
-
this.#dirty = false;
|
|
34
|
-
this.#defs = [...this.#host.children].filter((c) => c instanceof ArgusColumn).map((c) => c.toColumnDef());
|
|
35
|
-
}
|
|
46
|
+
this.#read();
|
|
36
47
|
return this.#defs;
|
|
37
48
|
}
|
|
49
|
+
/** The `<argus-column-group>` children's groups, in document order. */
|
|
50
|
+
get groups() {
|
|
51
|
+
this.#read();
|
|
52
|
+
return this.#groups;
|
|
53
|
+
}
|
|
54
|
+
#read() {
|
|
55
|
+
if (!this.#dirty) return;
|
|
56
|
+
this.#dirty = false;
|
|
57
|
+
this.#defs = columnsIn(this.#host).map((c) => c.toColumnDef());
|
|
58
|
+
const groups = [...this.#host.children].filter((c) => c instanceof ArgusColumnGroup).map((c) => c.toColumnGroup());
|
|
59
|
+
this.#groups = groups.length ? groups : NO_GROUPS;
|
|
60
|
+
}
|
|
38
61
|
};
|
|
39
62
|
export { ColumnChildrenController };
|
|
40
63
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"column-children.js","names":[],"sources":["../../src/controllers/column-children.ts"],"sourcesContent":["import type { ReactiveController, ReactiveControllerHost } from 'lit';\nimport { ArgusColumn
|
|
1
|
+
{"version":3,"file":"column-children.js","names":[],"sources":["../../src/controllers/column-children.ts"],"sourcesContent":["import type { ReactiveController, ReactiveControllerHost } from 'lit';\nimport { ArgusColumn } from '../argus-column.js';\nimport { ArgusColumnGroup } from '../argus-column-group.js';\nimport { unwatchColumns, watchColumns } from '../column-registry.js';\nimport type { ColumnDef, ColumnGroup } from '../types.js';\n\nconst NO_GROUPS: readonly ColumnGroup[] = [];\n\n/** The `<argus-column>`s among an element's children and in its groups, in document order. */\nfunction columnsIn<T>(parent: Element): ArgusColumn<T>[] {\n return [...parent.children].flatMap((c) => {\n if (c instanceof ArgusColumn) return [c as ArgusColumn<T>];\n if (c instanceof ArgusColumnGroup) return columnsIn<T>(c);\n return [];\n });\n}\n\n/**\n * Reads the host's `<argus-column>` children, and those in its\n * `<argus-column-group>` children, into `ColumnDef`s, and the groups into\n * `ColumnGroup`s. Columns and groups tell the host when they change\n * (`watchColumns`); the lists are rebuilt only then, so they keep their identity\n * between changes.\n */\nexport class ColumnChildrenController<T> implements ReactiveController {\n readonly #host: ReactiveControllerHost & HTMLElement;\n #defs: readonly ColumnDef<T>[] = [];\n #groups: readonly ColumnGroup[] = NO_GROUPS;\n #dirty = true;\n\n constructor(host: ReactiveControllerHost & HTMLElement) {\n this.#host = host;\n host.addController(this);\n }\n\n hostConnected(): void {\n this.#dirty = true;\n watchColumns(this.#host, () => {\n this.#dirty = true;\n this.#host.requestUpdate();\n });\n }\n\n hostDisconnected(): void {\n unwatchColumns(this.#host);\n }\n\n /** Whether the host has any `<argus-column>` child. */\n get present(): boolean {\n return this.defs.length > 0;\n }\n\n /** The children's columns, in document order, those in groups included. */\n get defs(): readonly ColumnDef<T>[] {\n this.#read();\n return this.#defs;\n }\n\n /** The `<argus-column-group>` children's groups, in document order. */\n get groups(): readonly ColumnGroup[] {\n this.#read();\n return this.#groups;\n }\n\n #read(): void {\n if (!this.#dirty) return;\n this.#dirty = false;\n this.#defs = columnsIn<T>(this.#host).map((c) => c.toColumnDef());\n const groups = [...this.#host.children]\n .filter((c): c is ArgusColumnGroup => c instanceof ArgusColumnGroup)\n .map((c) => c.toColumnGroup());\n this.#groups = groups.length ? groups : NO_GROUPS;\n }\n}\n"],"mappings":";;;;AAMA,IAAM,YAAoC,CAAC;;AAG3C,SAAS,UAAa,QAAmC;CACvD,OAAO,CAAC,GAAG,OAAO,QAAQ,CAAC,CAAC,SAAS,MAAM;EACzC,IAAI,aAAa,aAAa,OAAO,CAAC,CAAmB;EACzD,IAAI,aAAa,kBAAkB,OAAO,UAAa,CAAC;EACxD,OAAO,CAAC;CACV,CAAC;AACH;;;;;;;;AASA,IAAa,2BAAb,MAAuE;CACrE;CACA,QAAiC,CAAC;CAClC,UAAkC;CAClC,SAAS;CAET,YAAY,MAA4C;EACtD,KAAK,QAAQ;EACb,KAAK,cAAc,IAAI;CACzB;CAEA,gBAAsB;EACpB,KAAK,SAAS;EACd,aAAa,KAAK,aAAa;GAC7B,KAAK,SAAS;GACd,KAAK,MAAM,cAAc;EAC3B,CAAC;CACH;CAEA,mBAAyB;EACvB,eAAe,KAAK,KAAK;CAC3B;;CAGA,IAAI,UAAmB;EACrB,OAAO,KAAK,KAAK,SAAS;CAC5B;;CAGA,IAAI,OAAgC;EAClC,KAAK,MAAM;EACX,OAAO,KAAK;CACd;;CAGA,IAAI,SAAiC;EACnC,KAAK,MAAM;EACX,OAAO,KAAK;CACd;CAEA,QAAc;EACZ,IAAI,CAAC,KAAK,QAAQ;EAClB,KAAK,SAAS;EACd,KAAK,QAAQ,UAAa,KAAK,KAAK,CAAC,CAAC,KAAK,MAAM,EAAE,YAAY,CAAC;EAChE,MAAM,SAAS,CAAC,GAAG,KAAK,MAAM,QAAQ,CAAC,CACpC,QAAQ,MAA6B,aAAa,gBAAgB,CAAC,CACnE,KAAK,MAAM,EAAE,cAAc,CAAC;EAC/B,KAAK,UAAU,OAAO,SAAS,SAAS;CAC1C;AACF"}
|
|
@@ -48,10 +48,6 @@ var ColumnWindowController = class {
|
|
|
48
48
|
this.#resize?.disconnect();
|
|
49
49
|
this.#attached = void 0;
|
|
50
50
|
}
|
|
51
|
-
/** Number of column pool slots created so far. */
|
|
52
|
-
get poolSize() {
|
|
53
|
-
return this.#pool.size;
|
|
54
|
-
}
|
|
55
51
|
/**
|
|
56
52
|
* The columns to render, given the model's columns in visual order. With
|
|
57
53
|
* `enabled`, `keep` names columns to render wherever they are. Call once per
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"column-window.js","names":[],"sources":["../../src/controllers/column-window.ts"],"sourcesContent":["import type { ReactiveController, ReactiveControllerHost } from 'lit';\nimport { columnWindow, maxVisibleColumns, RowPool, type ColumnView } from '@argus-table/core';\nimport type { CellContent } from '../types.js';\n\n/** Scrolling columns rendered beyond each side of the visible ones. */\nexport const COLUMN_OVERSCAN = 2;\n\n/**\n * A rendered column: the column, the slot its cells and light-DOM content use\n * (`h-{slot}`, `c-{row}-{slot}`), the key its cells are rendered by, and with\n * `virtualizeColumns` the grid track it is placed in.\n */\nexport type RenderedColumn<T> = ColumnView<T, CellContent> & {\n slot: number;\n key: unknown;\n /** Grid column the cells are placed in; `undefined`: in DOM order. */\n place: number | undefined;\n};\n\ntype Column<T> = ColumnView<T, CellContent>;\n\n/**\n * Column virtualization (`virtualizeColumns`): which columns render, and which\n * pool slot's DOM each one uses.\n *\n * Off, every column renders in visual order, its slot is its `colIndex` and\n * its cells are keyed by column id: the DOM is what it always was.\n *\n * On, the scrolling columns in view render, plus `COLUMN_OVERSCAN` on each\n * side, and the pinned columns, the checkbox column and the columns the table\n * keeps (the active one, one being dragged, auto widths still to measure)\n * whatever the scroll position. They are pooled like rows (ADR 0001): a column\n * entering the view reuses the cells, slots and content of one that left, so\n * the DOM is updated in place, and the window has one size, so the pool fills\n * on the first render and scrolling only reuses it (in WebKit, new slots\n * while scrolling cut a wheel scroll short; see `steadyWindow`). DOM order is\n * then pool order: each cell is placed in its grid column explicitly, and\n * `aria-colindex` carries the position, as `aria-rowindex` does for rows.\n */\nexport class ColumnWindowController<T> implements ReactiveController {\n readonly #host: ReactiveControllerHost & HTMLElement;\n readonly #scroller: () => HTMLElement | null;\n readonly #pool = new RowPool();\n #attached: HTMLElement | undefined;\n #resize: ResizeObserver | undefined;\n #enabled = false;\n /** The columns of the last render, and what the window was computed from. */\n #columns: readonly Column<T>[] = [];\n #scrolling: Column<T>[] = [];\n #pinnedWidth = 0;\n #maxVisible: { columns: readonly Column<T>[]; view: number; value: number } | undefined;\n /** First index and size of the last window rendered. */\n #key = '';\n\n constructor(host: ReactiveControllerHost & HTMLElement, scroller: () => HTMLElement | null) {\n this.#host = host;\n this.#scroller = scroller;\n host.addController(this);\n }\n\n hostConnected(): void {\n this.#attach();\n }\n\n hostUpdated(): void {\n this.#attach();\n }\n\n hostDisconnected(): void {\n this.#attached?.removeEventListener('scroll', this.#onScroll);\n this.#resize?.disconnect();\n this.#attached = undefined;\n }\n\n
|
|
1
|
+
{"version":3,"file":"column-window.js","names":[],"sources":["../../src/controllers/column-window.ts"],"sourcesContent":["import type { ReactiveController, ReactiveControllerHost } from 'lit';\nimport { columnWindow, maxVisibleColumns, RowPool, type ColumnView } from '@argus-table/core';\nimport type { CellContent } from '../types.js';\n\n/** Scrolling columns rendered beyond each side of the visible ones. */\nexport const COLUMN_OVERSCAN = 2;\n\n/**\n * A rendered column: the column, the slot its cells and light-DOM content use\n * (`h-{slot}`, `c-{row}-{slot}`), the key its cells are rendered by, and with\n * `virtualizeColumns` the grid track it is placed in.\n */\nexport type RenderedColumn<T> = ColumnView<T, CellContent> & {\n slot: number;\n key: unknown;\n /** Grid column the cells are placed in; `undefined`: in DOM order. */\n place: number | undefined;\n};\n\ntype Column<T> = ColumnView<T, CellContent>;\n\n/**\n * Column virtualization (`virtualizeColumns`): which columns render, and which\n * pool slot's DOM each one uses.\n *\n * Off, every column renders in visual order, its slot is its `colIndex` and\n * its cells are keyed by column id: the DOM is what it always was.\n *\n * On, the scrolling columns in view render, plus `COLUMN_OVERSCAN` on each\n * side, and the pinned columns, the checkbox column and the columns the table\n * keeps (the active one, one being dragged, auto widths still to measure)\n * whatever the scroll position. They are pooled like rows (ADR 0001): a column\n * entering the view reuses the cells, slots and content of one that left, so\n * the DOM is updated in place, and the window has one size, so the pool fills\n * on the first render and scrolling only reuses it (in WebKit, new slots\n * while scrolling cut a wheel scroll short; see `steadyWindow`). DOM order is\n * then pool order: each cell is placed in its grid column explicitly, and\n * `aria-colindex` carries the position, as `aria-rowindex` does for rows.\n */\nexport class ColumnWindowController<T> implements ReactiveController {\n readonly #host: ReactiveControllerHost & HTMLElement;\n readonly #scroller: () => HTMLElement | null;\n readonly #pool = new RowPool();\n #attached: HTMLElement | undefined;\n #resize: ResizeObserver | undefined;\n #enabled = false;\n /** The columns of the last render, and what the window was computed from. */\n #columns: readonly Column<T>[] = [];\n #scrolling: Column<T>[] = [];\n #pinnedWidth = 0;\n #maxVisible: { columns: readonly Column<T>[]; view: number; value: number } | undefined;\n /** First index and size of the last window rendered. */\n #key = '';\n\n constructor(host: ReactiveControllerHost & HTMLElement, scroller: () => HTMLElement | null) {\n this.#host = host;\n this.#scroller = scroller;\n host.addController(this);\n }\n\n hostConnected(): void {\n this.#attach();\n }\n\n hostUpdated(): void {\n this.#attach();\n }\n\n hostDisconnected(): void {\n this.#attached?.removeEventListener('scroll', this.#onScroll);\n this.#resize?.disconnect();\n this.#attached = undefined;\n }\n\n /**\n * The columns to render, given the model's columns in visual order. With\n * `enabled`, `keep` names columns to render wherever they are. Call once per\n * render.\n */\n compute(\n columns: readonly Column<T>[],\n enabled: boolean,\n keep: Iterable<string | undefined>,\n ): RenderedColumn<T>[] {\n this.#enabled = enabled;\n if (!enabled) {\n this.#pool.clear();\n this.#key = '';\n return columns.map((c) => ({ ...c, slot: c.colIndex, key: c.id, place: undefined }));\n }\n if (columns !== this.#columns) {\n this.#columns = columns;\n this.#scrolling = columns.filter((c) => c.kind === 'data' && c.pin === undefined);\n this.#pinnedWidth = columns.reduce((sum, c) => (c.pin ? sum + c.width : sum), 0);\n }\n const wanted = new Set<string | undefined>(keep);\n const window = this.#window();\n for (const i of window) wanted.add(this.#scrolling[i]?.id);\n this.#key = keyOf(window);\n // In visual order, so the first render's slots (and DOM order) follow the columns.\n const ids: string[] = [];\n const byId = new Map<string, Column<T>>();\n for (const c of columns) {\n if (c.pin !== undefined || c.kind !== 'data' || wanted.has(c.id)) {\n ids.push(c.id);\n byId.set(c.id, c);\n }\n }\n const rendered: RenderedColumn<T>[] = [];\n for (const { id, slot } of this.#pool.update(ids)) {\n const c = byId.get(id);\n if (c) rendered.push({ ...c, slot, key: slot, place: c.colIndex });\n }\n return rendered;\n }\n\n /** The window's indexes in the scrolling columns, for the scroller as it is now. */\n #window(): number[] {\n // Before the first render there is no scroller: the host is as wide.\n const s = this.#attached ?? this.#scroller();\n const view = Math.max(0, (s ?? this.#host).clientWidth - this.#pinnedWidth);\n const columns = this.#scrolling;\n let max = this.#maxVisible;\n if (max?.columns !== this.#columns || max.view !== view) {\n max = { columns: this.#columns, view, value: maxVisibleColumns(columns, view) };\n this.#maxVisible = max;\n }\n // Negative in RTL.\n const offset = Math.abs(s?.scrollLeft ?? 0);\n return columnWindow(columns, offset, view, COLUMN_OVERSCAN, max.value);\n }\n\n #attach(): void {\n const s = this.#scroller();\n if (!s || s === this.#attached) return;\n this.#attached?.removeEventListener('scroll', this.#onScroll);\n this.#attached = s;\n s.addEventListener('scroll', this.#onScroll, { passive: true });\n this.#resize ??= new ResizeObserver(() => {\n this.#onScroll();\n });\n this.#resize.disconnect();\n this.#resize.observe(s);\n }\n\n /** Renders again when the window moves: not on every scroll event. */\n readonly #onScroll = () => {\n if (!this.#enabled) return;\n const window = this.#window();\n if (keyOf(window) !== this.#key) this.#host.requestUpdate();\n };\n}\n\nfunction keyOf(window: readonly number[]): string {\n return `${String(window[0] ?? -1)}:${String(window.length)}`;\n}\n"],"mappings":"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"}
|
|
@@ -15,6 +15,7 @@ var PluginController = class {
|
|
|
15
15
|
#failed = /* @__PURE__ */ new Set();
|
|
16
16
|
#version = 0;
|
|
17
17
|
#memo;
|
|
18
|
+
#groupsMemo;
|
|
18
19
|
constructor(host, api) {
|
|
19
20
|
this.#host = host;
|
|
20
21
|
this.#api = api;
|
|
@@ -33,25 +34,43 @@ var PluginController = class {
|
|
|
33
34
|
hostDisconnected() {
|
|
34
35
|
for (const plugin of [...this.#connected.keys()].reverse()) this.#disconnect(plugin);
|
|
35
36
|
}
|
|
37
|
+
/** Tells the plugins, in order, which properties changed; before the transforms. */
|
|
38
|
+
willUpdate(changed) {
|
|
39
|
+
for (const plugin of this.#plugins) {
|
|
40
|
+
if (!plugin.willUpdate || this.#failed.has(plugin)) continue;
|
|
41
|
+
this.#guard(plugin, "willUpdate", () => {
|
|
42
|
+
plugin.willUpdate?.(changed, this.#api);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
36
46
|
/** The plugins' `columns` transforms applied in order; the same array while nothing changed. */
|
|
37
47
|
transform(input) {
|
|
38
|
-
const memo = this.#memo;
|
|
39
|
-
|
|
48
|
+
const memo = this.#run("columns", input, this.#memo, (plugin, defs) => plugin.columns?.(defs, this.#api));
|
|
49
|
+
this.#memo = memo;
|
|
50
|
+
return memo.output;
|
|
51
|
+
}
|
|
52
|
+
/** The plugins' `columnGroups` transforms applied in order; the same array while nothing changed. */
|
|
53
|
+
transformGroups(input) {
|
|
54
|
+
const memo = this.#run("columnGroups", input, this.#groupsMemo, (plugin, groups) => plugin.columnGroups?.(groups, this.#api));
|
|
55
|
+
this.#groupsMemo = memo;
|
|
56
|
+
return memo.output;
|
|
57
|
+
}
|
|
58
|
+
#run(hook, input, memo, apply) {
|
|
59
|
+
if (memo?.input === input && memo.plugins === this.#plugins && memo.version === this.#version) return memo;
|
|
40
60
|
let output = input;
|
|
41
61
|
for (const plugin of this.#plugins) {
|
|
42
|
-
if (!plugin
|
|
62
|
+
if (!plugin[hook] || this.#failed.has(plugin)) continue;
|
|
43
63
|
const before = output;
|
|
44
|
-
const result = this.#guard(plugin,
|
|
64
|
+
const result = this.#guard(plugin, hook, () => apply(plugin, before));
|
|
45
65
|
if (result.ok && result.value) output = result.value;
|
|
46
66
|
}
|
|
47
67
|
if (memo && sameItems(memo.output, output)) output = memo.output;
|
|
48
|
-
|
|
68
|
+
return {
|
|
49
69
|
input,
|
|
50
70
|
plugins: this.#plugins,
|
|
51
71
|
version: this.#version,
|
|
52
72
|
output
|
|
53
73
|
};
|
|
54
|
-
return output;
|
|
55
74
|
}
|
|
56
75
|
/** Runs the transforms again on the next `transform()`. */
|
|
57
76
|
invalidate() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugins.js","names":[],"sources":["../../src/controllers/plugins.ts"],"sourcesContent":["import type { ReactiveController, ReactiveControllerHost } from 'lit';\nimport type { ArgusPlugin, CellTarget, PluginHost } from '../plugins.js';\nimport type { ColumnDef } from '../types.js';\n\ntype Hook = 'connect' | 'cleanup' | 'columns' | 'keydown';\n\n/**\n * Runs the table's plugins: connects them while the table is connected (in\n * array order, cleaning up in reverse), chains their column transforms, and\n * offers keys to them before the table handles them. A hook that throws is\n * reported and skipped, so a broken plugin can't break the update.\n */\nexport class PluginController<T> implements ReactiveController {\n readonly #host: ReactiveControllerHost & HTMLElement;\n readonly #api: PluginHost<T>;\n #plugins: readonly ArgusPlugin<T>[] = [];\n /** Connected plugins and their cleanups. */\n readonly #connected = new Map<ArgusPlugin<T>, (() => void) | undefined>();\n /** Plugins whose `connect` threw: off until `plugins` changes. */\n readonly #failed = new Set<ArgusPlugin<T>>();\n #version = 0;\n #memo
|
|
1
|
+
{"version":3,"file":"plugins.js","names":[],"sources":["../../src/controllers/plugins.ts"],"sourcesContent":["import type { ReactiveController, ReactiveControllerHost } from 'lit';\nimport type { ArgusPlugin, CellTarget, PluginHost } from '../plugins.js';\nimport type { ColumnDef, ColumnGroup } from '../types.js';\n\ntype Hook = 'connect' | 'cleanup' | 'willUpdate' | 'columns' | 'columnGroups' | 'keydown';\n\ninterface Memo<V> {\n input: readonly V[];\n plugins: readonly unknown[];\n version: number;\n output: readonly V[];\n}\n\n/**\n * Runs the table's plugins: connects them while the table is connected (in\n * array order, cleaning up in reverse), chains their column transforms, and\n * offers keys to them before the table handles them. A hook that throws is\n * reported and skipped, so a broken plugin can't break the update.\n */\nexport class PluginController<T> implements ReactiveController {\n readonly #host: ReactiveControllerHost & HTMLElement;\n readonly #api: PluginHost<T>;\n #plugins: readonly ArgusPlugin<T>[] = [];\n /** Connected plugins and their cleanups. */\n readonly #connected = new Map<ArgusPlugin<T>, (() => void) | undefined>();\n /** Plugins whose `connect` threw: off until `plugins` changes. */\n readonly #failed = new Set<ArgusPlugin<T>>();\n #version = 0;\n #memo: Memo<ColumnDef<T, unknown>> | undefined;\n #groupsMemo: Memo<ColumnGroup<unknown>> | undefined;\n\n constructor(host: ReactiveControllerHost & HTMLElement, api: PluginHost<T>) {\n this.#host = host;\n this.#api = api;\n host.addController(this);\n }\n\n /** Takes a new plugin list; connects and cleans up to match it if the host is connected. */\n set(plugins: readonly ArgusPlugin<T>[]): void {\n if (plugins === this.#plugins) return;\n this.#plugins = plugins;\n this.#failed.clear();\n if (this.#host.isConnected) this.#sync();\n }\n\n hostConnected(): void {\n this.#sync();\n }\n\n hostDisconnected(): void {\n for (const plugin of [...this.#connected.keys()].reverse()) this.#disconnect(plugin);\n }\n\n /** Tells the plugins, in order, which properties changed; before the transforms. */\n willUpdate(changed: ReadonlySet<string>): void {\n for (const plugin of this.#plugins) {\n if (!plugin.willUpdate || this.#failed.has(plugin)) continue;\n this.#guard(plugin, 'willUpdate', () => {\n plugin.willUpdate?.(changed, this.#api);\n });\n }\n }\n\n /** The plugins' `columns` transforms applied in order; the same array while nothing changed. */\n transform(input: readonly ColumnDef<T>[]): readonly ColumnDef<T>[] {\n const memo = this.#run('columns', input, this.#memo, (plugin, defs) =>\n plugin.columns?.(defs, this.#api),\n );\n this.#memo = memo;\n // Plugins see content as `unknown`; what they return renders through the\n // `content()` directive, which hands anything else to the `contentRenderer`.\n return memo.output as readonly ColumnDef<T>[];\n }\n\n /** The plugins' `columnGroups` transforms applied in order; the same array while nothing changed. */\n transformGroups(input: readonly ColumnGroup[]): readonly ColumnGroup[] {\n const memo = this.#run('columnGroups', input, this.#groupsMemo, (plugin, groups) =>\n plugin.columnGroups?.(groups, this.#api),\n );\n this.#groupsMemo = memo;\n return memo.output as readonly ColumnGroup[];\n }\n\n #run<V>(\n hook: 'columns' | 'columnGroups',\n input: readonly V[],\n memo: Memo<V> | undefined,\n apply: (plugin: ArgusPlugin<T>, items: readonly V[]) => readonly V[] | undefined,\n ): Memo<V> {\n if (memo?.input === input && memo.plugins === this.#plugins && memo.version === this.#version) {\n return memo;\n }\n let output = input;\n for (const plugin of this.#plugins) {\n if (!plugin[hook] || this.#failed.has(plugin)) continue;\n const before = output;\n const result = this.#guard(plugin, hook, () => apply(plugin, before));\n if (result.ok && result.value) output = result.value;\n }\n // Plugins may return a fresh array each time: keep the old one when its items are the same.\n if (memo && sameItems(memo.output, output)) output = memo.output;\n return { input, plugins: this.#plugins, version: this.#version, output };\n }\n\n /** Runs the transforms again on the next `transform()`. */\n invalidate(): void {\n this.#version++;\n }\n\n /** Offers a key to the plugins in order; `true` once one claims it. */\n keydown(event: KeyboardEvent, target: CellTarget): boolean {\n for (const plugin of this.#plugins) {\n if (!plugin.keydown || this.#failed.has(plugin)) continue;\n const result = this.#guard(plugin, 'keydown', () =>\n plugin.keydown?.(event, target, this.#api),\n );\n if (result.ok && result.value === true) return true;\n }\n return false;\n }\n\n #sync(): void {\n const wanted = new Set(this.#plugins);\n for (const plugin of [...this.#connected.keys()].reverse()) {\n if (!wanted.has(plugin)) this.#disconnect(plugin);\n }\n for (const plugin of this.#plugins) {\n if (this.#connected.has(plugin) || this.#failed.has(plugin)) continue;\n const result = this.#guard(plugin, 'connect', () => plugin.connect?.(this.#api));\n if (result.ok) this.#connected.set(plugin, result.value);\n else this.#failed.add(plugin);\n }\n }\n\n #disconnect(plugin: ArgusPlugin<T>): void {\n const cleanup = this.#connected.get(plugin);\n this.#connected.delete(plugin);\n if (cleanup) this.#guard(plugin, 'cleanup', cleanup);\n }\n\n #guard<R>(\n plugin: ArgusPlugin<T>,\n hook: Hook,\n run: () => R,\n ): { ok: true; value: R } | { ok: false } {\n try {\n return { ok: true, value: run() };\n } catch (error) {\n console.error(`argus-table: plugin \"${plugin.name}\" threw in ${hook}`, error);\n return { ok: false };\n }\n }\n}\n\n/** Whether two lists hold the same items in the same order. */\nexport function sameItems<V>(a: readonly V[], b: readonly V[]): boolean {\n return a.length === b.length && a.every((item, i) => item === b[i]);\n}\n"],"mappings":"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"}
|
|
@@ -12,16 +12,19 @@ function dropTarget(slots, draggedId, x, direction) {
|
|
|
12
12
|
const last = slots.at(-1);
|
|
13
13
|
if (!dragged || !last) return void 0;
|
|
14
14
|
const at = slots.find((s) => (x - (s.rect.left + s.rect.right) / 2) * direction < 0);
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
if (!at) {
|
|
16
|
+
const to = last.index >= dragged.index ? last.index : last.index + 1;
|
|
17
|
+
return to === dragged.index ? void 0 : {
|
|
18
|
+
to,
|
|
19
|
+
id: last.id,
|
|
20
|
+
edge: "end"
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
const to = at.index > dragged.index ? at.index - 1 : at.index;
|
|
24
|
+
return to === dragged.index ? void 0 : {
|
|
18
25
|
to,
|
|
19
|
-
|
|
26
|
+
id: at.id,
|
|
20
27
|
edge: "start"
|
|
21
|
-
} : {
|
|
22
|
-
to,
|
|
23
|
-
columnId: last.id,
|
|
24
|
-
edge: "end"
|
|
25
28
|
};
|
|
26
29
|
}
|
|
27
30
|
/** Movement before a press becomes a drag, so a click still sorts. */
|
|
@@ -59,7 +62,7 @@ function startReorderDrag(cell, down, options) {
|
|
|
59
62
|
let dragging = false;
|
|
60
63
|
let frame = 0;
|
|
61
64
|
let target;
|
|
62
|
-
const same = (a, b) => a?.to === b?.to && a?.
|
|
65
|
+
const same = (a, b) => a?.to === b?.to && a?.id === b?.id && a?.edge === b?.edge;
|
|
63
66
|
const tick = () => {
|
|
64
67
|
frame = 0;
|
|
65
68
|
if (!dragging) return;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reorder-drag.js","names":[],"sources":["../../src/controllers/reorder-drag.ts"],"sourcesContent":["import { INTERACTIVE } from '../interactive.js';\n\n/** Where a dragged column would land: its new position and the header that shows the drop line. */\nexport interface DropTarget {\n /** Position in the whole column order after the move, 0-based. */\n to: number;\n /** The header that draws the drop line. */\n columnId: string;\n /** The edge of that header the line is on, in logical terms. */\n edge: 'start' | 'end';\n}\n\n/** A header of the dragged column's group, in visual order. */\nexport interface DropSlot {\n id: string;\n /** Its position in the whole column order, 0-based. */\n index: number;\n rect: { left: number; right: number };\n}\n\n/**\n * The drop target for a pointer at `x`: the gap between headers nearest to it,\n * decided by each header's midpoint. `undefined` where the column would stay\n * put. `direction` is 1 in LTR and -1 in RTL, where the group runs right to left.\n * The slots needn't be next to each other in the order: with\n * `virtualizeColumns`, headers out of view aren't rendered.\n */\nexport function dropTarget(\n slots: readonly DropSlot[],\n draggedId: string,\n x: number,\n direction: 1 | -1,\n): DropTarget | undefined {\n const dragged = slots.find((s) => s.id === draggedId);\n const last = slots.at(-1);\n if (!dragged || !last) return undefined;\n // The gap before the first header whose middle is past the pointer, in reading order.\n const at = slots.find((s) => (x - (s.rect.left + s.rect.right) / 2) * direction < 0);\n // Taking the column out shifts the ones after it one place back.\n const to = at\n ? at.index > dragged.index\n ? at.index - 1\n : at.index\n : last.index >= dragged.index\n ? last.index\n : last.index + 1;\n if (to === dragged.index) return undefined;\n return at ? { to, columnId: at.id, edge: 'start' } : { to, columnId: last.id, edge: 'end' };\n}\n\nexport interface ReorderDragOptions {\n /** The scroll container: it scrolls while the pointer is near its edges. */\n scroller: HTMLElement;\n /** The drop target for a pointer at `x` (client coordinates). */\n targetAt: (x: number) => DropTarget | undefined;\n /**\n * Where the dragged column's group is visible, in client x, if it can scroll\n * (the scrolling columns). Near these edges the scroller scrolls with the drag.\n */\n scrollArea: () => { left: number; right: number } | undefined;\n onStart: () => void;\n /** The target changed (`undefined`: the column would stay put). */\n onTarget: (target: DropTarget | undefined) => void;\n /** The pointer was released over a target, or `undefined` when it would stay put. */\n onDrop: (target: DropTarget | undefined) => void;\n /** Escape, a cancelled pointer, or a release without a drag. */\n onCancel: () => void;\n}\n\n/** Movement before a press becomes a drag, so a click still sorts. */\nconst THRESHOLD = 4;\n/** Width of the band at the scroll area's edges that scrolls, and the top speed there. */\nconst EDGE = 40;\nconst MAX_SPEED = 16;\n\n/**\n * Whether a press on a header may start a reorder drag: the primary button of a\n * mouse or pen (a touch pans the table instead), not on a resize handle, and not\n * in interactive content slotted into the header.\n */\nexport function canStartReorder(down: PointerEvent, cell: HTMLElement): boolean {\n if (down.button !== 0 || down.pointerType === 'touch') return false;\n for (const target of down.composedPath()) {\n if (target === cell) return true;\n if (!(target instanceof Element)) continue;\n if (target instanceof HTMLElement && target.dataset.resizeCol !== undefined) return false;\n if (target.matches(INTERACTIVE)) return false;\n }\n return false;\n}\n\n/**\n * One pointer drag of a column header. It becomes a drag after a few pixels of\n * movement; until then the press is a click. While dragging, the pointer is\n * captured by the header, the target follows the pointer once per frame, and the\n * scroll area scrolls near its edges. Escape cancels. The click that ends a drag\n * is swallowed, so it neither sorts nor counts as a cell click.\n */\nexport function startReorderDrag(\n cell: HTMLElement,\n down: PointerEvent,\n options: ReorderDragOptions,\n): void {\n const view = cell.ownerDocument.defaultView ?? window;\n const startX = down.clientX;\n const startY = down.clientY;\n let x = startX;\n let dragging = false;\n let frame = 0;\n let target: DropTarget | undefined;\n\n const same = (a: DropTarget | undefined, b: DropTarget | undefined) =>\n a?.to === b?.to && a?.columnId === b?.columnId && a?.edge === b?.edge;\n\n const tick = () => {\n frame = 0;\n if (!dragging) return;\n const area = options.scrollArea();\n if (area) {\n const speed =\n x < area.left + EDGE\n ? -Math.min(1, (area.left + EDGE - x) / EDGE)\n : x > area.right - EDGE\n ? Math.min(1, (x - (area.right - EDGE)) / EDGE)\n : 0;\n // A visual delta: scrollLeft runs the same way in LTR and RTL.\n if (speed !== 0) options.scroller.scrollLeft += Math.round(speed * MAX_SPEED);\n // Targets past the visible part would be hidden under pinned columns.\n x = Math.min(Math.max(x, area.left), area.right);\n }\n const next = options.targetAt(x);\n if (!same(next, target)) {\n target = next;\n options.onTarget(target);\n }\n // Keep running while dragging: the view may still be scrolling.\n frame = view.requestAnimationFrame(tick);\n };\n\n const listeners = new AbortController();\n const cleanup = (event?: PointerEvent) => {\n if (frame) view.cancelAnimationFrame(frame);\n frame = 0;\n listeners.abort();\n if (event && cell.hasPointerCapture(event.pointerId)) {\n cell.releasePointerCapture(event.pointerId);\n }\n };\n\n const move = (event: PointerEvent) => {\n if (event.pointerId !== down.pointerId) return;\n x = event.clientX;\n if (!dragging) {\n if (Math.hypot(x - startX, event.clientY - startY) < THRESHOLD) return;\n dragging = true;\n try {\n cell.setPointerCapture(event.pointerId);\n } catch {\n // Synthetic pointers can't be captured; the window listeners still see the moves.\n }\n options.onStart();\n }\n // No text selection while dragging.\n event.preventDefault();\n frame ||= view.requestAnimationFrame(tick);\n };\n\n const up = (event: PointerEvent) => {\n if (event.pointerId !== down.pointerId) return;\n cleanup(event);\n if (!dragging) return;\n x = event.clientX;\n const area = options.scrollArea();\n if (area) x = Math.min(Math.max(x, area.left), area.right);\n swallowClick(view);\n options.onDrop(options.targetAt(x));\n };\n\n const cancel = (event: PointerEvent) => {\n if (event.pointerId !== down.pointerId) return;\n cleanup(event);\n if (dragging) options.onCancel();\n };\n\n const key = (event: KeyboardEvent) => {\n if (!dragging || event.key !== 'Escape') return;\n event.preventDefault();\n event.stopPropagation();\n cleanup();\n // The pointer is still down: its release must not count as a click.\n swallowClick(view, false);\n options.onCancel();\n };\n\n const listen = { capture: true, signal: listeners.signal };\n view.addEventListener('pointermove', move, listen);\n view.addEventListener('pointerup', up, listen);\n view.addEventListener('pointercancel', cancel, listen);\n view.addEventListener('keydown', key, listen);\n}\n\n/**\n * Stops the click that follows the release of a drag, if one comes. It comes in\n * the same task as the release, so the guard goes once that task is over. After\n * Escape the pointer is still down (`released: false`): the guard waits for its release.\n */\nfunction swallowClick(view: Window, released = true): void {\n const stop = (event: MouseEvent) => {\n event.preventDefault();\n event.stopPropagation();\n };\n const remove = () => {\n setTimeout(() => {\n view.removeEventListener('click', stop, { capture: true });\n });\n };\n view.addEventListener('click', stop, { capture: true, once: true });\n if (released) remove();\n else view.addEventListener('pointerup', remove, { capture: true, once: true });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"reorder-drag.js","names":[],"sources":["../../src/controllers/reorder-drag.ts"],"sourcesContent":["import { INTERACTIVE } from '../interactive.js';\n\n/** Where a dragged column or group would land: its new position and the header that shows the drop line. */\nexport interface DropTarget {\n /** Position among its siblings after the move, 0-based. */\n to: number;\n /** The slot (a column or group header) that draws the drop line. */\n id: string;\n /** The edge of that header the line is on, in logical terms. */\n edge: 'start' | 'end';\n}\n\n/** A header of what the dragged column or group moves among, in visual order. */\nexport interface DropSlot {\n id: string;\n /** Its position among them, 0-based. */\n index: number;\n rect: { left: number; right: number };\n}\n\n/**\n * The drop target for a pointer at `x`: the gap between headers nearest to it,\n * decided by each header's midpoint. `undefined` where the column would stay\n * put. `direction` is 1 in LTR and -1 in RTL, where the group runs right to left.\n * The slots needn't be next to each other in the order: with\n * `virtualizeColumns`, headers out of view aren't rendered.\n */\nexport function dropTarget(\n slots: readonly DropSlot[],\n draggedId: string,\n x: number,\n direction: 1 | -1,\n): DropTarget | undefined {\n const dragged = slots.find((s) => s.id === draggedId);\n const last = slots.at(-1);\n if (!dragged || !last) return undefined;\n // The gap before the first header whose middle is past the pointer, in reading order.\n const at = slots.find((s) => (x - (s.rect.left + s.rect.right) / 2) * direction < 0);\n if (!at) {\n // Past the last header: after it.\n const to = last.index >= dragged.index ? last.index : last.index + 1;\n return to === dragged.index ? undefined : { to, id: last.id, edge: 'end' };\n }\n // Taking the column out shifts the ones after it one place back.\n const to = at.index > dragged.index ? at.index - 1 : at.index;\n return to === dragged.index ? undefined : { to, id: at.id, edge: 'start' };\n}\n\nexport interface ReorderDragOptions {\n /** The scroll container: it scrolls while the pointer is near its edges. */\n scroller: HTMLElement;\n /** The drop target for a pointer at `x` (client coordinates). */\n targetAt: (x: number) => DropTarget | undefined;\n /**\n * Where the dragged column's group is visible, in client x, if it can scroll\n * (the scrolling columns). Near these edges the scroller scrolls with the drag.\n */\n scrollArea: () => { left: number; right: number } | undefined;\n onStart: () => void;\n /** The target changed (`undefined`: the column would stay put). */\n onTarget: (target: DropTarget | undefined) => void;\n /** The pointer was released over a target, or `undefined` when it would stay put. */\n onDrop: (target: DropTarget | undefined) => void;\n /** Escape, a cancelled pointer, or a release without a drag. */\n onCancel: () => void;\n}\n\n/** Movement before a press becomes a drag, so a click still sorts. */\nconst THRESHOLD = 4;\n/** Width of the band at the scroll area's edges that scrolls, and the top speed there. */\nconst EDGE = 40;\nconst MAX_SPEED = 16;\n\n/**\n * Whether a press on a header may start a reorder drag: the primary button of a\n * mouse or pen (a touch pans the table instead), not on a resize handle, and not\n * in interactive content slotted into the header.\n */\nexport function canStartReorder(down: PointerEvent, cell: HTMLElement): boolean {\n if (down.button !== 0 || down.pointerType === 'touch') return false;\n for (const target of down.composedPath()) {\n if (target === cell) return true;\n if (!(target instanceof Element)) continue;\n if (target instanceof HTMLElement && target.dataset.resizeCol !== undefined) return false;\n if (target.matches(INTERACTIVE)) return false;\n }\n return false;\n}\n\n/**\n * One pointer drag of a column header. It becomes a drag after a few pixels of\n * movement; until then the press is a click. While dragging, the pointer is\n * captured by the header, the target follows the pointer once per frame, and the\n * scroll area scrolls near its edges. Escape cancels. The click that ends a drag\n * is swallowed, so it neither sorts nor counts as a cell click.\n */\nexport function startReorderDrag(\n cell: HTMLElement,\n down: PointerEvent,\n options: ReorderDragOptions,\n): void {\n const view = cell.ownerDocument.defaultView ?? window;\n const startX = down.clientX;\n const startY = down.clientY;\n let x = startX;\n let dragging = false;\n let frame = 0;\n let target: DropTarget | undefined;\n\n const same = (a: DropTarget | undefined, b: DropTarget | undefined) =>\n a?.to === b?.to && a?.id === b?.id && a?.edge === b?.edge;\n\n const tick = () => {\n frame = 0;\n if (!dragging) return;\n const area = options.scrollArea();\n if (area) {\n const speed =\n x < area.left + EDGE\n ? -Math.min(1, (area.left + EDGE - x) / EDGE)\n : x > area.right - EDGE\n ? Math.min(1, (x - (area.right - EDGE)) / EDGE)\n : 0;\n // A visual delta: scrollLeft runs the same way in LTR and RTL.\n if (speed !== 0) options.scroller.scrollLeft += Math.round(speed * MAX_SPEED);\n // Targets past the visible part would be hidden under pinned columns.\n x = Math.min(Math.max(x, area.left), area.right);\n }\n const next = options.targetAt(x);\n if (!same(next, target)) {\n target = next;\n options.onTarget(target);\n }\n // Keep running while dragging: the view may still be scrolling.\n frame = view.requestAnimationFrame(tick);\n };\n\n const listeners = new AbortController();\n const cleanup = (event?: PointerEvent) => {\n if (frame) view.cancelAnimationFrame(frame);\n frame = 0;\n listeners.abort();\n if (event && cell.hasPointerCapture(event.pointerId)) {\n cell.releasePointerCapture(event.pointerId);\n }\n };\n\n const move = (event: PointerEvent) => {\n if (event.pointerId !== down.pointerId) return;\n x = event.clientX;\n if (!dragging) {\n if (Math.hypot(x - startX, event.clientY - startY) < THRESHOLD) return;\n dragging = true;\n try {\n cell.setPointerCapture(event.pointerId);\n } catch {\n // Synthetic pointers can't be captured; the window listeners still see the moves.\n }\n options.onStart();\n }\n // No text selection while dragging.\n event.preventDefault();\n frame ||= view.requestAnimationFrame(tick);\n };\n\n const up = (event: PointerEvent) => {\n if (event.pointerId !== down.pointerId) return;\n cleanup(event);\n if (!dragging) return;\n x = event.clientX;\n const area = options.scrollArea();\n if (area) x = Math.min(Math.max(x, area.left), area.right);\n swallowClick(view);\n options.onDrop(options.targetAt(x));\n };\n\n const cancel = (event: PointerEvent) => {\n if (event.pointerId !== down.pointerId) return;\n cleanup(event);\n if (dragging) options.onCancel();\n };\n\n const key = (event: KeyboardEvent) => {\n if (!dragging || event.key !== 'Escape') return;\n event.preventDefault();\n event.stopPropagation();\n cleanup();\n // The pointer is still down: its release must not count as a click.\n swallowClick(view, false);\n options.onCancel();\n };\n\n const listen = { capture: true, signal: listeners.signal };\n view.addEventListener('pointermove', move, listen);\n view.addEventListener('pointerup', up, listen);\n view.addEventListener('pointercancel', cancel, listen);\n view.addEventListener('keydown', key, listen);\n}\n\n/**\n * Stops the click that follows the release of a drag, if one comes. It comes in\n * the same task as the release, so the guard goes once that task is over. After\n * Escape the pointer is still down (`released: false`): the guard waits for its release.\n */\nfunction swallowClick(view: Window, released = true): void {\n const stop = (event: MouseEvent) => {\n event.preventDefault();\n event.stopPropagation();\n };\n const remove = () => {\n setTimeout(() => {\n view.removeEventListener('click', stop, { capture: true });\n });\n };\n view.addEventListener('click', stop, { capture: true, once: true });\n if (released) remove();\n else view.addEventListener('pointerup', remove, { capture: true, once: true });\n}\n"],"mappings":"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"}
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* rendered, and watches them for changes the table doesn't render (an image
|
|
6
6
|
* loading, a web font, a framework component updating), which request an
|
|
7
7
|
* update that reads them again. Pooled rows show other rows as the body
|
|
8
|
-
* scrolls, so
|
|
8
|
+
* scrolls, so each update reads every rendered row that shows another row than
|
|
9
|
+
* at its last read (all of them after `invalidate()`): one that now shows a row
|
|
9
10
|
* of the same height doesn't resize, and the observer wouldn't report it.
|
|
10
11
|
*/
|
|
11
12
|
var RowMeasure = class {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"row-measure.js","names":[],"sources":["../../src/controllers/row-measure.ts"],"sourcesContent":["import type { ReactiveControllerHost } from 'lit';\n\n/** The heights of the pinned row groups, in px. */\nexport interface PinnedHeights {\n top: number;\n bottom: number;\n}\n\nexport interface RowMeasureOptions {\n /** A body row element's index in the row order; `undefined` to leave it out (a row still loading). */\n bodyIndex: (row: Element) => number | undefined;\n /** The height the virtualizer has for a body row now. */\n sizeOf: (index: number) => number;\n /** The least a row is (the row height). */\n minimum: () => number;\n}\n\n/**\n * Rows of different heights (`rowHeight: 'auto'`): reads the heights of the\n * rendered body rows and of the pinned row groups once their content has\n * rendered, and watches them for changes the table doesn't render (an image\n * loading, a web font, a framework component updating), which request an\n * update that reads them again. Pooled rows show other rows as the body\n * scrolls, so
|
|
1
|
+
{"version":3,"file":"row-measure.js","names":[],"sources":["../../src/controllers/row-measure.ts"],"sourcesContent":["import type { ReactiveControllerHost } from 'lit';\n\n/** The heights of the pinned row groups, in px. */\nexport interface PinnedHeights {\n top: number;\n bottom: number;\n}\n\nexport interface RowMeasureOptions {\n /** A body row element's index in the row order; `undefined` to leave it out (a row still loading). */\n bodyIndex: (row: Element) => number | undefined;\n /** The height the virtualizer has for a body row now. */\n sizeOf: (index: number) => number;\n /** The least a row is (the row height). */\n minimum: () => number;\n}\n\n/**\n * Rows of different heights (`rowHeight: 'auto'`): reads the heights of the\n * rendered body rows and of the pinned row groups once their content has\n * rendered, and watches them for changes the table doesn't render (an image\n * loading, a web font, a framework component updating), which request an\n * update that reads them again. Pooled rows show other rows as the body\n * scrolls, so each update reads every rendered row that shows another row than\n * at its last read (all of them after `invalidate()`): one that now shows a row\n * of the same height doesn't resize, and the observer wouldn't report it.\n */\nexport class RowMeasure {\n readonly #host: ReactiveControllerHost;\n readonly #options: RowMeasureOptions;\n #observer: ResizeObserver | undefined;\n readonly #observed = new Set<Element>();\n #pinned: PinnedHeights = { top: 0, bottom: 0 };\n /** The row each body row element showed when it was last read. */\n #read = new WeakMap<Element, number>();\n /** Read every row next time, not only those that show another row now. */\n #all = true;\n\n constructor(host: ReactiveControllerHost, options: RowMeasureOptions) {\n this.#host = host;\n this.#options = options;\n }\n\n /** The pinned groups' heights from the last `read()`. */\n get pinned(): PinnedHeights {\n return this.#pinned;\n }\n\n /**\n * The next `read()` reads every row: their content may have changed size\n * (new data or columns, a resize, content that grew).\n */\n invalidate(): void {\n this.#all = true;\n }\n\n /**\n * Reads the rows in `root` (the shadow root): the body rows' heights as\n * `[index, px]`, and whether the pinned groups changed height. Scrolling,\n * only the pooled rows that now show another row are read.\n */\n read(root: ParentNode): { rows: [number, number][]; pinnedChanged: boolean } {\n const observer = (this.#observer ??= new ResizeObserver(this.#onResize));\n for (const element of this.#observed) {\n if (!element.isConnected) {\n observer.unobserve(element);\n this.#observed.delete(element);\n }\n }\n const rows: [number, number][] = [];\n const due: [Element, number][] = [];\n for (const row of root.querySelectorAll('[part~=\"body\"] > .row')) {\n this.#observe(row);\n const index = this.#options.bodyIndex(row);\n if (index === undefined) {\n this.#read.delete(row);\n } else if (this.#all || this.#read.get(row) !== index) {\n due.push([row, index]);\n }\n }\n this.#all = false;\n // Body rows render at least their measured height (on the px grid): drop it\n // for one synchronous read, so rows can shrink too. Never painted.\n for (const [row] of due) row.setAttribute('data-measure-row', '');\n for (const [row, index] of due) {\n rows.push([index, row.getBoundingClientRect().height]);\n this.#read.set(row, index);\n }\n for (const [row] of due) row.removeAttribute('data-measure-row');\n const group = (side: 'top' | 'bottom') => {\n const element = root.querySelector(`.pinned[data-side=\"${side}\"]`);\n if (element) this.#observe(element);\n return element ? element.getBoundingClientRect().height : 0;\n };\n const pinned = { top: group('top'), bottom: group('bottom') };\n const pinnedChanged =\n Math.abs(pinned.top - this.#pinned.top) > 0.01 ||\n Math.abs(pinned.bottom - this.#pinned.bottom) > 0.01;\n if (pinnedChanged) this.#pinned = pinned;\n return { rows, pinnedChanged };\n }\n\n /** Stops watching (fixed row heights again, or the table left the page). */\n stop(): void {\n this.#observer?.disconnect();\n this.#observed.clear();\n this.#pinned = { top: 0, bottom: 0 };\n this.#all = true;\n }\n\n #observe(element: Element): void {\n if (this.#observed.has(element)) return;\n this.#observed.add(element);\n this.#observer?.observe(element);\n }\n\n /** Requests an update only for a height the table doesn't have yet. */\n readonly #onResize = (entries: ResizeObserverEntry[]) => {\n const { bodyIndex, sizeOf, minimum } = this.#options;\n for (const entry of entries) {\n const target = entry.target;\n if (!target.isConnected) continue;\n const px = entry.borderBoxSize[0]?.blockSize ?? target.getBoundingClientRect().height;\n let known: number | undefined;\n if (target.classList.contains('pinned')) {\n known = target.getAttribute('data-side') === 'top' ? this.#pinned.top : this.#pinned.bottom;\n } else {\n const index = bodyIndex(target);\n if (index === undefined) continue;\n known = sizeOf(index);\n // Rows render at least their known height: only growing shows here.\n if (px < minimum() || px <= known + 0.01) continue;\n }\n if (Math.abs(px - known) > 0.01) {\n this.#all = true;\n this.#host.requestUpdate();\n return;\n }\n }\n };\n}\n"],"mappings":"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"}
|
|
@@ -175,12 +175,17 @@ var VirtualController = class {
|
|
|
175
175
|
});
|
|
176
176
|
if (next.rowHeight !== prev.rowHeight) this.#virtual.getVirtualizer().measure();
|
|
177
177
|
}
|
|
178
|
+
this.#rescale(next.count * next.rowHeight, this.#virtual.getVirtualizer().scrollOffset);
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* A new logical body height. A new mapping would move the rows under a scaled
|
|
182
|
+
* scroll position (a data source growing, a zoom change): keep the logical
|
|
183
|
+
* offset (read before the new mapping) once the body has its new height.
|
|
184
|
+
*/
|
|
185
|
+
#rescale(logical, offset) {
|
|
178
186
|
const wasScaled = this.#scale.excess > 0;
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (this.#scale.configure(logical, next.cap(logical))) {
|
|
182
|
-
if ((wasScaled || this.#scale.scaled) && offset !== null && offset > 0) this.#anchor = offset;
|
|
183
|
-
}
|
|
187
|
+
if (!this.#scale.configure(logical, this.#config.cap(logical))) return;
|
|
188
|
+
if ((wasScaled || this.#scale.scaled) && offset !== null && offset > 0) this.#anchor = offset;
|
|
184
189
|
}
|
|
185
190
|
#configureAuto(auto, prev) {
|
|
186
191
|
const next = this.#config;
|
|
@@ -203,12 +208,7 @@ var VirtualController = class {
|
|
|
203
208
|
reset();
|
|
204
209
|
}
|
|
205
210
|
const element = this.#element;
|
|
206
|
-
|
|
207
|
-
const offset = element ? this.#top(element) : 0;
|
|
208
|
-
const logical = sizes.total;
|
|
209
|
-
if (this.#scale.configure(logical, next.cap(logical))) {
|
|
210
|
-
if ((wasScaled || this.#scale.scaled) && offset > 0) this.#anchor = offset;
|
|
211
|
-
}
|
|
211
|
+
this.#rescale(sizes.total, element ? this.#top(element) : 0);
|
|
212
212
|
}
|
|
213
213
|
/** The measured heights by index in the current order. Forgets rows long gone. */
|
|
214
214
|
*#known(auto, count) {
|
|
@@ -325,14 +325,7 @@ var VirtualController = class {
|
|
|
325
325
|
start: scaled ? Math.min(Math.max(start, -rowHeight), size) : start
|
|
326
326
|
});
|
|
327
327
|
}
|
|
328
|
-
const rows =
|
|
329
|
-
for (const { id, slot } of this.#pool.update(byId.keys())) {
|
|
330
|
-
const item = byId.get(id);
|
|
331
|
-
if (item) rows.push({
|
|
332
|
-
slot,
|
|
333
|
-
...item
|
|
334
|
-
});
|
|
335
|
-
}
|
|
328
|
+
const rows = this.#pooled(byId);
|
|
336
329
|
const range = v.range;
|
|
337
330
|
return {
|
|
338
331
|
rows,
|
|
@@ -343,6 +336,18 @@ var VirtualController = class {
|
|
|
343
336
|
} : null
|
|
344
337
|
};
|
|
345
338
|
}
|
|
339
|
+
/** The rows to render, in pool order: each holds its pool slot while in range. */
|
|
340
|
+
#pooled(byId) {
|
|
341
|
+
const rows = [];
|
|
342
|
+
for (const { id, slot } of this.#pool.update(byId.keys())) {
|
|
343
|
+
const item = byId.get(id);
|
|
344
|
+
if (item) rows.push({
|
|
345
|
+
slot,
|
|
346
|
+
...item
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
return rows;
|
|
350
|
+
}
|
|
346
351
|
/** `computeWindow()` for rows of different heights. */
|
|
347
352
|
#autoWindow() {
|
|
348
353
|
const { count, overscan, rowHeight, keep, rowIdAt } = this.#config;
|
|
@@ -382,16 +387,8 @@ var VirtualController = class {
|
|
|
382
387
|
size
|
|
383
388
|
});
|
|
384
389
|
}
|
|
385
|
-
const rows = [];
|
|
386
|
-
for (const { id, slot } of this.#pool.update(byId.keys())) {
|
|
387
|
-
const item = byId.get(id);
|
|
388
|
-
if (item) rows.push({
|
|
389
|
-
slot,
|
|
390
|
-
...item
|
|
391
|
-
});
|
|
392
|
-
}
|
|
393
390
|
return {
|
|
394
|
-
rows,
|
|
391
|
+
rows: this.#pooled(byId),
|
|
395
392
|
totalSize: scale.scaled ? scale.size : sizes.total,
|
|
396
393
|
range: visible ? {
|
|
397
394
|
start: visible.start,
|
|
@@ -456,10 +453,6 @@ var VirtualController = class {
|
|
|
456
453
|
if (sizes.startOf(to) < start - view - .01) to += 1;
|
|
457
454
|
return Math.max(1, from - to);
|
|
458
455
|
}
|
|
459
|
-
/** Number of pool slots created so far: bounded by the most rows rendered at once. */
|
|
460
|
-
get poolSize() {
|
|
461
|
-
return this.#pool.size;
|
|
462
|
-
}
|
|
463
456
|
/** Whether the body is taller than the cap and scrolls scaled. */
|
|
464
457
|
get scaled() {
|
|
465
458
|
return this.#scale.scaled;
|