@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
|
@@ -0,0 +1,907 @@
|
|
|
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, and a second one to compare it
|
|
6
|
+
* with.
|
|
7
|
+
*
|
|
8
|
+
* The viewer (C_YUI_JSON: the lazy tree, the raw text and the
|
|
9
|
+
* graph) only ever showed what an app fetched. A JSON that came
|
|
10
|
+
* from anywhere else -- a log line, a config file, an answer copied
|
|
11
|
+
* from a terminal -- had to be read in whatever the operator had
|
|
12
|
+
* at hand. This is the blank page for it: paste, and read it the
|
|
13
|
+
* way every other document of the app is read.
|
|
14
|
+
*
|
|
15
|
+
* Two documents are the other half of the job: the config that
|
|
16
|
+
* works and the one that does not, a record before and after. The
|
|
17
|
+
* second pane is there on demand ("second json"), and "compare"
|
|
18
|
+
* puts their differences in place of the two viewers -- one row
|
|
19
|
+
* per id of the flat form (json2flat), the form a person reads
|
|
20
|
+
* when two configurations disagree.
|
|
21
|
+
*
|
|
22
|
+
* DOM (UPPER_SNAKE logical classes):
|
|
23
|
+
*
|
|
24
|
+
* JSON_PAD the root
|
|
25
|
+
* JSON_PAD_TOOLBAR the notice and the two toggles
|
|
26
|
+
* JSON_PAD_NOTICE "json not kept": the storage refused
|
|
27
|
+
* JSON_PAD_SECOND show / hide the second pane
|
|
28
|
+
* JSON_PAD_COMPARE show / hide the differences
|
|
29
|
+
* JSON_PAD_PANES the panes, side by side (stacked when
|
|
30
|
+
* the pad is narrow)
|
|
31
|
+
* JSON_PAD_PANE JSON_PAD_PANE_A / JSON_PAD_PANE_B
|
|
32
|
+
* JSON_PAD_BAR the text area and its two buttons
|
|
33
|
+
* JSON_PAD_INPUT where the text goes
|
|
34
|
+
* JSON_PAD_VIEW read what is in the text area
|
|
35
|
+
* JSON_PAD_CLEAR empty the pane
|
|
36
|
+
* JSON_PAD_ERROR "invalid JSON" and the parser's reason
|
|
37
|
+
* JSON_PAD_BODY the pane's C_YUI_JSON child
|
|
38
|
+
* JSON_PAD_DIFF the differences: JSON_PAD_DIFF_SUMMARY
|
|
39
|
+
* and JSON_PAD_DIFF_TABLE, one
|
|
40
|
+
* JSON_PAD_DIFF_ROW per id
|
|
41
|
+
*
|
|
42
|
+
* Every way in is an event, and a pane event names its pane
|
|
43
|
+
* (`pane`: "a" or "b"):
|
|
44
|
+
*
|
|
45
|
+
* EV_PASTE {pane, text} a paste ANYWHERE in a pane replaces
|
|
46
|
+
* its document and shows it at once --
|
|
47
|
+
* a pane holds one document, not a
|
|
48
|
+
* text to edit
|
|
49
|
+
* EV_VIEW {pane} the text area as it is (typed, or
|
|
50
|
+
* edited after a paste); also Ctrl+Enter
|
|
51
|
+
* EV_CLEAR {pane} back to a blank pane
|
|
52
|
+
* EV_TEXT_CHANGED {pane} the text area was edited: keep it
|
|
53
|
+
* EV_TOGGLE_SECOND show / hide the second pane
|
|
54
|
+
* EV_TOGGLE_DIFF show / hide the differences
|
|
55
|
+
*
|
|
56
|
+
* FSM: the states are the LAYOUT -- ST_SINGLE (one pane), ST_DUAL
|
|
57
|
+
* (two), ST_DIFF (two, with their differences in place of the
|
|
58
|
+
* viewers). Comparing needs two panes, so EV_TOGGLE_DIFF is not
|
|
59
|
+
* defined in ST_SINGLE and its button is not on screen there.
|
|
60
|
+
* EV_EXPAND_PATH, which a viewer publishes when a `__collapsed__`
|
|
61
|
+
* sentinel is opened, is answered with EV_SUBTREE_ERROR to the
|
|
62
|
+
* viewer that asked: a pasted document has no backend to fetch the
|
|
63
|
+
* rest from -- the source truncated it, and the pad says so. It is
|
|
64
|
+
* not defined in ST_DIFF, where no viewer is on screen.
|
|
65
|
+
*
|
|
66
|
+
* What is kept: both texts and the layout, in localStorage under
|
|
67
|
+
* `storage_key` (empty: nothing is kept), written when a text is
|
|
68
|
+
* pasted, viewed, cleared or edited and when the layout changes,
|
|
69
|
+
* so the next pad opens as this one was left. localStorage is per
|
|
70
|
+
* browser and per origin, and has a quota: a text too big for it
|
|
71
|
+
* is not kept, the toolbar says so, and the log says why.
|
|
72
|
+
*
|
|
73
|
+
* Hosted by setup_json_pad() (yui_json_pad.js) in a C_YUI_WINDOW;
|
|
74
|
+
* any host can mount `$container` itself.
|
|
75
|
+
*
|
|
76
|
+
* Copyright (c) 2026, ArtGins.
|
|
77
|
+
* All Rights Reserved.
|
|
78
|
+
***********************************************************************/
|
|
79
|
+
import {
|
|
80
|
+
SDATA,
|
|
81
|
+
SDATA_END,
|
|
82
|
+
data_type_t,
|
|
83
|
+
gclass_create,
|
|
84
|
+
gclass_find_by_name,
|
|
85
|
+
log_error,
|
|
86
|
+
log_warning,
|
|
87
|
+
gobj_create_pure_child,
|
|
88
|
+
gobj_read_pointer_attr,
|
|
89
|
+
gobj_read_str_attr,
|
|
90
|
+
gobj_read_attr,
|
|
91
|
+
gobj_write_attr,
|
|
92
|
+
gobj_subscribe_event,
|
|
93
|
+
gobj_send_event,
|
|
94
|
+
gobj_change_state,
|
|
95
|
+
gobj_current_state,
|
|
96
|
+
gobj_start,
|
|
97
|
+
gobj_stop,
|
|
98
|
+
gobj_is_running,
|
|
99
|
+
gobj_short_name,
|
|
100
|
+
gobj_parent,
|
|
101
|
+
createElement2,
|
|
102
|
+
kw_get_local_storage_value,
|
|
103
|
+
kw_set_local_storage_value,
|
|
104
|
+
} from "@yuneta/gobj-js";
|
|
105
|
+
|
|
106
|
+
import {register_c_yui_json} from "./c_yui_json.js";
|
|
107
|
+
import {yui_toolbar_icon, set_pressed_state} from "./yui_toolbar.js";
|
|
108
|
+
import {json_diff_rows} from "./json_view_helpers.js";
|
|
109
|
+
|
|
110
|
+
import {t} from "i18next";
|
|
111
|
+
|
|
112
|
+
import "./c_yui_json_pad.css";
|
|
113
|
+
|
|
114
|
+
/***************************************************************
|
|
115
|
+
* Constants
|
|
116
|
+
***************************************************************/
|
|
117
|
+
const GCLASS_NAME = "C_YUI_JSON_PAD";
|
|
118
|
+
|
|
119
|
+
const PANES = ["a", "b"];
|
|
120
|
+
|
|
121
|
+
/* The layout each state is kept as. */
|
|
122
|
+
const MODE_OF_STATE = {
|
|
123
|
+
ST_SINGLE: "single",
|
|
124
|
+
ST_DUAL: "dual",
|
|
125
|
+
ST_DIFF: "diff"
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/* The tag of each kind of difference. Written out, so every key is
|
|
129
|
+
* a literal the consumers' validate-locales can see. */
|
|
130
|
+
const KIND_TAG = {
|
|
131
|
+
added: () => ['span', {class: 'tag is-success is-light', i18n: 'added'}, 'added'],
|
|
132
|
+
removed: () => ['span', {class: 'tag is-danger is-light', i18n: 'removed'}, 'removed'],
|
|
133
|
+
changed: () => ['span', {class: 'tag is-warning is-light', i18n: 'changed'}, 'changed'],
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/***************************************************************
|
|
137
|
+
* Data
|
|
138
|
+
***************************************************************/
|
|
139
|
+
const attrs_table = [
|
|
140
|
+
SDATA(data_type_t.DTP_POINTER, "subscriber", 0, null, "Subscriber of output events"),
|
|
141
|
+
SDATA(data_type_t.DTP_STRING, "storage_key", 0, "yui_json_pad", "localStorage key the two texts and the layout are kept under. Empty: nothing is kept"),
|
|
142
|
+
SDATA(data_type_t.DTP_POINTER, "$container", 0, null, "HTMLElement root, mounted by the host"),
|
|
143
|
+
SDATA_END()
|
|
144
|
+
];
|
|
145
|
+
|
|
146
|
+
let PRIVATE_DATA = {
|
|
147
|
+
panes: null, /* {a, b}: $pane $input $error $reason $body gobj_json doc has_doc */
|
|
148
|
+
$compare: null, /* the "compare" toggle */
|
|
149
|
+
$notice: null, /* "json not kept" */
|
|
150
|
+
$diff: null, /* the differences */
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
let __gclass__ = null;
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
/******************************
|
|
159
|
+
* Framework Methods
|
|
160
|
+
******************************/
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
/***************************************************************
|
|
166
|
+
* Framework Method: Create
|
|
167
|
+
***************************************************************/
|
|
168
|
+
function mt_create(gobj)
|
|
169
|
+
{
|
|
170
|
+
let priv = gobj.priv;
|
|
171
|
+
|
|
172
|
+
/*
|
|
173
|
+
* CHILD subscription model
|
|
174
|
+
*/
|
|
175
|
+
let subscriber = gobj_read_pointer_attr(gobj, "subscriber");
|
|
176
|
+
if(!subscriber) {
|
|
177
|
+
subscriber = gobj_parent(gobj);
|
|
178
|
+
}
|
|
179
|
+
gobj_subscribe_event(gobj, null, {}, subscriber);
|
|
180
|
+
|
|
181
|
+
build_ui(gobj);
|
|
182
|
+
|
|
183
|
+
/*
|
|
184
|
+
* One viewer per pane, as pure children: each publishes
|
|
185
|
+
* EV_EXPAND_PATH to its parent (this gobj).
|
|
186
|
+
*/
|
|
187
|
+
for(const id of PANES) {
|
|
188
|
+
let pane = priv.panes[id];
|
|
189
|
+
pane.gobj_json = gobj_create_pure_child(`json_pad_view_${id}`, "C_YUI_JSON", {}, gobj);
|
|
190
|
+
let $view = pane.gobj_json? gobj_read_attr(pane.gobj_json, "$container") : null;
|
|
191
|
+
if(!$view) {
|
|
192
|
+
log_error(`${gobj_short_name(gobj)}: C_YUI_JSON without $container`);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
pane.$body.appendChild($view);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/***************************************************************
|
|
200
|
+
* Framework Method: Start
|
|
201
|
+
***************************************************************/
|
|
202
|
+
function mt_start(gobj)
|
|
203
|
+
{
|
|
204
|
+
let priv = gobj.priv;
|
|
205
|
+
|
|
206
|
+
for(const id of PANES) {
|
|
207
|
+
if(priv.panes[id].gobj_json) {
|
|
208
|
+
gobj_start(priv.panes[id].gobj_json);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
restore_state(gobj);
|
|
213
|
+
|
|
214
|
+
/* The caret goes where a paste lands. */
|
|
215
|
+
priv.panes.a.$input.focus();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/***************************************************************
|
|
219
|
+
* Framework Method: Stop
|
|
220
|
+
***************************************************************/
|
|
221
|
+
function mt_stop(gobj)
|
|
222
|
+
{
|
|
223
|
+
let priv = gobj.priv;
|
|
224
|
+
|
|
225
|
+
/* A text typed and never left has had no `change` yet. */
|
|
226
|
+
save_state(gobj);
|
|
227
|
+
|
|
228
|
+
for(const id of PANES) {
|
|
229
|
+
let gobj_json = priv.panes[id].gobj_json;
|
|
230
|
+
if(gobj_json && gobj_is_running(gobj_json)) {
|
|
231
|
+
gobj_stop(gobj_json);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/***************************************************************
|
|
237
|
+
* Framework Method: Destroy
|
|
238
|
+
***************************************************************/
|
|
239
|
+
function mt_destroy(gobj)
|
|
240
|
+
{
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
/***************************
|
|
247
|
+
* Local Methods
|
|
248
|
+
***************************/
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
/***************************************************************
|
|
254
|
+
* A button of the pad: an icon, its label, and an event.
|
|
255
|
+
***************************************************************/
|
|
256
|
+
function pad_button(gobj, cls, icon, key, event_name, kw)
|
|
257
|
+
{
|
|
258
|
+
return ['button', {class: `${cls} button`, type: 'button',
|
|
259
|
+
title: t(key), 'data-i18n-title': key,
|
|
260
|
+
'aria-label': t(key), 'data-i18n-aria-label': key},
|
|
261
|
+
[
|
|
262
|
+
yui_toolbar_icon(icon),
|
|
263
|
+
['span', {style: 'padding-left:5px;', i18n: key}, key]
|
|
264
|
+
],
|
|
265
|
+
{
|
|
266
|
+
click: (evt) => {
|
|
267
|
+
evt.stopPropagation();
|
|
268
|
+
gobj_send_event(gobj, event_name, {...kw}, gobj);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
];
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/***************************************************************
|
|
275
|
+
* The toolbar, the two panes, and the differences.
|
|
276
|
+
***************************************************************/
|
|
277
|
+
function build_ui(gobj)
|
|
278
|
+
{
|
|
279
|
+
let priv = gobj.priv;
|
|
280
|
+
|
|
281
|
+
priv.$notice = createElement2(
|
|
282
|
+
['div', {class: 'JSON_PAD_NOTICE is-hidden has-text-danger is-size-7', role: 'status'}, [
|
|
283
|
+
['span', {style: 'font-weight:600;', i18n: 'json not kept'}, 'json not kept']
|
|
284
|
+
]]
|
|
285
|
+
);
|
|
286
|
+
priv.$compare = createElement2(
|
|
287
|
+
pad_button(gobj, 'JSON_PAD_COMPARE is-hidden', 'yi-right-left', 'compare', 'EV_TOGGLE_DIFF', {})
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
priv.panes = {
|
|
291
|
+
a: build_pane(gobj, "a", "first json"),
|
|
292
|
+
b: build_pane(gobj, "b", "second json"),
|
|
293
|
+
};
|
|
294
|
+
priv.panes.b.$pane.classList.add('is-hidden');
|
|
295
|
+
|
|
296
|
+
priv.$diff = createElement2(
|
|
297
|
+
['div', {class: 'JSON_PAD_DIFF is-hidden'}, []]
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
let $container = createElement2(
|
|
301
|
+
['div', {class: 'C_YUI_JSON_PAD JSON_PAD'}, [
|
|
302
|
+
['div', {class: 'JSON_PAD_TOOLBAR'}, [
|
|
303
|
+
priv.$notice,
|
|
304
|
+
pad_button(gobj, 'JSON_PAD_SECOND', 'yi-columns', 'second json', 'EV_TOGGLE_SECOND', {}),
|
|
305
|
+
priv.$compare
|
|
306
|
+
]],
|
|
307
|
+
['div', {class: 'JSON_PAD_PANES'}, [
|
|
308
|
+
priv.panes.a.$pane,
|
|
309
|
+
priv.panes.b.$pane
|
|
310
|
+
]],
|
|
311
|
+
priv.$diff
|
|
312
|
+
]]
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
gobj_write_attr(gobj, "$container", $container);
|
|
316
|
+
|
|
317
|
+
press_toggle(gobj, '.JSON_PAD_SECOND', false);
|
|
318
|
+
press_toggle(gobj, '.JSON_PAD_COMPARE', false);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/***************************************************************
|
|
322
|
+
* One pane: the text area and its buttons, the error line, and
|
|
323
|
+
* the body its viewer is mounted in. `name_key` names the pane
|
|
324
|
+
* for a reader (its text area's title and aria-label).
|
|
325
|
+
***************************************************************/
|
|
326
|
+
function build_pane(gobj, id, name_key)
|
|
327
|
+
{
|
|
328
|
+
let pane = {
|
|
329
|
+
$pane: null,
|
|
330
|
+
$input: null,
|
|
331
|
+
$error: null,
|
|
332
|
+
$reason: null,
|
|
333
|
+
$body: null,
|
|
334
|
+
gobj_json: null,
|
|
335
|
+
doc: null,
|
|
336
|
+
has_doc: false, /* `null` is a document too */
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
pane.$input = createElement2(
|
|
340
|
+
['textarea', {class: 'JSON_PAD_INPUT textarea', rows: 3, spellcheck: 'false',
|
|
341
|
+
placeholder: t('paste json here'), 'data-i18n-placeholder': 'paste json here',
|
|
342
|
+
title: t(name_key), 'data-i18n-title': name_key,
|
|
343
|
+
'aria-label': t(name_key), 'data-i18n-aria-label': name_key,
|
|
344
|
+
style: 'font-family:monospace; resize:vertical; min-height:4.5em;'}, '', {
|
|
345
|
+
keydown: (evt) => {
|
|
346
|
+
if((evt.ctrlKey || evt.metaKey) && evt.key === 'Enter') {
|
|
347
|
+
evt.preventDefault();
|
|
348
|
+
gobj_send_event(gobj, "EV_VIEW", {pane: id}, gobj);
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
change: () => {
|
|
352
|
+
gobj_send_event(gobj, "EV_TEXT_CHANGED", {pane: id}, gobj);
|
|
353
|
+
}
|
|
354
|
+
}]
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
pane.$reason = createElement2(
|
|
358
|
+
['span', {class: 'JSON_PAD_ERROR_REASON', style: 'margin-left:.5em; font-family:monospace;'}, '']
|
|
359
|
+
);
|
|
360
|
+
pane.$error = createElement2(
|
|
361
|
+
['div', {class: 'JSON_PAD_ERROR is-hidden has-text-danger is-size-7', role: 'alert'}, [
|
|
362
|
+
['span', {class: 'JSON_PAD_ERROR_LABEL', style: 'font-weight:600;', i18n: 'invalid json'},
|
|
363
|
+
'invalid json'],
|
|
364
|
+
pane.$reason
|
|
365
|
+
]]
|
|
366
|
+
);
|
|
367
|
+
pane.$body = createElement2(
|
|
368
|
+
['div', {class: 'JSON_PAD_BODY',
|
|
369
|
+
style: 'flex:1 1 auto; min-height:0; display:flex; flex-direction:column;'}, []]
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
pane.$pane = createElement2(
|
|
373
|
+
['div', {class: `JSON_PAD_PANE JSON_PAD_PANE_${id.toUpperCase()}`}, [
|
|
374
|
+
['div', {class: 'JSON_PAD_BAR',
|
|
375
|
+
style: 'display:flex; gap:.5rem; align-items:flex-start;'}, [
|
|
376
|
+
['div', {style: 'flex:1 1 auto; min-width:0;'}, [pane.$input]],
|
|
377
|
+
['div', {style: 'flex:0 0 auto; display:flex; flex-direction:column; gap:.5rem;'}, [
|
|
378
|
+
pad_button(gobj, 'JSON_PAD_VIEW', 'yi-eye', 'view', 'EV_VIEW', {pane: id}),
|
|
379
|
+
pad_button(gobj, 'JSON_PAD_CLEAR', 'yi-broom', 'clear', 'EV_CLEAR', {pane: id}),
|
|
380
|
+
]]
|
|
381
|
+
]],
|
|
382
|
+
pane.$error,
|
|
383
|
+
pane.$body
|
|
384
|
+
], {
|
|
385
|
+
/* A paste anywhere in the pane is its document: it replaces
|
|
386
|
+
* what the text area held instead of landing at the caret. */
|
|
387
|
+
paste: (evt) => {
|
|
388
|
+
let text = evt.clipboardData? evt.clipboardData.getData("text") : "";
|
|
389
|
+
if(!text) {
|
|
390
|
+
return; /* an image or nothing: the browser's business */
|
|
391
|
+
}
|
|
392
|
+
evt.preventDefault();
|
|
393
|
+
gobj_send_event(gobj, "EV_PASTE", {pane: id, text: text}, gobj);
|
|
394
|
+
}
|
|
395
|
+
}]
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
return pane;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/***************************************************************
|
|
402
|
+
* The pane a pane event names.
|
|
403
|
+
***************************************************************/
|
|
404
|
+
function pane_of(gobj, kw)
|
|
405
|
+
{
|
|
406
|
+
let priv = gobj.priv;
|
|
407
|
+
let id = kw? kw.pane : undefined;
|
|
408
|
+
|
|
409
|
+
if(!PANES.includes(id)) {
|
|
410
|
+
log_error(`${gobj_short_name(gobj)}: no pane '${id}'`);
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
return priv.panes[id];
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/***************************************************************
|
|
417
|
+
* A toggle of the toolbar, pressed or not -- for the eye and
|
|
418
|
+
* for a reader.
|
|
419
|
+
***************************************************************/
|
|
420
|
+
function press_toggle(gobj, selector, on)
|
|
421
|
+
{
|
|
422
|
+
let $container = gobj_read_attr(gobj, "$container");
|
|
423
|
+
|
|
424
|
+
set_pressed_state($container, selector, on);
|
|
425
|
+
$container.querySelectorAll(selector).forEach(($el) => {
|
|
426
|
+
$el.setAttribute('aria-pressed', on? 'true' : 'false');
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/***************************************************************
|
|
431
|
+
* Read `text` as JSON and show it in `pane`, or say why it is
|
|
432
|
+
* not. A text that is not JSON leaves the last document up.
|
|
433
|
+
***************************************************************/
|
|
434
|
+
function show_text(gobj, pane, text)
|
|
435
|
+
{
|
|
436
|
+
let src = String(text || "").trim();
|
|
437
|
+
|
|
438
|
+
if(!src) {
|
|
439
|
+
clear_pane(gobj, pane, false);
|
|
440
|
+
return 0;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
let doc;
|
|
444
|
+
try {
|
|
445
|
+
doc = JSON.parse(src);
|
|
446
|
+
} catch(e) {
|
|
447
|
+
/* The reader's text, not a fault of the app: said on the pad, and
|
|
448
|
+
* a warning for whoever reads the console. */
|
|
449
|
+
log_warning(`${gobj_short_name(gobj)}: the pasted text is not JSON: ${e.message}`);
|
|
450
|
+
pane.$reason.textContent = e.message;
|
|
451
|
+
pane.$error.classList.remove('is-hidden');
|
|
452
|
+
return -1;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
pane.$error.classList.add('is-hidden');
|
|
456
|
+
pane.$reason.textContent = "";
|
|
457
|
+
pane.doc = doc;
|
|
458
|
+
pane.has_doc = true;
|
|
459
|
+
gobj_send_event(pane.gobj_json, "EV_SET_JSON", {json: doc}, gobj);
|
|
460
|
+
return 0;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/***************************************************************
|
|
464
|
+
* A blank pane again; `with_text` empties the text area too.
|
|
465
|
+
***************************************************************/
|
|
466
|
+
function clear_pane(gobj, pane, with_text)
|
|
467
|
+
{
|
|
468
|
+
if(with_text) {
|
|
469
|
+
pane.$input.value = "";
|
|
470
|
+
}
|
|
471
|
+
pane.$error.classList.add('is-hidden');
|
|
472
|
+
pane.$reason.textContent = "";
|
|
473
|
+
pane.doc = null;
|
|
474
|
+
pane.has_doc = false;
|
|
475
|
+
gobj_send_event(pane.gobj_json, "EV_SET_JSON", {json: null}, gobj);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/***************************************************************
|
|
479
|
+
* A pane changed its document: the differences follow it, and
|
|
480
|
+
* the texts are kept.
|
|
481
|
+
***************************************************************/
|
|
482
|
+
function document_changed(gobj)
|
|
483
|
+
{
|
|
484
|
+
if(gobj_current_state(gobj) === "ST_DIFF") {
|
|
485
|
+
render_diff(gobj);
|
|
486
|
+
}
|
|
487
|
+
save_state(gobj);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/***************************************************************
|
|
491
|
+
* Enter one of the three layouts. The state IS the layout;
|
|
492
|
+
* this is what the screen shows for it.
|
|
493
|
+
***************************************************************/
|
|
494
|
+
function enter_layout(gobj, state)
|
|
495
|
+
{
|
|
496
|
+
let priv = gobj.priv;
|
|
497
|
+
let $container = gobj_read_attr(gobj, "$container");
|
|
498
|
+
let second = (state !== "ST_SINGLE");
|
|
499
|
+
let diff = (state === "ST_DIFF");
|
|
500
|
+
|
|
501
|
+
gobj_change_state(gobj, state);
|
|
502
|
+
|
|
503
|
+
priv.panes.b.$pane.classList.toggle('is-hidden', !second);
|
|
504
|
+
priv.$compare.classList.toggle('is-hidden', !second);
|
|
505
|
+
press_toggle(gobj, '.JSON_PAD_SECOND', second);
|
|
506
|
+
press_toggle(gobj, '.JSON_PAD_COMPARE', diff);
|
|
507
|
+
|
|
508
|
+
$container.classList.toggle('is-comparing', diff);
|
|
509
|
+
for(const id of PANES) {
|
|
510
|
+
priv.panes[id].$body.classList.toggle('is-hidden', diff);
|
|
511
|
+
}
|
|
512
|
+
priv.$diff.classList.toggle('is-hidden', !diff);
|
|
513
|
+
|
|
514
|
+
if(diff) {
|
|
515
|
+
render_diff(gobj);
|
|
516
|
+
} else {
|
|
517
|
+
/* A viewer that was hidden, or had another width, drew for a
|
|
518
|
+
* box it no longer has: its graph measures its canvas again. */
|
|
519
|
+
for(const id of PANES) {
|
|
520
|
+
if(id === "b" && !second) {
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
gobj_send_event(priv.panes[id].gobj_json, "EV_REFRESH", {}, gobj);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
save_state(gobj);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/***************************************************************
|
|
531
|
+
* The differences of the two documents, in place of the two
|
|
532
|
+
* viewers. Only a pane that shows a document takes part: a
|
|
533
|
+
* text that is not JSON leaves its last document, as on screen.
|
|
534
|
+
***************************************************************/
|
|
535
|
+
function render_diff(gobj)
|
|
536
|
+
{
|
|
537
|
+
let priv = gobj.priv;
|
|
538
|
+
let a = priv.panes.a;
|
|
539
|
+
let b = priv.panes.b;
|
|
540
|
+
|
|
541
|
+
priv.$diff.replaceChildren();
|
|
542
|
+
|
|
543
|
+
if(!a.has_doc || !b.has_doc) {
|
|
544
|
+
priv.$diff.appendChild(createElement2(
|
|
545
|
+
['p', {class: 'JSON_PAD_DIFF_MESSAGE', i18n: 'compare needs two json'},
|
|
546
|
+
'compare needs two json']
|
|
547
|
+
));
|
|
548
|
+
return 0;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
let rows;
|
|
552
|
+
try {
|
|
553
|
+
rows = json_diff_rows(a.doc, b.doc);
|
|
554
|
+
} catch(e) {
|
|
555
|
+
log_warning(`${gobj_short_name(gobj)}: cannot compare: ${e.message}`);
|
|
556
|
+
priv.$diff.appendChild(createElement2(
|
|
557
|
+
['p', {class: 'JSON_PAD_DIFF_MESSAGE has-text-danger'}, [
|
|
558
|
+
['span', {style: 'font-weight:600;', i18n: 'cannot compare'}, 'cannot compare'],
|
|
559
|
+
['span', {class: 'JSON_PAD_DIFF_REASON',
|
|
560
|
+
style: 'margin-left:.5em; font-family:monospace;'}, e.message]
|
|
561
|
+
]]
|
|
562
|
+
));
|
|
563
|
+
return -1;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if(rows.length === 0) {
|
|
567
|
+
priv.$diff.appendChild(createElement2(
|
|
568
|
+
['p', {class: 'JSON_PAD_DIFF_MESSAGE has-text-success', i18n: 'no differences'},
|
|
569
|
+
'no differences']
|
|
570
|
+
));
|
|
571
|
+
return 0;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
let count = {added: 0, removed: 0, changed: 0};
|
|
575
|
+
for(const row of rows) {
|
|
576
|
+
count[row.kind]++;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
let value_cell = (cls, row, side) => {
|
|
580
|
+
let has = Object.prototype.hasOwnProperty.call(row, side);
|
|
581
|
+
return ['td', {class: cls}, has? JSON.stringify(row[side]) : ''];
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
priv.$diff.appendChild(createElement2(
|
|
585
|
+
['div', {class: 'JSON_PAD_DIFF_SUMMARY'},
|
|
586
|
+
["removed", "added", "changed"].map((kind) => {
|
|
587
|
+
return ['span', {class: 'JSON_PAD_DIFF_COUNT'}, [
|
|
588
|
+
KIND_TAG[kind](),
|
|
589
|
+
['strong', {}, String(count[kind])]
|
|
590
|
+
]];
|
|
591
|
+
})
|
|
592
|
+
]
|
|
593
|
+
));
|
|
594
|
+
|
|
595
|
+
priv.$diff.appendChild(createElement2(
|
|
596
|
+
['table', {class: 'JSON_PAD_DIFF_TABLE table is-fullwidth is-narrow is-hoverable'}, [
|
|
597
|
+
['thead', {}, [
|
|
598
|
+
['tr', {}, [
|
|
599
|
+
['th', {}, ''],
|
|
600
|
+
['th', {i18n: 'path'}, 'path'],
|
|
601
|
+
['th', {i18n: 'first json'}, 'first json'],
|
|
602
|
+
['th', {i18n: 'second json'}, 'second json']
|
|
603
|
+
]]
|
|
604
|
+
]],
|
|
605
|
+
['tbody', {}, rows.map((row) => {
|
|
606
|
+
return ['tr', {class: `JSON_PAD_DIFF_ROW JSON_PAD_DIFF_${row.kind.toUpperCase()}`}, [
|
|
607
|
+
['td', {}, [KIND_TAG[row.kind]()]],
|
|
608
|
+
['td', {class: 'JSON_PAD_DIFF_PATH'}, row.id],
|
|
609
|
+
value_cell('JSON_PAD_DIFF_FROM', row, 'from'),
|
|
610
|
+
value_cell('JSON_PAD_DIFF_TO', row, 'to')
|
|
611
|
+
]];
|
|
612
|
+
})]
|
|
613
|
+
]]
|
|
614
|
+
));
|
|
615
|
+
return 0;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/***************************************************************
|
|
619
|
+
* Keep both texts and the layout.
|
|
620
|
+
***************************************************************/
|
|
621
|
+
function save_state(gobj)
|
|
622
|
+
{
|
|
623
|
+
let priv = gobj.priv;
|
|
624
|
+
let key = gobj_read_str_attr(gobj, "storage_key");
|
|
625
|
+
|
|
626
|
+
if(!key) {
|
|
627
|
+
return 0;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
let ret = kw_set_local_storage_value(key, {
|
|
631
|
+
a: priv.panes.a.$input.value,
|
|
632
|
+
b: priv.panes.b.$input.value,
|
|
633
|
+
mode: MODE_OF_STATE[gobj_current_state(gobj)] || "single"
|
|
634
|
+
});
|
|
635
|
+
/* On -1 the reason is already logged; the pad says it here. */
|
|
636
|
+
priv.$notice.classList.toggle('is-hidden', ret === 0);
|
|
637
|
+
return ret;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/***************************************************************
|
|
641
|
+
* The pad as the last one was left: its texts, shown, and its
|
|
642
|
+
* layout -- through the same events a reader would send.
|
|
643
|
+
***************************************************************/
|
|
644
|
+
function restore_state(gobj)
|
|
645
|
+
{
|
|
646
|
+
let priv = gobj.priv;
|
|
647
|
+
let key = gobj_read_str_attr(gobj, "storage_key");
|
|
648
|
+
|
|
649
|
+
if(!key) {
|
|
650
|
+
return 0;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
let kept = kw_get_local_storage_value(key, null, false);
|
|
654
|
+
if(kept === null || kept === undefined) {
|
|
655
|
+
return 0; /* nothing kept yet */
|
|
656
|
+
}
|
|
657
|
+
if(typeof kept !== "object" || Array.isArray(kept)) {
|
|
658
|
+
log_warning(`${gobj_short_name(gobj)}: '${key}' in localStorage is not what the pad keeps`);
|
|
659
|
+
return -1;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
for(const id of PANES) {
|
|
663
|
+
if(typeof kept[id] === "string") {
|
|
664
|
+
priv.panes[id].$input.value = kept[id];
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
for(const id of PANES) {
|
|
668
|
+
if(priv.panes[id].$input.value.trim()) {
|
|
669
|
+
gobj_send_event(gobj, "EV_VIEW", {pane: id}, gobj);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
if(kept.mode === "dual" || kept.mode === "diff") {
|
|
673
|
+
gobj_send_event(gobj, "EV_TOGGLE_SECOND", {}, gobj);
|
|
674
|
+
}
|
|
675
|
+
if(kept.mode === "diff") {
|
|
676
|
+
gobj_send_event(gobj, "EV_TOGGLE_DIFF", {}, gobj);
|
|
677
|
+
}
|
|
678
|
+
return 0;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
/***************************
|
|
685
|
+
* Actions
|
|
686
|
+
***************************/
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
/***************************************************************
|
|
692
|
+
* A paste: it becomes the pane's text area and its document
|
|
693
|
+
* at once.
|
|
694
|
+
***************************************************************/
|
|
695
|
+
function ac_paste(gobj, event, kw, src)
|
|
696
|
+
{
|
|
697
|
+
let pane = pane_of(gobj, kw);
|
|
698
|
+
if(!pane) {
|
|
699
|
+
return -1; // Error already logged
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
let text = (typeof kw.text === "string")? kw.text : "";
|
|
703
|
+
pane.$input.value = text;
|
|
704
|
+
let ret = show_text(gobj, pane, text);
|
|
705
|
+
document_changed(gobj);
|
|
706
|
+
return ret;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/***************************************************************
|
|
710
|
+
* The pane's text area as it is now.
|
|
711
|
+
***************************************************************/
|
|
712
|
+
function ac_view(gobj, event, kw, src)
|
|
713
|
+
{
|
|
714
|
+
let pane = pane_of(gobj, kw);
|
|
715
|
+
if(!pane) {
|
|
716
|
+
return -1; // Error already logged
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
let ret = show_text(gobj, pane, pane.$input.value);
|
|
720
|
+
document_changed(gobj);
|
|
721
|
+
return ret;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/***************************************************************
|
|
725
|
+
* A blank pane again.
|
|
726
|
+
***************************************************************/
|
|
727
|
+
function ac_clear(gobj, event, kw, src)
|
|
728
|
+
{
|
|
729
|
+
let pane = pane_of(gobj, kw);
|
|
730
|
+
if(!pane) {
|
|
731
|
+
return -1; // Error already logged
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
clear_pane(gobj, pane, true);
|
|
735
|
+
pane.$input.focus();
|
|
736
|
+
document_changed(gobj);
|
|
737
|
+
return 0;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/***************************************************************
|
|
741
|
+
* A text area was edited and left: keep what it holds. What
|
|
742
|
+
* is SHOWN changes only with a view, as before.
|
|
743
|
+
***************************************************************/
|
|
744
|
+
function ac_text_changed(gobj, event, kw, src)
|
|
745
|
+
{
|
|
746
|
+
if(!pane_of(gobj, kw)) {
|
|
747
|
+
return -1; // Error already logged
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
save_state(gobj);
|
|
751
|
+
return 0;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/***************************************************************
|
|
755
|
+
* The second pane, beside the first.
|
|
756
|
+
***************************************************************/
|
|
757
|
+
function ac_show_second(gobj, event, kw, src)
|
|
758
|
+
{
|
|
759
|
+
let priv = gobj.priv;
|
|
760
|
+
|
|
761
|
+
enter_layout(gobj, "ST_DUAL");
|
|
762
|
+
priv.panes.b.$input.focus();
|
|
763
|
+
return 0;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/***************************************************************
|
|
767
|
+
* One pane again. Its text is kept: hiding is not clearing.
|
|
768
|
+
***************************************************************/
|
|
769
|
+
function ac_hide_second(gobj, event, kw, src)
|
|
770
|
+
{
|
|
771
|
+
enter_layout(gobj, "ST_SINGLE");
|
|
772
|
+
return 0;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/***************************************************************
|
|
776
|
+
* The differences, in place of the viewers.
|
|
777
|
+
***************************************************************/
|
|
778
|
+
function ac_show_diff(gobj, event, kw, src)
|
|
779
|
+
{
|
|
780
|
+
enter_layout(gobj, "ST_DIFF");
|
|
781
|
+
return 0;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/***************************************************************
|
|
785
|
+
* The viewers again.
|
|
786
|
+
***************************************************************/
|
|
787
|
+
function ac_hide_diff(gobj, event, kw, src)
|
|
788
|
+
{
|
|
789
|
+
enter_layout(gobj, "ST_DUAL");
|
|
790
|
+
return 0;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/***************************************************************
|
|
794
|
+
* A viewer asks for a `__collapsed__` subtree. A pasted
|
|
795
|
+
* document has no backend to ask for it: the SOURCE truncated
|
|
796
|
+
* it, and that is the answer, to the viewer that asked.
|
|
797
|
+
***************************************************************/
|
|
798
|
+
function ac_expand_path(gobj, event, kw, src)
|
|
799
|
+
{
|
|
800
|
+
gobj_send_event(src, "EV_SUBTREE_ERROR", {
|
|
801
|
+
path: (kw && kw.path) || "",
|
|
802
|
+
error: t("collapsed in the source")
|
|
803
|
+
}, gobj);
|
|
804
|
+
return 0;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/***************************************************************
|
|
808
|
+
* FSM
|
|
809
|
+
***************************************************************/
|
|
810
|
+
/*---------------------------------------------*
|
|
811
|
+
* Global methods table
|
|
812
|
+
*---------------------------------------------*/
|
|
813
|
+
const gmt = {
|
|
814
|
+
mt_create: mt_create,
|
|
815
|
+
mt_start: mt_start,
|
|
816
|
+
mt_stop: mt_stop,
|
|
817
|
+
mt_destroy: mt_destroy
|
|
818
|
+
};
|
|
819
|
+
|
|
820
|
+
/***************************************************************
|
|
821
|
+
* Create the GClass
|
|
822
|
+
***************************************************************/
|
|
823
|
+
function create_gclass(gclass_name)
|
|
824
|
+
{
|
|
825
|
+
if(__gclass__) {
|
|
826
|
+
log_error(`GClass ALREADY created: ${gclass_name}`);
|
|
827
|
+
return -1;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/*---------------------------------------------*
|
|
831
|
+
* States
|
|
832
|
+
*---------------------------------------------*/
|
|
833
|
+
const states = [
|
|
834
|
+
["ST_SINGLE", [
|
|
835
|
+
["EV_PASTE", ac_paste, null],
|
|
836
|
+
["EV_VIEW", ac_view, null],
|
|
837
|
+
["EV_CLEAR", ac_clear, null],
|
|
838
|
+
["EV_TEXT_CHANGED", ac_text_changed, null],
|
|
839
|
+
["EV_TOGGLE_SECOND", ac_show_second, null],
|
|
840
|
+
["EV_EXPAND_PATH", ac_expand_path, null],
|
|
841
|
+
]],
|
|
842
|
+
["ST_DUAL", [
|
|
843
|
+
["EV_PASTE", ac_paste, null],
|
|
844
|
+
["EV_VIEW", ac_view, null],
|
|
845
|
+
["EV_CLEAR", ac_clear, null],
|
|
846
|
+
["EV_TEXT_CHANGED", ac_text_changed, null],
|
|
847
|
+
["EV_TOGGLE_SECOND", ac_hide_second, null],
|
|
848
|
+
["EV_TOGGLE_DIFF", ac_show_diff, null],
|
|
849
|
+
["EV_EXPAND_PATH", ac_expand_path, null],
|
|
850
|
+
]],
|
|
851
|
+
["ST_DIFF", [
|
|
852
|
+
["EV_PASTE", ac_paste, null],
|
|
853
|
+
["EV_VIEW", ac_view, null],
|
|
854
|
+
["EV_CLEAR", ac_clear, null],
|
|
855
|
+
["EV_TEXT_CHANGED", ac_text_changed, null],
|
|
856
|
+
["EV_TOGGLE_SECOND", ac_hide_second, null],
|
|
857
|
+
["EV_TOGGLE_DIFF", ac_hide_diff, null],
|
|
858
|
+
]]
|
|
859
|
+
];
|
|
860
|
+
|
|
861
|
+
/*---------------------------------------------*
|
|
862
|
+
* Events
|
|
863
|
+
*---------------------------------------------*/
|
|
864
|
+
const event_types = [
|
|
865
|
+
["EV_PASTE", 0],
|
|
866
|
+
["EV_VIEW", 0],
|
|
867
|
+
["EV_CLEAR", 0],
|
|
868
|
+
["EV_TEXT_CHANGED", 0],
|
|
869
|
+
["EV_TOGGLE_SECOND", 0],
|
|
870
|
+
["EV_TOGGLE_DIFF", 0],
|
|
871
|
+
["EV_EXPAND_PATH", 0],
|
|
872
|
+
];
|
|
873
|
+
|
|
874
|
+
__gclass__ = gclass_create(
|
|
875
|
+
gclass_name,
|
|
876
|
+
event_types,
|
|
877
|
+
states,
|
|
878
|
+
gmt,
|
|
879
|
+
0, // lmt,
|
|
880
|
+
attrs_table,
|
|
881
|
+
PRIVATE_DATA,
|
|
882
|
+
0, // authz_table,
|
|
883
|
+
0, // command_table,
|
|
884
|
+
0, // s_user_trace_level
|
|
885
|
+
0 // gclass_flag
|
|
886
|
+
);
|
|
887
|
+
|
|
888
|
+
if(!__gclass__) {
|
|
889
|
+
return -1;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
return 0;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/***************************************************************
|
|
896
|
+
* Register GClass
|
|
897
|
+
***************************************************************/
|
|
898
|
+
function register_c_yui_json_pad()
|
|
899
|
+
{
|
|
900
|
+
/* the viewer it hosts, if the app did not register it */
|
|
901
|
+
if(!gclass_find_by_name("C_YUI_JSON", false)) {
|
|
902
|
+
register_c_yui_json();
|
|
903
|
+
}
|
|
904
|
+
return create_gclass(GCLASS_NAME);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
export { register_c_yui_json_pad };
|