@yuneta/gobj-ui 7.23.143 → 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.
@@ -2,7 +2,8 @@
2
2
  * c_yui_json_pad.js
3
3
  *
4
4
  * C_YUI_JSON_PAD -- a pad to paste JSON from OUTSIDE and read it
5
- * with the library's own viewer.
5
+ * with the library's own viewer, and a second one to compare it
6
+ * with.
6
7
  *
7
8
  * The viewer (C_YUI_JSON: the lazy tree, the raw text and the
8
9
  * graph) only ever showed what an app fetched. A JSON that came
@@ -11,30 +12,63 @@
11
12
  * at hand. This is the blank page for it: paste, and read it the
12
13
  * way every other document of the app is read.
13
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
+ *
14
22
  * DOM (UPPER_SNAKE logical classes):
15
23
  *
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
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"):
23
44
  *
24
- * Every way in is an event:
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
25
55
  *
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
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.
32
65
  *
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.
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.
38
72
  *
39
73
  * Hosted by setup_json_pad() (yui_json_pad.js) in a C_YUI_WINDOW;
40
74
  * any host can mount `$container` itself.
@@ -52,43 +86,68 @@ import {
52
86
  log_warning,
53
87
  gobj_create_pure_child,
54
88
  gobj_read_pointer_attr,
89
+ gobj_read_str_attr,
55
90
  gobj_read_attr,
56
91
  gobj_write_attr,
57
92
  gobj_subscribe_event,
58
93
  gobj_send_event,
59
94
  gobj_change_state,
95
+ gobj_current_state,
60
96
  gobj_start,
61
97
  gobj_stop,
62
98
  gobj_is_running,
63
99
  gobj_short_name,
64
100
  gobj_parent,
65
101
  createElement2,
102
+ kw_get_local_storage_value,
103
+ kw_set_local_storage_value,
66
104
  } from "@yuneta/gobj-js";
67
105
 
68
106
  import {register_c_yui_json} from "./c_yui_json.js";
69
- import {yui_toolbar_icon} from "./yui_toolbar.js";
107
+ import {yui_toolbar_icon, set_pressed_state} from "./yui_toolbar.js";
108
+ import {json_diff_rows} from "./json_view_helpers.js";
70
109
 
71
110
  import {t} from "i18next";
72
111
 
112
+ import "./c_yui_json_pad.css";
113
+
73
114
  /***************************************************************
74
115
  * Constants
75
116
  ***************************************************************/
76
117
  const GCLASS_NAME = "C_YUI_JSON_PAD";
77
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
+
78
136
  /***************************************************************
79
137
  * Data
80
138
  ***************************************************************/
81
139
  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"),
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"),
84
143
  SDATA_END()
85
144
  ];
86
145
 
87
146
  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 */
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 */
92
151
  };
93
152
 
94
153
  let __gclass__ = null;
@@ -122,16 +181,19 @@ function mt_create(gobj)
122
181
  build_ui(gobj);
123
182
 
124
183
  /*
125
- * The viewer, as a pure child: it publishes EV_EXPAND_PATH to its
126
- * parent (this gobj), which declares it in ST_SHOWING.
184
+ * One viewer per pane, as pure children: each publishes
185
+ * EV_EXPAND_PATH to its parent (this gobj).
127
186
  */
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;
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);
133
196
  }
134
- gobj_read_attr(gobj, "$container").querySelector(".JSON_PAD_BODY").appendChild($view);
135
197
  }
136
198
 
137
199
  /***************************************************************
@@ -141,13 +203,16 @@ function mt_start(gobj)
141
203
  {
142
204
  let priv = gobj.priv;
143
205
 
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();
206
+ for(const id of PANES) {
207
+ if(priv.panes[id].gobj_json) {
208
+ gobj_start(priv.panes[id].gobj_json);
209
+ }
150
210
  }
211
+
212
+ restore_state(gobj);
213
+
214
+ /* The caret goes where a paste lands. */
215
+ priv.panes.a.$input.focus();
151
216
  }
152
217
 
153
218
  /***************************************************************
@@ -157,8 +222,14 @@ function mt_stop(gobj)
157
222
  {
158
223
  let priv = gobj.priv;
159
224
 
160
- if(priv.gobj_json && gobj_is_running(priv.gobj_json)) {
161
- gobj_stop(priv.gobj_json);
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
+ }
162
233
  }
163
234
  }
164
235
 
@@ -180,72 +251,138 @@ function mt_destroy(gobj)
180
251
 
181
252
 
182
253
  /***************************************************************
183
- * The pad: the text area and its buttons, the error line, and
184
- * the body the viewer is mounted in.
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.
185
276
  ***************************************************************/
186
277
  function build_ui(gobj)
187
278
  {
188
279
  let priv = gobj.priv;
189
280
 
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
- ];
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 */
205
337
  };
206
338
 
207
- priv.$input = createElement2(
339
+ pane.$input = createElement2(
208
340
  ['textarea', {class: 'JSON_PAD_INPUT textarea', rows: 3, spellcheck: 'false',
209
341
  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',
342
+ title: t(name_key), 'data-i18n-title': name_key,
343
+ 'aria-label': t(name_key), 'data-i18n-aria-label': name_key,
212
344
  style: 'font-family:monospace; resize:vertical; min-height:4.5em;'}, '', {
213
345
  keydown: (evt) => {
214
346
  if((evt.ctrlKey || evt.metaKey) && evt.key === 'Enter') {
215
347
  evt.preventDefault();
216
- gobj_send_event(gobj, "EV_VIEW", {}, gobj);
348
+ gobj_send_event(gobj, "EV_VIEW", {pane: id}, gobj);
217
349
  }
350
+ },
351
+ change: () => {
352
+ gobj_send_event(gobj, "EV_TEXT_CHANGED", {pane: id}, gobj);
218
353
  }
219
354
  }]
220
355
  );
221
356
 
222
- priv.$reason = createElement2(
357
+ pane.$reason = createElement2(
223
358
  ['span', {class: 'JSON_PAD_ERROR_REASON', style: 'margin-left:.5em; font-family:monospace;'}, '']
224
359
  );
225
- priv.$error = createElement2(
360
+ pane.$error = createElement2(
226
361
  ['div', {class: 'JSON_PAD_ERROR is-hidden has-text-danger is-size-7', role: 'alert'}, [
227
362
  ['span', {class: 'JSON_PAD_ERROR_LABEL', style: 'font-weight:600;', i18n: 'invalid json'},
228
363
  'invalid json'],
229
- priv.$reason
364
+ pane.$reason
230
365
  ]]
231
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
+ );
232
371
 
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;'}, [
372
+ pane.$pane = createElement2(
373
+ ['div', {class: `JSON_PAD_PANE JSON_PAD_PANE_${id.toUpperCase()}`}, [
236
374
  ['div', {class: 'JSON_PAD_BAR',
237
375
  style: 'display:flex; gap:.5rem; align-items:flex-start;'}, [
238
- ['div', {style: 'flex:1 1 auto; min-width:0;'}, [priv.$input]],
376
+ ['div', {style: 'flex:1 1 auto; min-width:0;'}, [pane.$input]],
239
377
  ['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'),
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}),
242
380
  ]]
243
381
  ]],
244
- priv.$error,
245
- ['div', {class: 'JSON_PAD_BODY',
246
- style: 'flex:1 1 auto; min-height:0; display:flex; flex-direction:column;'}, []]
382
+ pane.$error,
383
+ pane.$body
247
384
  ], {
248
- /* A paste anywhere in the pad is the document: it replaces
385
+ /* A paste anywhere in the pane is its document: it replaces
249
386
  * what the text area held instead of landing at the caret. */
250
387
  paste: (evt) => {
251
388
  let text = evt.clipboardData? evt.clipboardData.getData("text") : "";
@@ -253,24 +390,53 @@ function build_ui(gobj)
253
390
  return; /* an image or nothing: the browser's business */
254
391
  }
255
392
  evt.preventDefault();
256
- gobj_send_event(gobj, "EV_PASTE", {text: text}, gobj);
393
+ gobj_send_event(gobj, "EV_PASTE", {pane: id, text: text}, gobj);
257
394
  }
258
395
  }]
259
396
  );
260
397
 
261
- gobj_write_attr(gobj, "$container", $container);
398
+ return pane;
262
399
  }
263
400
 
264
401
  /***************************************************************
265
- * Read `text` as JSON and show it, or say why it is not.
402
+ * The pane a pane event names.
266
403
  ***************************************************************/
267
- function show_text(gobj, text)
404
+ function pane_of(gobj, kw)
268
405
  {
269
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
+ {
270
436
  let src = String(text || "").trim();
271
437
 
272
438
  if(!src) {
273
- clear_pad(gobj, false);
439
+ clear_pane(gobj, pane, false);
274
440
  return 0;
275
441
  }
276
442
 
@@ -281,32 +447,235 @@ function show_text(gobj, text)
281
447
  /* The reader's text, not a fault of the app: said on the pad, and
282
448
  * a warning for whoever reads the console. */
283
449
  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');
450
+ pane.$reason.textContent = e.message;
451
+ pane.$error.classList.remove('is-hidden');
286
452
  return -1;
287
453
  }
288
454
 
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");
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);
293
460
  return 0;
294
461
  }
295
462
 
296
463
  /***************************************************************
297
- * Back to the blank pad; `with_text` empties the text area too.
464
+ * A blank pane again; `with_text` empties the text area too.
298
465
  ***************************************************************/
299
- function clear_pad(gobj, with_text)
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)
300
495
  {
301
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");
302
500
 
303
- if(with_text) {
304
- priv.$input.value = "";
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);
305
511
  }
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");
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;
310
679
  }
311
680
 
312
681
 
@@ -320,48 +689,115 @@ function clear_pad(gobj, with_text)
320
689
 
321
690
 
322
691
  /***************************************************************
323
- * A paste: it becomes the text area and the document at once.
692
+ * A paste: it becomes the pane's text area and its document
693
+ * at once.
324
694
  ***************************************************************/
325
695
  function ac_paste(gobj, event, kw, src)
326
696
  {
327
- let priv = gobj.priv;
328
- let text = (kw && typeof kw.text === "string")? kw.text : "";
697
+ let pane = pane_of(gobj, kw);
698
+ if(!pane) {
699
+ return -1; // Error already logged
700
+ }
329
701
 
330
- priv.$input.value = text;
331
- return show_text(gobj, text);
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;
332
707
  }
333
708
 
334
709
  /***************************************************************
335
- * The text area as it is now.
710
+ * The pane's text area as it is now.
336
711
  ***************************************************************/
337
712
  function ac_view(gobj, event, kw, src)
338
713
  {
339
- let priv = gobj.priv;
340
- return show_text(gobj, priv.$input.value);
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;
341
722
  }
342
723
 
343
724
  /***************************************************************
344
- * The blank pad again.
725
+ * A blank pane again.
345
726
  ***************************************************************/
346
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)
347
758
  {
348
759
  let priv = gobj.priv;
349
760
 
350
- clear_pad(gobj, true);
351
- priv.$input.focus();
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");
352
790
  return 0;
353
791
  }
354
792
 
355
793
  /***************************************************************
356
- * The viewer asks for a `__collapsed__` subtree. A pasted
794
+ * A viewer asks for a `__collapsed__` subtree. A pasted
357
795
  * document has no backend to ask for it: the SOURCE truncated
358
- * it, and that is the answer.
796
+ * it, and that is the answer, to the viewer that asked.
359
797
  ***************************************************************/
360
798
  function ac_expand_path(gobj, event, kw, src)
361
799
  {
362
- let priv = gobj.priv;
363
-
364
- gobj_send_event(priv.gobj_json, "EV_SUBTREE_ERROR", {
800
+ gobj_send_event(src, "EV_SUBTREE_ERROR", {
365
801
  path: (kw && kw.path) || "",
366
802
  error: t("collapsed in the source")
367
803
  }, gobj);
@@ -395,16 +831,30 @@ function create_gclass(gclass_name)
395
831
  * States
396
832
  *---------------------------------------------*/
397
833
  const states = [
398
- ["ST_EMPTY", [
834
+ ["ST_SINGLE", [
399
835
  ["EV_PASTE", ac_paste, null],
400
836
  ["EV_VIEW", ac_view, null],
401
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],
402
841
  ]],
403
- ["ST_SHOWING", [
842
+ ["ST_DUAL", [
404
843
  ["EV_PASTE", ac_paste, null],
405
844
  ["EV_VIEW", ac_view, null],
406
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],
407
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],
408
858
  ]]
409
859
  ];
410
860
 
@@ -415,6 +865,9 @@ function create_gclass(gclass_name)
415
865
  ["EV_PASTE", 0],
416
866
  ["EV_VIEW", 0],
417
867
  ["EV_CLEAR", 0],
868
+ ["EV_TEXT_CHANGED", 0],
869
+ ["EV_TOGGLE_SECOND", 0],
870
+ ["EV_TOGGLE_DIFF", 0],
418
871
  ["EV_EXPAND_PATH", 0],
419
872
  ];
420
873