@loadbare/widgets 0.11.1 → 0.12.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.
@@ -3,10 +3,7 @@ import { WrappedControl } from "./control.js";
3
3
  /**
4
4
  * A `<select>` whose choices are the rows of the query it names.
5
5
  *
6
- * The hub places the options by the query's order, and a group template
7
- * holding an `<optgroup>` puts them in groups. What landing does not give an
8
- * `<optgroup>` is its label, so the widget sets it from the `lb-group-value`
9
- * the hub stamps on it.
6
+ * The hub places the options by the query's order.
10
7
  *
11
8
  * The identity of a row is the value of its option, so the `lb-key-value` the
12
9
  * hub stamps on each option is copied to its `value`. The key column is the
@@ -1 +1 @@
1
- {"version":3,"file":"lb-options.browser.d.ts","sourceRoot":"","sources":["../widgets/lb-options.browser.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,qBAAa,SAAU,SAAQ,cAAe,YAAW,YAAY;IACnE,SAAS,CAAC,KAAK,SAAY;IAE3B;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAuB;IAErC,IAAI,KAAK,IAAI,MAAM,CAElB;IAED,IAAI,KAAK,CAAC,KAAK,EAAE,MAAM,EAGtB;IAED,iBAAiB,IAAI,IAAI;IAKzB,SAAS,CAAC,OAAO,IAAI,IAAI;IAKzB,0EAA0E;IAC1E,YAAY,IAAI,IAAI;CAsBrB"}
1
+ {"version":3,"file":"lb-options.browser.d.ts","sourceRoot":"","sources":["../widgets/lb-options.browser.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,qBAAa,SAAU,SAAQ,cAAe,YAAW,YAAY;IACnE,SAAS,CAAC,KAAK,SAAY;IAE3B;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAuB;IAErC,IAAI,KAAK,IAAI,MAAM,CAElB;IAED,IAAI,KAAK,CAAC,KAAK,EAAE,MAAM,EAGtB;IAED,iBAAiB,IAAI,IAAI;IAKzB,SAAS,CAAC,OAAO,IAAI,IAAI;IAKzB,0EAA0E;IAC1E,YAAY,IAAI,IAAI;CAarB"}
@@ -1,13 +1,10 @@
1
1
  /// <reference lib="dom" />
2
- import { ATTR_GROUP_VALUE, ATTR_KEY_VALUE, LIVE_GROUP, LIVE_ROW, } from "@loadbare/app/constants";
2
+ import { ATTR_KEY_VALUE, LIVE_ROW, } from "@loadbare/app/constants";
3
3
  import { WrappedControl } from "./control.js";
4
4
  /**
5
5
  * A `<select>` whose choices are the rows of the query it names.
6
6
  *
7
- * The hub places the options by the query's order, and a group template
8
- * holding an `<optgroup>` puts them in groups. What landing does not give an
9
- * `<optgroup>` is its label, so the widget sets it from the `lb-group-value`
10
- * the hub stamps on it.
7
+ * The hub places the options by the query's order.
11
8
  *
12
9
  * The identity of a row is the value of its option, so the `lb-key-value` the
13
10
  * hub stamps on each option is copied to its `value`. The key column is the
@@ -58,13 +55,6 @@ export class LbOptions extends WrappedControl {
58
55
  if (option.getAttribute("value") !== key)
59
56
  option.setAttribute("value", key);
60
57
  }
61
- for (const group of this.querySelectorAll(`optgroup${LIVE_GROUP}`)) {
62
- const heading = group.querySelector(":scope > option[data-group-label]");
63
- const label = heading?.textContent?.trim() ||
64
- (group.getAttribute(ATTR_GROUP_VALUE) ?? "");
65
- if (group.label !== label)
66
- group.label = label;
67
- }
68
58
  // After the values above, since the select matches against them.
69
59
  const select = this.querySelector("select");
70
60
  if (select && this.chosen !== null)
@@ -1 +1 @@
1
- {"version":3,"file":"lb-options.browser.js","sourceRoot":"","sources":["../widgets/lb-options.browser.ts"],"names":[],"mappings":"AAAA,2BAA2B;AAE3B,OAAO,EACL,gBAAgB,EAChB,cAAc,EACd,UAAU,EACV,QAAQ,GACT,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,MAAM,OAAO,SAAU,SAAQ,cAAc;IACjC,KAAK,GAAG,QAAQ,CAAC;IAE3B;;;OAGG;IACK,MAAM,GAAkB,IAAI,CAAC;IAErC,IAAI,KAAK;QACP,OAAO,IAAI,CAAC,MAAM,IAAI,KAAK,CAAC,KAAK,CAAC;IACpC,CAAC;IAED,IAAI,KAAK,CAAC,KAAa;QACrB,IAAI,CAAC,MAAM,GAAG,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAC3E,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC;IACtB,CAAC;IAED,iBAAiB;QACf,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACnB,KAAK,CAAC,iBAAiB,EAAE,CAAC;IAC5B,CAAC;IAES,OAAO;QACf,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,OAAO,EAAE,KAAK,IAAI,IAAI,CAAC;QAC1C,KAAK,CAAC,OAAO,EAAE,CAAC;IAClB,CAAC;IAED,0EAA0E;IAC1E,YAAY;QACV,uEAAuE;QACvE,0BAA0B;QAC1B,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,gBAAgB,CAAC,SAAS,QAAQ,EAAE,CAAC,EAAE,CAAC;YAChE,MAAM,GAAG,GAAG,MAAM,CAAC,YAAY,CAAC,cAAc,CAAE,CAAC;YACjD,IAAI,MAAM,CAAC,YAAY,CAAC,OAAO,CAAC,KAAK,GAAG;gBACtC,MAAM,CAAC,YAAY,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;QACtC,CAAC;QACD,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,gBAAgB,CACvC,WAAW,UAAU,EAAE,CACxB,EAAE,CAAC;YACF,MAAM,OAAO,GAAG,KAAK,CAAC,aAAa,CAAC,mCAAmC,CAAC,CAAC;YACzE,MAAM,KAAK,GACT,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE;gBAC5B,CAAC,KAAK,CAAC,YAAY,CAAC,gBAAgB,CAAC,IAAI,EAAE,CAAC,CAAC;YAC/C,IAAI,KAAK,CAAC,KAAK,KAAK,KAAK;gBAAE,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC;QACjD,CAAC;QACD,iEAAiE;QACjE,MAAM,MAAM,GAAG,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;QAC5C,IAAI,MAAM,IAAI,IAAI,CAAC,MAAM,KAAK,IAAI;YAAE,MAAM,CAAC,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC;QAC/D,IAAI,CAAC,SAAS,EAAE,CAAC;IACnB,CAAC;CACF;AAED,cAAc,CAAC,MAAM,CAAC,YAAY,EAAE,SAAS,CAAC,CAAC","sourcesContent":["/// <reference lib=\"dom\" />\n\nimport {\n ATTR_GROUP_VALUE,\n ATTR_KEY_VALUE,\n LIVE_GROUP,\n LIVE_ROW,\n} from \"@loadbare/app/constants\";\nimport type { HubLifecycle } from \"@loadbare/app/types\";\nimport { WrappedControl } from \"./control.js\";\n\n/**\n * A `<select>` whose choices are the rows of the query it names.\n *\n * The hub places the options by the query's order, and a group template\n * holding an `<optgroup>` puts them in groups. What landing does not give an\n * `<optgroup>` is its label, so the widget sets it from the `lb-group-value`\n * the hub stamps on it.\n *\n * The identity of a row is the value of its option, so the `lb-key-value` the\n * hub stamps on each option is copied to its `value`. The key column is the\n * one the server declared for the query; the page never names it.\n *\n * It is two things at once, and the hub keeps them apart: its own `lb-query`\n * names the rows it offers, and its `lb-column` is a column of the row around\n * it, which is what it holds and what it sends. As a control it is\n * form-associated, with a `value` and a `change` of its own, so the hub lands\n * that column on `value` and commits `lb-request` on `change`.\n *\n * The value is held here as well as on the select. A `<select>` does not hold\n * a value it has no option for: it selects nothing and forgets it. The value\n * and the choices are two answers that land in either order, so the held\n * value is put on the select again whenever rows land.\n *\n * Exported because `lb-picker` is this widget with its row template supplied\n * by the definition instead of by the page.\n */\n\nexport class LbOptions extends WrappedControl implements HubLifecycle {\n protected inner = \"select\";\n\n /**\n * The value last set or chosen, which the options may not include yet.\n * Null when nothing chose one, so the select's own default stands.\n */\n private chosen: string | null = null;\n\n get value(): string {\n return this.chosen ?? super.value;\n }\n\n set value(value: string) {\n this.chosen = value === null || value === undefined ? null : String(value);\n super.value = value;\n }\n\n formResetCallback(): void {\n this.chosen = null;\n super.formResetCallback();\n }\n\n protected changed(): void {\n this.chosen = this.control?.value ?? null;\n super.changed();\n }\n\n /** Everything derived from the rows, once the whole answer has landed. */\n lbRowsLanded(): void {\n // Written only when they differ: a refresh that changed nothing leaves\n // every option as it was.\n for (const option of this.querySelectorAll(`option${LIVE_ROW}`)) {\n const key = option.getAttribute(ATTR_KEY_VALUE)!;\n if (option.getAttribute(\"value\") !== key)\n option.setAttribute(\"value\", key);\n }\n for (const group of this.querySelectorAll<HTMLOptGroupElement>(\n `optgroup${LIVE_GROUP}`,\n )) {\n const heading = group.querySelector(\":scope > option[data-group-label]\");\n const label =\n heading?.textContent?.trim() ||\n (group.getAttribute(ATTR_GROUP_VALUE) ?? \"\");\n if (group.label !== label) group.label = label;\n }\n // After the values above, since the select matches against them.\n const select = this.querySelector(\"select\");\n if (select && this.chosen !== null) select.value = this.chosen;\n this.formValue();\n }\n}\n\ncustomElements.define(\"lb-options\", LbOptions);\n"]}
1
+ {"version":3,"file":"lb-options.browser.js","sourceRoot":"","sources":["../widgets/lb-options.browser.ts"],"names":[],"mappings":"AAAA,2BAA2B;AAE3B,OAAO,EACL,cAAc,EACd,QAAQ,GACT,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,MAAM,OAAO,SAAU,SAAQ,cAAc;IACjC,KAAK,GAAG,QAAQ,CAAC;IAE3B;;;OAGG;IACK,MAAM,GAAkB,IAAI,CAAC;IAErC,IAAI,KAAK;QACP,OAAO,IAAI,CAAC,MAAM,IAAI,KAAK,CAAC,KAAK,CAAC;IACpC,CAAC;IAED,IAAI,KAAK,CAAC,KAAa;QACrB,IAAI,CAAC,MAAM,GAAG,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAC3E,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC;IACtB,CAAC;IAED,iBAAiB;QACf,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACnB,KAAK,CAAC,iBAAiB,EAAE,CAAC;IAC5B,CAAC;IAES,OAAO;QACf,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,OAAO,EAAE,KAAK,IAAI,IAAI,CAAC;QAC1C,KAAK,CAAC,OAAO,EAAE,CAAC;IAClB,CAAC;IAED,0EAA0E;IAC1E,YAAY;QACV,uEAAuE;QACvE,0BAA0B;QAC1B,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,gBAAgB,CAAC,SAAS,QAAQ,EAAE,CAAC,EAAE,CAAC;YAChE,MAAM,GAAG,GAAG,MAAM,CAAC,YAAY,CAAC,cAAc,CAAE,CAAC;YACjD,IAAI,MAAM,CAAC,YAAY,CAAC,OAAO,CAAC,KAAK,GAAG;gBACtC,MAAM,CAAC,YAAY,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;QACtC,CAAC;QACD,iEAAiE;QACjE,MAAM,MAAM,GAAG,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;QAC5C,IAAI,MAAM,IAAI,IAAI,CAAC,MAAM,KAAK,IAAI;YAAE,MAAM,CAAC,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC;QAC/D,IAAI,CAAC,SAAS,EAAE,CAAC;IACnB,CAAC;CACF;AAED,cAAc,CAAC,MAAM,CAAC,YAAY,EAAE,SAAS,CAAC,CAAC","sourcesContent":["/// <reference lib=\"dom\" />\n\nimport {\n ATTR_KEY_VALUE,\n LIVE_ROW,\n} from \"@loadbare/app/constants\";\nimport type { HubLifecycle } from \"@loadbare/app/types\";\nimport { WrappedControl } from \"./control.js\";\n\n/**\n * A `<select>` whose choices are the rows of the query it names.\n *\n * The hub places the options by the query's order.\n *\n * The identity of a row is the value of its option, so the `lb-key-value` the\n * hub stamps on each option is copied to its `value`. The key column is the\n * one the server declared for the query; the page never names it.\n *\n * It is two things at once, and the hub keeps them apart: its own `lb-query`\n * names the rows it offers, and its `lb-column` is a column of the row around\n * it, which is what it holds and what it sends. As a control it is\n * form-associated, with a `value` and a `change` of its own, so the hub lands\n * that column on `value` and commits `lb-request` on `change`.\n *\n * The value is held here as well as on the select. A `<select>` does not hold\n * a value it has no option for: it selects nothing and forgets it. The value\n * and the choices are two answers that land in either order, so the held\n * value is put on the select again whenever rows land.\n *\n * Exported because `lb-picker` is this widget with its row template supplied\n * by the definition instead of by the page.\n */\n\nexport class LbOptions extends WrappedControl implements HubLifecycle {\n protected inner = \"select\";\n\n /**\n * The value last set or chosen, which the options may not include yet.\n * Null when nothing chose one, so the select's own default stands.\n */\n private chosen: string | null = null;\n\n get value(): string {\n return this.chosen ?? super.value;\n }\n\n set value(value: string) {\n this.chosen = value === null || value === undefined ? null : String(value);\n super.value = value;\n }\n\n formResetCallback(): void {\n this.chosen = null;\n super.formResetCallback();\n }\n\n protected changed(): void {\n this.chosen = this.control?.value ?? null;\n super.changed();\n }\n\n /** Everything derived from the rows, once the whole answer has landed. */\n lbRowsLanded(): void {\n // Written only when they differ: a refresh that changed nothing leaves\n // every option as it was.\n for (const option of this.querySelectorAll(`option${LIVE_ROW}`)) {\n const key = option.getAttribute(ATTR_KEY_VALUE)!;\n if (option.getAttribute(\"value\") !== key)\n option.setAttribute(\"value\", key);\n }\n // After the values above, since the select matches against them.\n const select = this.querySelector(\"select\");\n if (select && this.chosen !== null) select.value = this.chosen;\n this.formValue();\n }\n}\n\ncustomElements.define(\"lb-options\", LbOptions);\n"]}
@@ -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_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
  *
@@ -11,14 +11,14 @@ import { ATTR_COLUMN, ATTR_FILTER, ATTR_GROUP, ATTR_ORDER, ATTR_QUERY, ATTR_QUER
11
11
  * `<template>` survives anywhere, so the heading row arrives wrapped and
12
12
  * expansion unwraps it into the `lb-exp-template` destination beside the slot.
13
13
  *
14
- * Placement and groups are the hub's, by the query's order and the page's
15
- * group templates. What this widget adds at run time is the ghost row,
16
- * scrolling to the row the page's own request created or moved, filtering
17
- * by selected text, and the order shown on the heading.
14
+ * Placement is the hub's, by the query's order. What this widget adds at
15
+ * run time is the ghost row, scrolling to the row the page's own request
16
+ * created or moved, filtering by selected text, and the order shown on the
17
+ * heading.
18
18
  *
19
19
  * The `foot` destination needs no code here. A `<tfoot>` is a place on the
20
- * page, not a row the hub delivers, so what lands in it is an aggregate or a
21
- * `row` query of its own, named on its `<tr>`.
20
+ * page, not a row the hub delivers, so what lands in it is a `row` query of
21
+ * its own, named on its `<tr>`.
22
22
  */
23
23
  /**
24
24
  * Text selected in a cell filters the table by that cell's column. When a
@@ -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,16 @@ 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.
46
45
  *
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.
46
+ * While rows stand out of place the triangle carries `data-unplaced`, and a
47
+ * click on its heading places them again with `--lb-place`. Otherwise a
48
+ * click on the sorted heading reverses the order and a click on another
49
+ * heading sorts by its column, ascending: either way the table writes the
50
+ * order to its query's row of `lb-sort`.
53
51
  */
54
52
  const SORT = "data-sort";
55
- const UNSORTED = "data-unsorted";
53
+ const UNPLACED = "data-unplaced";
56
54
  const SORTABLE = "data-sortable";
57
55
  /**
58
56
  * The ghost row's `<tbody>`, a blank row for entering a new one, between the
@@ -121,8 +119,6 @@ class LbTable extends HTMLElement {
121
119
  if (!(template instanceof HTMLTemplateElement) || !head) {
122
120
  return this.headings;
123
121
  }
124
- if (template.hasAttribute(ATTR_GROUP))
125
- return this.headings;
126
122
  const cells = template.content.querySelector("tr")?.children ?? [];
127
123
  [...head.children].forEach((th, i) => {
128
124
  const cell = cells[i];
@@ -145,15 +141,14 @@ class LbTable extends HTMLElement {
145
141
  descending,
146
142
  };
147
143
  }
148
- /** Whether rows stand where the order would not put them. */
149
- unsorted() {
150
- const count = this.getAttribute(ATTR_QUERY_UNSORTED_COUNT);
144
+ /** Whether rows stand where the order or the filter would not leave them. */
145
+ unplaced() {
146
+ const count = this.getAttribute(ATTR_QUERY_UNPLACED_COUNT);
151
147
  return count !== null && count !== "0";
152
148
  }
153
149
  /** The triangle on the sorted heading, and every heading a click sorts. */
154
150
  showOrder() {
155
151
  const term = this.leading();
156
- const writable = this.hasAttribute(ATTR_ORDER);
157
152
  for (const [th, column] of this.columns()) {
158
153
  const sorted = column === term?.column;
159
154
  let mark = th.querySelector(`:scope > [${SORT}]`);
@@ -165,17 +160,17 @@ class LbTable extends HTMLElement {
165
160
  th.append(mark);
166
161
  }
167
162
  mark.textContent = term.descending ? "▼" : "▲";
168
- mark.toggleAttribute(UNSORTED, this.unsorted());
163
+ mark.toggleAttribute(UNPLACED, this.unplaced());
169
164
  th.setAttribute("aria-sort", term.descending ? "descending" : "ascending");
170
165
  }
171
166
  else {
172
167
  mark?.remove();
173
168
  th.removeAttribute("aria-sort");
174
169
  }
175
- th.toggleAttribute(SORTABLE, writable || (sorted && this.unsorted()));
170
+ th.toggleAttribute(SORTABLE, true);
176
171
  }
177
172
  }
178
- /** A click on a heading: re-sort, reverse, or sort by its column. */
173
+ /** A click on a heading: place the rows, reverse, or sort by its column. */
179
174
  sortFromHeading(e) {
180
175
  const th = e.target.closest("th");
181
176
  const column = th && this.columns().get(th);
@@ -183,20 +178,20 @@ class LbTable extends HTMLElement {
183
178
  return;
184
179
  const term = this.leading();
185
180
  const sorted = column === term?.column;
186
- if (sorted && this.unsorted()) {
181
+ if (sorted && this.unplaced()) {
187
182
  const command = new Event("command");
188
- Object.defineProperty(command, "command", { value: COMMAND_SORT });
183
+ Object.defineProperty(command, "command", { value: COMMAND_PLACE });
189
184
  this.dispatchEvent(command);
190
185
  return;
191
186
  }
192
- const choice = this.getAttribute(ATTR_ORDER);
193
- if (!choice)
194
- return;
195
187
  const descending = sorted && !term.descending;
196
188
  const detail = {
197
189
  name: REQUEST_ROW_UPDATE,
198
- query: CHOICES_QUERY,
199
- values: { [choice]: (descending ? ORDER_DESCENDING : "") + column },
190
+ query: SORT_QUERY,
191
+ key: this.getAttribute(ATTR_QUERY),
192
+ values: {
193
+ [SORT_COLUMN_ORDER]: (descending ? ORDER_DESCENDING : "") + column,
194
+ },
200
195
  };
201
196
  this.dispatchEvent(new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail }));
202
197
  }
@@ -230,7 +225,8 @@ class LbTable extends HTMLElement {
230
225
  selection.removeAllRanges();
231
226
  const detail = {
232
227
  name: REQUEST_ROW_UPDATE,
233
- query: CHOICES_QUERY,
228
+ query: FILTERS_QUERY,
229
+ key: this.getAttribute(ATTR_QUERY),
234
230
  values: { [column]: term },
235
231
  };
236
232
  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_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 is the hub's, by the query's order. What this widget adds at\n * run time is the ghost row, scrolling to the row the page's own request\n * created or moved, filtering by selected text, and the order shown on the\n * 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 a `row` query of\n * 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.\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 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.12.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