@loadbare/widgets 0.5.0 → 0.7.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 (47) hide show
  1. package/README.md +6 -1
  2. package/dist/control.d.ts +15 -7
  3. package/dist/control.d.ts.map +1 -1
  4. package/dist/control.js +19 -3
  5. package/dist/control.js.map +1 -1
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +2 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/lb-checkbox.html +13 -0
  11. package/dist/lb-confirm.browser.d.ts +2 -0
  12. package/dist/lb-confirm.browser.d.ts.map +1 -0
  13. package/dist/lb-confirm.browser.js +36 -0
  14. package/dist/lb-confirm.browser.js.map +1 -0
  15. package/dist/lb-confirm.html +27 -0
  16. package/dist/lb-form.html +7 -0
  17. package/dist/lb-input.browser.js +7 -7
  18. package/dist/lb-input.browser.js.map +1 -1
  19. package/dist/lb-input.html +1 -1
  20. package/dist/lb-insert-dialog.browser.d.ts +2 -0
  21. package/dist/lb-insert-dialog.browser.d.ts.map +1 -0
  22. package/dist/lb-insert-dialog.browser.js +59 -0
  23. package/dist/lb-insert-dialog.browser.js.map +1 -0
  24. package/dist/lb-insert-dialog.html +25 -0
  25. package/dist/lb-master.html +56 -0
  26. package/dist/lb-options.browser.d.ts +29 -10
  27. package/dist/lb-options.browser.d.ts.map +1 -1
  28. package/dist/lb-options.browser.js +26 -55
  29. package/dist/lb-options.browser.js.map +1 -1
  30. package/dist/lb-picker.browser.js +7 -10
  31. package/dist/lb-picker.browser.js.map +1 -1
  32. package/dist/lb-picker.html +1 -1
  33. package/dist/lb-select.browser.js +4 -7
  34. package/dist/lb-select.browser.js.map +1 -1
  35. package/dist/lb-table.browser.js +61 -88
  36. package/dist/lb-table.browser.js.map +1 -1
  37. package/dist/lb-table.html +1 -0
  38. package/docs/lb-checkbox.md +63 -0
  39. package/docs/lb-confirm.md +45 -0
  40. package/docs/lb-form.md +35 -0
  41. package/docs/lb-input.md +1 -0
  42. package/docs/lb-insert-dialog.md +50 -0
  43. package/docs/lb-master.md +65 -0
  44. package/docs/lb-options.md +11 -7
  45. package/docs/lb-picker.md +10 -11
  46. package/docs/lb-table.md +52 -20
  47. package/package.json +4 -4
@@ -1,33 +1,32 @@
1
1
  /// <reference lib="dom" />
2
- import { ATTR_KEY_VALUE } from "@loadbare/app/constants";
2
+ import { ATTR_GROUP_VALUE, ATTR_KEY_VALUE, LIVE_GROUP, LIVE_ROW, } from "@loadbare/app/constants";
3
3
  import { WrappedControl } from "./control.js";
4
4
  /**
5
- * A `<select>` whose choices are the rows of a query, and the case that
6
- * shows why placement belongs to the widget rather than to the framework.
5
+ * A `<select>` whose choices are the rows of the query it names.
7
6
  *
8
- * The rows arrive flat and keyed. Nothing on the wire knows about
9
- * `<optgroup>`, and nothing needs to: a group is a column like any other, and
10
- * turning a repeated column value into an element is exactly the local
11
- * knowledge a widget has and the hub does not. So the attribute that names
12
- * the column is this widget's own, in HTML's `data-` namespace rather than
13
- * the hub's: the hub never reads it, and `lb-` is reserved for what the hub
14
- * 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.
15
11
  *
16
- * The identity of a row is the value of the option, so the `lb-key-value`
17
- * the hub stamps on each option is copied to its `value`. The server declares
18
- * the key, and the page never names it.
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.
19
15
  *
20
- * It is a control like `lb-select`: `lb-column` sets which option is
21
- * selected, from the row around it, and `lb-request` commits the choice on
22
- * `change`. A value can arrive before the options it names, so the widget
23
- * keeps the value it was last given and selects it again whenever rows land.
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`.
24
21
  *
25
- * Exported because `lb-picker` is this widget with its row template
26
- * supplied by the definition instead of by the page. Nothing about the
27
- * 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.
28
29
  */
29
- /** Named on the row template: the column whose distinct values become groups. */
30
- const GROUP_COLUMN = "data-group";
31
30
  export class LbOptions extends WrappedControl {
32
31
  inner = "select";
33
32
  /**
@@ -50,48 +49,20 @@ export class LbOptions extends WrappedControl {
50
49
  this.chosen = this.control?.value ?? null;
51
50
  super.changed();
52
51
  }
53
- /**
54
- * The hub reconciles the rows and calls this for placement only, so this
55
- * widget carries no copy of what a row is or when one arrives.
56
- */
57
- lbPlaceRow(el, row, template) {
58
- const select = this.querySelector("select");
59
- const groupColumn = template.getAttribute(GROUP_COLUMN);
60
- if (!groupColumn) {
61
- select.insertBefore(el, template);
62
- return;
63
- }
64
- this.groupFor(select, template, String(row[groupColumn] ?? "")).append(el);
65
- }
66
- /** Everything derived from the rows, once the whole result has landed. */
52
+ /** Everything derived from the rows, once the whole answer has landed. */
67
53
  lbRowsLanded() {
68
- // The identity of the row is the value of the option, so nothing on the
69
- // page has to say so.
70
- for (const option of this.querySelectorAll(`option[${ATTR_KEY_VALUE}]`)) {
54
+ for (const option of this.querySelectorAll(`option${LIVE_ROW}`)) {
71
55
  option.setAttribute("value", option.getAttribute(ATTR_KEY_VALUE));
72
56
  }
73
- // A group is derived, so it goes when its last row does.
74
- for (const group of this.querySelectorAll("optgroup")) {
75
- if (!group.querySelector("option"))
76
- group.remove();
57
+ for (const group of this.querySelectorAll(`optgroup${LIVE_GROUP}`)) {
58
+ group.label = group.getAttribute(ATTR_GROUP_VALUE) ?? "";
77
59
  }
78
- // A value that arrived before its option selects it now.
60
+ // After the values above, since the select matches against them.
79
61
  const select = this.querySelector("select");
80
62
  if (select && this.chosen !== null)
81
63
  select.value = this.chosen;
82
64
  this.formValue();
83
65
  }
84
- /** The group with this label, or a new one in front of the template. */
85
- groupFor(select, template, label) {
86
- for (const group of select.querySelectorAll("optgroup")) {
87
- if (group.label === label)
88
- return group;
89
- }
90
- const group = document.createElement("optgroup");
91
- group.label = label;
92
- select.insertBefore(group, template);
93
- return group;
94
- }
95
66
  }
96
67
  customElements.define("lb-options", LbOptions);
97
68
  //# sourceMappingURL=lb-options.browser.js.map
@@ -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 { ATTR_KEY_VALUE } from \"@loadbare/app/constants\";\nimport type { Row, RowsHost } from \"@loadbare/app/types\";\n\nimport { WrappedControl } from \"./control.js\";\n\n/**\n * A `<select>` whose choices are the rows of a query, and the case that\n * shows why 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`. The server declares\n * the key, and the page never names it.\n *\n * It is a control like `lb-select`: `lb-column` sets which option is\n * selected, from the row around it, and `lb-request` commits the choice on\n * `change`. A value can arrive before the options it names, so the widget\n * keeps the value it was last given and selects it again whenever rows land.\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 WrappedControl implements RowsHost {\n protected readonly 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 /**\n * The hub reconciles the rows 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.\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 arrived before its option selects it now.\n const select = this.querySelector(\"select\");\n if (select && this.chosen !== null) select.value = this.chosen;\n this.formValue();\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\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"]}
@@ -6,18 +6,15 @@ 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-column` 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. The key
12
- * is the server's, declared with the query, so the page names none.
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.
13
12
  *
14
- * It is the same trade as anywhere else in expansion. A page author who wants
15
- * a second element in the row, or an option built from two columns, writes
16
- * `lb-options` and the template themselves. This one is for when they do
17
- * 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.
18
16
  *
19
- * The behavior is inherited whole, because it is the same behavior: rows land
20
- * in groups, the key becomes the value, and the widget is a control. A
17
+ * The behavior is inherited whole, because it is the same behavior. A
21
18
  * distinct class exists only because `customElements.define` requires one
22
19
  * constructor per name.
23
20
  */
@@ -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;;;;;;;;;;;;;;;;;;;;GAoBG;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-column` 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. The key\n * is the server's, declared with the query, so the page names none.\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 the widget is a control. 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
+ {"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 data-group="{{group}}">
4
+ <template>
5
5
  <option lb-column="{{column}}"></option>
6
6
  </template></select
7
7
  ></label>
@@ -1,14 +1,11 @@
1
1
  /// <reference lib="dom" />
2
2
  import { WrappedControl } from "./control.js";
3
3
  /**
4
- * A control wrapping a `<select>` whose options the page writes. The hub
5
- * sets its `value` from the column its `lb-column` names, and gathers it back
6
- * under the same name.
4
+ * A `<select>` with its label and the options the page writes, as one
5
+ * control, like `lb-input` and for the same reasons.
7
6
  *
8
- * The choice is the interaction, so this is the control that most often
9
- * carries `lb-request`: it commits on `change`, and the hub sends that request
10
- * with this select's column alone. The server still decides what the choice
11
- * means.
7
+ * Inside `lb-query="lb-url"` with `lb-request="lb-row-update"` it writes a
8
+ * query parm, and the hub answers that itself.
12
9
  */
13
10
  class LbSelect extends WrappedControl {
14
11
  inner = "select";
@@ -1 +1 @@
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;;;;;;;;;GASG;AACH,MAAM,QAAS,SAAQ,cAAc;IAChB,KAAK,GAAG,QAAQ,CAAC;CACrC;AAED,cAAc,CAAC,MAAM,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC","sourcesContent":["/// <reference lib=\"dom\" />\n\nimport { WrappedControl } from \"./control.js\";\n\n/**\n * A control wrapping a `<select>` whose options the page writes. The hub\n * sets its `value` from the column its `lb-column` names, and gathers it back\n * under the same name.\n *\n * The choice is the interaction, so this is the control that most often\n * carries `lb-request`: it commits on `change`, and the hub sends that request\n * with this select's column alone. The server still decides what the choice\n * means.\n */\nclass LbSelect extends WrappedControl {\n protected readonly inner = \"select\";\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,8 +1,7 @@
1
1
  /// <reference lib="dom" />
2
- import { ATTR_COLUMN, 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
5
- * widget holding rows is for once the hub reconciles rows on its own.
4
+ * A table that owns its own scaffolding.
6
5
  *
7
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
@@ -12,105 +11,79 @@ import { ATTR_COLUMN, ATTR_KEY_VALUE } from "@loadbare/app/constants";
12
11
  * `<template>` survives anywhere, so the heading row arrives wrapped and
13
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 element carrying its own `lb-query` — a grand
25
- * total is a second query over the same data, of kind `row`, and the hub
26
- * lands it by name like any other.
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_COLUMN}="${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
@@ -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 { ATTR_COLUMN, ATTR_KEY_VALUE } from \"@loadbare/app/constants\";\nimport type { Row, RowsHost } from \"@loadbare/app/types\";\n\n/**\n * A table that owns its own scaffolding, and the case that shows what a\n * widget holding rows is for once the hub reconciles rows on its own.\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 * What it adds at run time is placement, and only placement. `data-sort`\n * names the column rows are ordered by; `data-group` names the column they\n * are sectioned by. Both are this widget's own attributes, in HTML's `data-`\n * namespace rather than the hub's: the hub never reads them, and `lb-` is\n * reserved for what the hub does read. `lb-options` happens to spell its\n * grouping attribute the same way, which is a convenience and not a contract.\n *\n * The `foot` destination is the head's argument a second time and needs no\n * code here. A `<tfoot>` is a place on the page, not a row the hub delivers,\n * so what lands in it is an element carrying its own `lb-query` — a grand\n * total is a second query over the same data, of kind `row`, and the hub\n * lands it by name like any other.\n */\n\n/** Named on the row template: the column whose distinct values become sections. */\nconst GROUP_COLUMN = \"data-group\";\n\n/** Named on the row template: the column rows are ordered by within a section. */\nconst SORT_COLUMN = \"data-sort\";\n\n/**\n * The heading row of a section, carrying the value it stands for.\n *\n * The widget's own scaffolding, derived from rows the hub knows and invisible\n * to the hub itself, exactly as the `<optgroup>` in `lb-options` is. Nothing\n * binds to it, so it carries no `lb-key-value` and no name from the vocabulary.\n */\nconst SECTION_ROW = \"data-section\";\n\nclass LbTable extends HTMLElement implements RowsHost {\n /**\n * Group first, then sort within the group.\n *\n * Inserting a row before the first one that sorts after it keeps the\n * section ordered, whatever else is in it — which is what lets a whole set\n * and a single patched row take the same path.\n */\n lbPlaceRow(el: Element, row: Row, template: HTMLTemplateElement): void {\n const groupColumn = template.getAttribute(GROUP_COLUMN);\n const sortColumn = template.getAttribute(SORT_COLUMN);\n const heading = groupColumn\n ? this.headingFor(template, String(row[groupColumn] ?? \"\"))\n : null;\n const { rows, end } = this.section(heading);\n\n const value = sortColumn ? String(row[sortColumn] ?? \"\") : \"\";\n const after = sortColumn\n ? rows.find((other) => other !== el && cellOf(other, sortColumn) > value)\n : undefined;\n template.parentElement!.insertBefore(el, after ?? end);\n }\n\n /** A section is derived, so it goes when its last row does. */\n lbRowsLanded(): void {\n for (const heading of this.querySelectorAll(`tr[${SECTION_ROW}]`)) {\n if (this.section(heading).rows.length === 0) heading.remove();\n }\n }\n\n /**\n * The rows of one section, and the node it ends at.\n *\n * A section runs from its heading to the next heading, or to the template\n * if there is none after it. Without grouping there is one section and it\n * is the whole body — the heading row the developer wrote is skipped along\n * the way, because it carries no key and is therefore not a row.\n */\n private section(heading: Element | null): { rows: Element[]; end: Element } {\n const template = this.querySelector(\"template\")!;\n const rows: Element[] = [];\n let node = heading\n ? heading.nextElementSibling\n : template.parentElement!.firstElementChild;\n while (node && node !== template && !node.hasAttribute(SECTION_ROW)) {\n if (node.hasAttribute(ATTR_KEY_VALUE)) rows.push(node);\n node = node.nextElementSibling;\n }\n return { rows, end: node ?? template };\n }\n\n /** The section with this label, or a new one in front of the template. */\n private headingFor(template: HTMLTemplateElement, label: string): Element {\n const body = template.parentElement!;\n for (const heading of body.querySelectorAll(`tr[${SECTION_ROW}]`)) {\n if (heading.getAttribute(SECTION_ROW) === label) return heading;\n }\n const heading = document.createElement(\"tr\");\n heading.setAttribute(SECTION_ROW, label);\n const cell = document.createElement(\"th\");\n cell.setAttribute(\"scope\", \"rowgroup\");\n // How wide the table is, from the row the developer wrote. Nothing\n // else knows, and nothing had to be told twice.\n cell.colSpan = template.content.firstElementChild?.childElementCount ?? 1;\n cell.textContent = label;\n heading.append(cell);\n body.insertBefore(heading, template);\n return heading;\n }\n}\n\n/** What a live row is showing in one column. */\nfunction cellOf(row: Element, column: string): string {\n const selector = `[${ATTR_COLUMN}=\"${column}\"]`;\n const el = row.matches(selector) ? row : row.querySelector(selector);\n return el?.textContent ?? \"\";\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":"AAAA,2BAA2B;AAE3B,OAAO,EACL,WAAW,EACX,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,EAClB,iBAAiB,EACjB,eAAe,GAChB,MAAM,yBAAyB,CAAC;AAGjC;;;;;;;;;;;;;;;;;;GAkBG;AAEH;;;;;;;;;;;;GAYG;AACH,MAAM,KAAK,GAAG,YAAY,CAAC;AAE3B,iFAAiF;AACjF,IAAI,UAAU,GAAG,CAAC,CAAC;AAEnB,MAAM,OAAQ,SAAQ,WAAW;IAC/B;;;;;;OAMG;IACH;QACE,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,gBAAgB,CAAC,kBAAkB,EAAE,CAAC,CAAC,EAAE,EAAE;YAC9C,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,GAAI,CAAiC,CAAC,MAAM,CAAC;YACrE,IAAI,KAAK,IAAI,OAAO,CAAC,IAAI,KAAK,kBAAkB;gBAAE,OAAO;YACzD,MAAM,GAAG,GAAI,CAAC,CAAC,MAAkB,CAAC,OAAO,CAAC,IAAI,KAAK,GAAG,CAAC,CAAC;YACxD,IAAI,GAAG;gBAAE,YAAY,CAAC,GAAG,CAAC,EAAE,KAAK,EAAE,CAAC;QACtC,CAAC,CAAC,CAAC;IACL,CAAC;IAED,iBAAiB;QACf,MAAM,KAAK,GAAG,IAAI,CAAC,aAAa,CAAC,SAAS,KAAK,GAAG,CAAC,CAAC;QACpD,IAAI,CAAC,KAAK,EAAE,iBAAiB,IAAI,IAAI,CAAC,SAAS,EAAE;YAAE,OAAO;QAE1D,MAAM,IAAI,GAAG,QAAQ,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;QAC5C,IAAI,CAAC,EAAE,GAAG,aAAa,EAAE,UAAU,EAAE,CAAC;QACtC,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACnB,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QAClB,KAAK,MAAM,EAAE,IAAI,KAAK,CAAC,gBAAgB,CAAC,IAAI,WAAW,WAAW,CAAC,EAAE,CAAC;YACpE,EAAE,CAAC,YAAY,CAAC,MAAM,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC;QACnC,CAAC;IACH,CAAC;IAED,uEAAuE;IACvE,YAAY;QACV,IAAI,CAAC,aAAa,CAChB,IAAI,kBAAkB,KAAK,iBAAiB,MAAM;YAChD,IAAI,kBAAkB,KAAK,eAAe,IAAI,CACjD,EAAE,cAAc,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;IAC1C,CAAC;IAED,yDAAyD;IACjD,SAAS;QACf,OAAO,IAAI,CAAC,aAAa,CAAC,eAAe,CAAC,CAAC;IAC7C,CAAC;CACF;AAED;;;GAGG;AACH,SAAS,YAAY,CAAC,GAAY;IAChC,OAAO;QACL,GAAG,GAAG,CAAC,gBAAgB,CACrB,8CAA8C,CAC/C;KACF,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC,eAAe,EAAE,EAAE,KAAK,KAAK,CAAC,CAAC;AAC/E,CAAC;AAED,cAAc,CAAC,MAAM,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC","sourcesContent":["/// <reference lib=\"dom\" />\n\nimport {\n ATTR_COLUMN,\n ATTR_ROW_REQUESTED,\n LB_DONE_EVENT_NAME,\n REQUEST_ROW_INSERT,\n REQUESTED_CREATED,\n REQUESTED_MOVED,\n} from \"@loadbare/app/constants\";\nimport type { HubRequestDone, RowsHost } 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, and\n * scrolling to the row the page's own request created or moved.\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 * 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 RowsHost {\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. `lb-request-done` comes after the\n * response items have landed, so nothing the hub places afterwards takes\n * the focus away again. A refused insert leaves the cursor where the user\n * left it.\n */\n constructor() {\n super();\n this.addEventListener(LB_DONE_EVENT_NAME, (e) => {\n const { request, error } = (e as CustomEvent<HubRequestDone>).detail;\n if (error || request.name !== REQUEST_ROW_INSERT) return;\n const row = (e.target as Element).closest(`[${GHOST}]`);\n if (row) firstControl(row)?.focus();\n });\n }\n\n connectedCallback(): void {\n const ghost = this.querySelector(`tbody[${GHOST}]`);\n if (!ghost?.firstElementChild || this.ghostForm()) 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.querySelector(\n `[${ATTR_ROW_REQUESTED}=\"${REQUESTED_CREATED}\"], ` +\n `[${ATTR_ROW_REQUESTED}=\"${REQUESTED_MOVED}\"]`,\n )?.scrollIntoView({ block: \"nearest\" });\n }\n\n /** The ghost row's form, once this table has made it. */\n private ghostForm(): HTMLFormElement | null {\n return this.querySelector(\":scope > form\");\n }\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"]}
@@ -4,5 +4,6 @@
4
4
  </caption>
5
5
  <thead lb-exp-template="head"></thead>
6
6
  <tbody lb-exp-slot></tbody>
7
+ <tbody data-ghost lb-exp-template="ghost"></tbody>
7
8
  <tfoot lb-exp-template="foot"></tfoot>
8
9
  </table>
@@ -0,0 +1,63 @@
1
+ # `lb-checkbox`
2
+
3
+ > Status: a barebones starter. Iterative use will drive improvements.
4
+
5
+ A checkbox and its label as one element, for an application to style. A
6
+ definition only, with no script: the hub treats a native checkbox as a
7
+ control, so the widget adds nothing but the markup.
8
+
9
+ Requires `exp-column` and `exp-label`.
10
+
11
+ ```html
12
+ <div lb-query="people">
13
+ <template>
14
+ <div>
15
+ <lb-checkbox exp-column="active" exp-label="Active" exp-request="lb-row-update"></lb-checkbox>
16
+ </div>
17
+ </template>
18
+ </div>
19
+ ```
20
+
21
+ ## Expansion Attributes
22
+
23
+ | Parameter | Fills |
24
+ | ------------- | ----------------------------------------------- |
25
+ | `exp-column` | the checkbox's `lb-column` |
26
+ | `exp-label` | the label's text |
27
+ | `exp-request` | the checkbox's `lb-request`, none when absent |
28
+
29
+ The column and request go on the checkbox inside, since it is the control,
30
+ so they are expansion attributes here rather than `lb-column` and
31
+ `lb-request` on the tag.
32
+
33
+ ## Why it is not like `lb-input`
34
+
35
+ `lb-input` has a script that makes the tag itself the control, so the page
36
+ writes `lb-column` on the tag. `lb-checkbox` cannot do the same. A value
37
+ that lands before a widget's script has loaded reaches it only as the
38
+ `lb-column-value` stamp, a string, and a checkbox's state is a boolean. A
39
+ script `lb-checkbox` would read the stamp `"false"` as checked, since no
40
+ string is read. The native checkbox has no such gap: the hub sets `checked`
41
+ on it directly.
42
+
43
+ `lb-input` could equally be markup alone; its script is a convention, kept
44
+ for now.
45
+
46
+ ## The control
47
+
48
+ The checkbox is checked when its column is anything but `false` or null, and
49
+ sends `"true"` or `"false"`. See
50
+ [checkboxes](../../app/docs/reference/data-binding.md#checkboxes).
51
+
52
+ ## Styling
53
+
54
+ The definition is a `<label>` holding the `<input type="checkbox">` and a
55
+ `<span>` with the text:
56
+
57
+ ```css
58
+ lb-checkbox label {
59
+ display: inline-flex;
60
+ gap: 0.5em;
61
+ align-items: center;
62
+ }
63
+ ```
@@ -0,0 +1,45 @@
1
+ # `lb-confirm`
2
+
3
+ > Status: a barebones starter. Iterative use will drive improvements.
4
+
5
+ A button that asks before it sends. The button opens a dialog holding the
6
+ message the page writes inside the tag, a Cancel button, and a confirm button
7
+ carrying the request.
8
+
9
+ Requires `exp-label` and `exp-request`.
10
+
11
+ ```html
12
+ <template>
13
+ <tr>
14
+ <td lb-column="name"></td>
15
+ <td>
16
+ <lb-confirm exp-label="Delete" exp-request="lb-row-delete">
17
+ Delete <b lb-column="name"></b>?
18
+ </lb-confirm>
19
+ </td>
20
+ </tr>
21
+ </template>
22
+ ```
23
+
24
+ ## Expansion Attributes
25
+
26
+ | Parameter | Fills |
27
+ | ------------- | --------------------------------------------- |
28
+ | `exp-label` | the text of the button that opens the dialog |
29
+ | `exp-request` | the confirm button's `lb-request` |
30
+ | `exp-confirm` | the confirm button's text, `Confirm` when absent |
31
+
32
+ ## The request
33
+
34
+ The request is on the dialog's confirm button, never on the tag, so nothing
35
+ reaches the hub until the user says yes. The dialog sits in the row the tag
36
+ sits in, so the request carries that row's query and key, and the message
37
+ can show the row's columns with `lb-column`.
38
+
39
+ Cancel comes first and takes focus, so Enter on a dialog opened by accident
40
+ sends nothing. The widget closes the dialog when `lb-request-done` says the
41
+ request succeeded. A refused request leaves it open, with the hub's
42
+ `lb-request-error` on the confirm button.
43
+
44
+ The dialog is found by its place in the tag, not by id, so the tag works in a
45
+ row template, where the hub clones it once per row.
@@ -0,0 +1,35 @@
1
+ # `lb-form`
2
+
3
+ > Status: a barebones starter. Iterative use will drive improvements.
4
+
5
+ A `<form>` over one row's controls, in one of two modes the row decides.
6
+
7
+ Requires `exp-id`.
8
+
9
+ ```html
10
+ <section lb-query="account">
11
+ <lb-form exp-id="account-form">
12
+ <lb-input lb-column="name" exp-label="Name" lb-request="lb-row-update"></lb-input>
13
+ </lb-form>
14
+ <button type="submit" form="account-form" lb-request="lb-row-insert">Save new</button>
15
+ </section>
16
+ ```
17
+
18
+ ## Expansion Attributes
19
+
20
+ | Parameter | Fills |
21
+ | --------- | ------------------ |
22
+ | `exp-id` | the form's `id` |
23
+
24
+ ## Two modes
25
+
26
+ A row with a key is on record, and each control saves its own column on
27
+ `change` with `lb-request="lb-row-update"`. A row whose key is empty is a new
28
+ one not yet entered: the hub sends no update for it, so the controls only
29
+ hold what a submit will gather. See
30
+ [a row not yet entered](../../app/docs/reference/data-binding.md#a-row-not-yet-entered).
31
+
32
+ A definition only, with no script of its own.
33
+
34
+ A button that acts on the row as a whole, such as one adding it to the set,
35
+ belongs to whatever holds the set, and reaches the form by `form="<id>"`.