@yuneta/gobj-ui 7.23.142 → 7.23.143

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/index.js CHANGED
@@ -23,6 +23,7 @@ export { register_c_yui_map } from "./src/c_yui_map.js";
23
23
  export { register_c_yui_uplot } from "./src/c_yui_uplot.js";
24
24
  export { register_c_yui_json_graph } from "./src/c_yui_json_graph.js";
25
25
  export { register_c_yui_json } from "./src/c_yui_json.js";
26
+ export { register_c_yui_json_pad } from "./src/c_yui_json_pad.js";
26
27
  export { register_c_yui_gclass } from "./src/c_yui_gclass.js";
27
28
  export { register_c_yui_fsm_graph } from "./src/c_yui_fsm_graph.js";
28
29
  export { register_c_yui_gobj_tree_js } from "./src/c_yui_gobj_tree_js.js";
@@ -129,6 +130,7 @@ export { yui_theme_now, yui_is_dark, yui_watch_theme } from "./src/yui_theme.js"
129
130
  export { attach_clear, refresh_clear } from "./src/yui_inputs.js";
130
131
  export { info_traffic, setup_dev, build_dev_panel, apply_dev_traces, dev_window_was_open } from "./src/yui_dev.js";
131
132
  export { setup_frontend_view } from "./src/yui_frontend_view.js";
133
+ export { setup_json_pad } from "./src/yui_json_pad.js";
132
134
 
133
135
  /*
134
136
  * CSS - import these in your main entry point
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuneta/gobj-ui",
3
- "version": "7.23.142",
3
+ "version": "7.23.143",
4
4
  "type": "module",
5
5
  "main": "dist/gobj-ui.cjs.js",
6
6
  "module": "dist/gobj-ui.es.js",
@@ -0,0 +1,454 @@
1
+ /***********************************************************************
2
+ * c_yui_json_pad.js
3
+ *
4
+ * C_YUI_JSON_PAD -- a pad to paste JSON from OUTSIDE and read it
5
+ * with the library's own viewer.
6
+ *
7
+ * The viewer (C_YUI_JSON: the lazy tree, the raw text and the
8
+ * graph) only ever showed what an app fetched. A JSON that came
9
+ * from anywhere else -- a log line, a config file, an answer copied
10
+ * from a terminal -- had to be read in whatever the operator had
11
+ * at hand. This is the blank page for it: paste, and read it the
12
+ * way every other document of the app is read.
13
+ *
14
+ * DOM (UPPER_SNAKE logical classes):
15
+ *
16
+ * JSON_PAD the root
17
+ * JSON_PAD_BAR the text area and its two buttons
18
+ * JSON_PAD_INPUT where the text goes
19
+ * JSON_PAD_VIEW read what is in the text area
20
+ * JSON_PAD_CLEAR empty the pad
21
+ * JSON_PAD_ERROR "invalid JSON" and the parser's reason
22
+ * JSON_PAD_BODY the C_YUI_JSON child
23
+ *
24
+ * Every way in is an event:
25
+ *
26
+ * EV_PASTE {text} a paste ANYWHERE in the pad replaces the
27
+ * document and shows it at once -- there
28
+ * is one document here, not a text to edit
29
+ * EV_VIEW the text area as it is (typed, or edited
30
+ * after a paste); also Ctrl+Enter
31
+ * EV_CLEAR back to the blank pad
32
+ *
33
+ * FSM: ST_EMPTY (nothing shown) and ST_SHOWING. EV_EXPAND_PATH,
34
+ * which the viewer publishes when a `__collapsed__` sentinel is
35
+ * opened, only exists in ST_SHOWING, and is answered with
36
+ * EV_SUBTREE_ERROR: a pasted document has no backend to fetch the
37
+ * rest from -- the source truncated it, and the pad says so.
38
+ *
39
+ * Hosted by setup_json_pad() (yui_json_pad.js) in a C_YUI_WINDOW;
40
+ * any host can mount `$container` itself.
41
+ *
42
+ * Copyright (c) 2026, ArtGins.
43
+ * All Rights Reserved.
44
+ ***********************************************************************/
45
+ import {
46
+ SDATA,
47
+ SDATA_END,
48
+ data_type_t,
49
+ gclass_create,
50
+ gclass_find_by_name,
51
+ log_error,
52
+ log_warning,
53
+ gobj_create_pure_child,
54
+ gobj_read_pointer_attr,
55
+ gobj_read_attr,
56
+ gobj_write_attr,
57
+ gobj_subscribe_event,
58
+ gobj_send_event,
59
+ gobj_change_state,
60
+ gobj_start,
61
+ gobj_stop,
62
+ gobj_is_running,
63
+ gobj_short_name,
64
+ gobj_parent,
65
+ createElement2,
66
+ } from "@yuneta/gobj-js";
67
+
68
+ import {register_c_yui_json} from "./c_yui_json.js";
69
+ import {yui_toolbar_icon} from "./yui_toolbar.js";
70
+
71
+ import {t} from "i18next";
72
+
73
+ /***************************************************************
74
+ * Constants
75
+ ***************************************************************/
76
+ const GCLASS_NAME = "C_YUI_JSON_PAD";
77
+
78
+ /***************************************************************
79
+ * Data
80
+ ***************************************************************/
81
+ const attrs_table = [
82
+ SDATA(data_type_t.DTP_POINTER, "subscriber", 0, null, "Subscriber of output events"),
83
+ SDATA(data_type_t.DTP_POINTER, "$container", 0, null, "HTMLElement root, mounted by the host"),
84
+ SDATA_END()
85
+ ];
86
+
87
+ let PRIVATE_DATA = {
88
+ $input: null, /* the text area */
89
+ $error: null, /* the "invalid JSON" line */
90
+ $reason: null, /* ...and the parser's reason inside it */
91
+ gobj_json: null, /* the C_YUI_JSON child */
92
+ };
93
+
94
+ let __gclass__ = null;
95
+
96
+
97
+
98
+
99
+ /******************************
100
+ * Framework Methods
101
+ ******************************/
102
+
103
+
104
+
105
+
106
+ /***************************************************************
107
+ * Framework Method: Create
108
+ ***************************************************************/
109
+ function mt_create(gobj)
110
+ {
111
+ let priv = gobj.priv;
112
+
113
+ /*
114
+ * CHILD subscription model
115
+ */
116
+ let subscriber = gobj_read_pointer_attr(gobj, "subscriber");
117
+ if(!subscriber) {
118
+ subscriber = gobj_parent(gobj);
119
+ }
120
+ gobj_subscribe_event(gobj, null, {}, subscriber);
121
+
122
+ build_ui(gobj);
123
+
124
+ /*
125
+ * The viewer, as a pure child: it publishes EV_EXPAND_PATH to its
126
+ * parent (this gobj), which declares it in ST_SHOWING.
127
+ */
128
+ priv.gobj_json = gobj_create_pure_child("json_pad_view", "C_YUI_JSON", {}, gobj);
129
+ let $view = gobj_read_attr(priv.gobj_json, "$container");
130
+ if(!$view) {
131
+ log_error(`${gobj_short_name(gobj)}: C_YUI_JSON without $container`);
132
+ return;
133
+ }
134
+ gobj_read_attr(gobj, "$container").querySelector(".JSON_PAD_BODY").appendChild($view);
135
+ }
136
+
137
+ /***************************************************************
138
+ * Framework Method: Start
139
+ ***************************************************************/
140
+ function mt_start(gobj)
141
+ {
142
+ let priv = gobj.priv;
143
+
144
+ if(priv.gobj_json) {
145
+ gobj_start(priv.gobj_json);
146
+ }
147
+ /* A blank pad is waiting for a paste: the caret goes where it lands. */
148
+ if(priv.$input) {
149
+ priv.$input.focus();
150
+ }
151
+ }
152
+
153
+ /***************************************************************
154
+ * Framework Method: Stop
155
+ ***************************************************************/
156
+ function mt_stop(gobj)
157
+ {
158
+ let priv = gobj.priv;
159
+
160
+ if(priv.gobj_json && gobj_is_running(priv.gobj_json)) {
161
+ gobj_stop(priv.gobj_json);
162
+ }
163
+ }
164
+
165
+ /***************************************************************
166
+ * Framework Method: Destroy
167
+ ***************************************************************/
168
+ function mt_destroy(gobj)
169
+ {
170
+ }
171
+
172
+
173
+
174
+
175
+ /***************************
176
+ * Local Methods
177
+ ***************************/
178
+
179
+
180
+
181
+
182
+ /***************************************************************
183
+ * The pad: the text area and its buttons, the error line, and
184
+ * the body the viewer is mounted in.
185
+ ***************************************************************/
186
+ function build_ui(gobj)
187
+ {
188
+ let priv = gobj.priv;
189
+
190
+ let button = (cls, icon, key, event_name) => {
191
+ return ['button', {class: `${cls} button`, type: 'button',
192
+ title: t(key), 'data-i18n-title': key,
193
+ 'aria-label': t(key), 'data-i18n-aria-label': key},
194
+ [
195
+ yui_toolbar_icon(icon),
196
+ ['span', {style: 'padding-left:5px;', i18n: key}, key]
197
+ ],
198
+ {
199
+ click: (evt) => {
200
+ evt.stopPropagation();
201
+ gobj_send_event(gobj, event_name, {}, gobj);
202
+ }
203
+ }
204
+ ];
205
+ };
206
+
207
+ priv.$input = createElement2(
208
+ ['textarea', {class: 'JSON_PAD_INPUT textarea', rows: 3, spellcheck: 'false',
209
+ placeholder: t('paste json here'), 'data-i18n-placeholder': 'paste json here',
210
+ title: t('paste json here'), 'data-i18n-title': 'paste json here',
211
+ 'aria-label': t('paste json here'), 'data-i18n-aria-label': 'paste json here',
212
+ style: 'font-family:monospace; resize:vertical; min-height:4.5em;'}, '', {
213
+ keydown: (evt) => {
214
+ if((evt.ctrlKey || evt.metaKey) && evt.key === 'Enter') {
215
+ evt.preventDefault();
216
+ gobj_send_event(gobj, "EV_VIEW", {}, gobj);
217
+ }
218
+ }
219
+ }]
220
+ );
221
+
222
+ priv.$reason = createElement2(
223
+ ['span', {class: 'JSON_PAD_ERROR_REASON', style: 'margin-left:.5em; font-family:monospace;'}, '']
224
+ );
225
+ priv.$error = createElement2(
226
+ ['div', {class: 'JSON_PAD_ERROR is-hidden has-text-danger is-size-7', role: 'alert'}, [
227
+ ['span', {class: 'JSON_PAD_ERROR_LABEL', style: 'font-weight:600;', i18n: 'invalid json'},
228
+ 'invalid json'],
229
+ priv.$reason
230
+ ]]
231
+ );
232
+
233
+ let $container = createElement2(
234
+ ['div', {class: 'C_YUI_JSON_PAD JSON_PAD',
235
+ style: 'height:100%; display:flex; flex-direction:column; gap:.5rem; padding:.5rem;'}, [
236
+ ['div', {class: 'JSON_PAD_BAR',
237
+ style: 'display:flex; gap:.5rem; align-items:flex-start;'}, [
238
+ ['div', {style: 'flex:1 1 auto; min-width:0;'}, [priv.$input]],
239
+ ['div', {style: 'flex:0 0 auto; display:flex; flex-direction:column; gap:.5rem;'}, [
240
+ button('JSON_PAD_VIEW', 'yi-eye', 'view', 'EV_VIEW'),
241
+ button('JSON_PAD_CLEAR', 'yi-broom', 'clear', 'EV_CLEAR'),
242
+ ]]
243
+ ]],
244
+ priv.$error,
245
+ ['div', {class: 'JSON_PAD_BODY',
246
+ style: 'flex:1 1 auto; min-height:0; display:flex; flex-direction:column;'}, []]
247
+ ], {
248
+ /* A paste anywhere in the pad is the document: it replaces
249
+ * what the text area held instead of landing at the caret. */
250
+ paste: (evt) => {
251
+ let text = evt.clipboardData? evt.clipboardData.getData("text") : "";
252
+ if(!text) {
253
+ return; /* an image or nothing: the browser's business */
254
+ }
255
+ evt.preventDefault();
256
+ gobj_send_event(gobj, "EV_PASTE", {text: text}, gobj);
257
+ }
258
+ }]
259
+ );
260
+
261
+ gobj_write_attr(gobj, "$container", $container);
262
+ }
263
+
264
+ /***************************************************************
265
+ * Read `text` as JSON and show it, or say why it is not.
266
+ ***************************************************************/
267
+ function show_text(gobj, text)
268
+ {
269
+ let priv = gobj.priv;
270
+ let src = String(text || "").trim();
271
+
272
+ if(!src) {
273
+ clear_pad(gobj, false);
274
+ return 0;
275
+ }
276
+
277
+ let doc;
278
+ try {
279
+ doc = JSON.parse(src);
280
+ } catch(e) {
281
+ /* The reader's text, not a fault of the app: said on the pad, and
282
+ * a warning for whoever reads the console. */
283
+ log_warning(`${gobj_short_name(gobj)}: the pasted text is not JSON: ${e.message}`);
284
+ priv.$reason.textContent = e.message;
285
+ priv.$error.classList.remove('is-hidden');
286
+ return -1;
287
+ }
288
+
289
+ priv.$error.classList.add('is-hidden');
290
+ priv.$reason.textContent = "";
291
+ gobj_send_event(priv.gobj_json, "EV_SET_JSON", {json: doc}, gobj);
292
+ gobj_change_state(gobj, "ST_SHOWING");
293
+ return 0;
294
+ }
295
+
296
+ /***************************************************************
297
+ * Back to the blank pad; `with_text` empties the text area too.
298
+ ***************************************************************/
299
+ function clear_pad(gobj, with_text)
300
+ {
301
+ let priv = gobj.priv;
302
+
303
+ if(with_text) {
304
+ priv.$input.value = "";
305
+ }
306
+ priv.$error.classList.add('is-hidden');
307
+ priv.$reason.textContent = "";
308
+ gobj_send_event(priv.gobj_json, "EV_SET_JSON", {json: null}, gobj);
309
+ gobj_change_state(gobj, "ST_EMPTY");
310
+ }
311
+
312
+
313
+
314
+
315
+ /***************************
316
+ * Actions
317
+ ***************************/
318
+
319
+
320
+
321
+
322
+ /***************************************************************
323
+ * A paste: it becomes the text area and the document at once.
324
+ ***************************************************************/
325
+ function ac_paste(gobj, event, kw, src)
326
+ {
327
+ let priv = gobj.priv;
328
+ let text = (kw && typeof kw.text === "string")? kw.text : "";
329
+
330
+ priv.$input.value = text;
331
+ return show_text(gobj, text);
332
+ }
333
+
334
+ /***************************************************************
335
+ * The text area as it is now.
336
+ ***************************************************************/
337
+ function ac_view(gobj, event, kw, src)
338
+ {
339
+ let priv = gobj.priv;
340
+ return show_text(gobj, priv.$input.value);
341
+ }
342
+
343
+ /***************************************************************
344
+ * The blank pad again.
345
+ ***************************************************************/
346
+ function ac_clear(gobj, event, kw, src)
347
+ {
348
+ let priv = gobj.priv;
349
+
350
+ clear_pad(gobj, true);
351
+ priv.$input.focus();
352
+ return 0;
353
+ }
354
+
355
+ /***************************************************************
356
+ * The viewer asks for a `__collapsed__` subtree. A pasted
357
+ * document has no backend to ask for it: the SOURCE truncated
358
+ * it, and that is the answer.
359
+ ***************************************************************/
360
+ function ac_expand_path(gobj, event, kw, src)
361
+ {
362
+ let priv = gobj.priv;
363
+
364
+ gobj_send_event(priv.gobj_json, "EV_SUBTREE_ERROR", {
365
+ path: (kw && kw.path) || "",
366
+ error: t("collapsed in the source")
367
+ }, gobj);
368
+ return 0;
369
+ }
370
+
371
+ /***************************************************************
372
+ * FSM
373
+ ***************************************************************/
374
+ /*---------------------------------------------*
375
+ * Global methods table
376
+ *---------------------------------------------*/
377
+ const gmt = {
378
+ mt_create: mt_create,
379
+ mt_start: mt_start,
380
+ mt_stop: mt_stop,
381
+ mt_destroy: mt_destroy
382
+ };
383
+
384
+ /***************************************************************
385
+ * Create the GClass
386
+ ***************************************************************/
387
+ function create_gclass(gclass_name)
388
+ {
389
+ if(__gclass__) {
390
+ log_error(`GClass ALREADY created: ${gclass_name}`);
391
+ return -1;
392
+ }
393
+
394
+ /*---------------------------------------------*
395
+ * States
396
+ *---------------------------------------------*/
397
+ const states = [
398
+ ["ST_EMPTY", [
399
+ ["EV_PASTE", ac_paste, null],
400
+ ["EV_VIEW", ac_view, null],
401
+ ["EV_CLEAR", ac_clear, null],
402
+ ]],
403
+ ["ST_SHOWING", [
404
+ ["EV_PASTE", ac_paste, null],
405
+ ["EV_VIEW", ac_view, null],
406
+ ["EV_CLEAR", ac_clear, null],
407
+ ["EV_EXPAND_PATH", ac_expand_path, null],
408
+ ]]
409
+ ];
410
+
411
+ /*---------------------------------------------*
412
+ * Events
413
+ *---------------------------------------------*/
414
+ const event_types = [
415
+ ["EV_PASTE", 0],
416
+ ["EV_VIEW", 0],
417
+ ["EV_CLEAR", 0],
418
+ ["EV_EXPAND_PATH", 0],
419
+ ];
420
+
421
+ __gclass__ = gclass_create(
422
+ gclass_name,
423
+ event_types,
424
+ states,
425
+ gmt,
426
+ 0, // lmt,
427
+ attrs_table,
428
+ PRIVATE_DATA,
429
+ 0, // authz_table,
430
+ 0, // command_table,
431
+ 0, // s_user_trace_level
432
+ 0 // gclass_flag
433
+ );
434
+
435
+ if(!__gclass__) {
436
+ return -1;
437
+ }
438
+
439
+ return 0;
440
+ }
441
+
442
+ /***************************************************************
443
+ * Register GClass
444
+ ***************************************************************/
445
+ function register_c_yui_json_pad()
446
+ {
447
+ /* the viewer it hosts, if the app did not register it */
448
+ if(!gclass_find_by_name("C_YUI_JSON", false)) {
449
+ register_c_yui_json();
450
+ }
451
+ return create_gclass(GCLASS_NAME);
452
+ }
453
+
454
+ export { register_c_yui_json_pad };
@@ -1522,6 +1522,20 @@ function create_tabulator(gobj)
1522
1522
  * had once opened the table. Only a real change is written. */
1523
1523
  gobj.priv._page_size = tabulator_settings.paginationSize;
1524
1524
 
1525
+ /* Normalized records are read by their KEY: a topic table opens
1526
+ * sorted by `id` (the pkey), ascending and natural (`d2` before
1527
+ * `d10`) -- unless the app set its own order, or the table pages
1528
+ * from the backend, where a sort in the browser would reorder one
1529
+ * page and say nothing about the others. */
1530
+ let pkey_col = columns.find((c) => c && c.field === pkey);
1531
+ if(pkey_col && !tabulator_settings.initialSort &&
1532
+ !gobj_read_bool_attr(gobj, "with_remote_paging")) {
1533
+ if(!pkey_col.sorter) {
1534
+ pkey_col.sorter = "alphanum";
1535
+ }
1536
+ tabulator_settings.initialSort = [{column: pkey, dir: "asc"}];
1537
+ }
1538
+
1525
1539
  Object.assign(tabulator_settings, {
1526
1540
  index: pkey,
1527
1541
  columns: columns,
@@ -0,0 +1,109 @@
1
+ /***********************************************************************
2
+ * yui_json_pad.js
3
+ *
4
+ * JSON viewer -- a blank C_YUI_JSON_PAD in a floating window,
5
+ * to paste JSON from outside and read it with the library's own
6
+ * viewer. Peer of the frontend view (yui_frontend_view.js), and
7
+ * opened the same way: an entry of the account menu.
8
+ *
9
+ * Copyright (c) 2026, ArtGins.
10
+ * All Rights Reserved.
11
+ ***********************************************************************/
12
+ import {
13
+ createElement2,
14
+ gclass_find_by_name,
15
+ gobj_create_service,
16
+ gobj_create_pure_child,
17
+ gobj_find_service,
18
+ gobj_read_attr,
19
+ gobj_start_tree,
20
+ log_error,
21
+ } from "@yuneta/gobj-js";
22
+
23
+ import {register_c_yui_json_pad} from "./c_yui_json_pad.js";
24
+
25
+ /* Service name of the window. The host toggles the entry by looking
26
+ * it up, exactly as it does with "Frontend-View-Window". */
27
+ const WIN_NAME = "Json-Viewer-Window";
28
+
29
+
30
+ /******************************
31
+ * Public API
32
+ ******************************/
33
+
34
+
35
+ /************************************************************
36
+ * Open a blank JSON pad (C_YUI_JSON_PAD) inside a non-modal
37
+ * C_YUI_WINDOW, the same shape as the frontend view.
38
+ *
39
+ * Self-contained: it registers C_YUI_JSON_PAD (and the
40
+ * C_YUI_JSON it hosts) if the app did not, so an app gets the
41
+ * viewer with one menu entry and one action.
42
+ *
43
+ * Returns the window gobj, or null.
44
+ ************************************************************/
45
+ function setup_json_pad(self)
46
+ {
47
+ if(gobj_find_service(WIN_NAME, false)) {
48
+ return null;
49
+ }
50
+ if(!gclass_find_by_name("C_YUI_JSON_PAD", false)) {
51
+ register_c_yui_json_pad();
52
+ }
53
+
54
+ /* The pad is created AFTER the window, as a pure child of it, so
55
+ * every teardown path (the ✕, or the host destroying the window to
56
+ * toggle the entry off) takes the pad down with it -- which is why
57
+ * the body is a placeholder: C_YUI_WINDOW builds its UI in
58
+ * mt_create, before the pad exists. */
59
+ let $body = createElement2(
60
+ ["div", {class: "YJSONPAD_BODY",
61
+ style: "height:100%; display:flex; flex-direction:column;"}, []]
62
+ );
63
+
64
+ let win = gobj_create_service(
65
+ WIN_NAME,
66
+ "C_YUI_WINDOW",
67
+ {
68
+ $parent: (typeof document !== "undefined" &&
69
+ document.getElementById("top-layer")) || null,
70
+ subscriber: null,
71
+ modal: false,
72
+ showMax: true,
73
+ showFooter: false,
74
+ resizable: true,
75
+ center: false,
76
+ auto_save_size_and_position: true,
77
+ width: 900,
78
+ height: 640,
79
+ logical_class: "JSON_VIEWER_WINDOW",
80
+ title: "json viewer",
81
+ icon: "yi-code",
82
+ body: $body,
83
+ /* `|| null`: an undefined attr value logs "attr undefined:
84
+ * manager" in an app without a window manager. */
85
+ manager: gobj_find_service("__window_manager__", false) || null,
86
+ },
87
+ self
88
+ );
89
+ if(!win) {
90
+ log_error("yui_json_pad: cannot create the json-viewer window");
91
+ return null;
92
+ }
93
+
94
+ /* The window is mounted by its mt_create, so $body is live DOM here:
95
+ * the viewer's graph measures its canvas when it is first shown. */
96
+ let pad = gobj_create_pure_child("json_pad", "C_YUI_JSON_PAD", {}, win);
97
+ let $pad = pad? gobj_read_attr(pad, "$container") : null;
98
+ if(!$pad) {
99
+ log_error("yui_json_pad: C_YUI_JSON_PAD without $container");
100
+ return win;
101
+ }
102
+ $body.appendChild($pad);
103
+
104
+ gobj_start_tree(win);
105
+
106
+ return win;
107
+ }
108
+
109
+ export {setup_json_pad};