@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
package/README.md CHANGED
@@ -10,8 +10,13 @@ Inventory:
10
10
  | [`lb-select`](docs/lb-select.md) | a control wrapping a `<select>` whose options the page writes |
11
11
  | [`lb-options`](docs/lb-options.md) | a control wrapping a `<select>` whose options come from a query |
12
12
  | [`lb-picker`](docs/lb-picker.md) | `lb-options` with the row template built in |
13
- | [`lb-table`](docs/lb-table.md) | a `<table>` supplying its own scaffolding |
13
+ | [`lb-table`](docs/lb-table.md) | a `<table>` supplying its own scaffolding and a ghost row |
14
14
  | [`lb-unknown-page`](docs/lb-unknown-page.md) | the chrome's dialog for a URL that names no page |
15
+ | [`lb-confirm`](docs/lb-confirm.md) | a button that asks before it sends |
16
+ | [`lb-form`](docs/lb-form.md) | a form over one row, which saves per control once the row is on record |
17
+ | [`lb-insert-dialog`](docs/lb-insert-dialog.md) | a row entered in a modal dialog, chosen in the picker that opened it |
18
+ | [`lb-master`](docs/lb-master.md) | a set, one of its rows, and the buttons that act on the set |
19
+ | [`lb-checkbox`](docs/lb-checkbox.md) | a checkbox and its label, as markup to style |
15
20
 
16
21
  ## Installing
17
22
 
package/dist/control.d.ts CHANGED
@@ -1,5 +1,3 @@
1
- /** The native controls a widget wraps. */
2
- type Inner = HTMLInputElement | HTMLSelectElement;
3
1
  /**
4
2
  * A form-associated custom element wrapping one native control, which makes
5
3
  * the widget itself the control: the hub sets its `value` when a column
@@ -16,14 +14,14 @@ type Inner = HTMLInputElement | HTMLSelectElement;
16
14
  */
17
15
  export declare abstract class WrappedControl extends HTMLElement {
18
16
  static formAssociated: boolean;
19
- /** `<input>` or `<select>`: what this widget wraps. */
20
- protected abstract readonly inner: "input" | "select";
17
+ /** The tag of the native control this widget wraps. */
18
+ protected abstract inner: string;
21
19
  /**
22
20
  * Null where the DOM implementation offers no form value, which includes
23
21
  * jsdom: the widget still works as a control, and a native form submission
24
22
  * leaves it out.
25
23
  */
26
- private readonly internals;
24
+ private internals;
27
25
  /** Whether the widget has connected once. */
28
26
  private connected;
29
27
  constructor();
@@ -39,7 +37,7 @@ export declare abstract class WrappedControl extends HTMLElement {
39
37
  */
40
38
  connectedCallback(): void;
41
39
  /** The native control this widget wraps, once it is there. */
42
- protected get control(): Inner | null;
40
+ protected get control(): HTMLInputElement | HTMLSelectElement | null;
43
41
  get value(): string;
44
42
  set value(value: string);
45
43
  /**
@@ -48,10 +46,20 @@ export declare abstract class WrappedControl extends HTMLElement {
48
46
  * form its value once the control has been reset.
49
47
  */
50
48
  formResetCallback(): void;
49
+ /**
50
+ * A message the hub sets when a handler refuses this control's column. It
51
+ * goes on the native control inside, which is what the form checks before
52
+ * it submits, what the browser shows the message beside, and what takes
53
+ * focus when it is reported.
54
+ */
55
+ setCustomValidity(message: string): void;
56
+ /** Show the native control's message, and focus it. */
57
+ reportValidity(): boolean;
58
+ /** The native control's message, empty when it has none. */
59
+ get validationMessage(): string;
51
60
  /** The inner control changed, before the widget fires its own `change`. */
52
61
  protected changed(): void;
53
62
  /** Tell the form owner the value, where the implementation allows it. */
54
63
  protected formValue(): void;
55
64
  }
56
- export {};
57
65
  //# sourceMappingURL=control.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"control.d.ts","sourceRoot":"","sources":["../widgets/control.ts"],"names":[],"mappings":"AAIA,0CAA0C;AAC1C,KAAK,KAAK,GAAG,gBAAgB,GAAG,iBAAiB,CAAC;AAElD;;;;;;;;;;;;;GAaG;AACH,8BAAsB,cAAe,SAAQ,WAAW;IACtD,MAAM,CAAC,cAAc,UAAQ;IAE7B,uDAAuD;IACvD,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,QAAQ,CAAC;IAEtD;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA0B;IAEpD,6CAA6C;IAC7C,OAAO,CAAC,SAAS,CAAS;;IAa1B;;;;;;;;;OASG;IACH,iBAAiB,IAAI,IAAI;IASzB,8DAA8D;IAC9D,SAAS,KAAK,OAAO,IAAI,KAAK,GAAG,IAAI,CAEpC;IAED,IAAI,KAAK,IAAI,MAAM,CAElB;IAED,IAAI,KAAK,CAAC,KAAK,EAAE,MAAM,EAUtB;IAED;;;;OAIG;IACH,iBAAiB,IAAI,IAAI;IAIzB,2EAA2E;IAC3E,SAAS,CAAC,OAAO,IAAI,IAAI;IAIzB,yEAAyE;IACzE,SAAS,CAAC,SAAS,IAAI,IAAI;CAG5B"}
1
+ {"version":3,"file":"control.d.ts","sourceRoot":"","sources":["../widgets/control.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;GAaG;AACH,8BAAsB,cAAe,SAAQ,WAAW;IACtD,MAAM,CAAC,cAAc,UAAQ;IAE7B,uDAAuD;IACvD,SAAS,CAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAEjC;;;;OAIG;IACH,OAAO,CAAC,SAAS,CAAqB;IAEtC,6CAA6C;IAC7C,OAAO,CAAC,SAAS,CAAS;;IAY1B;;;;;;;;;OASG;IACH,iBAAiB,IAAI,IAAI;IASzB,8DAA8D;IAC9D,SAAS,KAAK,OAAO,IAAI,gBAAgB,GAAG,iBAAiB,GAAG,IAAI,CAEnE;IAED,IAAI,KAAK,IAAI,MAAM,CAElB;IAED,IAAI,KAAK,CAAC,KAAK,EAAE,MAAM,EAUtB;IAED;;;;OAIG;IACH,iBAAiB,IAAI,IAAI;IAIzB;;;;;OAKG;IACH,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAIxC,uDAAuD;IACvD,cAAc,IAAI,OAAO;IAIzB,4DAA4D;IAC5D,IAAI,iBAAiB,IAAI,MAAM,CAE9B;IAED,2EAA2E;IAC3E,SAAS,CAAC,OAAO,IAAI,IAAI;IAIzB,yEAAyE;IACzE,SAAS,CAAC,SAAS,IAAI,IAAI;CAG5B"}
package/dist/control.js CHANGED
@@ -21,12 +21,11 @@ export class WrappedControl extends HTMLElement {
21
21
  * jsdom: the widget still works as a control, and a native form submission
22
22
  * leaves it out.
23
23
  */
24
- internals;
24
+ internals = internalsOf(this);
25
25
  /** Whether the widget has connected once. */
26
26
  connected = false;
27
27
  constructor() {
28
28
  super();
29
- this.internals = internalsOf(this);
30
29
  this.addEventListener("change", (e) => {
31
30
  if (e.target === this || e.target !== this.control)
32
31
  return;
@@ -69,7 +68,7 @@ export class WrappedControl extends HTMLElement {
69
68
  console.error(`${this.localName}: no <${this.inner}> to receive the value`);
70
69
  return;
71
70
  }
72
- control.value = value;
71
+ control.value = value ?? "";
73
72
  this.formValue();
74
73
  }
75
74
  /**
@@ -80,6 +79,23 @@ export class WrappedControl extends HTMLElement {
80
79
  formResetCallback() {
81
80
  queueMicrotask(() => this.formValue());
82
81
  }
82
+ /**
83
+ * A message the hub sets when a handler refuses this control's column. It
84
+ * goes on the native control inside, which is what the form checks before
85
+ * it submits, what the browser shows the message beside, and what takes
86
+ * focus when it is reported.
87
+ */
88
+ setCustomValidity(message) {
89
+ this.control?.setCustomValidity(message);
90
+ }
91
+ /** Show the native control's message, and focus it. */
92
+ reportValidity() {
93
+ return this.control?.reportValidity() ?? true;
94
+ }
95
+ /** The native control's message, empty when it has none. */
96
+ get validationMessage() {
97
+ return this.control?.validationMessage ?? "";
98
+ }
83
99
  /** The inner control changed, before the widget fires its own `change`. */
84
100
  changed() {
85
101
  this.formValue();
@@ -1 +1 @@
1
- {"version":3,"file":"control.js","sourceRoot":"","sources":["../widgets/control.ts"],"names":[],"mappings":"AAAA,2BAA2B;AAE3B,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAK5D;;;;;;;;;;;;;GAaG;AACH,MAAM,OAAgB,cAAe,SAAQ,WAAW;IACtD,MAAM,CAAC,cAAc,GAAG,IAAI,CAAC;IAK7B;;;;OAIG;IACc,SAAS,CAA0B;IAEpD,6CAA6C;IACrC,SAAS,GAAG,KAAK,CAAC;IAE1B;QACE,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,SAAS,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;QACnC,IAAI,CAAC,gBAAgB,CAAC,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE;YACpC,IAAI,CAAC,CAAC,MAAM,KAAK,IAAI,IAAI,CAAC,CAAC,MAAM,KAAK,IAAI,CAAC,OAAO;gBAAE,OAAO;YAC3D,CAAC,CAAC,eAAe,EAAE,CAAC;YACpB,IAAI,CAAC,OAAO,EAAE,CAAC;YACf,IAAI,CAAC,aAAa,CAAC,IAAI,KAAK,CAAC,QAAQ,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;QAC7D,CAAC,CAAC,CAAC;IACL,CAAC;IAED;;;;;;;;;OASG;IACH,iBAAiB;QACf,MAAM,IAAI,GAAG,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;QACvC,IAAI,IAAI,KAAK,IAAI;YAAE,IAAI,CAAC,OAAO,EAAE,YAAY,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QAC5D,IAAI,IAAI,CAAC,SAAS;YAAE,OAAO;QAC3B,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;QACtB,MAAM,OAAO,GAAG,IAAI,CAAC,YAAY,CAAC,iBAAiB,CAAC,CAAC;QACrD,IAAI,OAAO,KAAK,IAAI;YAAE,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC;IAC7C,CAAC;IAED,8DAA8D;IAC9D,IAAc,OAAO;QACnB,OAAO,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACxC,CAAC;IAED,IAAI,KAAK;QACP,OAAO,IAAI,CAAC,OAAO,EAAE,KAAK,IAAI,EAAE,CAAC;IACnC,CAAC;IAED,IAAI,KAAK,CAAC,KAAa;QACrB,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC;QAC7B,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,OAAO,CAAC,KAAK,CACX,GAAG,IAAI,CAAC,SAAS,SAAS,IAAI,CAAC,KAAK,wBAAwB,CAC7D,CAAC;YACF,OAAO;QACT,CAAC;QACD,OAAO,CAAC,KAAK,GAAG,KAAK,CAAC;QACtB,IAAI,CAAC,SAAS,EAAE,CAAC;IACnB,CAAC;IAED;;;;OAIG;IACH,iBAAiB;QACf,cAAc,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC;IACzC,CAAC;IAED,2EAA2E;IACjE,OAAO;QACf,IAAI,CAAC,SAAS,EAAE,CAAC;IACnB,CAAC;IAED,yEAAyE;IAC/D,SAAS;QACjB,IAAI,CAAC,SAAS,EAAE,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC3C,CAAC;;AAGH,SAAS,WAAW,CAAC,EAAe;IAClC,IAAI,CAAC;QACH,MAAM,SAAS,GAAG,EAAE,CAAC,eAAe,EAAE,CAAC;QACvC,OAAO,OAAO,SAAS,CAAC,YAAY,KAAK,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC;IACzE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC","sourcesContent":["/// <reference lib=\"dom\" />\n\nimport { ATTR_COLUMN_VALUE } from \"@loadbare/app/constants\";\n\n/** The native controls a widget wraps. */\ntype Inner = HTMLInputElement | HTMLSelectElement;\n\n/**\n * A form-associated custom element wrapping one native control, which makes\n * the widget itself the control: the hub sets its `value` when a column\n * lands, reads it back when a request gathers, and commits it on `change`\n * when it carries `lb-request`. Nothing about a request is the widget's.\n *\n * The native control inside is an implementation detail. Its `change` is\n * stopped and fired again on the widget, so a listener sees one control and\n * one event per edit.\n *\n * Not a widget itself, so the file carries no `.browser` and no `lb-` name:\n * a consumer's builder passes it over, and each widget that extends it\n * brings it along.\n */\nexport abstract class WrappedControl extends HTMLElement {\n static formAssociated = true;\n\n /** `<input>` or `<select>`: what this widget wraps. */\n protected abstract readonly inner: \"input\" | \"select\";\n\n /**\n * Null where the DOM implementation offers no form value, which includes\n * jsdom: the widget still works as a control, and a native form submission\n * leaves it out.\n */\n private readonly internals: ElementInternals | null;\n\n /** Whether the widget has connected once. */\n private connected = false;\n\n constructor() {\n super();\n this.internals = internalsOf(this);\n this.addEventListener(\"change\", (e) => {\n if (e.target === this || e.target !== this.control) return;\n e.stopPropagation();\n this.changed();\n this.dispatchEvent(new Event(\"change\", { bubbles: true }));\n });\n }\n\n /**\n * A value that landed before this widget's definition did is only a\n * stamp, since the hub sets `value` on a control and the element was not\n * one yet. It is taken up the first time the widget connects, and never\n * again: after that the hub sets `value` itself, and a later move must not\n * undo an edit.\n *\n * The native control follows the widget's `form` attribute, so a form\n * this widget joins by id resets the control inside it too.\n */\n connectedCallback(): void {\n const form = this.getAttribute(\"form\");\n if (form !== null) this.control?.setAttribute(\"form\", form);\n if (this.connected) return;\n this.connected = true;\n const stamped = this.getAttribute(ATTR_COLUMN_VALUE);\n if (stamped !== null) this.value = stamped;\n }\n\n /** The native control this widget wraps, once it is there. */\n protected get control(): Inner | null {\n return this.querySelector(this.inner);\n }\n\n get value(): string {\n return this.control?.value ?? \"\";\n }\n\n set value(value: string) {\n const control = this.control;\n if (!control) {\n console.error(\n `${this.localName}: no <${this.inner}> to receive the value`,\n );\n return;\n }\n control.value = value;\n this.formValue();\n }\n\n /**\n * The native control is the form's too, and the form resets it by HTML's\n * own algorithm. This widget comes first in tree order, so it tells the\n * form its value once the control has been reset.\n */\n formResetCallback(): void {\n queueMicrotask(() => this.formValue());\n }\n\n /** The inner control changed, before the widget fires its own `change`. */\n protected changed(): void {\n this.formValue();\n }\n\n /** Tell the form owner the value, where the implementation allows it. */\n protected formValue(): void {\n this.internals?.setFormValue(this.value);\n }\n}\n\nfunction internalsOf(el: HTMLElement): ElementInternals | null {\n try {\n const internals = el.attachInternals();\n return typeof internals.setFormValue === \"function\" ? internals : null;\n } catch {\n return null;\n }\n}\n"]}
1
+ {"version":3,"file":"control.js","sourceRoot":"","sources":["../widgets/control.ts"],"names":[],"mappings":"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","sourcesContent":["/// <reference lib=\"dom\" />\n\nimport { ATTR_COLUMN_VALUE } from \"@loadbare/app/constants\";\n\n/**\n * A form-associated custom element wrapping one native control, which makes\n * the widget itself the control: the hub sets its `value` when a column\n * lands, reads it back when a request gathers, and commits it on `change`\n * when it carries `lb-request`. Nothing about a request is the widget's.\n *\n * The native control inside is an implementation detail. Its `change` is\n * stopped and fired again on the widget, so a listener sees one control and\n * one event per edit.\n *\n * Not a widget itself, so the file carries no `.browser` and no `lb-` name:\n * a consumer's builder passes it over, and each widget that extends it\n * brings it along.\n */\nexport abstract class WrappedControl extends HTMLElement {\n static formAssociated = true;\n\n /** The tag of the native control this widget wraps. */\n protected abstract inner: string;\n\n /**\n * Null where the DOM implementation offers no form value, which includes\n * jsdom: the widget still works as a control, and a native form submission\n * leaves it out.\n */\n private internals = internalsOf(this);\n\n /** Whether the widget has connected once. */\n private connected = false;\n\n constructor() {\n super();\n this.addEventListener(\"change\", (e) => {\n if (e.target === this || e.target !== this.control) return;\n e.stopPropagation();\n this.changed();\n this.dispatchEvent(new Event(\"change\", { bubbles: true }));\n });\n }\n\n /**\n * A value that landed before this widget's definition did is only a\n * stamp, since the hub sets `value` on a control and the element was not\n * one yet. It is taken up the first time the widget connects, and never\n * again: after that the hub sets `value` itself, and a later move must not\n * undo an edit.\n *\n * The native control follows the widget's `form` attribute, so a form\n * this widget joins by id resets the control inside it too.\n */\n connectedCallback(): void {\n const form = this.getAttribute(\"form\");\n if (form !== null) this.control?.setAttribute(\"form\", form);\n if (this.connected) return;\n this.connected = true;\n const stamped = this.getAttribute(ATTR_COLUMN_VALUE);\n if (stamped !== null) this.value = stamped;\n }\n\n /** The native control this widget wraps, once it is there. */\n protected get control(): HTMLInputElement | HTMLSelectElement | null {\n return this.querySelector(this.inner);\n }\n\n get value(): string {\n return this.control?.value ?? \"\";\n }\n\n set value(value: string) {\n const control = this.control;\n if (!control) {\n console.error(\n `${this.localName}: no <${this.inner}> to receive the value`,\n );\n return;\n }\n control.value = value ?? \"\";\n this.formValue();\n }\n\n /**\n * The native control is the form's too, and the form resets it by HTML's\n * own algorithm. This widget comes first in tree order, so it tells the\n * form its value once the control has been reset.\n */\n formResetCallback(): void {\n queueMicrotask(() => this.formValue());\n }\n\n /**\n * A message the hub sets when a handler refuses this control's column. It\n * goes on the native control inside, which is what the form checks before\n * it submits, what the browser shows the message beside, and what takes\n * focus when it is reported.\n */\n setCustomValidity(message: string): void {\n this.control?.setCustomValidity(message);\n }\n\n /** Show the native control's message, and focus it. */\n reportValidity(): boolean {\n return this.control?.reportValidity() ?? true;\n }\n\n /** The native control's message, empty when it has none. */\n get validationMessage(): string {\n return this.control?.validationMessage ?? \"\";\n }\n\n /** The inner control changed, before the widget fires its own `change`. */\n protected changed(): void {\n this.formValue();\n }\n\n /** Tell the form owner the value, where the implementation allows it. */\n protected formValue(): void {\n this.internals?.setFormValue(this.value);\n }\n}\n\nfunction internalsOf(el: HTMLElement): ElementInternals | null {\n try {\n const internals = el.attachInternals();\n return typeof internals.setFormValue === \"function\" ? internals : null;\n } catch {\n return null;\n }\n}\n"]}
package/dist/index.d.ts CHANGED
@@ -3,4 +3,6 @@ import "./lb-select.browser.js";
3
3
  import "./lb-options.browser.js";
4
4
  import "./lb-table.browser.js";
5
5
  import "./lb-picker.browser.js";
6
+ import "./lb-confirm.browser.js";
7
+ import "./lb-insert-dialog.browser.js";
6
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../widgets/index.ts"],"names":[],"mappings":"AAAA,OAAO,uBAAuB,CAAC;AAC/B,OAAO,wBAAwB,CAAC;AAChC,OAAO,yBAAyB,CAAC;AACjC,OAAO,uBAAuB,CAAC;AAC/B,OAAO,wBAAwB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../widgets/index.ts"],"names":[],"mappings":"AAAA,OAAO,uBAAuB,CAAC;AAC/B,OAAO,wBAAwB,CAAC;AAChC,OAAO,yBAAyB,CAAC;AACjC,OAAO,uBAAuB,CAAC;AAC/B,OAAO,wBAAwB,CAAC;AAChC,OAAO,yBAAyB,CAAC;AACjC,OAAO,+BAA+B,CAAC"}
package/dist/index.js CHANGED
@@ -3,4 +3,6 @@ import "./lb-select.browser.js";
3
3
  import "./lb-options.browser.js";
4
4
  import "./lb-table.browser.js";
5
5
  import "./lb-picker.browser.js";
6
+ import "./lb-confirm.browser.js";
7
+ import "./lb-insert-dialog.browser.js";
6
8
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../widgets/index.ts"],"names":[],"mappings":"AAAA,OAAO,uBAAuB,CAAC;AAC/B,OAAO,wBAAwB,CAAC;AAChC,OAAO,yBAAyB,CAAC;AACjC,OAAO,uBAAuB,CAAC;AAC/B,OAAO,wBAAwB,CAAC","sourcesContent":["import \"./lb-input.browser.js\";\nimport \"./lb-select.browser.js\";\nimport \"./lb-options.browser.js\";\nimport \"./lb-table.browser.js\";\nimport \"./lb-picker.browser.js\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../widgets/index.ts"],"names":[],"mappings":"AAAA,OAAO,uBAAuB,CAAC;AAC/B,OAAO,wBAAwB,CAAC;AAChC,OAAO,yBAAyB,CAAC;AACjC,OAAO,uBAAuB,CAAC;AAC/B,OAAO,wBAAwB,CAAC;AAChC,OAAO,yBAAyB,CAAC;AACjC,OAAO,+BAA+B,CAAC","sourcesContent":["import \"./lb-input.browser.js\";\nimport \"./lb-select.browser.js\";\nimport \"./lb-options.browser.js\";\nimport \"./lb-table.browser.js\";\nimport \"./lb-picker.browser.js\";\nimport \"./lb-confirm.browser.js\";\nimport \"./lb-insert-dialog.browser.js\";\n"]}
@@ -0,0 +1,13 @@
1
+ <!--
2
+ A checkbox with its label, as markup alone. The hub treats a native
3
+ checkbox as a control, so there is nothing for script to do: the widget
4
+ gives the box and its text one element an application styles.
5
+
6
+ The column and request go on the checkbox inside, since that is the
7
+ control, so they arrive as exp-column and exp-request rather than as
8
+ lb-column and lb-request on the tag.
9
+ -->
10
+ <label>
11
+ <input type="checkbox" lb-column="{{column}}" lb-request="{{request}}" />
12
+ <span>{{label}}</span>
13
+ </label>
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=lb-confirm.browser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lb-confirm.browser.d.ts","sourceRoot":"","sources":["../widgets/lb-confirm.browser.ts"],"names":[],"mappings":""}
@@ -0,0 +1,36 @@
1
+ /// <reference lib="dom" />
2
+ import { LB_DONE_EVENT_NAME } from "@loadbare/app/constants";
3
+ /**
4
+ * Opens and closes its own dialog, and nothing else.
5
+ *
6
+ * The dialog is found by position rather than by id, because ids repeat when
7
+ * the tag sits in a row template and the hub clones it once per row. HTML's
8
+ * own `commandfor` would open it with no script, but only by id.
9
+ *
10
+ * The confirm button carries `lb-request`, and the hub commits it. This code
11
+ * never touches the request; it closes the dialog when `lb-request-done` says
12
+ * the request succeeded. A refused request leaves the dialog open, with the
13
+ * hub's `lb-request-error` on the confirm button, so the user sees the
14
+ * refusal where they asked and can cancel or try again.
15
+ */
16
+ class LbConfirm extends HTMLElement {
17
+ constructor() {
18
+ super();
19
+ this.addEventListener("click", (e) => {
20
+ const target = e.target;
21
+ if (target.closest("[data-open]"))
22
+ this.dialog.showModal();
23
+ else if (target.closest("[data-close]"))
24
+ this.dialog.close();
25
+ });
26
+ this.addEventListener(LB_DONE_EVENT_NAME, (e) => {
27
+ if (!e.detail.error)
28
+ this.dialog.close();
29
+ });
30
+ }
31
+ get dialog() {
32
+ return this.querySelector("dialog");
33
+ }
34
+ }
35
+ customElements.define("lb-confirm", LbConfirm);
36
+ //# sourceMappingURL=lb-confirm.browser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lb-confirm.browser.js","sourceRoot":"","sources":["../widgets/lb-confirm.browser.ts"],"names":[],"mappings":"AAAA,2BAA2B;AAE3B,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAG7D;;;;;;;;;;;;GAYG;AACH,MAAM,SAAU,SAAQ,WAAW;IACjC;QACE,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,gBAAgB,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE;YACnC,MAAM,MAAM,GAAG,CAAC,CAAC,MAAiB,CAAC;YACnC,IAAI,MAAM,CAAC,OAAO,CAAC,aAAa,CAAC;gBAAE,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC;iBACtD,IAAI,MAAM,CAAC,OAAO,CAAC,cAAc,CAAC;gBAAE,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;QAC/D,CAAC,CAAC,CAAC;QACH,IAAI,CAAC,gBAAgB,CAAC,kBAAkB,EAAE,CAAC,CAAC,EAAE,EAAE;YAC9C,IAAI,CAAE,CAAiC,CAAC,MAAM,CAAC,KAAK;gBAAE,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;QAC5E,CAAC,CAAC,CAAC;IACL,CAAC;IAED,IAAY,MAAM;QAChB,OAAO,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAE,CAAC;IACvC,CAAC;CACF;AAED,cAAc,CAAC,MAAM,CAAC,YAAY,EAAE,SAAS,CAAC,CAAC","sourcesContent":["/// <reference lib=\"dom\" />\n\nimport { LB_DONE_EVENT_NAME } from \"@loadbare/app/constants\";\nimport type { HubRequestDone } from \"@loadbare/app/types\";\n\n/**\n * Opens and closes its own dialog, and nothing else.\n *\n * The dialog is found by position rather than by id, because ids repeat when\n * the tag sits in a row template and the hub clones it once per row. HTML's\n * own `commandfor` would open it with no script, but only by id.\n *\n * The confirm button carries `lb-request`, and the hub commits it. This code\n * never touches the request; it closes the dialog when `lb-request-done` says\n * the request succeeded. A refused request leaves the dialog open, with the\n * hub's `lb-request-error` on the confirm button, so the user sees the\n * refusal where they asked and can cancel or try again.\n */\nclass LbConfirm extends HTMLElement {\n constructor() {\n super();\n this.addEventListener(\"click\", (e) => {\n const target = e.target as Element;\n if (target.closest(\"[data-open]\")) this.dialog.showModal();\n else if (target.closest(\"[data-close]\")) this.dialog.close();\n });\n this.addEventListener(LB_DONE_EVENT_NAME, (e) => {\n if (!(e as CustomEvent<HubRequestDone>).detail.error) this.dialog.close();\n });\n }\n\n private get dialog(): HTMLDialogElement {\n return this.querySelector(\"dialog\")!;\n }\n}\n\ncustomElements.define(\"lb-confirm\", LbConfirm);\n"]}
@@ -0,0 +1,27 @@
1
+ <!--
2
+ A button that asks before it sends. The request is on the dialog's confirm
3
+ button, never on the tag: nothing reaches the hub until the user says yes,
4
+ so there is no request to catch and send again.
5
+
6
+ The dialog is in the page like any other element, inside whatever row the
7
+ tag sits in, so the confirm button's query and key are that row's, and the
8
+ message written inside the tag can show the row's columns with lb-column.
9
+
10
+ Cancel comes first and takes focus, so Enter on a dialog opened by accident
11
+ does nothing harmful. The confirm button does not close the dialog; the
12
+ widget does, once the request has succeeded.
13
+ -->
14
+ <button type="button" class="lb-confirm-open" data-open>{{label}}</button>
15
+ <dialog class="lb-confirm-dialog">
16
+ <div class="lb-confirm-message" lb-exp-slot></div>
17
+ <div class="lb-confirm-buttons">
18
+ <button type="button" data-close autofocus>Cancel</button>
19
+ <button
20
+ type="button"
21
+ class="lb-confirm-yes"
22
+ lb-request="{{request}}"
23
+ >
24
+ {{confirm|Confirm}}
25
+ </button>
26
+ </div>
27
+ </dialog>
@@ -0,0 +1,7 @@
1
+ <!--
2
+ One row's controls, and nothing else. A button that acts on the row as a
3
+ whole adds a row to a set or takes one out, so it belongs to whatever holds
4
+ the set, and reaches the form from there the way HTML lets any button reach
5
+ any form: by `form="<id>"`.
6
+ -->
7
+ <form id="{{id}}" class="lb-form" lb-exp-slot></form>
@@ -1,14 +1,14 @@
1
1
  /// <reference lib="dom" />
2
2
  import { WrappedControl } from "./control.js";
3
3
  /**
4
- * A control wrapping an `<input>`. The hub sets its `value` from the column
5
- * its `lb-column` names, and gathers it back under the same name.
4
+ * An `<input>` with its label, as one control. The hub sets its `value` from
5
+ * the column its `lb-column` names, and gathers it back under the same name.
6
6
  *
7
- * It issues nothing on its own. With `lb-request` it commits on `change`, and
8
- * the hub sends that request with this input's column alone: an
9
- * `lb-row-update` reaches the server with the query and key of the row it
10
- * sits in. Without `lb-request` an edit waits for the form or row around it,
11
- * because committing per field is one editing model and not the only one.
7
+ * Without `lb-request` a change commits nothing, which is what an input inside
8
+ * a form or a ghost row wants: the form gathers it on submit, and one that also
9
+ * sent its own request would write the same edit twice.
10
+ *
11
+ * Its definition takes `exp-type`, the input's `type`, `text` when absent.
12
12
  */
13
13
  class LbInput extends WrappedControl {
14
14
  inner = "input";
@@ -1 +1 @@
1
- {"version":3,"file":"lb-input.browser.js","sourceRoot":"","sources":["../widgets/lb-input.browser.ts"],"names":[],"mappings":"AAAA,2BAA2B;AAE3B,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE9C;;;;;;;;;GASG;AACH,MAAM,OAAQ,SAAQ,cAAc;IACf,KAAK,GAAG,OAAO,CAAC;CACpC;AAED,cAAc,CAAC,MAAM,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC","sourcesContent":["/// <reference lib=\"dom\" />\n\nimport { WrappedControl } from \"./control.js\";\n\n/**\n * A control wrapping an `<input>`. The hub sets its `value` from the column\n * its `lb-column` names, and gathers it back under the same name.\n *\n * It issues nothing on its own. With `lb-request` it commits on `change`, and\n * the hub sends that request with this input's column alone: an\n * `lb-row-update` reaches the server with the query and key of the row it\n * sits in. Without `lb-request` an edit waits for the form or row around it,\n * because committing per field is one editing model and not the only one.\n */\nclass LbInput extends WrappedControl {\n protected readonly inner = \"input\";\n}\n\ncustomElements.define(\"lb-input\", LbInput);\n"]}
1
+ {"version":3,"file":"lb-input.browser.js","sourceRoot":"","sources":["../widgets/lb-input.browser.ts"],"names":[],"mappings":"AAAA,2BAA2B;AAE3B,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE9C;;;;;;;;;GASG;AACH,MAAM,OAAQ,SAAQ,cAAc;IACxB,KAAK,GAAG,OAAO,CAAC;CAC3B;AAED,cAAc,CAAC,MAAM,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC","sourcesContent":["/// <reference lib=\"dom\" />\n\nimport { WrappedControl } from \"./control.js\";\n\n/**\n * An `<input>` with its label, as one control. The hub sets its `value` from\n * the column its `lb-column` names, and gathers it back under the same name.\n *\n * Without `lb-request` a change commits nothing, which is what an input inside\n * a form or a ghost row wants: the form gathers it on submit, and one that also\n * sent its own request would write the same edit twice.\n *\n * Its definition takes `exp-type`, the input's `type`, `text` when absent.\n */\nclass LbInput extends WrappedControl {\n protected inner = \"input\";\n}\n\ncustomElements.define(\"lb-input\", LbInput);\n"]}
@@ -1 +1 @@
1
- <label>{{label}} <input readonly="{{readonly}}" /></label>
1
+ <label>{{label}} <input type="{{type|text}}" readonly="{{readonly}}" /></label>
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=lb-insert-dialog.browser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lb-insert-dialog.browser.d.ts","sourceRoot":"","sources":["../widgets/lb-insert-dialog.browser.ts"],"names":[],"mappings":""}
@@ -0,0 +1,59 @@
1
+ /// <reference lib="dom" />
2
+ import { LB_DONE_EVENT_NAME } from "@loadbare/app/constants";
3
+ /**
4
+ * Two rules. Closing the dialog resets the form, and the row the form creates
5
+ * is chosen in the picker the dialog was opened from.
6
+ *
7
+ * Save closes the dialog when `lb-request-done` says the insert succeeded,
8
+ * so there is a row to show for it, and a refusal leaves the entry open to
9
+ * correct. Cancel and Escape close it, and throw away what was typed.
10
+ *
11
+ * The picker is the nearest `lb-options` around the button that opened the
12
+ * dialog, which a `command` event names as its `source`. The new row's key is
13
+ * in the patch the insert answers with, and by the time `lb-request-done`
14
+ * reaches here that row is among the picker's options. Choosing it fires
15
+ * `change`, so a picker that saves its own column saves it, and one in a ghost
16
+ * row or a new record holds it until Save.
17
+ *
18
+ * `close` and `command` do not bubble, so they are heard in capture.
19
+ */
20
+ class LbInsertDialog extends HTMLElement {
21
+ /** The button that last opened the dialog. */
22
+ opener = null;
23
+ constructor() {
24
+ super();
25
+ this.addEventListener("close", () => this.querySelector("form")?.reset(), true);
26
+ this.addEventListener("command", (e) => {
27
+ this.opener = e.source ?? null;
28
+ }, true);
29
+ this.addEventListener(LB_DONE_EVENT_NAME, (e) => {
30
+ const { request, items, error } = e
31
+ .detail;
32
+ if (error)
33
+ return;
34
+ const item = items.find((i) => i.query === request.query);
35
+ const created = item?.patch?.rows?.[0];
36
+ if (created)
37
+ this.choose(String(created[item.key]));
38
+ this.querySelector("dialog")?.close();
39
+ });
40
+ }
41
+ choose(key) {
42
+ const picker = pickerNear(this.opener);
43
+ if (!picker)
44
+ return;
45
+ picker.value = key;
46
+ picker.dispatchEvent(new Event("change", { bubbles: true }));
47
+ }
48
+ }
49
+ /** The nearest `lb-options` around an element, searching outward. */
50
+ function pickerNear(el) {
51
+ for (let node = el?.parentElement; node; node = node.parentElement) {
52
+ const found = node.querySelector("lb-options");
53
+ if (found)
54
+ return found;
55
+ }
56
+ return null;
57
+ }
58
+ customElements.define("lb-insert-dialog", LbInsertDialog);
59
+ //# sourceMappingURL=lb-insert-dialog.browser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lb-insert-dialog.browser.js","sourceRoot":"","sources":["../widgets/lb-insert-dialog.browser.ts"],"names":[],"mappings":"AAAA,2BAA2B;AAE3B,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAG7D;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,cAAe,SAAQ,WAAW;IACtC,8CAA8C;IACtC,MAAM,GAAmB,IAAI,CAAC;IAEtC;QACE,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,gBAAgB,CACnB,OAAO,EACP,GAAG,EAAE,CAAC,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,EACzC,IAAI,CACL,CAAC;QACF,IAAI,CAAC,gBAAgB,CACnB,SAAS,EACT,CAAC,CAAC,EAAE,EAAE;YACJ,IAAI,CAAC,MAAM,GAAI,CAAyC,CAAC,MAAM,IAAI,IAAI,CAAC;QAC1E,CAAC,EACD,IAAI,CACL,CAAC;QACF,IAAI,CAAC,gBAAgB,CAAC,kBAAkB,EAAE,CAAC,CAAC,EAAE,EAAE;YAC9C,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,GAAI,CAAiC;iBACjE,MAAM,CAAC;YACV,IAAI,KAAK;gBAAE,OAAO;YAClB,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,OAAO,CAAC,KAAK,CAAC,CAAC;YAC1D,MAAM,OAAO,GAAG,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;YACvC,IAAI,OAAO;gBAAE,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,IAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;YACrD,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,EAAE,KAAK,EAAE,CAAC;QACxC,CAAC,CAAC,CAAC;IACL,CAAC;IAEO,MAAM,CAAC,GAAW;QACxB,MAAM,MAAM,GAAG,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACvC,IAAI,CAAC,MAAM;YAAE,OAAO;QACpB,MAAM,CAAC,KAAK,GAAG,GAAG,CAAC;QACnB,MAAM,CAAC,aAAa,CAAC,IAAI,KAAK,CAAC,QAAQ,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IAC/D,CAAC;CACF;AAED,qEAAqE;AACrE,SAAS,UAAU,CACjB,EAAkB;IAElB,KAAK,IAAI,IAAI,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,IAAI,GAAG,IAAI,CAAC,aAAa,EAAE,CAAC;QACnE,MAAM,KAAK,GAAG,IAAI,CAAC,aAAa,CAAC,YAAY,CAAC,CAAC;QAC/C,IAAI,KAAK;YAAE,OAAO,KAAwC,CAAC;IAC7D,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,cAAc,CAAC,MAAM,CAAC,kBAAkB,EAAE,cAAc,CAAC,CAAC","sourcesContent":["/// <reference lib=\"dom\" />\n\nimport { LB_DONE_EVENT_NAME } from \"@loadbare/app/constants\";\nimport type { HubRequestDone } from \"@loadbare/app/types\";\n\n/**\n * Two rules. Closing the dialog resets the form, and the row the form creates\n * is chosen in the picker the dialog was opened from.\n *\n * Save closes the dialog when `lb-request-done` says the insert succeeded,\n * so there is a row to show for it, and a refusal leaves the entry open to\n * correct. Cancel and Escape close it, and throw away what was typed.\n *\n * The picker is the nearest `lb-options` around the button that opened the\n * dialog, which a `command` event names as its `source`. The new row's key is\n * in the patch the insert answers with, and by the time `lb-request-done`\n * reaches here that row is among the picker's options. Choosing it fires\n * `change`, so a picker that saves its own column saves it, and one in a ghost\n * row or a new record holds it until Save.\n *\n * `close` and `command` do not bubble, so they are heard in capture.\n */\nclass LbInsertDialog extends HTMLElement {\n /** The button that last opened the dialog. */\n private opener: Element | null = null;\n\n constructor() {\n super();\n this.addEventListener(\n \"close\",\n () => this.querySelector(\"form\")?.reset(),\n true,\n );\n this.addEventListener(\n \"command\",\n (e) => {\n this.opener = (e as Event & { source?: Element | null }).source ?? null;\n },\n true,\n );\n this.addEventListener(LB_DONE_EVENT_NAME, (e) => {\n const { request, items, error } = (e as CustomEvent<HubRequestDone>)\n .detail;\n if (error) return;\n const item = items.find((i) => i.query === request.query);\n const created = item?.patch?.rows?.[0];\n if (created) this.choose(String(created[item!.key]));\n this.querySelector(\"dialog\")?.close();\n });\n }\n\n private choose(key: string): void {\n const picker = pickerNear(this.opener);\n if (!picker) return;\n picker.value = key;\n picker.dispatchEvent(new Event(\"change\", { bubbles: true }));\n }\n}\n\n/** The nearest `lb-options` around an element, searching outward. */\nfunction pickerNear(\n el: Element | null,\n): (HTMLElement & { value: string }) | null {\n for (let node = el?.parentElement; node; node = node.parentElement) {\n const found = node.querySelector(\"lb-options\");\n if (found) return found as HTMLElement & { value: string };\n }\n return null;\n}\n\ncustomElements.define(\"lb-insert-dialog\", LbInsertDialog);\n"]}
@@ -0,0 +1,25 @@
1
+ <!--
2
+ A row entered in a modal dialog, for a set that is being chosen from
3
+ somewhere else on the page. The page writes the controls inside the tag and
4
+ names the query with exp-query; the dialog supplies the form and its two
5
+ buttons.
6
+
7
+ It is written once, outside every row, and opened by id with HTML's own
8
+ commandfor="{{id}}" command="show-modal" on as many buttons as want it. It
9
+ cannot sit beside the control it serves: a form inside another form is
10
+ dropped by the parser, and lb-table joins every control in a ghost row to
11
+ that row's form.
12
+
13
+ Save is the form's lb-row-insert. Cancel is HTML's close command, and
14
+ closing resets the form.
15
+ -->
16
+ <dialog id="{{id}}" class="lb-insert-dialog">
17
+ <h2>{{title}}</h2>
18
+ <form lb-query="{{query}}" lb-request="lb-row-insert">
19
+ <div class="lb-insert-dialog-controls" lb-exp-slot></div>
20
+ <div class="lb-insert-dialog-buttons">
21
+ <button type="button" commandfor="{{id}}" command="close">Cancel</button>
22
+ <button type="submit">{{save|Save}}</button>
23
+ </div>
24
+ </form>
25
+ </dialog>
@@ -0,0 +1,56 @@
1
+ <!--
2
+ A set on the left, one of its rows on the right, and the buttons that act on
3
+ the set. The page writes the picker into the "picker" template and the row's
4
+ controls inside the tag, and names the row's query with exp-row.
5
+
6
+ The row lands on the element around the form and the bar, so every button
7
+ there knows which row it means. A row with an empty key is a new one not
8
+ yet entered, and the stylesheet shows each button only in the mode it
9
+ belongs to.
10
+
11
+ The standard bar is [New] [Copy] [Delete] [Save new] [Cancel].
12
+ - New is a request the page declares, `new`. Which row shows is a query
13
+ parm, and a button writes none, so the page's handler returns url()
14
+ emptying the parm, and the row query answers a row not yet entered.
15
+ It pushes a history entry, so Back returns to the row on record.
16
+ - Copy is a request the page declares, `copy`. It goes to the server by
17
+ design rather than filling the form for Save new: a copy is a new row in
18
+ the set, and only the server can bring its children along. What comes
19
+ along is the page's to decide.
20
+ - Delete asks first. The page writes the question into the
21
+ "delete-message" template; it sits in the row, so lb-column can name
22
+ what is about to go.
23
+ - Cancel is HTML's reset: it throws away what was typed into a new row.
24
+ -->
25
+ <div class="lb-master">
26
+ <div class="lb-master-picker" lb-exp-template="picker"></div>
27
+ <div class="lb-master-row" lb-query="{{row}}">
28
+ <lb-form exp-id="{{form-id|master-form}}" lb-exp-slot></lb-form>
29
+ <div class="lb-master-buttons">
30
+ <button type="button" lb-request="new" lb-url-push data-mode="on-record">
31
+ New
32
+ </button>
33
+ <button type="button" lb-request="copy" data-mode="on-record">Copy</button>
34
+ <lb-confirm
35
+ exp-label="Delete"
36
+ exp-request="lb-row-delete"
37
+ exp-confirm="Delete"
38
+ data-mode="on-record"
39
+ >
40
+ <div lb-exp-template="delete-message"></div>
41
+ </lb-confirm>
42
+ <button
43
+ type="submit"
44
+ form="{{form-id|master-form}}"
45
+ lb-request="lb-row-insert"
46
+ lb-url-push
47
+ data-mode="new"
48
+ >
49
+ Save new
50
+ </button>
51
+ <button type="reset" form="{{form-id|master-form}}" data-mode="new">
52
+ Cancel
53
+ </button>
54
+ </div>
55
+ </div>
56
+ </div>
@@ -1,7 +1,33 @@
1
- import type { Row, RowsHost } from "@loadbare/app/types";
1
+ import type { RowsHost } from "@loadbare/app/types";
2
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
+ */
3
29
  export declare class LbOptions extends WrappedControl implements RowsHost {
4
- protected readonly inner = "select";
30
+ protected inner: string;
5
31
  /**
6
32
  * The value last set or chosen, which the options may not include yet.
7
33
  * Null when nothing chose one, so the select's own default stands.
@@ -11,14 +37,7 @@ export declare class LbOptions extends WrappedControl implements RowsHost {
11
37
  set value(value: string);
12
38
  formResetCallback(): void;
13
39
  protected changed(): void;
14
- /**
15
- * The hub reconciles the rows and calls this for placement only, so this
16
- * widget carries no copy of what a row is or when one arrives.
17
- */
18
- lbPlaceRow(el: Element, row: Row, template: HTMLTemplateElement): void;
19
- /** Everything derived from the rows, once the whole result has landed. */
40
+ /** Everything derived from the rows, once the whole answer has landed. */
20
41
  lbRowsLanded(): void;
21
- /** The group with this label, or a new one in front of the template. */
22
- private groupFor;
23
42
  }
24
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":"AAGA,OAAO,KAAK,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAEzD,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AA8B9C,qBAAa,SAAU,SAAQ,cAAe,YAAW,QAAQ;IAC/D,SAAS,CAAC,QAAQ,CAAC,KAAK,YAAY;IAEpC;;;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;;;OAGG;IACH,UAAU,CAAC,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,mBAAmB,GAAG,IAAI;IAUtE,0EAA0E;IAC1E,YAAY,IAAI,IAAI;IAgBpB,wEAAwE;IACxE,OAAO,CAAC,QAAQ;CASjB"}
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"}