@yuneta/gobj-ui 7.23.142 → 7.23.144
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/README.md +40 -0
- package/dist/gobj-ui.cjs.js +1225 -213
- package/dist/gobj-ui.es.js +1225 -215
- package/index.js +2 -0
- package/package.json +1 -1
- package/src/c_yui_json_pad.css +94 -0
- package/src/c_yui_json_pad.js +907 -0
- package/src/c_yui_treedb_topic_with_form.js +14 -0
- package/src/json_view_helpers.js +56 -1
- package/src/json_view_helpers.test.js +43 -0
- package/src/yui_icons.css +12 -0
- package/src/yui_json_pad.js +109 -0
|
@@ -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,
|
package/src/json_view_helpers.js
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* viewer). Kept out of the gclass so it can be unit-tested with
|
|
6
6
|
* no DOM: collapsed-sentinel detection, path (segments) algebra,
|
|
7
7
|
* search matching, timestamp recognition/formatting, the JSON
|
|
8
|
-
* type discriminator and the text view's capped dump
|
|
8
|
+
* type discriminator and the text view's capped dump -- and the
|
|
9
|
+
* differences of two documents, for the JSON pad's compare.
|
|
9
10
|
*
|
|
10
11
|
* Path convention mirrors the C kernel (kw_collapse / kw_find_path
|
|
11
12
|
* in kwid.c): segments are joined by the backtick delimiter, arrays
|
|
@@ -14,6 +15,7 @@
|
|
|
14
15
|
* Copyright (c) 2026, ArtGins.
|
|
15
16
|
* All Rights Reserved.
|
|
16
17
|
***********************************************************************/
|
|
18
|
+
import {json2flat, flat_diff} from "@yuneta/gobj-js";
|
|
17
19
|
|
|
18
20
|
/*
|
|
19
21
|
* Path delimiter, identical to the kernel's `delimiter` in kwid.c.
|
|
@@ -397,3 +399,56 @@ export function count_branches(value)
|
|
|
397
399
|
}
|
|
398
400
|
return n;
|
|
399
401
|
}
|
|
402
|
+
|
|
403
|
+
/************************************************************
|
|
404
|
+
* What differs between two documents: one row per id of
|
|
405
|
+
* their flat form (json2flat), sorted by id in natural order,
|
|
406
|
+
* so `[2]` comes before `[10]`:
|
|
407
|
+
*
|
|
408
|
+
* [{id, kind: "added"|"removed"|"changed", from?, to?}]
|
|
409
|
+
*
|
|
410
|
+
* `from` is the first document's value and `to` the second's:
|
|
411
|
+
* an added row has no `from`, a removed row no `to`.
|
|
412
|
+
*
|
|
413
|
+
* Two roots that are not containers of the same kind have no
|
|
414
|
+
* ids in common to compare by: they are one value each, and
|
|
415
|
+
* the row that says they differ has the empty id.
|
|
416
|
+
*
|
|
417
|
+
* Throws what json2flat throws (a document too deep).
|
|
418
|
+
************************************************************/
|
|
419
|
+
export function json_diff_rows(doc_a, doc_b)
|
|
420
|
+
{
|
|
421
|
+
const kind_of = (v) => {
|
|
422
|
+
if(Array.isArray(v)) {
|
|
423
|
+
return "array";
|
|
424
|
+
}
|
|
425
|
+
if(v !== null && typeof v === "object") {
|
|
426
|
+
return "object";
|
|
427
|
+
}
|
|
428
|
+
return "scalar";
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
let kind_a = kind_of(doc_a);
|
|
432
|
+
if(kind_a === "scalar" || kind_a !== kind_of(doc_b)) {
|
|
433
|
+
if(JSON.stringify(doc_a) === JSON.stringify(doc_b)) {
|
|
434
|
+
return [];
|
|
435
|
+
}
|
|
436
|
+
return [{id: "", kind: "changed", from: doc_a, to: doc_b}];
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
let diff = flat_diff(json2flat(doc_a), json2flat(doc_b));
|
|
440
|
+
let rows = [];
|
|
441
|
+
for(const id of Object.keys(diff.added)) {
|
|
442
|
+
rows.push({id: id, kind: "added", to: diff.added[id]});
|
|
443
|
+
}
|
|
444
|
+
for(const id of Object.keys(diff.removed)) {
|
|
445
|
+
rows.push({id: id, kind: "removed", from: diff.removed[id]});
|
|
446
|
+
}
|
|
447
|
+
for(const id of Object.keys(diff.changed)) {
|
|
448
|
+
rows.push({id: id, kind: "changed", from: diff.changed[id].from, to: diff.changed[id].to});
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const collator = new Intl.Collator("en", {numeric: true});
|
|
452
|
+
rows.sort((x, y) => collator.compare(x.id, y.id));
|
|
453
|
+
return rows;
|
|
454
|
+
}
|
|
@@ -21,9 +21,52 @@ import {
|
|
|
21
21
|
is_pure_collection,
|
|
22
22
|
has_branch,
|
|
23
23
|
count_branches,
|
|
24
|
+
json_diff_rows,
|
|
24
25
|
} from "./json_view_helpers.js";
|
|
25
26
|
|
|
26
27
|
|
|
28
|
+
/*============================================================
|
|
29
|
+
* json_diff_rows
|
|
30
|
+
*============================================================*/
|
|
31
|
+
test("json_diff_rows: two equal documents have no rows, whatever their key order", () => {
|
|
32
|
+
expect(json_diff_rows({a: 1, b: [1, {c: null}]}, {b: [1, {c: null}], a: 1})).toEqual([]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("json_diff_rows: one row per differing id, sorted by id", () => {
|
|
36
|
+
let a = {a: 1, b: {c: [1, 2]}, e: "x"};
|
|
37
|
+
let b = {a: 2, b: {c: [1]}, d: {}, e: "x"};
|
|
38
|
+
expect(json_diff_rows(a, b)).toEqual([
|
|
39
|
+
{id: "a", kind: "changed", from: 1, to: 2},
|
|
40
|
+
{id: "b`c`[1]", kind: "removed", from: 2},
|
|
41
|
+
{id: "d", kind: "added", to: {}},
|
|
42
|
+
]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("json_diff_rows: array indexes sort as numbers, [2] before [10]", () => {
|
|
46
|
+
let a = [];
|
|
47
|
+
let b = [];
|
|
48
|
+
for(let i = 0; i < 12; i++) {
|
|
49
|
+
a.push(i);
|
|
50
|
+
b.push((i === 2 || i === 10)? -1 : i);
|
|
51
|
+
}
|
|
52
|
+
expect(json_diff_rows(a, b).map((r) => r.id)).toEqual(["[2]", "[10]"]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("json_diff_rows: a leaf that becomes a container is one id gone and one new", () => {
|
|
56
|
+
expect(json_diff_rows({a: 1}, {a: {b: 2}})).toEqual([
|
|
57
|
+
{id: "a", kind: "removed", from: 1},
|
|
58
|
+
{id: "a`b", kind: "added", to: 2},
|
|
59
|
+
]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("json_diff_rows: roots that are not containers of one kind are one value each", () => {
|
|
63
|
+
expect(json_diff_rows(1, 1)).toEqual([]);
|
|
64
|
+
expect(json_diff_rows("x", "y")).toEqual([{id: "", kind: "changed", from: "x", to: "y"}]);
|
|
65
|
+
expect(json_diff_rows({}, [])).toEqual([{id: "", kind: "changed", from: {}, to: []}]);
|
|
66
|
+
expect(json_diff_rows(null, {a: 1})).toEqual([{id: "", kind: "changed", from: null, to: {a: 1}}]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
|
|
27
70
|
/*============================================================
|
|
28
71
|
* json_type
|
|
29
72
|
*============================================================*/
|
package/src/yui_icons.css
CHANGED
|
@@ -392,3 +392,15 @@
|
|
|
392
392
|
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 448 512'%3E%3Cpath fill-rule='evenodd' d='M190 32h68l150 448h-72l-40-120H152l-40 120H40L190 32zM172 300h104l-52-158-52 158z'/%3E%3C/svg%3E");
|
|
393
393
|
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 448 512'%3E%3Cpath fill-rule='evenodd' d='M190 32h68l150 448h-72l-40-120H152l-40 120H40L190 32zM172 300h104l-52-158-52 158z'/%3E%3C/svg%3E");
|
|
394
394
|
}
|
|
395
|
+
|
|
396
|
+
/* Two columns, for "a second pane beside the first" (the JSON pad). */
|
|
397
|
+
.yi-columns::before {
|
|
398
|
+
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath fill-rule='evenodd' d='M0 96C0 60.7 28.7 32 64 32h384c35.3 0 64 28.7 64 64v320c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V96zm64 32v288h160V128H64zm224 0v288h160V128H288z'/%3E%3C/svg%3E");
|
|
399
|
+
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath fill-rule='evenodd' d='M0 96C0 60.7 28.7 32 64 32h384c35.3 0 64 28.7 64 64v320c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V96zm64 32v288h160V128H64zm224 0v288h160V128H288z'/%3E%3C/svg%3E");
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/* Two opposite arrows, for "compare" (the JSON pad's differences). */
|
|
403
|
+
.yi-right-left::before {
|
|
404
|
+
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath d='M352 64L480 160 352 256V192H64V128H352ZM160 256L32 352 160 448V384H448V320H160Z'/%3E%3C/svg%3E");
|
|
405
|
+
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath d='M352 64L480 160 352 256V192H64V128H352ZM160 256L32 352 160 448V384H448V320H160Z'/%3E%3C/svg%3E");
|
|
406
|
+
}
|
|
@@ -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};
|