@loadbare/widgets 0.4.0 → 0.6.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.
Files changed (54) hide show
  1. package/README.md +9 -5
  2. package/dist/control.d.ts +54 -0
  3. package/dist/control.d.ts.map +1 -0
  4. package/dist/control.js +100 -0
  5. package/dist/control.js.map +1 -0
  6. package/dist/index.d.ts +3 -0
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +3 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/lb-confirm.browser.d.ts +2 -0
  11. package/dist/lb-confirm.browser.d.ts.map +1 -0
  12. package/dist/lb-confirm.browser.js +36 -0
  13. package/dist/lb-confirm.browser.js.map +1 -0
  14. package/dist/lb-confirm.html +27 -0
  15. package/dist/lb-form.browser.d.ts +2 -0
  16. package/dist/lb-form.browser.d.ts.map +1 -0
  17. package/dist/lb-form.browser.js +28 -0
  18. package/dist/lb-form.browser.js.map +1 -0
  19. package/dist/lb-form.html +7 -0
  20. package/dist/lb-input.browser.js +9 -37
  21. package/dist/lb-input.browser.js.map +1 -1
  22. package/dist/lb-input.html +1 -1
  23. package/dist/lb-insert-dialog.browser.d.ts +2 -0
  24. package/dist/lb-insert-dialog.browser.d.ts.map +1 -0
  25. package/dist/lb-insert-dialog.browser.js +59 -0
  26. package/dist/lb-insert-dialog.browser.js.map +1 -0
  27. package/dist/lb-insert-dialog.html +25 -0
  28. package/dist/lb-master.html +51 -0
  29. package/dist/lb-options.browser.d.ts +38 -11
  30. package/dist/lb-options.browser.d.ts.map +1 -1
  31. package/dist/lb-options.browser.js +50 -80
  32. package/dist/lb-options.browser.js.map +1 -1
  33. package/dist/lb-options.html +1 -1
  34. package/dist/lb-picker.browser.js +9 -11
  35. package/dist/lb-picker.browser.js.map +1 -1
  36. package/dist/lb-picker.html +2 -2
  37. package/dist/lb-select.browser.js +7 -39
  38. package/dist/lb-select.browser.js.map +1 -1
  39. package/dist/lb-select.html +1 -1
  40. package/dist/lb-table.browser.js +63 -90
  41. package/dist/lb-table.browser.js.map +1 -1
  42. package/dist/lb-table.html +4 -3
  43. package/dist/lb-unknown-page.html +2 -4
  44. package/docs/lb-confirm.md +45 -0
  45. package/docs/lb-form.md +33 -0
  46. package/docs/lb-input.md +42 -40
  47. package/docs/lb-insert-dialog.md +50 -0
  48. package/docs/lb-master.md +53 -0
  49. package/docs/lb-options.md +54 -34
  50. package/docs/lb-picker.md +24 -24
  51. package/docs/lb-select.md +37 -19
  52. package/docs/lb-table.md +63 -29
  53. package/docs/lb-unknown-page.md +11 -13
  54. package/package.json +4 -4
@@ -1,16 +1,43 @@
1
- import type { ListHost, Row } from "@loadbare/app/types";
2
- export declare class LbOptions extends HTMLElement implements ListHost {
3
- static observedAttributes: string[];
4
- attributeChangedCallback(_name: string, _old: string, value: string): void;
1
+ import type { RowsHost } from "@loadbare/app/types";
2
+ import { WrappedControl } from "./control.js";
3
+ /**
4
+ * A `<select>` whose choices are the rows of the query it names.
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.
10
+ *
11
+ * The identity of a row is the value of its option, so the `lb-key-value` the
12
+ * hub stamps on each option is copied to its `value`. The key column is the
13
+ * one the server declared for the query; the page never names it.
14
+ *
15
+ * It is two things at once, and the hub keeps them apart: its own `lb-query`
16
+ * names the rows it offers, and its `lb-column` is a column of the row around
17
+ * it, which is what it holds and what it sends. As a control it is
18
+ * form-associated, with a `value` and a `change` of its own, so the hub lands
19
+ * that column on `value` and commits `lb-request` on `change`.
20
+ *
21
+ * The value is held here as well as on the select. A `<select>` does not hold
22
+ * a value it has no option for: it selects nothing and forgets it. The value
23
+ * and the choices are two answers that land in either order, so the held
24
+ * value is put on the select again whenever rows land.
25
+ *
26
+ * Exported because `lb-picker` is this widget with its row template supplied
27
+ * by the definition instead of by the page.
28
+ */
29
+ export declare class LbOptions extends WrappedControl implements RowsHost {
30
+ protected inner: string;
5
31
  /**
6
- * The hub reconciles the list and calls this for placement only, so this
7
- * widget carries no copy of what a row is or when one arrives.
32
+ * The value last set or chosen, which the options may not include yet.
33
+ * Null when nothing chose one, so the select's own default stands.
8
34
  */
9
- lbPlaceRow(el: Element, row: Row, template: HTMLTemplateElement): void;
10
- /** Everything derived from the rows, once the whole result has landed. */
35
+ private chosen;
36
+ get value(): string;
37
+ set value(value: string);
38
+ formResetCallback(): void;
39
+ protected changed(): void;
40
+ /** Everything derived from the rows, once the whole answer has landed. */
11
41
  lbRowsLanded(): void;
12
- /** The group with this label, or a new one in front of the template. */
13
- private groupFor;
14
- connectedCallback(): void;
15
42
  }
16
43
  //# sourceMappingURL=lb-options.browser.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"lb-options.browser.d.ts","sourceRoot":"","sources":["../widgets/lb-options.browser.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAc,QAAQ,EAAE,GAAG,EAAE,MAAM,qBAAqB,CAAC;AA6BrE,qBAAa,SAAU,SAAQ,WAAY,YAAW,QAAQ;IAC5D,MAAM,CAAC,kBAAkB,WAAgB;IAEzC,wBAAwB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;IAKnE;;;OAGG;IACH,UAAU,CAAC,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,mBAAmB,GAAG,IAAI;IAUtE,0EAA0E;IAC1E,YAAY,IAAI,IAAI;IAepB,wEAAwE;IACxE,OAAO,CAAC,QAAQ;IAUhB,iBAAiB;CAoBlB"}
1
+ {"version":3,"file":"lb-options.browser.d.ts","sourceRoot":"","sources":["../widgets/lb-options.browser.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,qBAAa,SAAU,SAAQ,cAAe,YAAW,QAAQ;IAC/D,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;CAcrB"}
@@ -1,97 +1,67 @@
1
1
  /// <reference lib="dom" />
2
- import { ATTR_ACTION, ATTR_KEY_VALUE, ATTR_QUERY_PARM, ATTR_VALUE, LB_EVENT_NAME, } from "@loadbare/app/constants";
2
+ import { ATTR_GROUP_VALUE, ATTR_KEY_VALUE, LIVE_GROUP, LIVE_ROW, } from "@loadbare/app/constants";
3
+ import { WrappedControl } from "./control.js";
3
4
  /**
4
- * A `<select>` whose choices come from a list, and the case that shows why
5
- * placement belongs to the widget rather than to the framework.
5
+ * A `<select>` whose choices are the rows of the query it names.
6
6
  *
7
- * The rows arrive flat and keyed. Nothing on the wire knows about
8
- * `<optgroup>`, and nothing needs to: a group is a column like any other, and
9
- * turning a repeated column value into an element is exactly the local
10
- * knowledge a widget has and the hub does not. So the attribute that names
11
- * the column is this widget's own, in HTML's `data-` namespace rather than
12
- * the hub's: the hub never reads it, and `lb-` is reserved for what the hub
13
- * does read.
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.
14
11
  *
15
- * The identity of a row is the value of the option, so the `lb-key-value`
16
- * the hub stamps on each option is copied to its `value`, and the developer
17
- * names the key column once, with `lb-key` on the template.
12
+ * The identity of a row is the value of its option, so the `lb-key-value` the
13
+ * hub stamps on each option is copied to its `value`. The key column is the
14
+ * one the server declared for the query; the page never names it.
18
15
  *
19
- * It receives a value like any other cell, which selects the option whose
20
- * key it is. A picker that writes a query parm sends nothing: the hub reads
21
- * its choice off the bubbling `change` and lands the URL's back on it.
16
+ * It is two things at once, and the hub keeps them apart: its own `lb-query`
17
+ * names the rows it offers, and its `lb-column` is a column of the row around
18
+ * it, which is what it holds and what it sends. As a control it is
19
+ * form-associated, with a `value` and a `change` of its own, so the hub lands
20
+ * that column on `value` and commits `lb-request` on `change`.
22
21
  *
23
- * Exported because `lb-picker` is this widget with its row template
24
- * supplied by the definition instead of by the page. Nothing about the
25
- * behavior changes, so nothing about the behavior is written twice.
22
+ * The value is held here as well as on the select. A `<select>` does not hold
23
+ * a value it has no option for: it selects nothing and forgets it. The value
24
+ * and the choices are two answers that land in either order, so the held
25
+ * value is put on the select again whenever rows land.
26
+ *
27
+ * Exported because `lb-picker` is this widget with its row template supplied
28
+ * by the definition instead of by the page.
26
29
  */
27
- /** Named on the row template: the column whose distinct values become groups. */
28
- const GROUP_COLUMN = "data-group";
29
- export class LbOptions extends HTMLElement {
30
- static observedAttributes = [ATTR_VALUE];
31
- attributeChangedCallback(_name, _old, value) {
32
- const select = this.querySelector("select");
33
- if (select)
34
- select.value = value;
35
- }
30
+ export class LbOptions extends WrappedControl {
31
+ inner = "select";
36
32
  /**
37
- * The hub reconciles the list and calls this for placement only, so this
38
- * widget carries no copy of what a row is or when one arrives.
33
+ * The value last set or chosen, which the options may not include yet.
34
+ * Null when nothing chose one, so the select's own default stands.
39
35
  */
40
- lbPlaceRow(el, row, template) {
41
- const select = this.querySelector("select");
42
- const groupColumn = template.getAttribute(GROUP_COLUMN);
43
- if (!groupColumn) {
44
- select.insertBefore(el, template);
45
- return;
46
- }
47
- this.groupFor(select, template, String(row[groupColumn] ?? "")).append(el);
36
+ chosen = null;
37
+ get value() {
38
+ return this.chosen ?? super.value;
39
+ }
40
+ set value(value) {
41
+ this.chosen = value === null || value === undefined ? null : String(value);
42
+ super.value = value;
48
43
  }
49
- /** Everything derived from the rows, once the whole result has landed. */
44
+ formResetCallback() {
45
+ this.chosen = null;
46
+ super.formResetCallback();
47
+ }
48
+ changed() {
49
+ this.chosen = this.control?.value ?? null;
50
+ super.changed();
51
+ }
52
+ /** Everything derived from the rows, once the whole answer has landed. */
50
53
  lbRowsLanded() {
51
- // The identity of the row is the value of the option, so nothing on the
52
- // page has to say so twice.
53
- for (const option of this.querySelectorAll(`option[${ATTR_KEY_VALUE}]`)) {
54
+ for (const option of this.querySelectorAll(`option${LIVE_ROW}`)) {
54
55
  option.setAttribute("value", option.getAttribute(ATTR_KEY_VALUE));
55
56
  }
56
- // A group is derived, so it goes when its last row does.
57
- for (const group of this.querySelectorAll("optgroup")) {
58
- if (!group.querySelector("option"))
59
- group.remove();
60
- }
61
- // A value that landed before its option did selects it now.
62
- const value = this.getAttribute(ATTR_VALUE);
63
- if (value !== null)
64
- this.querySelector("select").value = value;
65
- }
66
- /** The group with this label, or a new one in front of the template. */
67
- groupFor(select, template, label) {
68
- for (const group of select.querySelectorAll("optgroup")) {
69
- if (group.label === label)
70
- return group;
57
+ for (const group of this.querySelectorAll(`optgroup${LIVE_GROUP}`)) {
58
+ group.label = group.getAttribute(ATTR_GROUP_VALUE) ?? "";
71
59
  }
72
- const group = document.createElement("optgroup");
73
- group.label = label;
74
- select.insertBefore(group, template);
75
- return group;
76
- }
77
- connectedCallback() {
78
- this.addEventListener("change", () => {
79
- const select = this.querySelector("select");
80
- if (!select) {
81
- console.error(`lb-options: change with no <select>, ignoring`);
82
- return;
83
- }
84
- const name = this.getAttribute(ATTR_ACTION);
85
- if (!name && this.hasAttribute(ATTR_QUERY_PARM))
86
- return;
87
- if (!name) {
88
- console.error(`lb-options: no ${ATTR_ACTION}, nothing to send`);
89
- return;
90
- }
91
- // The hub scopes it, so a choice inside a row says which row.
92
- const detail = { action: name, value: select.value };
93
- this.dispatchEvent(new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail }));
94
- });
60
+ // After the values above, since the select matches against them.
61
+ const select = this.querySelector("select");
62
+ if (select && this.chosen !== null)
63
+ select.value = this.chosen;
64
+ this.formValue();
95
65
  }
96
66
  }
97
67
  customElements.define("lb-options", LbOptions);
@@ -1 +1 @@
1
- {"version":3,"file":"lb-options.browser.js","sourceRoot":"","sources":["../widgets/lb-options.browser.ts"],"names":[],"mappings":"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","sourcesContent":["/// <reference lib=\"dom\" />\n\nimport {\n ATTR_ACTION,\n ATTR_KEY_VALUE,\n ATTR_QUERY_PARM,\n ATTR_VALUE,\n LB_EVENT_NAME,\n} from \"@loadbare/app/constants\";\nimport type { HubRequest, ListHost, Row } from \"@loadbare/app/types\";\n\n/**\n * A `<select>` whose choices come from a list, and the case that shows why\n * placement belongs to the widget rather than to the framework.\n *\n * The rows arrive flat and keyed. Nothing on the wire knows about\n * `<optgroup>`, and nothing needs to: a group is a column like any other, and\n * turning a repeated column value into an element is exactly the local\n * knowledge a widget has and the hub does not. So the attribute that names\n * the column is this widget's own, in HTML's `data-` namespace rather than\n * the hub's: the hub never reads it, and `lb-` is reserved for what the hub\n * does read.\n *\n * The identity of a row is the value of the option, so the `lb-key-value`\n * the hub stamps on each option is copied to its `value`, and the developer\n * names the key column once, with `lb-key` on the template.\n *\n * It receives a value like any other cell, which selects the option whose\n * key it is. A picker that writes a query parm sends nothing: the hub reads\n * its choice off the bubbling `change` and lands the URL's back on it.\n *\n * Exported because `lb-picker` is this widget with its row template\n * supplied by the definition instead of by the page. Nothing about the\n * behavior changes, so nothing about the behavior is written twice.\n */\n/** Named on the row template: the column whose distinct values become groups. */\nconst GROUP_COLUMN = \"data-group\";\n\nexport class LbOptions extends HTMLElement implements ListHost {\n static observedAttributes = [ATTR_VALUE];\n\n attributeChangedCallback(_name: string, _old: string, value: string) {\n const select = this.querySelector(\"select\");\n if (select) select.value = value;\n }\n\n /**\n * The hub reconciles the list and calls this for placement only, so this\n * widget carries no copy of what a row is or when one arrives.\n */\n lbPlaceRow(el: Element, row: Row, template: HTMLTemplateElement): void {\n const select = this.querySelector(\"select\")!;\n const groupColumn = template.getAttribute(GROUP_COLUMN);\n if (!groupColumn) {\n select.insertBefore(el, template);\n return;\n }\n this.groupFor(select, template, String(row[groupColumn] ?? \"\")).append(el);\n }\n\n /** Everything derived from the rows, once the whole result has landed. */\n lbRowsLanded(): void {\n // The identity of the row is the value of the option, so nothing on the\n // page has to say so twice.\n for (const option of this.querySelectorAll(`option[${ATTR_KEY_VALUE}]`)) {\n option.setAttribute(\"value\", option.getAttribute(ATTR_KEY_VALUE)!);\n }\n // A group is derived, so it goes when its last row does.\n for (const group of this.querySelectorAll(\"optgroup\")) {\n if (!group.querySelector(\"option\")) group.remove();\n }\n // A value that landed before its option did selects it now.\n const value = this.getAttribute(ATTR_VALUE);\n if (value !== null) this.querySelector(\"select\")!.value = value;\n }\n\n /** The group with this label, or a new one in front of the template. */\n private groupFor(select: Element, template: Element, label: string): Element {\n for (const group of select.querySelectorAll(\"optgroup\")) {\n if (group.label === label) return group;\n }\n const group = document.createElement(\"optgroup\");\n group.label = label;\n select.insertBefore(group, template);\n return group;\n }\n\n connectedCallback() {\n this.addEventListener(\"change\", () => {\n const select = this.querySelector(\"select\");\n if (!select) {\n console.error(`lb-options: change with no <select>, ignoring`);\n return;\n }\n const name = this.getAttribute(ATTR_ACTION);\n if (!name && this.hasAttribute(ATTR_QUERY_PARM)) return;\n if (!name) {\n console.error(`lb-options: no ${ATTR_ACTION}, nothing to send`);\n return;\n }\n // The hub scopes it, so a choice inside a row says which row.\n const detail: HubRequest = { action: name, value: select.value };\n this.dispatchEvent(\n new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail }),\n );\n });\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,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,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,gBAAgB,CAAC,SAAS,QAAQ,EAAE,CAAC,EAAE,CAAC;YAChE,MAAM,CAAC,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC,YAAY,CAAC,cAAc,CAAE,CAAC,CAAC;QACrE,CAAC;QACD,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,gBAAgB,CACvC,WAAW,UAAU,EAAE,CACxB,EAAE,CAAC;YACF,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC,YAAY,CAAC,gBAAgB,CAAC,IAAI,EAAE,CAAC;QAC3D,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 { RowsHost } 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 RowsHost {\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 for (const option of this.querySelectorAll(`option${LIVE_ROW}`)) {\n option.setAttribute(\"value\", option.getAttribute(ATTR_KEY_VALUE)!);\n }\n for (const group of this.querySelectorAll<HTMLOptGroupElement>(\n `optgroup${LIVE_GROUP}`,\n )) {\n group.label = group.getAttribute(ATTR_GROUP_VALUE) ?? \"\";\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,4 +1,4 @@
1
1
  <label
2
2
  >{{label}}
3
- <select lb-slot></select
3
+ <select lb-exp-slot></select
4
4
  ></label>
@@ -6,19 +6,17 @@ import { LbOptions } from "./lb-options.browser.js";
6
6
  *
7
7
  * A row template is markup, so writing it is the page author's job — until
8
8
  * every row is one option and the only thing that varies is which columns it
9
- * reads. Those are words, and words are parameters: `exp-key`, `exp-cell` and
10
- * `exp-group` fill the placeholders in the template the definition already
11
- * has, at build time, and the page writes one tag with no children.
9
+ * reads. That is a word, and words are parameters: `exp-column` fills the
10
+ * placeholder in the template the definition already has, at build time, and
11
+ * the page writes one tag with no children.
12
12
  *
13
- * It is the same trade as anywhere else in expansion. A page author who wants
14
- * a second element in the row, or an option built from two columns, writes
15
- * `lb-options` and the template themselves. This one is for when they do
16
- * not.
13
+ * A page author who wants a second element in the row, an option built from
14
+ * two columns, or options in groups, writes `lb-options` and the templates
15
+ * themselves.
17
16
  *
18
- * The behavior is inherited whole, because it is the same behavior: rows land
19
- * in groups, the key becomes the value, and a change sends the declared
20
- * action. A distinct class exists only because `customElements.define`
21
- * requires one constructor per name.
17
+ * The behavior is inherited whole, because it is the same behavior. A
18
+ * distinct class exists only because `customElements.define` requires one
19
+ * constructor per name.
22
20
  */
23
21
  class LbPicker extends LbOptions {
24
22
  }
@@ -1 +1 @@
1
- {"version":3,"file":"lb-picker.browser.js","sourceRoot":"","sources":["../widgets/lb-picker.browser.ts"],"names":[],"mappings":"AAAA,2BAA2B;AAE3B,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAEpD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,QAAS,SAAQ,SAAS;CAAG;AAEnC,cAAc,CAAC,MAAM,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC","sourcesContent":["/// <reference lib=\"dom\" />\n\nimport { LbOptions } from \"./lb-options.browser.js\";\n\n/**\n * The same `<select>` as `lb-options`, with the row template supplied by\n * the definition instead of by the page.\n *\n * A row template is markup, so writing it is the page author's job — until\n * every row is one option and the only thing that varies is which columns it\n * reads. Those are words, and words are parameters: `exp-key`, `exp-cell` and\n * `exp-group` fill the placeholders in the template the definition already\n * has, at build time, and the page writes one tag with no children.\n *\n * It is the same trade as anywhere else in expansion. A page author who wants\n * a second element in the row, or an option built from two columns, writes\n * `lb-options` and the template themselves. This one is for when they do\n * not.\n *\n * The behavior is inherited whole, because it is the same behavior: rows land\n * in groups, the key becomes the value, and a change sends the declared\n * action. A distinct class exists only because `customElements.define`\n * requires one constructor per name.\n */\nclass LbPicker extends LbOptions {}\n\ncustomElements.define(\"lb-picker\", LbPicker);\n"]}
1
+ {"version":3,"file":"lb-picker.browser.js","sourceRoot":"","sources":["../widgets/lb-picker.browser.ts"],"names":[],"mappings":"AAAA,2BAA2B;AAE3B,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAEpD;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,QAAS,SAAQ,SAAS;CAAG;AAEnC,cAAc,CAAC,MAAM,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC","sourcesContent":["/// <reference lib=\"dom\" />\n\nimport { LbOptions } from \"./lb-options.browser.js\";\n\n/**\n * The same `<select>` as `lb-options`, with the row template supplied by\n * the definition instead of by the page.\n *\n * A row template is markup, so writing it is the page author's job — until\n * every row is one option and the only thing that varies is which columns it\n * reads. That is a word, and words are parameters: `exp-column` fills the\n * placeholder in the template the definition already has, at build time, and\n * the page writes one tag with no children.\n *\n * A page author who wants a second element in the row, an option built from\n * two columns, or options in groups, writes `lb-options` and the templates\n * themselves.\n *\n * The behavior is inherited whole, because it is the same behavior. A\n * distinct class exists only because `customElements.define` requires one\n * constructor per name.\n */\nclass LbPicker extends LbOptions {}\n\ncustomElements.define(\"lb-picker\", LbPicker);\n"]}
@@ -1,7 +1,7 @@
1
1
  <label
2
2
  >{{label}}
3
3
  <select>
4
- <template lb-key="{{key}}" data-group="{{group}}">
5
- <option lb-cell="{{cell}}"></option>
4
+ <template>
5
+ <option lb-column="{{column}}"></option>
6
6
  </template></select
7
7
  ></label>
@@ -1,46 +1,14 @@
1
1
  /// <reference lib="dom" />
2
- import { ATTR_ACTION, ATTR_QUERY_PARM, ATTR_VALUE, LB_EVENT_NAME, } from "@loadbare/app/constants";
2
+ import { WrappedControl } from "./control.js";
3
3
  /**
4
- * A leaf cell wrapping a <select>. It receives its value like any other cell,
5
- * and on change it sends the action the developer declared for it — the
6
- * request vocabulary and the binding vocabulary running in both directions on
7
- * one widget.
4
+ * A `<select>` with its label and the options the page writes, as one
5
+ * control, like `lb-input` and for the same reasons.
8
6
  *
9
- * The choice is the interaction, so this is the case where an action carries
10
- * a value. The server still decides what that choice means.
11
- *
12
- * A select that writes a query parm sends nothing: the hub reads its choice
13
- * off the bubbling `change` and puts it in the address bar.
7
+ * Inside `lb-query="lb-url"` with `lb-request="lb-row-update"` it writes a
8
+ * query parm, and the hub answers that itself.
14
9
  */
15
- class LbSelect extends HTMLElement {
16
- static observedAttributes = [ATTR_VALUE];
17
- attributeChangedCallback(_name, _old, value) {
18
- const select = this.querySelector("select");
19
- if (!select) {
20
- console.error(`lb-select: no <select> to receive the value`);
21
- return;
22
- }
23
- select.value = value;
24
- }
25
- connectedCallback() {
26
- this.addEventListener("change", () => {
27
- const select = this.querySelector("select");
28
- if (!select) {
29
- console.error(`lb-select: change with no <select>, ignoring`);
30
- return;
31
- }
32
- const name = this.getAttribute(ATTR_ACTION);
33
- if (!name && this.hasAttribute(ATTR_QUERY_PARM))
34
- return;
35
- if (!name) {
36
- console.error(`lb-select: no ${ATTR_ACTION}, nothing to send`);
37
- return;
38
- }
39
- // The hub scopes it, so a choice inside a row says which row.
40
- const detail = { action: name, value: select.value };
41
- this.dispatchEvent(new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail }));
42
- });
43
- }
10
+ class LbSelect extends WrappedControl {
11
+ inner = "select";
44
12
  }
45
13
  customElements.define("lb-select", LbSelect);
46
14
  //# sourceMappingURL=lb-select.browser.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"lb-select.browser.js","sourceRoot":"","sources":["../widgets/lb-select.browser.ts"],"names":[],"mappings":"AAAA,2BAA2B;AAE3B,OAAO,EACL,WAAW,EACX,eAAe,EACf,UAAU,EACV,aAAa,GACd,MAAM,yBAAyB,CAAC;AAGjC;;;;;;;;;;;GAWG;AACH,MAAM,QAAS,SAAQ,WAAW;IAChC,MAAM,CAAC,kBAAkB,GAAG,CAAC,UAAU,CAAC,CAAC;IAEzC,wBAAwB,CAAC,KAAa,EAAE,IAAY,EAAE,KAAa;QACjE,MAAM,MAAM,GAAG,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;QAC5C,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,OAAO,CAAC,KAAK,CAAC,6CAA6C,CAAC,CAAC;YAC7D,OAAO;QACT,CAAC;QACD,MAAM,CAAC,KAAK,GAAG,KAAK,CAAC;IACvB,CAAC;IAED,iBAAiB;QACf,IAAI,CAAC,gBAAgB,CAAC,QAAQ,EAAE,GAAG,EAAE;YACnC,MAAM,MAAM,GAAG,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;YAC5C,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,OAAO,CAAC,KAAK,CAAC,8CAA8C,CAAC,CAAC;gBAC9D,OAAO;YACT,CAAC;YACD,MAAM,IAAI,GAAG,IAAI,CAAC,YAAY,CAAC,WAAW,CAAC,CAAC;YAC5C,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,YAAY,CAAC,eAAe,CAAC;gBAAE,OAAO;YACxD,IAAI,CAAC,IAAI,EAAE,CAAC;gBACV,OAAO,CAAC,KAAK,CAAC,iBAAiB,WAAW,mBAAmB,CAAC,CAAC;gBAC/D,OAAO;YACT,CAAC;YACD,8DAA8D;YAC9D,MAAM,MAAM,GAAe,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC;YACjE,IAAI,CAAC,aAAa,CAChB,IAAI,WAAW,CAAC,aAAa,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAC1D,CAAC;QACJ,CAAC,CAAC,CAAC;IACL,CAAC;;AAGH,cAAc,CAAC,MAAM,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC","sourcesContent":["/// <reference lib=\"dom\" />\n\nimport {\n ATTR_ACTION,\n ATTR_QUERY_PARM,\n ATTR_VALUE,\n LB_EVENT_NAME,\n} from \"@loadbare/app/constants\";\nimport type { HubRequest } from \"@loadbare/app/types\";\n\n/**\n * A leaf cell wrapping a <select>. It receives its value like any other cell,\n * and on change it sends the action the developer declared for it — the\n * request vocabulary and the binding vocabulary running in both directions on\n * one widget.\n *\n * The choice is the interaction, so this is the case where an action carries\n * a value. The server still decides what that choice means.\n *\n * A select that writes a query parm sends nothing: the hub reads its choice\n * off the bubbling `change` and puts it in the address bar.\n */\nclass LbSelect extends HTMLElement {\n static observedAttributes = [ATTR_VALUE];\n\n attributeChangedCallback(_name: string, _old: string, value: string) {\n const select = this.querySelector(\"select\");\n if (!select) {\n console.error(`lb-select: no <select> to receive the value`);\n return;\n }\n select.value = value;\n }\n\n connectedCallback() {\n this.addEventListener(\"change\", () => {\n const select = this.querySelector(\"select\");\n if (!select) {\n console.error(`lb-select: change with no <select>, ignoring`);\n return;\n }\n const name = this.getAttribute(ATTR_ACTION);\n if (!name && this.hasAttribute(ATTR_QUERY_PARM)) return;\n if (!name) {\n console.error(`lb-select: no ${ATTR_ACTION}, nothing to send`);\n return;\n }\n // The hub scopes it, so a choice inside a row says which row.\n const detail: HubRequest = { action: name, value: select.value };\n this.dispatchEvent(\n new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail }),\n );\n });\n }\n}\n\ncustomElements.define(\"lb-select\", LbSelect);\n"]}
1
+ {"version":3,"file":"lb-select.browser.js","sourceRoot":"","sources":["../widgets/lb-select.browser.ts"],"names":[],"mappings":"AAAA,2BAA2B;AAE3B,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE9C;;;;;;GAMG;AACH,MAAM,QAAS,SAAQ,cAAc;IACzB,KAAK,GAAG,QAAQ,CAAC;CAC5B;AAED,cAAc,CAAC,MAAM,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC","sourcesContent":["/// <reference lib=\"dom\" />\n\nimport { WrappedControl } from \"./control.js\";\n\n/**\n * A `<select>` with its label and the options the page writes, as one\n * control, like `lb-input` and for the same reasons.\n *\n * Inside `lb-query=\"lb-url\"` with `lb-request=\"lb-row-update\"` it writes a\n * query parm, and the hub answers that itself.\n */\nclass LbSelect extends WrappedControl {\n protected inner = \"select\";\n}\n\ncustomElements.define(\"lb-select\", LbSelect);\n"]}
@@ -1,4 +1,4 @@
1
1
  <label
2
2
  >{{label}}
3
- <select lb-slot></select
3
+ <select lb-exp-slot></select
4
4
  ></label>
@@ -1,116 +1,89 @@
1
1
  /// <reference lib="dom" />
2
- import { ATTR_CELL, ATTR_KEY_VALUE } from "@loadbare/app/constants";
2
+ import { ATTR_COLUMN, ATTR_ROW_REQUESTED, LB_DONE_EVENT_NAME, REQUEST_ROW_INSERT, REQUESTED_CREATED, REQUESTED_MOVED, } from "@loadbare/app/constants";
3
3
  /**
4
- * A table that owns its own scaffolding, and the case that shows what a list
5
- * widget is for once the hub reconciles lists on its own.
4
+ * A table that owns its own scaffolding.
6
5
  *
7
- * A plain `<table lb-list>` with a row template inside it needs no widget at
6
+ * A plain `<table lb-query>` with a row template inside it needs no widget at
8
7
  * all. This one supplies the table, and the developer writes what only the
9
8
  * page knows: the heading row and the row template. That division is forced
10
9
  * rather than chosen — a `<thead>` written inside a custom element is not
11
10
  * inside a table, so the parser drops the tag and keeps the text. A
12
11
  * `<template>` survives anywhere, so the heading row arrives wrapped and
13
- * expansion unwraps it into the `lb-template` destination beside the slot.
12
+ * expansion unwraps it into the `lb-exp-template` destination beside the slot.
14
13
  *
15
- * What it adds at run time is placement, and only placement. `data-sort`
16
- * names the column rows are ordered by; `data-group` names the column they
17
- * are sectioned by. Both are this widget's own attributes, in HTML's `data-`
18
- * namespace rather than the hub's: the hub never reads them, and `lb-` is
19
- * reserved for what the hub does read. `lb-options` happens to spell its
20
- * grouping attribute the same way, which is a convenience and not a contract.
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, and
16
+ * scrolling to the row the page's own request created or moved.
21
17
  *
22
- * The `foot` destination is the head's argument a second time and needs no
23
- * code here. A `<tfoot>` is a place on the page, not a row the hub delivers,
24
- * so what lands in it is an ordinary scope carrying its own `lb-row` — a
25
- * grand total is a second query over the same data, and the hub resolves it
26
- * by name like any other. See demo/pages/ledger.html.
18
+ * The `foot` destination needs no code here. A `<tfoot>` is a place on the
19
+ * page, not a row the hub delivers, so what lands in it is an aggregate or a
20
+ * `row` query of its own, named on its `<tr>`.
27
21
  */
28
- /** Named on the row template: the column whose distinct values become sections. */
29
- const GROUP_COLUMN = "data-group";
30
- /** Named on the row template: the column rows are ordered by within a section. */
31
- const SORT_COLUMN = "data-sort";
32
22
  /**
33
- * The heading row of a section, carrying the value it stands for.
23
+ * The ghost row's `<tbody>`, a blank row for entering a new one, between the
24
+ * rows and the footer. The page writes it as the `ghost` template, so it
25
+ * ships in the document like the heading and the footer, and the hub lands
26
+ * on whatever in it names a query. It is not a live row: the hub never
27
+ * reconciles it and never counts it.
34
28
  *
35
- * The widget's own scaffolding, derived from rows the hub knows and invisible
36
- * to the hub itself, exactly as the `<optgroup>` in `lb-options` is. Nothing
37
- * binds to it, so it carries no `lb-key-value` and no name from the vocabulary.
29
+ * A `<form>` cannot wrap a `<tr>`, so the widget makes one outside the table
30
+ * and joins the row's controls and buttons to it with the HTML `form`
31
+ * attribute. Sending is the page's: a button in the row carrying
32
+ * `lb-row-insert` submits that form, and the hub gathers what the form owns.
33
+ * The hub resets the form once the insert succeeds.
38
34
  */
39
- const SECTION_ROW = "data-section";
35
+ const GHOST = "data-ghost";
36
+ /** Ghost row forms need ids for the `form` attribute; any unique one will do. */
37
+ let ghostForms = 0;
40
38
  class LbTable extends HTMLElement {
41
39
  /**
42
- * Group first, then sort within the group.
43
- *
44
- * Inserting a row before the first one that sorts after it keeps the
45
- * section ordered, whatever else is in it — which is what lets a whole set
46
- * and a single patched row take the same path.
40
+ * A ghost row is for entering the next row, so once one is saved the
41
+ * cursor goes back to the start of it. `lb-request-done` comes after the
42
+ * response items have landed, so nothing the hub places afterwards takes
43
+ * the focus away again. A refused insert leaves the cursor where the user
44
+ * left it.
47
45
  */
48
- lbPlaceRow(el, row, template) {
49
- const groupColumn = template.getAttribute(GROUP_COLUMN);
50
- const sortColumn = template.getAttribute(SORT_COLUMN);
51
- const heading = groupColumn
52
- ? this.headingFor(template, String(row[groupColumn] ?? ""))
53
- : null;
54
- const { rows, end } = this.section(heading);
55
- const value = sortColumn ? String(row[sortColumn] ?? "") : "";
56
- const after = sortColumn
57
- ? rows.find((other) => other !== el && cellOf(other, sortColumn) > value)
58
- : undefined;
59
- template.parentElement.insertBefore(el, after ?? end);
46
+ constructor() {
47
+ super();
48
+ this.addEventListener(LB_DONE_EVENT_NAME, (e) => {
49
+ const { request, error } = e.detail;
50
+ if (error || request.name !== REQUEST_ROW_INSERT)
51
+ return;
52
+ const row = e.target.closest(`[${GHOST}]`);
53
+ if (row)
54
+ firstControl(row)?.focus();
55
+ });
60
56
  }
61
- /** A section is derived, so it goes when its last row does. */
62
- lbRowsLanded() {
63
- for (const heading of this.querySelectorAll(`tr[${SECTION_ROW}]`)) {
64
- if (this.section(heading).rows.length === 0)
65
- heading.remove();
57
+ connectedCallback() {
58
+ const ghost = this.querySelector(`tbody[${GHOST}]`);
59
+ if (!ghost?.firstElementChild || this.ghostForm())
60
+ return;
61
+ const form = document.createElement("form");
62
+ form.id = `ghost-row-${++ghostForms}`;
63
+ form.hidden = true;
64
+ this.append(form);
65
+ for (const el of ghost.querySelectorAll(`[${ATTR_COLUMN}], button`)) {
66
+ el.setAttribute("form", form.id);
66
67
  }
67
68
  }
68
- /**
69
- * The rows of one section, and the node it ends at.
70
- *
71
- * A section runs from its heading to the next heading, or to the template
72
- * if there is none after it. Without grouping there is one section and it
73
- * is the whole body — the heading row the developer wrote is skipped along
74
- * the way, because it carries no key and is therefore not a row.
75
- */
76
- section(heading) {
77
- const template = this.querySelector("template");
78
- const rows = [];
79
- let node = heading
80
- ? heading.nextElementSibling
81
- : template.parentElement.firstElementChild;
82
- while (node && node !== template && !node.hasAttribute(SECTION_ROW)) {
83
- if (node.hasAttribute(ATTR_KEY_VALUE))
84
- rows.push(node);
85
- node = node.nextElementSibling;
86
- }
87
- return { rows, end: node ?? template };
69
+ /** The row this page's request created or moved, brought into view. */
70
+ lbRowsLanded() {
71
+ this.querySelector(`[${ATTR_ROW_REQUESTED}="${REQUESTED_CREATED}"], ` +
72
+ `[${ATTR_ROW_REQUESTED}="${REQUESTED_MOVED}"]`)?.scrollIntoView({ block: "nearest" });
88
73
  }
89
- /** The section with this label, or a new one in front of the template. */
90
- headingFor(template, label) {
91
- const body = template.parentElement;
92
- for (const heading of body.querySelectorAll(`tr[${SECTION_ROW}]`)) {
93
- if (heading.getAttribute(SECTION_ROW) === label)
94
- return heading;
95
- }
96
- const heading = document.createElement("tr");
97
- heading.setAttribute(SECTION_ROW, label);
98
- const cell = document.createElement("th");
99
- cell.setAttribute("scope", "rowgroup");
100
- // How wide the table is, from the row the developer wrote. Nothing
101
- // else knows, and nothing had to be told twice.
102
- cell.colSpan = template.content.firstElementChild?.childElementCount ?? 1;
103
- cell.textContent = label;
104
- heading.append(cell);
105
- body.insertBefore(heading, template);
106
- return heading;
74
+ /** The ghost row's form, once this table has made it. */
75
+ ghostForm() {
76
+ return this.querySelector(":scope > form");
107
77
  }
108
78
  }
109
- /** What a live row is showing in one column. */
110
- function cellOf(row, column) {
111
- const selector = `[${ATTR_CELL}="${column}"]`;
112
- const el = row.matches(selector) ? row : row.querySelector(selector);
113
- return el?.textContent ?? "";
79
+ /**
80
+ * The first control in a row the user can type into or choose from. A
81
+ * control a widget wraps is found inside it, since the wrapper takes no focus.
82
+ */
83
+ function firstControl(row) {
84
+ return [
85
+ ...row.querySelectorAll('input:not([type="hidden"]), select, textarea'),
86
+ ].find((el) => !el.matches(":disabled") && el.checkVisibility?.() !== false);
114
87
  }
115
88
  customElements.define("lb-table", LbTable);
116
89
  //# sourceMappingURL=lb-table.browser.js.map