@loadbare/app 0.24.0 → 0.26.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/dist/build/assemble.d.ts +1 -2
- package/dist/build/assemble.d.ts.map +1 -1
- package/dist/build/assemble.js +61 -53
- package/dist/build/assemble.js.map +1 -1
- package/dist/build/cli.d.ts +4 -10
- package/dist/build/cli.d.ts.map +1 -1
- package/dist/build/cli.js +11 -33
- package/dist/build/cli.js.map +1 -1
- package/dist/build/declared.js +1 -1
- package/dist/build/declared.js.map +1 -1
- package/dist/core/lb-constants.d.ts +8 -18
- package/dist/core/lb-constants.d.ts.map +1 -1
- package/dist/core/lb-constants.js +32 -72
- package/dist/core/lb-constants.js.map +1 -1
- package/dist/core/lb-types.d.ts +12 -11
- package/dist/core/lb-types.d.ts.map +1 -1
- package/dist/core/lb-types.js +7 -0
- package/dist/core/lb-types.js.map +1 -1
- package/dist/hub/lb-apply.d.ts +23 -37
- package/dist/hub/lb-apply.d.ts.map +1 -1
- package/dist/hub/lb-apply.js +205 -518
- package/dist/hub/lb-apply.js.map +1 -1
- package/dist/hub/lb-hub.browser.d.ts.map +1 -1
- package/dist/hub/lb-hub.browser.js +159 -132
- package/dist/hub/lb-hub.browser.js.map +1 -1
- package/dist/hub/lb-lifecycle.d.ts +2 -2
- package/dist/hub/lb-lifecycle.d.ts.map +1 -1
- package/dist/hub/lb-lifecycle.js +9 -17
- package/dist/hub/lb-lifecycle.js.map +1 -1
- package/dist/server/lb-express.d.ts.map +1 -1
- package/dist/server/lb-express.js +3 -3
- package/dist/server/lb-express.js.map +1 -1
- package/dist/server/lb-server.d.ts +35 -19
- package/dist/server/lb-server.d.ts.map +1 -1
- package/dist/server/lb-server.js +141 -88
- package/dist/server/lb-server.js.map +1 -1
- package/docs/binding-cases.md +150 -0
- package/docs/comparison.md +20 -29
- package/docs/miswarts.md +2 -2
- package/docs/reference/css.md +2 -15
- package/docs/reference/custom-elements.md +6 -8
- package/docs/reference/data-binding.md +100 -185
- package/docs/reference/names.md +11 -27
- package/docs/reference/page-files.md +4 -14
- package/docs/reference/server.md +0 -1
- package/docs/testing.md +2 -5
- package/package.json +1 -1
- package/skills/loadbare-app/SKILL.md +66 -56
- package/skills/loadbare-app/references/css.md +2 -15
- package/skills/loadbare-app/references/custom-elements.md +6 -8
- package/skills/loadbare-app/references/data-binding.md +100 -185
- package/skills/loadbare-app/references/miswarts.md +2 -2
- package/skills/loadbare-app/references/names.md +11 -27
- package/skills/loadbare-app/references/page-files.md +4 -14
- package/skills/loadbare-app/references/server.md +0 -1
- package/docs/temp-proposals.md +0 -251
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/// <reference lib="dom" />
|
|
2
|
-
import { ATTR_COLUMN, ATTR_COLUMN_VALUE, ATTR_KEY_VALUE, ATTR_PAGE, ATTR_PAGE_TITLE, ATTR_QUERY, ATTR_REQUEST, ATTR_REQUEST_ERROR, ATTR_REQUEST_PENDING, ATTR_REQUEST_REFUSED, ATTR_ROW_EDITING,
|
|
3
|
-
import { isRowRequest, nextLocation, pageNameFor, parmsRow, } from "../core/lb-types.js";
|
|
4
|
-
import { applyResponse, applyRow, blankRow, focusIn, gatherInto,
|
|
2
|
+
import { ATTR_COLUMN, ATTR_COLUMN_VALUE, ATTR_KEY_VALUE, ATTR_PAGE, ATTR_PAGE_TITLE, ATTR_QUERY, ATTR_REQUEST, ATTR_REQUEST_ERROR, ATTR_REQUEST_PENDING, ATTR_REQUEST_REFUSED, ATTR_ROW_EDITING, ATTR_URL_LINK, ATTR_URL_PUSH, ATTR_URL_UNKNOWN, COMMAND_EDIT, COMMAND_NEW, COMMAND_RELEASE, COMMAND_PLACE, FILTERS_COLUMN_QUERY, FILTERS_QUERY, HUB_COLUMN_AS_OF, HUB_COLUMN_ERROR, HUB_COLUMN_LOAD_STATE, HUB_COLUMN_PENDING, HUB_COLUMN_SERVER_STATE, HUB_ERROR_NETWORK, HUB_ERROR_TIMEOUT, HUB_LOAD_FAILED, HUB_LOAD_LOADED, HUB_LOAD_LOADING, HUB_QUERY, HUB_SERVER_FAILING, HUB_SERVER_REACHABLE, HUB_SERVER_UNREACHABLE, HUB_TAG_NAME, KIND_ROW, KIND_ROWS, LB_DONE_EVENT_NAME, LB_ENDPOINT, LB_EVENT_NAME, LB_REFUSAL_STATUS, LB_REQUEST_TIMEOUT_MS, LB_COMMAND_PREFIX, LB_RESERVED_PREFIX, LIVE_ROW, REQUEST_READ, REQUEST_ROW_DELETE, REQUEST_ROW_INSERT, REQUEST_ROW_UPDATE, SORT_COLUMN_ORDER, SORT_COLUMN_QUERY, SORT_QUERY, URL_COLUMN_PAGE_LABEL, URL_COLUMN_PAGE_UNKNOWN, URL_COLUMN_PATH, URL_QUERY, } from "../core/lb-constants.js";
|
|
3
|
+
import { isRowRequest, nextLocation, pageNameFor, parmsRow, oneRow, } from "../core/lb-types.js";
|
|
4
|
+
import { addUnentered, applyResponse, discardUnentered, applyRow, blankRow, focusIn, gatherInto, forget, isControl, isFormAssociated, keptAnswer, placeAgain, templateIn, within, } from "./lb-apply.js";
|
|
5
5
|
import { attachPlaced, detachRemoved, placed, requestDone, upgradeAbsent, } from "./lb-lifecycle.js";
|
|
6
6
|
export { applyResponse, applyRow } from "./lb-apply.js";
|
|
7
7
|
/** What each request Loadbare provides cannot be issued without. */
|
|
@@ -55,8 +55,7 @@ function formOwnerOf(el) {
|
|
|
55
55
|
: null;
|
|
56
56
|
}
|
|
57
57
|
/**
|
|
58
|
-
* The controls a request gathers
|
|
59
|
-
* "Gathering":
|
|
58
|
+
* The controls a request gathers:
|
|
60
59
|
*
|
|
61
60
|
* 1. the element carrying `lb-request` has `lb-column`: it alone;
|
|
62
61
|
* 2. the element is a `<form>`: the controls whose form owner it is;
|
|
@@ -94,6 +93,8 @@ function gather(group) {
|
|
|
94
93
|
const controls = [];
|
|
95
94
|
const values = {};
|
|
96
95
|
const nestedControls = [];
|
|
96
|
+
// By row element rather than by key: rows not yet entered share the empty
|
|
97
|
+
// key, and each is a row of its own.
|
|
97
98
|
const byQuery = new Map();
|
|
98
99
|
const form = group?.form;
|
|
99
100
|
const own = form
|
|
@@ -113,12 +114,14 @@ function gather(group) {
|
|
|
113
114
|
live &&
|
|
114
115
|
queryOf(live) === nested) {
|
|
115
116
|
const name = nested.getAttribute(ATTR_QUERY);
|
|
116
|
-
const key = live.getAttribute(ATTR_KEY_VALUE);
|
|
117
117
|
const set = byQuery.get(name) ?? new Map();
|
|
118
118
|
byQuery.set(name, set);
|
|
119
|
-
const row = set.get(
|
|
120
|
-
|
|
121
|
-
|
|
119
|
+
const row = set.get(live) ?? {
|
|
120
|
+
key: live.getAttribute(ATTR_KEY_VALUE),
|
|
121
|
+
values: {},
|
|
122
|
+
};
|
|
123
|
+
set.set(live, row);
|
|
124
|
+
gatherInto(row.values, column, el);
|
|
122
125
|
nestedControls.push(el);
|
|
123
126
|
}
|
|
124
127
|
continue;
|
|
@@ -128,12 +131,17 @@ function gather(group) {
|
|
|
128
131
|
}
|
|
129
132
|
const rows = {};
|
|
130
133
|
for (const [name, set] of byQuery) {
|
|
131
|
-
rows[name] = [...set
|
|
134
|
+
rows[name] = [...set.values()];
|
|
132
135
|
}
|
|
133
136
|
return { controls, values, rows, nestedControls, own };
|
|
134
137
|
}
|
|
135
138
|
/** The controls each request gathered, so a refusal can find them. */
|
|
136
139
|
const gatheredFrom = new WeakMap();
|
|
140
|
+
/**
|
|
141
|
+
* The rows not yet entered each request carries, which leave their list
|
|
142
|
+
* once it succeeds: the rows its answer lands take their place.
|
|
143
|
+
*/
|
|
144
|
+
const enteredWith = new WeakMap();
|
|
137
145
|
/** A request's round trip came back refused, with the handler's words. */
|
|
138
146
|
class RefusedError extends Error {
|
|
139
147
|
refusal;
|
|
@@ -185,24 +193,6 @@ const insertedFrom = new WeakMap();
|
|
|
185
193
|
const pointed = new WeakMap();
|
|
186
194
|
/** The pointed form each request gathered from, until it succeeds. */
|
|
187
195
|
const savedFrom = new WeakMap();
|
|
188
|
-
/**
|
|
189
|
-
* The `lb-select` checkboxes showing for a query, under the hub an element
|
|
190
|
-
* is in. One parked by `lb-show` is in template content, out of reach, so
|
|
191
|
-
* only a selection the user can see counts.
|
|
192
|
-
*/
|
|
193
|
-
function selectorsFor(el, query) {
|
|
194
|
-
const root = el.closest(HUB_TAG_NAME) ?? el.ownerDocument;
|
|
195
|
-
return [...root.querySelectorAll(`[${ATTR_SELECT}]`)].filter((box) => {
|
|
196
|
-
const live = liveRowOf(box);
|
|
197
|
-
return live !== null && queryOf(live)?.getAttribute(ATTR_QUERY) === query;
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
/** The keys of the rows a query's showing checkboxes select. */
|
|
201
|
-
function selectedKeys(boxes) {
|
|
202
|
-
return boxes
|
|
203
|
-
.filter((box) => box.checked)
|
|
204
|
-
.map((box) => liveRowOf(box).getAttribute(ATTR_KEY_VALUE));
|
|
205
|
-
}
|
|
206
196
|
/**
|
|
207
197
|
* Complete a request from where its element sits. Every request passes
|
|
208
198
|
* through here, whoever issued it; a field it already carries is its
|
|
@@ -225,6 +215,15 @@ function complete(request, el) {
|
|
|
225
215
|
const group = groupFor(el);
|
|
226
216
|
const { controls, values, rows, nestedControls, own } = gather(group);
|
|
227
217
|
gatheredFrom.set(request, [...controls, ...nestedControls]);
|
|
218
|
+
const fresh = new Set();
|
|
219
|
+
for (const control of nestedControls) {
|
|
220
|
+
const live = liveRowOf(control);
|
|
221
|
+
if (live?.getAttribute(ATTR_KEY_VALUE) === "")
|
|
222
|
+
fresh.add(live);
|
|
223
|
+
}
|
|
224
|
+
if (fresh.size > 0 && name !== REQUEST_ROW_DELETE) {
|
|
225
|
+
enteredWith.set(request, [...fresh]);
|
|
226
|
+
}
|
|
228
227
|
if (request.values === undefined && controls.length > 0) {
|
|
229
228
|
request.values = values;
|
|
230
229
|
}
|
|
@@ -263,7 +262,7 @@ function complete(request, el) {
|
|
|
263
262
|
}
|
|
264
263
|
// A query the hub serves has one row, keyed by the path, so a request for
|
|
265
264
|
// one from anywhere on the page is for that row.
|
|
266
|
-
if (request.query === URL_QUERY
|
|
265
|
+
if (request.query === URL_QUERY) {
|
|
267
266
|
request.key ??= location.pathname;
|
|
268
267
|
}
|
|
269
268
|
if (name === REQUEST_ROW_INSERT && group?.form) {
|
|
@@ -279,32 +278,19 @@ function complete(request, el) {
|
|
|
279
278
|
const unentered = request.key === "";
|
|
280
279
|
if (unentered) {
|
|
281
280
|
delete request.key;
|
|
281
|
+
// A row `--lb-new` put in a list is deleted by taking it out.
|
|
282
|
+
if (name === REQUEST_ROW_DELETE) {
|
|
283
|
+
const live = liveRowOf(el);
|
|
284
|
+
if (live)
|
|
285
|
+
discardUnentered(live);
|
|
286
|
+
}
|
|
282
287
|
if (name === REQUEST_ROW_UPDATE || name === REQUEST_ROW_DELETE) {
|
|
283
288
|
return false;
|
|
284
289
|
}
|
|
285
290
|
}
|
|
286
|
-
// A request that names no one row acts on the rows the user selected,
|
|
287
|
-
// when its query shows checkboxes to select them by. An insert adds a row
|
|
288
|
-
// and selects nothing. With none ticked, an update or a delete has
|
|
289
|
-
// nothing to act on, and is not sent, as for a row not yet entered.
|
|
290
|
-
if (request.key === undefined &&
|
|
291
|
-
request.keys === undefined &&
|
|
292
|
-
request.query !== undefined &&
|
|
293
|
-
!unentered &&
|
|
294
|
-
name !== REQUEST_ROW_INSERT) {
|
|
295
|
-
const boxes = selectorsFor(el, request.query);
|
|
296
|
-
if (boxes.length > 0) {
|
|
297
|
-
request.keys = selectedKeys(boxes);
|
|
298
|
-
if (request.keys.length === 0 &&
|
|
299
|
-
(name === REQUEST_ROW_UPDATE || name === REQUEST_ROW_DELETE)) {
|
|
300
|
-
return false;
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
291
|
const needs = NEEDS[name];
|
|
305
292
|
if (needs) {
|
|
306
|
-
const missing = needs.filter((field) =>
|
|
307
|
-
!(field === "key" && request.keys !== undefined)) ||
|
|
293
|
+
const missing = needs.filter((field) => request[field] === undefined ||
|
|
308
294
|
(field === "values" &&
|
|
309
295
|
Object.keys(request.values).length === 0 &&
|
|
310
296
|
request.rows === undefined));
|
|
@@ -396,9 +382,18 @@ function pageHostFor(page) {
|
|
|
396
382
|
* browser is showing rides after it verbatim, so every round trip reaches
|
|
397
383
|
* the server with the query parms in hand.
|
|
398
384
|
*/
|
|
399
|
-
async function fetchItems(page, request) {
|
|
385
|
+
async function fetchItems(page, request, parms) {
|
|
400
386
|
const path = page.split("/").map(encodeURIComponent).join("/");
|
|
401
|
-
|
|
387
|
+
// Parms set for this request alone, as a bound row's key is for the lists
|
|
388
|
+
// inside it. An empty one is still sent: it names no row.
|
|
389
|
+
let query = location.search;
|
|
390
|
+
if (parms) {
|
|
391
|
+
const search = new URLSearchParams(location.search);
|
|
392
|
+
for (const [name, value] of Object.entries(parms))
|
|
393
|
+
search.set(name, value);
|
|
394
|
+
query = `?${search.toString()}`;
|
|
395
|
+
}
|
|
396
|
+
const url = `${LB_ENDPOINT}/${path}${query}`;
|
|
402
397
|
const controller = new AbortController();
|
|
403
398
|
const timeout = setTimeout(() => controller.abort(), LB_REQUEST_TIMEOUT_MS);
|
|
404
399
|
try {
|
|
@@ -492,7 +487,7 @@ function clear(form) {
|
|
|
492
487
|
*/
|
|
493
488
|
function point(form, row, key, live) {
|
|
494
489
|
clear(form);
|
|
495
|
-
if (row)
|
|
490
|
+
if (row !== null)
|
|
496
491
|
applyRow(form, row);
|
|
497
492
|
form.setAttribute(ATTR_KEY_VALUE, key);
|
|
498
493
|
live?.setAttribute(ATTR_ROW_EDITING, "");
|
|
@@ -575,7 +570,7 @@ class LbHub extends HTMLElement {
|
|
|
575
570
|
* lands, so each custom element in it is filled before it is attached.
|
|
576
571
|
*/
|
|
577
572
|
unplaced = [];
|
|
578
|
-
/** The hub's own state, as the query `lb-hub` serves it. */
|
|
573
|
+
/** The hub's own state, as the query `lb-hub-status` serves it. */
|
|
579
574
|
trips = 0;
|
|
580
575
|
loadState = HUB_LOAD_LOADING;
|
|
581
576
|
serverState = "";
|
|
@@ -597,10 +592,18 @@ class LbHub extends HTMLElement {
|
|
|
597
592
|
// The element that issued the request, not e.currentTarget (the hub
|
|
598
593
|
// itself, since the event bubbles).
|
|
599
594
|
const origin = e.target ?? undefined;
|
|
600
|
-
if (request.query
|
|
595
|
+
if (request.query === URL_QUERY) {
|
|
601
596
|
void this.answer(request, origin);
|
|
602
597
|
return;
|
|
603
598
|
}
|
|
599
|
+
if (request.query === SORT_QUERY) {
|
|
600
|
+
void this.chooseSort(request, origin);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
if (request.query === FILTERS_QUERY) {
|
|
604
|
+
void this.chooseFilters(request, origin);
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
604
607
|
void this.send(request, origin);
|
|
605
608
|
});
|
|
606
609
|
// A form commits on submit. The submitter's own lb-request replaces the
|
|
@@ -641,14 +644,6 @@ class LbHub extends HTMLElement {
|
|
|
641
644
|
edited(e.target);
|
|
642
645
|
}, { capture: true });
|
|
643
646
|
}
|
|
644
|
-
// A selector selects its row, and is no column.
|
|
645
|
-
this.addEventListener("change", (e) => {
|
|
646
|
-
const box = e.target;
|
|
647
|
-
if (box?.hasAttribute(ATTR_SELECT)) {
|
|
648
|
-
select(box, box.checked);
|
|
649
|
-
}
|
|
650
|
-
});
|
|
651
|
-
// A control commits on change.
|
|
652
647
|
this.addEventListener("change", (e) => {
|
|
653
648
|
const el = e.target;
|
|
654
649
|
if (!el || !el.hasAttribute(ATTR_REQUEST) || !isControl(el))
|
|
@@ -702,8 +697,19 @@ class LbHub extends HTMLElement {
|
|
|
702
697
|
return;
|
|
703
698
|
}
|
|
704
699
|
const form = e.target;
|
|
700
|
+
// New aimed at a list, not a form: a row not yet entered joins it.
|
|
701
|
+
if (command === COMMAND_NEW &&
|
|
702
|
+
form.localName !== "form" &&
|
|
703
|
+
form.hasAttribute(ATTR_QUERY)) {
|
|
704
|
+
if (!addUnentered(form)) {
|
|
705
|
+
console.error(`lb-hub: '${command}' is for a list holding a row template, ` +
|
|
706
|
+
`and <${form.localName}> holds none, ignoring`, form);
|
|
707
|
+
}
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
705
710
|
if (form.localName !== "form" || !form.hasAttribute(ATTR_QUERY)) {
|
|
706
|
-
|
|
711
|
+
const what = command === COMMAND_NEW ? `a <form> or a list` : `a <form>`;
|
|
712
|
+
console.error(`lb-hub: '${command}' is for ${what} carrying ${ATTR_QUERY}, ` +
|
|
707
713
|
`not <${form.localName}>, ignoring`, form);
|
|
708
714
|
return;
|
|
709
715
|
}
|
|
@@ -746,10 +752,12 @@ class LbHub extends HTMLElement {
|
|
|
746
752
|
return;
|
|
747
753
|
}
|
|
748
754
|
point(form, row, key, live);
|
|
755
|
+
void this.readNested(form, key);
|
|
749
756
|
return;
|
|
750
757
|
}
|
|
751
758
|
case COMMAND_NEW:
|
|
752
759
|
point(form, null, "", null);
|
|
760
|
+
void this.readNested(form, "");
|
|
753
761
|
return;
|
|
754
762
|
case COMMAND_RELEASE:
|
|
755
763
|
release(form);
|
|
@@ -759,6 +767,36 @@ class LbHub extends HTMLElement {
|
|
|
759
767
|
`names no command Loadbare provides, ignoring`);
|
|
760
768
|
}
|
|
761
769
|
}
|
|
770
|
+
/**
|
|
771
|
+
* Make the lists nested in a form follow the row it is now bound to: drop
|
|
772
|
+
* the rows not yet entered there, and ask the server for each list's
|
|
773
|
+
* query again with the row's key as the parm its key column names. A new
|
|
774
|
+
* row's key is empty, so its lists answer with none. A form bound to
|
|
775
|
+
* another row, or a page moved on, before the answer returns lands
|
|
776
|
+
* nothing.
|
|
777
|
+
*/
|
|
778
|
+
async readNested(form, key) {
|
|
779
|
+
const lists = [...form.querySelectorAll(`[${ATTR_QUERY}]`)].filter((el) => queryOf(el) === form && templateIn(el) !== null);
|
|
780
|
+
if (lists.length === 0)
|
|
781
|
+
return;
|
|
782
|
+
const column = keptAnswer(this, form.getAttribute(ATTR_QUERY))?.key;
|
|
783
|
+
if (column === undefined)
|
|
784
|
+
return;
|
|
785
|
+
for (const list of lists) {
|
|
786
|
+
for (const row of list.querySelectorAll(`[${ATTR_KEY_VALUE}=""]`)) {
|
|
787
|
+
if (queryOf(row) === list)
|
|
788
|
+
discardUnentered(row);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
const queries = [...new Set(lists.map((el) => el.getAttribute(ATTR_QUERY)))];
|
|
792
|
+
const at = here();
|
|
793
|
+
const items = await this.roundTrip(this.page, { name: REQUEST_READ, queries }, undefined, { [column]: key });
|
|
794
|
+
if (items instanceof Error || here() !== at)
|
|
795
|
+
return;
|
|
796
|
+
if (form.getAttribute(ATTR_KEY_VALUE) !== key)
|
|
797
|
+
return;
|
|
798
|
+
applyResponse(this, items);
|
|
799
|
+
}
|
|
762
800
|
/**
|
|
763
801
|
* Place a list's rows by the order and the filter again, from the rows
|
|
764
802
|
* the hub holds: no round trip, since the user asks only to see what they
|
|
@@ -792,6 +830,9 @@ class LbHub extends HTMLElement {
|
|
|
792
830
|
this.refused(request, items, origin);
|
|
793
831
|
if (items instanceof Error)
|
|
794
832
|
return done(origin, request, items);
|
|
833
|
+
// The rows not yet entered are on record now, and the answer lands them.
|
|
834
|
+
for (const row of enteredWith.get(request) ?? [])
|
|
835
|
+
discardUnentered(row);
|
|
795
836
|
let landed = [];
|
|
796
837
|
// The server answered for the URL the request was sent from. A user who
|
|
797
838
|
// moved while it was in flight, to another page or to other parms, is
|
|
@@ -807,11 +848,6 @@ class LbHub extends HTMLElement {
|
|
|
807
848
|
else
|
|
808
849
|
landed = await this.follow(url, load, origin);
|
|
809
850
|
}
|
|
810
|
-
// The selected rows have been acted on, and the selection is done.
|
|
811
|
-
if (request.keys !== undefined && request.query !== undefined) {
|
|
812
|
-
for (const box of selectorsFor(this, request.query))
|
|
813
|
-
select(box, false);
|
|
814
|
-
}
|
|
815
851
|
// A pointed form has done what it was pointed for, and lets go. A form
|
|
816
852
|
// an insert was gathered from starts over, unless the answer landed the
|
|
817
853
|
// new row on it.
|
|
@@ -851,7 +887,7 @@ class LbHub extends HTMLElement {
|
|
|
851
887
|
* element that issued it carries `lb-request-pending` while it is in
|
|
852
888
|
* flight, and `lb-request-error` when it fails. The error on failure.
|
|
853
889
|
*/
|
|
854
|
-
async roundTrip(page, request, origin) {
|
|
890
|
+
async roundTrip(page, request, origin, parms) {
|
|
855
891
|
// A page load says how the page showing got its data, unless the URL
|
|
856
892
|
// has moved on by the time it returns, and a later load says instead.
|
|
857
893
|
const load = request === undefined;
|
|
@@ -862,7 +898,7 @@ class LbHub extends HTMLElement {
|
|
|
862
898
|
this.loadState = HUB_LOAD_LOADING;
|
|
863
899
|
this.landHub();
|
|
864
900
|
try {
|
|
865
|
-
const items = await fetchItems(page, request);
|
|
901
|
+
const items = await fetchItems(page, request, parms);
|
|
866
902
|
this.serverState = HUB_SERVER_REACHABLE;
|
|
867
903
|
this.error = "";
|
|
868
904
|
this.asOf = new Date().toISOString();
|
|
@@ -923,19 +959,15 @@ class LbHub extends HTMLElement {
|
|
|
923
959
|
if (!this.querySelector(`[${ATTR_QUERY}="${HUB_QUERY}"]`))
|
|
924
960
|
return;
|
|
925
961
|
applyResponse(this, [
|
|
926
|
-
{ query: HUB_QUERY, kind: KIND_ROW, key: URL_COLUMN_PATH, row },
|
|
962
|
+
{ query: HUB_QUERY, kind: KIND_ROW, key: URL_COLUMN_PATH, rows: [row] },
|
|
927
963
|
]);
|
|
928
964
|
}
|
|
929
965
|
/**
|
|
930
|
-
* Answer a request for
|
|
931
|
-
*
|
|
932
|
-
* `lb-hub` too, which nothing but the hub writes.
|
|
966
|
+
* Answer a request for `lb-url`, the one query the hub serves, with no
|
|
967
|
+
* round trip: write the URL and load the page it names.
|
|
933
968
|
*/
|
|
934
969
|
async answer(request, origin) {
|
|
935
|
-
if (request.
|
|
936
|
-
return done(origin, request, [this.choose(request.values ?? {})]);
|
|
937
|
-
}
|
|
938
|
-
if (request.query !== URL_QUERY || request.name !== REQUEST_ROW_UPDATE) {
|
|
970
|
+
if (request.name !== REQUEST_ROW_UPDATE) {
|
|
939
971
|
const message = `lb-hub: '${request.name}' for '${request.query}' is not a request ` +
|
|
940
972
|
`the hub answers, ignoring`;
|
|
941
973
|
console.error(message);
|
|
@@ -948,50 +980,64 @@ class LbHub extends HTMLElement {
|
|
|
948
980
|
done(origin, request, await this.navigate(origin));
|
|
949
981
|
}
|
|
950
982
|
/**
|
|
951
|
-
*
|
|
952
|
-
*
|
|
953
|
-
*
|
|
954
|
-
*
|
|
983
|
+
* Answer an order the user chose: land its row of `lb-sort`, which places
|
|
984
|
+
* the rows it holds again by the new order, and send the order to the
|
|
985
|
+
* server to keep for the next page. Re-sorting is the browser's, from the
|
|
986
|
+
* rows it holds, so the round trip only persists the change.
|
|
955
987
|
*/
|
|
956
|
-
|
|
957
|
-
const
|
|
958
|
-
|
|
959
|
-
...values,
|
|
960
|
-
[URL_COLUMN_PATH]: location.pathname,
|
|
961
|
-
};
|
|
988
|
+
async chooseSort(request, origin) {
|
|
989
|
+
const query = request.key ?? "";
|
|
990
|
+
const order = request.values?.[SORT_COLUMN_ORDER] ?? "";
|
|
962
991
|
const item = {
|
|
963
|
-
query:
|
|
964
|
-
kind:
|
|
965
|
-
key:
|
|
966
|
-
|
|
992
|
+
query: SORT_QUERY,
|
|
993
|
+
kind: KIND_ROWS,
|
|
994
|
+
key: SORT_COLUMN_QUERY,
|
|
995
|
+
rows: [{ [SORT_COLUMN_QUERY]: query, [SORT_COLUMN_ORDER]: order }],
|
|
967
996
|
};
|
|
968
997
|
applyResponse(this, [item]);
|
|
969
|
-
|
|
970
|
-
|
|
998
|
+
const items = await this.roundTrip(this.page, {
|
|
999
|
+
name: REQUEST_ROW_UPDATE,
|
|
1000
|
+
query: SORT_QUERY,
|
|
1001
|
+
key: query,
|
|
1002
|
+
values: { [SORT_COLUMN_ORDER]: order },
|
|
1003
|
+
}, undefined);
|
|
1004
|
+
if (!(items instanceof Error))
|
|
1005
|
+
applyResponse(this, items, { requested: true });
|
|
1006
|
+
done(origin, request, [item]);
|
|
971
1007
|
}
|
|
972
1008
|
/**
|
|
973
|
-
*
|
|
974
|
-
* `lb-
|
|
975
|
-
*
|
|
976
|
-
*
|
|
1009
|
+
* Answer a filter the user chose: merge it into the query's row of
|
|
1010
|
+
* `lb-filters`, land it, and send the whole row to the server to keep for
|
|
1011
|
+
* the next page. Filtering is the browser's, so no round trip answers the
|
|
1012
|
+
* change; the round trip only persists it.
|
|
977
1013
|
*/
|
|
978
|
-
async
|
|
979
|
-
const
|
|
980
|
-
const values = {};
|
|
981
|
-
|
|
982
|
-
|
|
1014
|
+
async chooseFilters(request, origin) {
|
|
1015
|
+
const query = request.key ?? "";
|
|
1016
|
+
const values = request.values ?? {};
|
|
1017
|
+
const held = keptAnswer(this, FILTERS_QUERY)?.rows?.find((row) => row[FILTERS_COLUMN_QUERY] === query);
|
|
1018
|
+
const row = { ...held, ...values, [FILTERS_COLUMN_QUERY]: query };
|
|
1019
|
+
const item = {
|
|
1020
|
+
query: FILTERS_QUERY,
|
|
1021
|
+
kind: KIND_ROWS,
|
|
1022
|
+
key: FILTERS_COLUMN_QUERY,
|
|
1023
|
+
rows: [row],
|
|
1024
|
+
};
|
|
1025
|
+
applyResponse(this, [item]);
|
|
1026
|
+
const columns = {};
|
|
1027
|
+
for (const [column, value] of Object.entries(row)) {
|
|
1028
|
+
if (column !== FILTERS_COLUMN_QUERY && typeof value === "string") {
|
|
1029
|
+
columns[column] = value;
|
|
1030
|
+
}
|
|
983
1031
|
}
|
|
984
|
-
const at = here();
|
|
985
1032
|
const items = await this.roundTrip(this.page, {
|
|
986
1033
|
name: REQUEST_ROW_UPDATE,
|
|
987
|
-
query:
|
|
988
|
-
key:
|
|
989
|
-
values,
|
|
1034
|
+
query: FILTERS_QUERY,
|
|
1035
|
+
key: query,
|
|
1036
|
+
values: columns,
|
|
990
1037
|
}, undefined);
|
|
991
|
-
if (items instanceof Error
|
|
992
|
-
return;
|
|
993
|
-
if (items.length > 0)
|
|
1038
|
+
if (!(items instanceof Error))
|
|
994
1039
|
applyResponse(this, items, { requested: true });
|
|
1040
|
+
done(origin, request, [item]);
|
|
995
1041
|
}
|
|
996
1042
|
/**
|
|
997
1043
|
* Write a row for `lb-url` into the URL. A new page pushes a history
|
|
@@ -1018,7 +1064,7 @@ class LbHub extends HTMLElement {
|
|
|
1018
1064
|
* asked for again that fails is not the request's failure.
|
|
1019
1065
|
*/
|
|
1020
1066
|
async follow(item, load, origin) {
|
|
1021
|
-
this.moveTo(item
|
|
1067
|
+
this.moveTo(oneRow(item) ?? {}, origin?.hasAttribute(ATTR_URL_PUSH) ?? false);
|
|
1022
1068
|
if (load.length === 0) {
|
|
1023
1069
|
const again = await this.navigate();
|
|
1024
1070
|
return again instanceof Error ? [item] : [item, ...again];
|
|
@@ -1121,10 +1167,9 @@ class LbHub extends HTMLElement {
|
|
|
1121
1167
|
this.unplaced.push(this.main);
|
|
1122
1168
|
forget(this);
|
|
1123
1169
|
this.entering = true;
|
|
1124
|
-
this.landChoices();
|
|
1125
1170
|
}
|
|
1126
1171
|
this.landUrl(template ? titleOf(template, page) : null);
|
|
1127
|
-
// An app that ships `<body hidden>`
|
|
1172
|
+
// An app that ships `<body hidden>` stays
|
|
1128
1173
|
// invisible until there is something coherent to show.
|
|
1129
1174
|
document.body.hidden = false;
|
|
1130
1175
|
if (!template) {
|
|
@@ -1136,24 +1181,6 @@ class LbHub extends HTMLElement {
|
|
|
1136
1181
|
}
|
|
1137
1182
|
return template;
|
|
1138
1183
|
}
|
|
1139
|
-
/**
|
|
1140
|
-
* The user's choices as a page begins: none made, so every column an
|
|
1141
|
-
* element under `lb-query="lb-choices"` shows lands empty. Choices hold
|
|
1142
|
-
* until the page changes, unless the server restores them.
|
|
1143
|
-
*/
|
|
1144
|
-
landChoices() {
|
|
1145
|
-
const row = {};
|
|
1146
|
-
for (const scope of this.querySelectorAll(`[${ATTR_QUERY}="${CHOICES_QUERY}"]`)) {
|
|
1147
|
-
for (const el of within(scope, `[${ATTR_COLUMN}]`)) {
|
|
1148
|
-
if (el !== scope)
|
|
1149
|
-
row[el.getAttribute(ATTR_COLUMN)] = "";
|
|
1150
|
-
}
|
|
1151
|
-
}
|
|
1152
|
-
row[URL_COLUMN_PATH] = location.pathname;
|
|
1153
|
-
applyResponse(this, [
|
|
1154
|
-
{ query: CHOICES_QUERY, kind: KIND_ROW, key: URL_COLUMN_PATH, row },
|
|
1155
|
-
]);
|
|
1156
|
-
}
|
|
1157
1184
|
/**
|
|
1158
1185
|
* The hub's own query: one row, keyed by the path, whose other columns are
|
|
1159
1186
|
* the query parms.
|
|
@@ -1183,7 +1210,7 @@ class LbHub extends HTMLElement {
|
|
|
1183
1210
|
row[URL_COLUMN_PATH] = location.pathname;
|
|
1184
1211
|
row[URL_COLUMN_PAGE_LABEL] = label;
|
|
1185
1212
|
row[URL_COLUMN_PAGE_UNKNOWN] = label === null;
|
|
1186
|
-
applyResponse(this, [{ query: URL_QUERY, kind: KIND_ROW, key: URL_COLUMN_PATH, row }]);
|
|
1213
|
+
applyResponse(this, [{ query: URL_QUERY, kind: KIND_ROW, key: URL_COLUMN_PATH, rows: [row] }]);
|
|
1187
1214
|
}
|
|
1188
1215
|
}
|
|
1189
1216
|
customElements.define(HUB_TAG_NAME, LbHub);
|