@loadbare/app 0.22.0 → 0.24.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.
Files changed (45) hide show
  1. package/dist/build/assemble.d.ts.map +1 -1
  2. package/dist/build/assemble.js +28 -1
  3. package/dist/build/assemble.js.map +1 -1
  4. package/dist/build/build.d.ts +16 -0
  5. package/dist/build/build.d.ts.map +1 -1
  6. package/dist/build/build.js +77 -5
  7. package/dist/build/build.js.map +1 -1
  8. package/dist/core/lb-constants.d.ts +8 -5
  9. package/dist/core/lb-constants.d.ts.map +1 -1
  10. package/dist/core/lb-constants.js +20 -11
  11. package/dist/core/lb-constants.js.map +1 -1
  12. package/dist/core/lb-types.d.ts +19 -3
  13. package/dist/core/lb-types.d.ts.map +1 -1
  14. package/dist/core/lb-types.js +35 -5
  15. package/dist/core/lb-types.js.map +1 -1
  16. package/dist/hub/lb-apply.d.ts +18 -22
  17. package/dist/hub/lb-apply.d.ts.map +1 -1
  18. package/dist/hub/lb-apply.js +127 -57
  19. package/dist/hub/lb-apply.js.map +1 -1
  20. package/dist/hub/lb-hub.browser.d.ts.map +1 -1
  21. package/dist/hub/lb-hub.browser.js +15 -22
  22. package/dist/hub/lb-hub.browser.js.map +1 -1
  23. package/dist/server/lb-server.d.ts +2 -2
  24. package/dist/server/lb-server.d.ts.map +1 -1
  25. package/dist/server/lb-server.js +29 -9
  26. package/dist/server/lb-server.js.map +1 -1
  27. package/docs/comparison.md +11 -11
  28. package/docs/miswarts.md +7 -5
  29. package/docs/reference/chrome.md +42 -0
  30. package/docs/reference/custom-elements.md +9 -4
  31. package/docs/reference/data-binding.md +89 -67
  32. package/docs/reference/dev-server.md +10 -3
  33. package/docs/reference/names.md +10 -7
  34. package/docs/reference/page-files.md +8 -4
  35. package/docs/temp-proposals.md +251 -0
  36. package/docs/testing.md +3 -3
  37. package/package.json +1 -1
  38. package/skills/loadbare-app/SKILL.md +24 -12
  39. package/skills/loadbare-app/references/chrome.md +42 -0
  40. package/skills/loadbare-app/references/custom-elements.md +9 -4
  41. package/skills/loadbare-app/references/data-binding.md +89 -67
  42. package/skills/loadbare-app/references/dev-server.md +10 -3
  43. package/skills/loadbare-app/references/miswarts.md +7 -5
  44. package/skills/loadbare-app/references/names.md +10 -7
  45. package/skills/loadbare-app/references/page-files.md +8 -4
@@ -38,7 +38,7 @@
38
38
  * landing reaches in there as it reaches everywhere else, so a branch that
39
39
  * returns is already current. Nothing but landing reaches in.
40
40
  */
41
- import { AGGREGATES, ATTR_COLUMN, ATTR_FILTER, ATTR_ORDER, CHOICES_QUERY, LB_RESERVED_PREFIX, ATTR_COLUMN_VALUE, ATTR_COUNT, ATTR_GROUP, ATTR_GROUP_COLUMN, ATTR_GROUP_LEAVING, ATTR_GROUP_LIVE, ATTR_GROUP_VALUE, ATTR_KEY_VALUE, ATTR_MAX, ATTR_MIN, ATTR_QUERY, ATTR_QUERY_ORDER, ATTR_QUERY_ROW_COUNT, ATTR_QUERY_UNSORTED_COUNT, ATTR_ROW_CHANGED, ATTR_ROW_CREATED, ATTR_ROW_LEAVING, ATTR_ROW_LIVE, ATTR_ROW_MOVED, ATTR_ROW_REQUESTED, ATTR_ROW_UNSORTED, ATTR_SHOW, ATTR_SUM, ATTR_URL_PARM, KIND_ROW, LIVE_GROUP, LIVE_ROW, ORDER_DESCENDING, REQUESTED_CHANGED, REQUESTED_CREATED, REQUESTED_MOVED, SHOW_NOT, ATTR_SELECT, ATTR_ROW_SELECTED, } from "../core/lb-constants.js";
41
+ import { AGGREGATES, ATTR_COLUMN, ATTR_FILTER, ATTR_ORDER, CHOICES_QUERY, LB_RESERVED_PREFIX, ATTR_COLUMN_VALUE, ATTR_COUNT, ATTR_GROUP, ATTR_GROUP_COLUMN, ATTR_GROUP_LEAVING, ATTR_GROUP_LIVE, ATTR_GROUP_VALUE, ATTR_KEY_VALUE, ATTR_MAX, ATTR_MIN, ATTR_QUERY, ATTR_QUERY_ORDER, ATTR_QUERY_ROW_COUNT, ATTR_MANUAL, MANUAL_FILTER, MANUAL_ORDER, ATTR_QUERY_UNPLACED_COUNT, ATTR_ROW_CHANGED, ATTR_ROW_CREATED, ATTR_ROW_LEAVING, ATTR_ROW_LIVE, ATTR_ROW_MOVED, ATTR_ROW_REQUESTED, ATTR_ROW_UNPLACED, ATTR_SHOW, ATTR_SUM, ATTR_URL_PARM, KIND_ROW, LIVE_GROUP, LIVE_ROW, ORDER_DESCENDING, REQUESTED_CHANGED, REQUESTED_CREATED, REQUESTED_MOVED, SHOW_NOT, ATTR_SELECT, ATTR_ROW_SELECTED, } from "../core/lb-constants.js";
42
42
  import { compareRows, compareValues, isPatch, parseFilter, parseOrder, passes, readAs, } from "../core/lb-types.js";
43
43
  import { attachPlaced, detachRemoved, LEAVING, PARKED, placed, upgradeAbsent, } from "./lb-lifecycle.js";
44
44
  /** A DocumentFragment's nodeType. Tier 3 has no `Node` global to read it from. */
@@ -100,23 +100,58 @@ export function isFormAssociated(el) {
100
100
  export function isControl(el) {
101
101
  return (isNativeControl(el) || isCheckbox(el) || isRadio(el) || isFormAssociated(el));
102
102
  }
103
+ /**
104
+ * A control that holds several values: a `<select multiple>`, or a checkbox
105
+ * carrying a `value`, one of a set naming one column. A checkbox with no
106
+ * `value` is a flag.
107
+ */
108
+ function holdsSeveral(el) {
109
+ if (el.localName === "select")
110
+ return el.multiple;
111
+ return isCheckbox(el) && el.hasAttribute("value");
112
+ }
113
+ /** A landed value as the list of strings a control holding several shows. */
114
+ function listOf(value) {
115
+ if (Array.isArray(value))
116
+ return value.map((v) => String(v));
117
+ if (value === null || value === undefined || value === "")
118
+ return [];
119
+ return [String(value)];
120
+ }
103
121
  /**
104
122
  * What a control gathers: a checkbox `"true"` or `"false"`, so unchecked
105
- * still says so, and any other control its `value`.
123
+ * still says so, a `<select multiple>` the values of its selected options,
124
+ * and any other control its `value`, which a custom element may give as
125
+ * several.
106
126
  */
107
127
  function controlValue(el) {
108
- return isCheckbox(el) ? String(el.checked) : el.value;
128
+ if (isCheckbox(el))
129
+ return String(el.checked);
130
+ if (el.localName === "select" && el.multiple) {
131
+ return [...el.selectedOptions].map((o) => o.value);
132
+ }
133
+ const value = el.value;
134
+ return Array.isArray(value) ? value.map((v) => String(v)) : el.value;
109
135
  }
110
136
  /**
111
137
  * Gather a control into `values` under its column. Of the radio buttons
112
138
  * naming one column, the one checked gives the value, and `""` stands when
113
- * none is.
139
+ * none is. Of the checkboxes in a set naming one column, the checked ones
140
+ * give their values, and none checked gives none.
114
141
  */
115
142
  export function gatherInto(values, column, el) {
116
143
  if (isRadio(el) && !el.checked) {
117
144
  values[column] ??= "";
118
145
  return;
119
146
  }
147
+ if (isCheckbox(el) && holdsSeveral(el)) {
148
+ const had = values[column];
149
+ const set = Array.isArray(had) ? had : [];
150
+ values[column] = set;
151
+ if (el.checked)
152
+ set.push(el.value);
153
+ return;
154
+ }
120
155
  values[column] = controlValue(el);
121
156
  }
122
157
  /**
@@ -126,6 +161,12 @@ export function gatherInto(values, column, el) {
126
161
  * text content is set. Every one carries the value as `lb-column-value`.
127
162
  */
128
163
  function land(el, value) {
164
+ if (holdsSeveral(el))
165
+ return landSeveral(el, value);
166
+ // A custom element control takes several values as they are. Anything
167
+ // else holds one, and shows the first, as a URL's parm reads.
168
+ if (Array.isArray(value) && !isFormAssociated(el))
169
+ value = value[0];
129
170
  // Untouched, whatever the server sent: the browser decides what a
130
171
  // non-string looks like. See Row in core/lb-types.ts.
131
172
  if (value === null || value === undefined) {
@@ -141,9 +182,11 @@ function land(el, value) {
141
182
  return;
142
183
  }
143
184
  if (isCheckbox(el)) {
144
- // Checked by the rule `lb-show` is on by: only null and false are off,
145
- // and no string is read.
146
- el.checked = value !== false;
185
+ // Off for false, and for what an unchecked checkbox gathers or an absent
186
+ // query parm lands as, so a checkbox lands what it gathered even from a
187
+ // query whose values are strings, such as lb-url.
188
+ el.checked =
189
+ value !== false && value !== "false" && value !== "";
147
190
  }
148
191
  else if (isRadio(el)) {
149
192
  el.checked =
@@ -161,6 +204,26 @@ function land(el, value) {
161
204
  }
162
205
  setStamp(el, ATTR_COLUMN_VALUE, String(value));
163
206
  }
207
+ /**
208
+ * Set a control that holds several values: a checkbox in a set is checked
209
+ * when its `value` is among them, and a `<select multiple>` selects each
210
+ * option whose `value` is.
211
+ */
212
+ function landSeveral(el, value) {
213
+ const list = listOf(value);
214
+ if (isCheckbox(el)) {
215
+ el.checked = list.includes(el.value);
216
+ }
217
+ else {
218
+ for (const option of el.options) {
219
+ option.selected = list.includes(option.value);
220
+ }
221
+ }
222
+ if (list.length === 0)
223
+ el.removeAttribute(ATTR_COLUMN_VALUE);
224
+ else
225
+ setStamp(el, ATTR_COLUMN_VALUE, JSON.stringify(list));
226
+ }
164
227
  /**
165
228
  * Set an element's text, unless it already holds exactly that text. Writing
166
229
  * the same text again replaces the text node, which is two mutations and a
@@ -658,7 +721,7 @@ function listFor(el, template) {
658
721
  data: {},
659
722
  placed: {},
660
723
  hidden: false,
661
- unsorted: false,
724
+ unplaced: false,
662
725
  };
663
726
  list.byKey.set(key, entry);
664
727
  list.root.rows.push(entry);
@@ -698,7 +761,7 @@ function create(list, doc, key) {
698
761
  data: {},
699
762
  placed: {},
700
763
  hidden: false,
701
- unsorted: false,
764
+ unplaced: false,
702
765
  };
703
766
  list.byKey.set(key, entry);
704
767
  return entry;
@@ -1014,7 +1077,8 @@ function hide(el, hidden) {
1014
1077
  }
1015
1078
  /**
1016
1079
  * Finish a group and everything under it, from the rows it shows: each
1017
- * heading filled from the first, as it was when last placed by the order,
1080
+ * heading filled and linked from the first, as it was when last placed by
1081
+ * the order,
1018
1082
  * each aggregate set from all of them, and a group hidden when a filter hides
1019
1083
  * every row in it. The rows it shows are returned.
1020
1084
  */
@@ -1032,9 +1096,12 @@ function finish(group, types) {
1032
1096
  }
1033
1097
  }
1034
1098
  }
1035
- if (shown[0])
1036
- for (const head of group.heads)
1099
+ if (shown[0]) {
1100
+ for (const head of group.heads) {
1037
1101
  fill(head, shown[0].placed);
1102
+ link(head, shown[0].placed);
1103
+ }
1104
+ }
1038
1105
  aggregate(group.aggregates, rows, types);
1039
1106
  return shown;
1040
1107
  }
@@ -1130,10 +1197,12 @@ function differs(before, after) {
1130
1197
  * the same rows again moves nothing. A row a filter the user chose does not
1131
1198
  * pass is `hidden`.
1132
1199
  *
1133
- * A landing `held` for the user working in the list moves nothing already
1134
- * showing, whole or a patch. A row it changed or created that the order
1135
- * would put elsewhere, or that the filter would hide, stays and is stamped
1136
- * unsorted until the rows are next placed by the order.
1200
+ * A landing that answers a request leaves alone each side `lb-manual` names
1201
+ * on the element, whole or a patch. Under `order`, nothing already showing
1202
+ * moves, and a new row goes last in its group. Under `filter`, a row the
1203
+ * answer changed or created stays showing. A row left where the order or
1204
+ * the filter would not leave it is stamped unplaced until the rows are next
1205
+ * placed.
1137
1206
  */
1138
1207
  export function applyRows(el, template, keyColumn, result, options = {}) {
1139
1208
  const list = listFor(el, template);
@@ -1218,39 +1287,39 @@ export function applyRows(el, template, keyColumn, result, options = {}) {
1218
1287
  a.i - b.i)
1219
1288
  .map(({ entry }) => entry);
1220
1289
  const passing = filterFor(el, options.choices, options.types ?? {});
1290
+ const manual = options.requested ? manualOf(el) : NO_MANUAL;
1291
+ const touched = new Set([...changed, ...created]);
1292
+ // A row left out of place stays touched until the rows are placed again.
1293
+ for (const entry of shown)
1294
+ if (entry.unplaced)
1295
+ touched.add(entry);
1221
1296
  let order;
1222
- if (options.held) {
1223
- // The user is working in this list: what already shows stays where it
1224
- // stands, and what is new goes last in the group it belongs to.
1297
+ let out = new Set();
1298
+ if (manual.order) {
1299
+ // What already shows stays where it stands, and what is new goes last
1300
+ // in the group it belongs to.
1225
1301
  for (const entry of created)
1226
1302
  entry.placed = entry.data;
1227
1303
  order = heldOrder(stayed, created, groupTerms(list, terms));
1228
- const touched = new Set([...changed, ...created]);
1229
- for (const entry of order)
1230
- if (entry.unsorted)
1231
- touched.add(entry);
1232
- const out = movedRows(order, sort(order), touched);
1233
- for (const entry of order) {
1234
- const fails = !passing(entry.data);
1235
- // A row the user just changed does not vanish from under them.
1236
- const kept = touched.has(entry) && !entry.hidden;
1237
- filterRow(entry, fails && !kept);
1238
- // A row stands in the group it was placed in until it is placed again.
1239
- const moves = regrouped(entry, list, terms);
1240
- if (!moves)
1241
- entry.placed = entry.data;
1242
- entry.unsorted = out.has(entry) || (kept && fails) || moves;
1243
- }
1304
+ out = movedRows(order, sort(order), touched);
1244
1305
  }
1245
1306
  else {
1246
1307
  order = sort(whole
1247
1308
  ? [...arriving.keys()].map((key) => list.byKey.get(key))
1248
1309
  : [...stayed, ...created]);
1249
- for (const entry of order) {
1310
+ }
1311
+ for (const entry of order) {
1312
+ const fails = !passing(entry.data);
1313
+ // A row the answer touched does not vanish from under the user.
1314
+ const kept = manual.filter && touched.has(entry) && !entry.hidden && fails;
1315
+ const hides = fails && !kept;
1316
+ filterRow(entry, hides);
1317
+ // A row stands in the group it was placed in until it is placed again.
1318
+ const moves = manual.order && regrouped(entry, list, terms);
1319
+ if (!moves)
1250
1320
  entry.placed = entry.data;
1251
- entry.unsorted = false;
1252
- filterRow(entry, !passing(entry.data));
1253
- }
1321
+ // A row nobody sees stands nowhere the user would notice.
1322
+ entry.unplaced = !hides && (out.has(entry) || kept || moves);
1254
1323
  }
1255
1324
  const moved = movedRows(stayed, order, changed);
1256
1325
  const retired = arrange(list, doc, order, terms);
@@ -1275,18 +1344,18 @@ export function applyRows(el, template, keyColumn, result, options = {}) {
1275
1344
  }
1276
1345
  }
1277
1346
  stamps.done(doc);
1278
- let unsorted = 0;
1347
+ let unplaced = 0;
1279
1348
  for (const entry of order) {
1280
- if (entry.el.hasAttribute(ATTR_ROW_UNSORTED) !== entry.unsorted) {
1281
- entry.el.toggleAttribute(ATTR_ROW_UNSORTED, entry.unsorted);
1349
+ if (entry.el.hasAttribute(ATTR_ROW_UNPLACED) !== entry.unplaced) {
1350
+ entry.el.toggleAttribute(ATTR_ROW_UNPLACED, entry.unplaced);
1282
1351
  }
1283
- if (entry.unsorted)
1284
- unsorted++;
1352
+ if (entry.unplaced)
1353
+ unplaced++;
1285
1354
  }
1286
1355
  const rows = finish(list.root, options.types ?? {});
1287
1356
  // Counted from what the hub holds, which is what the rows show.
1288
1357
  setStamp(el, ATTR_QUERY_ROW_COUNT, String(rows.length));
1289
- setStamp(el, ATTR_QUERY_UNSORTED_COUNT, String(unsorted));
1358
+ setStamp(el, ATTR_QUERY_UNPLACED_COUNT, String(unplaced));
1290
1359
  if (terms.length === 0)
1291
1360
  el.removeAttribute(ATTR_QUERY_ORDER);
1292
1361
  else
@@ -1298,6 +1367,14 @@ export function applyRows(el, template, keyColumn, result, options = {}) {
1298
1367
  depart(departing);
1299
1368
  host.lbRowsLanded?.();
1300
1369
  }
1370
+ const NO_MANUAL = { order: false, filter: false };
1371
+ function manualOf(el) {
1372
+ const sides = (el.getAttribute(ATTR_MANUAL) ?? "").split(/\s+/);
1373
+ return {
1374
+ order: sides.includes(MANUAL_ORDER),
1375
+ filter: sides.includes(MANUAL_FILTER),
1376
+ };
1377
+ }
1301
1378
  /** The order's terms a list's groups break on, outermost first. */
1302
1379
  function groupTerms(list, terms) {
1303
1380
  return terms.slice(0, list.levels.length);
@@ -1527,13 +1604,6 @@ function fillArrivals(root, state) {
1527
1604
  }
1528
1605
  }
1529
1606
  }
1530
- /**
1531
- * Whether a request from `origin` was sent from inside `el`'s list: `el` is
1532
- * the nearest element carrying `lb-query` around it.
1533
- */
1534
- function sentFrom(el, origin) {
1535
- return origin?.parentElement?.closest(`[${ATTR_QUERY}]`) === el;
1536
- }
1537
1607
  /**
1538
1608
  * Land response items. Every answer arrives through here, and is kept for
1539
1609
  * `root`, so that what arrives after it is filled from it — see forget().
@@ -1557,7 +1627,6 @@ export function applyResponse(root, items, options = {}) {
1557
1627
  landItem(el, item, {
1558
1628
  ...rowsOptions(state, item),
1559
1629
  requested: options.requested,
1560
- held: sentFrom(el, options.origin),
1561
1630
  });
1562
1631
  }
1563
1632
  if (choices.size > 0)
@@ -1567,11 +1636,12 @@ export function applyResponse(root, items, options = {}) {
1567
1636
  attachPlaced();
1568
1637
  }
1569
1638
  /**
1570
- * Place an element's rows by the order again, from the answer kept for its
1571
- * query, with no round trip: what the user asks for once an edit has left
1572
- * rows out of order. False when the element shows no rows the hub holds.
1639
+ * Place an element's rows by the order and the filter again, from the answer
1640
+ * kept for its query, with no round trip: what the user asks for once an
1641
+ * answer has left rows out of place. False when the element shows no rows
1642
+ * the hub holds.
1573
1643
  */
1574
- export function resort(root, el) {
1644
+ export function placeAgain(root, el) {
1575
1645
  const state = kept.get(root);
1576
1646
  const item = state?.answers.get(el.getAttribute(ATTR_QUERY) ?? "");
1577
1647
  if (!state || !item || item.kind === KIND_ROW || !state.landed.has(el)) {