argus-table 0.1.0 → 0.2.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/README.md +25 -2
- package/dist/announce.js +27 -0
- package/dist/announce.js.map +1 -0
- package/dist/argus-column.js +121 -0
- package/dist/argus-column.js.map +1 -0
- package/dist/argus-table.js +585 -83
- package/dist/argus-table.js.map +1 -1
- package/dist/cdn/argus-table.js +79 -4533
- package/dist/cdn/argus-table.js.map +1 -1
- package/dist/controllers/auto-width.js +89 -0
- package/dist/controllers/auto-width.js.map +1 -0
- package/dist/controllers/column-children.js +41 -0
- package/dist/controllers/column-children.js.map +1 -0
- package/dist/controllers/data-controller.js +177 -0
- package/dist/controllers/data-controller.js.map +1 -0
- package/dist/controllers/reorder-drag.js +160 -0
- package/dist/controllers/reorder-drag.js.map +1 -0
- package/dist/controllers/resize-drag.js +3 -0
- package/dist/controllers/resize-drag.js.map +1 -1
- package/dist/controllers/table-controller.js +2 -1
- package/dist/controllers/table-controller.js.map +1 -1
- package/dist/controllers/virtual-controller.js +146 -9
- package/dist/controllers/virtual-controller.js.map +1 -1
- package/dist/core/src/columns/column-order.js +50 -0
- package/dist/core/src/columns/column-order.js.map +1 -0
- package/dist/core/src/columns/resolve-columns.js +5 -1
- package/dist/core/src/columns/resolve-columns.js.map +1 -1
- package/dist/core/src/data/page-cache.js +143 -0
- package/dist/core/src/data/page-cache.js.map +1 -0
- package/dist/core/src/grid/row-layout.js +58 -0
- package/dist/core/src/grid/row-layout.js.map +1 -0
- package/dist/core/src/table/create-table.js +112 -14
- package/dist/core/src/table/create-table.js.map +1 -1
- package/dist/core/src/table/sorted-row-model.js +86 -0
- package/dist/core/src/table/sorted-row-model.js.map +1 -0
- package/dist/core/src/virtual/scroll-scale.js +74 -0
- package/dist/core/src/virtual/scroll-scale.js.map +1 -0
- package/dist/custom-elements.json +1463 -62
- package/dist/events.js +5 -1
- package/dist/events.js.map +1 -1
- package/dist/index.d.ts +252 -27
- package/dist/index.js +3 -2
- package/dist/max-height.js +68 -0
- package/dist/max-height.js.map +1 -0
- package/dist/render/body.js +53 -20
- package/dist/render/body.js.map +1 -1
- package/dist/render/cells.js +21 -4
- package/dist/render/cells.js.map +1 -1
- package/dist/render/content.js +12 -5
- package/dist/render/content.js.map +1 -1
- package/dist/render/header.js +21 -4
- package/dist/render/header.js.map +1 -1
- package/dist/render/pinned-rows.js +29 -0
- package/dist/render/pinned-rows.js.map +1 -0
- package/dist/styles/base.js +1 -1
- package/dist/styles/base.js.map +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// src/controllers/auto-width.ts
|
|
2
|
+
/**
|
|
3
|
+
* The width each column's content needs, in px, from the cells rendered now:
|
|
4
|
+
* the header, the pinned rows and the rendered body rows. Rows that haven't
|
|
5
|
+
* loaded are skipped (their skeleton bars say nothing about the content).
|
|
6
|
+
*
|
|
7
|
+
* Every cell of the columns gets `data-measure`, which sizes it to its
|
|
8
|
+
* `max-content` (base styles), so the browser accounts for padding, the sort
|
|
9
|
+
* indicator and whatever the content is. One forced layout, no paint: the
|
|
10
|
+
* attribute is gone before the frame ends. A column with nothing laid out
|
|
11
|
+
* (the table is hidden) is left out.
|
|
12
|
+
*/
|
|
13
|
+
function measureColumns(root, ids) {
|
|
14
|
+
const wanted = new Set(ids);
|
|
15
|
+
const widths = /* @__PURE__ */ new Map();
|
|
16
|
+
if (wanted.size === 0) return widths;
|
|
17
|
+
const cells = [...root.querySelectorAll("[data-col]")].filter((cell) => wanted.has(cell.dataset.col ?? "") && !cell.parentElement?.matches("[part~=\"row-loading\"]"));
|
|
18
|
+
for (const cell of cells) cell.setAttribute("data-measure", "");
|
|
19
|
+
for (const cell of cells) {
|
|
20
|
+
const id = cell.dataset.col ?? "";
|
|
21
|
+
const px = Math.ceil(cell.getBoundingClientRect().width);
|
|
22
|
+
if (px > 0 && px > (widths.get(id) ?? 0)) widths.set(id, px);
|
|
23
|
+
}
|
|
24
|
+
for (const cell of cells) cell.removeAttribute("data-measure");
|
|
25
|
+
return widths;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Widths of `width: 'auto'` columns. A column measures once its first loaded
|
|
29
|
+
* rows render, and before that fits the header and pinned rows. So each column
|
|
30
|
+
* is measured at most twice (three times if fonts were still loading), and a
|
|
31
|
+
* table that stays empty doesn't measure on every update. State is keyed by
|
|
32
|
+
* column id, so new `columns` arrays with the same ids don't measure again.
|
|
33
|
+
*/
|
|
34
|
+
var AutoWidths = class {
|
|
35
|
+
#state;
|
|
36
|
+
#fontsPending;
|
|
37
|
+
#requestUpdate;
|
|
38
|
+
constructor(requestUpdate) {
|
|
39
|
+
this.widths = {};
|
|
40
|
+
this.#state = /* @__PURE__ */ new Map();
|
|
41
|
+
this.#fontsPending = false;
|
|
42
|
+
this.#requestUpdate = requestUpdate;
|
|
43
|
+
}
|
|
44
|
+
/** Forgets columns that are gone or no longer `'auto'`, given the ids of the auto columns. */
|
|
45
|
+
sync(ids) {
|
|
46
|
+
const auto = new Set(ids);
|
|
47
|
+
for (const id of this.#state.keys()) if (!auto.has(id)) this.#state.delete(id);
|
|
48
|
+
const kept = Object.entries(this.widths).filter(([id]) => auto.has(id));
|
|
49
|
+
if (kept.length !== Object.keys(this.widths).length) this.widths = Object.fromEntries(kept);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Measures the auto columns that need it. Returns `true` if a width changed;
|
|
53
|
+
* the caller then renders again (the one extra pass of the render cycle).
|
|
54
|
+
*/
|
|
55
|
+
measure(root, columns, hasLoadedRows) {
|
|
56
|
+
const due = columns.filter((c) => c.auto).map((c) => c.id).filter((id) => {
|
|
57
|
+
const state = this.#state.get(id);
|
|
58
|
+
return state === void 0 || state === "provisional" && hasLoadedRows;
|
|
59
|
+
});
|
|
60
|
+
if (due.length === 0) return false;
|
|
61
|
+
const measured = measureColumns(root, due);
|
|
62
|
+
const fontsLoading = typeof document !== "undefined" && document.fonts.status === "loading";
|
|
63
|
+
let next;
|
|
64
|
+
for (const [id, px] of measured) {
|
|
65
|
+
this.#state.set(id, fontsLoading ? "fonts" : hasLoadedRows ? "final" : "provisional");
|
|
66
|
+
if (this.widths[id] !== px) (next ??= { ...this.widths })[id] = px;
|
|
67
|
+
}
|
|
68
|
+
if (fontsLoading && measured.size > 0) this.#remeasureAfterFonts();
|
|
69
|
+
if (next) this.widths = next;
|
|
70
|
+
return next !== void 0;
|
|
71
|
+
}
|
|
72
|
+
/** Once web fonts are ready, columns measured meanwhile measure again. */
|
|
73
|
+
#remeasureAfterFonts() {
|
|
74
|
+
if (this.#fontsPending) return;
|
|
75
|
+
this.#fontsPending = true;
|
|
76
|
+
document.fonts.ready.then(() => {
|
|
77
|
+
this.#fontsPending = false;
|
|
78
|
+
let any = false;
|
|
79
|
+
for (const [id, state] of this.#state) if (state === "fonts") {
|
|
80
|
+
this.#state.delete(id);
|
|
81
|
+
any = true;
|
|
82
|
+
}
|
|
83
|
+
if (any) this.#requestUpdate();
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
export { AutoWidths, measureColumns };
|
|
88
|
+
|
|
89
|
+
//# sourceMappingURL=auto-width.js.map
|
|
@@ -0,0 +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 * 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 = columns\n .filter((c) => c.auto)\n .map((c) => c.id)\n .filter((id) => {\n const state = this.#state.get(id);\n return state === undefined || (state === 'provisional' && hasLoadedRows);\n });\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,QACE,MACA,SACA,eACS;EACT,MAAM,MAAM,QACT,QAAQ,MAAM,EAAE,IAAI,CAAC,CACrB,KAAK,MAAM,EAAE,EAAE,CAAC,CAChB,QAAQ,OAAO;GACd,MAAM,QAAQ,KAAK,OAAO,IAAI,EAAE;GAChC,OAAO,UAAU,KAAA,KAAc,UAAU,iBAAiB;EAC5D,CAAC;EACH,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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { ArgusColumn, unwatchColumns, watchColumns } from "../argus-column.js";
|
|
2
|
+
// src/controllers/column-children.ts
|
|
3
|
+
/**
|
|
4
|
+
* Reads the host's `<argus-column>` children into `ColumnDef`s. The columns tell
|
|
5
|
+
* the host when they change (`watchColumns`); the list is rebuilt only then, so
|
|
6
|
+
* `defs` keeps its identity between changes.
|
|
7
|
+
*/
|
|
8
|
+
var ColumnChildrenController = class {
|
|
9
|
+
#host;
|
|
10
|
+
#defs = [];
|
|
11
|
+
#dirty = true;
|
|
12
|
+
constructor(host) {
|
|
13
|
+
this.#host = host;
|
|
14
|
+
host.addController(this);
|
|
15
|
+
}
|
|
16
|
+
hostConnected() {
|
|
17
|
+
this.#dirty = true;
|
|
18
|
+
watchColumns(this.#host, () => {
|
|
19
|
+
this.#dirty = true;
|
|
20
|
+
this.#host.requestUpdate();
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
hostDisconnected() {
|
|
24
|
+
unwatchColumns(this.#host);
|
|
25
|
+
}
|
|
26
|
+
/** Whether the host has any `<argus-column>` child. */
|
|
27
|
+
get present() {
|
|
28
|
+
return this.defs.length > 0;
|
|
29
|
+
}
|
|
30
|
+
/** The children's columns, in document order. */
|
|
31
|
+
get defs() {
|
|
32
|
+
if (this.#dirty) {
|
|
33
|
+
this.#dirty = false;
|
|
34
|
+
this.#defs = [...this.#host.children].filter((c) => c instanceof ArgusColumn).map((c) => c.toColumnDef());
|
|
35
|
+
}
|
|
36
|
+
return this.#defs;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
export { ColumnChildrenController };
|
|
40
|
+
|
|
41
|
+
//# sourceMappingURL=column-children.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"column-children.js","names":[],"sources":["../../src/controllers/column-children.ts"],"sourcesContent":["import type { ReactiveController, ReactiveControllerHost } from 'lit';\nimport { ArgusColumn, unwatchColumns, watchColumns } from '../argus-column.js';\nimport type { ColumnDef } from '../types.js';\n\n/**\n * Reads the host's `<argus-column>` children into `ColumnDef`s. The columns tell\n * the host when they change (`watchColumns`); the list is rebuilt only then, so\n * `defs` keeps its identity between changes.\n */\nexport class ColumnChildrenController<T> implements ReactiveController {\n readonly #host: ReactiveControllerHost & HTMLElement;\n #defs: readonly ColumnDef<T>[] = [];\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. */\n get defs(): readonly ColumnDef<T>[] {\n if (this.#dirty) {\n this.#dirty = false;\n this.#defs = [...this.#host.children]\n .filter((c): c is ArgusColumn<T> => c instanceof ArgusColumn)\n .map((c) => c.toColumnDef());\n }\n return this.#defs;\n }\n}\n"],"mappings":";;;;;;;AASA,IAAa,2BAAb,MAAuE;CACrE;CACA,QAAiC,CAAC;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,IAAI,KAAK,QAAQ;GACf,KAAK,SAAS;GACd,KAAK,QAAQ,CAAC,GAAG,KAAK,MAAM,QAAQ,CAAC,CAClC,QAAQ,MAA2B,aAAa,WAAW,CAAC,CAC5D,KAAK,MAAM,EAAE,YAAY,CAAC;EAC/B;EACA,OAAO,KAAK;CACd;AACF"}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { PageCache } from "../core/src/data/page-cache.js";
|
|
2
|
+
/**
|
|
3
|
+
* Loads `dataSource` pages for the rendered rows (infinite loading). It owns the
|
|
4
|
+
* page cache: a new source, sort order or row id function starts a fresh one, and
|
|
5
|
+
* answers for an old cache are dropped. Requests wait until the range has been
|
|
6
|
+
* quiet for `LOAD_DEBOUNCE_MS`; pages that scrolled away are aborted.
|
|
7
|
+
*/
|
|
8
|
+
var DataController = class {
|
|
9
|
+
#host;
|
|
10
|
+
#config = {
|
|
11
|
+
source: void 0,
|
|
12
|
+
sorting: [],
|
|
13
|
+
getRowId: void 0
|
|
14
|
+
};
|
|
15
|
+
#cache;
|
|
16
|
+
#requests = /* @__PURE__ */ new Map();
|
|
17
|
+
#range;
|
|
18
|
+
#timer;
|
|
19
|
+
#errors = [];
|
|
20
|
+
constructor(host) {
|
|
21
|
+
this.#host = host;
|
|
22
|
+
host.addController(this);
|
|
23
|
+
}
|
|
24
|
+
hostDisconnected() {
|
|
25
|
+
clearTimeout(this.#timer);
|
|
26
|
+
this.#timer = void 0;
|
|
27
|
+
this.#abortAll();
|
|
28
|
+
}
|
|
29
|
+
hostConnected() {
|
|
30
|
+
this.#range = void 0;
|
|
31
|
+
}
|
|
32
|
+
/** The page cache, while a data source is set: the model reads rows from it. */
|
|
33
|
+
get rows() {
|
|
34
|
+
return this.#cache;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Takes the source, sort order and row id function. Any change starts a fresh
|
|
38
|
+
* cache; a sort or row id change keeps the row count (the rows are the same).
|
|
39
|
+
*/
|
|
40
|
+
configure(next) {
|
|
41
|
+
const prev = this.#config;
|
|
42
|
+
this.#config = next;
|
|
43
|
+
if (!next.source) {
|
|
44
|
+
this.#reset(void 0);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (next.source !== prev.source || !this.#cache) this.#reset({
|
|
48
|
+
rowCount: void 0,
|
|
49
|
+
totalKnown: false
|
|
50
|
+
});
|
|
51
|
+
else if (next.sorting !== prev.sorting || next.getRowId !== prev.getRowId) this.#reset({
|
|
52
|
+
rowCount: this.#cache.rowCount,
|
|
53
|
+
totalKnown: this.#cache.totalKnown
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
/** Drops every loaded page and loads the rendered ones again. The row count stays until pages answer. */
|
|
57
|
+
refresh() {
|
|
58
|
+
if (!this.#cache) return;
|
|
59
|
+
this.#reset({
|
|
60
|
+
rowCount: this.#cache.rowCount,
|
|
61
|
+
totalKnown: false
|
|
62
|
+
});
|
|
63
|
+
this.#host.requestUpdate();
|
|
64
|
+
}
|
|
65
|
+
stateAt(index) {
|
|
66
|
+
return this.#cache?.stateAt(index) ?? "loaded";
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The rendered rows, `start` to `end` (exclusive). Loads what's missing once the
|
|
70
|
+
* range has been quiet for a moment. Call after every update.
|
|
71
|
+
*/
|
|
72
|
+
request(start, end) {
|
|
73
|
+
const cache = this.#cache;
|
|
74
|
+
if (!cache) return;
|
|
75
|
+
const prev = this.#range;
|
|
76
|
+
const moved = prev?.start !== start || prev.end !== end;
|
|
77
|
+
this.#range = {
|
|
78
|
+
start,
|
|
79
|
+
end
|
|
80
|
+
};
|
|
81
|
+
if (moved) clearTimeout(this.#timer);
|
|
82
|
+
else if (this.#timer !== void 0 || cache.missingPages(start, end).length === 0) return;
|
|
83
|
+
this.#timer = setTimeout(() => {
|
|
84
|
+
this.#timer = void 0;
|
|
85
|
+
this.#load();
|
|
86
|
+
}, 50);
|
|
87
|
+
}
|
|
88
|
+
/** Requests the failed page holding row `index` again (the Retry button, or Enter on a failed row). */
|
|
89
|
+
retry(index) {
|
|
90
|
+
const cache = this.#cache;
|
|
91
|
+
const page = cache?.pageOf(index);
|
|
92
|
+
if (!cache || page === void 0 || !cache.isFailed(page)) return;
|
|
93
|
+
cache.retry(page);
|
|
94
|
+
this.#range = void 0;
|
|
95
|
+
this.#host.requestUpdate();
|
|
96
|
+
}
|
|
97
|
+
/** Load errors since the last call, to fire as `argus-load-error` after the update. */
|
|
98
|
+
takeErrors() {
|
|
99
|
+
const errors = this.#errors;
|
|
100
|
+
this.#errors = [];
|
|
101
|
+
return errors;
|
|
102
|
+
}
|
|
103
|
+
#reset(carry) {
|
|
104
|
+
clearTimeout(this.#timer);
|
|
105
|
+
this.#timer = void 0;
|
|
106
|
+
this.#abortAll();
|
|
107
|
+
this.#range = void 0;
|
|
108
|
+
this.#errors = [];
|
|
109
|
+
const { source, getRowId } = this.#config;
|
|
110
|
+
this.#cache = source && carry ? new PageCache({
|
|
111
|
+
pageSize: source.pageSize ?? 100,
|
|
112
|
+
getRowId,
|
|
113
|
+
rowCount: carry.rowCount,
|
|
114
|
+
totalKnown: carry.totalKnown
|
|
115
|
+
}) : void 0;
|
|
116
|
+
}
|
|
117
|
+
#abortAll() {
|
|
118
|
+
for (const [page, controller] of this.#requests) {
|
|
119
|
+
controller.abort();
|
|
120
|
+
this.#cache?.cancel(page);
|
|
121
|
+
}
|
|
122
|
+
this.#requests.clear();
|
|
123
|
+
}
|
|
124
|
+
#load() {
|
|
125
|
+
const cache = this.#cache;
|
|
126
|
+
const range = this.#range;
|
|
127
|
+
if (!cache || !range) return;
|
|
128
|
+
const margin = cache.pageSize;
|
|
129
|
+
for (const page of cache.inFlightOutside(range.start - margin, range.end + margin)) {
|
|
130
|
+
this.#requests.get(page)?.abort();
|
|
131
|
+
this.#requests.delete(page);
|
|
132
|
+
cache.cancel(page);
|
|
133
|
+
}
|
|
134
|
+
cache.evictOutside(range.start, range.end);
|
|
135
|
+
for (const page of cache.missingPages(range.start, range.end)) this.#fetch(cache, page);
|
|
136
|
+
}
|
|
137
|
+
#fetch(cache, page) {
|
|
138
|
+
const source = this.#config.source;
|
|
139
|
+
if (!source) return;
|
|
140
|
+
const { start, end } = cache.begin(page);
|
|
141
|
+
const controller = new AbortController();
|
|
142
|
+
this.#requests.set(page, controller);
|
|
143
|
+
const current = () => !controller.signal.aborted && cache === this.#cache;
|
|
144
|
+
let pending;
|
|
145
|
+
try {
|
|
146
|
+
pending = Promise.resolve(source.getRows({
|
|
147
|
+
start,
|
|
148
|
+
end,
|
|
149
|
+
sorting: this.#config.sorting,
|
|
150
|
+
signal: controller.signal
|
|
151
|
+
}));
|
|
152
|
+
} catch (error) {
|
|
153
|
+
pending = Promise.reject(error instanceof Error ? error : new Error(String(error)));
|
|
154
|
+
}
|
|
155
|
+
pending.then((response) => {
|
|
156
|
+
if (!current()) return;
|
|
157
|
+
const { rows, total } = response ?? {};
|
|
158
|
+
if (!Array.isArray(rows)) throw new TypeError("argus-table: getRows() must resolve to { rows: [], total? }.");
|
|
159
|
+
this.#requests.delete(page);
|
|
160
|
+
cache.resolve(page, rows, typeof total === "number" ? total : void 0);
|
|
161
|
+
this.#host.requestUpdate();
|
|
162
|
+
}).catch((error) => {
|
|
163
|
+
if (!current()) return;
|
|
164
|
+
this.#requests.delete(page);
|
|
165
|
+
cache.fail(page);
|
|
166
|
+
this.#errors.push({
|
|
167
|
+
error,
|
|
168
|
+
start,
|
|
169
|
+
end
|
|
170
|
+
});
|
|
171
|
+
this.#host.requestUpdate();
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
export { DataController };
|
|
176
|
+
|
|
177
|
+
//# sourceMappingURL=data-controller.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"data-controller.js","names":[],"sources":["../../src/controllers/data-controller.ts"],"sourcesContent":["import type { ReactiveController, ReactiveControllerHost } from 'lit';\nimport {\n PageCache,\n type DataSource,\n type LoadErrorDetail,\n type RowState,\n type SortState,\n} from '@argus-table/core';\n\n/** Rows per request when the data source sets no `pageSize`. */\nexport const DEFAULT_PAGE_SIZE = 100;\n\n/** Quiet time after the last range change before pages load, so fast scrolling loads only where it stops. */\nexport const LOAD_DEBOUNCE_MS = 50;\n\nexport interface DataConfig<T> {\n source: DataSource<T> | undefined;\n sorting: SortState;\n getRowId: ((row: T, index: number) => string) | undefined;\n}\n\n/**\n * Loads `dataSource` pages for the rendered rows (infinite loading). It owns the\n * page cache: a new source, sort order or row id function starts a fresh one, and\n * answers for an old cache are dropped. Requests wait until the range has been\n * quiet for `LOAD_DEBOUNCE_MS`; pages that scrolled away are aborted.\n */\nexport class DataController<T> implements ReactiveController {\n readonly #host: ReactiveControllerHost;\n #config: DataConfig<T> = { source: undefined, sorting: [], getRowId: undefined };\n #cache: PageCache<T> | undefined;\n readonly #requests = new Map<number, AbortController>();\n #range: { start: number; end: number } | undefined;\n #timer: ReturnType<typeof setTimeout> | undefined;\n #errors: LoadErrorDetail[] = [];\n\n constructor(host: ReactiveControllerHost) {\n this.#host = host;\n host.addController(this);\n }\n\n hostDisconnected(): void {\n clearTimeout(this.#timer);\n this.#timer = undefined;\n this.#abortAll();\n }\n\n hostConnected(): void {\n // Requests aborted on disconnect load again on the next range request.\n this.#range = undefined;\n }\n\n /** The page cache, while a data source is set: the model reads rows from it. */\n get rows(): PageCache<T> | undefined {\n return this.#cache;\n }\n\n /**\n * Takes the source, sort order and row id function. Any change starts a fresh\n * cache; a sort or row id change keeps the row count (the rows are the same).\n */\n configure(next: DataConfig<T>): void {\n const prev = this.#config;\n this.#config = next;\n if (!next.source) {\n this.#reset(undefined);\n return;\n }\n if (next.source !== prev.source || !this.#cache) {\n this.#reset({ rowCount: undefined, totalKnown: false });\n } else if (next.sorting !== prev.sorting || next.getRowId !== prev.getRowId) {\n this.#reset({ rowCount: this.#cache.rowCount, totalKnown: this.#cache.totalKnown });\n }\n }\n\n /** Drops every loaded page and loads the rendered ones again. The row count stays until pages answer. */\n refresh(): void {\n if (!this.#cache) return;\n this.#reset({ rowCount: this.#cache.rowCount, totalKnown: false });\n this.#host.requestUpdate();\n }\n\n stateAt(index: number): RowState {\n return this.#cache?.stateAt(index) ?? 'loaded';\n }\n\n /**\n * The rendered rows, `start` to `end` (exclusive). Loads what's missing once the\n * range has been quiet for a moment. Call after every update.\n */\n request(start: number, end: number): void {\n const cache = this.#cache;\n if (!cache) return;\n const prev = this.#range;\n const moved = prev?.start !== start || prev.end !== end;\n this.#range = { start, end };\n if (moved) {\n clearTimeout(this.#timer);\n } else if (this.#timer !== undefined || cache.missingPages(start, end).length === 0) {\n return;\n }\n this.#timer = setTimeout(() => {\n this.#timer = undefined;\n this.#load();\n }, LOAD_DEBOUNCE_MS);\n }\n\n /** Requests the failed page holding row `index` again (the Retry button, or Enter on a failed row). */\n retry(index: number): void {\n const cache = this.#cache;\n const page = cache?.pageOf(index);\n if (!cache || page === undefined || !cache.isFailed(page)) return;\n cache.retry(page);\n this.#range = undefined;\n this.#host.requestUpdate();\n }\n\n /** Load errors since the last call, to fire as `argus-load-error` after the update. */\n takeErrors(): LoadErrorDetail[] {\n const errors = this.#errors;\n this.#errors = [];\n return errors;\n }\n\n #reset(carry: { rowCount: number | undefined; totalKnown: boolean } | undefined): void {\n clearTimeout(this.#timer);\n this.#timer = undefined;\n this.#abortAll();\n this.#range = undefined;\n this.#errors = [];\n const { source, getRowId } = this.#config;\n this.#cache =\n source && carry\n ? new PageCache<T>({\n pageSize: source.pageSize ?? DEFAULT_PAGE_SIZE,\n getRowId,\n rowCount: carry.rowCount,\n totalKnown: carry.totalKnown,\n })\n : undefined;\n }\n\n #abortAll(): void {\n for (const [page, controller] of this.#requests) {\n controller.abort();\n this.#cache?.cancel(page);\n }\n this.#requests.clear();\n }\n\n #load(): void {\n const cache = this.#cache;\n const range = this.#range;\n if (!cache || !range) return;\n // Keep a page of margin, so a small scroll back doesn't abort a page about to land.\n const margin = cache.pageSize;\n for (const page of cache.inFlightOutside(range.start - margin, range.end + margin)) {\n this.#requests.get(page)?.abort();\n this.#requests.delete(page);\n cache.cancel(page);\n }\n cache.evictOutside(range.start, range.end);\n for (const page of cache.missingPages(range.start, range.end)) this.#fetch(cache, page);\n }\n\n #fetch(cache: PageCache<T>, page: number): void {\n const source = this.#config.source;\n if (!source) return;\n const { start, end } = cache.begin(page);\n const controller = new AbortController();\n this.#requests.set(page, controller);\n const current = () => !controller.signal.aborted && cache === this.#cache;\n let pending: Promise<unknown>;\n try {\n pending = Promise.resolve(\n source.getRows({ start, end, sorting: this.#config.sorting, signal: controller.signal }),\n );\n } catch (error) {\n pending = Promise.reject(error instanceof Error ? error : new Error(String(error)));\n }\n pending\n .then((response) => {\n if (!current()) return;\n const { rows, total } = (response ?? {}) as { rows?: unknown; total?: unknown };\n if (!Array.isArray(rows)) {\n throw new TypeError('argus-table: getRows() must resolve to { rows: [], total? }.');\n }\n this.#requests.delete(page);\n cache.resolve(page, rows as T[], typeof total === 'number' ? total : undefined);\n this.#host.requestUpdate();\n })\n .catch((error: unknown) => {\n if (!current()) return;\n this.#requests.delete(page);\n cache.fail(page);\n this.#errors.push({ error, start, end });\n this.#host.requestUpdate();\n });\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// src/controllers/reorder-drag.ts
|
|
2
|
+
/**
|
|
3
|
+
* The drop target for a pointer at `x`: the gap between headers nearest to it,
|
|
4
|
+
* decided by each header's midpoint. `undefined` where the column would stay
|
|
5
|
+
* put. `direction` is 1 in LTR and -1 in RTL, where the group runs right to left.
|
|
6
|
+
*/
|
|
7
|
+
function dropTarget(slots, draggedId, x, direction) {
|
|
8
|
+
const from = slots.findIndex((s) => s.id === draggedId);
|
|
9
|
+
const first = slots[0];
|
|
10
|
+
const last = slots.at(-1);
|
|
11
|
+
if (from < 0 || !first || !last) return void 0;
|
|
12
|
+
let gap = slots.findIndex((s) => (x - (s.rect.left + s.rect.right) / 2) * direction < 0);
|
|
13
|
+
if (gap < 0) gap = slots.length;
|
|
14
|
+
if (gap === from || gap === from + 1) return void 0;
|
|
15
|
+
const to = first.index + (gap > from ? gap - 1 : gap);
|
|
16
|
+
const at = slots[gap];
|
|
17
|
+
return at ? {
|
|
18
|
+
to,
|
|
19
|
+
columnId: at.id,
|
|
20
|
+
edge: "start"
|
|
21
|
+
} : {
|
|
22
|
+
to,
|
|
23
|
+
columnId: last.id,
|
|
24
|
+
edge: "end"
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** Movement before a press becomes a drag, so a click still sorts. */
|
|
28
|
+
var THRESHOLD = 4;
|
|
29
|
+
/** Width of the band at the scroll area's edges that scrolls, and the top speed there. */
|
|
30
|
+
var EDGE = 40;
|
|
31
|
+
var MAX_SPEED = 16;
|
|
32
|
+
var INTERACTIVE = "button,input,select,textarea,a[href],label,[contenteditable]";
|
|
33
|
+
/**
|
|
34
|
+
* Whether a press on a header may start a reorder drag: the primary button of a
|
|
35
|
+
* mouse or pen (a touch pans the table instead), not on a resize handle, and not
|
|
36
|
+
* in interactive content slotted into the header.
|
|
37
|
+
*/
|
|
38
|
+
function canStartReorder(down, cell) {
|
|
39
|
+
if (down.button !== 0 || down.pointerType === "touch") return false;
|
|
40
|
+
for (const target of down.composedPath()) {
|
|
41
|
+
if (target === cell) return true;
|
|
42
|
+
if (!(target instanceof Element)) continue;
|
|
43
|
+
if (target instanceof HTMLElement && target.dataset.resizeCol !== void 0) return false;
|
|
44
|
+
if (target.matches(INTERACTIVE)) return false;
|
|
45
|
+
}
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* One pointer drag of a column header. It becomes a drag after a few pixels of
|
|
50
|
+
* movement; until then the press is a click. While dragging, the pointer is
|
|
51
|
+
* captured by the header, the target follows the pointer once per frame, and the
|
|
52
|
+
* scroll area scrolls near its edges. Escape cancels. The click that ends a drag
|
|
53
|
+
* is swallowed, so it neither sorts nor counts as a cell click.
|
|
54
|
+
*/
|
|
55
|
+
function startReorderDrag(cell, down, options) {
|
|
56
|
+
const view = cell.ownerDocument.defaultView ?? window;
|
|
57
|
+
const startX = down.clientX;
|
|
58
|
+
const startY = down.clientY;
|
|
59
|
+
let x = startX;
|
|
60
|
+
let dragging = false;
|
|
61
|
+
let frame = 0;
|
|
62
|
+
let target;
|
|
63
|
+
const same = (a, b) => a?.to === b?.to && a?.columnId === b?.columnId && a?.edge === b?.edge;
|
|
64
|
+
const tick = () => {
|
|
65
|
+
frame = 0;
|
|
66
|
+
if (!dragging) return;
|
|
67
|
+
const area = options.scrollArea();
|
|
68
|
+
if (area) {
|
|
69
|
+
const speed = x < area.left + EDGE ? -Math.min(1, (area.left + EDGE - x) / EDGE) : x > area.right - EDGE ? Math.min(1, (x - (area.right - EDGE)) / EDGE) : 0;
|
|
70
|
+
if (speed !== 0) options.scroller.scrollLeft += Math.round(speed * MAX_SPEED);
|
|
71
|
+
x = Math.min(Math.max(x, area.left), area.right);
|
|
72
|
+
}
|
|
73
|
+
const next = options.targetAt(x);
|
|
74
|
+
if (!same(next, target)) {
|
|
75
|
+
target = next;
|
|
76
|
+
options.onTarget(target);
|
|
77
|
+
}
|
|
78
|
+
frame = view.requestAnimationFrame(tick);
|
|
79
|
+
};
|
|
80
|
+
const listeners = new AbortController();
|
|
81
|
+
const cleanup = (event) => {
|
|
82
|
+
if (frame) view.cancelAnimationFrame(frame);
|
|
83
|
+
frame = 0;
|
|
84
|
+
listeners.abort();
|
|
85
|
+
if (event && cell.hasPointerCapture(event.pointerId)) cell.releasePointerCapture(event.pointerId);
|
|
86
|
+
};
|
|
87
|
+
const move = (event) => {
|
|
88
|
+
if (event.pointerId !== down.pointerId) return;
|
|
89
|
+
x = event.clientX;
|
|
90
|
+
if (!dragging) {
|
|
91
|
+
if (Math.hypot(x - startX, event.clientY - startY) < THRESHOLD) return;
|
|
92
|
+
dragging = true;
|
|
93
|
+
try {
|
|
94
|
+
cell.setPointerCapture(event.pointerId);
|
|
95
|
+
} catch {}
|
|
96
|
+
options.onStart();
|
|
97
|
+
}
|
|
98
|
+
event.preventDefault();
|
|
99
|
+
frame ||= view.requestAnimationFrame(tick);
|
|
100
|
+
};
|
|
101
|
+
const up = (event) => {
|
|
102
|
+
if (event.pointerId !== down.pointerId) return;
|
|
103
|
+
cleanup(event);
|
|
104
|
+
if (!dragging) return;
|
|
105
|
+
x = event.clientX;
|
|
106
|
+
const area = options.scrollArea();
|
|
107
|
+
if (area) x = Math.min(Math.max(x, area.left), area.right);
|
|
108
|
+
swallowClick(view);
|
|
109
|
+
options.onDrop(options.targetAt(x));
|
|
110
|
+
};
|
|
111
|
+
const cancel = (event) => {
|
|
112
|
+
if (event.pointerId !== down.pointerId) return;
|
|
113
|
+
cleanup(event);
|
|
114
|
+
if (dragging) options.onCancel();
|
|
115
|
+
};
|
|
116
|
+
const key = (event) => {
|
|
117
|
+
if (!dragging || event.key !== "Escape") return;
|
|
118
|
+
event.preventDefault();
|
|
119
|
+
event.stopPropagation();
|
|
120
|
+
cleanup();
|
|
121
|
+
swallowClick(view, false);
|
|
122
|
+
options.onCancel();
|
|
123
|
+
};
|
|
124
|
+
const listen = {
|
|
125
|
+
capture: true,
|
|
126
|
+
signal: listeners.signal
|
|
127
|
+
};
|
|
128
|
+
view.addEventListener("pointermove", move, listen);
|
|
129
|
+
view.addEventListener("pointerup", up, listen);
|
|
130
|
+
view.addEventListener("pointercancel", cancel, listen);
|
|
131
|
+
view.addEventListener("keydown", key, listen);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Stops the click that follows the release of a drag, if one comes. It comes in
|
|
135
|
+
* the same task as the release, so the guard goes once that task is over. After
|
|
136
|
+
* Escape the pointer is still down (`released: false`): the guard waits for its release.
|
|
137
|
+
*/
|
|
138
|
+
function swallowClick(view, released = true) {
|
|
139
|
+
const stop = (event) => {
|
|
140
|
+
event.preventDefault();
|
|
141
|
+
event.stopPropagation();
|
|
142
|
+
};
|
|
143
|
+
const remove = () => {
|
|
144
|
+
setTimeout(() => {
|
|
145
|
+
view.removeEventListener("click", stop, { capture: true });
|
|
146
|
+
});
|
|
147
|
+
};
|
|
148
|
+
view.addEventListener("click", stop, {
|
|
149
|
+
capture: true,
|
|
150
|
+
once: true
|
|
151
|
+
});
|
|
152
|
+
if (released) remove();
|
|
153
|
+
else view.addEventListener("pointerup", remove, {
|
|
154
|
+
capture: true,
|
|
155
|
+
once: true
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
export { canStartReorder, dropTarget, startReorderDrag };
|
|
159
|
+
|
|
160
|
+
//# sourceMappingURL=reorder-drag.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reorder-drag.js","names":[],"sources":["../../src/controllers/reorder-drag.ts"],"sourcesContent":["/** 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 */\nexport function dropTarget(\n slots: readonly DropSlot[],\n draggedId: string,\n x: number,\n direction: 1 | -1,\n): DropTarget | undefined {\n const from = slots.findIndex((s) => s.id === draggedId);\n const first = slots[0];\n const last = slots.at(-1);\n if (from < 0 || !first || !last) return undefined;\n // The gap before the first header whose middle is past the pointer, in reading order.\n let gap = slots.findIndex((s) => (x - (s.rect.left + s.rect.right) / 2) * direction < 0);\n if (gap < 0) gap = slots.length;\n if (gap === from || gap === from + 1) return undefined;\n const to = first.index + (gap > from ? gap - 1 : gap);\n const at = slots[gap];\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\nconst INTERACTIVE = 'button,input,select,textarea,a[href],label,[contenteditable]';\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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"}
|
|
@@ -16,7 +16,10 @@ function startResizeDrag(handle, down, startWidth, direction, onWidth) {
|
|
|
16
16
|
if (pending !== void 0) onWidth(pending);
|
|
17
17
|
pending = void 0;
|
|
18
18
|
};
|
|
19
|
+
let moved = false;
|
|
19
20
|
const move = (event) => {
|
|
21
|
+
moved ||= event.clientX !== startX;
|
|
22
|
+
if (!moved) return;
|
|
20
23
|
pending = startWidth + (event.clientX - startX) * direction;
|
|
21
24
|
frame ||= requestAnimationFrame(flush);
|
|
22
25
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resize-drag.js","names":[],"sources":["../../src/controllers/resize-drag.ts"],"sourcesContent":["/**\n * One pointer drag on a resize handle. The pointer is captured, so the drag keeps\n * working outside the handle, and moves are batched to one `onWidth` call per\n * animation frame (TanStack's `onChange` mode: the width updates while dragging).\n *\n * `direction` is 1 in LTR and -1 in RTL: the handle sits on the inline-end edge,\n * so moving toward the inline end always grows the column.\n */\nexport function startResizeDrag(\n handle: HTMLElement,\n down: PointerEvent,\n startWidth: number,\n direction: 1 | -1,\n onWidth: (width: number) => void,\n): void {\n const startX = down.clientX;\n let frame = 0;\n let pending: number | undefined;\n\n const flush = () => {\n frame = 0;\n if (pending !== undefined) onWidth(pending);\n pending = undefined;\n };\n const move = (event: PointerEvent) => {\n pending = startWidth + (event.clientX - startX) * direction;\n frame ||= requestAnimationFrame(flush);\n };\n const end = (event: PointerEvent) => {\n if (event.type === 'pointerup') move(event);\n if (frame) cancelAnimationFrame(frame);\n flush();\n handle.removeAttribute('data-active');\n handle.removeEventListener('pointermove', move);\n handle.removeEventListener('pointerup', end);\n handle.removeEventListener('pointercancel', end);\n if (handle.hasPointerCapture(event.pointerId)) handle.releasePointerCapture(event.pointerId);\n };\n\n down.preventDefault();\n try {\n handle.setPointerCapture(down.pointerId);\n } catch {\n // Synthetic or already released pointers can't be captured; moves on the handle still work.\n }\n handle.setAttribute('data-active', '');\n handle.addEventListener('pointermove', move);\n handle.addEventListener('pointerup', end);\n handle.addEventListener('pointercancel', end);\n}\n"],"mappings":";;;;;;;;;AAQA,SAAgB,gBACd,QACA,MACA,YACA,WACA,SACM;CACN,MAAM,SAAS,KAAK;CACpB,IAAI,QAAQ;CACZ,IAAI;CAEJ,MAAM,cAAc;EAClB,QAAQ;EACR,IAAI,YAAY,KAAA,GAAW,QAAQ,OAAO;EAC1C,UAAU,KAAA;CACZ;
|
|
1
|
+
{"version":3,"file":"resize-drag.js","names":[],"sources":["../../src/controllers/resize-drag.ts"],"sourcesContent":["/**\n * One pointer drag on a resize handle. The pointer is captured, so the drag keeps\n * working outside the handle, and moves are batched to one `onWidth` call per\n * animation frame (TanStack's `onChange` mode: the width updates while dragging).\n *\n * `direction` is 1 in LTR and -1 in RTL: the handle sits on the inline-end edge,\n * so moving toward the inline end always grows the column.\n */\nexport function startResizeDrag(\n handle: HTMLElement,\n down: PointerEvent,\n startWidth: number,\n direction: 1 | -1,\n onWidth: (width: number) => void,\n): void {\n const startX = down.clientX;\n let frame = 0;\n let pending: number | undefined;\n\n const flush = () => {\n frame = 0;\n if (pending !== undefined) onWidth(pending);\n pending = undefined;\n };\n // A press and release without moving (or the clicks of a double-click) changes nothing.\n let moved = false;\n const move = (event: PointerEvent) => {\n moved ||= event.clientX !== startX;\n if (!moved) return;\n pending = startWidth + (event.clientX - startX) * direction;\n frame ||= requestAnimationFrame(flush);\n };\n const end = (event: PointerEvent) => {\n if (event.type === 'pointerup') move(event);\n if (frame) cancelAnimationFrame(frame);\n flush();\n handle.removeAttribute('data-active');\n handle.removeEventListener('pointermove', move);\n handle.removeEventListener('pointerup', end);\n handle.removeEventListener('pointercancel', end);\n if (handle.hasPointerCapture(event.pointerId)) handle.releasePointerCapture(event.pointerId);\n };\n\n down.preventDefault();\n try {\n handle.setPointerCapture(down.pointerId);\n } catch {\n // Synthetic or already released pointers can't be captured; moves on the handle still work.\n }\n handle.setAttribute('data-active', '');\n handle.addEventListener('pointermove', move);\n handle.addEventListener('pointerup', end);\n handle.addEventListener('pointercancel', end);\n}\n"],"mappings":";;;;;;;;;AAQA,SAAgB,gBACd,QACA,MACA,YACA,WACA,SACM;CACN,MAAM,SAAS,KAAK;CACpB,IAAI,QAAQ;CACZ,IAAI;CAEJ,MAAM,cAAc;EAClB,QAAQ;EACR,IAAI,YAAY,KAAA,GAAW,QAAQ,OAAO;EAC1C,UAAU,KAAA;CACZ;CAEA,IAAI,QAAQ;CACZ,MAAM,QAAQ,UAAwB;EACpC,UAAU,MAAM,YAAY;EAC5B,IAAI,CAAC,OAAO;EACZ,UAAU,cAAc,MAAM,UAAU,UAAU;EAClD,UAAU,sBAAsB,KAAK;CACvC;CACA,MAAM,OAAO,UAAwB;EACnC,IAAI,MAAM,SAAS,aAAa,KAAK,KAAK;EAC1C,IAAI,OAAO,qBAAqB,KAAK;EACrC,MAAM;EACN,OAAO,gBAAgB,aAAa;EACpC,OAAO,oBAAoB,eAAe,IAAI;EAC9C,OAAO,oBAAoB,aAAa,GAAG;EAC3C,OAAO,oBAAoB,iBAAiB,GAAG;EAC/C,IAAI,OAAO,kBAAkB,MAAM,SAAS,GAAG,OAAO,sBAAsB,MAAM,SAAS;CAC7F;CAEA,KAAK,eAAe;CACpB,IAAI;EACF,OAAO,kBAAkB,KAAK,SAAS;CACzC,QAAQ,CAER;CACA,OAAO,aAAa,eAAe,EAAE;CACrC,OAAO,iBAAiB,eAAe,IAAI;CAC3C,OAAO,iBAAiB,aAAa,GAAG;CACxC,OAAO,iBAAiB,iBAAiB,GAAG;AAC9C"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"table-controller.js","names":[],"sources":["../../src/controllers/table-controller.ts"],"sourcesContent":["import { createTableModel, type TableModel, type TableModelOptions } from '@argus-table/core';\nimport type { CellContent } from '../types.js';\n\n/**\n * Owns the table model (core's TanStack adapter). The element pushes its\n * properties in from `willUpdate`; render functions read `model`. It needs no\n * lifecycle hooks yet: the model is pull-based and owns no state.\n */\nexport class TableController<T> {\n readonly model: TableModel<T, CellContent>;\n\n constructor() {\n this.model = createTableModel<T, CellContent>({\n columns: [],\n data: [],\n getRowId: undefined,\n sorting: [],\n columnSizing: {},\n });\n }\n\n update(options: Partial<TableModelOptions<T, CellContent>>): void {\n this.model.update(options);\n }\n}\n"],"mappings":";;;;;;;AAQA,IAAa,kBAAb,MAAgC;CAG9B,cAAc;EACZ,KAAK,QAAQ,iBAAiC;GAC5C,SAAS,CAAC;GACV,MAAM,CAAC;GACP,UAAU,KAAA;GACV,SAAS,CAAC;GACV,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"table-controller.js","names":[],"sources":["../../src/controllers/table-controller.ts"],"sourcesContent":["import { createTableModel, type TableModel, type TableModelOptions } from '@argus-table/core';\nimport type { CellContent } from '../types.js';\n\n/**\n * Owns the table model (core's TanStack adapter). The element pushes its\n * properties in from `willUpdate`; render functions read `model`. It needs no\n * lifecycle hooks yet: the model is pull-based and owns no state.\n */\nexport class TableController<T> {\n readonly model: TableModel<T, CellContent>;\n\n constructor() {\n this.model = createTableModel<T, CellContent>({\n columns: [],\n data: [],\n getRowId: undefined,\n sorting: [],\n columnSizing: {},\n columnOrder: [],\n });\n }\n\n update(options: Partial<TableModelOptions<T, CellContent>>): void {\n this.model.update(options);\n }\n}\n"],"mappings":";;;;;;;AAQA,IAAa,kBAAb,MAAgC;CAG9B,cAAc;EACZ,KAAK,QAAQ,iBAAiC;GAC5C,SAAS,CAAC;GACV,MAAM,CAAC;GACP,UAAU,KAAA;GACV,SAAS,CAAC;GACV,cAAc,CAAC;GACf,aAAa,CAAC;EAChB,CAAC;CACH;CAEA,OAAO,SAA2D;EAChE,KAAK,MAAM,OAAO,OAAO;CAC3B;AACF"}
|