@loadbare/widgets 0.9.1 → 0.10.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-mailto.browser.d.ts +2 -0
- package/dist/lb-mailto.browser.d.ts.map +1 -0
- package/dist/lb-mailto.browser.js +29 -0
- package/dist/lb-mailto.browser.js.map +1 -0
- package/dist/lb-mailto.html +6 -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/dist/lb-tel.browser.d.ts +2 -0
- package/dist/lb-tel.browser.d.ts.map +1 -0
- package/dist/lb-tel.browser.js +40 -0
- package/dist/lb-tel.browser.js.map +1 -0
- package/dist/lb-tel.html +6 -0
- package/docs/lb-mailto.md +29 -0
- package/docs/lb-table.md +34 -2
- package/docs/lb-tel.md +32 -0
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -19,6 +19,8 @@ Inventory:
|
|
|
19
19
|
| [`lb-checkbox`](docs/lb-checkbox.md) | a checkbox and its label, as markup to style |
|
|
20
20
|
| [`lb-number`](docs/lb-number.md) | a number shown by `Intl.NumberFormat` |
|
|
21
21
|
| [`lb-date`](docs/lb-date.md) | a date or time shown by `Intl.DateTimeFormat` |
|
|
22
|
+
| [`lb-tel`](docs/lb-tel.md) | a phone number shown as a `tel:` link |
|
|
23
|
+
| [`lb-mailto`](docs/lb-mailto.md) | an email address shown as a `mailto:` link |
|
|
22
24
|
|
|
23
25
|
## Installing
|
|
24
26
|
|
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;AACvC,OAAO,wBAAwB,CAAC;AAChC,OAAO,sBAAsB,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;AAC9B,OAAO,qBAAqB,CAAC;AAC7B,OAAO,wBAAwB,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;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"]}
|
|
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;AAC9B,OAAO,qBAAqB,CAAC;AAC7B,OAAO,wBAAwB,CAAC","sourcesContent":["import \"./lb-input.browser.js\";\nimport \"./lb-select.browser.js\";\nimport \"./lb-options.browser.js\";\nimport \"./lb-table.browser.js\";\nimport \"./lb-picker.browser.js\";\nimport \"./lb-confirm.browser.js\";\nimport \"./lb-insert-dialog.browser.js\";\nimport \"./lb-number.browser.js\";\nimport \"./lb-date.browser.js\";\nimport \"./lb-tel.browser.js\";\nimport \"./lb-mailto.browser.js\";\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lb-mailto.browser.d.ts","sourceRoot":"","sources":["../widgets/lb-mailto.browser.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
import { ATTR_COLUMN_VALUE } from "@loadbare/app/constants";
|
|
3
|
+
/**
|
|
4
|
+
* Shows the value that landed on it as written, in the `<a>` its definition
|
|
5
|
+
* holds, with a `mailto:` `href` that writes to it. Not a control: the hub
|
|
6
|
+
* stamps `lb-column-value` on the tag and never gathers it.
|
|
7
|
+
*
|
|
8
|
+
* The address is encoded into the `href`, so a value carrying `?` or `&`
|
|
9
|
+
* cannot add a subject, a body or another recipient to the message. A value
|
|
10
|
+
* with no `@` shows as it arrived, with no `href`.
|
|
11
|
+
*/
|
|
12
|
+
class LbMailto extends HTMLElement {
|
|
13
|
+
static observedAttributes = [ATTR_COLUMN_VALUE];
|
|
14
|
+
attributeChangedCallback(_name, _old, value) {
|
|
15
|
+
const a = this.querySelector("a");
|
|
16
|
+
if (!a)
|
|
17
|
+
return;
|
|
18
|
+
a.textContent = value ?? "";
|
|
19
|
+
const address = value?.trim();
|
|
20
|
+
if (address?.includes("@")) {
|
|
21
|
+
a.setAttribute("href", `mailto:${encodeURIComponent(address).replace(/%40/g, "@")}`);
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
a.removeAttribute("href");
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
customElements.define("lb-mailto", LbMailto);
|
|
29
|
+
//# sourceMappingURL=lb-mailto.browser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lb-mailto.browser.js","sourceRoot":"","sources":["../widgets/lb-mailto.browser.ts"],"names":[],"mappings":"AAAA,2BAA2B;AAE3B,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAE5D;;;;;;;;GAQG;AACH,MAAM,QAAS,SAAQ,WAAW;IAChC,MAAM,CAAC,kBAAkB,GAAG,CAAC,iBAAiB,CAAC,CAAC;IAEhD,wBAAwB,CACtB,KAAa,EACb,IAAmB,EACnB,KAAoB;QAEpB,MAAM,CAAC,GAAG,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC;QAClC,IAAI,CAAC,CAAC;YAAE,OAAO;QACf,CAAC,CAAC,WAAW,GAAG,KAAK,IAAI,EAAE,CAAC;QAC5B,MAAM,OAAO,GAAG,KAAK,EAAE,IAAI,EAAE,CAAC;QAC9B,IAAI,OAAO,EAAE,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;YAC3B,CAAC,CAAC,YAAY,CACZ,MAAM,EACN,UAAU,kBAAkB,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,CAC7D,CAAC;QACJ,CAAC;aAAM,CAAC;YACN,CAAC,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC;QAC5B,CAAC;IACH,CAAC;;AAGH,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 as written, in the `<a>` its definition\n * holds, with a `mailto:` `href` that writes to it. Not a control: the hub\n * stamps `lb-column-value` on the tag and never gathers it.\n *\n * The address is encoded into the `href`, so a value carrying `?` or `&`\n * cannot add a subject, a body or another recipient to the message. A value\n * with no `@` shows as it arrived, with no `href`.\n */\nclass LbMailto extends HTMLElement {\n static observedAttributes = [ATTR_COLUMN_VALUE];\n\n attributeChangedCallback(\n _name: string,\n _old: string | null,\n value: string | null,\n ): void {\n const a = this.querySelector(\"a\");\n if (!a) return;\n a.textContent = value ?? \"\";\n const address = value?.trim();\n if (address?.includes(\"@\")) {\n a.setAttribute(\n \"href\",\n `mailto:${encodeURIComponent(address).replace(/%40/g, \"@\")}`,\n );\n } else {\n a.removeAttribute(\"href\");\n }\n }\n}\n\ncustomElements.define(\"lb-mailto\", LbMailto);\n"]}
|
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
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lb-tel.browser.d.ts","sourceRoot":"","sources":["../widgets/lb-tel.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 as written, in the `<a>` its definition
|
|
5
|
+
* holds, with a `tel:` `href` that dials it. Not a control: the hub stamps
|
|
6
|
+
* `lb-column-value` on the tag and never gathers it.
|
|
7
|
+
*
|
|
8
|
+
* The `href` keeps a leading `+` and the digits, and drops the rest, so
|
|
9
|
+
* `(555) 123-4567` dials as `5551234567`. An extension written after `x`,
|
|
10
|
+
* `ext` or `extension` becomes `;ext=`, rather than digits dialed on the end
|
|
11
|
+
* of the number. A value with no digits shows as it arrived, with no `href`.
|
|
12
|
+
*/
|
|
13
|
+
class LbTel extends HTMLElement {
|
|
14
|
+
static observedAttributes = [ATTR_COLUMN_VALUE];
|
|
15
|
+
attributeChangedCallback(_name, _old, value) {
|
|
16
|
+
const a = this.querySelector("a");
|
|
17
|
+
if (!a)
|
|
18
|
+
return;
|
|
19
|
+
a.textContent = value ?? "";
|
|
20
|
+
const href = value === null ? null : tel(value);
|
|
21
|
+
if (href)
|
|
22
|
+
a.setAttribute("href", href);
|
|
23
|
+
else
|
|
24
|
+
a.removeAttribute("href");
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** The `tel:` URI for a number as a person writes it, null for no number. */
|
|
28
|
+
function tel(value) {
|
|
29
|
+
const [number, ext] = value.split(EXTENSION, 2);
|
|
30
|
+
const digits = number.replace(/\D/g, "");
|
|
31
|
+
if (!digits)
|
|
32
|
+
return null;
|
|
33
|
+
const plus = number.trim().startsWith("+") ? "+" : "";
|
|
34
|
+
const extDigits = ext?.replace(/\D/g, "");
|
|
35
|
+
return `tel:${plus}${digits}${extDigits ? `;ext=${extDigits}` : ""}`;
|
|
36
|
+
}
|
|
37
|
+
/** Where a number ends and its extension begins. */
|
|
38
|
+
const EXTENSION = /\s*(?:extension|ext\.?|x)\s*(?=\d)/i;
|
|
39
|
+
customElements.define("lb-tel", LbTel);
|
|
40
|
+
//# sourceMappingURL=lb-tel.browser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lb-tel.browser.js","sourceRoot":"","sources":["../widgets/lb-tel.browser.ts"],"names":[],"mappings":"AAAA,2BAA2B;AAE3B,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAE5D;;;;;;;;;GASG;AACH,MAAM,KAAM,SAAQ,WAAW;IAC7B,MAAM,CAAC,kBAAkB,GAAG,CAAC,iBAAiB,CAAC,CAAC;IAEhD,wBAAwB,CACtB,KAAa,EACb,IAAmB,EACnB,KAAoB;QAEpB,MAAM,CAAC,GAAG,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC;QAClC,IAAI,CAAC,CAAC;YAAE,OAAO;QACf,CAAC,CAAC,WAAW,GAAG,KAAK,IAAI,EAAE,CAAC;QAC5B,MAAM,IAAI,GAAG,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QAChD,IAAI,IAAI;YAAE,CAAC,CAAC,YAAY,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;;YAClC,CAAC,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC;IACjC,CAAC;;AAGH,6EAA6E;AAC7E,SAAS,GAAG,CAAC,KAAa;IACxB,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC;IAChD,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACzC,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IACzB,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;IACtD,MAAM,SAAS,GAAG,GAAG,EAAE,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IAC1C,OAAO,OAAO,IAAI,GAAG,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC,QAAQ,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;AACvE,CAAC;AAED,oDAAoD;AACpD,MAAM,SAAS,GAAG,qCAAqC,CAAC;AAExD,cAAc,CAAC,MAAM,CAAC,QAAQ,EAAE,KAAK,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 as written, in the `<a>` its definition\n * holds, with a `tel:` `href` that dials it. Not a control: the hub stamps\n * `lb-column-value` on the tag and never gathers it.\n *\n * The `href` keeps a leading `+` and the digits, and drops the rest, so\n * `(555) 123-4567` dials as `5551234567`. An extension written after `x`,\n * `ext` or `extension` becomes `;ext=`, rather than digits dialed on the end\n * of the number. A value with no digits shows as it arrived, with no `href`.\n */\nclass LbTel extends HTMLElement {\n static observedAttributes = [ATTR_COLUMN_VALUE];\n\n attributeChangedCallback(\n _name: string,\n _old: string | null,\n value: string | null,\n ): void {\n const a = this.querySelector(\"a\");\n if (!a) return;\n a.textContent = value ?? \"\";\n const href = value === null ? null : tel(value);\n if (href) a.setAttribute(\"href\", href);\n else a.removeAttribute(\"href\");\n }\n}\n\n/** The `tel:` URI for a number as a person writes it, null for no number. */\nfunction tel(value: string): string | null {\n const [number, ext] = value.split(EXTENSION, 2);\n const digits = number.replace(/\\D/g, \"\");\n if (!digits) return null;\n const plus = number.trim().startsWith(\"+\") ? \"+\" : \"\";\n const extDigits = ext?.replace(/\\D/g, \"\");\n return `tel:${plus}${digits}${extDigits ? `;ext=${extDigits}` : \"\"}`;\n}\n\n/** Where a number ends and its extension begins. */\nconst EXTENSION = /\\s*(?:extension|ext\\.?|x)\\s*(?=\\d)/i;\n\ncustomElements.define(\"lb-tel\", LbTel);\n"]}
|
package/dist/lb-tel.html
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# `lb-mailto`
|
|
2
|
+
|
|
3
|
+
> Status: a barebones starter. Iterative use will drive improvements.
|
|
4
|
+
|
|
5
|
+
An email address shown as written, as a `mailto:` link. Not a control:
|
|
6
|
+
write `lb-column` on the tag, and the hub lands the column on it and never
|
|
7
|
+
gathers it.
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<lb-mailto lb-column="email"></lb-mailto>
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
It takes no parameters. To edit the address, use
|
|
14
|
+
`<lb-input exp-type="email">`.
|
|
15
|
+
|
|
16
|
+
## The value
|
|
17
|
+
|
|
18
|
+
The link's text is the value as it arrived. Its `href` is `mailto:` and the
|
|
19
|
+
address, encoded, so a value carrying `?` or `&` cannot add a subject, a body
|
|
20
|
+
or another recipient to the message.
|
|
21
|
+
|
|
22
|
+
A value with no `@` shows as it arrived, with no `href`. Null shows
|
|
23
|
+
nothing.
|
|
24
|
+
|
|
25
|
+
## Styling
|
|
26
|
+
|
|
27
|
+
The definition is an `<a>`, with no `href` while there is no address, so
|
|
28
|
+
`lb-mailto a:not([href])` selects a value that is no link. The hub stamps
|
|
29
|
+
the tag with `lb-column-value`, the value as it arrived.
|
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
|
package/docs/lb-tel.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# `lb-tel`
|
|
2
|
+
|
|
3
|
+
> Status: a barebones starter. Iterative use will drive improvements.
|
|
4
|
+
|
|
5
|
+
A phone number shown as written, as a `tel:` link. 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-tel lb-column="phone"></lb-tel>
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
It takes no parameters. To edit the number, use
|
|
14
|
+
`<lb-input exp-type="tel">`.
|
|
15
|
+
|
|
16
|
+
## The value
|
|
17
|
+
|
|
18
|
+
The link's text is the value as it arrived. Its `href` keeps a leading `+`
|
|
19
|
+
and the digits, and drops everything else, so `(555) 123-4567` dials
|
|
20
|
+
`tel:5551234567`.
|
|
21
|
+
|
|
22
|
+
An extension written after `x`, `ext`, `ext.` or `extension` goes in the
|
|
23
|
+
`href` as `;ext=`, so `555-123-4567 x89` dials `tel:5551234567;ext=89`.
|
|
24
|
+
|
|
25
|
+
A value with no digits shows as it arrived, with no `href`. Null shows
|
|
26
|
+
nothing.
|
|
27
|
+
|
|
28
|
+
## Styling
|
|
29
|
+
|
|
30
|
+
The definition is an `<a>`, with no `href` while there is nothing to dial,
|
|
31
|
+
so `lb-tel a:not([href])` selects a number that is no link. The hub stamps
|
|
32
|
+
the tag with `lb-column-value`, the value as it arrived.
|
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, number, date",
|
|
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, tel, mailto",
|
|
4
|
+
"version": "0.10.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.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",
|