@yuneta/gobj-ui 7.23.18 → 7.23.20
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/dist/gobj-ui.cjs.js +819 -380
- package/dist/gobj-ui.es.js +811 -372
- package/package.json +1 -1
- package/src/c_g6_nodes_tree.js +135 -3
- package/src/c_yui_gobj_tree_js.js +185 -7
- package/src/c_yui_json_graph.js +269 -24
- package/src/json_view_helpers.js +67 -0
- package/src/json_view_helpers.test.js +67 -0
- package/src/lib_icons.js +7 -0
- package/src/yui_graph_camera.js +95 -0
package/dist/gobj-ui.es.js
CHANGED
|
@@ -9345,6 +9345,123 @@ function yui_toolbar(attrs = {}, items = []) {
|
|
|
9345
9345
|
return $toolbar;
|
|
9346
9346
|
}
|
|
9347
9347
|
//#endregion
|
|
9348
|
+
//#region src/lib_graph.js
|
|
9349
|
+
/************************************************************
|
|
9350
|
+
* Add class to elements selected
|
|
9351
|
+
************************************************************/
|
|
9352
|
+
function addClasses($container, selector, ...classNames) {
|
|
9353
|
+
$container.querySelectorAll(selector).forEach((el) => el.classList.add(...classNames));
|
|
9354
|
+
}
|
|
9355
|
+
/************************************************************
|
|
9356
|
+
* Remove class from elements selected
|
|
9357
|
+
************************************************************/
|
|
9358
|
+
function removeClasses($container, selector, ...classNames) {
|
|
9359
|
+
$container.querySelectorAll(selector).forEach((el) => el.classList.remove(...classNames));
|
|
9360
|
+
}
|
|
9361
|
+
/************************************************************
|
|
9362
|
+
* Toggle class to elements selected
|
|
9363
|
+
************************************************************/
|
|
9364
|
+
function toggleClasses($container, selector, ...classNames) {
|
|
9365
|
+
$container.querySelectorAll(selector).forEach((el) => classNames.forEach((cls) => el.classList.toggle(cls)));
|
|
9366
|
+
}
|
|
9367
|
+
/************************************************************
|
|
9368
|
+
* Remove child elements
|
|
9369
|
+
************************************************************/
|
|
9370
|
+
function removeChildElements($element) {
|
|
9371
|
+
$element.innerHTML = "";
|
|
9372
|
+
}
|
|
9373
|
+
/************************************************************
|
|
9374
|
+
* Disable/Enable elements
|
|
9375
|
+
************************************************************/
|
|
9376
|
+
function disableElements($container, selector) {
|
|
9377
|
+
$container.querySelectorAll(selector).forEach((el) => {
|
|
9378
|
+
el.setAttribute("disabled", "");
|
|
9379
|
+
});
|
|
9380
|
+
}
|
|
9381
|
+
function enableElements($container, selector) {
|
|
9382
|
+
$container.querySelectorAll(selector).forEach((el) => {
|
|
9383
|
+
if (el.hasAttribute("disabled")) el.removeAttribute("disabled");
|
|
9384
|
+
});
|
|
9385
|
+
}
|
|
9386
|
+
/************************************************************
|
|
9387
|
+
* Set or reset the color of 'enable submit action' state
|
|
9388
|
+
* Color green
|
|
9389
|
+
************************************************************/
|
|
9390
|
+
function set_submit_state($container, selector, set) {
|
|
9391
|
+
if (set) addClasses($container, selector, "color_submit_state");
|
|
9392
|
+
else removeClasses($container, selector, "color_submit_state");
|
|
9393
|
+
}
|
|
9394
|
+
/************************************************************
|
|
9395
|
+
* Set or reset the color of 'enable cancel action' state
|
|
9396
|
+
* Color red
|
|
9397
|
+
************************************************************/
|
|
9398
|
+
function set_cancel_state($container, selector, set) {
|
|
9399
|
+
if (set) addClasses($container, selector, "color_cancel_state");
|
|
9400
|
+
else removeClasses($container, selector, "color_cancel_state");
|
|
9401
|
+
}
|
|
9402
|
+
/************************************************************
|
|
9403
|
+
* Set or reset the color of 'enable actived' state
|
|
9404
|
+
* Color orange
|
|
9405
|
+
************************************************************/
|
|
9406
|
+
function set_active_state($container, selector, set) {
|
|
9407
|
+
if (set) addClasses($container, selector, "color_active_state");
|
|
9408
|
+
else removeClasses($container, selector, "color_active_state");
|
|
9409
|
+
}
|
|
9410
|
+
/************************************************************
|
|
9411
|
+
* Set or reset the 'pressed' state of a TOGGLE.
|
|
9412
|
+
*
|
|
9413
|
+
* Not one of the colours above: each of those names a KIND of
|
|
9414
|
+
* action (create, pending, history, destructive), and a toggle
|
|
9415
|
+
* is not an action -- it is a state the control is IN. So it
|
|
9416
|
+
* looks pressed instead of changing category, which also keeps
|
|
9417
|
+
* it out of the way of a neighbour wearing the same hue.
|
|
9418
|
+
************************************************************/
|
|
9419
|
+
function set_pressed_state($container, selector, set) {
|
|
9420
|
+
if (set) addClasses($container, selector, "pressed_state");
|
|
9421
|
+
else removeClasses($container, selector, "pressed_state");
|
|
9422
|
+
}
|
|
9423
|
+
/**
|
|
9424
|
+
* Returns a smart stroke color in `rgba()` format based on:
|
|
9425
|
+
* - The given fill color (hex or rgb[a])
|
|
9426
|
+
* - The current theme ('light' or 'dark')
|
|
9427
|
+
* - The adjustment factor (default 0.2)
|
|
9428
|
+
*
|
|
9429
|
+
* @param {string} fillColor - Fill color string
|
|
9430
|
+
* @param {string} theme - 'light' or 'dark'
|
|
9431
|
+
* @param {number} factor - Lighten/darken factor (default 0.2)
|
|
9432
|
+
* @returns {string} rgba(r, g, b, a)
|
|
9433
|
+
*/
|
|
9434
|
+
function getStrokeColor(fillColor, theme = "light", factor = .2) {
|
|
9435
|
+
let r, g, b, a = 1;
|
|
9436
|
+
if (fillColor.startsWith("#")) {
|
|
9437
|
+
let hex = fillColor.slice(1);
|
|
9438
|
+
if (hex.length === 3 || hex.length === 4) hex = hex.split("").map((c) => c + c).join("");
|
|
9439
|
+
if (hex.length === 6) {
|
|
9440
|
+
r = parseInt(hex.slice(0, 2), 16);
|
|
9441
|
+
g = parseInt(hex.slice(2, 4), 16);
|
|
9442
|
+
b = parseInt(hex.slice(4, 6), 16);
|
|
9443
|
+
} else if (hex.length === 8) {
|
|
9444
|
+
r = parseInt(hex.slice(0, 2), 16);
|
|
9445
|
+
g = parseInt(hex.slice(2, 4), 16);
|
|
9446
|
+
b = parseInt(hex.slice(4, 6), 16);
|
|
9447
|
+
a = parseInt(hex.slice(6, 8), 16) / 255;
|
|
9448
|
+
} else throw new Error("Unsupported hex format");
|
|
9449
|
+
} else if (fillColor.startsWith("rgb")) {
|
|
9450
|
+
const match = fillColor.match(/rgba?\(\s*(\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\s*\)/);
|
|
9451
|
+
if (!match) throw new Error("Invalid rgb()/rgba() format.");
|
|
9452
|
+
r = parseInt(match[1], 10);
|
|
9453
|
+
g = parseInt(match[2], 10);
|
|
9454
|
+
b = parseInt(match[3], 10);
|
|
9455
|
+
if (match[4] !== void 0) a = parseFloat(match[4]);
|
|
9456
|
+
} else throw new Error("Unsupported color format.");
|
|
9457
|
+
const luminance = (.299 * r + .587 * g + .114 * b) / 255;
|
|
9458
|
+
let lighten;
|
|
9459
|
+
if (theme === "dark") lighten = luminance < .7;
|
|
9460
|
+
else lighten = luminance < .3;
|
|
9461
|
+
const adjust = (v) => lighten ? Math.min(255, Math.floor(v + (255 - v) * factor)) : Math.max(0, Math.floor(v * (1 - factor)));
|
|
9462
|
+
return `rgba(${adjust(r)}, ${adjust(g)}, ${adjust(b)}, ${a.toFixed(3)})`;
|
|
9463
|
+
}
|
|
9464
|
+
//#endregion
|
|
9348
9465
|
//#region src/yui_graph_camera.js
|
|
9349
9466
|
/***********************************************************************
|
|
9350
9467
|
* yui_graph_camera.js
|
|
@@ -9493,6 +9610,79 @@ function yui_graph_fold_items(gobj, wide) {
|
|
|
9493
9610
|
return [camera_button(gobj, "yi-chevron-right", "EV_EXPAND_ALL", "expand all", wide, " transform: rotate(90deg);"), camera_button(gobj, "yi-chevron-right", "EV_COLLAPSE_ALL", "collapse all", wide)];
|
|
9494
9611
|
}
|
|
9495
9612
|
/************************************************************
|
|
9613
|
+
* The ANCHOR toggle: pick one element and every camera move
|
|
9614
|
+
* from then on leaves it in the MIDDLE.
|
|
9615
|
+
*
|
|
9616
|
+
* Why it exists: a graph that fits on screen is unreadable at
|
|
9617
|
+
* the zoom that makes it fit, and the zoom that makes it
|
|
9618
|
+
* readable does not fit -- so the useful view is always a
|
|
9619
|
+
* fraction of the document, and which fraction is the whole
|
|
9620
|
+
* question. Without an anchor, `1:1` lands wherever the
|
|
9621
|
+
* layout's origin happens to be and the reader hunts for the
|
|
9622
|
+
* node they were looking at.
|
|
9623
|
+
*
|
|
9624
|
+
* Crosshairs and not a thumbtack. A tack says "do not move
|
|
9625
|
+
* this", which is what a pinned NODE would mean in a graph you
|
|
9626
|
+
* can drag nodes around in; this pins the CAMERA to a target,
|
|
9627
|
+
* which is what a crosshair says and what every map that
|
|
9628
|
+
* offers it draws. It is also in the icon set already, and a
|
|
9629
|
+
* `yi-*` class that is not is a black square.
|
|
9630
|
+
*
|
|
9631
|
+
* Three states, because two would not say what a press does:
|
|
9632
|
+
*
|
|
9633
|
+
* off no target; the camera behaves as it always did
|
|
9634
|
+
* arming waiting for you to click the element you mean
|
|
9635
|
+
* on locked; every zoom re-centres on it
|
|
9636
|
+
************************************************************/
|
|
9637
|
+
function yui_graph_anchor_item(gobj, wide) {
|
|
9638
|
+
return camera_button(gobj, "yi-location-crosshairs", "EV_TOGGLE_ANCHOR", "anchor view", wide);
|
|
9639
|
+
}
|
|
9640
|
+
/************************************************************
|
|
9641
|
+
* Paint the anchor button. `state` is "off" | "arming" | "on".
|
|
9642
|
+
*
|
|
9643
|
+
* Both live states look PRESSED, because both are states the
|
|
9644
|
+
* control is IN rather than actions; what separates them is the
|
|
9645
|
+
* colour, and only the waiting one takes a palette colour --
|
|
9646
|
+
* it is the one that needs the reader to do something next.
|
|
9647
|
+
************************************************************/
|
|
9648
|
+
function yui_graph_update_anchor($container, state) {
|
|
9649
|
+
if (!$container) return;
|
|
9650
|
+
let $btn = $container.querySelector(".EV_TOGGLE_ANCHOR");
|
|
9651
|
+
if (!$btn) return;
|
|
9652
|
+
set_pressed_state($container, ".EV_TOGGLE_ANCHOR", state !== "off");
|
|
9653
|
+
$btn.classList.toggle("color_pending_state", state === "arming");
|
|
9654
|
+
$btn.setAttribute("aria-pressed", state === "on" ? "true" : "false");
|
|
9655
|
+
let key = state === "arming" ? "click the element to centre on" : state === "on" ? "centred: click to release" : "anchor view";
|
|
9656
|
+
$btn.setAttribute("title", t(key));
|
|
9657
|
+
$btn.setAttribute("data-i18n-title", key);
|
|
9658
|
+
$btn.setAttribute("aria-label", t(key));
|
|
9659
|
+
$btn.setAttribute("data-i18n-aria-label", key);
|
|
9660
|
+
}
|
|
9661
|
+
/************************************************************
|
|
9662
|
+
* Put `node_id` back in the middle of the viewport.
|
|
9663
|
+
*
|
|
9664
|
+
* Called AFTER the camera moved, never instead of moving it:
|
|
9665
|
+
* the zoom is what the reader asked for, the centring is what
|
|
9666
|
+
* keeps the thing they are reading under their eyes.
|
|
9667
|
+
*
|
|
9668
|
+
* Guarded on every side because it runs on every wheel notch:
|
|
9669
|
+
* a graph mid-rebuild, an anchor whose node went away with a
|
|
9670
|
+
* fold, a G6 version without `focusElement` -- none of those is
|
|
9671
|
+
* an error worth a log line per notch, they are all "there is
|
|
9672
|
+
* nothing to centre right now".
|
|
9673
|
+
************************************************************/
|
|
9674
|
+
function yui_graph_center_on(graph, node_id) {
|
|
9675
|
+
if (!graph || !node_id) return false;
|
|
9676
|
+
try {
|
|
9677
|
+
if (typeof graph.getElementData === "function" && !graph.getElementData(node_id)) return false;
|
|
9678
|
+
if (typeof graph.focusElement !== "function") return false;
|
|
9679
|
+
graph.focusElement(node_id);
|
|
9680
|
+
return true;
|
|
9681
|
+
} catch (e) {
|
|
9682
|
+
return false;
|
|
9683
|
+
}
|
|
9684
|
+
}
|
|
9685
|
+
/************************************************************
|
|
9496
9686
|
* A refresh button, for the graphs that offer one.
|
|
9497
9687
|
************************************************************/
|
|
9498
9688
|
function yui_graph_refresh_item(gobj, wide) {
|
|
@@ -9969,6 +10159,268 @@ function ensure_drag_canvas_patch() {
|
|
|
9969
10159
|
register(ExtensionCategory.BEHAVIOR, "drag-canvas", DragCanvasTouch);
|
|
9970
10160
|
__drag_canvas_patched__ = true;
|
|
9971
10161
|
}
|
|
10162
|
+
/************************************************************
|
|
10163
|
+
* JSON type discriminator (viewer vocabulary)
|
|
10164
|
+
************************************************************/
|
|
10165
|
+
function json_type(value) {
|
|
10166
|
+
if (value === null) return "null";
|
|
10167
|
+
if (Array.isArray(value)) return "array";
|
|
10168
|
+
switch (typeof value) {
|
|
10169
|
+
case "string": return "string";
|
|
10170
|
+
case "number": return "number";
|
|
10171
|
+
case "boolean": return "boolean";
|
|
10172
|
+
case "object": return "object";
|
|
10173
|
+
default: return "unknown";
|
|
10174
|
+
}
|
|
10175
|
+
}
|
|
10176
|
+
/************************************************************
|
|
10177
|
+
* Detect a kw_collapse() sentinel.
|
|
10178
|
+
*
|
|
10179
|
+
* The kernel replaces an over-limit dict with:
|
|
10180
|
+
* { "__collapsed__": { "path": ..., "size": N } }
|
|
10181
|
+
* and an over-limit array with:
|
|
10182
|
+
* [ { "__collapsed__": { "path": ..., "size": N } } ]
|
|
10183
|
+
*
|
|
10184
|
+
* Returns { size, path, is_array } or null.
|
|
10185
|
+
************************************************************/
|
|
10186
|
+
function is_collapsed(value) {
|
|
10187
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
10188
|
+
let keys = Object.keys(value);
|
|
10189
|
+
if (keys.length === 1 && keys[0] === "__collapsed__") {
|
|
10190
|
+
let c = value.__collapsed__ || {};
|
|
10191
|
+
return {
|
|
10192
|
+
size: c.size,
|
|
10193
|
+
path: c.path,
|
|
10194
|
+
is_array: false
|
|
10195
|
+
};
|
|
10196
|
+
}
|
|
10197
|
+
return null;
|
|
10198
|
+
}
|
|
10199
|
+
if (Array.isArray(value) && value.length === 1) {
|
|
10200
|
+
let e = value[0];
|
|
10201
|
+
if (e && typeof e === "object" && !Array.isArray(e) && Object.keys(e).length === 1 && e.__collapsed__) {
|
|
10202
|
+
let c = e.__collapsed__ || {};
|
|
10203
|
+
return {
|
|
10204
|
+
size: c.size,
|
|
10205
|
+
path: c.path,
|
|
10206
|
+
is_array: true
|
|
10207
|
+
};
|
|
10208
|
+
}
|
|
10209
|
+
}
|
|
10210
|
+
return null;
|
|
10211
|
+
}
|
|
10212
|
+
/************************************************************
|
|
10213
|
+
* Join / split absolute path segments (kernel convention)
|
|
10214
|
+
************************************************************/
|
|
10215
|
+
function seg_join(segments) {
|
|
10216
|
+
return segments.map(function(s) {
|
|
10217
|
+
return String(s);
|
|
10218
|
+
}).join("`");
|
|
10219
|
+
}
|
|
10220
|
+
function seg_split(path) {
|
|
10221
|
+
if (path === "" || path === null || path === void 0) return [];
|
|
10222
|
+
return path.split("`");
|
|
10223
|
+
}
|
|
10224
|
+
/************************************************************
|
|
10225
|
+
* Walk a JSON tree by absolute segments; undefined if the
|
|
10226
|
+
* path does not resolve.
|
|
10227
|
+
************************************************************/
|
|
10228
|
+
function get_by_segments(root, segments) {
|
|
10229
|
+
let v = root;
|
|
10230
|
+
for (let seg of segments) {
|
|
10231
|
+
if (v === null || v === void 0) return;
|
|
10232
|
+
if (Array.isArray(v)) v = v[Number(seg)];
|
|
10233
|
+
else if (typeof v === "object") v = v[seg];
|
|
10234
|
+
else return;
|
|
10235
|
+
}
|
|
10236
|
+
return v;
|
|
10237
|
+
}
|
|
10238
|
+
/************************************************************
|
|
10239
|
+
* Set a value at absolute segments (mutates root).
|
|
10240
|
+
*
|
|
10241
|
+
* segments == [] means "replace the whole root" — the caller
|
|
10242
|
+
* must use the RETURNED value in that case (a primitive/array
|
|
10243
|
+
* root cannot be mutated in place).
|
|
10244
|
+
************************************************************/
|
|
10245
|
+
function set_by_segments(root, segments, new_value) {
|
|
10246
|
+
if (segments.length === 0) return new_value;
|
|
10247
|
+
let parent = get_by_segments(root, segments.slice(0, -1));
|
|
10248
|
+
if (parent === null || parent === void 0 || typeof parent !== "object") return root;
|
|
10249
|
+
let last = segments[segments.length - 1];
|
|
10250
|
+
if (Array.isArray(parent)) parent[Number(last)] = new_value;
|
|
10251
|
+
else parent[last] = new_value;
|
|
10252
|
+
return root;
|
|
10253
|
+
}
|
|
10254
|
+
/************************************************************
|
|
10255
|
+
* Does `value` (or any loaded descendant / its own key)
|
|
10256
|
+
* contain the lower-cased search term?
|
|
10257
|
+
*
|
|
10258
|
+
* Collapsed (not-yet-loaded) subtrees can't be searched, so
|
|
10259
|
+
* they never match on content — only their key can match,
|
|
10260
|
+
* which the renderer checks separately.
|
|
10261
|
+
************************************************************/
|
|
10262
|
+
function subtree_matches(value, term) {
|
|
10263
|
+
if (!term) return true;
|
|
10264
|
+
if (is_collapsed(value)) return false;
|
|
10265
|
+
let type = json_type(value);
|
|
10266
|
+
if (type === "object") {
|
|
10267
|
+
for (let [k, v] of Object.entries(value)) {
|
|
10268
|
+
if (String(k).toLowerCase().includes(term)) return true;
|
|
10269
|
+
if (subtree_matches(v, term)) return true;
|
|
10270
|
+
}
|
|
10271
|
+
return false;
|
|
10272
|
+
}
|
|
10273
|
+
if (type === "array") {
|
|
10274
|
+
for (let v of value) if (subtree_matches(v, term)) return true;
|
|
10275
|
+
return false;
|
|
10276
|
+
}
|
|
10277
|
+
return String(value).toLowerCase().includes(term);
|
|
10278
|
+
}
|
|
10279
|
+
/************************************************************
|
|
10280
|
+
* Fields whose numeric value is an epoch timestamp.
|
|
10281
|
+
* Mirrors the timestampTag list wired into C_YUI_FORM's
|
|
10282
|
+
* jsoneditor control, so the two viewers agree.
|
|
10283
|
+
************************************************************/
|
|
10284
|
+
function is_time_field(field) {
|
|
10285
|
+
return field === "__t__" || field === "__tm__" || field === "tm" || field === "t" || field === "time" || field === "from_t" || field === "to_t" || field === "t_input" || field === "t_output" || field === "from_tm" || field === "to_tm";
|
|
10286
|
+
}
|
|
10287
|
+
/************************************************************
|
|
10288
|
+
* Format an epoch value as a local wall-clock string.
|
|
10289
|
+
* Accepts seconds or milliseconds; returns null when the
|
|
10290
|
+
* value is not a usable positive timestamp (0 == "unset").
|
|
10291
|
+
************************************************************/
|
|
10292
|
+
function format_epoch(value) {
|
|
10293
|
+
if (typeof value !== "number" || !isFinite(value) || value <= 0) return null;
|
|
10294
|
+
let ms = Math.abs(value) < 0xe8d4a51000 ? value * 1e3 : value;
|
|
10295
|
+
let d = new Date(ms);
|
|
10296
|
+
if (isNaN(d.getTime())) return null;
|
|
10297
|
+
return d.toLocaleString();
|
|
10298
|
+
}
|
|
10299
|
+
/************************************************************
|
|
10300
|
+
* The text view's payload: the document as text, indented
|
|
10301
|
+
* four characters, cut at `max_chars`.
|
|
10302
|
+
*
|
|
10303
|
+
* Returns {text, capped}. `capped` is what the caller
|
|
10304
|
+
* announces under the dump — a cut that is not announced
|
|
10305
|
+
* reads as a document that ends there.
|
|
10306
|
+
************************************************************/
|
|
10307
|
+
function json_text_dump(root, max_chars) {
|
|
10308
|
+
let text;
|
|
10309
|
+
try {
|
|
10310
|
+
text = JSON.stringify(root, null, 4);
|
|
10311
|
+
} catch (e) {
|
|
10312
|
+
return {
|
|
10313
|
+
text: "",
|
|
10314
|
+
capped: false,
|
|
10315
|
+
error: String(e)
|
|
10316
|
+
};
|
|
10317
|
+
}
|
|
10318
|
+
if (text === void 0) return {
|
|
10319
|
+
text: "",
|
|
10320
|
+
capped: false,
|
|
10321
|
+
error: "not representable as JSON"
|
|
10322
|
+
};
|
|
10323
|
+
if (typeof max_chars === "number" && max_chars > 0 && text.length > max_chars) return {
|
|
10324
|
+
text: text.slice(0, max_chars),
|
|
10325
|
+
capped: true
|
|
10326
|
+
};
|
|
10327
|
+
return {
|
|
10328
|
+
text,
|
|
10329
|
+
capped: false
|
|
10330
|
+
};
|
|
10331
|
+
}
|
|
10332
|
+
/************************************************************
|
|
10333
|
+
* Which view a freshly mounted viewer opens on.
|
|
10334
|
+
*
|
|
10335
|
+
* Three answers, in order of authority: what the HOST asked
|
|
10336
|
+
* for, what the READER chose last time, and `fallback`.
|
|
10337
|
+
*
|
|
10338
|
+
* The host wins because a viewer mounted to show a graph has
|
|
10339
|
+
* to show one. The memory comes next, because reopening a
|
|
10340
|
+
* document in the view you just left is the whole point of
|
|
10341
|
+
* remembering it. `fallback` is the floor.
|
|
10342
|
+
*
|
|
10343
|
+
* Anything not in `modes` is treated as unsaid, which covers
|
|
10344
|
+
* an absent value, an empty one, and a stored string from an
|
|
10345
|
+
* older release that named a view no longer offered.
|
|
10346
|
+
************************************************************/
|
|
10347
|
+
function pick_view_mode(asked, remembered, modes, fallback) {
|
|
10348
|
+
let known = (m) => Array.isArray(modes) && modes.indexOf(m) >= 0;
|
|
10349
|
+
if (known(asked)) return asked;
|
|
10350
|
+
if (known(remembered)) return remembered;
|
|
10351
|
+
return fallback;
|
|
10352
|
+
}
|
|
10353
|
+
/************************************************************
|
|
10354
|
+
* The short label a container row carries beside its size.
|
|
10355
|
+
*
|
|
10356
|
+
* An array of dicts that carry an `id` is a list of RECORDS,
|
|
10357
|
+
* and a row that says only `2: {15}` makes you open all
|
|
10358
|
+
* fifteen fields of every one of them to find out WHICH
|
|
10359
|
+
* record it is. The id is the one field that answers that,
|
|
10360
|
+
* so the row says it without being opened.
|
|
10361
|
+
*
|
|
10362
|
+
* Only a SCALAR id, and only when it is really there: an `id`
|
|
10363
|
+
* that is itself a dict or a list is not a name, it is more
|
|
10364
|
+
* document, and printing `[object Object]` beside the size
|
|
10365
|
+
* would be worse than printing nothing.
|
|
10366
|
+
*
|
|
10367
|
+
* Cut at `max_chars`, because this rides on a row whose job
|
|
10368
|
+
* is to stay one line.
|
|
10369
|
+
************************************************************/
|
|
10370
|
+
function container_label(value, max_chars) {
|
|
10371
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return "";
|
|
10372
|
+
if (!Object.prototype.hasOwnProperty.call(value, "id")) return "";
|
|
10373
|
+
let id = value.id;
|
|
10374
|
+
let t = typeof id;
|
|
10375
|
+
if (t !== "string" && t !== "number" && t !== "boolean") return "";
|
|
10376
|
+
let text = String(id).trim();
|
|
10377
|
+
if (text === "") return "";
|
|
10378
|
+
let cap = typeof max_chars === "number" && max_chars > 0 ? max_chars : 0;
|
|
10379
|
+
if (cap && text.length > cap) return text.slice(0, cap) + "…";
|
|
10380
|
+
return text;
|
|
10381
|
+
}
|
|
10382
|
+
/************************************************************
|
|
10383
|
+
* A dict, in the JSON sense: not an array and not null.
|
|
10384
|
+
************************************************************/
|
|
10385
|
+
function is_plain_object(v) {
|
|
10386
|
+
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
10387
|
+
}
|
|
10388
|
+
/************************************************************
|
|
10389
|
+
* Does this container hold ONLY other containers?
|
|
10390
|
+
*
|
|
10391
|
+
* Such a value is not a thing, it is a LIST of things -- and a
|
|
10392
|
+
* list is fully described by the row that names it in its
|
|
10393
|
+
* parent, plus the edges to what it holds. Drawing it as a
|
|
10394
|
+
* card of its own put a node in the middle of the graph with no
|
|
10395
|
+
* data in it, saying nothing the parent's row did not already
|
|
10396
|
+
* say, and pushed everything below it one level down.
|
|
10397
|
+
*
|
|
10398
|
+
* An EMPTY container inside counts as content: `{0}` is
|
|
10399
|
+
* something the card has to show, and there is no child card
|
|
10400
|
+
* to show it instead.
|
|
10401
|
+
************************************************************/
|
|
10402
|
+
function is_pure_collection(value) {
|
|
10403
|
+
let entries = is_plain_object(value) ? Object.entries(value) : Array.isArray(value) ? value.map((v, i) => [i, v]) : [];
|
|
10404
|
+
if (entries.length === 0) return false;
|
|
10405
|
+
for (let [, v] of entries) if (!has_branch(v)) return false;
|
|
10406
|
+
return true;
|
|
10407
|
+
}
|
|
10408
|
+
/************************************************************
|
|
10409
|
+
* A value that gets a card, or children, of its own.
|
|
10410
|
+
************************************************************/
|
|
10411
|
+
function has_branch(value) {
|
|
10412
|
+
return is_plain_object(value) && Object.keys(value).length > 0 || Array.isArray(value) && value.length > 0;
|
|
10413
|
+
}
|
|
10414
|
+
/************************************************************
|
|
10415
|
+
* How many branches a container has, which is what the fold
|
|
10416
|
+
* chip counts.
|
|
10417
|
+
************************************************************/
|
|
10418
|
+
function count_branches(value) {
|
|
10419
|
+
let entries = is_plain_object(value) ? Object.values(value) : Array.isArray(value) ? value : [];
|
|
10420
|
+
let n = 0;
|
|
10421
|
+
for (let v of entries) if (has_branch(v)) n++;
|
|
10422
|
+
return n;
|
|
10423
|
+
}
|
|
9972
10424
|
//#endregion
|
|
9973
10425
|
//#region src/c_yui_json_graph.js
|
|
9974
10426
|
/***********************************************************************
|
|
@@ -10070,7 +10522,11 @@ var PRIVATE_DATA$11 = {
|
|
|
10070
10522
|
$layout_select: null,
|
|
10071
10523
|
$find_input: null,
|
|
10072
10524
|
$find_result: null,
|
|
10073
|
-
$find_count: null
|
|
10525
|
+
$find_count: null,
|
|
10526
|
+
anchor_id: "",
|
|
10527
|
+
anchor_path: "",
|
|
10528
|
+
anchor_state: "off",
|
|
10529
|
+
last_zoom: 1
|
|
10074
10530
|
};
|
|
10075
10531
|
var __gclass__$11 = null;
|
|
10076
10532
|
var __layout_registered__$1 = false;
|
|
@@ -10363,6 +10819,7 @@ function make_toolbar$3(gobj) {
|
|
|
10363
10819
|
]]);
|
|
10364
10820
|
let center_items = [];
|
|
10365
10821
|
center_items = yui_graph_camera_items(gobj, priv.graph, toolbar_wide);
|
|
10822
|
+
center_items.push(yui_graph_anchor_item(gobj, toolbar_wide));
|
|
10366
10823
|
center_items.push(yui_graph_refresh_item(gobj, toolbar_wide));
|
|
10367
10824
|
let current_layout = gobj_read_str_attr(gobj, "layout") || DEFAULT_LAYOUT;
|
|
10368
10825
|
let options = Object.keys(LAYOUTS$1).map(function(key) {
|
|
@@ -10445,6 +10902,15 @@ function build_graph$3(gobj) {
|
|
|
10445
10902
|
});
|
|
10446
10903
|
graph.on("aftertransform", () => {
|
|
10447
10904
|
yui_graph_update_zoom(gobj_read_attr(gobj, "$container"), priv.graph);
|
|
10905
|
+
let z = priv.last_zoom;
|
|
10906
|
+
try {
|
|
10907
|
+
z = priv.graph.getZoom();
|
|
10908
|
+
} catch (e) {
|
|
10909
|
+
return;
|
|
10910
|
+
}
|
|
10911
|
+
if (z === priv.last_zoom) return;
|
|
10912
|
+
priv.last_zoom = z;
|
|
10913
|
+
if (priv.anchor_state === "on" && priv.anchor_id) yui_graph_center_on(priv.graph, priv.anchor_id);
|
|
10448
10914
|
});
|
|
10449
10915
|
graph.on(CanvasEvent.CLICK, (evt) => {});
|
|
10450
10916
|
if (typeof ResizeObserver !== "undefined") {
|
|
@@ -10465,7 +10931,7 @@ function build_graph$3(gobj) {
|
|
|
10465
10931
|
/************************************************************
|
|
10466
10932
|
* Build cell value HTML (matching old mx_json_viewer colors)
|
|
10467
10933
|
************************************************************/
|
|
10468
|
-
function build_cell_html(key, value, type, dark, matched) {
|
|
10934
|
+
function build_cell_html(key, value, type, dark, matched, fold) {
|
|
10469
10935
|
let on_light = matched ? false : dark;
|
|
10470
10936
|
let color = type_color(type, on_light);
|
|
10471
10937
|
let key_color = json_card_style(GROUP_COLORS.dict, on_light).key;
|
|
@@ -10492,6 +10958,7 @@ function build_cell_html(key, value, type, dark, matched) {
|
|
|
10492
10958
|
default: display_value = String(value);
|
|
10493
10959
|
}
|
|
10494
10960
|
let row = `<span style="color:${key_color}">• ${escapeHtml(String(key))}: </span><span style="color:${color}">${display_value}</span>`;
|
|
10961
|
+
if (fold) row += `<span style="margin-left:8px;">` + fold_toggle_html(fold.path, true, fold.folded, fold.branches, fold.colors) + `</span>`;
|
|
10495
10962
|
if (matched) row = `<span style="background:#ffe082; border-radius:2px; padding:0 2px;">${row}</span>`;
|
|
10496
10963
|
return row;
|
|
10497
10964
|
}
|
|
@@ -10615,6 +11082,7 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id) {
|
|
|
10615
11082
|
let lines = [];
|
|
10616
11083
|
let pending_complex = [];
|
|
10617
11084
|
let card_matched = false;
|
|
11085
|
+
let colors = json_card_style(is_dict ? GROUP_COLORS.dict : GROUP_COLORS.list, dark);
|
|
10618
11086
|
let entries = is_dict ? Object.entries(kw) : kw.map((v, i) => [i, v]);
|
|
10619
11087
|
for (let [key, value] of entries) {
|
|
10620
11088
|
let type = get_json_type(value);
|
|
@@ -10623,17 +11091,32 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id) {
|
|
|
10623
11091
|
card_matched = true;
|
|
10624
11092
|
priv.match_count++;
|
|
10625
11093
|
}
|
|
10626
|
-
let
|
|
10627
|
-
|
|
10628
|
-
|
|
10629
|
-
|
|
10630
|
-
|
|
10631
|
-
|
|
11094
|
+
let fold = null;
|
|
11095
|
+
if (has_branch(value)) {
|
|
11096
|
+
pending_complex.push({
|
|
11097
|
+
key,
|
|
11098
|
+
value
|
|
11099
|
+
});
|
|
11100
|
+
if (is_pure_collection(value)) {
|
|
11101
|
+
let child_path = add_segment(path, key);
|
|
11102
|
+
fold = {
|
|
11103
|
+
path: child_path,
|
|
11104
|
+
folded: priv.collapsed.has(child_path),
|
|
11105
|
+
branches: count_branches(value),
|
|
11106
|
+
colors
|
|
11107
|
+
};
|
|
11108
|
+
}
|
|
11109
|
+
}
|
|
11110
|
+
lines.push(build_cell_html(key, value, type, dark, matched, fold));
|
|
11111
|
+
}
|
|
11112
|
+
if (parent_id && is_pure_collection(kw)) {
|
|
11113
|
+
if (priv.collapsed.has(path)) return parent_id;
|
|
11114
|
+
for (let pending of pending_complex) build_json_nodes(gobj, add_segment(path, pending.key), pending.value, nodes, edges, parent_id);
|
|
11115
|
+
return parent_id;
|
|
10632
11116
|
}
|
|
10633
11117
|
let label = get_group_label(path) || (is_dict ? "{}" : "[]");
|
|
10634
11118
|
let foldable = pending_complex.length > 0;
|
|
10635
11119
|
let folded = foldable && priv.collapsed.has(path);
|
|
10636
|
-
let colors = json_card_style(is_dict ? GROUP_COLORS.dict : GROUP_COLORS.list, dark);
|
|
10637
11120
|
let border_color = card_matched ? dark ? "#ffb300" : "#ff8f00" : colors.border;
|
|
10638
11121
|
let border_width = card_matched ? 2 : 1;
|
|
10639
11122
|
let content_html = lines.map((line) => `<div style="font-family:monospace; font-size:13px; padding:1px 6px; white-space:nowrap;">${line}</div>`).join("");
|
|
@@ -10706,34 +11189,46 @@ function load_json(gobj) {
|
|
|
10706
11189
|
let preserve_view = !!priv.pending_preserve_view;
|
|
10707
11190
|
priv.pending_preserve_view = false;
|
|
10708
11191
|
if (nodes.length > 0) {
|
|
11192
|
+
let root_id = nodes[0].id;
|
|
10709
11193
|
graph.setData({
|
|
10710
11194
|
nodes,
|
|
10711
11195
|
edges
|
|
10712
11196
|
});
|
|
10713
11197
|
graph.render().then(() => {
|
|
10714
|
-
|
|
10715
|
-
|
|
11198
|
+
reanchor$1(gobj);
|
|
11199
|
+
if (preserve_view) {
|
|
11200
|
+
yui_graph_update_zoom(gobj_read_attr(gobj, "$container"), priv.graph);
|
|
11201
|
+
return;
|
|
11202
|
+
}
|
|
11203
|
+
Promise.resolve(graph.zoomTo(1)).then(() => {
|
|
11204
|
+
if (!yui_graph_center_on(graph, priv.anchor_id)) yui_graph_center_on(graph, root_id);
|
|
11205
|
+
priv.last_zoom = 1;
|
|
11206
|
+
yui_graph_update_zoom(gobj_read_attr(gobj, "$container"), priv.graph);
|
|
11207
|
+
});
|
|
10716
11208
|
});
|
|
10717
11209
|
}
|
|
10718
11210
|
}
|
|
10719
11211
|
/************************************************************
|
|
10720
11212
|
* Every path that HAS a branch to fold, in the same recursion
|
|
10721
|
-
* build_json_nodes uses — so "collapse all" can never name
|
|
10722
|
-
*
|
|
11213
|
+
* build_json_nodes uses — so "collapse all" can never name
|
|
11214
|
+
* something with no handle. The handle is a card's header chip
|
|
11215
|
+
* for a container that gets a card, and the chip on its ROW in
|
|
11216
|
+
* the parent for a pure collection, which gets none; both are
|
|
11217
|
+
* keyed by this same path. The root is excluded: folding it
|
|
10723
11218
|
* would leave an empty canvas.
|
|
10724
11219
|
************************************************************/
|
|
10725
11220
|
function collect_foldable_paths(value, path, out, is_root) {
|
|
10726
11221
|
let is_dict = is_object(value);
|
|
10727
11222
|
let is_list = is_array(value);
|
|
10728
11223
|
if (!is_dict && !is_list) return false;
|
|
10729
|
-
let
|
|
11224
|
+
let found_branch = false;
|
|
10730
11225
|
let entries = is_dict ? Object.entries(value) : value.map((v, i) => [i, v]);
|
|
10731
|
-
for (let [key, child] of entries) if (
|
|
10732
|
-
|
|
11226
|
+
for (let [key, child] of entries) if (has_branch(child)) {
|
|
11227
|
+
found_branch = true;
|
|
10733
11228
|
collect_foldable_paths(child, add_segment(path, key), out, false);
|
|
10734
11229
|
}
|
|
10735
|
-
if (
|
|
10736
|
-
return
|
|
11230
|
+
if (found_branch && !is_root) out.push(path);
|
|
11231
|
+
return found_branch;
|
|
10737
11232
|
}
|
|
10738
11233
|
/************************************************************
|
|
10739
11234
|
* Show the find count, or hide the chip when there is no
|
|
@@ -10975,13 +11470,79 @@ function ac_theme$3(gobj, event, kw, src) {
|
|
|
10975
11470
|
return 0;
|
|
10976
11471
|
}
|
|
10977
11472
|
/************************************************************
|
|
11473
|
+
* Every camera action ends the same way once an anchor is
|
|
11474
|
+
* set: the thing you were reading goes back to the middle.
|
|
11475
|
+
*
|
|
11476
|
+
* Chained on the promise G6 returns rather than run beside it
|
|
11477
|
+
* -- centring before the zoom has finished centres the OLD
|
|
11478
|
+
* view, and the reader watches the node slide away and come
|
|
11479
|
+
* back.
|
|
11480
|
+
************************************************************/
|
|
11481
|
+
function after_camera$2(gobj, moved) {
|
|
11482
|
+
let priv = gobj.priv;
|
|
11483
|
+
if (priv.anchor_state !== "on" || !priv.anchor_id) return;
|
|
11484
|
+
Promise.resolve(moved).then(function() {
|
|
11485
|
+
yui_graph_center_on(priv.graph, priv.anchor_id);
|
|
11486
|
+
}).catch(function() {});
|
|
11487
|
+
}
|
|
11488
|
+
/************************************************************
|
|
11489
|
+
* Point the anchor back at its node after a rebuild.
|
|
11490
|
+
*
|
|
11491
|
+
* Node ids are generated per build, so the id the reader
|
|
11492
|
+
* chose does not survive a refresh, a fold or a layout
|
|
11493
|
+
* change -- the PATH does, and it is what the anchor really
|
|
11494
|
+
* means: "this place in the document", not "this card".
|
|
11495
|
+
*
|
|
11496
|
+
* A path that is no longer drawn (its parent got folded)
|
|
11497
|
+
* releases the anchor rather than leaving a button that says
|
|
11498
|
+
* it is locked onto something invisible.
|
|
11499
|
+
************************************************************/
|
|
11500
|
+
function reanchor$1(gobj) {
|
|
11501
|
+
let priv = gobj.priv;
|
|
11502
|
+
if (priv.anchor_state !== "on" || !priv.anchor_path) return;
|
|
11503
|
+
let found = "";
|
|
11504
|
+
try {
|
|
11505
|
+
for (let node of priv.graph.getNodeData()) if (node && node.data && node.data.path === priv.anchor_path) {
|
|
11506
|
+
found = node.id;
|
|
11507
|
+
break;
|
|
11508
|
+
}
|
|
11509
|
+
} catch (e) {
|
|
11510
|
+
return;
|
|
11511
|
+
}
|
|
11512
|
+
priv.anchor_id = found;
|
|
11513
|
+
if (!found) {
|
|
11514
|
+
priv.anchor_state = "off";
|
|
11515
|
+
priv.anchor_path = "";
|
|
11516
|
+
}
|
|
11517
|
+
yui_graph_update_anchor(gobj_read_attr(gobj, "$container"), priv.anchor_state);
|
|
11518
|
+
}
|
|
11519
|
+
/************************************************************
|
|
11520
|
+
* Arm the anchor, or let it go.
|
|
11521
|
+
*
|
|
11522
|
+
* Three states and one button, so a press ADVANCES: with no
|
|
11523
|
+
* target it starts waiting for one, and with a target it
|
|
11524
|
+
* releases. Pressing it while it waits is a cancel, which is
|
|
11525
|
+
* what somebody who pressed it by mistake will try.
|
|
11526
|
+
************************************************************/
|
|
11527
|
+
function ac_toggle_anchor$2(gobj, event, kw, src) {
|
|
11528
|
+
let priv = gobj.priv;
|
|
11529
|
+
if (priv.anchor_state === "off") priv.anchor_state = "arming";
|
|
11530
|
+
else {
|
|
11531
|
+
priv.anchor_state = "off";
|
|
11532
|
+
priv.anchor_id = "";
|
|
11533
|
+
priv.anchor_path = "";
|
|
11534
|
+
}
|
|
11535
|
+
yui_graph_update_anchor(gobj_read_attr(gobj, "$container"), priv.anchor_state);
|
|
11536
|
+
return 0;
|
|
11537
|
+
}
|
|
11538
|
+
/************************************************************
|
|
10978
11539
|
*
|
|
10979
11540
|
************************************************************/
|
|
10980
11541
|
function ac_zoom_in$2(gobj, event, kw, src) {
|
|
10981
11542
|
let graph = gobj.priv.graph;
|
|
10982
11543
|
if (graph) {
|
|
10983
11544
|
let z = graph.getZoom();
|
|
10984
|
-
graph.zoomTo(z * 1.2);
|
|
11545
|
+
after_camera$2(gobj, graph.zoomTo(z * 1.2));
|
|
10985
11546
|
}
|
|
10986
11547
|
return 0;
|
|
10987
11548
|
}
|
|
@@ -10992,7 +11553,7 @@ function ac_zoom_out$2(gobj, event, kw, src) {
|
|
|
10992
11553
|
let graph = gobj.priv.graph;
|
|
10993
11554
|
if (graph) {
|
|
10994
11555
|
let z = graph.getZoom();
|
|
10995
|
-
graph.zoomTo(z * .8);
|
|
11556
|
+
after_camera$2(gobj, graph.zoomTo(z * .8));
|
|
10996
11557
|
}
|
|
10997
11558
|
return 0;
|
|
10998
11559
|
}
|
|
@@ -11000,10 +11561,14 @@ function ac_zoom_out$2(gobj, event, kw, src) {
|
|
|
11000
11561
|
*
|
|
11001
11562
|
************************************************************/
|
|
11002
11563
|
function ac_zoom_reset$2(gobj, event, kw, src) {
|
|
11003
|
-
let
|
|
11564
|
+
let priv = gobj.priv;
|
|
11565
|
+
let graph = priv.graph;
|
|
11004
11566
|
if (graph && graph.rendered) {
|
|
11005
|
-
graph.zoomTo(1);
|
|
11006
|
-
|
|
11567
|
+
if (priv.anchor_state === "on" && priv.anchor_id) after_camera$2(gobj, graph.zoomTo(1));
|
|
11568
|
+
else {
|
|
11569
|
+
graph.zoomTo(1);
|
|
11570
|
+
graph.translateTo([0, 0]);
|
|
11571
|
+
}
|
|
11007
11572
|
}
|
|
11008
11573
|
return 0;
|
|
11009
11574
|
}
|
|
@@ -11022,12 +11587,21 @@ function ac_center$2(gobj, event, kw, src) {
|
|
|
11022
11587
|
*
|
|
11023
11588
|
************************************************************/
|
|
11024
11589
|
function ac_node_click$4(gobj, event, kw, src) {
|
|
11025
|
-
let
|
|
11590
|
+
let priv = gobj.priv;
|
|
11591
|
+
let graph = priv.graph;
|
|
11026
11592
|
let node_id = kw.evt.target.id;
|
|
11027
11593
|
let nodedata;
|
|
11028
11594
|
try {
|
|
11029
11595
|
nodedata = graph.getNodeData(node_id);
|
|
11030
11596
|
} catch (e) {}
|
|
11597
|
+
if (priv.anchor_state === "arming") {
|
|
11598
|
+
priv.anchor_id = node_id;
|
|
11599
|
+
priv.anchor_path = nodedata && nodedata.data ? nodedata.data.path : "";
|
|
11600
|
+
priv.anchor_state = "on";
|
|
11601
|
+
yui_graph_update_anchor(gobj_read_attr(gobj, "$container"), priv.anchor_state);
|
|
11602
|
+
yui_graph_center_on(graph, node_id);
|
|
11603
|
+
return 0;
|
|
11604
|
+
}
|
|
11031
11605
|
if (nodedata && nodedata.data) gobj_publish_event(gobj, "EV_JSON_ITEM_CLICKED", {
|
|
11032
11606
|
path: nodedata.data.path,
|
|
11033
11607
|
id: node_id
|
|
@@ -11161,6 +11735,11 @@ function create_gclass$11(gclass_name) {
|
|
|
11161
11735
|
ac_node_click$4,
|
|
11162
11736
|
null
|
|
11163
11737
|
],
|
|
11738
|
+
[
|
|
11739
|
+
"EV_TOGGLE_ANCHOR",
|
|
11740
|
+
ac_toggle_anchor$2,
|
|
11741
|
+
null
|
|
11742
|
+
],
|
|
11164
11743
|
[
|
|
11165
11744
|
"EV_RESIZE",
|
|
11166
11745
|
ac_resize$2,
|
|
@@ -11191,6 +11770,7 @@ function create_gclass$11(gclass_name) {
|
|
|
11191
11770
|
["EV_ZOOM_RESET", 0],
|
|
11192
11771
|
["EV_CENTER", 0],
|
|
11193
11772
|
["EV_NODE_CLICK", 0],
|
|
11773
|
+
["EV_TOGGLE_ANCHOR", 0],
|
|
11194
11774
|
["EV_RESIZE", 0],
|
|
11195
11775
|
["EV_SHOW", 0],
|
|
11196
11776
|
["EV_HIDE", 0],
|
|
@@ -11207,226 +11787,6 @@ function register_c_yui_json_graph() {
|
|
|
11207
11787
|
if (gclass_find_by_name(GCLASS_NAME$11, false)) return 0;
|
|
11208
11788
|
return create_gclass$11(GCLASS_NAME$11);
|
|
11209
11789
|
}
|
|
11210
|
-
/************************************************************
|
|
11211
|
-
* JSON type discriminator (viewer vocabulary)
|
|
11212
|
-
************************************************************/
|
|
11213
|
-
function json_type(value) {
|
|
11214
|
-
if (value === null) return "null";
|
|
11215
|
-
if (Array.isArray(value)) return "array";
|
|
11216
|
-
switch (typeof value) {
|
|
11217
|
-
case "string": return "string";
|
|
11218
|
-
case "number": return "number";
|
|
11219
|
-
case "boolean": return "boolean";
|
|
11220
|
-
case "object": return "object";
|
|
11221
|
-
default: return "unknown";
|
|
11222
|
-
}
|
|
11223
|
-
}
|
|
11224
|
-
/************************************************************
|
|
11225
|
-
* Detect a kw_collapse() sentinel.
|
|
11226
|
-
*
|
|
11227
|
-
* The kernel replaces an over-limit dict with:
|
|
11228
|
-
* { "__collapsed__": { "path": ..., "size": N } }
|
|
11229
|
-
* and an over-limit array with:
|
|
11230
|
-
* [ { "__collapsed__": { "path": ..., "size": N } } ]
|
|
11231
|
-
*
|
|
11232
|
-
* Returns { size, path, is_array } or null.
|
|
11233
|
-
************************************************************/
|
|
11234
|
-
function is_collapsed(value) {
|
|
11235
|
-
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
11236
|
-
let keys = Object.keys(value);
|
|
11237
|
-
if (keys.length === 1 && keys[0] === "__collapsed__") {
|
|
11238
|
-
let c = value.__collapsed__ || {};
|
|
11239
|
-
return {
|
|
11240
|
-
size: c.size,
|
|
11241
|
-
path: c.path,
|
|
11242
|
-
is_array: false
|
|
11243
|
-
};
|
|
11244
|
-
}
|
|
11245
|
-
return null;
|
|
11246
|
-
}
|
|
11247
|
-
if (Array.isArray(value) && value.length === 1) {
|
|
11248
|
-
let e = value[0];
|
|
11249
|
-
if (e && typeof e === "object" && !Array.isArray(e) && Object.keys(e).length === 1 && e.__collapsed__) {
|
|
11250
|
-
let c = e.__collapsed__ || {};
|
|
11251
|
-
return {
|
|
11252
|
-
size: c.size,
|
|
11253
|
-
path: c.path,
|
|
11254
|
-
is_array: true
|
|
11255
|
-
};
|
|
11256
|
-
}
|
|
11257
|
-
}
|
|
11258
|
-
return null;
|
|
11259
|
-
}
|
|
11260
|
-
/************************************************************
|
|
11261
|
-
* Join / split absolute path segments (kernel convention)
|
|
11262
|
-
************************************************************/
|
|
11263
|
-
function seg_join(segments) {
|
|
11264
|
-
return segments.map(function(s) {
|
|
11265
|
-
return String(s);
|
|
11266
|
-
}).join("`");
|
|
11267
|
-
}
|
|
11268
|
-
function seg_split(path) {
|
|
11269
|
-
if (path === "" || path === null || path === void 0) return [];
|
|
11270
|
-
return path.split("`");
|
|
11271
|
-
}
|
|
11272
|
-
/************************************************************
|
|
11273
|
-
* Walk a JSON tree by absolute segments; undefined if the
|
|
11274
|
-
* path does not resolve.
|
|
11275
|
-
************************************************************/
|
|
11276
|
-
function get_by_segments(root, segments) {
|
|
11277
|
-
let v = root;
|
|
11278
|
-
for (let seg of segments) {
|
|
11279
|
-
if (v === null || v === void 0) return;
|
|
11280
|
-
if (Array.isArray(v)) v = v[Number(seg)];
|
|
11281
|
-
else if (typeof v === "object") v = v[seg];
|
|
11282
|
-
else return;
|
|
11283
|
-
}
|
|
11284
|
-
return v;
|
|
11285
|
-
}
|
|
11286
|
-
/************************************************************
|
|
11287
|
-
* Set a value at absolute segments (mutates root).
|
|
11288
|
-
*
|
|
11289
|
-
* segments == [] means "replace the whole root" — the caller
|
|
11290
|
-
* must use the RETURNED value in that case (a primitive/array
|
|
11291
|
-
* root cannot be mutated in place).
|
|
11292
|
-
************************************************************/
|
|
11293
|
-
function set_by_segments(root, segments, new_value) {
|
|
11294
|
-
if (segments.length === 0) return new_value;
|
|
11295
|
-
let parent = get_by_segments(root, segments.slice(0, -1));
|
|
11296
|
-
if (parent === null || parent === void 0 || typeof parent !== "object") return root;
|
|
11297
|
-
let last = segments[segments.length - 1];
|
|
11298
|
-
if (Array.isArray(parent)) parent[Number(last)] = new_value;
|
|
11299
|
-
else parent[last] = new_value;
|
|
11300
|
-
return root;
|
|
11301
|
-
}
|
|
11302
|
-
/************************************************************
|
|
11303
|
-
* Does `value` (or any loaded descendant / its own key)
|
|
11304
|
-
* contain the lower-cased search term?
|
|
11305
|
-
*
|
|
11306
|
-
* Collapsed (not-yet-loaded) subtrees can't be searched, so
|
|
11307
|
-
* they never match on content — only their key can match,
|
|
11308
|
-
* which the renderer checks separately.
|
|
11309
|
-
************************************************************/
|
|
11310
|
-
function subtree_matches(value, term) {
|
|
11311
|
-
if (!term) return true;
|
|
11312
|
-
if (is_collapsed(value)) return false;
|
|
11313
|
-
let type = json_type(value);
|
|
11314
|
-
if (type === "object") {
|
|
11315
|
-
for (let [k, v] of Object.entries(value)) {
|
|
11316
|
-
if (String(k).toLowerCase().includes(term)) return true;
|
|
11317
|
-
if (subtree_matches(v, term)) return true;
|
|
11318
|
-
}
|
|
11319
|
-
return false;
|
|
11320
|
-
}
|
|
11321
|
-
if (type === "array") {
|
|
11322
|
-
for (let v of value) if (subtree_matches(v, term)) return true;
|
|
11323
|
-
return false;
|
|
11324
|
-
}
|
|
11325
|
-
return String(value).toLowerCase().includes(term);
|
|
11326
|
-
}
|
|
11327
|
-
/************************************************************
|
|
11328
|
-
* Fields whose numeric value is an epoch timestamp.
|
|
11329
|
-
* Mirrors the timestampTag list wired into C_YUI_FORM's
|
|
11330
|
-
* jsoneditor control, so the two viewers agree.
|
|
11331
|
-
************************************************************/
|
|
11332
|
-
function is_time_field(field) {
|
|
11333
|
-
return field === "__t__" || field === "__tm__" || field === "tm" || field === "t" || field === "time" || field === "from_t" || field === "to_t" || field === "t_input" || field === "t_output" || field === "from_tm" || field === "to_tm";
|
|
11334
|
-
}
|
|
11335
|
-
/************************************************************
|
|
11336
|
-
* Format an epoch value as a local wall-clock string.
|
|
11337
|
-
* Accepts seconds or milliseconds; returns null when the
|
|
11338
|
-
* value is not a usable positive timestamp (0 == "unset").
|
|
11339
|
-
************************************************************/
|
|
11340
|
-
function format_epoch(value) {
|
|
11341
|
-
if (typeof value !== "number" || !isFinite(value) || value <= 0) return null;
|
|
11342
|
-
let ms = Math.abs(value) < 0xe8d4a51000 ? value * 1e3 : value;
|
|
11343
|
-
let d = new Date(ms);
|
|
11344
|
-
if (isNaN(d.getTime())) return null;
|
|
11345
|
-
return d.toLocaleString();
|
|
11346
|
-
}
|
|
11347
|
-
/************************************************************
|
|
11348
|
-
* The text view's payload: the document as text, indented
|
|
11349
|
-
* four characters, cut at `max_chars`.
|
|
11350
|
-
*
|
|
11351
|
-
* Returns {text, capped}. `capped` is what the caller
|
|
11352
|
-
* announces under the dump — a cut that is not announced
|
|
11353
|
-
* reads as a document that ends there.
|
|
11354
|
-
************************************************************/
|
|
11355
|
-
function json_text_dump(root, max_chars) {
|
|
11356
|
-
let text;
|
|
11357
|
-
try {
|
|
11358
|
-
text = JSON.stringify(root, null, 4);
|
|
11359
|
-
} catch (e) {
|
|
11360
|
-
return {
|
|
11361
|
-
text: "",
|
|
11362
|
-
capped: false,
|
|
11363
|
-
error: String(e)
|
|
11364
|
-
};
|
|
11365
|
-
}
|
|
11366
|
-
if (text === void 0) return {
|
|
11367
|
-
text: "",
|
|
11368
|
-
capped: false,
|
|
11369
|
-
error: "not representable as JSON"
|
|
11370
|
-
};
|
|
11371
|
-
if (typeof max_chars === "number" && max_chars > 0 && text.length > max_chars) return {
|
|
11372
|
-
text: text.slice(0, max_chars),
|
|
11373
|
-
capped: true
|
|
11374
|
-
};
|
|
11375
|
-
return {
|
|
11376
|
-
text,
|
|
11377
|
-
capped: false
|
|
11378
|
-
};
|
|
11379
|
-
}
|
|
11380
|
-
/************************************************************
|
|
11381
|
-
* Which view a freshly mounted viewer opens on.
|
|
11382
|
-
*
|
|
11383
|
-
* Three answers, in order of authority: what the HOST asked
|
|
11384
|
-
* for, what the READER chose last time, and `fallback`.
|
|
11385
|
-
*
|
|
11386
|
-
* The host wins because a viewer mounted to show a graph has
|
|
11387
|
-
* to show one. The memory comes next, because reopening a
|
|
11388
|
-
* document in the view you just left is the whole point of
|
|
11389
|
-
* remembering it. `fallback` is the floor.
|
|
11390
|
-
*
|
|
11391
|
-
* Anything not in `modes` is treated as unsaid, which covers
|
|
11392
|
-
* an absent value, an empty one, and a stored string from an
|
|
11393
|
-
* older release that named a view no longer offered.
|
|
11394
|
-
************************************************************/
|
|
11395
|
-
function pick_view_mode(asked, remembered, modes, fallback) {
|
|
11396
|
-
let known = (m) => Array.isArray(modes) && modes.indexOf(m) >= 0;
|
|
11397
|
-
if (known(asked)) return asked;
|
|
11398
|
-
if (known(remembered)) return remembered;
|
|
11399
|
-
return fallback;
|
|
11400
|
-
}
|
|
11401
|
-
/************************************************************
|
|
11402
|
-
* The short label a container row carries beside its size.
|
|
11403
|
-
*
|
|
11404
|
-
* An array of dicts that carry an `id` is a list of RECORDS,
|
|
11405
|
-
* and a row that says only `2: {15}` makes you open all
|
|
11406
|
-
* fifteen fields of every one of them to find out WHICH
|
|
11407
|
-
* record it is. The id is the one field that answers that,
|
|
11408
|
-
* so the row says it without being opened.
|
|
11409
|
-
*
|
|
11410
|
-
* Only a SCALAR id, and only when it is really there: an `id`
|
|
11411
|
-
* that is itself a dict or a list is not a name, it is more
|
|
11412
|
-
* document, and printing `[object Object]` beside the size
|
|
11413
|
-
* would be worse than printing nothing.
|
|
11414
|
-
*
|
|
11415
|
-
* Cut at `max_chars`, because this rides on a row whose job
|
|
11416
|
-
* is to stay one line.
|
|
11417
|
-
************************************************************/
|
|
11418
|
-
function container_label(value, max_chars) {
|
|
11419
|
-
if (value === null || typeof value !== "object" || Array.isArray(value)) return "";
|
|
11420
|
-
if (!Object.prototype.hasOwnProperty.call(value, "id")) return "";
|
|
11421
|
-
let id = value.id;
|
|
11422
|
-
let t = typeof id;
|
|
11423
|
-
if (t !== "string" && t !== "number" && t !== "boolean") return "";
|
|
11424
|
-
let text = String(id).trim();
|
|
11425
|
-
if (text === "") return "";
|
|
11426
|
-
let cap = typeof max_chars === "number" && max_chars > 0 ? max_chars : 0;
|
|
11427
|
-
if (cap && text.length > cap) return text.slice(0, cap) + "…";
|
|
11428
|
-
return text;
|
|
11429
|
-
}
|
|
11430
11790
|
//#endregion
|
|
11431
11791
|
//#region src/c_yui_json.js
|
|
11432
11792
|
/***********************************************************************
|
|
@@ -12712,7 +13072,11 @@ var PRIVATE_DATA$9 = {
|
|
|
12712
13072
|
node_by_id: null,
|
|
12713
13073
|
collapse_state: null,
|
|
12714
13074
|
resize_observer: null,
|
|
12715
|
-
_resize_raf: 0
|
|
13075
|
+
_resize_raf: 0,
|
|
13076
|
+
anchor_id: "",
|
|
13077
|
+
anchor_name: "",
|
|
13078
|
+
anchor_state: "off",
|
|
13079
|
+
last_zoom: 1
|
|
12716
13080
|
};
|
|
12717
13081
|
var __gclass__$9 = null;
|
|
12718
13082
|
var __layout_registered__ = false;
|
|
@@ -13105,6 +13469,7 @@ function make_toolbar$1(gobj) {
|
|
|
13105
13469
|
let center_items = [];
|
|
13106
13470
|
let right_items = [];
|
|
13107
13471
|
center_items = yui_graph_camera_items(gobj, priv.graph, toolbar_wide);
|
|
13472
|
+
center_items.push(yui_graph_anchor_item(gobj, toolbar_wide));
|
|
13108
13473
|
center_items.push(yui_graph_refresh_item(gobj, toolbar_wide));
|
|
13109
13474
|
let options = [];
|
|
13110
13475
|
for (let key of Object.keys(LAYOUTS)) {
|
|
@@ -13180,6 +13545,15 @@ function build_graph$2(gobj) {
|
|
|
13180
13545
|
if (priv.theme) graph.setTheme(priv.theme);
|
|
13181
13546
|
graph.on("aftertransform", () => {
|
|
13182
13547
|
yui_graph_update_zoom(gobj_read_attr(gobj, "$container"), priv.graph);
|
|
13548
|
+
let z = priv.last_zoom;
|
|
13549
|
+
try {
|
|
13550
|
+
z = priv.graph.getZoom();
|
|
13551
|
+
} catch (e) {
|
|
13552
|
+
return;
|
|
13553
|
+
}
|
|
13554
|
+
if (z === priv.last_zoom) return;
|
|
13555
|
+
priv.last_zoom = z;
|
|
13556
|
+
if (priv.anchor_state === "on" && priv.anchor_id) yui_graph_center_on(priv.graph, priv.anchor_id);
|
|
13183
13557
|
});
|
|
13184
13558
|
graph.on(NodeEvent.CLICK, (evt) => {
|
|
13185
13559
|
gobj_send_event(gobj, "EV_NODE_CLICK", { evt }, gobj);
|
|
@@ -13467,13 +13841,23 @@ function load_tree(gobj) {
|
|
|
13467
13841
|
let edges = [];
|
|
13468
13842
|
build_gobj_nodes(gobj, yuno, nodes, edges, null, true, layout_cfg.compact);
|
|
13469
13843
|
if (nodes.length > 0) {
|
|
13844
|
+
let root_id = nodes[0].id;
|
|
13470
13845
|
graph.setData({
|
|
13471
13846
|
nodes,
|
|
13472
13847
|
edges
|
|
13473
13848
|
});
|
|
13474
13849
|
graph.render().then(() => {
|
|
13475
|
-
|
|
13476
|
-
|
|
13850
|
+
reanchor(gobj);
|
|
13851
|
+
if (anchor) {
|
|
13852
|
+
restore_view_anchored(gobj, anchor);
|
|
13853
|
+
return;
|
|
13854
|
+
}
|
|
13855
|
+
if (preserve_view) return;
|
|
13856
|
+
Promise.resolve(graph.zoomTo(1)).then(() => {
|
|
13857
|
+
if (!yui_graph_center_on(graph, priv.anchor_id)) yui_graph_center_on(graph, root_id);
|
|
13858
|
+
priv.last_zoom = 1;
|
|
13859
|
+
yui_graph_update_zoom(gobj_read_attr(gobj, "$container"), priv.graph);
|
|
13860
|
+
});
|
|
13477
13861
|
});
|
|
13478
13862
|
}
|
|
13479
13863
|
}
|
|
@@ -13975,13 +14359,77 @@ function ac_collapse_all(gobj, event, kw, src) {
|
|
|
13975
14359
|
return 0;
|
|
13976
14360
|
}
|
|
13977
14361
|
/************************************************************
|
|
14362
|
+
* Every camera action ends the same way once an anchor is
|
|
14363
|
+
* set: the thing you were reading goes back to the middle.
|
|
14364
|
+
*
|
|
14365
|
+
* Chained on the promise G6 returns rather than run beside it
|
|
14366
|
+
* -- centring before the zoom has finished centres the OLD
|
|
14367
|
+
* view, and the reader watches the node slide away and come
|
|
14368
|
+
* back.
|
|
14369
|
+
************************************************************/
|
|
14370
|
+
function after_camera$1(gobj, moved) {
|
|
14371
|
+
let priv = gobj.priv;
|
|
14372
|
+
if (priv.anchor_state !== "on" || !priv.anchor_id) return;
|
|
14373
|
+
Promise.resolve(moved).then(function() {
|
|
14374
|
+
yui_graph_center_on(priv.graph, priv.anchor_id);
|
|
14375
|
+
}).catch(function() {});
|
|
14376
|
+
}
|
|
14377
|
+
/************************************************************
|
|
14378
|
+
* Point the anchor back at its node after a rebuild.
|
|
14379
|
+
*
|
|
14380
|
+
* Node ids are generated per build, so the id the reader chose
|
|
14381
|
+
* does not survive an expand, a collapse or a refresh -- the
|
|
14382
|
+
* `full_name` does, and it is what the anchor really means:
|
|
14383
|
+
* "this gobj", not "this card".
|
|
14384
|
+
*
|
|
14385
|
+
* A gobj no longer drawn (its branch got collapsed, or it went
|
|
14386
|
+
* away) releases the anchor rather than leaving a button that
|
|
14387
|
+
* claims to be locked onto something invisible.
|
|
14388
|
+
************************************************************/
|
|
14389
|
+
function reanchor(gobj) {
|
|
14390
|
+
let priv = gobj.priv;
|
|
14391
|
+
if (priv.anchor_state !== "on" || !priv.anchor_name) return;
|
|
14392
|
+
let found = "";
|
|
14393
|
+
if (priv.node_by_id) {
|
|
14394
|
+
for (let id of Object.keys(priv.node_by_id)) if (priv.node_by_id[id].full_name === priv.anchor_name) {
|
|
14395
|
+
found = id;
|
|
14396
|
+
break;
|
|
14397
|
+
}
|
|
14398
|
+
}
|
|
14399
|
+
priv.anchor_id = found;
|
|
14400
|
+
if (!found) {
|
|
14401
|
+
priv.anchor_state = "off";
|
|
14402
|
+
priv.anchor_name = "";
|
|
14403
|
+
}
|
|
14404
|
+
yui_graph_update_anchor(gobj_read_attr(gobj, "$container"), priv.anchor_state);
|
|
14405
|
+
}
|
|
14406
|
+
/************************************************************
|
|
14407
|
+
* Arm the anchor, or let it go.
|
|
14408
|
+
*
|
|
14409
|
+
* Three states and one button, so a press ADVANCES: with no
|
|
14410
|
+
* target it starts waiting for one, and with a target it
|
|
14411
|
+
* releases. Pressing it while it waits is a cancel, which is
|
|
14412
|
+
* what somebody who pressed it by mistake will try.
|
|
14413
|
+
************************************************************/
|
|
14414
|
+
function ac_toggle_anchor$1(gobj, event, kw, src) {
|
|
14415
|
+
let priv = gobj.priv;
|
|
14416
|
+
if (priv.anchor_state === "off") priv.anchor_state = "arming";
|
|
14417
|
+
else {
|
|
14418
|
+
priv.anchor_state = "off";
|
|
14419
|
+
priv.anchor_id = "";
|
|
14420
|
+
priv.anchor_name = "";
|
|
14421
|
+
}
|
|
14422
|
+
yui_graph_update_anchor(gobj_read_attr(gobj, "$container"), priv.anchor_state);
|
|
14423
|
+
return 0;
|
|
14424
|
+
}
|
|
14425
|
+
/************************************************************
|
|
13978
14426
|
*
|
|
13979
14427
|
************************************************************/
|
|
13980
14428
|
function ac_zoom_in$1(gobj, event, kw, src) {
|
|
13981
14429
|
let graph = gobj.priv.graph;
|
|
13982
14430
|
if (graph) {
|
|
13983
14431
|
let z = graph.getZoom();
|
|
13984
|
-
graph.zoomTo(z * 1.2);
|
|
14432
|
+
after_camera$1(gobj, graph.zoomTo(z * 1.2));
|
|
13985
14433
|
}
|
|
13986
14434
|
return 0;
|
|
13987
14435
|
}
|
|
@@ -13992,7 +14440,7 @@ function ac_zoom_out$1(gobj, event, kw, src) {
|
|
|
13992
14440
|
let graph = gobj.priv.graph;
|
|
13993
14441
|
if (graph) {
|
|
13994
14442
|
let z = graph.getZoom();
|
|
13995
|
-
graph.zoomTo(z * .8);
|
|
14443
|
+
after_camera$1(gobj, graph.zoomTo(z * .8));
|
|
13996
14444
|
}
|
|
13997
14445
|
return 0;
|
|
13998
14446
|
}
|
|
@@ -14000,10 +14448,14 @@ function ac_zoom_out$1(gobj, event, kw, src) {
|
|
|
14000
14448
|
*
|
|
14001
14449
|
************************************************************/
|
|
14002
14450
|
function ac_zoom_reset$1(gobj, event, kw, src) {
|
|
14003
|
-
let
|
|
14451
|
+
let priv = gobj.priv;
|
|
14452
|
+
let graph = priv.graph;
|
|
14004
14453
|
if (graph && graph.rendered) {
|
|
14005
|
-
graph.zoomTo(1);
|
|
14006
|
-
|
|
14454
|
+
if (priv.anchor_state === "on" && priv.anchor_id) after_camera$1(gobj, graph.zoomTo(1));
|
|
14455
|
+
else {
|
|
14456
|
+
graph.zoomTo(1);
|
|
14457
|
+
graph.translateTo([0, 0]);
|
|
14458
|
+
}
|
|
14007
14459
|
}
|
|
14008
14460
|
return 0;
|
|
14009
14461
|
}
|
|
@@ -14031,6 +14483,14 @@ function ac_node_click$3(gobj, event, kw, src) {
|
|
|
14031
14483
|
if (nd && nd.data) node_data = nd.data;
|
|
14032
14484
|
} catch (e) {}
|
|
14033
14485
|
if (!node_data) return 0;
|
|
14486
|
+
if (priv.anchor_state === "arming") {
|
|
14487
|
+
priv.anchor_id = node_id;
|
|
14488
|
+
priv.anchor_name = node_data.full_name || "";
|
|
14489
|
+
priv.anchor_state = "on";
|
|
14490
|
+
yui_graph_update_anchor(gobj_read_attr(gobj, "$container"), priv.anchor_state);
|
|
14491
|
+
yui_graph_center_on(graph, node_id);
|
|
14492
|
+
return 0;
|
|
14493
|
+
}
|
|
14034
14494
|
let client_x = kw.evt && kw.evt.client ? kw.evt.client.x : 0;
|
|
14035
14495
|
let client_y = kw.evt && kw.evt.client ? kw.evt.client.y : 0;
|
|
14036
14496
|
show_popover(gobj, node_data, client_x, client_y);
|
|
@@ -14111,6 +14571,7 @@ function create_gclass$9(gclass_name) {
|
|
|
14111
14571
|
["EV_ZOOM_RESET", 0],
|
|
14112
14572
|
["EV_CENTER", 0],
|
|
14113
14573
|
["EV_NODE_CLICK", 0],
|
|
14574
|
+
["EV_TOGGLE_ANCHOR", 0],
|
|
14114
14575
|
["EV_RESIZE", 0],
|
|
14115
14576
|
["EV_SHOW", 0],
|
|
14116
14577
|
["EV_HIDE", 0]
|
|
@@ -14170,6 +14631,11 @@ function create_gclass$9(gclass_name) {
|
|
|
14170
14631
|
ac_node_click$3,
|
|
14171
14632
|
null
|
|
14172
14633
|
],
|
|
14634
|
+
[
|
|
14635
|
+
"EV_TOGGLE_ANCHOR",
|
|
14636
|
+
ac_toggle_anchor$1,
|
|
14637
|
+
null
|
|
14638
|
+
],
|
|
14173
14639
|
[
|
|
14174
14640
|
"EV_RESIZE",
|
|
14175
14641
|
ac_resize$1,
|
|
@@ -20732,123 +21198,6 @@ function register_c_yui_treedb_topic_with_form() {
|
|
|
20732
21198
|
return create_gclass$5(GCLASS_NAME$5);
|
|
20733
21199
|
}
|
|
20734
21200
|
//#endregion
|
|
20735
|
-
//#region src/lib_graph.js
|
|
20736
|
-
/************************************************************
|
|
20737
|
-
* Add class to elements selected
|
|
20738
|
-
************************************************************/
|
|
20739
|
-
function addClasses($container, selector, ...classNames) {
|
|
20740
|
-
$container.querySelectorAll(selector).forEach((el) => el.classList.add(...classNames));
|
|
20741
|
-
}
|
|
20742
|
-
/************************************************************
|
|
20743
|
-
* Remove class from elements selected
|
|
20744
|
-
************************************************************/
|
|
20745
|
-
function removeClasses($container, selector, ...classNames) {
|
|
20746
|
-
$container.querySelectorAll(selector).forEach((el) => el.classList.remove(...classNames));
|
|
20747
|
-
}
|
|
20748
|
-
/************************************************************
|
|
20749
|
-
* Toggle class to elements selected
|
|
20750
|
-
************************************************************/
|
|
20751
|
-
function toggleClasses($container, selector, ...classNames) {
|
|
20752
|
-
$container.querySelectorAll(selector).forEach((el) => classNames.forEach((cls) => el.classList.toggle(cls)));
|
|
20753
|
-
}
|
|
20754
|
-
/************************************************************
|
|
20755
|
-
* Remove child elements
|
|
20756
|
-
************************************************************/
|
|
20757
|
-
function removeChildElements($element) {
|
|
20758
|
-
$element.innerHTML = "";
|
|
20759
|
-
}
|
|
20760
|
-
/************************************************************
|
|
20761
|
-
* Disable/Enable elements
|
|
20762
|
-
************************************************************/
|
|
20763
|
-
function disableElements($container, selector) {
|
|
20764
|
-
$container.querySelectorAll(selector).forEach((el) => {
|
|
20765
|
-
el.setAttribute("disabled", "");
|
|
20766
|
-
});
|
|
20767
|
-
}
|
|
20768
|
-
function enableElements($container, selector) {
|
|
20769
|
-
$container.querySelectorAll(selector).forEach((el) => {
|
|
20770
|
-
if (el.hasAttribute("disabled")) el.removeAttribute("disabled");
|
|
20771
|
-
});
|
|
20772
|
-
}
|
|
20773
|
-
/************************************************************
|
|
20774
|
-
* Set or reset the color of 'enable submit action' state
|
|
20775
|
-
* Color green
|
|
20776
|
-
************************************************************/
|
|
20777
|
-
function set_submit_state($container, selector, set) {
|
|
20778
|
-
if (set) addClasses($container, selector, "color_submit_state");
|
|
20779
|
-
else removeClasses($container, selector, "color_submit_state");
|
|
20780
|
-
}
|
|
20781
|
-
/************************************************************
|
|
20782
|
-
* Set or reset the color of 'enable cancel action' state
|
|
20783
|
-
* Color red
|
|
20784
|
-
************************************************************/
|
|
20785
|
-
function set_cancel_state($container, selector, set) {
|
|
20786
|
-
if (set) addClasses($container, selector, "color_cancel_state");
|
|
20787
|
-
else removeClasses($container, selector, "color_cancel_state");
|
|
20788
|
-
}
|
|
20789
|
-
/************************************************************
|
|
20790
|
-
* Set or reset the color of 'enable actived' state
|
|
20791
|
-
* Color orange
|
|
20792
|
-
************************************************************/
|
|
20793
|
-
function set_active_state($container, selector, set) {
|
|
20794
|
-
if (set) addClasses($container, selector, "color_active_state");
|
|
20795
|
-
else removeClasses($container, selector, "color_active_state");
|
|
20796
|
-
}
|
|
20797
|
-
/************************************************************
|
|
20798
|
-
* Set or reset the 'pressed' state of a TOGGLE.
|
|
20799
|
-
*
|
|
20800
|
-
* Not one of the colours above: each of those names a KIND of
|
|
20801
|
-
* action (create, pending, history, destructive), and a toggle
|
|
20802
|
-
* is not an action -- it is a state the control is IN. So it
|
|
20803
|
-
* looks pressed instead of changing category, which also keeps
|
|
20804
|
-
* it out of the way of a neighbour wearing the same hue.
|
|
20805
|
-
************************************************************/
|
|
20806
|
-
function set_pressed_state($container, selector, set) {
|
|
20807
|
-
if (set) addClasses($container, selector, "pressed_state");
|
|
20808
|
-
else removeClasses($container, selector, "pressed_state");
|
|
20809
|
-
}
|
|
20810
|
-
/**
|
|
20811
|
-
* Returns a smart stroke color in `rgba()` format based on:
|
|
20812
|
-
* - The given fill color (hex or rgb[a])
|
|
20813
|
-
* - The current theme ('light' or 'dark')
|
|
20814
|
-
* - The adjustment factor (default 0.2)
|
|
20815
|
-
*
|
|
20816
|
-
* @param {string} fillColor - Fill color string
|
|
20817
|
-
* @param {string} theme - 'light' or 'dark'
|
|
20818
|
-
* @param {number} factor - Lighten/darken factor (default 0.2)
|
|
20819
|
-
* @returns {string} rgba(r, g, b, a)
|
|
20820
|
-
*/
|
|
20821
|
-
function getStrokeColor(fillColor, theme = "light", factor = .2) {
|
|
20822
|
-
let r, g, b, a = 1;
|
|
20823
|
-
if (fillColor.startsWith("#")) {
|
|
20824
|
-
let hex = fillColor.slice(1);
|
|
20825
|
-
if (hex.length === 3 || hex.length === 4) hex = hex.split("").map((c) => c + c).join("");
|
|
20826
|
-
if (hex.length === 6) {
|
|
20827
|
-
r = parseInt(hex.slice(0, 2), 16);
|
|
20828
|
-
g = parseInt(hex.slice(2, 4), 16);
|
|
20829
|
-
b = parseInt(hex.slice(4, 6), 16);
|
|
20830
|
-
} else if (hex.length === 8) {
|
|
20831
|
-
r = parseInt(hex.slice(0, 2), 16);
|
|
20832
|
-
g = parseInt(hex.slice(2, 4), 16);
|
|
20833
|
-
b = parseInt(hex.slice(4, 6), 16);
|
|
20834
|
-
a = parseInt(hex.slice(6, 8), 16) / 255;
|
|
20835
|
-
} else throw new Error("Unsupported hex format");
|
|
20836
|
-
} else if (fillColor.startsWith("rgb")) {
|
|
20837
|
-
const match = fillColor.match(/rgba?\(\s*(\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\s*\)/);
|
|
20838
|
-
if (!match) throw new Error("Invalid rgb()/rgba() format.");
|
|
20839
|
-
r = parseInt(match[1], 10);
|
|
20840
|
-
g = parseInt(match[2], 10);
|
|
20841
|
-
b = parseInt(match[3], 10);
|
|
20842
|
-
if (match[4] !== void 0) a = parseFloat(match[4]);
|
|
20843
|
-
} else throw new Error("Unsupported color format.");
|
|
20844
|
-
const luminance = (.299 * r + .587 * g + .114 * b) / 255;
|
|
20845
|
-
let lighten;
|
|
20846
|
-
if (theme === "dark") lighten = luminance < .7;
|
|
20847
|
-
else lighten = luminance < .3;
|
|
20848
|
-
const adjust = (v) => lighten ? Math.min(255, Math.floor(v + (255 - v) * factor)) : Math.max(0, Math.floor(v * (1 - factor)));
|
|
20849
|
-
return `rgba(${adjust(r)}, ${adjust(g)}, ${adjust(b)}, ${a.toFixed(3)})`;
|
|
20850
|
-
}
|
|
20851
|
-
//#endregion
|
|
20852
21201
|
//#region src/treedb_node_label.js
|
|
20853
21202
|
/***********************************************************************
|
|
20854
21203
|
* treedb_node_label.js
|
|
@@ -47716,6 +48065,13 @@ var ICONS = `<svg xmlns="http://www.w3.org/2000/svg">
|
|
|
47716
48065
|
stroke="currentColor" stroke-width="40"
|
|
47717
48066
|
stroke-dasharray="59 36"/>
|
|
47718
48067
|
</symbol>
|
|
48068
|
+
<!-- location-crosshairs (anchor the camera on one element).
|
|
48069
|
+
The SAME path as the DOM toolbars and their yi-location-crosshairs
|
|
48070
|
+
mask: the two toolbars sit in one console, and a control that
|
|
48071
|
+
does one thing has to be drawn one way. -->
|
|
48072
|
+
<symbol id="g6-icon-anchor" viewBox="0 0 576 512">
|
|
48073
|
+
<path fill="currentColor" d="M288-16c17.7 0 32 14.3 32 32l0 18.3c98.1 14 175.7 91.6 189.7 189.7l18.3 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l-18.3 0c-14 98.1-91.6 175.7-189.7 189.7l0 18.3c0 17.7-14.3 32-32 32s-32-14.3-32-32l0-18.3C157.9 463.7 80.3 386.1 66.3 288L48 288c-17.7 0-32-14.3-32-32s14.3-32 32-32l18.3 0C80.3 125.9 157.9 48.3 256 34.3L256 16c0-17.7 14.3-32 32-32zM128 256a160 160 0 1 0 320 0 160 160 0 1 0 -320 0zm160-96a96 96 0 1 1 0 192 96 96 0 1 1 0-192z"/>
|
|
48074
|
+
</symbol>
|
|
47719
48075
|
<!-- floppy-disk (save) -->
|
|
47720
48076
|
<symbol id="g6-icon-save" viewBox="0 0 448 512">
|
|
47721
48077
|
<path fill="currentColor" d="M64 32C28.7 32 0 60.7 0 96L0 416c0 35.3 28.7 64 64 64l320 0c35.3 0 64-28.7 64-64l0-242.7c0-17-6.7-33.3-18.7-45.3L352 50.7C340 38.7 323.7 32 306.7 32L64 32zm32 96c0-17.7 14.3-32 32-32l160 0c17.7 0 32 14.3 32 32l0 64c0 17.7-14.3 32-32 32l-160 0c-17.7 0-32-14.3-32-32l0-64zM224 288a64 64 0 1 1 0 128 64 64 0 1 1 0-128z"/>
|
|
@@ -47909,6 +48265,9 @@ var attrs_table$4 = [
|
|
|
47909
48265
|
SDATA_END()
|
|
47910
48266
|
];
|
|
47911
48267
|
var PRIVATE_DATA$4 = {
|
|
48268
|
+
anchor_id: "",
|
|
48269
|
+
anchor_state: "off",
|
|
48270
|
+
last_zoom: 1,
|
|
47912
48271
|
_xy: 100,
|
|
47913
48272
|
_edge_seq: 0,
|
|
47914
48273
|
_history_paused: false,
|
|
@@ -48205,6 +48564,15 @@ function configure_events(gobj) {
|
|
|
48205
48564
|
update_node_icon_position(gobj);
|
|
48206
48565
|
update_link_icon_position(gobj);
|
|
48207
48566
|
update_zoom_readout(gobj);
|
|
48567
|
+
let z = priv.last_zoom;
|
|
48568
|
+
try {
|
|
48569
|
+
z = priv.graph.getZoom();
|
|
48570
|
+
} catch (e) {
|
|
48571
|
+
return;
|
|
48572
|
+
}
|
|
48573
|
+
if (z === priv.last_zoom) return;
|
|
48574
|
+
priv.last_zoom = z;
|
|
48575
|
+
if (priv.anchor_state === "on" && priv.anchor_id) yui_graph_center_on(priv.graph, priv.anchor_id);
|
|
48208
48576
|
});
|
|
48209
48577
|
priv._on_language_changed = () => {
|
|
48210
48578
|
update_toolbar(gobj);
|
|
@@ -48442,6 +48810,12 @@ function configure_toolbar(gobj) {
|
|
|
48442
48810
|
value: "reset",
|
|
48443
48811
|
className: "EV_ZOOM_RESET",
|
|
48444
48812
|
title: t("actual size")
|
|
48813
|
+
},
|
|
48814
|
+
{
|
|
48815
|
+
id: "g6-icon-anchor",
|
|
48816
|
+
value: "anchor",
|
|
48817
|
+
className: "EV_TOGGLE_ANCHOR" + (priv.anchor_state !== "off" ? " pressed_state" : "") + (priv.anchor_state === "arming" ? " color_pending_state" : ""),
|
|
48818
|
+
title: anchor_title(priv.anchor_state)
|
|
48445
48819
|
}
|
|
48446
48820
|
];
|
|
48447
48821
|
if (priv.edit_mode) items.push({
|
|
@@ -48483,6 +48857,9 @@ function configure_toolbar(gobj) {
|
|
|
48483
48857
|
case "reset":
|
|
48484
48858
|
gobj_send_event(gobj, "EV_ZOOM_RESET", {}, gobj);
|
|
48485
48859
|
break;
|
|
48860
|
+
case "anchor":
|
|
48861
|
+
gobj_send_event(gobj, "EV_TOGGLE_ANCHOR", {}, gobj);
|
|
48862
|
+
break;
|
|
48486
48863
|
case "auto-fit":
|
|
48487
48864
|
gobj_send_event(gobj, "EV_AUTO_FIT", {}, gobj);
|
|
48488
48865
|
break;
|
|
@@ -49493,12 +49870,12 @@ function graph_find_nodes(gobj, term) {
|
|
|
49493
49870
|
async function graph_zoom_in(gobj) {
|
|
49494
49871
|
let graph = gobj.priv.graph;
|
|
49495
49872
|
const z = graph.getZoom();
|
|
49496
|
-
|
|
49873
|
+
after_camera(gobj, graph.zoomTo(z * 1.1));
|
|
49497
49874
|
}
|
|
49498
49875
|
async function graph_zoom_out(gobj) {
|
|
49499
49876
|
let graph = gobj.priv.graph;
|
|
49500
49877
|
const z = graph.getZoom();
|
|
49501
|
-
|
|
49878
|
+
after_camera(gobj, graph.zoomTo(z * .9));
|
|
49502
49879
|
}
|
|
49503
49880
|
async function graph_render(gobj) {
|
|
49504
49881
|
await gobj.priv.graph.render();
|
|
@@ -52499,7 +52876,56 @@ function ac_zoom_out(gobj, event, kw, src) {
|
|
|
52499
52876
|
return 0;
|
|
52500
52877
|
}
|
|
52501
52878
|
function ac_zoom_reset(gobj, event, kw, src) {
|
|
52502
|
-
gobj.priv
|
|
52879
|
+
let priv = gobj.priv;
|
|
52880
|
+
let graph = priv.graph;
|
|
52881
|
+
if (priv.anchor_state === "on" && priv.anchor_id) after_camera(gobj, graph.zoomTo(1));
|
|
52882
|
+
else graph.zoomTo(1);
|
|
52883
|
+
return 0;
|
|
52884
|
+
}
|
|
52885
|
+
/************************************************************
|
|
52886
|
+
* What the anchor button says it will do next.
|
|
52887
|
+
************************************************************/
|
|
52888
|
+
function anchor_title(state) {
|
|
52889
|
+
if (state === "arming") return t("click the element to centre on");
|
|
52890
|
+
if (state === "on") return t("centred: click to release");
|
|
52891
|
+
return t("anchor view");
|
|
52892
|
+
}
|
|
52893
|
+
/************************************************************
|
|
52894
|
+
* Every camera action ends the same way once an anchor is
|
|
52895
|
+
* set: the thing you were reading goes back to the middle.
|
|
52896
|
+
*
|
|
52897
|
+
* Chained on the promise G6 returns rather than run beside it
|
|
52898
|
+
* -- centring before the zoom has finished centres the OLD
|
|
52899
|
+
* view, and the reader watches the node slide away and come
|
|
52900
|
+
* back.
|
|
52901
|
+
************************************************************/
|
|
52902
|
+
function after_camera(gobj, moved) {
|
|
52903
|
+
let priv = gobj.priv;
|
|
52904
|
+
if (priv.anchor_state !== "on" || !priv.anchor_id) return;
|
|
52905
|
+
Promise.resolve(moved).then(function() {
|
|
52906
|
+
yui_graph_center_on(priv.graph, priv.anchor_id);
|
|
52907
|
+
}).catch(function() {});
|
|
52908
|
+
}
|
|
52909
|
+
/************************************************************
|
|
52910
|
+
* Arm the anchor, or let it go.
|
|
52911
|
+
*
|
|
52912
|
+
* Three states and one button, so a press ADVANCES: with no
|
|
52913
|
+
* target it starts waiting for one, and with a target it
|
|
52914
|
+
* releases. Pressing it while it waits is a cancel, which is
|
|
52915
|
+
* what somebody who pressed it by mistake will try.
|
|
52916
|
+
*
|
|
52917
|
+
* Unlike the DOM toolbars, the button is not repainted in
|
|
52918
|
+
* place: this toolbar is a G6 plugin that rebuilds its whole
|
|
52919
|
+
* item list, and the pressed classes are baked into that list.
|
|
52920
|
+
************************************************************/
|
|
52921
|
+
function ac_toggle_anchor(gobj, event, kw, src) {
|
|
52922
|
+
let priv = gobj.priv;
|
|
52923
|
+
if (priv.anchor_state === "off") priv.anchor_state = "arming";
|
|
52924
|
+
else {
|
|
52925
|
+
priv.anchor_state = "off";
|
|
52926
|
+
priv.anchor_id = "";
|
|
52927
|
+
}
|
|
52928
|
+
update_toolbar(gobj);
|
|
52503
52929
|
return 0;
|
|
52504
52930
|
}
|
|
52505
52931
|
/************************************************************
|
|
@@ -52673,6 +53099,13 @@ function ac_node_click$2(gobj, event, kw, src) {
|
|
|
52673
53099
|
let priv = gobj.priv;
|
|
52674
53100
|
let graph = priv.graph;
|
|
52675
53101
|
let node_id = kw.evt.target.id;
|
|
53102
|
+
if (priv.anchor_state === "arming") {
|
|
53103
|
+
priv.anchor_id = node_id;
|
|
53104
|
+
priv.anchor_state = "on";
|
|
53105
|
+
update_toolbar(gobj);
|
|
53106
|
+
yui_graph_center_on(graph, node_id);
|
|
53107
|
+
return 0;
|
|
53108
|
+
}
|
|
52676
53109
|
try {
|
|
52677
53110
|
let nodedata = graph.getNodeData(node_id);
|
|
52678
53111
|
if (nodedata && nodedata.data && nodedata.data.desc) {
|
|
@@ -52934,6 +53367,11 @@ function create_gclass$4(gclass_name) {
|
|
|
52934
53367
|
ac_zoom_reset,
|
|
52935
53368
|
null
|
|
52936
53369
|
],
|
|
53370
|
+
[
|
|
53371
|
+
"EV_TOGGLE_ANCHOR",
|
|
53372
|
+
ac_toggle_anchor,
|
|
53373
|
+
null
|
|
53374
|
+
],
|
|
52937
53375
|
[
|
|
52938
53376
|
"EV_TOGGLE_TOOLBARS",
|
|
52939
53377
|
ac_toggle_toolbars,
|
|
@@ -53047,6 +53485,7 @@ function create_gclass$4(gclass_name) {
|
|
|
53047
53485
|
["EV_ZOOM_IN", 0],
|
|
53048
53486
|
["EV_ZOOM_OUT", 0],
|
|
53049
53487
|
["EV_ZOOM_RESET", 0],
|
|
53488
|
+
["EV_TOGGLE_ANCHOR", 0],
|
|
53050
53489
|
["EV_TOGGLE_TOOLBARS", 0],
|
|
53051
53490
|
["EV_AUTO_FIT", 0],
|
|
53052
53491
|
["EV_ZOOM_SELECTION", 0],
|