@yuneta/gobj-ui 5.2.0 → 5.3.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.
@@ -40,6 +40,16 @@ import i18next from "i18next";
40
40
 
41
41
  const WIN_NAME = "shell-route-map-window";
42
42
 
43
+ /* Reference rows — a route's second and third entry point: a drawer
44
+ * entry, a toolbar SHORTCUT. Shown by default: they are one line
45
+ * each, and they are exactly what someone auditing the navigation
46
+ * came to see ("which quick links does this app define?"). What was
47
+ * ever noisy is the repeated SUBTREE, and that is gone for good
48
+ * (dedupe_subtrees) rather than hidden behind a switch. The toggle
49
+ * stays for reading the bare structure, and remembers the choice for
50
+ * this page. */
51
+ let __show_refs__ = true;
52
+
43
53
  /* Modal fallback currently open (the window flavour is a service and
44
54
  * is found by name; the modal is not, so it is tracked here to make
45
55
  * the second call a TOGGLE for both flavours). */
@@ -81,6 +91,16 @@ function render_node(node)
81
91
  if(node.event) {
82
92
  row.push(["span", {class: "ROUTEMAP_EVENT"}, node.event]);
83
93
  }
94
+ /* A reference: this route's structure is drawn under whichever
95
+ * surface owns it (the model's dedupe keeps one subtree per
96
+ * route). Still a live link — it just does not repeat the
97
+ * branch. */
98
+ if(node.ref) {
99
+ row.push(["span", {class: "ROUTEMAP_REF", i18n: "shown above",
100
+ title: "shown above", "data-i18n-title": "shown above"},
101
+ "shown above"]);
102
+ }
103
+
84
104
  /* "You are here" — the node whose route best matches the current
85
105
  * one (marked by the model; at most one). */
86
106
  if(node.current) {
@@ -88,7 +108,8 @@ function render_node(node)
88
108
  "you are here"]);
89
109
  }
90
110
 
91
- let row_class = node.current ? " ROUTEMAP_CURRENT" : "";
111
+ let row_class = (node.current ? " ROUTEMAP_CURRENT" : "") +
112
+ (node.ref ? " ROUTEMAP_IS_REF" : "");
92
113
  let $row;
93
114
  if(node.route) {
94
115
  $row = createElement2(
@@ -113,29 +134,64 @@ function render_node(node)
113
134
  * descendant matches (keep the ancestor path). Empty `q` shows all.
114
135
  * Matches against the visible row text (name + route + event), so it
115
136
  * honours the current translation. Returns whether the <li> is shown.
137
+ *
138
+ * `show_refs` off hides the reference rows — the second and third
139
+ * place a route is reachable from, which carry no structure. They are
140
+ * hidden by DEFAULT: in an app with a drawer and an account menu they
141
+ * are a third of the map, and what a reader comes here for is the
142
+ * structure. A ref stays hidden even inside a matched subtree — an
143
+ * ancestor match must not smuggle it back in — and a group left with
144
+ * nothing but refs collapses with them, since a group is only ever
145
+ * shown by what it contains.
116
146
  ***************************************************************/
117
- function filter_li($li, q, ancestor_match)
147
+ function filter_li($li, q, ancestor_match, show_refs)
118
148
  {
119
149
  let $row = $li.querySelector(":scope > .ROUTEMAP_ROW");
120
150
  let text = $row ? ($row.textContent || "").toLowerCase() : "";
151
+ let hidden_ref = !show_refs &&
152
+ !!($row && $row.classList.contains("ROUTEMAP_IS_REF"));
121
153
  let self_match = q === "" || text.indexOf(q) >= 0;
122
- let show_all = ancestor_match || self_match;
154
+ let show_all = (ancestor_match || self_match) && !hidden_ref;
123
155
 
124
156
  let any_child = false;
125
157
  let $ul = $li.querySelector(":scope > ul");
126
158
  if($ul) {
127
159
  let kids = $ul.children;
128
160
  for(let i = 0; i < kids.length; i++) {
129
- if(filter_li(kids[i], q, show_all)) {
161
+ if(filter_li(kids[i], q, show_all, show_refs)) {
162
+ any_child = true;
163
+ }
164
+ }
165
+ }
166
+
167
+ let is_group = !!($row && $row.classList.contains("ROUTEMAP_STRUCT"));
168
+ let had_children = !!($ul && $ul.children.length);
169
+
170
+ /* Hiding references removes NOISE, never a navigation surface. A
171
+ * whole menu whose entries are all references — a drawer that
172
+ * reaches the same routes as the primary nav — would otherwise
173
+ * vanish heading and all, and the map would be quietly lying about
174
+ * what the app has. So when hiding would empty a group, its
175
+ * entries are shown anyway: one line each, still without the
176
+ * repeated subtree, which is the part that was actually noisy. */
177
+ if(is_group && had_children && !any_child && !show_refs) {
178
+ let kids = $ul.children;
179
+ for(let i = 0; i < kids.length; i++) {
180
+ if(filter_li(kids[i], q, show_all, true)) {
130
181
  any_child = true;
131
182
  }
132
183
  }
133
184
  }
134
185
 
135
- let visible = show_all || any_child;
186
+ /* A group is only ever shown by what it contains. While a search
187
+ * is running its own heading may still match — a query that finds
188
+ * a name must not come back empty. */
189
+ let visible = (is_group && had_children)
190
+ ? (any_child || (q !== "" && self_match))
191
+ : ((show_all || any_child) && !hidden_ref);
136
192
  $li.style.display = visible ? "" : "none";
137
193
  if($row) {
138
- $row.classList.toggle("ROUTEMAP_MATCH", q !== "" && self_match);
194
+ $row.classList.toggle("ROUTEMAP_MATCH", q !== "" && self_match && visible);
139
195
  }
140
196
  return visible;
141
197
  }
@@ -147,13 +203,23 @@ function filter_li($li, q, ancestor_match)
147
203
  * on a resting-route change. `on_jump()` closes the host for the
148
204
  * one click that navigates nowhere (the current route).
149
205
  ***************************************************************/
150
- function build_body(shell, t, on_jump)
206
+ function build_body(shell, t)
151
207
  {
152
208
  let map = yui_shell_nav_map(shell);
153
- let children = [
154
- {label: "toolbar", icon: "", route: "", event: "",
155
- kind: "group", children: map.toolbar}
156
- ].concat(map.nav);
209
+
210
+ /* STRUCTURE FIRST. The nav leads, then the toolbar, then the
211
+ * route-table leftovers. The toolbar used to come first, which
212
+ * put a shortcut ("Go to Cards", an account-menu entry) above the
213
+ * section it points at: with one subtree per route the shortcut
214
+ * renders as a reference, so the first /cards a reader met had no
215
+ * tree under it and the real one was thirty rows down. It also
216
+ * made the reference's own "shown above" a lie — the owner was
217
+ * BELOW it. Leading with the structure fixes both. */
218
+ let children = [].concat(map.nav);
219
+ if(Array.isArray(map.toolbar) && map.toolbar.length) {
220
+ children.push({label: "toolbar", icon: "", route: "", event: "",
221
+ kind: "group", children: map.toolbar});
222
+ }
157
223
  /* Routes declared only in the route table (config.shell.routes)
158
224
  * that no menu/toolbar item points at — root "/", URL-only action
159
225
  * routes. Rendered last, as their own group. */
@@ -197,15 +263,33 @@ function build_body(shell, t, on_jump)
197
263
  ["span", {class: "ROUTEMAP_COUNT is-size-7 has-text-grey is-hidden"},
198
264
  [$count_n, ["span", {i18n: "matches"}, "matches"]]]
199
265
  );
266
+ /* References are hidden by default and revealed by this toggle.
267
+ * Remembered for the page session (not localStorage): it is a
268
+ * reading preference of the panel, not a position (ROUTING.md §3),
269
+ * and it costs nothing to be back at the useful default tomorrow. */
270
+ let $refs_input = createElement2(
271
+ ["input", {class: "ROUTEMAP_REFS_INPUT", type: "checkbox"}]
272
+ );
273
+ if(__show_refs__) {
274
+ $refs_input.checked = true;
275
+ }
276
+ let $refs_toggle = createElement2(
277
+ ["label", {class: "checkbox is-size-7 ROUTEMAP_REFS_TOGGLE",
278
+ title: "show references", "data-i18n-title": "show references"},
279
+ [$refs_input, ["span", {class: "ml-1", i18n: "show references"},
280
+ "show references"]]]
281
+ );
282
+
200
283
  let $search_row = createElement2(
201
- ["div", {class: "ROUTEMAP_SEARCH_ROW mb-2"}, [$search_ctrl, $count]]
284
+ ["div", {class: "ROUTEMAP_SEARCH_ROW mb-2"},
285
+ [$search_ctrl, $count, $refs_toggle]]
202
286
  );
203
287
 
204
- $search.addEventListener("input", function() {
288
+ let apply_filter = function() {
205
289
  let q = ($search.value || "").trim().toLowerCase();
206
290
  let $root_li = $tree.querySelector(".ROUTEMAP_ROOT > li");
207
291
  if($root_li) {
208
- filter_li($root_li, q, false);
292
+ filter_li($root_li, q, false, __show_refs__);
209
293
  }
210
294
  if(q === "") {
211
295
  $count.classList.add("is-hidden");
@@ -213,8 +297,24 @@ function build_body(shell, t, on_jump)
213
297
  $count_n.textContent = $tree.querySelectorAll(".ROUTEMAP_MATCH").length;
214
298
  $count.classList.remove("is-hidden");
215
299
  }
300
+ };
301
+
302
+ $search.addEventListener("input", apply_filter);
303
+ $refs_input.addEventListener("change", function() {
304
+ __show_refs__ = !!$refs_input.checked;
305
+ apply_filter();
216
306
  });
217
307
 
308
+ /* Nothing to toggle in an app where every route is reachable from
309
+ * exactly one place. */
310
+ if($tree.querySelectorAll(".ROUTEMAP_IS_REF").length === 0) {
311
+ $refs_toggle.classList.add("is-hidden");
312
+ }
313
+
314
+ /* Apply the default (references hidden) before the panel is shown,
315
+ * so it is never painted once with them and once without. */
316
+ apply_filter();
317
+
218
318
  let $body = createElement2(
219
319
  ["div", {class: "C_YUI_SHELL_ROUTEMAP ROUTEMAP_BODY"}, [
220
320
  ["p", {class: "ROUTEMAP_HINT is-size-7 mb-2", i18n: "site map hint"},
@@ -251,34 +351,85 @@ function build_body(shell, t, on_jump)
251
351
  });
252
352
  }
253
353
 
254
- /* A route link jumps there NATIVELY (browser push + hashchange
255
- * no preventDefault): a resting-route change then closes this
256
- * window/modal through the shell's transient-overlay drain, which
257
- * is deterministic (the old close-then-deferred-navigate raced the
258
- * dismissal's history.back() and could land back where it
259
- * started). A subpath or action-route jump keeps the map open —
260
- * it doubles as a navigation panel. Clicking the route the user
261
- * is ALREADY on navigates nowhere: close the host instead.
354
+ /* A route link jumps there NATIVELY (browser push + hashchange, no
355
+ * preventDefault) and the map STAYS OPEN — every row, not just the
356
+ * subpath ones. It is a navigation panel: the user opens it to
357
+ * move around, and having half the rows close it under them made
358
+ * the same gesture mean two different things. The window is
359
+ * either dock-managed or registered with `keep_on_navigate`, so
360
+ * the shell's transient-overlay drain leaves it alone.
262
361
  * Action nodes (no route) are documentation only and do not fire. */
263
- $body.addEventListener("click", function(ev) {
264
- let $link = ev.target && ev.target.closest &&
265
- ev.target.closest(".ROUTEMAP_LINK");
266
- if(!$link) {
362
+
363
+ /* "You are here" has to follow the url now that the panel outlives
364
+ * the navigation, or it would keep pointing at where the user was
365
+ * when they opened it. Self-detaching: the panel's DOM is owned by
366
+ * the window/modal that hosts it, and this helper has no hook into
367
+ * their teardown — once the body leaves the document the listener
368
+ * retires itself instead of leaking one per open. */
369
+ let on_hash = function() {
370
+ if(typeof document === "undefined" || !$body.isConnected) {
371
+ if(typeof window !== "undefined") {
372
+ window.removeEventListener("hashchange", on_hash);
373
+ }
267
374
  return;
268
375
  }
269
- let href = $link.getAttribute("href");
270
- if(href && typeof window !== "undefined" &&
271
- window.location.hash === href &&
272
- typeof on_jump === "function") {
273
- ev.preventDefault();
274
- on_jump();
275
- }
276
- });
376
+ mark_current_row($body, window.location.hash, t);
377
+ };
378
+ if(typeof window !== "undefined") {
379
+ window.addEventListener("hashchange", on_hash);
380
+ }
277
381
 
278
382
  refresh_language($body, t);
279
383
  return $body;
280
384
  }
281
385
 
386
+ /***************************************************************
387
+ * Move the "you are here" mark to the row that best matches
388
+ * `hash`: an exact route wins, else the LONGEST declared route
389
+ * that is a path-prefix of it (the base view of a deep subpath
390
+ * position). Same rule as the model's mark_current(), applied
391
+ * to the rendered rows so the open panel stays truthful.
392
+ ***************************************************************/
393
+ function mark_current_row($body, hash, t)
394
+ {
395
+ let route = String(hash || "").replace(/^#/, "");
396
+ let $best = null;
397
+ let best_len = -1;
398
+
399
+ for(let $a of $body.querySelectorAll(".ROUTEMAP_LINK")) {
400
+ let r = String($a.getAttribute("href") || "").replace(/^#/, "");
401
+ if(!r) {
402
+ continue;
403
+ }
404
+ if(r === route) {
405
+ $best = $a;
406
+ break;
407
+ }
408
+ if(route.indexOf(r + "/") === 0 && r.length > best_len) {
409
+ $best = $a;
410
+ best_len = r.length;
411
+ }
412
+ }
413
+
414
+ for(let $marked of $body.querySelectorAll(".ROUTEMAP_CURRENT")) {
415
+ $marked.classList.remove("ROUTEMAP_CURRENT");
416
+ }
417
+ if(!$best) {
418
+ return;
419
+ }
420
+ $best.classList.add("ROUTEMAP_CURRENT");
421
+
422
+ /* One badge, moved — never a second one left behind on the row the
423
+ * user came from. */
424
+ let $badge = $body.querySelector(".ROUTEMAP_HERE");
425
+ if(!$badge) {
426
+ $badge = createElement2(
427
+ ["span", {class: "ROUTEMAP_HERE", i18n: "you are here"},
428
+ t ? t("you are here") : "you are here"]);
429
+ }
430
+ $best.appendChild($badge);
431
+ }
432
+
282
433
 
283
434
  /***************************************************************
284
435
  * Show the site map. Toggles: a second call closes the open one.
@@ -305,11 +456,7 @@ export function yui_shell_show_route_map(shell, opts)
305
456
  /* Preferred: a resizable, maximisable floating window. */
306
457
  if(gclass_find_by_name("C_YUI_WINDOW") !== null) {
307
458
  let win_ref = {gobj: null};
308
- let $body = build_body(shell, t, function() {
309
- if(win_ref.gobj && is_gobj(win_ref.gobj)) {
310
- gobj_send_event(win_ref.gobj, "EV_CLOSE_WINDOW", {}, shell);
311
- }
312
- });
459
+ let $body = build_body(shell, t);
313
460
  let $parent = yui_shell_popup_layer(shell) ||
314
461
  (typeof document !== "undefined" &&
315
462
  document.getElementById("top-layer")) || null;
@@ -328,7 +475,13 @@ export function yui_shell_show_route_map(shell, opts)
328
475
  title: "site map",
329
476
  icon: "yi-bars",
330
477
  body: $body,
331
- manager: null
478
+ /* A workspace surface, not a thing floating over one view:
479
+ * it joins the dock when the app has one (minimise, restore,
480
+ * focus like any other window). Without a dock it stays a
481
+ * floating overlay, and `keep_on_navigate` gives it the same
482
+ * survives-navigation behaviour there. */
483
+ manager: gobj_find_service("__window_manager__", false),
484
+ keep_on_navigate: true
332
485
  }, shell);
333
486
  if(!win) {
334
487
  log_error("C_YUI_SHELL: cannot create the site-map window");
@@ -342,11 +495,7 @@ export function yui_shell_show_route_map(shell, opts)
342
495
 
343
496
  /* Fallback: a modal (no C_YUI_WINDOW registered). */
344
497
  let modal_ref = {modal: null};
345
- let $body = build_body(shell, t, function() {
346
- if(modal_ref.modal && typeof modal_ref.modal.close === "function") {
347
- modal_ref.modal.close();
348
- }
349
- });
498
+ let $body = build_body(shell, t);
350
499
  modal_ref.modal = yui_shell_show_modal(shell, $body, {
351
500
  dialog: true,
352
501
  logical_class: "ROUTEMAP_SHEET",
package/src/yui_dev.js CHANGED
@@ -715,7 +715,7 @@ function render_full(e)
715
715
  let payload = full_sections(e.kw ? e.kw : e.jn);
716
716
  let text;
717
717
  try {
718
- text = JSON.stringify(payload, null, 2);
718
+ text = JSON.stringify(payload, null, 4);
719
719
  } catch(err) {
720
720
  text = String(payload);
721
721
  }
@@ -1021,7 +1021,7 @@ function info_log(level, msg, hora)
1021
1021
  let text;
1022
1022
  if(lvl === "json") {
1023
1023
  try {
1024
- text = JSON.stringify(msg, null, 2);
1024
+ text = JSON.stringify(msg, null, 4);
1025
1025
  } catch(e) {
1026
1026
  text = String(msg);
1027
1027
  }
@@ -1248,7 +1248,7 @@ function traffic_to_text()
1248
1248
  out.push(head);
1249
1249
  let payload = e.kw ? e.kw : e.jn;
1250
1250
  try {
1251
- out.push(JSON.stringify(payload, null, 2));
1251
+ out.push(JSON.stringify(payload, null, 4));
1252
1252
  } catch(err) {
1253
1253
  out.push(String(payload));
1254
1254
  }
package/src/yui_icons.css CHANGED
@@ -336,3 +336,16 @@
336
336
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 320 512'%3E%3Cpath d='M267.5 440.6c9.5 7.9 22.8 9.7 34.1 4.4s18.4-16.6 18.4-29l0-320c0-12.4-7.2-23.7-18.4-29s-24.5-3.6-34.1 4.4l-192 160L64 241l0-145c0-17.7-14.3-32-32-32S0 78.3 0 96L0 416c0 17.7 14.3 32 32 32s32-14.3 32-32l0-145 11.5 9.6 192 160z'/%3E%3C/svg%3E");
337
337
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 320 512'%3E%3Cpath d='M267.5 440.6c9.5 7.9 22.8 9.7 34.1 4.4s18.4-16.6 18.4-29l0-320c0-12.4-7.2-23.7-18.4-29s-24.5-3.6-34.1 4.4l-192 160L64 241l0-145c0-17.7-14.3-32-32-32S0 78.3 0 96L0 416c0 17.7 14.3 32 32 32s32-14.3 32-32l0-145 11.5 9.6 192 160z'/%3E%3C/svg%3E");
338
338
  }
339
+
340
+ /* yi-bolt / yi-droplet: hand-drawn masks. Added for the node-tree
341
+ * demo, but they earn their place in the set on their own: energy and
342
+ * water are two of the meter families the AMR apps read. */
343
+ .yi-bolt::before {
344
+ -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 448 512'%3E%3Cpath d='M300 0 60 288l140 0-50 224 240-288-140 0z'/%3E%3C/svg%3E");
345
+ mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 448 512'%3E%3Cpath d='M300 0 60 288l140 0-50 224 240-288-140 0z'/%3E%3C/svg%3E");
346
+ }
347
+
348
+ .yi-droplet::before {
349
+ -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 384 512'%3E%3Cpath d='M192 0C192 0 48 176 48 304a144 144 0 1 0 288 0C336 176 192 0 192 0z'/%3E%3C/svg%3E");
350
+ mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 384 512'%3E%3Cpath d='M192 0C192 0 48 176 48 304a144 144 0 1 0 288 0C336 176 192 0 192 0z'/%3E%3C/svg%3E");
351
+ }