@loadbare/widgets 0.8.1 → 0.9.1
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 +2 -0
- 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-date.browser.d.ts +2 -0
- package/dist/lb-date.browser.d.ts.map +1 -0
- package/dist/lb-date.browser.js +47 -0
- package/dist/lb-date.browser.js.map +1 -0
- package/dist/lb-date.html +23 -0
- package/dist/lb-insert-dialog.browser.js +21 -4
- package/dist/lb-insert-dialog.browser.js.map +1 -1
- package/dist/lb-number.browser.d.ts +2 -0
- package/dist/lb-number.browser.d.ts.map +1 -0
- package/dist/lb-number.browser.js +40 -0
- package/dist/lb-number.browser.js.map +1 -0
- package/dist/lb-number.html +24 -0
- package/dist/lb-options.browser.d.ts.map +1 -1
- package/dist/lb-options.browser.js +3 -1
- package/dist/lb-options.browser.js.map +1 -1
- package/dist/lb-options.html +1 -1
- package/dist/lb-table.browser.js +71 -3
- package/dist/lb-table.browser.js.map +1 -1
- package/docs/lb-date.md +48 -0
- package/docs/lb-insert-dialog.md +24 -0
- package/docs/lb-number.md +53 -0
- package/docs/lb-options.md +18 -0
- package/docs/lb-table.md +38 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -17,6 +17,8 @@ Inventory:
|
|
|
17
17
|
| [`lb-insert-dialog`](docs/lb-insert-dialog.md) | a row entered in a modal dialog, chosen in the picker that opened it |
|
|
18
18
|
| [`lb-master`](docs/lb-master.md) | a set, one of its rows, and the buttons that act on the set |
|
|
19
19
|
| [`lb-checkbox`](docs/lb-checkbox.md) | a checkbox and its label, as markup to style |
|
|
20
|
+
| [`lb-number`](docs/lb-number.md) | a number shown by `Intl.NumberFormat` |
|
|
21
|
+
| [`lb-date`](docs/lb-date.md) | a date or time shown by `Intl.DateTimeFormat` |
|
|
20
22
|
|
|
21
23
|
## Installing
|
|
22
24
|
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -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;AAChC,OAAO,yBAAyB,CAAC;AACjC,OAAO,+BAA+B,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;AACvC,OAAO,wBAAwB,CAAC;AAChC,OAAO,sBAAsB,CAAC"}
|
package/dist/index.js
CHANGED
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;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"]}
|
|
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;AACvC,OAAO,wBAAwB,CAAC;AAChC,OAAO,sBAAsB,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\";\nimport \"./lb-number.browser.js\";\nimport \"./lb-date.browser.js\";\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lb-date.browser.d.ts","sourceRoot":"","sources":["../widgets/lb-date.browser.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
import { ATTR_COLUMN_VALUE } from "@loadbare/app/constants";
|
|
3
|
+
/**
|
|
4
|
+
* Shows the value that landed on it through `Intl.DateTimeFormat`, in the
|
|
5
|
+
* `<time>` its definition holds, with the value itself as the `<time>`'s
|
|
6
|
+
* `datetime`. Not a control: the hub stamps `lb-column-value` on the tag and
|
|
7
|
+
* never gathers it.
|
|
8
|
+
*
|
|
9
|
+
* A date alone, `YYYY-MM-DD`, names a day and no instant, so it shows as
|
|
10
|
+
* that day in UTC rather than as the midnight it would be in UTC, shifted to
|
|
11
|
+
* the browser's zone. A value that is no date shows as it arrived.
|
|
12
|
+
*/
|
|
13
|
+
class LbDate extends HTMLElement {
|
|
14
|
+
static observedAttributes = [ATTR_COLUMN_VALUE];
|
|
15
|
+
formatters = new Map();
|
|
16
|
+
attributeChangedCallback(_name, _old, value) {
|
|
17
|
+
const time = this.querySelector("time");
|
|
18
|
+
if (!time)
|
|
19
|
+
return;
|
|
20
|
+
if (value === null) {
|
|
21
|
+
time.removeAttribute("datetime");
|
|
22
|
+
time.textContent = "";
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
time.dateTime = value;
|
|
26
|
+
const date = new Date(value);
|
|
27
|
+
time.textContent = Number.isNaN(date.getTime())
|
|
28
|
+
? value
|
|
29
|
+
: this.format(time, DAY.test(value)).format(date);
|
|
30
|
+
}
|
|
31
|
+
format(time, day) {
|
|
32
|
+
let formatter = this.formatters.get(day);
|
|
33
|
+
if (!formatter) {
|
|
34
|
+
const { locale, ...options } = time.dataset;
|
|
35
|
+
formatter = new Intl.DateTimeFormat(locale, {
|
|
36
|
+
...options,
|
|
37
|
+
...(day ? { timeZone: "UTC" } : {}),
|
|
38
|
+
});
|
|
39
|
+
this.formatters.set(day, formatter);
|
|
40
|
+
}
|
|
41
|
+
return formatter;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** A date with no time, as Postgres writes a `date`. */
|
|
45
|
+
const DAY = /^\d{4}-\d{2}-\d{2}$/;
|
|
46
|
+
customElements.define("lb-date", LbDate);
|
|
47
|
+
//# sourceMappingURL=lb-date.browser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lb-date.browser.js","sourceRoot":"","sources":["../widgets/lb-date.browser.ts"],"names":[],"mappings":"AAAA,2BAA2B;AAE3B,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAE5D;;;;;;;;;GASG;AACH,MAAM,MAAO,SAAQ,WAAW;IAC9B,MAAM,CAAC,kBAAkB,GAAG,CAAC,iBAAiB,CAAC,CAAC;IAExC,UAAU,GAAG,IAAI,GAAG,EAAgC,CAAC;IAE7D,wBAAwB,CACtB,KAAa,EACb,IAAmB,EACnB,KAAoB;QAEpB,MAAM,IAAI,GAAG,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;QACxC,IAAI,CAAC,IAAI;YAAE,OAAO;QAClB,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;YACnB,IAAI,CAAC,eAAe,CAAC,UAAU,CAAC,CAAC;YACjC,IAAI,CAAC,WAAW,GAAG,EAAE,CAAC;YACtB,OAAO;QACT,CAAC;QACD,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAC;QACtB,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC;QAC7B,IAAI,CAAC,WAAW,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;YAC7C,CAAC,CAAC,KAAK;YACP,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACtD,CAAC;IAEO,MAAM,CAAC,IAAqB,EAAE,GAAY;QAChD,IAAI,SAAS,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACzC,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC;YAC5C,SAAS,GAAG,IAAI,IAAI,CAAC,cAAc,CAAC,MAAM,EAAE;gBAC1C,GAAI,OAAsC;gBAC1C,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aACpC,CAAC,CAAC;YACH,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;QACtC,CAAC;QACD,OAAO,SAAS,CAAC;IACnB,CAAC;;AAGH,wDAAwD;AACxD,MAAM,GAAG,GAAG,qBAAqB,CAAC;AAElC,cAAc,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC","sourcesContent":["/// <reference lib=\"dom\" />\n\nimport { ATTR_COLUMN_VALUE } from \"@loadbare/app/constants\";\n\n/**\n * Shows the value that landed on it through `Intl.DateTimeFormat`, in the\n * `<time>` its definition holds, with the value itself as the `<time>`'s\n * `datetime`. Not a control: the hub stamps `lb-column-value` on the tag and\n * never gathers it.\n *\n * A date alone, `YYYY-MM-DD`, names a day and no instant, so it shows as\n * that day in UTC rather than as the midnight it would be in UTC, shifted to\n * the browser's zone. A value that is no date shows as it arrived.\n */\nclass LbDate extends HTMLElement {\n static observedAttributes = [ATTR_COLUMN_VALUE];\n\n private formatters = new Map<boolean, Intl.DateTimeFormat>();\n\n attributeChangedCallback(\n _name: string,\n _old: string | null,\n value: string | null,\n ): void {\n const time = this.querySelector(\"time\");\n if (!time) return;\n if (value === null) {\n time.removeAttribute(\"datetime\");\n time.textContent = \"\";\n return;\n }\n time.dateTime = value;\n const date = new Date(value);\n time.textContent = Number.isNaN(date.getTime())\n ? value\n : this.format(time, DAY.test(value)).format(date);\n }\n\n private format(time: HTMLTimeElement, day: boolean): Intl.DateTimeFormat {\n let formatter = this.formatters.get(day);\n if (!formatter) {\n const { locale, ...options } = time.dataset;\n formatter = new Intl.DateTimeFormat(locale, {\n ...(options as Intl.DateTimeFormatOptions),\n ...(day ? { timeZone: \"UTC\" } : {}),\n });\n this.formatters.set(day, formatter);\n }\n return formatter;\n }\n}\n\n/** A date with no time, as Postgres writes a `date`. */\nconst DAY = /^\\d{4}-\\d{2}-\\d{2}$/;\n\ncustomElements.define(\"lb-date\", LbDate);\n"]}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
A date or a time, as the browser's Intl.DateTimeFormat shows it. Each
|
|
3
|
+
parameter is one of Intl's own options, in kebab-case, carried to the
|
|
4
|
+
script on the <time> as a data-* attribute; the dataset reads them back in
|
|
5
|
+
camelCase, as Intl names them. A parameter not given drops its attribute,
|
|
6
|
+
and Intl's default stands.
|
|
7
|
+
-->
|
|
8
|
+
<time
|
|
9
|
+
data-locale="{{locale}}"
|
|
10
|
+
data-date-style="{{date-style}}"
|
|
11
|
+
data-time-style="{{time-style}}"
|
|
12
|
+
data-time-zone="{{time-zone}}"
|
|
13
|
+
data-time-zone-name="{{time-zone-name}}"
|
|
14
|
+
data-hour-cycle="{{hour-cycle}}"
|
|
15
|
+
data-weekday="{{weekday}}"
|
|
16
|
+
data-era="{{era}}"
|
|
17
|
+
data-year="{{year}}"
|
|
18
|
+
data-month="{{month}}"
|
|
19
|
+
data-day="{{day}}"
|
|
20
|
+
data-hour="{{hour}}"
|
|
21
|
+
data-minute="{{minute}}"
|
|
22
|
+
data-second="{{second}}"
|
|
23
|
+
></time>
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/// <reference lib="dom" />
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
* is chosen in the picker the dialog was opened from
|
|
3
|
+
* Three rules. Closing the dialog resets the form, the row the form creates
|
|
4
|
+
* is chosen in the picker the dialog was opened from, and the dialog opens
|
|
5
|
+
* beside the button that opened it.
|
|
5
6
|
*
|
|
6
7
|
* Save closes the dialog when the hub says the insert succeeded, so there
|
|
7
8
|
* is a row to show for it, and a refusal leaves the entry open to correct. Cancel and Escape close it, and throw away what was typed.
|
|
@@ -13,6 +14,12 @@
|
|
|
13
14
|
* `change`, so a picker that saves its own column saves it, and one in a ghost
|
|
14
15
|
* row or a new record holds it until Save.
|
|
15
16
|
*
|
|
17
|
+
* The opener carries `insert-dialog-opener` while the dialog is open, and
|
|
18
|
+
* the widget carries `anchored`; the page's stylesheet makes the one an
|
|
19
|
+
* anchor and positions the other's dialog on it. Only one modal is open at a
|
|
20
|
+
* time, so one anchor name serves every dialog. Opened with no source, the
|
|
21
|
+
* dialog is not anchored.
|
|
22
|
+
*
|
|
16
23
|
* `close` and `command` do not bubble, so they are heard in capture.
|
|
17
24
|
*/
|
|
18
25
|
class LbInsertDialog extends HTMLElement {
|
|
@@ -20,9 +27,19 @@ class LbInsertDialog extends HTMLElement {
|
|
|
20
27
|
opener = null;
|
|
21
28
|
constructor() {
|
|
22
29
|
super();
|
|
23
|
-
this.addEventListener("close", () =>
|
|
30
|
+
this.addEventListener("close", () => {
|
|
31
|
+
this.querySelector("form")?.reset();
|
|
32
|
+
this.opener?.removeAttribute("insert-dialog-opener");
|
|
33
|
+
this.removeAttribute("anchored");
|
|
34
|
+
}, true);
|
|
24
35
|
this.addEventListener("command", (e) => {
|
|
25
|
-
|
|
36
|
+
const { command, source } = e;
|
|
37
|
+
if (command !== "show-modal")
|
|
38
|
+
return;
|
|
39
|
+
this.opener?.removeAttribute("insert-dialog-opener");
|
|
40
|
+
this.opener = source ?? null;
|
|
41
|
+
this.opener?.setAttribute("insert-dialog-opener", "");
|
|
42
|
+
this.toggleAttribute("anchored", this.opener !== null);
|
|
26
43
|
}, true);
|
|
27
44
|
}
|
|
28
45
|
lbRequestDone({ request, items, error }) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lb-insert-dialog.browser.js","sourceRoot":"","sources":["../widgets/lb-insert-dialog.browser.ts"],"names":[],"mappings":"AAAA,2BAA2B;AAI3B
|
|
1
|
+
{"version":3,"file":"lb-insert-dialog.browser.js","sourceRoot":"","sources":["../widgets/lb-insert-dialog.browser.ts"],"names":[],"mappings":"AAAA,2BAA2B;AAI3B;;;;;;;;;;;;;;;;;;;;;;GAsBG;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;YACH,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC;YACpC,IAAI,CAAC,MAAM,EAAE,eAAe,CAAC,sBAAsB,CAAC,CAAC;YACrD,IAAI,CAAC,eAAe,CAAC,UAAU,CAAC,CAAC;QACnC,CAAC,EACD,IAAI,CACL,CAAC;QACF,IAAI,CAAC,gBAAgB,CACnB,SAAS,EACT,CAAC,CAAC,EAAE,EAAE;YACJ,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,CAG3B,CAAC;YACF,IAAI,OAAO,KAAK,YAAY;gBAAE,OAAO;YACrC,IAAI,CAAC,MAAM,EAAE,eAAe,CAAC,sBAAsB,CAAC,CAAC;YACrD,IAAI,CAAC,MAAM,GAAG,MAAM,IAAI,IAAI,CAAC;YAC7B,IAAI,CAAC,MAAM,EAAE,YAAY,CAAC,sBAAsB,EAAE,EAAE,CAAC,CAAC;YACtD,IAAI,CAAC,eAAe,CAAC,UAAU,EAAE,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC,CAAC;QACzD,CAAC,EACD,IAAI,CACL,CAAC;IACJ,CAAC;IAED,aAAa,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAkB;QACrD,IAAI,KAAK;YAAE,OAAO;QAClB,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,OAAO,CAAC,KAAK,CAAC,CAAC;QAC1D,MAAM,OAAO,GAAG,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;QACvC,IAAI,OAAO;YAAE,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,IAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QACrD,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,EAAE,KAAK,EAAE,CAAC;IACxC,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 type { HubLifecycle, HubRequestDone } from \"@loadbare/app/types\";\n\n/**\n * Three rules. Closing the dialog resets the form, the row the form creates\n * is chosen in the picker the dialog was opened from, and the dialog opens\n * beside the button that opened it.\n *\n * Save closes the dialog when the hub says the insert succeeded, so there\n * is a row to show for it, and a refusal leaves the entry open to 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 the hub calls\n * `lbRequestDone` 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 * The opener carries `insert-dialog-opener` while the dialog is open, and\n * the widget carries `anchored`; the page's stylesheet makes the one an\n * anchor and positions the other's dialog on it. Only one modal is open at a\n * time, so one anchor name serves every dialog. Opened with no source, the\n * dialog is not anchored.\n *\n * `close` and `command` do not bubble, so they are heard in capture.\n */\nclass LbInsertDialog extends HTMLElement implements HubLifecycle {\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 () => {\n this.querySelector(\"form\")?.reset();\n this.opener?.removeAttribute(\"insert-dialog-opener\");\n this.removeAttribute(\"anchored\");\n },\n true,\n );\n this.addEventListener(\n \"command\",\n (e) => {\n const { command, source } = e as Event & {\n command: string;\n source?: Element | null;\n };\n if (command !== \"show-modal\") return;\n this.opener?.removeAttribute(\"insert-dialog-opener\");\n this.opener = source ?? null;\n this.opener?.setAttribute(\"insert-dialog-opener\", \"\");\n this.toggleAttribute(\"anchored\", this.opener !== null);\n },\n true,\n );\n }\n\n lbRequestDone({ request, items, error }: HubRequestDone): void {\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 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 @@
|
|
|
1
|
+
{"version":3,"file":"lb-number.browser.d.ts","sourceRoot":"","sources":["../widgets/lb-number.browser.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
import { ATTR_COLUMN_VALUE } from "@loadbare/app/constants";
|
|
3
|
+
/**
|
|
4
|
+
* Shows the value that landed on it through `Intl.NumberFormat`, in the
|
|
5
|
+
* `<data>` its definition holds, with the value itself as the `<data>`'s
|
|
6
|
+
* `value`. Not a control: the hub stamps `lb-column-value` on the tag and
|
|
7
|
+
* never gathers it.
|
|
8
|
+
*
|
|
9
|
+
* The value reaches `format` as the string it arrived as, so a decimal keeps
|
|
10
|
+
* every digit a number would round away. A value that is no number shows as
|
|
11
|
+
* it arrived.
|
|
12
|
+
*/
|
|
13
|
+
class LbNumber extends HTMLElement {
|
|
14
|
+
static observedAttributes = [ATTR_COLUMN_VALUE];
|
|
15
|
+
formatter;
|
|
16
|
+
attributeChangedCallback(_name, _old, value) {
|
|
17
|
+
const data = this.querySelector("data");
|
|
18
|
+
if (!data)
|
|
19
|
+
return;
|
|
20
|
+
if (value === null) {
|
|
21
|
+
data.removeAttribute("value");
|
|
22
|
+
data.textContent = "";
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
data.value = value;
|
|
26
|
+
data.textContent = isNumber(value)
|
|
27
|
+
? this.format(data).format(value)
|
|
28
|
+
: value;
|
|
29
|
+
}
|
|
30
|
+
format(data) {
|
|
31
|
+
const { locale, ...options } = data.dataset;
|
|
32
|
+
return (this.formatter ??= new Intl.NumberFormat(locale, options));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Whether a stamp holds a number: anything `Number` reads but blank. */
|
|
36
|
+
function isNumber(value) {
|
|
37
|
+
return value.trim() !== "" && !Number.isNaN(Number(value));
|
|
38
|
+
}
|
|
39
|
+
customElements.define("lb-number", LbNumber);
|
|
40
|
+
//# sourceMappingURL=lb-number.browser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lb-number.browser.js","sourceRoot":"","sources":["../widgets/lb-number.browser.ts"],"names":[],"mappings":"AAAA,2BAA2B;AAE3B,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAE5D;;;;;;;;;GASG;AACH,MAAM,QAAS,SAAQ,WAAW;IAChC,MAAM,CAAC,kBAAkB,GAAG,CAAC,iBAAiB,CAAC,CAAC;IAExC,SAAS,CAAqB;IAEtC,wBAAwB,CACtB,KAAa,EACb,IAAmB,EACnB,KAAoB;QAEpB,MAAM,IAAI,GAAG,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;QACxC,IAAI,CAAC,IAAI;YAAE,OAAO;QAClB,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;YACnB,IAAI,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC;YAC9B,IAAI,CAAC,WAAW,GAAG,EAAE,CAAC;YACtB,OAAO;QACT,CAAC;QACD,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,WAAW,GAAG,QAAQ,CAAC,KAAK,CAAC;YAChC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,KAA0B,CAAC;YACtD,CAAC,CAAC,KAAK,CAAC;IACZ,CAAC;IAEO,MAAM,CAAC,IAAqB;QAClC,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC;QAC5C,OAAO,CAAC,IAAI,CAAC,SAAS,KAAK,IAAI,IAAI,CAAC,YAAY,CAC9C,MAAM,EACN,OAAmC,CACpC,CAAC,CAAC;IACL,CAAC;;AAGH,yEAAyE;AACzE,SAAS,QAAQ,CAAC,KAAa;IAC7B,OAAO,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;AAC7D,CAAC;AAED,cAAc,CAAC,MAAM,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC","sourcesContent":["/// <reference lib=\"dom\" />\n\nimport { ATTR_COLUMN_VALUE } from \"@loadbare/app/constants\";\n\n/**\n * Shows the value that landed on it through `Intl.NumberFormat`, in the\n * `<data>` its definition holds, with the value itself as the `<data>`'s\n * `value`. Not a control: the hub stamps `lb-column-value` on the tag and\n * never gathers it.\n *\n * The value reaches `format` as the string it arrived as, so a decimal keeps\n * every digit a number would round away. A value that is no number shows as\n * it arrived.\n */\nclass LbNumber extends HTMLElement {\n static observedAttributes = [ATTR_COLUMN_VALUE];\n\n private formatter?: Intl.NumberFormat;\n\n attributeChangedCallback(\n _name: string,\n _old: string | null,\n value: string | null,\n ): void {\n const data = this.querySelector(\"data\");\n if (!data) return;\n if (value === null) {\n data.removeAttribute(\"value\");\n data.textContent = \"\";\n return;\n }\n data.value = value;\n data.textContent = isNumber(value)\n ? this.format(data).format(value as unknown as number)\n : value;\n }\n\n private format(data: HTMLDataElement): Intl.NumberFormat {\n const { locale, ...options } = data.dataset;\n return (this.formatter ??= new Intl.NumberFormat(\n locale,\n options as Intl.NumberFormatOptions,\n ));\n }\n}\n\n/** Whether a stamp holds a number: anything `Number` reads but blank. */\nfunction isNumber(value: string): boolean {\n return value.trim() !== \"\" && !Number.isNaN(Number(value));\n}\n\ncustomElements.define(\"lb-number\", LbNumber);\n"]}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
A number, as the browser's Intl.NumberFormat shows it. Each parameter is
|
|
3
|
+
one of Intl's own options, in kebab-case, carried to the script on the
|
|
4
|
+
<data> as a data-* attribute; the dataset reads them back in camelCase, as
|
|
5
|
+
Intl names them. A parameter not given drops its attribute, and Intl's
|
|
6
|
+
default stands.
|
|
7
|
+
-->
|
|
8
|
+
<data
|
|
9
|
+
data-locale="{{locale}}"
|
|
10
|
+
data-style="{{style}}"
|
|
11
|
+
data-currency="{{currency}}"
|
|
12
|
+
data-currency-display="{{currency-display}}"
|
|
13
|
+
data-currency-sign="{{currency-sign}}"
|
|
14
|
+
data-unit="{{unit}}"
|
|
15
|
+
data-unit-display="{{unit-display}}"
|
|
16
|
+
data-notation="{{notation}}"
|
|
17
|
+
data-compact-display="{{compact-display}}"
|
|
18
|
+
data-sign-display="{{sign-display}}"
|
|
19
|
+
data-minimum-integer-digits="{{minimum-integer-digits}}"
|
|
20
|
+
data-minimum-fraction-digits="{{minimum-fraction-digits}}"
|
|
21
|
+
data-maximum-fraction-digits="{{maximum-fraction-digits}}"
|
|
22
|
+
data-minimum-significant-digits="{{minimum-significant-digits}}"
|
|
23
|
+
data-maximum-significant-digits="{{maximum-significant-digits}}"
|
|
24
|
+
></data>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lb-options.browser.d.ts","sourceRoot":"","sources":["../widgets/lb-options.browser.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,qBAAa,SAAU,SAAQ,cAAe,YAAW,YAAY;IACnE,SAAS,CAAC,KAAK,SAAY;IAE3B;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAuB;IAErC,IAAI,KAAK,IAAI,MAAM,CAElB;IAED,IAAI,KAAK,CAAC,KAAK,EAAE,MAAM,EAGtB;IAED,iBAAiB,IAAI,IAAI;IAKzB,SAAS,CAAC,OAAO,IAAI,IAAI;IAKzB,0EAA0E;IAC1E,YAAY,IAAI,IAAI;
|
|
1
|
+
{"version":3,"file":"lb-options.browser.d.ts","sourceRoot":"","sources":["../widgets/lb-options.browser.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,qBAAa,SAAU,SAAQ,cAAe,YAAW,YAAY;IACnE,SAAS,CAAC,KAAK,SAAY;IAE3B;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAuB;IAErC,IAAI,KAAK,IAAI,MAAM,CAElB;IAED,IAAI,KAAK,CAAC,KAAK,EAAE,MAAM,EAGtB;IAED,iBAAiB,IAAI,IAAI;IAKzB,SAAS,CAAC,OAAO,IAAI,IAAI;IAKzB,0EAA0E;IAC1E,YAAY,IAAI,IAAI;CAsBrB"}
|
|
@@ -59,7 +59,9 @@ export class LbOptions extends WrappedControl {
|
|
|
59
59
|
option.setAttribute("value", key);
|
|
60
60
|
}
|
|
61
61
|
for (const group of this.querySelectorAll(`optgroup${LIVE_GROUP}`)) {
|
|
62
|
-
const
|
|
62
|
+
const heading = group.querySelector(":scope > option[data-group-label]");
|
|
63
|
+
const label = heading?.textContent?.trim() ||
|
|
64
|
+
(group.getAttribute(ATTR_GROUP_VALUE) ?? "");
|
|
63
65
|
if (group.label !== label)
|
|
64
66
|
group.label = label;
|
|
65
67
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lb-options.browser.js","sourceRoot":"","sources":["../widgets/lb-options.browser.ts"],"names":[],"mappings":"AAAA,2BAA2B;AAE3B,OAAO,EACL,gBAAgB,EAChB,cAAc,EACd,UAAU,EACV,QAAQ,GACT,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,MAAM,OAAO,SAAU,SAAQ,cAAc;IACjC,KAAK,GAAG,QAAQ,CAAC;IAE3B;;;OAGG;IACK,MAAM,GAAkB,IAAI,CAAC;IAErC,IAAI,KAAK;QACP,OAAO,IAAI,CAAC,MAAM,IAAI,KAAK,CAAC,KAAK,CAAC;IACpC,CAAC;IAED,IAAI,KAAK,CAAC,KAAa;QACrB,IAAI,CAAC,MAAM,GAAG,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAC3E,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC;IACtB,CAAC;IAED,iBAAiB;QACf,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACnB,KAAK,CAAC,iBAAiB,EAAE,CAAC;IAC5B,CAAC;IAES,OAAO;QACf,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,OAAO,EAAE,KAAK,IAAI,IAAI,CAAC;QAC1C,KAAK,CAAC,OAAO,EAAE,CAAC;IAClB,CAAC;IAED,0EAA0E;IAC1E,YAAY;QACV,uEAAuE;QACvE,0BAA0B;QAC1B,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,gBAAgB,CAAC,SAAS,QAAQ,EAAE,CAAC,EAAE,CAAC;YAChE,MAAM,GAAG,GAAG,MAAM,CAAC,YAAY,CAAC,cAAc,CAAE,CAAC;YACjD,IAAI,MAAM,CAAC,YAAY,CAAC,OAAO,CAAC,KAAK,GAAG;
|
|
1
|
+
{"version":3,"file":"lb-options.browser.js","sourceRoot":"","sources":["../widgets/lb-options.browser.ts"],"names":[],"mappings":"AAAA,2BAA2B;AAE3B,OAAO,EACL,gBAAgB,EAChB,cAAc,EACd,UAAU,EACV,QAAQ,GACT,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,MAAM,OAAO,SAAU,SAAQ,cAAc;IACjC,KAAK,GAAG,QAAQ,CAAC;IAE3B;;;OAGG;IACK,MAAM,GAAkB,IAAI,CAAC;IAErC,IAAI,KAAK;QACP,OAAO,IAAI,CAAC,MAAM,IAAI,KAAK,CAAC,KAAK,CAAC;IACpC,CAAC;IAED,IAAI,KAAK,CAAC,KAAa;QACrB,IAAI,CAAC,MAAM,GAAG,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAC3E,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC;IACtB,CAAC;IAED,iBAAiB;QACf,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACnB,KAAK,CAAC,iBAAiB,EAAE,CAAC;IAC5B,CAAC;IAES,OAAO;QACf,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,OAAO,EAAE,KAAK,IAAI,IAAI,CAAC;QAC1C,KAAK,CAAC,OAAO,EAAE,CAAC;IAClB,CAAC;IAED,0EAA0E;IAC1E,YAAY;QACV,uEAAuE;QACvE,0BAA0B;QAC1B,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,gBAAgB,CAAC,SAAS,QAAQ,EAAE,CAAC,EAAE,CAAC;YAChE,MAAM,GAAG,GAAG,MAAM,CAAC,YAAY,CAAC,cAAc,CAAE,CAAC;YACjD,IAAI,MAAM,CAAC,YAAY,CAAC,OAAO,CAAC,KAAK,GAAG;gBACtC,MAAM,CAAC,YAAY,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;QACtC,CAAC;QACD,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,gBAAgB,CACvC,WAAW,UAAU,EAAE,CACxB,EAAE,CAAC;YACF,MAAM,OAAO,GAAG,KAAK,CAAC,aAAa,CAAC,mCAAmC,CAAC,CAAC;YACzE,MAAM,KAAK,GACT,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE;gBAC5B,CAAC,KAAK,CAAC,YAAY,CAAC,gBAAgB,CAAC,IAAI,EAAE,CAAC,CAAC;YAC/C,IAAI,KAAK,CAAC,KAAK,KAAK,KAAK;gBAAE,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC;QACjD,CAAC;QACD,iEAAiE;QACjE,MAAM,MAAM,GAAG,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;QAC5C,IAAI,MAAM,IAAI,IAAI,CAAC,MAAM,KAAK,IAAI;YAAE,MAAM,CAAC,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC;QAC/D,IAAI,CAAC,SAAS,EAAE,CAAC;IACnB,CAAC;CACF;AAED,cAAc,CAAC,MAAM,CAAC,YAAY,EAAE,SAAS,CAAC,CAAC","sourcesContent":["/// <reference lib=\"dom\" />\n\nimport {\n ATTR_GROUP_VALUE,\n ATTR_KEY_VALUE,\n LIVE_GROUP,\n LIVE_ROW,\n} from \"@loadbare/app/constants\";\nimport type { HubLifecycle } from \"@loadbare/app/types\";\nimport { WrappedControl } from \"./control.js\";\n\n/**\n * A `<select>` whose choices are the rows of the query it names.\n *\n * The hub places the options by the query's order, and a group template\n * holding an `<optgroup>` puts them in groups. What landing does not give an\n * `<optgroup>` is its label, so the widget sets it from the `lb-group-value`\n * the hub stamps on it.\n *\n * The identity of a row is the value of its option, so the `lb-key-value` the\n * hub stamps on each option is copied to its `value`. The key column is the\n * one the server declared for the query; the page never names it.\n *\n * It is two things at once, and the hub keeps them apart: its own `lb-query`\n * names the rows it offers, and its `lb-column` is a column of the row around\n * it, which is what it holds and what it sends. As a control it is\n * form-associated, with a `value` and a `change` of its own, so the hub lands\n * that column on `value` and commits `lb-request` on `change`.\n *\n * The value is held here as well as on the select. A `<select>` does not hold\n * a value it has no option for: it selects nothing and forgets it. The value\n * and the choices are two answers that land in either order, so the held\n * value is put on the select again whenever rows land.\n *\n * Exported because `lb-picker` is this widget with its row template supplied\n * by the definition instead of by the page.\n */\n\nexport class LbOptions extends WrappedControl implements HubLifecycle {\n protected inner = \"select\";\n\n /**\n * The value last set or chosen, which the options may not include yet.\n * Null when nothing chose one, so the select's own default stands.\n */\n private chosen: string | null = null;\n\n get value(): string {\n return this.chosen ?? super.value;\n }\n\n set value(value: string) {\n this.chosen = value === null || value === undefined ? null : String(value);\n super.value = value;\n }\n\n formResetCallback(): void {\n this.chosen = null;\n super.formResetCallback();\n }\n\n protected changed(): void {\n this.chosen = this.control?.value ?? null;\n super.changed();\n }\n\n /** Everything derived from the rows, once the whole answer has landed. */\n lbRowsLanded(): void {\n // Written only when they differ: a refresh that changed nothing leaves\n // every option as it was.\n for (const option of this.querySelectorAll(`option${LIVE_ROW}`)) {\n const key = option.getAttribute(ATTR_KEY_VALUE)!;\n if (option.getAttribute(\"value\") !== key)\n option.setAttribute(\"value\", key);\n }\n for (const group of this.querySelectorAll<HTMLOptGroupElement>(\n `optgroup${LIVE_GROUP}`,\n )) {\n const heading = group.querySelector(\":scope > option[data-group-label]\");\n const label =\n heading?.textContent?.trim() ||\n (group.getAttribute(ATTR_GROUP_VALUE) ?? \"\");\n if (group.label !== label) group.label = label;\n }\n // After the values above, since the select matches against them.\n const select = this.querySelector(\"select\");\n if (select && this.chosen !== null) select.value = this.chosen;\n this.formValue();\n }\n}\n\ncustomElements.define(\"lb-options\", LbOptions);\n"]}
|
package/dist/lb-options.html
CHANGED
package/dist/lb-table.browser.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/// <reference lib="dom" />
|
|
2
|
-
import { ATTR_COLUMN, ATTR_ROW_REQUESTED, REQUEST_ROW_INSERT, REQUESTED_CREATED, REQUESTED_MOVED, } from "@loadbare/app/constants";
|
|
2
|
+
import { ATTR_COLUMN, ATTR_FILTER, ATTR_ROW_REQUESTED, CHOICES_QUERY, LB_EVENT_NAME, LIVE_ROW, REQUEST_ROW_INSERT, REQUEST_ROW_UPDATE, REQUESTED_CREATED, REQUESTED_MOVED, } from "@loadbare/app/constants";
|
|
3
3
|
/**
|
|
4
4
|
* A table that owns its own scaffolding.
|
|
5
5
|
*
|
|
@@ -12,13 +12,30 @@ import { ATTR_COLUMN, ATTR_ROW_REQUESTED, REQUEST_ROW_INSERT, REQUESTED_CREATED,
|
|
|
12
12
|
* expansion unwraps it into the `lb-exp-template` destination beside the slot.
|
|
13
13
|
*
|
|
14
14
|
* Placement and groups are the hub's, by the query's order and the page's
|
|
15
|
-
* group templates. What this widget adds at run time is the ghost row,
|
|
16
|
-
* scrolling to the row the page's own request created or moved
|
|
15
|
+
* group templates. What this widget adds at run time is the ghost row,
|
|
16
|
+
* scrolling to the row the page's own request created or moved, and filtering
|
|
17
|
+
* by selected text.
|
|
17
18
|
*
|
|
18
19
|
* The `foot` destination needs no code here. A `<tfoot>` is a place on the
|
|
19
20
|
* page, not a row the hub delivers, so what lands in it is an aggregate or a
|
|
20
21
|
* `row` query of its own, named on its `<tr>`.
|
|
21
22
|
*/
|
|
23
|
+
/**
|
|
24
|
+
* Text selected in a cell filters the table by that cell's column. When a
|
|
25
|
+
* selection finishes inside one element of a live row whose `lb-column` the
|
|
26
|
+
* table's `lb-filter` names, the table makes the choice of that name, and the
|
|
27
|
+
* hub hides the rows it does not pass. Where the selection lies in the
|
|
28
|
+
* element's text says how:
|
|
29
|
+
*
|
|
30
|
+
* the whole text `=` equals
|
|
31
|
+
* from its start `^=` begins with
|
|
32
|
+
* up to its end `$=` ends with
|
|
33
|
+
* anywhere else `*=` contains
|
|
34
|
+
*
|
|
35
|
+
* The choice replaces whatever that column held, so a control naming the
|
|
36
|
+
* same column of `lb-choices` shows it and clears it. The selection is
|
|
37
|
+
* removed once it is used.
|
|
38
|
+
*/
|
|
22
39
|
/**
|
|
23
40
|
* The ghost row's `<tbody>`, a blank row for entering a new one, between the
|
|
24
41
|
* rows and the footer. The page writes it as the `ghost` template, so it
|
|
@@ -36,6 +53,10 @@ const GHOST = "data-ghost";
|
|
|
36
53
|
/** Ghost row forms need ids for the `form` attribute; any unique one will do. */
|
|
37
54
|
let ghostForms = 0;
|
|
38
55
|
class LbTable extends HTMLElement {
|
|
56
|
+
constructor() {
|
|
57
|
+
super();
|
|
58
|
+
this.addEventListener("mouseup", () => this.filterBySelection());
|
|
59
|
+
}
|
|
39
60
|
lbAttached() {
|
|
40
61
|
const ghost = this.querySelector(`tbody[${GHOST}]`);
|
|
41
62
|
if (!ghost?.firstElementChild)
|
|
@@ -67,6 +88,53 @@ class LbTable extends HTMLElement {
|
|
|
67
88
|
if (row)
|
|
68
89
|
firstControl(row)?.focus();
|
|
69
90
|
}
|
|
91
|
+
/** The columns of this table's rows a choice may filter. */
|
|
92
|
+
filtered() {
|
|
93
|
+
return (this.getAttribute(ATTR_FILTER) ?? "")
|
|
94
|
+
.split(",")
|
|
95
|
+
.map((column) => column.trim())
|
|
96
|
+
.filter(Boolean);
|
|
97
|
+
}
|
|
98
|
+
/** A finished selection in one filtered cell, made a choice. */
|
|
99
|
+
filterBySelection() {
|
|
100
|
+
const selection = getSelection();
|
|
101
|
+
if (!selection || selection.isCollapsed || selection.rangeCount === 0) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const range = selection.getRangeAt(0);
|
|
105
|
+
const start = range.startContainer;
|
|
106
|
+
const el = (start instanceof Element ? start : start.parentElement)?.closest(`[${ATTR_COLUMN}]`);
|
|
107
|
+
if (!el || !el.contains(range.endContainer))
|
|
108
|
+
return;
|
|
109
|
+
const row = el.closest(LIVE_ROW);
|
|
110
|
+
if (!row || row.closest("lb-table") !== this)
|
|
111
|
+
return;
|
|
112
|
+
const column = el.getAttribute(ATTR_COLUMN);
|
|
113
|
+
if (!this.filtered().includes(column))
|
|
114
|
+
return;
|
|
115
|
+
const term = termFor(selection.toString().trim(), (el.textContent ?? "").trim());
|
|
116
|
+
if (term === null)
|
|
117
|
+
return;
|
|
118
|
+
selection.removeAllRanges();
|
|
119
|
+
const detail = {
|
|
120
|
+
name: REQUEST_ROW_UPDATE,
|
|
121
|
+
query: CHOICES_QUERY,
|
|
122
|
+
values: { [column]: term },
|
|
123
|
+
};
|
|
124
|
+
this.dispatchEvent(new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail }));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/** A filter's term for text selected from a cell's whole text. */
|
|
128
|
+
function termFor(selected, whole) {
|
|
129
|
+
if (selected === "")
|
|
130
|
+
return null;
|
|
131
|
+
if (selected === whole)
|
|
132
|
+
return `=${selected}`;
|
|
133
|
+
if (whole.startsWith(selected))
|
|
134
|
+
return `^=${selected}`;
|
|
135
|
+
if (whole.endsWith(selected))
|
|
136
|
+
return `$=${selected}`;
|
|
137
|
+
return `*=${selected}`;
|
|
70
138
|
}
|
|
71
139
|
/**
|
|
72
140
|
* The first control in a row the user can type into or choose from. A
|
|
@@ -1 +1 @@
|
|
|
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,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,UAAU;QACR,MAAM,KAAK,GAAG,IAAI,CAAC,aAAa,CAAC,SAAS,KAAK,GAAG,CAAC,CAAC;QACpD,IAAI,CAAC,KAAK,EAAE,iBAAiB;YAAE,OAAO;QAEtC,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;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,OAAO,EAAE,KAAK,EAAkB,EAAE,MAAe;QAC/D,IAAI,KAAK,IAAI,OAAO,CAAC,IAAI,KAAK,kBAAkB;YAAE,OAAO;QACzD,MAAM,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC,IAAI,KAAK,GAAG,CAAC,CAAC;QACzC,IAAI,GAAG;YAAE,YAAY,CAAC,GAAG,CAAC,EAAE,KAAK,EAAE,CAAC;IACtC,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 REQUEST_ROW_INSERT,\n REQUESTED_CREATED,\n REQUESTED_MOVED,\n} from \"@loadbare/app/constants\";\nimport type { HubLifecycle, HubRequestDone } 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 HubLifecycle {\n lbAttached(): void {\n const ghost = this.querySelector(`tbody[${GHOST}]`);\n if (!ghost?.firstElementChild) return;\n\n const form = document.createElement(\"form\");\n form.id = `ghost-row-${++ghostForms}`;\n form.hidden = true;\n this.append(form);\n for (const el of ghost.querySelectorAll(`[${ATTR_COLUMN}], button`)) {\n el.setAttribute(\"form\", form.id);\n }\n }\n\n /** The row this page's request created or moved, brought into view. */\n lbRowsLanded(): void {\n this.querySelector(\n `[${ATTR_ROW_REQUESTED}=\"${REQUESTED_CREATED}\"], ` +\n `[${ATTR_ROW_REQUESTED}=\"${REQUESTED_MOVED}\"]`,\n )?.scrollIntoView({ block: \"nearest\" });\n }\n\n /**\n * A ghost row is for entering the next row, so once one is saved the\n * cursor goes back to the start of it. The hub calls this after the\n * response items have landed, so nothing it places afterwards takes the\n * focus away again. A refused insert leaves the cursor where the user left\n * it.\n */\n lbRequestDone({ request, error }: HubRequestDone, origin: Element): void {\n if (error || request.name !== REQUEST_ROW_INSERT) return;\n const row = origin.closest(`[${GHOST}]`);\n if (row) firstControl(row)?.focus();\n }\n}\n\n/**\n * The first control in a row the user can type into or choose from. A\n * control a widget wraps is found inside it, since the wrapper takes no focus.\n */\nfunction firstControl(row: Element): HTMLElement | undefined {\n return [\n ...row.querySelectorAll<HTMLElement>(\n 'input:not([type=\"hidden\"]), select, textarea',\n ),\n ].find((el) => !el.matches(\":disabled\") && el.checkVisibility?.() !== false);\n}\n\ncustomElements.define(\"lb-table\", LbTable);\n"]}
|
|
1
|
+
{"version":3,"file":"lb-table.browser.js","sourceRoot":"","sources":["../widgets/lb-table.browser.ts"],"names":[],"mappings":"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","sourcesContent":["/// <reference lib=\"dom\" />\n\nimport {\n ATTR_COLUMN,\n ATTR_FILTER,\n ATTR_ROW_REQUESTED,\n CHOICES_QUERY,\n LB_EVENT_NAME,\n LIVE_ROW,\n REQUEST_ROW_INSERT,\n REQUEST_ROW_UPDATE,\n REQUESTED_CREATED,\n REQUESTED_MOVED,\n} from \"@loadbare/app/constants\";\nimport type {\n HubLifecycle,\n HubRequest,\n HubRequestDone,\n} from \"@loadbare/app/types\";\n\n/**\n * A table that owns its own scaffolding.\n *\n * A plain `<table lb-query>` with a row template inside it needs no widget at\n * all. This one supplies the table, and the developer writes what only the\n * page knows: the heading row and the row template. That division is forced\n * rather than chosen — a `<thead>` written inside a custom element is not\n * inside a table, so the parser drops the tag and keeps the text. A\n * `<template>` survives anywhere, so the heading row arrives wrapped and\n * expansion unwraps it into the `lb-exp-template` destination beside the slot.\n *\n * Placement and groups are the hub's, by the query's order and the page's\n * group templates. What this widget adds at run time is the ghost row,\n * scrolling to the row the page's own request created or moved, and filtering\n * by selected text.\n *\n * The `foot` destination needs no code here. A `<tfoot>` is a place on the\n * page, not a row the hub delivers, so what lands in it is an aggregate or a\n * `row` query of its own, named on its `<tr>`.\n */\n\n/**\n * Text selected in a cell filters the table by that cell's column. When a\n * selection finishes inside one element of a live row whose `lb-column` the\n * table's `lb-filter` names, the table makes the choice of that name, and the\n * hub hides the rows it does not pass. Where the selection lies in the\n * element's text says how:\n *\n * the whole text `=` equals\n * from its start `^=` begins with\n * up to its end `$=` ends with\n * anywhere else `*=` contains\n *\n * The choice replaces whatever that column held, so a control naming the\n * same column of `lb-choices` shows it and clears it. The selection is\n * removed once it is used.\n */\n\n/**\n * The ghost row's `<tbody>`, a blank row for entering a new one, between the\n * rows and the footer. The page writes it as the `ghost` template, so it\n * ships in the document like the heading and the footer, and the hub lands\n * on whatever in it names a query. It is not a live row: the hub never\n * reconciles it and never counts it.\n *\n * A `<form>` cannot wrap a `<tr>`, so the widget makes one outside the table\n * and joins the row's controls and buttons to it with the HTML `form`\n * attribute. Sending is the page's: a button in the row carrying\n * `lb-row-insert` submits that form, and the hub gathers what the form owns.\n * The hub resets the form once the insert succeeds.\n */\nconst GHOST = \"data-ghost\";\n\n/** Ghost row forms need ids for the `form` attribute; any unique one will do. */\nlet ghostForms = 0;\n\nclass LbTable extends HTMLElement implements HubLifecycle {\n constructor() {\n super();\n this.addEventListener(\"mouseup\", () => this.filterBySelection());\n }\n\n lbAttached(): void {\n const ghost = this.querySelector(`tbody[${GHOST}]`);\n if (!ghost?.firstElementChild) return;\n\n const form = document.createElement(\"form\");\n form.id = `ghost-row-${++ghostForms}`;\n form.hidden = true;\n this.append(form);\n for (const el of ghost.querySelectorAll(`[${ATTR_COLUMN}], button`)) {\n el.setAttribute(\"form\", form.id);\n }\n }\n\n /** The row this page's request created or moved, brought into view. */\n lbRowsLanded(): void {\n this.querySelector(\n `[${ATTR_ROW_REQUESTED}=\"${REQUESTED_CREATED}\"], ` +\n `[${ATTR_ROW_REQUESTED}=\"${REQUESTED_MOVED}\"]`,\n )?.scrollIntoView({ block: \"nearest\" });\n }\n\n /**\n * A ghost row is for entering the next row, so once one is saved the\n * cursor goes back to the start of it. The hub calls this after the\n * response items have landed, so nothing it places afterwards takes the\n * focus away again. A refused insert leaves the cursor where the user left\n * it.\n */\n lbRequestDone({ request, error }: HubRequestDone, origin: Element): void {\n if (error || request.name !== REQUEST_ROW_INSERT) return;\n const row = origin.closest(`[${GHOST}]`);\n if (row) firstControl(row)?.focus();\n }\n\n /** The columns of this table's rows a choice may filter. */\n private filtered(): string[] {\n return (this.getAttribute(ATTR_FILTER) ?? \"\")\n .split(\",\")\n .map((column) => column.trim())\n .filter(Boolean);\n }\n\n /** A finished selection in one filtered cell, made a choice. */\n private filterBySelection(): void {\n const selection = getSelection();\n if (!selection || selection.isCollapsed || selection.rangeCount === 0) {\n return;\n }\n const range = selection.getRangeAt(0);\n const start = range.startContainer;\n const el = (\n start instanceof Element ? start : start.parentElement\n )?.closest(`[${ATTR_COLUMN}]`);\n if (!el || !el.contains(range.endContainer)) return;\n const row = el.closest(LIVE_ROW);\n if (!row || row.closest(\"lb-table\") !== this) return;\n const column = el.getAttribute(ATTR_COLUMN)!;\n if (!this.filtered().includes(column)) return;\n\n const term = termFor(\n selection.toString().trim(),\n (el.textContent ?? \"\").trim(),\n );\n if (term === null) return;\n selection.removeAllRanges();\n const detail: HubRequest = {\n name: REQUEST_ROW_UPDATE,\n query: CHOICES_QUERY,\n values: { [column]: term },\n };\n this.dispatchEvent(\n new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail }),\n );\n }\n}\n\n/** A filter's term for text selected from a cell's whole text. */\nfunction termFor(selected: string, whole: string): string | null {\n if (selected === \"\") return null;\n if (selected === whole) return `=${selected}`;\n if (whole.startsWith(selected)) return `^=${selected}`;\n if (whole.endsWith(selected)) return `$=${selected}`;\n return `*=${selected}`;\n}\n\n/**\n * The first control in a row the user can type into or choose from. A\n * control a widget wraps is found inside it, since the wrapper takes no focus.\n */\nfunction firstControl(row: Element): HTMLElement | undefined {\n return [\n ...row.querySelectorAll<HTMLElement>(\n 'input:not([type=\"hidden\"]), select, textarea',\n ),\n ].find((el) => !el.matches(\":disabled\") && el.checkVisibility?.() !== false);\n}\n\ncustomElements.define(\"lb-table\", LbTable);\n"]}
|
package/docs/lb-date.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# `lb-date`
|
|
2
|
+
|
|
3
|
+
> Status: a barebones starter. Iterative use will drive improvements.
|
|
4
|
+
|
|
5
|
+
A date or a time shown by the browser's `Intl.DateTimeFormat`. Not a
|
|
6
|
+
control: write `lb-column` on the tag, and the hub lands the column on it
|
|
7
|
+
and never gathers it.
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<lb-date lb-column="due" exp-date-style="medium"></lb-date>
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Expansion Attributes
|
|
14
|
+
|
|
15
|
+
Each parameter is one of `Intl.DateTimeFormat`'s options, in kebab-case. A
|
|
16
|
+
parameter not given leaves Intl's default.
|
|
17
|
+
|
|
18
|
+
| Parameter | `Intl.DateTimeFormat` option |
|
|
19
|
+
| -------------------- | ------------------------------------- |
|
|
20
|
+
| `exp-locale` | the locale, the browser's when absent |
|
|
21
|
+
| `exp-date-style` | `dateStyle` |
|
|
22
|
+
| `exp-time-style` | `timeStyle` |
|
|
23
|
+
| `exp-time-zone` | `timeZone` |
|
|
24
|
+
| `exp-time-zone-name` | `timeZoneName` |
|
|
25
|
+
| `exp-hour-cycle` | `hourCycle` |
|
|
26
|
+
| `exp-weekday` | `weekday` |
|
|
27
|
+
| `exp-era` | `era` |
|
|
28
|
+
| `exp-year` | `year` |
|
|
29
|
+
| `exp-month` | `month` |
|
|
30
|
+
| `exp-day` | `day` |
|
|
31
|
+
| `exp-hour` | `hour` |
|
|
32
|
+
| `exp-minute` | `minute` |
|
|
33
|
+
| `exp-second` | `second` |
|
|
34
|
+
|
|
35
|
+
Intl refuses `exp-date-style` or `exp-time-style` beside any of
|
|
36
|
+
`exp-weekday` through `exp-second`.
|
|
37
|
+
|
|
38
|
+
## The value
|
|
39
|
+
|
|
40
|
+
The widget reads the value as JavaScript's `Date` does. A date alone, as
|
|
41
|
+
`YYYY-MM-DD`, shows as that day in every time zone. A value that is no
|
|
42
|
+
date shows as it arrived. Null shows nothing.
|
|
43
|
+
|
|
44
|
+
## Styling
|
|
45
|
+
|
|
46
|
+
The definition is a `<time>` whose text is the formatted date and whose
|
|
47
|
+
`datetime` is the value as it arrived. The hub stamps the tag with
|
|
48
|
+
`lb-column-value`, the value as it arrived.
|
package/docs/lb-insert-dialog.md
CHANGED
|
@@ -47,3 +47,27 @@ saves it; one in a ghost row or a new record holds it until Save.
|
|
|
47
47
|
|
|
48
48
|
A refused insert leaves the dialog open to correct. Cancel and Escape close
|
|
49
49
|
it, and closing resets the form.
|
|
50
|
+
|
|
51
|
+
## Opening beside the button
|
|
52
|
+
|
|
53
|
+
While the dialog is open, the button that opened it carries
|
|
54
|
+
`insert-dialog-opener` and the widget carries `anchored`, for a stylesheet to
|
|
55
|
+
place the dialog beside the button with CSS anchor positioning:
|
|
56
|
+
|
|
57
|
+
```css
|
|
58
|
+
[insert-dialog-opener] {
|
|
59
|
+
anchor-name: --insert-dialog-opener;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
lb-insert-dialog[anchored] .lb-insert-dialog {
|
|
63
|
+
position-anchor: --insert-dialog-opener;
|
|
64
|
+
inset: auto;
|
|
65
|
+
margin: 0;
|
|
66
|
+
position-area: block-end span-inline-end;
|
|
67
|
+
position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline;
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Only one modal is open at a time, so one anchor name serves every dialog.
|
|
72
|
+
Without such a rule, or opened with no button, the dialog stays where the
|
|
73
|
+
browser puts a modal.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# `lb-number`
|
|
2
|
+
|
|
3
|
+
> Status: a barebones starter. Iterative use will drive improvements.
|
|
4
|
+
|
|
5
|
+
A number shown by the browser's `Intl.NumberFormat`. Not a control: write
|
|
6
|
+
`lb-column` on the tag, and the hub lands the column on it and never gathers
|
|
7
|
+
it.
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<lb-number lb-column="balance" exp-style="currency" exp-currency="USD"></lb-number>
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Expansion Attributes
|
|
14
|
+
|
|
15
|
+
Each parameter is one of `Intl.NumberFormat`'s options, in kebab-case. A
|
|
16
|
+
parameter not given leaves Intl's default.
|
|
17
|
+
|
|
18
|
+
| Parameter | `Intl.NumberFormat` option |
|
|
19
|
+
| -------------------------------- | -------------------------------------- |
|
|
20
|
+
| `exp-locale` | the locale, the browser's when absent |
|
|
21
|
+
| `exp-style` | `style` |
|
|
22
|
+
| `exp-currency` | `currency` |
|
|
23
|
+
| `exp-currency-display` | `currencyDisplay` |
|
|
24
|
+
| `exp-currency-sign` | `currencySign` |
|
|
25
|
+
| `exp-unit` | `unit` |
|
|
26
|
+
| `exp-unit-display` | `unitDisplay` |
|
|
27
|
+
| `exp-notation` | `notation` |
|
|
28
|
+
| `exp-compact-display` | `compactDisplay` |
|
|
29
|
+
| `exp-sign-display` | `signDisplay` |
|
|
30
|
+
| `exp-minimum-integer-digits` | `minimumIntegerDigits` |
|
|
31
|
+
| `exp-minimum-fraction-digits` | `minimumFractionDigits` |
|
|
32
|
+
| `exp-maximum-fraction-digits` | `maximumFractionDigits` |
|
|
33
|
+
| `exp-minimum-significant-digits` | `minimumSignificantDigits` |
|
|
34
|
+
| `exp-maximum-significant-digits` | `maximumSignificantDigits` |
|
|
35
|
+
|
|
36
|
+
## The value
|
|
37
|
+
|
|
38
|
+
The widget formats the value as the string it arrived as, so a decimal the
|
|
39
|
+
query sends as a string keeps every digit. A value that is no number shows
|
|
40
|
+
as it arrived. Null shows nothing.
|
|
41
|
+
|
|
42
|
+
## Styling
|
|
43
|
+
|
|
44
|
+
The definition is a `<data>` whose text is the formatted number and whose
|
|
45
|
+
`value` is the number as it arrived. The hub stamps the tag with
|
|
46
|
+
`lb-column-value`, the number as it arrived, so a stylesheet selects on the
|
|
47
|
+
value itself:
|
|
48
|
+
|
|
49
|
+
```css
|
|
50
|
+
lb-number[lb-column-value^="-"] {
|
|
51
|
+
color: var(--negative);
|
|
52
|
+
}
|
|
53
|
+
```
|
package/docs/lb-options.md
CHANGED
|
@@ -36,6 +36,7 @@ improvements through iterative use.
|
|
|
36
36
|
| Parameter | Fills |
|
|
37
37
|
| ----------- | -------------------------- |
|
|
38
38
|
| `exp-label` | the visible `<label>` text |
|
|
39
|
+
| `exp-size` | the `<select>`'s `size`, an open list of that many rows rather than a drop-down |
|
|
39
40
|
|
|
40
41
|
## Attributes
|
|
41
42
|
|
|
@@ -57,6 +58,23 @@ An `<optgroup>` takes its label from an attribute, which landing does not
|
|
|
57
58
|
set, so the widget labels each from the `lb-group-value` the hub stamps on
|
|
58
59
|
it: the value the group breaks on.
|
|
59
60
|
|
|
61
|
+
That value is often a number kept for sorting, such as a category's display
|
|
62
|
+
order, and a number is no label. For a label from a column of the group's
|
|
63
|
+
first row, write a heading option marked `data-group-label` in the
|
|
64
|
+
`<optgroup>`, filled like any group heading:
|
|
65
|
+
|
|
66
|
+
```html
|
|
67
|
+
<template lb-group>
|
|
68
|
+
<optgroup>
|
|
69
|
+
<option data-group-label hidden disabled lb-column="category"></option>
|
|
70
|
+
<template><option lb-column="account_name"></option></template>
|
|
71
|
+
</optgroup>
|
|
72
|
+
</template>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The widget takes the heading option's text as the label. Mark it `hidden`
|
|
76
|
+
and `disabled` so it is never a choice.
|
|
77
|
+
|
|
60
78
|
## The control
|
|
61
79
|
|
|
62
80
|
`lb-options` is a form-associated custom element. Its `value` property gets
|
package/docs/lb-table.md
CHANGED
|
@@ -48,6 +48,8 @@ improvements through iterative use.
|
|
|
48
48
|
| ----------- | -------------------------------- | ---------------------------------- |
|
|
49
49
|
| `lb-query` | the element itself | the query whose rows fill the body |
|
|
50
50
|
| `lb-group` | a template holding the row template | a group, one per run of the order's leading term |
|
|
51
|
+
| `lb-order` | the element itself | the choice holding the user's order |
|
|
52
|
+
| `lb-filter` | the element itself | the columns the user's choices filter |
|
|
51
53
|
| `lb-column` | elements inside the row template | the column each element shows |
|
|
52
54
|
|
|
53
55
|
The server declares the query's key and its order.
|
|
@@ -60,6 +62,41 @@ so a group here is a heading row with its rows after it. For a `<tbody>` per
|
|
|
60
62
|
group, write a plain `<table lb-query>` whose group template is the
|
|
61
63
|
`<tbody>`.
|
|
62
64
|
|
|
65
|
+
`lb-order` and `lb-filter` work on `lb-table` as on any element carrying
|
|
66
|
+
`lb-query`: the user's order replaces the query's, and a row the user's
|
|
67
|
+
filter does not pass is hidden.
|
|
68
|
+
|
|
69
|
+
## Filtering by selection
|
|
70
|
+
|
|
71
|
+
Text selected in a cell filters the table by that cell's column, when the
|
|
72
|
+
cell's `lb-column` is one the table's `lb-filter` names. On mouse-up the
|
|
73
|
+
widget writes the filter to that column of `lb-choices`, replacing whatever
|
|
74
|
+
it held, and clears the selection. Where the selection lies in the cell's
|
|
75
|
+
text picks the operator:
|
|
76
|
+
|
|
77
|
+
| Selected | Filter |
|
|
78
|
+
| ---------------------- | ------ |
|
|
79
|
+
| the whole text | `=` |
|
|
80
|
+
| from its start | `^=` |
|
|
81
|
+
| up to its end | `$=` |
|
|
82
|
+
| anywhere else | `*=` |
|
|
83
|
+
|
|
84
|
+
A control naming the same column of `lb-choices` shows the filter and
|
|
85
|
+
clears it.
|
|
86
|
+
|
|
87
|
+
```html
|
|
88
|
+
<div lb-query="lb-choices">
|
|
89
|
+
<lb-input lb-column="payee" lb-request="lb-row-update" exp-label="Payee"></lb-input>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<lb-table lb-query="ledger" lb-filter="payee" exp-caption="Ledger">
|
|
93
|
+
...
|
|
94
|
+
</lb-table>
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
A selection reaching across two cells, or into a cell whose column the
|
|
98
|
+
table does not filter, is left alone.
|
|
99
|
+
|
|
63
100
|
## The ghost row
|
|
64
101
|
|
|
65
102
|
The `ghost` template is a blank row for entering a new one, in a `<tbody>` of
|
|
@@ -90,7 +127,7 @@ created, or moved by changing a column the order reads.
|
|
|
90
127
|
## The footer
|
|
91
128
|
|
|
92
129
|
The footer is an ordinary part of the page, not a row from the table's query.
|
|
93
|
-
An aggregate there covers every row. A figure that is not a sum of the rows,
|
|
130
|
+
An aggregate there covers every row showing. A figure that is not a sum of the rows,
|
|
94
131
|
such as a budget, is an element with its own `lb-query`, a query of kind
|
|
95
132
|
`row` declared on the server like any other.
|
|
96
133
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loadbare/widgets",
|
|
3
|
-
"description": "The basic widget set for @loadbare/app: input, select, options, picker, table, unknown-page, confirm, form, insert-dialog, master, checkbox",
|
|
4
|
-
"version": "0.
|
|
3
|
+
"description": "The basic widget set for @loadbare/app: input, select, options, picker, table, unknown-page, confirm, form, insert-dialog, master, checkbox, number, date",
|
|
4
|
+
"version": "0.9.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
7
7
|
"dist",
|
|
@@ -49,10 +49,10 @@
|
|
|
49
49
|
"node": ">=22"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
|
-
"@loadbare/app": ">=0.
|
|
52
|
+
"@loadbare/app": ">=0.17.0 <1.0.0"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
|
-
"@loadbare/app": ">=0.
|
|
55
|
+
"@loadbare/app": ">=0.17.0 <1.0.0",
|
|
56
56
|
"@types/jsdom": "^21.1.7",
|
|
57
57
|
"@types/node": "^22.0.0",
|
|
58
58
|
"jsdom": "^25.0.0",
|