@loadbare/widgets 0.11.1 → 0.13.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.
@@ -1,5 +1,5 @@
1
1
  /// <reference lib="dom" />
2
- import { ATTR_COLUMN, ATTR_FILTER, ATTR_GROUP, ATTR_ORDER, ATTR_QUERY, ATTR_QUERY_ORDER, ATTR_QUERY_UNSORTED_COUNT, ATTR_ROW_REQUESTED, ATTR_SHOW, CHOICES_QUERY, COMMAND_SORT, LB_EVENT_NAME, LIVE_ROW, ORDER_DESCENDING, REQUEST_ROW_INSERT, REQUEST_ROW_UPDATE, REQUESTED_CREATED, REQUESTED_MOVED, } from "@loadbare/app/constants";
2
+ import { ATTR_COLUMN, ATTR_FILTER, ATTR_GROUP, ATTR_QUERY, ATTR_QUERY_ORDER, ATTR_QUERY_UNPLACED_COUNT, ATTR_ROW_REQUESTED, ATTR_SHOW, COMMAND_PLACE, FILTERS_QUERY, LB_EVENT_NAME, LIVE_ROW, ORDER_DESCENDING, REQUEST_ROW_INSERT, REQUEST_ROW_UPDATE, REQUESTED_CREATED, REQUESTED_MOVED, SORT_COLUMN_ORDER, SORT_QUERY, } from "@loadbare/app/constants";
3
3
  /**
4
4
  * A table that owns its own scaffolding.
5
5
  *
@@ -33,7 +33,7 @@ import { ATTR_COLUMN, ATTR_FILTER, ATTR_GROUP, ATTR_ORDER, ATTR_QUERY, ATTR_QUER
33
33
  * anywhere else `*=` contains
34
34
  *
35
35
  * The choice replaces whatever that column held, so a control naming the
36
- * same column of `lb-choices` shows it and clears it. The selection is
36
+ * same column of `lb-filters` shows it and clears it. The selection is
37
37
  * removed once it is used.
38
38
  */
39
39
  /**
@@ -41,18 +41,17 @@ import { ATTR_COLUMN, ATTR_FILTER, ATTR_GROUP, ATTR_ORDER, ATTR_QUERY, ATTR_QUER
41
41
  * direction, and `aria-sort`. A heading's column is the one `lb-column` in
42
42
  * the cell at the same position in the row template, a branch `lb-show`
43
43
  * holds in a template included; a cell with none, or with several, gives its
44
- * heading no column. Only the order's leading term is
45
- * shown. A grouped table shows none of this.
44
+ * heading no column. Only the order's leading term is shown. A grouped table
45
+ * shows none of this.
46
46
  *
47
- * While rows stand out of order the triangle carries `data-unsorted`, and a
48
- * click on its heading places them again with `--lb-sort`. Otherwise, on a
49
- * table with `lb-order`, a click on the sorted heading reverses the order and
50
- * a click on another heading sorts by its column, ascending: either way the
51
- * table writes the user's order to the column of `lb-choices` that `lb-order`
52
- * names. A table with no `lb-order` has nowhere to write one.
47
+ * While rows stand out of place the triangle carries `data-unplaced`, and a
48
+ * click on its heading places them again with `--lb-place`. Otherwise a
49
+ * click on the sorted heading reverses the order and a click on another
50
+ * heading sorts by its column, ascending: either way the table writes the
51
+ * order to its query's row of `lb-sort`.
53
52
  */
54
53
  const SORT = "data-sort";
55
- const UNSORTED = "data-unsorted";
54
+ const UNPLACED = "data-unplaced";
56
55
  const SORTABLE = "data-sortable";
57
56
  /**
58
57
  * The ghost row's `<tbody>`, a blank row for entering a new one, between the
@@ -145,15 +144,14 @@ class LbTable extends HTMLElement {
145
144
  descending,
146
145
  };
147
146
  }
148
- /** Whether rows stand where the order would not put them. */
149
- unsorted() {
150
- const count = this.getAttribute(ATTR_QUERY_UNSORTED_COUNT);
147
+ /** Whether rows stand where the order or the filter would not leave them. */
148
+ unplaced() {
149
+ const count = this.getAttribute(ATTR_QUERY_UNPLACED_COUNT);
151
150
  return count !== null && count !== "0";
152
151
  }
153
152
  /** The triangle on the sorted heading, and every heading a click sorts. */
154
153
  showOrder() {
155
154
  const term = this.leading();
156
- const writable = this.hasAttribute(ATTR_ORDER);
157
155
  for (const [th, column] of this.columns()) {
158
156
  const sorted = column === term?.column;
159
157
  let mark = th.querySelector(`:scope > [${SORT}]`);
@@ -165,17 +163,17 @@ class LbTable extends HTMLElement {
165
163
  th.append(mark);
166
164
  }
167
165
  mark.textContent = term.descending ? "▼" : "▲";
168
- mark.toggleAttribute(UNSORTED, this.unsorted());
166
+ mark.toggleAttribute(UNPLACED, this.unplaced());
169
167
  th.setAttribute("aria-sort", term.descending ? "descending" : "ascending");
170
168
  }
171
169
  else {
172
170
  mark?.remove();
173
171
  th.removeAttribute("aria-sort");
174
172
  }
175
- th.toggleAttribute(SORTABLE, writable || (sorted && this.unsorted()));
173
+ th.toggleAttribute(SORTABLE, true);
176
174
  }
177
175
  }
178
- /** A click on a heading: re-sort, reverse, or sort by its column. */
176
+ /** A click on a heading: place the rows, reverse, or sort by its column. */
179
177
  sortFromHeading(e) {
180
178
  const th = e.target.closest("th");
181
179
  const column = th && this.columns().get(th);
@@ -183,20 +181,20 @@ class LbTable extends HTMLElement {
183
181
  return;
184
182
  const term = this.leading();
185
183
  const sorted = column === term?.column;
186
- if (sorted && this.unsorted()) {
184
+ if (sorted && this.unplaced()) {
187
185
  const command = new Event("command");
188
- Object.defineProperty(command, "command", { value: COMMAND_SORT });
186
+ Object.defineProperty(command, "command", { value: COMMAND_PLACE });
189
187
  this.dispatchEvent(command);
190
188
  return;
191
189
  }
192
- const choice = this.getAttribute(ATTR_ORDER);
193
- if (!choice)
194
- return;
195
190
  const descending = sorted && !term.descending;
196
191
  const detail = {
197
192
  name: REQUEST_ROW_UPDATE,
198
- query: CHOICES_QUERY,
199
- values: { [choice]: (descending ? ORDER_DESCENDING : "") + column },
193
+ query: SORT_QUERY,
194
+ key: this.getAttribute(ATTR_QUERY),
195
+ values: {
196
+ [SORT_COLUMN_ORDER]: (descending ? ORDER_DESCENDING : "") + column,
197
+ },
200
198
  };
201
199
  this.dispatchEvent(new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail }));
202
200
  }
@@ -230,7 +228,8 @@ class LbTable extends HTMLElement {
230
228
  selection.removeAllRanges();
231
229
  const detail = {
232
230
  name: REQUEST_ROW_UPDATE,
233
- query: CHOICES_QUERY,
231
+ query: FILTERS_QUERY,
232
+ key: this.getAttribute(ATTR_QUERY),
234
233
  values: { [column]: term },
235
234
  };
236
235
  this.dispatchEvent(new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail }));
@@ -1 +1 @@
1
- {"version":3,"file":"lb-table.browser.js","sourceRoot":"","sources":["../widgets/lb-table.browser.ts"],"names":[],"mappings":"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","sourcesContent":["/// <reference lib=\"dom\" />\n\nimport {\n ATTR_COLUMN,\n ATTR_FILTER,\n ATTR_GROUP,\n ATTR_ORDER,\n ATTR_QUERY,\n ATTR_QUERY_ORDER,\n ATTR_QUERY_UNSORTED_COUNT,\n ATTR_ROW_REQUESTED,\n ATTR_SHOW,\n CHOICES_QUERY,\n COMMAND_SORT,\n LB_EVENT_NAME,\n LIVE_ROW,\n ORDER_DESCENDING,\n REQUEST_ROW_INSERT,\n REQUEST_ROW_UPDATE,\n REQUESTED_CREATED,\n REQUESTED_MOVED,\n} from \"@loadbare/app/constants\";\nimport type {\n HubLifecycle,\n HubRequest,\n HubRequestDone,\n} from \"@loadbare/app/types\";\n\n/**\n * A table that owns its own scaffolding.\n *\n * A plain `<table lb-query>` with a row template inside it needs no widget at\n * all. This one supplies the table, and the developer writes what only the\n * page knows: the heading row and the row template. That division is forced\n * rather than chosen — a `<thead>` written inside a custom element is not\n * inside a table, so the parser drops the tag and keeps the text. A\n * `<template>` survives anywhere, so the heading row arrives wrapped and\n * expansion unwraps it into the `lb-exp-template` destination beside the slot.\n *\n * Placement and groups are the hub's, by the query's order and the page's\n * group templates. What this widget adds at run time is the ghost row,\n * scrolling to the row the page's own request created or moved, filtering\n * by selected text, and the order shown on the heading.\n *\n * The `foot` destination needs no code here. A `<tfoot>` is a place on the\n * page, not a row the hub delivers, so what lands in it is an aggregate or a\n * `row` query of its own, named on its `<tr>`.\n */\n\n/**\n * Text selected in a cell filters the table by that cell's column. When a\n * selection finishes inside one element of a live row whose `lb-column` the\n * table's `lb-filter` names, the table makes the choice of that name, and the\n * hub hides the rows it does not pass. Where the selection lies in the\n * element's text says how:\n *\n * the whole text `=` equals\n * from its start `^=` begins with\n * up to its end `$=` ends with\n * anywhere else `*=` contains\n *\n * The choice replaces whatever that column held, so a control naming the\n * same column of `lb-choices` shows it and clears it. The selection is\n * removed once it is used.\n */\n\n/**\n * The heading of the column the rows are sorted by carries a triangle for the\n * direction, and `aria-sort`. A heading's column is the one `lb-column` in\n * the cell at the same position in the row template, a branch `lb-show`\n * holds in a template included; a cell with none, or with several, gives its\n * heading no column. Only the order's leading term is\n * shown. A grouped table shows none of this.\n *\n * While rows stand out of order the triangle carries `data-unsorted`, and a\n * click on its heading places them again with `--lb-sort`. Otherwise, on a\n * table with `lb-order`, a click on the sorted heading reverses the order and\n * a click on another heading sorts by its column, ascending: either way the\n * table writes the user's order to the column of `lb-choices` that `lb-order`\n * names. A table with no `lb-order` has nowhere to write one.\n */\nconst SORT = \"data-sort\";\nconst UNSORTED = \"data-unsorted\";\nconst SORTABLE = \"data-sortable\";\n\n/**\n * The ghost row's `<tbody>`, a blank row for entering a new one, between the\n * rows and the footer. The page writes it as the `ghost` template, so it\n * ships in the document like the heading and the footer, and the hub lands\n * on whatever in it names a query. It is not a live row: the hub never\n * reconciles it and never counts it.\n *\n * A `<form>` cannot wrap a `<tr>`, so the widget makes one outside the table\n * and joins the row's controls and buttons to it with the HTML `form`\n * attribute. Sending is the page's: a button in the row carrying\n * `lb-row-insert` submits that form, and the hub gathers what the form owns.\n * The hub resets the form once the insert succeeds.\n */\nconst GHOST = \"data-ghost\";\n\n/** Ghost row forms need ids for the `form` attribute; any unique one will do. */\nlet ghostForms = 0;\n\nclass LbTable extends HTMLElement implements HubLifecycle {\n constructor() {\n super();\n this.addEventListener(\"mouseup\", () => this.filterBySelection());\n this.addEventListener(\"click\", (e) => this.sortFromHeading(e));\n }\n\n /** Each heading's column, by position in the row template; none when grouped. */\n private headings?: Map<Element, string>;\n\n lbAttached(): void {\n const ghost = this.querySelector(`tbody[${GHOST}]`);\n if (!ghost?.firstElementChild) return;\n\n const form = document.createElement(\"form\");\n form.id = `ghost-row-${++ghostForms}`;\n form.hidden = true;\n this.append(form);\n for (const el of ghost.querySelectorAll(`[${ATTR_COLUMN}], button`)) {\n el.setAttribute(\"form\", form.id);\n }\n }\n\n /** The row this page's request created or moved, brought into view. */\n lbRowsLanded(): void {\n this.showOrder();\n this.querySelector(\n `[${ATTR_ROW_REQUESTED}=\"${REQUESTED_CREATED}\"], ` +\n `[${ATTR_ROW_REQUESTED}=\"${REQUESTED_MOVED}\"]`,\n )?.scrollIntoView({ block: \"nearest\" });\n }\n\n /**\n * A ghost row is for entering the next row, so once one is saved the\n * cursor goes back to the start of it. The hub calls this after the\n * response items have landed, so nothing it places afterwards takes the\n * focus away again. A refused insert leaves the cursor where the user left\n * it.\n */\n lbRequestDone({ request, error }: HubRequestDone, origin: Element): void {\n if (error || request.name !== REQUEST_ROW_INSERT) return;\n const row = origin.closest(`[${GHOST}]`);\n if (row) firstControl(row)?.focus();\n }\n\n /** The headings a column can be read for, and their columns. */\n private columns(): Map<Element, string> {\n if (this.headings) return this.headings;\n this.headings = new Map();\n const body = this.querySelector(`tbody:not([${GHOST}])`);\n const template = body?.querySelector(\":scope > template\");\n const head = this.querySelector(\"thead tr\");\n if (!(template instanceof HTMLTemplateElement) || !head) {\n return this.headings;\n }\n if (template.hasAttribute(ATTR_GROUP)) return this.headings;\n const cells = template.content.querySelector(\"tr\")?.children ?? [];\n [...head.children].forEach((th, i) => {\n const cell = cells[i];\n if (!cell) return;\n const named = new Set(columnsIn(cell));\n if (named.size === 1) this.headings!.set(th, [...named][0]!);\n });\n return this.headings;\n }\n\n /** The order's leading term, from what the hub stamped. */\n private leading(): { column: string; descending: boolean } | undefined {\n const term = (this.getAttribute(ATTR_QUERY_ORDER) ?? \"\").split(\",\")[0];\n if (!term) return undefined;\n const descending = term.startsWith(ORDER_DESCENDING);\n return {\n column: descending ? term.slice(ORDER_DESCENDING.length) : term,\n descending,\n };\n }\n\n /** Whether rows stand where the order would not put them. */\n private unsorted(): boolean {\n const count = this.getAttribute(ATTR_QUERY_UNSORTED_COUNT);\n return count !== null && count !== \"0\";\n }\n\n /** The triangle on the sorted heading, and every heading a click sorts. */\n private showOrder(): void {\n const term = this.leading();\n const writable = this.hasAttribute(ATTR_ORDER);\n for (const [th, column] of this.columns()) {\n const sorted = column === term?.column;\n let mark = th.querySelector(`:scope > [${SORT}]`);\n if (sorted) {\n if (!mark) {\n mark = document.createElement(\"span\");\n mark.setAttribute(SORT, \"\");\n mark.setAttribute(\"aria-hidden\", \"true\");\n th.append(mark);\n }\n mark.textContent = term.descending ? \"▼\" : \"▲\";\n mark.toggleAttribute(UNSORTED, this.unsorted());\n th.setAttribute(\"aria-sort\", term.descending ? \"descending\" : \"ascending\");\n } else {\n mark?.remove();\n th.removeAttribute(\"aria-sort\");\n }\n th.toggleAttribute(SORTABLE, writable || (sorted && this.unsorted()));\n }\n }\n\n /** A click on a heading: re-sort, reverse, or sort by its column. */\n private sortFromHeading(e: Event): void {\n const th = (e.target as Element).closest(\"th\");\n const column = th && this.columns().get(th);\n if (!column) return;\n const term = this.leading();\n const sorted = column === term?.column;\n if (sorted && this.unsorted()) {\n const command = new Event(\"command\");\n Object.defineProperty(command, \"command\", { value: COMMAND_SORT });\n this.dispatchEvent(command);\n return;\n }\n const choice = this.getAttribute(ATTR_ORDER);\n if (!choice) return;\n const descending = sorted && !term.descending;\n const detail: HubRequest = {\n name: REQUEST_ROW_UPDATE,\n query: CHOICES_QUERY,\n values: { [choice]: (descending ? ORDER_DESCENDING : \"\") + column },\n };\n this.dispatchEvent(\n new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail }),\n );\n }\n\n /** The columns of this table's rows a choice may filter. */\n private filtered(): string[] {\n return (this.getAttribute(ATTR_FILTER) ?? \"\")\n .split(\",\")\n .map((column) => column.trim())\n .filter(Boolean);\n }\n\n /** A finished selection in one filtered cell, made a choice. */\n private filterBySelection(): void {\n const selection = getSelection();\n if (!selection || selection.isCollapsed || selection.rangeCount === 0) {\n return;\n }\n const range = selection.getRangeAt(0);\n const start = range.startContainer;\n const el = (\n start instanceof Element ? start : start.parentElement\n )?.closest(`[${ATTR_COLUMN}]`);\n if (!el || !el.contains(range.endContainer)) return;\n const row = el.closest(LIVE_ROW);\n if (!row || row.closest(\"lb-table\") !== this) return;\n const column = el.getAttribute(ATTR_COLUMN)!;\n if (!this.filtered().includes(column)) return;\n\n const term = termFor(\n selection.toString().trim(),\n (el.textContent ?? \"\").trim(),\n );\n if (term === null) return;\n selection.removeAllRanges();\n const detail: HubRequest = {\n name: REQUEST_ROW_UPDATE,\n query: CHOICES_QUERY,\n values: { [column]: term },\n };\n this.dispatchEvent(\n new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail }),\n );\n }\n}\n\n/**\n * The columns an element and everything in it show, read as the hub reads a\n * row: into a branch an `lb-show` holds in a template, but not into another\n * template, nor past an element naming a query of its own.\n */\nfunction columnsIn(el: Element): string[] {\n const own = el.getAttribute(ATTR_COLUMN);\n const out = own ? [own] : [];\n if (el.hasAttribute(ATTR_QUERY)) return out;\n const children =\n el instanceof HTMLTemplateElement\n ? el.hasAttribute(ATTR_SHOW)\n ? [...el.content.children]\n : []\n : [...el.children];\n for (const child of children) out.push(...columnsIn(child));\n return out;\n}\n\n/** A filter's term for text selected from a cell's whole text. */\nfunction termFor(selected: string, whole: string): string | null {\n if (selected === \"\") return null;\n if (selected === whole) return `=${selected}`;\n if (whole.startsWith(selected)) return `^=${selected}`;\n if (whole.endsWith(selected)) return `$=${selected}`;\n return `*=${selected}`;\n}\n\n/**\n * The first control in a row the user can type into or choose from. A\n * control a widget wraps is found inside it, since the wrapper takes no focus.\n */\nfunction firstControl(row: Element): HTMLElement | undefined {\n return [\n ...row.querySelectorAll<HTMLElement>(\n 'input:not([type=\"hidden\"]), select, textarea',\n ),\n ].find((el) => !el.matches(\":disabled\") && el.checkVisibility?.() !== false);\n}\n\ncustomElements.define(\"lb-table\", LbTable);\n"]}
1
+ {"version":3,"file":"lb-table.browser.js","sourceRoot":"","sources":["../widgets/lb-table.browser.ts"],"names":[],"mappings":"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","sourcesContent":["/// <reference lib=\"dom\" />\n\nimport {\n ATTR_COLUMN,\n ATTR_FILTER,\n ATTR_GROUP,\n ATTR_QUERY,\n ATTR_QUERY_ORDER,\n ATTR_QUERY_UNPLACED_COUNT,\n ATTR_ROW_REQUESTED,\n ATTR_SHOW,\n COMMAND_PLACE,\n FILTERS_QUERY,\n LB_EVENT_NAME,\n LIVE_ROW,\n ORDER_DESCENDING,\n REQUEST_ROW_INSERT,\n REQUEST_ROW_UPDATE,\n REQUESTED_CREATED,\n REQUESTED_MOVED,\n SORT_COLUMN_ORDER,\n SORT_QUERY,\n} from \"@loadbare/app/constants\";\nimport type {\n HubLifecycle,\n HubRequest,\n HubRequestDone,\n} from \"@loadbare/app/types\";\n\n/**\n * A table that owns its own scaffolding.\n *\n * A plain `<table lb-query>` with a row template inside it needs no widget at\n * all. This one supplies the table, and the developer writes what only the\n * page knows: the heading row and the row template. That division is forced\n * rather than chosen — a `<thead>` written inside a custom element is not\n * inside a table, so the parser drops the tag and keeps the text. A\n * `<template>` survives anywhere, so the heading row arrives wrapped and\n * expansion unwraps it into the `lb-exp-template` destination beside the slot.\n *\n * Placement and groups are the hub's, by the query's order and the page's\n * group templates. What this widget adds at run time is the ghost row,\n * scrolling to the row the page's own request created or moved, filtering\n * by selected text, and the order shown on the heading.\n *\n * The `foot` destination needs no code here. A `<tfoot>` is a place on the\n * page, not a row the hub delivers, so what lands in it is an aggregate or a\n * `row` query of its own, named on its `<tr>`.\n */\n\n/**\n * Text selected in a cell filters the table by that cell's column. When a\n * selection finishes inside one element of a live row whose `lb-column` the\n * table's `lb-filter` names, the table makes the choice of that name, and the\n * hub hides the rows it does not pass. Where the selection lies in the\n * element's text says how:\n *\n * the whole text `=` equals\n * from its start `^=` begins with\n * up to its end `$=` ends with\n * anywhere else `*=` contains\n *\n * The choice replaces whatever that column held, so a control naming the\n * same column of `lb-filters` shows it and clears it. The selection is\n * removed once it is used.\n */\n\n/**\n * The heading of the column the rows are sorted by carries a triangle for the\n * direction, and `aria-sort`. A heading's column is the one `lb-column` in\n * the cell at the same position in the row template, a branch `lb-show`\n * holds in a template included; a cell with none, or with several, gives its\n * heading no column. Only the order's leading term is shown. A grouped table\n * shows none of this.\n *\n * While rows stand out of place the triangle carries `data-unplaced`, and a\n * click on its heading places them again with `--lb-place`. Otherwise a\n * click on the sorted heading reverses the order and a click on another\n * heading sorts by its column, ascending: either way the table writes the\n * order to its query's row of `lb-sort`.\n */\nconst SORT = \"data-sort\";\nconst UNPLACED = \"data-unplaced\";\nconst SORTABLE = \"data-sortable\";\n\n/**\n * The ghost row's `<tbody>`, a blank row for entering a new one, between the\n * rows and the footer. The page writes it as the `ghost` template, so it\n * ships in the document like the heading and the footer, and the hub lands\n * on whatever in it names a query. It is not a live row: the hub never\n * reconciles it and never counts it.\n *\n * A `<form>` cannot wrap a `<tr>`, so the widget makes one outside the table\n * and joins the row's controls and buttons to it with the HTML `form`\n * attribute. Sending is the page's: a button in the row carrying\n * `lb-row-insert` submits that form, and the hub gathers what the form owns.\n * The hub resets the form once the insert succeeds.\n */\nconst GHOST = \"data-ghost\";\n\n/** Ghost row forms need ids for the `form` attribute; any unique one will do. */\nlet ghostForms = 0;\n\nclass LbTable extends HTMLElement implements HubLifecycle {\n constructor() {\n super();\n this.addEventListener(\"mouseup\", () => this.filterBySelection());\n this.addEventListener(\"click\", (e) => this.sortFromHeading(e));\n }\n\n /** Each heading's column, by position in the row template; none when grouped. */\n private headings?: Map<Element, string>;\n\n lbAttached(): void {\n const ghost = this.querySelector(`tbody[${GHOST}]`);\n if (!ghost?.firstElementChild) return;\n\n const form = document.createElement(\"form\");\n form.id = `ghost-row-${++ghostForms}`;\n form.hidden = true;\n this.append(form);\n for (const el of ghost.querySelectorAll(`[${ATTR_COLUMN}], button`)) {\n el.setAttribute(\"form\", form.id);\n }\n }\n\n /** The row this page's request created or moved, brought into view. */\n lbRowsLanded(): void {\n this.showOrder();\n this.querySelector(\n `[${ATTR_ROW_REQUESTED}=\"${REQUESTED_CREATED}\"], ` +\n `[${ATTR_ROW_REQUESTED}=\"${REQUESTED_MOVED}\"]`,\n )?.scrollIntoView({ block: \"nearest\" });\n }\n\n /**\n * A ghost row is for entering the next row, so once one is saved the\n * cursor goes back to the start of it. The hub calls this after the\n * response items have landed, so nothing it places afterwards takes the\n * focus away again. A refused insert leaves the cursor where the user left\n * it.\n */\n lbRequestDone({ request, error }: HubRequestDone, origin: Element): void {\n if (error || request.name !== REQUEST_ROW_INSERT) return;\n const row = origin.closest(`[${GHOST}]`);\n if (row) firstControl(row)?.focus();\n }\n\n /** The headings a column can be read for, and their columns. */\n private columns(): Map<Element, string> {\n if (this.headings) return this.headings;\n this.headings = new Map();\n const body = this.querySelector(`tbody:not([${GHOST}])`);\n const template = body?.querySelector(\":scope > template\");\n const head = this.querySelector(\"thead tr\");\n if (!(template instanceof HTMLTemplateElement) || !head) {\n return this.headings;\n }\n if (template.hasAttribute(ATTR_GROUP)) return this.headings;\n const cells = template.content.querySelector(\"tr\")?.children ?? [];\n [...head.children].forEach((th, i) => {\n const cell = cells[i];\n if (!cell) return;\n const named = new Set(columnsIn(cell));\n if (named.size === 1) this.headings!.set(th, [...named][0]!);\n });\n return this.headings;\n }\n\n /** The order's leading term, from what the hub stamped. */\n private leading(): { column: string; descending: boolean } | undefined {\n const term = (this.getAttribute(ATTR_QUERY_ORDER) ?? \"\").split(\",\")[0];\n if (!term) return undefined;\n const descending = term.startsWith(ORDER_DESCENDING);\n return {\n column: descending ? term.slice(ORDER_DESCENDING.length) : term,\n descending,\n };\n }\n\n /** Whether rows stand where the order or the filter would not leave them. */\n private unplaced(): boolean {\n const count = this.getAttribute(ATTR_QUERY_UNPLACED_COUNT);\n return count !== null && count !== \"0\";\n }\n\n /** The triangle on the sorted heading, and every heading a click sorts. */\n private showOrder(): void {\n const term = this.leading();\n for (const [th, column] of this.columns()) {\n const sorted = column === term?.column;\n let mark = th.querySelector(`:scope > [${SORT}]`);\n if (sorted) {\n if (!mark) {\n mark = document.createElement(\"span\");\n mark.setAttribute(SORT, \"\");\n mark.setAttribute(\"aria-hidden\", \"true\");\n th.append(mark);\n }\n mark.textContent = term.descending ? \"▼\" : \"▲\";\n mark.toggleAttribute(UNPLACED, this.unplaced());\n th.setAttribute(\"aria-sort\", term.descending ? \"descending\" : \"ascending\");\n } else {\n mark?.remove();\n th.removeAttribute(\"aria-sort\");\n }\n th.toggleAttribute(SORTABLE, true);\n }\n }\n\n /** A click on a heading: place the rows, reverse, or sort by its column. */\n private sortFromHeading(e: Event): void {\n const th = (e.target as Element).closest(\"th\");\n const column = th && this.columns().get(th);\n if (!column) return;\n const term = this.leading();\n const sorted = column === term?.column;\n if (sorted && this.unplaced()) {\n const command = new Event(\"command\");\n Object.defineProperty(command, \"command\", { value: COMMAND_PLACE });\n this.dispatchEvent(command);\n return;\n }\n const descending = sorted && !term.descending;\n const detail: HubRequest = {\n name: REQUEST_ROW_UPDATE,\n query: SORT_QUERY,\n key: this.getAttribute(ATTR_QUERY)!,\n values: {\n [SORT_COLUMN_ORDER]: (descending ? ORDER_DESCENDING : \"\") + column,\n },\n };\n this.dispatchEvent(\n new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail }),\n );\n }\n\n /** The columns of this table's rows a choice may filter. */\n private filtered(): string[] {\n return (this.getAttribute(ATTR_FILTER) ?? \"\")\n .split(\",\")\n .map((column) => column.trim())\n .filter(Boolean);\n }\n\n /** A finished selection in one filtered cell, made a choice. */\n private filterBySelection(): void {\n const selection = getSelection();\n if (!selection || selection.isCollapsed || selection.rangeCount === 0) {\n return;\n }\n const range = selection.getRangeAt(0);\n const start = range.startContainer;\n const el = (\n start instanceof Element ? start : start.parentElement\n )?.closest(`[${ATTR_COLUMN}]`);\n if (!el || !el.contains(range.endContainer)) return;\n const row = el.closest(LIVE_ROW);\n if (!row || row.closest(\"lb-table\") !== this) return;\n const column = el.getAttribute(ATTR_COLUMN)!;\n if (!this.filtered().includes(column)) return;\n\n const term = termFor(\n selection.toString().trim(),\n (el.textContent ?? \"\").trim(),\n );\n if (term === null) return;\n selection.removeAllRanges();\n const detail: HubRequest = {\n name: REQUEST_ROW_UPDATE,\n query: FILTERS_QUERY,\n key: this.getAttribute(ATTR_QUERY)!,\n values: { [column]: term },\n };\n this.dispatchEvent(\n new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail }),\n );\n }\n}\n\n/**\n * The columns an element and everything in it show, read as the hub reads a\n * row: into a branch an `lb-show` holds in a template, but not into another\n * template, nor past an element naming a query of its own.\n */\nfunction columnsIn(el: Element): string[] {\n const own = el.getAttribute(ATTR_COLUMN);\n const out = own ? [own] : [];\n if (el.hasAttribute(ATTR_QUERY)) return out;\n const children =\n el instanceof HTMLTemplateElement\n ? el.hasAttribute(ATTR_SHOW)\n ? [...el.content.children]\n : []\n : [...el.children];\n for (const child of children) out.push(...columnsIn(child));\n return out;\n}\n\n/** A filter's term for text selected from a cell's whole text. */\nfunction termFor(selected: string, whole: string): string | null {\n if (selected === \"\") return null;\n if (selected === whole) return `=${selected}`;\n if (whole.startsWith(selected)) return `^=${selected}`;\n if (whole.endsWith(selected)) return `$=${selected}`;\n return `*=${selected}`;\n}\n\n/**\n * The first control in a row the user can type into or choose from. A\n * control a widget wraps is found inside it, since the wrapper takes no focus.\n */\nfunction firstControl(row: Element): HTMLElement | undefined {\n return [\n ...row.querySelectorAll<HTMLElement>(\n 'input:not([type=\"hidden\"]), select, textarea',\n ),\n ].find((el) => !el.matches(\":disabled\") && el.checkVisibility?.() !== false);\n}\n\ncustomElements.define(\"lb-table\", LbTable);\n"]}
package/dist/lb-table.css CHANGED
@@ -15,11 +15,11 @@ lb-table th > [data-sort] {
15
15
  line-height: 1.3;
16
16
  }
17
17
 
18
- lb-table th > [data-sort][data-unsorted] {
18
+ lb-table th > [data-sort][data-unplaced] {
19
19
  color: #b58900;
20
20
  border-color: currentColor;
21
21
  }
22
22
 
23
- lb-table tr[lb-row-unsorted] > :first-child {
23
+ lb-table tr[lb-row-unplaced] > :first-child {
24
24
  box-shadow: inset 3px 0 0 #b58900;
25
25
  }
package/docs/lb-table.md CHANGED
@@ -23,7 +23,7 @@ Provide `exp-caption`.
23
23
  </template>
24
24
  </template>
25
25
  <template lb-exp-template="foot">
26
- <tr><td>Total</td><td lb-sum="amount"></td></tr>
26
+ <tr><td>Total</td><td lb-aggregate="sum" lb-column="amount"></td></tr>
27
27
  </template>
28
28
  </lb-table>
29
29
  ```
@@ -50,23 +50,25 @@ improvements through iterative use.
50
50
  | `lb-group` | a template holding the row template | a group, one per run of the order's leading term |
51
51
  | `lb-order` | the element itself | the choice holding the user's order |
52
52
  | `lb-filter` | the element itself | the columns the user's choices filter |
53
+ | `lb-manual` | the element itself | `order`, `filter` or both: what an answer leaves where it stands |
53
54
  | `lb-column` | elements inside the row template | the column each element shows |
54
55
 
55
56
  The server declares the query's key and its order.
56
57
 
57
58
  ## Order and groups
58
59
 
59
- The hub places the rows by the query's order and builds its groups. An
60
- edit from a control in a row, or from the ghost row, leaves the rows where
61
- they stand until the user asks for the order; a button with
62
- `commandfor` naming the table's `id` and `command="--lb-sort"` does that.
60
+ The hub places the rows by the query's order and builds its groups. A
61
+ table edited in its rows or its ghost row usually carries
62
+ `lb-manual="order"`, so an edited row stays where it stands and a new one
63
+ goes last until the user asks for the order; a button with `commandfor`
64
+ naming the table's `id` and `command="--lb-place"` does that.
63
65
  The rows land in the table's one `<tbody>`, and a `<tbody>` cannot hold another,
64
66
  so a group here is a heading row with its rows after it. For a `<tbody>` per
65
67
  group, write a plain `<table lb-query>` whose group template is the
66
68
  `<tbody>`.
67
69
 
68
- `lb-order` and `lb-filter` work on `lb-table` as on any element carrying
69
- `lb-query`: the user's order replaces the query's, and a row the user's
70
+ `lb-order`, `lb-filter` and `lb-manual` work on `lb-table` as on any
71
+ element carrying `lb-query`: the user's order replaces the query's, and a row the user's
70
72
  filter does not pass is hidden.
71
73
 
72
74
  ## The order on the heading
@@ -78,25 +80,25 @@ is shown. A heading's column is the `lb-column` in the cell
78
80
  at the same position in the row template, on the cell or inside it. A cell
79
81
  naming no column, or several, gives its heading none.
80
82
 
81
- While rows stand out of order, the triangle carries `data-unsorted`, and a
82
- click on its heading places them again, as `--lb-sort` does, keeping the
83
+ While rows stand out of place, the triangle carries `data-unplaced`, and a
84
+ click on its heading places them again, as `--lb-place` does, keeping the
83
85
  direction.
84
86
 
85
87
  With `lb-order` on the table, a click on the sorted heading reverses the
86
88
  order, and a click on another heading sorts by its column, ascending. The
87
89
  widget writes the order to the column of `lb-choices` that `lb-order`
88
90
  names. Without `lb-order` there is nowhere to write one, and only the
89
- re-sort answers a click. There is no click back to the server's order.
91
+ placing answers a click. There is no click back to the server's order.
90
92
 
91
93
  A heading that takes a click carries `data-sortable`.
92
94
 
93
95
  None of this shows on a grouped table, nor when the order's leading column
94
- has no heading. There, a button with `commandfor` and `--lb-sort` does the
95
- re-sort.
96
+ has no heading. There, a button with `commandfor` and `--lb-place` places
97
+ the rows.
96
98
 
97
99
  The package ships `lb-table.css`: the triangle turns amber and circled
98
- while rows stand out of order, and the first cell of a row carrying
99
- `lb-row-unsorted` is marked on its leading edge. The application's own
100
+ while rows stand out of place, and the first cell of a row carrying
101
+ `lb-row-unplaced` is marked on its leading edge. The application's own
100
102
  stylesheet comes after it and overrides any of it.
101
103
 
102
104
  ## Filtering by selection
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@loadbare/widgets",
3
3
  "description": "The basic widget set for @loadbare/app: input, select, options, picker, table, unknown-page, confirm, form, insert-dialog, master, checkbox, number, date, tel, mailto",
4
- "version": "0.11.1",
4
+ "version": "0.13.0",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "dist",
@@ -23,7 +23,7 @@ Provide `exp-caption`.
23
23
  </template>
24
24
  </template>
25
25
  <template lb-exp-template="foot">
26
- <tr><td>Total</td><td lb-sum="amount"></td></tr>
26
+ <tr><td>Total</td><td lb-aggregate="sum" lb-column="amount"></td></tr>
27
27
  </template>
28
28
  </lb-table>
29
29
  ```
@@ -50,23 +50,25 @@ improvements through iterative use.
50
50
  | `lb-group` | a template holding the row template | a group, one per run of the order's leading term |
51
51
  | `lb-order` | the element itself | the choice holding the user's order |
52
52
  | `lb-filter` | the element itself | the columns the user's choices filter |
53
+ | `lb-manual` | the element itself | `order`, `filter` or both: what an answer leaves where it stands |
53
54
  | `lb-column` | elements inside the row template | the column each element shows |
54
55
 
55
56
  The server declares the query's key and its order.
56
57
 
57
58
  ## Order and groups
58
59
 
59
- The hub places the rows by the query's order and builds its groups. An
60
- edit from a control in a row, or from the ghost row, leaves the rows where
61
- they stand until the user asks for the order; a button with
62
- `commandfor` naming the table's `id` and `command="--lb-sort"` does that.
60
+ The hub places the rows by the query's order and builds its groups. A
61
+ table edited in its rows or its ghost row usually carries
62
+ `lb-manual="order"`, so an edited row stays where it stands and a new one
63
+ goes last until the user asks for the order; a button with `commandfor`
64
+ naming the table's `id` and `command="--lb-place"` does that.
63
65
  The rows land in the table's one `<tbody>`, and a `<tbody>` cannot hold another,
64
66
  so a group here is a heading row with its rows after it. For a `<tbody>` per
65
67
  group, write a plain `<table lb-query>` whose group template is the
66
68
  `<tbody>`.
67
69
 
68
- `lb-order` and `lb-filter` work on `lb-table` as on any element carrying
69
- `lb-query`: the user's order replaces the query's, and a row the user's
70
+ `lb-order`, `lb-filter` and `lb-manual` work on `lb-table` as on any
71
+ element carrying `lb-query`: the user's order replaces the query's, and a row the user's
70
72
  filter does not pass is hidden.
71
73
 
72
74
  ## The order on the heading
@@ -78,25 +80,25 @@ is shown. A heading's column is the `lb-column` in the cell
78
80
  at the same position in the row template, on the cell or inside it. A cell
79
81
  naming no column, or several, gives its heading none.
80
82
 
81
- While rows stand out of order, the triangle carries `data-unsorted`, and a
82
- click on its heading places them again, as `--lb-sort` does, keeping the
83
+ While rows stand out of place, the triangle carries `data-unplaced`, and a
84
+ click on its heading places them again, as `--lb-place` does, keeping the
83
85
  direction.
84
86
 
85
87
  With `lb-order` on the table, a click on the sorted heading reverses the
86
88
  order, and a click on another heading sorts by its column, ascending. The
87
89
  widget writes the order to the column of `lb-choices` that `lb-order`
88
90
  names. Without `lb-order` there is nowhere to write one, and only the
89
- re-sort answers a click. There is no click back to the server's order.
91
+ placing answers a click. There is no click back to the server's order.
90
92
 
91
93
  A heading that takes a click carries `data-sortable`.
92
94
 
93
95
  None of this shows on a grouped table, nor when the order's leading column
94
- has no heading. There, a button with `commandfor` and `--lb-sort` does the
95
- re-sort.
96
+ has no heading. There, a button with `commandfor` and `--lb-place` places
97
+ the rows.
96
98
 
97
99
  The package ships `lb-table.css`: the triangle turns amber and circled
98
- while rows stand out of order, and the first cell of a row carrying
99
- `lb-row-unsorted` is marked on its leading edge. The application's own
100
+ while rows stand out of place, and the first cell of a row carrying
101
+ `lb-row-unplaced` is marked on its leading edge. The application's own
100
102
  stylesheet comes after it and overrides any of it.
101
103
 
102
104
  ## Filtering by selection