@yuneta/gobj-ui 7.26.33 → 7.26.35

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/README.md +223 -53
  2. package/dist/gobj-ui.cjs.js +1053 -144
  3. package/dist/gobj-ui.es.js +1053 -144
  4. package/package.json +2 -2
  5. package/scripts/validate-locales.mjs +672 -96
  6. package/src/c_yui_form.js +229 -11
  7. package/src/c_yui_gclass.js +2 -0
  8. package/src/c_yui_gobj_tree_js.js +2 -0
  9. package/src/c_yui_json.js +2 -0
  10. package/src/c_yui_json_graph.js +2 -0
  11. package/src/c_yui_nav.js +23 -5
  12. package/src/c_yui_shell.css +16 -0
  13. package/src/c_yui_shell.js +282 -26
  14. package/src/c_yui_treedb_graph.js +26 -8
  15. package/src/c_yui_treedb_topic_with_form.js +40 -18
  16. package/src/c_yui_treedb_topics.js +34 -11
  17. package/src/c_yui_window.js +132 -32
  18. package/src/c_yui_window_manager.js +156 -44
  19. package/src/form_json_editor_names.js +448 -0
  20. package/src/form_json_editor_names.wiring.test.js +595 -0
  21. package/src/form_label_i18n.wiring.test.js +169 -0
  22. package/src/form_table_validator.wiring.test.js +192 -0
  23. package/src/form_template_field.wiring.test.js +21 -0
  24. package/src/i18n_key_functions.js +14 -6
  25. package/src/i18n_key_functions.test.js +124 -2
  26. package/src/lib_maplibre.js +26 -2
  27. package/src/lib_maplibre_relocalize.test.js +78 -0
  28. package/src/named_controls.wiring.test.js +468 -0
  29. package/src/nav_cards_helpers.js +9 -1
  30. package/src/nav_item_badge.wiring.test.js +182 -1
  31. package/src/shell_route_map.js +25 -8
  32. package/src/table_editor_name.wiring.test.js +201 -0
  33. package/src/treedb_column_title.wiring.test.js +151 -0
  34. package/src/treedb_json_drill.wiring.test.js +41 -1
  35. package/src/window_layer.wiring.test.js +308 -43
  36. package/src/window_show.wiring.test.js +6 -2
  37. package/src/window_stacking.wiring.test.js +334 -0
  38. package/src/yui_badge.js +58 -13
  39. package/src/yui_badge.test.js +40 -3
  40. package/src/yui_dev.js +2 -1
  41. package/src/yui_file_field.js +2 -0
  42. package/src/yui_frontend_view.js +2 -1
  43. package/src/yui_gclass_view.js +9 -6
  44. package/src/yui_tabulator_i18n.js +42 -0
  45. package/src/yui_tabulator_i18n.test.js +48 -1
@@ -0,0 +1,448 @@
1
+ /***********************************************************************
2
+ * form_json_editor_names.js
3
+ *
4
+ * The NAMES of the buttons vanilla-jsoneditor draws in its menu
5
+ * (a json field of C_YUI_FORM): in the app's language, and with an
6
+ * aria-label.
7
+ *
8
+ * The editor draws them itself, in English, with a `title` and
9
+ * nothing else: "Format JSON: add proper indentation and new lines
10
+ * (Ctrl+I)", "Undo (Ctrl+Z)", and three mode buttons whose text is
11
+ * "text", "tree" and "table" (REV-CLOUD V23.5.6). Two hooks reach
12
+ * them, and both are needed:
13
+ *
14
+ * - json_editor_menu_items(), the editor's `onRenderMenu`: the
15
+ * items it is about to draw, with each title translated (its
16
+ * shortcut kept) and the mode buttons' text too. Svelte draws
17
+ * from these, so a button born in a re-render is born named.
18
+ * - name_json_editor_menu(), after the render, from the form's
19
+ * FSM: the aria-label of each button, with its key, so
20
+ * refresh_language() re-translates it, and the state of the
21
+ * mode buttons (aria-pressed). A menu item has no field for
22
+ * either.
23
+ *
24
+ * A button is known by the CLASS the editor gives it (`jse-format`,
25
+ * `jse-undo`...), never by its English title: a class is what the
26
+ * editor's own stylesheet relies on. A button not listed here keeps
27
+ * the editor's words.
28
+ *
29
+ * Its CONTEXT MENU (`onRenderContextMenu`,
30
+ * json_editor_context_menu_items()) has no class on its items, so
31
+ * an item is known by what it IS and where it IS: the icon the
32
+ * editor draws it with (a FontAwesome name: `paste`, `scissors`),
33
+ * the column of the menu it stands in, its place in a dropdown
34
+ * (the main button, the first or the second of its list), the
35
+ * mode of the editor (tree or table) -- never by its words. Its
36
+ * text and title go back translated; after the render the same
37
+ * pass names its buttons, and the arrow of each dropdown, which
38
+ * the editor draws with no name at all.
39
+ *
40
+ * Not reached, because the editor gives them no hook: its search
41
+ * box, its repair messages, the tip at the foot of the context
42
+ * menu, and its modals (sort, transform). They stay in its
43
+ * English.
44
+ *
45
+ * Copyright (c) 2026, ArtGins.
46
+ * All Rights Reserved.
47
+ ***********************************************************************/
48
+ import {t} from "i18next";
49
+
50
+
51
+ /***************************************************************
52
+ * Constants
53
+ ***************************************************************/
54
+ /* The buttons of the menu, by the class the editor gives each. */
55
+ const JSON_EDITOR_BUTTON_KEYS = {
56
+ "jse-expand-all": "expand all",
57
+ "jse-collapse-all": "collapse all",
58
+ "jse-search": "search",
59
+ "jse-copy": "copy",
60
+ "jse-undo": "undo",
61
+ "jse-redo": "redo",
62
+ "jse-cancel": "cancel",
63
+ "jse-format": "format json",
64
+ "jse-compact": "compact json",
65
+ "jse-sort": "sort",
66
+ "jse-transform": "transform",
67
+ "jse-contextmenu": "open context menu",
68
+ };
69
+
70
+ /* Its three MODE buttons share one class (`jse-group-button`) and are
71
+ * told apart by the mode they switch to: their name, and the short
72
+ * text they show. */
73
+ const JSON_EDITOR_MODE_NAME_KEYS = {
74
+ text: "text view",
75
+ tree: "tree view",
76
+ table: "table view",
77
+ };
78
+ const JSON_EDITOR_MODE_TEXT_KEYS = {
79
+ text: "text mode",
80
+ tree: "tree mode",
81
+ table: "table mode",
82
+ };
83
+
84
+ /* The class this module adds to a mode button, so that after the
85
+ * render it is still known by its mode -- its text is translated. */
86
+ const MODE_CLASS = "yui-jse-mode-";
87
+
88
+ /* The shortcut at the end of an editor title: "(Ctrl+Shift+Z)". */
89
+ const SHORTCUT = /\((?:Ctrl|Cmd|⌘)[^()]*\)\s*$/;
90
+
91
+ /* The CONTEXT MENU, by the icon each item is drawn with (vanilla-
92
+ * jsoneditor 3.13 builds it with FontAwesome icons, and gives its
93
+ * items no class). A button of its own: */
94
+ const CONTEXT_BUTTON_KEYS = {
95
+ "pen": "edit key",
96
+ "paste": "paste",
97
+ "clone": "duplicate",
98
+ "crop-simple": "extract",
99
+ "arrow-down-short-wide": "sort",
100
+ "filter": "transform",
101
+ "trash-can": "remove",
102
+ "square-caret-up": "insert before",
103
+ "square-caret-down": "insert after",
104
+ };
105
+
106
+ /* A button of the ROW column of the table mode's menu. */
107
+ const CONTEXT_ROW_BUTTON_KEYS = {
108
+ "pen": "edit row",
109
+ "clone": "duplicate row",
110
+ "trash-can": "remove row",
111
+ };
112
+
113
+ /* A dropdown, by the icon of its main button: the main, the first of
114
+ * its list and the second. */
115
+ const CONTEXT_DROPDOWN_MAIN_KEYS = {
116
+ "pen": "edit value",
117
+ "scissors": "cut",
118
+ "copy": "copy",
119
+ };
120
+ const CONTEXT_DROPDOWN_FIRST_KEYS = {
121
+ "pen": "edit value",
122
+ "scissors": "cut formatted",
123
+ "copy": "copy formatted",
124
+ };
125
+ const CONTEXT_DROPDOWN_SECOND_KEYS = {
126
+ "pen": "enforce string",
127
+ "scissors": "cut compacted",
128
+ "copy": "copy compacted",
129
+ };
130
+
131
+ /* What the tree mode converts a value to, or inserts: the buttons of
132
+ * its second column, in their order, all with one icon. */
133
+ const CONTEXT_TARGET_KEYS = {
134
+ "0": "structure",
135
+ "1": "object",
136
+ "2": "array",
137
+ "3": "value",
138
+ };
139
+ const TARGET_ICONS = ["arrow-right-arrow-left", "plus"];
140
+
141
+ /* The labels heading a column. */
142
+ const CONTEXT_LABEL_KEYS = {
143
+ convert: "convert to",
144
+ insert: "insert",
145
+ cell: "table cell",
146
+ row: "table row",
147
+ };
148
+
149
+
150
+
151
+
152
+ /***************************
153
+ * Local Methods
154
+ ***************************/
155
+
156
+
157
+
158
+
159
+ function has_key(table, name)
160
+ {
161
+ return Object.prototype.hasOwnProperty.call(table, name);
162
+ }
163
+
164
+ /* The listed class among `classes` (a className string or a list). */
165
+ function button_class(classes)
166
+ {
167
+ let list = Array.isArray(classes)? classes : String(classes || "").split(/\s+/);
168
+ return list.find((c) => has_key(JSON_EDITOR_BUTTON_KEYS, c)) || "";
169
+ }
170
+
171
+ /* The mode an item of the editor switches to, "" when it is none. */
172
+ function mode_of_item(item)
173
+ {
174
+ let classes = String(item.className || "").split(/\s+/);
175
+ if(classes.indexOf("jse-group-button") < 0) {
176
+ return "";
177
+ }
178
+ return has_key(JSON_EDITOR_MODE_NAME_KEYS, item.text)? item.text : "";
179
+ }
180
+
181
+ /* The FontAwesome name of an item's icon, "" without one. */
182
+ function icon_of(item)
183
+ {
184
+ return (item && item.icon && typeof item.icon.iconName === "string")?
185
+ item.icon.iconName : "";
186
+ }
187
+
188
+ /* An item with its name: the text a reader sees and its title, which
189
+ * keeps the editor's shortcut after it. `name` is translated. */
190
+ function named_item(item, name)
191
+ {
192
+ let m = SHORTCUT.exec(String(item.title || ""));
193
+ return Object.assign({}, item, {
194
+ text: name,
195
+ title: m? `${name} ${m[0].trim()}` : name
196
+ });
197
+ }
198
+
199
+ /* A dropdown of the context menu: its main button and its list, by
200
+ * the icon of the main one. One the module does not know comes back
201
+ * as it came. */
202
+ function context_dropdown(item)
203
+ {
204
+ let icon = icon_of(item.main);
205
+ if(!has_key(CONTEXT_DROPDOWN_MAIN_KEYS, icon)) {
206
+ return item;
207
+ }
208
+ let list = Array.isArray(item.items)? item.items : [];
209
+ return Object.assign({}, item, {
210
+ main: named_item(item.main, t(CONTEXT_DROPDOWN_MAIN_KEYS[icon])),
211
+ items: list.map(function(it, i) {
212
+ if(!it || it.type !== "button") {
213
+ return it;
214
+ }
215
+ if(i === 0) {
216
+ return named_item(it, t(CONTEXT_DROPDOWN_FIRST_KEYS[icon]));
217
+ }
218
+ if(i === 1) {
219
+ return named_item(it, t(CONTEXT_DROPDOWN_SECOND_KEYS[icon]));
220
+ }
221
+ return it;
222
+ })
223
+ });
224
+ }
225
+
226
+ /* A button of the context menu that stands on its own, by its icon;
227
+ * in the row column of the table mode, by the row's own table. */
228
+ function context_button(item, row_column)
229
+ {
230
+ let icon = icon_of(item);
231
+ if(row_column && has_key(CONTEXT_ROW_BUTTON_KEYS, icon)) {
232
+ return named_item(item, t(CONTEXT_ROW_BUTTON_KEYS[icon]));
233
+ }
234
+ if(has_key(CONTEXT_BUTTON_KEYS, icon)) {
235
+ return named_item(item, t(CONTEXT_BUTTON_KEYS[icon]));
236
+ }
237
+ return item;
238
+ }
239
+
240
+ /* A column of the context menu. What its label says depends on where
241
+ * it is: in the table mode, the first column is the cell's and the
242
+ * second the row's; in the tree mode, a column whose buttons all carry
243
+ * a target's icon converts (arrows) or inserts (a plus), and its
244
+ * buttons are the targets, in their order. */
245
+ function context_column(item, table, index)
246
+ {
247
+ let list = Array.isArray(item.items)? item.items : [];
248
+ let buttons = list.filter((it) => it && it.type === "button");
249
+ let targets = !table && buttons.length > 0 &&
250
+ buttons.every((it) => TARGET_ICONS.indexOf(icon_of(it)) >= 0);
251
+ let row_column = table && index === 1;
252
+
253
+ let label_kind = "";
254
+ if(table) {
255
+ label_kind = (index === 0)? "cell" : (index === 1? "row" : "");
256
+ } else if(targets) {
257
+ label_kind = (icon_of(buttons[0]) === "plus")? "insert" : "convert";
258
+ }
259
+
260
+ let target = 0;
261
+ let insert_row = 0;
262
+ return Object.assign({}, item, {items: list.map(function(it) {
263
+ if(!it || typeof it !== "object") {
264
+ return it;
265
+ }
266
+ if(it.type === "label") {
267
+ return label_kind?
268
+ Object.assign({}, it, {text: t(CONTEXT_LABEL_KEYS[label_kind])}) : it;
269
+ }
270
+ if(it.type === "dropdown-button") {
271
+ return context_dropdown(it);
272
+ }
273
+ if(it.type !== "button") {
274
+ return it;
275
+ }
276
+ if(targets) {
277
+ return named_item(it, t(CONTEXT_TARGET_KEYS[String(target++)]));
278
+ }
279
+ if(row_column && icon_of(it) === "plus") {
280
+ /* The row's two inserts share one icon: before, then after. */
281
+ return named_item(it, t(insert_row++ === 0?
282
+ CONTEXT_BUTTON_KEYS["square-caret-up"] : CONTEXT_BUTTON_KEYS["square-caret-down"]));
283
+ }
284
+ return context_button(it, row_column);
285
+ })});
286
+ }
287
+
288
+ /* An item of the context menu, as it comes: a row, a dropdown, a
289
+ * button, a separator (as it came). */
290
+ function context_item(item, table)
291
+ {
292
+ if(!item || typeof item !== "object") {
293
+ return item;
294
+ }
295
+ if(item.type === "row") {
296
+ let column = 0;
297
+ let list = Array.isArray(item.items)? item.items : [];
298
+ return Object.assign({}, item, {items: list.map(function(it) {
299
+ if(it && it.type === "column") {
300
+ return context_column(it, table, column++);
301
+ }
302
+ return context_item(it, table);
303
+ })});
304
+ }
305
+ if(item.type === "dropdown-button") {
306
+ return context_dropdown(item);
307
+ }
308
+ if(item.type === "button") {
309
+ return context_button(item, false);
310
+ }
311
+ return item;
312
+ }
313
+
314
+ /* The mode of a rendered button, by the class json_editor_menu_items()
315
+ * gave its item. */
316
+ function mode_of_button($b)
317
+ {
318
+ for(let c of $b.classList) {
319
+ if(c.indexOf(MODE_CLASS) === 0) {
320
+ let mode = c.slice(MODE_CLASS.length);
321
+ if(has_key(JSON_EDITOR_MODE_NAME_KEYS, mode)) {
322
+ return mode;
323
+ }
324
+ }
325
+ }
326
+ return "";
327
+ }
328
+
329
+
330
+
331
+
332
+ /***************************
333
+ * Public
334
+ ***************************/
335
+
336
+
337
+
338
+
339
+ /************************************************************
340
+ * The editor's `onRenderMenu`: the items it is about to draw,
341
+ * translated. A title keeps the editor's shortcut after the
342
+ * translated name ("Deshacer (Ctrl+Z)"); a mode button gets its
343
+ * mode's name as title and its short text translated, and a class
344
+ * that says its mode. Anything else is returned as it came.
345
+ *
346
+ * Pure: what it answers is data for the editor to draw.
347
+ ************************************************************/
348
+ export function json_editor_menu_items(items)
349
+ {
350
+ if(!Array.isArray(items)) {
351
+ return items;
352
+ }
353
+ return items.map(function(item) {
354
+ if(!item || item.type !== "button") {
355
+ return item;
356
+ }
357
+ let mode = mode_of_item(item);
358
+ if(mode) {
359
+ return Object.assign({}, item, {
360
+ className: `${item.className} ${MODE_CLASS}${mode}`,
361
+ title: t(JSON_EDITOR_MODE_NAME_KEYS[mode]),
362
+ text: t(JSON_EDITOR_MODE_TEXT_KEYS[mode])
363
+ });
364
+ }
365
+ let cls = button_class(item.className);
366
+ if(!cls) {
367
+ return item;
368
+ }
369
+ let m = SHORTCUT.exec(String(item.title || ""));
370
+ let name = t(JSON_EDITOR_BUTTON_KEYS[cls]);
371
+ return Object.assign({}, item, {title: m? `${name} ${m[0].trim()}` : name});
372
+ });
373
+ }
374
+
375
+ /************************************************************
376
+ * The editor's `onRenderContextMenu`: the items it is about to
377
+ * draw, translated -- each known by its icon and its place, never
378
+ * by its words (context_item()). Anything the module does not
379
+ * know comes back as it came: a newer editor that moves an item
380
+ * leaves it in English, it does not mislabel it.
381
+ *
382
+ * Pure: what it answers is data for the editor to draw.
383
+ ************************************************************/
384
+ export function json_editor_context_menu_items(items, context)
385
+ {
386
+ if(!Array.isArray(items)) {
387
+ return items;
388
+ }
389
+ let table = !!(context && context.mode === "table");
390
+ return items.map((item) => context_item(item, table));
391
+ }
392
+
393
+ /************************************************************
394
+ * Name the buttons of a rendered editor's menu: an aria-label
395
+ * with its key (data-i18n-aria-label, so a language change
396
+ * reaches it), and aria-pressed on the mode buttons -- the
397
+ * selected one is the mode on screen (`jse-selected`). Run
398
+ * after every render of the menu: a re-render may draw new
399
+ * buttons, which carry none of it.
400
+ ************************************************************/
401
+ export function name_json_editor_menu($editor)
402
+ {
403
+ if(!$editor || typeof $editor.querySelectorAll !== "function") {
404
+ return 0;
405
+ }
406
+ let named = 0;
407
+ for(let $b of $editor.querySelectorAll(".jse-menu button")) {
408
+ let mode = mode_of_button($b);
409
+ const key = mode?
410
+ JSON_EDITOR_MODE_NAME_KEYS[mode] :
411
+ JSON_EDITOR_BUTTON_KEYS[button_class([...$b.classList])];
412
+ if(!key) {
413
+ continue; // a button of the editor's own this module does not know
414
+ }
415
+ $b.setAttribute("aria-label", t(key));
416
+ $b.setAttribute("data-i18n-aria-label", key);
417
+ if(mode) {
418
+ $b.setAttribute("aria-pressed",
419
+ $b.classList.contains("jse-selected")? "true" : "false");
420
+ }
421
+ named++;
422
+ }
423
+
424
+ /* Its context menu, while it is open (a popup the editor draws in
425
+ * its own element). Its buttons carry the text and the title
426
+ * json_editor_context_menu_items() gave them, in the language of
427
+ * this moment -- the menu closes on the first press outside it, so
428
+ * it is never on screen across a language change: the aria-label
429
+ * is that text. The arrow that opens a dropdown carries nothing at
430
+ * all, and is named here, with its key. */
431
+ const MORE = "more options";
432
+ for(let $b of $editor.querySelectorAll("div.jse-contextmenu button")) {
433
+ if($b.getAttribute("data-type") === "jse-open-dropdown") {
434
+ $b.setAttribute("title", t(MORE));
435
+ $b.setAttribute("data-i18n-title", MORE);
436
+ $b.setAttribute("aria-label", t(MORE));
437
+ $b.setAttribute("data-i18n-aria-label", MORE);
438
+ named++;
439
+ continue;
440
+ }
441
+ let text = ($b.textContent || "").trim();
442
+ if(text) {
443
+ $b.setAttribute("aria-label", text);
444
+ named++;
445
+ }
446
+ }
447
+ return named;
448
+ }