@loadbare/widgets 0.9.0 → 0.10.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 +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-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-table.browser.js +134 -3
- package/dist/lb-table.browser.js.map +1 -1
- package/dist/lb-table.css +25 -0
- package/docs/lb-date.md +48 -0
- package/docs/lb-number.md +53 -0
- package/docs/lb-table.md +34 -2
- package/docs/temp-feature-request-lb-table.md +130 -0
- 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>
|
|
@@ -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>
|
package/dist/lb-table.browser.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/// <reference lib="dom" />
|
|
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";
|
|
2
|
+
import { ATTR_COLUMN, ATTR_FILTER, ATTR_GROUP, ATTR_ORDER, ATTR_QUERY, ATTR_QUERY_ORDER, ATTR_QUERY_UNSORTED_COUNT, ATTR_ROW_REQUESTED, ATTR_SHOW, CHOICES_QUERY, COMMAND_SORT, LB_EVENT_NAME, LIVE_ROW, ORDER_DESCENDING, 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
|
*
|
|
@@ -13,8 +13,8 @@ import { ATTR_COLUMN, ATTR_FILTER, ATTR_ROW_REQUESTED, CHOICES_QUERY, LB_EVENT_N
|
|
|
13
13
|
*
|
|
14
14
|
* Placement and groups are the hub's, by the query's order and the page's
|
|
15
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,
|
|
17
|
-
* by selected text.
|
|
16
|
+
* scrolling to the row the page's own request created or moved, filtering
|
|
17
|
+
* by selected text, and the order shown on the heading.
|
|
18
18
|
*
|
|
19
19
|
* The `foot` destination needs no code here. A `<tfoot>` is a place on the
|
|
20
20
|
* page, not a row the hub delivers, so what lands in it is an aggregate or a
|
|
@@ -36,6 +36,24 @@ import { ATTR_COLUMN, ATTR_FILTER, ATTR_ROW_REQUESTED, CHOICES_QUERY, LB_EVENT_N
|
|
|
36
36
|
* same column of `lb-choices` shows it and clears it. The selection is
|
|
37
37
|
* removed once it is used.
|
|
38
38
|
*/
|
|
39
|
+
/**
|
|
40
|
+
* The heading of the column the rows are sorted by carries a triangle for the
|
|
41
|
+
* direction, and `aria-sort`. A heading's column is the one `lb-column` in
|
|
42
|
+
* the cell at the same position in the row template, a branch `lb-show`
|
|
43
|
+
* holds in a template included; a cell with none, or with several, gives its
|
|
44
|
+
* heading no column. Only the order's leading term is
|
|
45
|
+
* shown. A grouped table shows none of this.
|
|
46
|
+
*
|
|
47
|
+
* While rows stand out of order the triangle carries `data-unsorted`, and a
|
|
48
|
+
* click on its heading places them again with `--lb-sort`. Otherwise, on a
|
|
49
|
+
* table with `lb-order`, a click on the sorted heading reverses the order and
|
|
50
|
+
* a click on another heading sorts by its column, ascending: either way the
|
|
51
|
+
* table writes the user's order to the column of `lb-choices` that `lb-order`
|
|
52
|
+
* names. A table with no `lb-order` has nowhere to write one.
|
|
53
|
+
*/
|
|
54
|
+
const SORT = "data-sort";
|
|
55
|
+
const UNSORTED = "data-unsorted";
|
|
56
|
+
const SORTABLE = "data-sortable";
|
|
39
57
|
/**
|
|
40
58
|
* The ghost row's `<tbody>`, a blank row for entering a new one, between the
|
|
41
59
|
* rows and the footer. The page writes it as the `ghost` template, so it
|
|
@@ -56,7 +74,10 @@ class LbTable extends HTMLElement {
|
|
|
56
74
|
constructor() {
|
|
57
75
|
super();
|
|
58
76
|
this.addEventListener("mouseup", () => this.filterBySelection());
|
|
77
|
+
this.addEventListener("click", (e) => this.sortFromHeading(e));
|
|
59
78
|
}
|
|
79
|
+
/** Each heading's column, by position in the row template; none when grouped. */
|
|
80
|
+
headings;
|
|
60
81
|
lbAttached() {
|
|
61
82
|
const ghost = this.querySelector(`tbody[${GHOST}]`);
|
|
62
83
|
if (!ghost?.firstElementChild)
|
|
@@ -71,6 +92,7 @@ class LbTable extends HTMLElement {
|
|
|
71
92
|
}
|
|
72
93
|
/** The row this page's request created or moved, brought into view. */
|
|
73
94
|
lbRowsLanded() {
|
|
95
|
+
this.showOrder();
|
|
74
96
|
this.querySelector(`[${ATTR_ROW_REQUESTED}="${REQUESTED_CREATED}"], ` +
|
|
75
97
|
`[${ATTR_ROW_REQUESTED}="${REQUESTED_MOVED}"]`)?.scrollIntoView({ block: "nearest" });
|
|
76
98
|
}
|
|
@@ -88,6 +110,96 @@ class LbTable extends HTMLElement {
|
|
|
88
110
|
if (row)
|
|
89
111
|
firstControl(row)?.focus();
|
|
90
112
|
}
|
|
113
|
+
/** The headings a column can be read for, and their columns. */
|
|
114
|
+
columns() {
|
|
115
|
+
if (this.headings)
|
|
116
|
+
return this.headings;
|
|
117
|
+
this.headings = new Map();
|
|
118
|
+
const body = this.querySelector(`tbody:not([${GHOST}])`);
|
|
119
|
+
const template = body?.querySelector(":scope > template");
|
|
120
|
+
const head = this.querySelector("thead tr");
|
|
121
|
+
if (!(template instanceof HTMLTemplateElement) || !head) {
|
|
122
|
+
return this.headings;
|
|
123
|
+
}
|
|
124
|
+
if (template.hasAttribute(ATTR_GROUP))
|
|
125
|
+
return this.headings;
|
|
126
|
+
const cells = template.content.querySelector("tr")?.children ?? [];
|
|
127
|
+
[...head.children].forEach((th, i) => {
|
|
128
|
+
const cell = cells[i];
|
|
129
|
+
if (!cell)
|
|
130
|
+
return;
|
|
131
|
+
const named = new Set(columnsIn(cell));
|
|
132
|
+
if (named.size === 1)
|
|
133
|
+
this.headings.set(th, [...named][0]);
|
|
134
|
+
});
|
|
135
|
+
return this.headings;
|
|
136
|
+
}
|
|
137
|
+
/** The order's leading term, from what the hub stamped. */
|
|
138
|
+
leading() {
|
|
139
|
+
const term = (this.getAttribute(ATTR_QUERY_ORDER) ?? "").split(",")[0];
|
|
140
|
+
if (!term)
|
|
141
|
+
return undefined;
|
|
142
|
+
const descending = term.startsWith(ORDER_DESCENDING);
|
|
143
|
+
return {
|
|
144
|
+
column: descending ? term.slice(ORDER_DESCENDING.length) : term,
|
|
145
|
+
descending,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
/** Whether rows stand where the order would not put them. */
|
|
149
|
+
unsorted() {
|
|
150
|
+
const count = this.getAttribute(ATTR_QUERY_UNSORTED_COUNT);
|
|
151
|
+
return count !== null && count !== "0";
|
|
152
|
+
}
|
|
153
|
+
/** The triangle on the sorted heading, and every heading a click sorts. */
|
|
154
|
+
showOrder() {
|
|
155
|
+
const term = this.leading();
|
|
156
|
+
const writable = this.hasAttribute(ATTR_ORDER);
|
|
157
|
+
for (const [th, column] of this.columns()) {
|
|
158
|
+
const sorted = column === term?.column;
|
|
159
|
+
let mark = th.querySelector(`:scope > [${SORT}]`);
|
|
160
|
+
if (sorted) {
|
|
161
|
+
if (!mark) {
|
|
162
|
+
mark = document.createElement("span");
|
|
163
|
+
mark.setAttribute(SORT, "");
|
|
164
|
+
mark.setAttribute("aria-hidden", "true");
|
|
165
|
+
th.append(mark);
|
|
166
|
+
}
|
|
167
|
+
mark.textContent = term.descending ? "▼" : "▲";
|
|
168
|
+
mark.toggleAttribute(UNSORTED, this.unsorted());
|
|
169
|
+
th.setAttribute("aria-sort", term.descending ? "descending" : "ascending");
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
mark?.remove();
|
|
173
|
+
th.removeAttribute("aria-sort");
|
|
174
|
+
}
|
|
175
|
+
th.toggleAttribute(SORTABLE, writable || (sorted && this.unsorted()));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/** A click on a heading: re-sort, reverse, or sort by its column. */
|
|
179
|
+
sortFromHeading(e) {
|
|
180
|
+
const th = e.target.closest("th");
|
|
181
|
+
const column = th && this.columns().get(th);
|
|
182
|
+
if (!column)
|
|
183
|
+
return;
|
|
184
|
+
const term = this.leading();
|
|
185
|
+
const sorted = column === term?.column;
|
|
186
|
+
if (sorted && this.unsorted()) {
|
|
187
|
+
const command = new Event("command");
|
|
188
|
+
Object.defineProperty(command, "command", { value: COMMAND_SORT });
|
|
189
|
+
this.dispatchEvent(command);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const choice = this.getAttribute(ATTR_ORDER);
|
|
193
|
+
if (!choice)
|
|
194
|
+
return;
|
|
195
|
+
const descending = sorted && !term.descending;
|
|
196
|
+
const detail = {
|
|
197
|
+
name: REQUEST_ROW_UPDATE,
|
|
198
|
+
query: CHOICES_QUERY,
|
|
199
|
+
values: { [choice]: (descending ? ORDER_DESCENDING : "") + column },
|
|
200
|
+
};
|
|
201
|
+
this.dispatchEvent(new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail }));
|
|
202
|
+
}
|
|
91
203
|
/** The columns of this table's rows a choice may filter. */
|
|
92
204
|
filtered() {
|
|
93
205
|
return (this.getAttribute(ATTR_FILTER) ?? "")
|
|
@@ -124,6 +236,25 @@ class LbTable extends HTMLElement {
|
|
|
124
236
|
this.dispatchEvent(new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail }));
|
|
125
237
|
}
|
|
126
238
|
}
|
|
239
|
+
/**
|
|
240
|
+
* The columns an element and everything in it show, read as the hub reads a
|
|
241
|
+
* row: into a branch an `lb-show` holds in a template, but not into another
|
|
242
|
+
* template, nor past an element naming a query of its own.
|
|
243
|
+
*/
|
|
244
|
+
function columnsIn(el) {
|
|
245
|
+
const own = el.getAttribute(ATTR_COLUMN);
|
|
246
|
+
const out = own ? [own] : [];
|
|
247
|
+
if (el.hasAttribute(ATTR_QUERY))
|
|
248
|
+
return out;
|
|
249
|
+
const children = el instanceof HTMLTemplateElement
|
|
250
|
+
? el.hasAttribute(ATTR_SHOW)
|
|
251
|
+
? [...el.content.children]
|
|
252
|
+
: []
|
|
253
|
+
: [...el.children];
|
|
254
|
+
for (const child of children)
|
|
255
|
+
out.push(...columnsIn(child));
|
|
256
|
+
return out;
|
|
257
|
+
}
|
|
127
258
|
/** A filter's term for text selected from a cell's whole text. */
|
|
128
259
|
function termFor(selected, whole) {
|
|
129
260
|
if (selected === "")
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lb-table.browser.js","sourceRoot":"","sources":["../widgets/lb-table.browser.ts"],"names":[],"mappings":"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","sourcesContent":["/// <reference lib=\"dom\" />\n\nimport {\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"]}
|
|
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_GROUP,\n ATTR_ORDER,\n ATTR_QUERY,\n ATTR_QUERY_ORDER,\n ATTR_QUERY_UNSORTED_COUNT,\n ATTR_ROW_REQUESTED,\n ATTR_SHOW,\n CHOICES_QUERY,\n COMMAND_SORT,\n LB_EVENT_NAME,\n LIVE_ROW,\n ORDER_DESCENDING,\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, filtering\n * by selected text, and the order shown on the heading.\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 heading of the column the rows are sorted by carries a triangle for the\n * direction, and `aria-sort`. A heading's column is the one `lb-column` in\n * the cell at the same position in the row template, a branch `lb-show`\n * holds in a template included; a cell with none, or with several, gives its\n * heading no column. Only the order's leading term is\n * shown. A grouped table shows none of this.\n *\n * While rows stand out of order the triangle carries `data-unsorted`, and a\n * click on its heading places them again with `--lb-sort`. Otherwise, on a\n * table with `lb-order`, a click on the sorted heading reverses the order and\n * a click on another heading sorts by its column, ascending: either way the\n * table writes the user's order to the column of `lb-choices` that `lb-order`\n * names. A table with no `lb-order` has nowhere to write one.\n */\nconst SORT = \"data-sort\";\nconst UNSORTED = \"data-unsorted\";\nconst SORTABLE = \"data-sortable\";\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 this.addEventListener(\"click\", (e) => this.sortFromHeading(e));\n }\n\n /** Each heading's column, by position in the row template; none when grouped. */\n private headings?: Map<Element, string>;\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.showOrder();\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 headings a column can be read for, and their columns. */\n private columns(): Map<Element, string> {\n if (this.headings) return this.headings;\n this.headings = new Map();\n const body = this.querySelector(`tbody:not([${GHOST}])`);\n const template = body?.querySelector(\":scope > template\");\n const head = this.querySelector(\"thead tr\");\n if (!(template instanceof HTMLTemplateElement) || !head) {\n return this.headings;\n }\n if (template.hasAttribute(ATTR_GROUP)) return this.headings;\n const cells = template.content.querySelector(\"tr\")?.children ?? [];\n [...head.children].forEach((th, i) => {\n const cell = cells[i];\n if (!cell) return;\n const named = new Set(columnsIn(cell));\n if (named.size === 1) this.headings!.set(th, [...named][0]!);\n });\n return this.headings;\n }\n\n /** The order's leading term, from what the hub stamped. */\n private leading(): { column: string; descending: boolean } | undefined {\n const term = (this.getAttribute(ATTR_QUERY_ORDER) ?? \"\").split(\",\")[0];\n if (!term) return undefined;\n const descending = term.startsWith(ORDER_DESCENDING);\n return {\n column: descending ? term.slice(ORDER_DESCENDING.length) : term,\n descending,\n };\n }\n\n /** Whether rows stand where the order would not put them. */\n private unsorted(): boolean {\n const count = this.getAttribute(ATTR_QUERY_UNSORTED_COUNT);\n return count !== null && count !== \"0\";\n }\n\n /** The triangle on the sorted heading, and every heading a click sorts. */\n private showOrder(): void {\n const term = this.leading();\n const writable = this.hasAttribute(ATTR_ORDER);\n for (const [th, column] of this.columns()) {\n const sorted = column === term?.column;\n let mark = th.querySelector(`:scope > [${SORT}]`);\n if (sorted) {\n if (!mark) {\n mark = document.createElement(\"span\");\n mark.setAttribute(SORT, \"\");\n mark.setAttribute(\"aria-hidden\", \"true\");\n th.append(mark);\n }\n mark.textContent = term.descending ? \"▼\" : \"▲\";\n mark.toggleAttribute(UNSORTED, this.unsorted());\n th.setAttribute(\"aria-sort\", term.descending ? \"descending\" : \"ascending\");\n } else {\n mark?.remove();\n th.removeAttribute(\"aria-sort\");\n }\n th.toggleAttribute(SORTABLE, writable || (sorted && this.unsorted()));\n }\n }\n\n /** A click on a heading: re-sort, reverse, or sort by its column. */\n private sortFromHeading(e: Event): void {\n const th = (e.target as Element).closest(\"th\");\n const column = th && this.columns().get(th);\n if (!column) return;\n const term = this.leading();\n const sorted = column === term?.column;\n if (sorted && this.unsorted()) {\n const command = new Event(\"command\");\n Object.defineProperty(command, \"command\", { value: COMMAND_SORT });\n this.dispatchEvent(command);\n return;\n }\n const choice = this.getAttribute(ATTR_ORDER);\n if (!choice) return;\n const descending = sorted && !term.descending;\n const detail: HubRequest = {\n name: REQUEST_ROW_UPDATE,\n query: CHOICES_QUERY,\n values: { [choice]: (descending ? ORDER_DESCENDING : \"\") + column },\n };\n this.dispatchEvent(\n new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail }),\n );\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/**\n * The columns an element and everything in it show, read as the hub reads a\n * row: into a branch an `lb-show` holds in a template, but not into another\n * template, nor past an element naming a query of its own.\n */\nfunction columnsIn(el: Element): string[] {\n const own = el.getAttribute(ATTR_COLUMN);\n const out = own ? [own] : [];\n if (el.hasAttribute(ATTR_QUERY)) return out;\n const children =\n el instanceof HTMLTemplateElement\n ? el.hasAttribute(ATTR_SHOW)\n ? [...el.content.children]\n : []\n : [...el.children];\n for (const child of children) out.push(...columnsIn(child));\n return out;\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"]}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/* lb-table: the order on its heading, and rows standing out of it. An
|
|
2
|
+
application's own stylesheet comes later and overrides any of this. */
|
|
3
|
+
|
|
4
|
+
lb-table th[data-sortable] {
|
|
5
|
+
cursor: pointer;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
lb-table th > [data-sort] {
|
|
9
|
+
display: inline-block;
|
|
10
|
+
margin-inline-start: 0.3em;
|
|
11
|
+
padding: 0 0.15em;
|
|
12
|
+
border: 1px solid transparent;
|
|
13
|
+
border-radius: 50%;
|
|
14
|
+
font-size: 0.8em;
|
|
15
|
+
line-height: 1.3;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
lb-table th > [data-sort][data-unsorted] {
|
|
19
|
+
color: #b58900;
|
|
20
|
+
border-color: currentColor;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
lb-table tr[lb-row-unsorted] > :first-child {
|
|
24
|
+
box-shadow: inset 3px 0 0 #b58900;
|
|
25
|
+
}
|
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.
|
|
@@ -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-table.md
CHANGED
|
@@ -56,8 +56,11 @@ The server declares the query's key and its order.
|
|
|
56
56
|
|
|
57
57
|
## Order and groups
|
|
58
58
|
|
|
59
|
-
The hub places the rows by the query's order and builds its groups.
|
|
60
|
-
|
|
59
|
+
The hub places the rows by the query's order and builds its groups. An
|
|
60
|
+
edit from a control in a row, or from the ghost row, leaves the rows where
|
|
61
|
+
they stand until the user asks for the order; a button with
|
|
62
|
+
`commandfor` naming the table's `id` and `command="--lb-sort"` does that.
|
|
63
|
+
The rows land in the table's one `<tbody>`, and a `<tbody>` cannot hold another,
|
|
61
64
|
so a group here is a heading row with its rows after it. For a `<tbody>` per
|
|
62
65
|
group, write a plain `<table lb-query>` whose group template is the
|
|
63
66
|
`<tbody>`.
|
|
@@ -66,6 +69,35 @@ group, write a plain `<table lb-query>` whose group template is the
|
|
|
66
69
|
`lb-query`: the user's order replaces the query's, and a row the user's
|
|
67
70
|
filter does not pass is hidden.
|
|
68
71
|
|
|
72
|
+
## The order on the heading
|
|
73
|
+
|
|
74
|
+
The heading of the column the rows are sorted by carries a triangle, up for
|
|
75
|
+
ascending and down for descending, and `aria-sort`. Only the order's
|
|
76
|
+
leading term is shown. A heading's column is the `lb-column` in the cell
|
|
77
|
+
at the same position in the row template, on the cell or inside it. A cell
|
|
78
|
+
naming no column, or several, gives its heading none.
|
|
79
|
+
|
|
80
|
+
While rows stand out of order, the triangle carries `data-unsorted`, and a
|
|
81
|
+
click on its heading places them again, as `--lb-sort` does, keeping the
|
|
82
|
+
direction.
|
|
83
|
+
|
|
84
|
+
With `lb-order` on the table, a click on the sorted heading reverses the
|
|
85
|
+
order, and a click on another heading sorts by its column, ascending. The
|
|
86
|
+
widget writes the order to the column of `lb-choices` that `lb-order`
|
|
87
|
+
names. Without `lb-order` there is nowhere to write one, and only the
|
|
88
|
+
re-sort answers a click. There is no click back to the server's order.
|
|
89
|
+
|
|
90
|
+
A heading that takes a click carries `data-sortable`.
|
|
91
|
+
|
|
92
|
+
None of this shows on a grouped table, nor when the order's leading column
|
|
93
|
+
has no heading. There, a button with `commandfor` and `--lb-sort` does the
|
|
94
|
+
re-sort.
|
|
95
|
+
|
|
96
|
+
The package ships `lb-table.css`: the triangle turns amber and circled
|
|
97
|
+
while rows stand out of order, and the first cell of a row carrying
|
|
98
|
+
`lb-row-unsorted` is marked on its leading edge. The application's own
|
|
99
|
+
stylesheet comes after it and overrides any of it.
|
|
100
|
+
|
|
69
101
|
## Filtering by selection
|
|
70
102
|
|
|
71
103
|
Text selected in a cell filters the table by that cell's column, when the
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Feature request: `lb-table` for maintenance pages
|
|
2
|
+
|
|
3
|
+
Temporary. Written from the rkmedia app, which uses `lb-table` for every
|
|
4
|
+
list it edits (lookup tables with a ghost row, edit-in-place cells,
|
|
5
|
+
drag-to-reorder). Two things every such page wants and today writes for
|
|
6
|
+
itself, or cannot have. To be settled here and then folded into
|
|
7
|
+
`lb-table.md` or deleted.
|
|
8
|
+
|
|
9
|
+
## 1. The order shown on the heading, and re-sorted from it
|
|
10
|
+
|
|
11
|
+
**Status:** first iteration implemented, as settled below; see
|
|
12
|
+
`lb-table.md`, *The order on the heading*.
|
|
13
|
+
|
|
14
|
+
**Want:** `lb-table` always marks the heading of the column the rows are
|
|
15
|
+
sorted by with an up or down triangle for the direction. While the order is
|
|
16
|
+
dirty from the user's edits (`lb-query-unsorted-count` is not 0), the
|
|
17
|
+
triangle changes color. Clicking the heading sorts by its column.
|
|
18
|
+
|
|
19
|
+
**Why:** an edit from a control in a row, or from the ghost row, leaves the
|
|
20
|
+
rows where they stand (`lb-table.md`, *Order and groups*). Every table
|
|
21
|
+
whose order reads a column the user edits needs a re-sort control, so every
|
|
22
|
+
page writes the same markup and CSS. What rkmedia writes today:
|
|
23
|
+
|
|
24
|
+
```html
|
|
25
|
+
<div class="list">
|
|
26
|
+
<button type="button" class="resort" commandfor="workshop-topics-list" command="--lb-sort">Re-sort</button>
|
|
27
|
+
<lb-table id="workshop-topics-list" lb-query="workshopTopics" exp-caption="Talks and workshops">
|
|
28
|
+
...
|
|
29
|
+
</lb-table>
|
|
30
|
+
</div>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```css
|
|
34
|
+
.resort { visibility: hidden; }
|
|
35
|
+
:has(> [lb-query-unsorted-count]:not([lb-query-unsorted-count="0"])) > .resort { visibility: visible; }
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The button cannot go inside the tag, so it needs an `id`, `commandfor` and
|
|
39
|
+
a `:has()` on a wrapper. Shown with `display` it moves the table; shown
|
|
40
|
+
with `visibility` it leaves a blank strip above every table.
|
|
41
|
+
|
|
42
|
+
The heading row is always laid out and the widget owns it, so the
|
|
43
|
+
triangle placed there moves nothing, needs no `id` and no page CSS. The
|
|
44
|
+
hub already knows the order: the server declares it, and `lb-order` names
|
|
45
|
+
the user's.
|
|
46
|
+
|
|
47
|
+
**Settled:**
|
|
48
|
+
|
|
49
|
+
- **The triangle is always shown** on the sorted column's heading, clean or
|
|
50
|
+
dirty.
|
|
51
|
+
- **Dirty is a color change** on the triangle.
|
|
52
|
+
- **Clicking a dirty triangle re-sorts** (`--lb-sort`): it places the rows
|
|
53
|
+
and clears `lb-row-unsorted`, keeping the direction. Only a click on a
|
|
54
|
+
clean triangle reverses.
|
|
55
|
+
- **Clicking another heading sorts by its column**, writing the user's
|
|
56
|
+
order (`lb-order`). That places every row and clears `lb-row-unsorted`.
|
|
57
|
+
- **Raw values.** A column shown formatted sorts by what the query sent,
|
|
58
|
+
so it must be sent raw or declared in `types` (the reference already says
|
|
59
|
+
so); worth repeating in `lb-table.md`.
|
|
60
|
+
- **A default style for `tr[lb-row-unsorted]`** (rkmedia marks the first
|
|
61
|
+
cell's left edge), subtle and overridable, saves every app writing one.
|
|
62
|
+
- **A heading takes its column from the row template.** The nth heading
|
|
63
|
+
shows the column of the nth cell of the row template, the `lb-column` in
|
|
64
|
+
that cell. The page writes nothing on the `<th>`. A cell with no
|
|
65
|
+
`lb-column`, or with several, gives its heading no column, and that
|
|
66
|
+
heading does not sort.
|
|
67
|
+
- **One column at a time.** A heading click writes an order of one term.
|
|
68
|
+
When the order has several terms, the triangle marks the leading term.
|
|
69
|
+
Sub-sorts are a UI question for later, built on this foundation.
|
|
70
|
+
- **Groups leave all of this out.** A table with group templates shows no
|
|
71
|
+
triangle and takes no heading clicks, so none of it triggers there.
|
|
72
|
+
- **Dirty is yellow, circled.** The triangle turns yellow and gains a
|
|
73
|
+
circle around it, so the shape says dirty without the color.
|
|
74
|
+
- **No route back to the server's order.** Once the user sorts by a
|
|
75
|
+
heading, the user's order stands.
|
|
76
|
+
- **No sort is the opt-out.** A table with no order shows no triangle and
|
|
77
|
+
takes no heading clicks.
|
|
78
|
+
|
|
79
|
+
**Open:**
|
|
80
|
+
|
|
81
|
+
- **The order's column not shown.** A drag-to-reorder table orders by a
|
|
82
|
+
position column no one displays, and a server's order may lead with any
|
|
83
|
+
column the template leaves out. No heading carries the triangle, so a
|
|
84
|
+
dirty order has no re-sort control. Either the triangle has an
|
|
85
|
+
always-present fallback, such tables keep a page-written button, or the
|
|
86
|
+
dirty state cannot arise for them (a drag rewrites positions so no row
|
|
87
|
+
stands out of order).
|
|
88
|
+
- **Heading clicks without `lb-order`.** A heading click writes the user's
|
|
89
|
+
order into the column of `lb-choices` that `lb-order` names. A table
|
|
90
|
+
with a server order and no `lb-order` has nowhere to write it: it can
|
|
91
|
+
show the triangle and re-sort from it, but not sort by another heading.
|
|
92
|
+
Whether that is the intended reading of "no sort", or the widget
|
|
93
|
+
supplies a choice of its own.
|
|
94
|
+
|
|
95
|
+
The first iteration takes the plain reading of both: a table whose order's
|
|
96
|
+
leading column has no heading shows no triangle and keeps a page-written
|
|
97
|
+
button, and a table without `lb-order` shows the triangle and re-sorts
|
|
98
|
+
from it but takes no clicks on other headings.
|
|
99
|
+
|
|
100
|
+
## 2. Highlight-to-filter that works on edit pages
|
|
101
|
+
|
|
102
|
+
**Want:** select text in a cell and the table filters to the rows that
|
|
103
|
+
match, on read-only and editable tables alike, and the table shows what it
|
|
104
|
+
is filtered by.
|
|
105
|
+
|
|
106
|
+
**What exists:** `lb-table.md`, *Filtering by selection*: a selection in a
|
|
107
|
+
cell whose `lb-column` the table's `lb-filter` names becomes that column's
|
|
108
|
+
choice (`=`, `^=`, `$=`, `*=`).
|
|
109
|
+
|
|
110
|
+
**Gaps:**
|
|
111
|
+
|
|
112
|
+
- **Inputs.** The widget reads `getSelection()`, which does not report text
|
|
113
|
+
selected inside an `<input>` or `<textarea>`. An editable cell is an
|
|
114
|
+
input, so on an edit page highlighting never filters. A control reports
|
|
115
|
+
its own range (`selectionStart` / `selectionEnd`); the widget could read
|
|
116
|
+
that when the selection is in one.
|
|
117
|
+
- **Selecting is also editing.** In an input, selecting text is how you
|
|
118
|
+
replace it, so filtering on every selection there would fight editing.
|
|
119
|
+
It needs a gesture that means "filter": a modifier key held on mouse-up,
|
|
120
|
+
or inputs that are `readonly`. Which gesture is Loadbare's call.
|
|
121
|
+
- **No indicator.** Nothing on the table says it is filtered, by what, or
|
|
122
|
+
offers to clear it; a page must add a control bound to `lb-choices`
|
|
123
|
+
itself. With the re-sort on the heading, the caption line is free for
|
|
124
|
+
this: the active filters with a clear button for each, and rows hidden
|
|
125
|
+
("12 of 40 shown"). Open whether the caption line is the place.
|
|
126
|
+
|
|
127
|
+
## Order of value to rkmedia
|
|
128
|
+
|
|
129
|
+
1, then 2. rkmedia has editable ordered tables now, will want heading
|
|
130
|
+
sorts on the first long list, and has no filters yet.
|
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.10.0",
|
|
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.20.0 <1.0.0"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
|
-
"@loadbare/app": ">=0.
|
|
55
|
+
"@loadbare/app": ">=0.20.0 <1.0.0",
|
|
56
56
|
"@types/jsdom": "^21.1.7",
|
|
57
57
|
"@types/node": "^22.0.0",
|
|
58
58
|
"jsdom": "^25.0.0",
|