@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.
- package/README.md +6 -1
- package/dist/control.d.ts +15 -7
- package/dist/control.d.ts.map +1 -1
- package/dist/control.js +19 -3
- package/dist/control.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/lb-checkbox.html +13 -0
- package/dist/lb-confirm.browser.d.ts +2 -0
- package/dist/lb-confirm.browser.d.ts.map +1 -0
- package/dist/lb-confirm.browser.js +36 -0
- package/dist/lb-confirm.browser.js.map +1 -0
- package/dist/lb-confirm.html +27 -0
- package/dist/lb-form.html +7 -0
- package/dist/lb-input.browser.js +7 -7
- package/dist/lb-input.browser.js.map +1 -1
- package/dist/lb-input.html +1 -1
- package/dist/lb-insert-dialog.browser.d.ts +2 -0
- package/dist/lb-insert-dialog.browser.d.ts.map +1 -0
- package/dist/lb-insert-dialog.browser.js +59 -0
- package/dist/lb-insert-dialog.browser.js.map +1 -0
- package/dist/lb-insert-dialog.html +25 -0
- package/dist/lb-master.html +56 -0
- package/dist/lb-options.browser.d.ts +29 -10
- package/dist/lb-options.browser.d.ts.map +1 -1
- package/dist/lb-options.browser.js +26 -55
- package/dist/lb-options.browser.js.map +1 -1
- package/dist/lb-picker.browser.js +7 -10
- package/dist/lb-picker.browser.js.map +1 -1
- package/dist/lb-picker.html +1 -1
- package/dist/lb-select.browser.js +4 -7
- package/dist/lb-select.browser.js.map +1 -1
- package/dist/lb-table.browser.js +61 -88
- package/dist/lb-table.browser.js.map +1 -1
- package/dist/lb-table.html +1 -0
- package/docs/lb-checkbox.md +63 -0
- package/docs/lb-confirm.md +45 -0
- package/docs/lb-form.md +35 -0
- package/docs/lb-input.md +1 -0
- package/docs/lb-insert-dialog.md +50 -0
- package/docs/lb-master.md +65 -0
- package/docs/lb-options.md +11 -7
- package/docs/lb-picker.md +10 -11
- package/docs/lb-table.md +52 -20
- 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
|
|
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
|
|
9
|
-
* `<optgroup
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
|
17
|
-
*
|
|
18
|
-
* the
|
|
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
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
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
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
74
|
-
|
|
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
|
-
//
|
|
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":"AAAA,2BAA2B;AAE3B,OAAO,
|
|
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.
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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
|
|
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
|
|
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"]}
|
package/dist/lb-picker.html
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
/// <reference lib="dom" />
|
|
2
2
|
import { WrappedControl } from "./control.js";
|
|
3
3
|
/**
|
|
4
|
-
* A
|
|
5
|
-
*
|
|
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
|
-
*
|
|
9
|
-
*
|
|
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
|
|
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"]}
|
package/dist/lb-table.browser.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/// <reference lib="dom" />
|
|
2
|
-
import { ATTR_COLUMN,
|
|
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
|
|
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
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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
|
|
23
|
-
*
|
|
24
|
-
*
|
|
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
|
|
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
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
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
|
|
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
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
/**
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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":"AAAA,2BAA2B;AAE3B,OAAO,
|
|
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"]}
|
package/dist/lb-table.html
CHANGED
|
@@ -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.
|
package/docs/lb-form.md
ADDED
|
@@ -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>"`.
|