@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.cjs.js
CHANGED
|
@@ -9350,6 +9350,131 @@ function yui_toolbar(attrs = {}, items = []) {
|
|
|
9350
9350
|
return $toolbar;
|
|
9351
9351
|
}
|
|
9352
9352
|
//#endregion
|
|
9353
|
+
//#region src/lib_graph.js
|
|
9354
|
+
/***********************************************************************
|
|
9355
|
+
* lib_graph.js
|
|
9356
|
+
*
|
|
9357
|
+
* Utilities for graph, using Bulma
|
|
9358
|
+
*
|
|
9359
|
+
* Copyright (c) 2025, ArtGins.
|
|
9360
|
+
* All Rights Reserved.
|
|
9361
|
+
***********************************************************************/
|
|
9362
|
+
/************************************************************
|
|
9363
|
+
* Add class to elements selected
|
|
9364
|
+
************************************************************/
|
|
9365
|
+
function addClasses($container, selector, ...classNames) {
|
|
9366
|
+
$container.querySelectorAll(selector).forEach((el) => el.classList.add(...classNames));
|
|
9367
|
+
}
|
|
9368
|
+
/************************************************************
|
|
9369
|
+
* Remove class from elements selected
|
|
9370
|
+
************************************************************/
|
|
9371
|
+
function removeClasses($container, selector, ...classNames) {
|
|
9372
|
+
$container.querySelectorAll(selector).forEach((el) => el.classList.remove(...classNames));
|
|
9373
|
+
}
|
|
9374
|
+
/************************************************************
|
|
9375
|
+
* Toggle class to elements selected
|
|
9376
|
+
************************************************************/
|
|
9377
|
+
function toggleClasses($container, selector, ...classNames) {
|
|
9378
|
+
$container.querySelectorAll(selector).forEach((el) => classNames.forEach((cls) => el.classList.toggle(cls)));
|
|
9379
|
+
}
|
|
9380
|
+
/************************************************************
|
|
9381
|
+
* Remove child elements
|
|
9382
|
+
************************************************************/
|
|
9383
|
+
function removeChildElements($element) {
|
|
9384
|
+
$element.innerHTML = "";
|
|
9385
|
+
}
|
|
9386
|
+
/************************************************************
|
|
9387
|
+
* Disable/Enable elements
|
|
9388
|
+
************************************************************/
|
|
9389
|
+
function disableElements($container, selector) {
|
|
9390
|
+
$container.querySelectorAll(selector).forEach((el) => {
|
|
9391
|
+
el.setAttribute("disabled", "");
|
|
9392
|
+
});
|
|
9393
|
+
}
|
|
9394
|
+
function enableElements($container, selector) {
|
|
9395
|
+
$container.querySelectorAll(selector).forEach((el) => {
|
|
9396
|
+
if (el.hasAttribute("disabled")) el.removeAttribute("disabled");
|
|
9397
|
+
});
|
|
9398
|
+
}
|
|
9399
|
+
/************************************************************
|
|
9400
|
+
* Set or reset the color of 'enable submit action' state
|
|
9401
|
+
* Color green
|
|
9402
|
+
************************************************************/
|
|
9403
|
+
function set_submit_state($container, selector, set) {
|
|
9404
|
+
if (set) addClasses($container, selector, "color_submit_state");
|
|
9405
|
+
else removeClasses($container, selector, "color_submit_state");
|
|
9406
|
+
}
|
|
9407
|
+
/************************************************************
|
|
9408
|
+
* Set or reset the color of 'enable cancel action' state
|
|
9409
|
+
* Color red
|
|
9410
|
+
************************************************************/
|
|
9411
|
+
function set_cancel_state($container, selector, set) {
|
|
9412
|
+
if (set) addClasses($container, selector, "color_cancel_state");
|
|
9413
|
+
else removeClasses($container, selector, "color_cancel_state");
|
|
9414
|
+
}
|
|
9415
|
+
/************************************************************
|
|
9416
|
+
* Set or reset the color of 'enable actived' state
|
|
9417
|
+
* Color orange
|
|
9418
|
+
************************************************************/
|
|
9419
|
+
function set_active_state($container, selector, set) {
|
|
9420
|
+
if (set) addClasses($container, selector, "color_active_state");
|
|
9421
|
+
else removeClasses($container, selector, "color_active_state");
|
|
9422
|
+
}
|
|
9423
|
+
/************************************************************
|
|
9424
|
+
* Set or reset the 'pressed' state of a TOGGLE.
|
|
9425
|
+
*
|
|
9426
|
+
* Not one of the colours above: each of those names a KIND of
|
|
9427
|
+
* action (create, pending, history, destructive), and a toggle
|
|
9428
|
+
* is not an action -- it is a state the control is IN. So it
|
|
9429
|
+
* looks pressed instead of changing category, which also keeps
|
|
9430
|
+
* it out of the way of a neighbour wearing the same hue.
|
|
9431
|
+
************************************************************/
|
|
9432
|
+
function set_pressed_state($container, selector, set) {
|
|
9433
|
+
if (set) addClasses($container, selector, "pressed_state");
|
|
9434
|
+
else removeClasses($container, selector, "pressed_state");
|
|
9435
|
+
}
|
|
9436
|
+
/**
|
|
9437
|
+
* Returns a smart stroke color in `rgba()` format based on:
|
|
9438
|
+
* - The given fill color (hex or rgb[a])
|
|
9439
|
+
* - The current theme ('light' or 'dark')
|
|
9440
|
+
* - The adjustment factor (default 0.2)
|
|
9441
|
+
*
|
|
9442
|
+
* @param {string} fillColor - Fill color string
|
|
9443
|
+
* @param {string} theme - 'light' or 'dark'
|
|
9444
|
+
* @param {number} factor - Lighten/darken factor (default 0.2)
|
|
9445
|
+
* @returns {string} rgba(r, g, b, a)
|
|
9446
|
+
*/
|
|
9447
|
+
function getStrokeColor(fillColor, theme = "light", factor = .2) {
|
|
9448
|
+
let r, g, b, a = 1;
|
|
9449
|
+
if (fillColor.startsWith("#")) {
|
|
9450
|
+
let hex = fillColor.slice(1);
|
|
9451
|
+
if (hex.length === 3 || hex.length === 4) hex = hex.split("").map((c) => c + c).join("");
|
|
9452
|
+
if (hex.length === 6) {
|
|
9453
|
+
r = parseInt(hex.slice(0, 2), 16);
|
|
9454
|
+
g = parseInt(hex.slice(2, 4), 16);
|
|
9455
|
+
b = parseInt(hex.slice(4, 6), 16);
|
|
9456
|
+
} else if (hex.length === 8) {
|
|
9457
|
+
r = parseInt(hex.slice(0, 2), 16);
|
|
9458
|
+
g = parseInt(hex.slice(2, 4), 16);
|
|
9459
|
+
b = parseInt(hex.slice(4, 6), 16);
|
|
9460
|
+
a = parseInt(hex.slice(6, 8), 16) / 255;
|
|
9461
|
+
} else throw new Error("Unsupported hex format");
|
|
9462
|
+
} else if (fillColor.startsWith("rgb")) {
|
|
9463
|
+
const match = fillColor.match(/rgba?\(\s*(\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\s*\)/);
|
|
9464
|
+
if (!match) throw new Error("Invalid rgb()/rgba() format.");
|
|
9465
|
+
r = parseInt(match[1], 10);
|
|
9466
|
+
g = parseInt(match[2], 10);
|
|
9467
|
+
b = parseInt(match[3], 10);
|
|
9468
|
+
if (match[4] !== void 0) a = parseFloat(match[4]);
|
|
9469
|
+
} else throw new Error("Unsupported color format.");
|
|
9470
|
+
const luminance = (.299 * r + .587 * g + .114 * b) / 255;
|
|
9471
|
+
let lighten;
|
|
9472
|
+
if (theme === "dark") lighten = luminance < .7;
|
|
9473
|
+
else lighten = luminance < .3;
|
|
9474
|
+
const adjust = (v) => lighten ? Math.min(255, Math.floor(v + (255 - v) * factor)) : Math.max(0, Math.floor(v * (1 - factor)));
|
|
9475
|
+
return `rgba(${adjust(r)}, ${adjust(g)}, ${adjust(b)}, ${a.toFixed(3)})`;
|
|
9476
|
+
}
|
|
9477
|
+
//#endregion
|
|
9353
9478
|
//#region src/yui_graph_camera.js
|
|
9354
9479
|
/***********************************************************************
|
|
9355
9480
|
* yui_graph_camera.js
|
|
@@ -9498,6 +9623,79 @@ function yui_graph_fold_items(gobj, wide) {
|
|
|
9498
9623
|
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)];
|
|
9499
9624
|
}
|
|
9500
9625
|
/************************************************************
|
|
9626
|
+
* The ANCHOR toggle: pick one element and every camera move
|
|
9627
|
+
* from then on leaves it in the MIDDLE.
|
|
9628
|
+
*
|
|
9629
|
+
* Why it exists: a graph that fits on screen is unreadable at
|
|
9630
|
+
* the zoom that makes it fit, and the zoom that makes it
|
|
9631
|
+
* readable does not fit -- so the useful view is always a
|
|
9632
|
+
* fraction of the document, and which fraction is the whole
|
|
9633
|
+
* question. Without an anchor, `1:1` lands wherever the
|
|
9634
|
+
* layout's origin happens to be and the reader hunts for the
|
|
9635
|
+
* node they were looking at.
|
|
9636
|
+
*
|
|
9637
|
+
* Crosshairs and not a thumbtack. A tack says "do not move
|
|
9638
|
+
* this", which is what a pinned NODE would mean in a graph you
|
|
9639
|
+
* can drag nodes around in; this pins the CAMERA to a target,
|
|
9640
|
+
* which is what a crosshair says and what every map that
|
|
9641
|
+
* offers it draws. It is also in the icon set already, and a
|
|
9642
|
+
* `yi-*` class that is not is a black square.
|
|
9643
|
+
*
|
|
9644
|
+
* Three states, because two would not say what a press does:
|
|
9645
|
+
*
|
|
9646
|
+
* off no target; the camera behaves as it always did
|
|
9647
|
+
* arming waiting for you to click the element you mean
|
|
9648
|
+
* on locked; every zoom re-centres on it
|
|
9649
|
+
************************************************************/
|
|
9650
|
+
function yui_graph_anchor_item(gobj, wide) {
|
|
9651
|
+
return camera_button(gobj, "yi-location-crosshairs", "EV_TOGGLE_ANCHOR", "anchor view", wide);
|
|
9652
|
+
}
|
|
9653
|
+
/************************************************************
|
|
9654
|
+
* Paint the anchor button. `state` is "off" | "arming" | "on".
|
|
9655
|
+
*
|
|
9656
|
+
* Both live states look PRESSED, because both are states the
|
|
9657
|
+
* control is IN rather than actions; what separates them is the
|
|
9658
|
+
* colour, and only the waiting one takes a palette colour --
|
|
9659
|
+
* it is the one that needs the reader to do something next.
|
|
9660
|
+
************************************************************/
|
|
9661
|
+
function yui_graph_update_anchor($container, state) {
|
|
9662
|
+
if (!$container) return;
|
|
9663
|
+
let $btn = $container.querySelector(".EV_TOGGLE_ANCHOR");
|
|
9664
|
+
if (!$btn) return;
|
|
9665
|
+
set_pressed_state($container, ".EV_TOGGLE_ANCHOR", state !== "off");
|
|
9666
|
+
$btn.classList.toggle("color_pending_state", state === "arming");
|
|
9667
|
+
$btn.setAttribute("aria-pressed", state === "on" ? "true" : "false");
|
|
9668
|
+
let key = state === "arming" ? "click the element to centre on" : state === "on" ? "centred: click to release" : "anchor view";
|
|
9669
|
+
$btn.setAttribute("title", (0, i18next.t)(key));
|
|
9670
|
+
$btn.setAttribute("data-i18n-title", key);
|
|
9671
|
+
$btn.setAttribute("aria-label", (0, i18next.t)(key));
|
|
9672
|
+
$btn.setAttribute("data-i18n-aria-label", key);
|
|
9673
|
+
}
|
|
9674
|
+
/************************************************************
|
|
9675
|
+
* Put `node_id` back in the middle of the viewport.
|
|
9676
|
+
*
|
|
9677
|
+
* Called AFTER the camera moved, never instead of moving it:
|
|
9678
|
+
* the zoom is what the reader asked for, the centring is what
|
|
9679
|
+
* keeps the thing they are reading under their eyes.
|
|
9680
|
+
*
|
|
9681
|
+
* Guarded on every side because it runs on every wheel notch:
|
|
9682
|
+
* a graph mid-rebuild, an anchor whose node went away with a
|
|
9683
|
+
* fold, a G6 version without `focusElement` -- none of those is
|
|
9684
|
+
* an error worth a log line per notch, they are all "there is
|
|
9685
|
+
* nothing to centre right now".
|
|
9686
|
+
************************************************************/
|
|
9687
|
+
function yui_graph_center_on(graph, node_id) {
|
|
9688
|
+
if (!graph || !node_id) return false;
|
|
9689
|
+
try {
|
|
9690
|
+
if (typeof graph.getElementData === "function" && !graph.getElementData(node_id)) return false;
|
|
9691
|
+
if (typeof graph.focusElement !== "function") return false;
|
|
9692
|
+
graph.focusElement(node_id);
|
|
9693
|
+
return true;
|
|
9694
|
+
} catch (e) {
|
|
9695
|
+
return false;
|
|
9696
|
+
}
|
|
9697
|
+
}
|
|
9698
|
+
/************************************************************
|
|
9501
9699
|
* A refresh button, for the graphs that offer one.
|
|
9502
9700
|
************************************************************/
|
|
9503
9701
|
function yui_graph_refresh_item(gobj, wide) {
|
|
@@ -9974,6 +10172,268 @@ function ensure_drag_canvas_patch() {
|
|
|
9974
10172
|
(0, _antv_g6.register)(_antv_g6.ExtensionCategory.BEHAVIOR, "drag-canvas", DragCanvasTouch);
|
|
9975
10173
|
__drag_canvas_patched__ = true;
|
|
9976
10174
|
}
|
|
10175
|
+
/************************************************************
|
|
10176
|
+
* JSON type discriminator (viewer vocabulary)
|
|
10177
|
+
************************************************************/
|
|
10178
|
+
function json_type(value) {
|
|
10179
|
+
if (value === null) return "null";
|
|
10180
|
+
if (Array.isArray(value)) return "array";
|
|
10181
|
+
switch (typeof value) {
|
|
10182
|
+
case "string": return "string";
|
|
10183
|
+
case "number": return "number";
|
|
10184
|
+
case "boolean": return "boolean";
|
|
10185
|
+
case "object": return "object";
|
|
10186
|
+
default: return "unknown";
|
|
10187
|
+
}
|
|
10188
|
+
}
|
|
10189
|
+
/************************************************************
|
|
10190
|
+
* Detect a kw_collapse() sentinel.
|
|
10191
|
+
*
|
|
10192
|
+
* The kernel replaces an over-limit dict with:
|
|
10193
|
+
* { "__collapsed__": { "path": ..., "size": N } }
|
|
10194
|
+
* and an over-limit array with:
|
|
10195
|
+
* [ { "__collapsed__": { "path": ..., "size": N } } ]
|
|
10196
|
+
*
|
|
10197
|
+
* Returns { size, path, is_array } or null.
|
|
10198
|
+
************************************************************/
|
|
10199
|
+
function is_collapsed(value) {
|
|
10200
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
10201
|
+
let keys = Object.keys(value);
|
|
10202
|
+
if (keys.length === 1 && keys[0] === "__collapsed__") {
|
|
10203
|
+
let c = value.__collapsed__ || {};
|
|
10204
|
+
return {
|
|
10205
|
+
size: c.size,
|
|
10206
|
+
path: c.path,
|
|
10207
|
+
is_array: false
|
|
10208
|
+
};
|
|
10209
|
+
}
|
|
10210
|
+
return null;
|
|
10211
|
+
}
|
|
10212
|
+
if (Array.isArray(value) && value.length === 1) {
|
|
10213
|
+
let e = value[0];
|
|
10214
|
+
if (e && typeof e === "object" && !Array.isArray(e) && Object.keys(e).length === 1 && e.__collapsed__) {
|
|
10215
|
+
let c = e.__collapsed__ || {};
|
|
10216
|
+
return {
|
|
10217
|
+
size: c.size,
|
|
10218
|
+
path: c.path,
|
|
10219
|
+
is_array: true
|
|
10220
|
+
};
|
|
10221
|
+
}
|
|
10222
|
+
}
|
|
10223
|
+
return null;
|
|
10224
|
+
}
|
|
10225
|
+
/************************************************************
|
|
10226
|
+
* Join / split absolute path segments (kernel convention)
|
|
10227
|
+
************************************************************/
|
|
10228
|
+
function seg_join(segments) {
|
|
10229
|
+
return segments.map(function(s) {
|
|
10230
|
+
return String(s);
|
|
10231
|
+
}).join("`");
|
|
10232
|
+
}
|
|
10233
|
+
function seg_split(path) {
|
|
10234
|
+
if (path === "" || path === null || path === void 0) return [];
|
|
10235
|
+
return path.split("`");
|
|
10236
|
+
}
|
|
10237
|
+
/************************************************************
|
|
10238
|
+
* Walk a JSON tree by absolute segments; undefined if the
|
|
10239
|
+
* path does not resolve.
|
|
10240
|
+
************************************************************/
|
|
10241
|
+
function get_by_segments(root, segments) {
|
|
10242
|
+
let v = root;
|
|
10243
|
+
for (let seg of segments) {
|
|
10244
|
+
if (v === null || v === void 0) return;
|
|
10245
|
+
if (Array.isArray(v)) v = v[Number(seg)];
|
|
10246
|
+
else if (typeof v === "object") v = v[seg];
|
|
10247
|
+
else return;
|
|
10248
|
+
}
|
|
10249
|
+
return v;
|
|
10250
|
+
}
|
|
10251
|
+
/************************************************************
|
|
10252
|
+
* Set a value at absolute segments (mutates root).
|
|
10253
|
+
*
|
|
10254
|
+
* segments == [] means "replace the whole root" — the caller
|
|
10255
|
+
* must use the RETURNED value in that case (a primitive/array
|
|
10256
|
+
* root cannot be mutated in place).
|
|
10257
|
+
************************************************************/
|
|
10258
|
+
function set_by_segments(root, segments, new_value) {
|
|
10259
|
+
if (segments.length === 0) return new_value;
|
|
10260
|
+
let parent = get_by_segments(root, segments.slice(0, -1));
|
|
10261
|
+
if (parent === null || parent === void 0 || typeof parent !== "object") return root;
|
|
10262
|
+
let last = segments[segments.length - 1];
|
|
10263
|
+
if (Array.isArray(parent)) parent[Number(last)] = new_value;
|
|
10264
|
+
else parent[last] = new_value;
|
|
10265
|
+
return root;
|
|
10266
|
+
}
|
|
10267
|
+
/************************************************************
|
|
10268
|
+
* Does `value` (or any loaded descendant / its own key)
|
|
10269
|
+
* contain the lower-cased search term?
|
|
10270
|
+
*
|
|
10271
|
+
* Collapsed (not-yet-loaded) subtrees can't be searched, so
|
|
10272
|
+
* they never match on content — only their key can match,
|
|
10273
|
+
* which the renderer checks separately.
|
|
10274
|
+
************************************************************/
|
|
10275
|
+
function subtree_matches(value, term) {
|
|
10276
|
+
if (!term) return true;
|
|
10277
|
+
if (is_collapsed(value)) return false;
|
|
10278
|
+
let type = json_type(value);
|
|
10279
|
+
if (type === "object") {
|
|
10280
|
+
for (let [k, v] of Object.entries(value)) {
|
|
10281
|
+
if (String(k).toLowerCase().includes(term)) return true;
|
|
10282
|
+
if (subtree_matches(v, term)) return true;
|
|
10283
|
+
}
|
|
10284
|
+
return false;
|
|
10285
|
+
}
|
|
10286
|
+
if (type === "array") {
|
|
10287
|
+
for (let v of value) if (subtree_matches(v, term)) return true;
|
|
10288
|
+
return false;
|
|
10289
|
+
}
|
|
10290
|
+
return String(value).toLowerCase().includes(term);
|
|
10291
|
+
}
|
|
10292
|
+
/************************************************************
|
|
10293
|
+
* Fields whose numeric value is an epoch timestamp.
|
|
10294
|
+
* Mirrors the timestampTag list wired into C_YUI_FORM's
|
|
10295
|
+
* jsoneditor control, so the two viewers agree.
|
|
10296
|
+
************************************************************/
|
|
10297
|
+
function is_time_field(field) {
|
|
10298
|
+
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";
|
|
10299
|
+
}
|
|
10300
|
+
/************************************************************
|
|
10301
|
+
* Format an epoch value as a local wall-clock string.
|
|
10302
|
+
* Accepts seconds or milliseconds; returns null when the
|
|
10303
|
+
* value is not a usable positive timestamp (0 == "unset").
|
|
10304
|
+
************************************************************/
|
|
10305
|
+
function format_epoch(value) {
|
|
10306
|
+
if (typeof value !== "number" || !isFinite(value) || value <= 0) return null;
|
|
10307
|
+
let ms = Math.abs(value) < 0xe8d4a51000 ? value * 1e3 : value;
|
|
10308
|
+
let d = new Date(ms);
|
|
10309
|
+
if (isNaN(d.getTime())) return null;
|
|
10310
|
+
return d.toLocaleString();
|
|
10311
|
+
}
|
|
10312
|
+
/************************************************************
|
|
10313
|
+
* The text view's payload: the document as text, indented
|
|
10314
|
+
* four characters, cut at `max_chars`.
|
|
10315
|
+
*
|
|
10316
|
+
* Returns {text, capped}. `capped` is what the caller
|
|
10317
|
+
* announces under the dump — a cut that is not announced
|
|
10318
|
+
* reads as a document that ends there.
|
|
10319
|
+
************************************************************/
|
|
10320
|
+
function json_text_dump(root, max_chars) {
|
|
10321
|
+
let text;
|
|
10322
|
+
try {
|
|
10323
|
+
text = JSON.stringify(root, null, 4);
|
|
10324
|
+
} catch (e) {
|
|
10325
|
+
return {
|
|
10326
|
+
text: "",
|
|
10327
|
+
capped: false,
|
|
10328
|
+
error: String(e)
|
|
10329
|
+
};
|
|
10330
|
+
}
|
|
10331
|
+
if (text === void 0) return {
|
|
10332
|
+
text: "",
|
|
10333
|
+
capped: false,
|
|
10334
|
+
error: "not representable as JSON"
|
|
10335
|
+
};
|
|
10336
|
+
if (typeof max_chars === "number" && max_chars > 0 && text.length > max_chars) return {
|
|
10337
|
+
text: text.slice(0, max_chars),
|
|
10338
|
+
capped: true
|
|
10339
|
+
};
|
|
10340
|
+
return {
|
|
10341
|
+
text,
|
|
10342
|
+
capped: false
|
|
10343
|
+
};
|
|
10344
|
+
}
|
|
10345
|
+
/************************************************************
|
|
10346
|
+
* Which view a freshly mounted viewer opens on.
|
|
10347
|
+
*
|
|
10348
|
+
* Three answers, in order of authority: what the HOST asked
|
|
10349
|
+
* for, what the READER chose last time, and `fallback`.
|
|
10350
|
+
*
|
|
10351
|
+
* The host wins because a viewer mounted to show a graph has
|
|
10352
|
+
* to show one. The memory comes next, because reopening a
|
|
10353
|
+
* document in the view you just left is the whole point of
|
|
10354
|
+
* remembering it. `fallback` is the floor.
|
|
10355
|
+
*
|
|
10356
|
+
* Anything not in `modes` is treated as unsaid, which covers
|
|
10357
|
+
* an absent value, an empty one, and a stored string from an
|
|
10358
|
+
* older release that named a view no longer offered.
|
|
10359
|
+
************************************************************/
|
|
10360
|
+
function pick_view_mode(asked, remembered, modes, fallback) {
|
|
10361
|
+
let known = (m) => Array.isArray(modes) && modes.indexOf(m) >= 0;
|
|
10362
|
+
if (known(asked)) return asked;
|
|
10363
|
+
if (known(remembered)) return remembered;
|
|
10364
|
+
return fallback;
|
|
10365
|
+
}
|
|
10366
|
+
/************************************************************
|
|
10367
|
+
* The short label a container row carries beside its size.
|
|
10368
|
+
*
|
|
10369
|
+
* An array of dicts that carry an `id` is a list of RECORDS,
|
|
10370
|
+
* and a row that says only `2: {15}` makes you open all
|
|
10371
|
+
* fifteen fields of every one of them to find out WHICH
|
|
10372
|
+
* record it is. The id is the one field that answers that,
|
|
10373
|
+
* so the row says it without being opened.
|
|
10374
|
+
*
|
|
10375
|
+
* Only a SCALAR id, and only when it is really there: an `id`
|
|
10376
|
+
* that is itself a dict or a list is not a name, it is more
|
|
10377
|
+
* document, and printing `[object Object]` beside the size
|
|
10378
|
+
* would be worse than printing nothing.
|
|
10379
|
+
*
|
|
10380
|
+
* Cut at `max_chars`, because this rides on a row whose job
|
|
10381
|
+
* is to stay one line.
|
|
10382
|
+
************************************************************/
|
|
10383
|
+
function container_label(value, max_chars) {
|
|
10384
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return "";
|
|
10385
|
+
if (!Object.prototype.hasOwnProperty.call(value, "id")) return "";
|
|
10386
|
+
let id = value.id;
|
|
10387
|
+
let t = typeof id;
|
|
10388
|
+
if (t !== "string" && t !== "number" && t !== "boolean") return "";
|
|
10389
|
+
let text = String(id).trim();
|
|
10390
|
+
if (text === "") return "";
|
|
10391
|
+
let cap = typeof max_chars === "number" && max_chars > 0 ? max_chars : 0;
|
|
10392
|
+
if (cap && text.length > cap) return text.slice(0, cap) + "…";
|
|
10393
|
+
return text;
|
|
10394
|
+
}
|
|
10395
|
+
/************************************************************
|
|
10396
|
+
* A dict, in the JSON sense: not an array and not null.
|
|
10397
|
+
************************************************************/
|
|
10398
|
+
function is_plain_object(v) {
|
|
10399
|
+
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
10400
|
+
}
|
|
10401
|
+
/************************************************************
|
|
10402
|
+
* Does this container hold ONLY other containers?
|
|
10403
|
+
*
|
|
10404
|
+
* Such a value is not a thing, it is a LIST of things -- and a
|
|
10405
|
+
* list is fully described by the row that names it in its
|
|
10406
|
+
* parent, plus the edges to what it holds. Drawing it as a
|
|
10407
|
+
* card of its own put a node in the middle of the graph with no
|
|
10408
|
+
* data in it, saying nothing the parent's row did not already
|
|
10409
|
+
* say, and pushed everything below it one level down.
|
|
10410
|
+
*
|
|
10411
|
+
* An EMPTY container inside counts as content: `{0}` is
|
|
10412
|
+
* something the card has to show, and there is no child card
|
|
10413
|
+
* to show it instead.
|
|
10414
|
+
************************************************************/
|
|
10415
|
+
function is_pure_collection(value) {
|
|
10416
|
+
let entries = is_plain_object(value) ? Object.entries(value) : Array.isArray(value) ? value.map((v, i) => [i, v]) : [];
|
|
10417
|
+
if (entries.length === 0) return false;
|
|
10418
|
+
for (let [, v] of entries) if (!has_branch(v)) return false;
|
|
10419
|
+
return true;
|
|
10420
|
+
}
|
|
10421
|
+
/************************************************************
|
|
10422
|
+
* A value that gets a card, or children, of its own.
|
|
10423
|
+
************************************************************/
|
|
10424
|
+
function has_branch(value) {
|
|
10425
|
+
return is_plain_object(value) && Object.keys(value).length > 0 || Array.isArray(value) && value.length > 0;
|
|
10426
|
+
}
|
|
10427
|
+
/************************************************************
|
|
10428
|
+
* How many branches a container has, which is what the fold
|
|
10429
|
+
* chip counts.
|
|
10430
|
+
************************************************************/
|
|
10431
|
+
function count_branches(value) {
|
|
10432
|
+
let entries = is_plain_object(value) ? Object.values(value) : Array.isArray(value) ? value : [];
|
|
10433
|
+
let n = 0;
|
|
10434
|
+
for (let v of entries) if (has_branch(v)) n++;
|
|
10435
|
+
return n;
|
|
10436
|
+
}
|
|
9977
10437
|
//#endregion
|
|
9978
10438
|
//#region src/c_yui_json_graph.js
|
|
9979
10439
|
/***********************************************************************
|
|
@@ -10075,7 +10535,11 @@ var PRIVATE_DATA$11 = {
|
|
|
10075
10535
|
$layout_select: null,
|
|
10076
10536
|
$find_input: null,
|
|
10077
10537
|
$find_result: null,
|
|
10078
|
-
$find_count: null
|
|
10538
|
+
$find_count: null,
|
|
10539
|
+
anchor_id: "",
|
|
10540
|
+
anchor_path: "",
|
|
10541
|
+
anchor_state: "off",
|
|
10542
|
+
last_zoom: 1
|
|
10079
10543
|
};
|
|
10080
10544
|
var __gclass__$11 = null;
|
|
10081
10545
|
var __layout_registered__$1 = false;
|
|
@@ -10368,6 +10832,7 @@ function make_toolbar$3(gobj) {
|
|
|
10368
10832
|
]]);
|
|
10369
10833
|
let center_items = [];
|
|
10370
10834
|
center_items = yui_graph_camera_items(gobj, priv.graph, toolbar_wide);
|
|
10835
|
+
center_items.push(yui_graph_anchor_item(gobj, toolbar_wide));
|
|
10371
10836
|
center_items.push(yui_graph_refresh_item(gobj, toolbar_wide));
|
|
10372
10837
|
let current_layout = (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "layout") || DEFAULT_LAYOUT;
|
|
10373
10838
|
let options = Object.keys(LAYOUTS$1).map(function(key) {
|
|
@@ -10450,6 +10915,15 @@ function build_graph$3(gobj) {
|
|
|
10450
10915
|
});
|
|
10451
10916
|
graph.on("aftertransform", () => {
|
|
10452
10917
|
yui_graph_update_zoom((0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container"), priv.graph);
|
|
10918
|
+
let z = priv.last_zoom;
|
|
10919
|
+
try {
|
|
10920
|
+
z = priv.graph.getZoom();
|
|
10921
|
+
} catch (e) {
|
|
10922
|
+
return;
|
|
10923
|
+
}
|
|
10924
|
+
if (z === priv.last_zoom) return;
|
|
10925
|
+
priv.last_zoom = z;
|
|
10926
|
+
if (priv.anchor_state === "on" && priv.anchor_id) yui_graph_center_on(priv.graph, priv.anchor_id);
|
|
10453
10927
|
});
|
|
10454
10928
|
graph.on(_antv_g6.CanvasEvent.CLICK, (evt) => {});
|
|
10455
10929
|
if (typeof ResizeObserver !== "undefined") {
|
|
@@ -10470,7 +10944,7 @@ function build_graph$3(gobj) {
|
|
|
10470
10944
|
/************************************************************
|
|
10471
10945
|
* Build cell value HTML (matching old mx_json_viewer colors)
|
|
10472
10946
|
************************************************************/
|
|
10473
|
-
function build_cell_html(key, value, type, dark, matched) {
|
|
10947
|
+
function build_cell_html(key, value, type, dark, matched, fold) {
|
|
10474
10948
|
let on_light = matched ? false : dark;
|
|
10475
10949
|
let color = type_color(type, on_light);
|
|
10476
10950
|
let key_color = json_card_style(GROUP_COLORS.dict, on_light).key;
|
|
@@ -10497,6 +10971,7 @@ function build_cell_html(key, value, type, dark, matched) {
|
|
|
10497
10971
|
default: display_value = String(value);
|
|
10498
10972
|
}
|
|
10499
10973
|
let row = `<span style="color:${key_color}">• ${(0, _yuneta_gobj_js.escapeHtml)(String(key))}: </span><span style="color:${color}">${display_value}</span>`;
|
|
10974
|
+
if (fold) row += `<span style="margin-left:8px;">` + fold_toggle_html(fold.path, true, fold.folded, fold.branches, fold.colors) + `</span>`;
|
|
10500
10975
|
if (matched) row = `<span style="background:#ffe082; border-radius:2px; padding:0 2px;">${row}</span>`;
|
|
10501
10976
|
return row;
|
|
10502
10977
|
}
|
|
@@ -10620,6 +11095,7 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id) {
|
|
|
10620
11095
|
let lines = [];
|
|
10621
11096
|
let pending_complex = [];
|
|
10622
11097
|
let card_matched = false;
|
|
11098
|
+
let colors = json_card_style(is_dict ? GROUP_COLORS.dict : GROUP_COLORS.list, dark);
|
|
10623
11099
|
let entries = is_dict ? Object.entries(kw) : kw.map((v, i) => [i, v]);
|
|
10624
11100
|
for (let [key, value] of entries) {
|
|
10625
11101
|
let type = get_json_type(value);
|
|
@@ -10628,17 +11104,32 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id) {
|
|
|
10628
11104
|
card_matched = true;
|
|
10629
11105
|
priv.match_count++;
|
|
10630
11106
|
}
|
|
10631
|
-
let
|
|
10632
|
-
|
|
10633
|
-
|
|
10634
|
-
|
|
10635
|
-
|
|
10636
|
-
|
|
11107
|
+
let fold = null;
|
|
11108
|
+
if (has_branch(value)) {
|
|
11109
|
+
pending_complex.push({
|
|
11110
|
+
key,
|
|
11111
|
+
value
|
|
11112
|
+
});
|
|
11113
|
+
if (is_pure_collection(value)) {
|
|
11114
|
+
let child_path = add_segment(path, key);
|
|
11115
|
+
fold = {
|
|
11116
|
+
path: child_path,
|
|
11117
|
+
folded: priv.collapsed.has(child_path),
|
|
11118
|
+
branches: count_branches(value),
|
|
11119
|
+
colors
|
|
11120
|
+
};
|
|
11121
|
+
}
|
|
11122
|
+
}
|
|
11123
|
+
lines.push(build_cell_html(key, value, type, dark, matched, fold));
|
|
11124
|
+
}
|
|
11125
|
+
if (parent_id && is_pure_collection(kw)) {
|
|
11126
|
+
if (priv.collapsed.has(path)) return parent_id;
|
|
11127
|
+
for (let pending of pending_complex) build_json_nodes(gobj, add_segment(path, pending.key), pending.value, nodes, edges, parent_id);
|
|
11128
|
+
return parent_id;
|
|
10637
11129
|
}
|
|
10638
11130
|
let label = get_group_label(path) || (is_dict ? "{}" : "[]");
|
|
10639
11131
|
let foldable = pending_complex.length > 0;
|
|
10640
11132
|
let folded = foldable && priv.collapsed.has(path);
|
|
10641
|
-
let colors = json_card_style(is_dict ? GROUP_COLORS.dict : GROUP_COLORS.list, dark);
|
|
10642
11133
|
let border_color = card_matched ? dark ? "#ffb300" : "#ff8f00" : colors.border;
|
|
10643
11134
|
let border_width = card_matched ? 2 : 1;
|
|
10644
11135
|
let content_html = lines.map((line) => `<div style="font-family:monospace; font-size:13px; padding:1px 6px; white-space:nowrap;">${line}</div>`).join("");
|
|
@@ -10711,34 +11202,46 @@ function load_json(gobj) {
|
|
|
10711
11202
|
let preserve_view = !!priv.pending_preserve_view;
|
|
10712
11203
|
priv.pending_preserve_view = false;
|
|
10713
11204
|
if (nodes.length > 0) {
|
|
11205
|
+
let root_id = nodes[0].id;
|
|
10714
11206
|
graph.setData({
|
|
10715
11207
|
nodes,
|
|
10716
11208
|
edges
|
|
10717
11209
|
});
|
|
10718
11210
|
graph.render().then(() => {
|
|
10719
|
-
|
|
10720
|
-
|
|
11211
|
+
reanchor$1(gobj);
|
|
11212
|
+
if (preserve_view) {
|
|
11213
|
+
yui_graph_update_zoom((0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container"), priv.graph);
|
|
11214
|
+
return;
|
|
11215
|
+
}
|
|
11216
|
+
Promise.resolve(graph.zoomTo(1)).then(() => {
|
|
11217
|
+
if (!yui_graph_center_on(graph, priv.anchor_id)) yui_graph_center_on(graph, root_id);
|
|
11218
|
+
priv.last_zoom = 1;
|
|
11219
|
+
yui_graph_update_zoom((0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container"), priv.graph);
|
|
11220
|
+
});
|
|
10721
11221
|
});
|
|
10722
11222
|
}
|
|
10723
11223
|
}
|
|
10724
11224
|
/************************************************************
|
|
10725
11225
|
* Every path that HAS a branch to fold, in the same recursion
|
|
10726
|
-
* build_json_nodes uses — so "collapse all" can never name
|
|
10727
|
-
*
|
|
11226
|
+
* build_json_nodes uses — so "collapse all" can never name
|
|
11227
|
+
* something with no handle. The handle is a card's header chip
|
|
11228
|
+
* for a container that gets a card, and the chip on its ROW in
|
|
11229
|
+
* the parent for a pure collection, which gets none; both are
|
|
11230
|
+
* keyed by this same path. The root is excluded: folding it
|
|
10728
11231
|
* would leave an empty canvas.
|
|
10729
11232
|
************************************************************/
|
|
10730
11233
|
function collect_foldable_paths(value, path, out, is_root) {
|
|
10731
11234
|
let is_dict = (0, _yuneta_gobj_js.is_object)(value);
|
|
10732
11235
|
let is_list = (0, _yuneta_gobj_js.is_array)(value);
|
|
10733
11236
|
if (!is_dict && !is_list) return false;
|
|
10734
|
-
let
|
|
11237
|
+
let found_branch = false;
|
|
10735
11238
|
let entries = is_dict ? Object.entries(value) : value.map((v, i) => [i, v]);
|
|
10736
|
-
for (let [key, child] of entries) if ((
|
|
10737
|
-
|
|
11239
|
+
for (let [key, child] of entries) if (has_branch(child)) {
|
|
11240
|
+
found_branch = true;
|
|
10738
11241
|
collect_foldable_paths(child, add_segment(path, key), out, false);
|
|
10739
11242
|
}
|
|
10740
|
-
if (
|
|
10741
|
-
return
|
|
11243
|
+
if (found_branch && !is_root) out.push(path);
|
|
11244
|
+
return found_branch;
|
|
10742
11245
|
}
|
|
10743
11246
|
/************************************************************
|
|
10744
11247
|
* Show the find count, or hide the chip when there is no
|
|
@@ -10980,13 +11483,79 @@ function ac_theme$3(gobj, event, kw, src) {
|
|
|
10980
11483
|
return 0;
|
|
10981
11484
|
}
|
|
10982
11485
|
/************************************************************
|
|
11486
|
+
* Every camera action ends the same way once an anchor is
|
|
11487
|
+
* set: the thing you were reading goes back to the middle.
|
|
11488
|
+
*
|
|
11489
|
+
* Chained on the promise G6 returns rather than run beside it
|
|
11490
|
+
* -- centring before the zoom has finished centres the OLD
|
|
11491
|
+
* view, and the reader watches the node slide away and come
|
|
11492
|
+
* back.
|
|
11493
|
+
************************************************************/
|
|
11494
|
+
function after_camera$2(gobj, moved) {
|
|
11495
|
+
let priv = gobj.priv;
|
|
11496
|
+
if (priv.anchor_state !== "on" || !priv.anchor_id) return;
|
|
11497
|
+
Promise.resolve(moved).then(function() {
|
|
11498
|
+
yui_graph_center_on(priv.graph, priv.anchor_id);
|
|
11499
|
+
}).catch(function() {});
|
|
11500
|
+
}
|
|
11501
|
+
/************************************************************
|
|
11502
|
+
* Point the anchor back at its node after a rebuild.
|
|
11503
|
+
*
|
|
11504
|
+
* Node ids are generated per build, so the id the reader
|
|
11505
|
+
* chose does not survive a refresh, a fold or a layout
|
|
11506
|
+
* change -- the PATH does, and it is what the anchor really
|
|
11507
|
+
* means: "this place in the document", not "this card".
|
|
11508
|
+
*
|
|
11509
|
+
* A path that is no longer drawn (its parent got folded)
|
|
11510
|
+
* releases the anchor rather than leaving a button that says
|
|
11511
|
+
* it is locked onto something invisible.
|
|
11512
|
+
************************************************************/
|
|
11513
|
+
function reanchor$1(gobj) {
|
|
11514
|
+
let priv = gobj.priv;
|
|
11515
|
+
if (priv.anchor_state !== "on" || !priv.anchor_path) return;
|
|
11516
|
+
let found = "";
|
|
11517
|
+
try {
|
|
11518
|
+
for (let node of priv.graph.getNodeData()) if (node && node.data && node.data.path === priv.anchor_path) {
|
|
11519
|
+
found = node.id;
|
|
11520
|
+
break;
|
|
11521
|
+
}
|
|
11522
|
+
} catch (e) {
|
|
11523
|
+
return;
|
|
11524
|
+
}
|
|
11525
|
+
priv.anchor_id = found;
|
|
11526
|
+
if (!found) {
|
|
11527
|
+
priv.anchor_state = "off";
|
|
11528
|
+
priv.anchor_path = "";
|
|
11529
|
+
}
|
|
11530
|
+
yui_graph_update_anchor((0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container"), priv.anchor_state);
|
|
11531
|
+
}
|
|
11532
|
+
/************************************************************
|
|
11533
|
+
* Arm the anchor, or let it go.
|
|
11534
|
+
*
|
|
11535
|
+
* Three states and one button, so a press ADVANCES: with no
|
|
11536
|
+
* target it starts waiting for one, and with a target it
|
|
11537
|
+
* releases. Pressing it while it waits is a cancel, which is
|
|
11538
|
+
* what somebody who pressed it by mistake will try.
|
|
11539
|
+
************************************************************/
|
|
11540
|
+
function ac_toggle_anchor$2(gobj, event, kw, src) {
|
|
11541
|
+
let priv = gobj.priv;
|
|
11542
|
+
if (priv.anchor_state === "off") priv.anchor_state = "arming";
|
|
11543
|
+
else {
|
|
11544
|
+
priv.anchor_state = "off";
|
|
11545
|
+
priv.anchor_id = "";
|
|
11546
|
+
priv.anchor_path = "";
|
|
11547
|
+
}
|
|
11548
|
+
yui_graph_update_anchor((0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container"), priv.anchor_state);
|
|
11549
|
+
return 0;
|
|
11550
|
+
}
|
|
11551
|
+
/************************************************************
|
|
10983
11552
|
*
|
|
10984
11553
|
************************************************************/
|
|
10985
11554
|
function ac_zoom_in$2(gobj, event, kw, src) {
|
|
10986
11555
|
let graph = gobj.priv.graph;
|
|
10987
11556
|
if (graph) {
|
|
10988
11557
|
let z = graph.getZoom();
|
|
10989
|
-
graph.zoomTo(z * 1.2);
|
|
11558
|
+
after_camera$2(gobj, graph.zoomTo(z * 1.2));
|
|
10990
11559
|
}
|
|
10991
11560
|
return 0;
|
|
10992
11561
|
}
|
|
@@ -10997,7 +11566,7 @@ function ac_zoom_out$2(gobj, event, kw, src) {
|
|
|
10997
11566
|
let graph = gobj.priv.graph;
|
|
10998
11567
|
if (graph) {
|
|
10999
11568
|
let z = graph.getZoom();
|
|
11000
|
-
graph.zoomTo(z * .8);
|
|
11569
|
+
after_camera$2(gobj, graph.zoomTo(z * .8));
|
|
11001
11570
|
}
|
|
11002
11571
|
return 0;
|
|
11003
11572
|
}
|
|
@@ -11005,10 +11574,14 @@ function ac_zoom_out$2(gobj, event, kw, src) {
|
|
|
11005
11574
|
*
|
|
11006
11575
|
************************************************************/
|
|
11007
11576
|
function ac_zoom_reset$2(gobj, event, kw, src) {
|
|
11008
|
-
let
|
|
11577
|
+
let priv = gobj.priv;
|
|
11578
|
+
let graph = priv.graph;
|
|
11009
11579
|
if (graph && graph.rendered) {
|
|
11010
|
-
graph.zoomTo(1);
|
|
11011
|
-
|
|
11580
|
+
if (priv.anchor_state === "on" && priv.anchor_id) after_camera$2(gobj, graph.zoomTo(1));
|
|
11581
|
+
else {
|
|
11582
|
+
graph.zoomTo(1);
|
|
11583
|
+
graph.translateTo([0, 0]);
|
|
11584
|
+
}
|
|
11012
11585
|
}
|
|
11013
11586
|
return 0;
|
|
11014
11587
|
}
|
|
@@ -11027,12 +11600,21 @@ function ac_center$2(gobj, event, kw, src) {
|
|
|
11027
11600
|
*
|
|
11028
11601
|
************************************************************/
|
|
11029
11602
|
function ac_node_click$4(gobj, event, kw, src) {
|
|
11030
|
-
let
|
|
11603
|
+
let priv = gobj.priv;
|
|
11604
|
+
let graph = priv.graph;
|
|
11031
11605
|
let node_id = kw.evt.target.id;
|
|
11032
11606
|
let nodedata;
|
|
11033
11607
|
try {
|
|
11034
11608
|
nodedata = graph.getNodeData(node_id);
|
|
11035
11609
|
} catch (e) {}
|
|
11610
|
+
if (priv.anchor_state === "arming") {
|
|
11611
|
+
priv.anchor_id = node_id;
|
|
11612
|
+
priv.anchor_path = nodedata && nodedata.data ? nodedata.data.path : "";
|
|
11613
|
+
priv.anchor_state = "on";
|
|
11614
|
+
yui_graph_update_anchor((0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container"), priv.anchor_state);
|
|
11615
|
+
yui_graph_center_on(graph, node_id);
|
|
11616
|
+
return 0;
|
|
11617
|
+
}
|
|
11036
11618
|
if (nodedata && nodedata.data) (0, _yuneta_gobj_js.gobj_publish_event)(gobj, "EV_JSON_ITEM_CLICKED", {
|
|
11037
11619
|
path: nodedata.data.path,
|
|
11038
11620
|
id: node_id
|
|
@@ -11166,6 +11748,11 @@ function create_gclass$11(gclass_name) {
|
|
|
11166
11748
|
ac_node_click$4,
|
|
11167
11749
|
null
|
|
11168
11750
|
],
|
|
11751
|
+
[
|
|
11752
|
+
"EV_TOGGLE_ANCHOR",
|
|
11753
|
+
ac_toggle_anchor$2,
|
|
11754
|
+
null
|
|
11755
|
+
],
|
|
11169
11756
|
[
|
|
11170
11757
|
"EV_RESIZE",
|
|
11171
11758
|
ac_resize$2,
|
|
@@ -11196,6 +11783,7 @@ function create_gclass$11(gclass_name) {
|
|
|
11196
11783
|
["EV_ZOOM_RESET", 0],
|
|
11197
11784
|
["EV_CENTER", 0],
|
|
11198
11785
|
["EV_NODE_CLICK", 0],
|
|
11786
|
+
["EV_TOGGLE_ANCHOR", 0],
|
|
11199
11787
|
["EV_RESIZE", 0],
|
|
11200
11788
|
["EV_SHOW", 0],
|
|
11201
11789
|
["EV_HIDE", 0],
|
|
@@ -11212,226 +11800,6 @@ function register_c_yui_json_graph() {
|
|
|
11212
11800
|
if ((0, _yuneta_gobj_js.gclass_find_by_name)(GCLASS_NAME$11, false)) return 0;
|
|
11213
11801
|
return create_gclass$11(GCLASS_NAME$11);
|
|
11214
11802
|
}
|
|
11215
|
-
/************************************************************
|
|
11216
|
-
* JSON type discriminator (viewer vocabulary)
|
|
11217
|
-
************************************************************/
|
|
11218
|
-
function json_type(value) {
|
|
11219
|
-
if (value === null) return "null";
|
|
11220
|
-
if (Array.isArray(value)) return "array";
|
|
11221
|
-
switch (typeof value) {
|
|
11222
|
-
case "string": return "string";
|
|
11223
|
-
case "number": return "number";
|
|
11224
|
-
case "boolean": return "boolean";
|
|
11225
|
-
case "object": return "object";
|
|
11226
|
-
default: return "unknown";
|
|
11227
|
-
}
|
|
11228
|
-
}
|
|
11229
|
-
/************************************************************
|
|
11230
|
-
* Detect a kw_collapse() sentinel.
|
|
11231
|
-
*
|
|
11232
|
-
* The kernel replaces an over-limit dict with:
|
|
11233
|
-
* { "__collapsed__": { "path": ..., "size": N } }
|
|
11234
|
-
* and an over-limit array with:
|
|
11235
|
-
* [ { "__collapsed__": { "path": ..., "size": N } } ]
|
|
11236
|
-
*
|
|
11237
|
-
* Returns { size, path, is_array } or null.
|
|
11238
|
-
************************************************************/
|
|
11239
|
-
function is_collapsed(value) {
|
|
11240
|
-
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
11241
|
-
let keys = Object.keys(value);
|
|
11242
|
-
if (keys.length === 1 && keys[0] === "__collapsed__") {
|
|
11243
|
-
let c = value.__collapsed__ || {};
|
|
11244
|
-
return {
|
|
11245
|
-
size: c.size,
|
|
11246
|
-
path: c.path,
|
|
11247
|
-
is_array: false
|
|
11248
|
-
};
|
|
11249
|
-
}
|
|
11250
|
-
return null;
|
|
11251
|
-
}
|
|
11252
|
-
if (Array.isArray(value) && value.length === 1) {
|
|
11253
|
-
let e = value[0];
|
|
11254
|
-
if (e && typeof e === "object" && !Array.isArray(e) && Object.keys(e).length === 1 && e.__collapsed__) {
|
|
11255
|
-
let c = e.__collapsed__ || {};
|
|
11256
|
-
return {
|
|
11257
|
-
size: c.size,
|
|
11258
|
-
path: c.path,
|
|
11259
|
-
is_array: true
|
|
11260
|
-
};
|
|
11261
|
-
}
|
|
11262
|
-
}
|
|
11263
|
-
return null;
|
|
11264
|
-
}
|
|
11265
|
-
/************************************************************
|
|
11266
|
-
* Join / split absolute path segments (kernel convention)
|
|
11267
|
-
************************************************************/
|
|
11268
|
-
function seg_join(segments) {
|
|
11269
|
-
return segments.map(function(s) {
|
|
11270
|
-
return String(s);
|
|
11271
|
-
}).join("`");
|
|
11272
|
-
}
|
|
11273
|
-
function seg_split(path) {
|
|
11274
|
-
if (path === "" || path === null || path === void 0) return [];
|
|
11275
|
-
return path.split("`");
|
|
11276
|
-
}
|
|
11277
|
-
/************************************************************
|
|
11278
|
-
* Walk a JSON tree by absolute segments; undefined if the
|
|
11279
|
-
* path does not resolve.
|
|
11280
|
-
************************************************************/
|
|
11281
|
-
function get_by_segments(root, segments) {
|
|
11282
|
-
let v = root;
|
|
11283
|
-
for (let seg of segments) {
|
|
11284
|
-
if (v === null || v === void 0) return;
|
|
11285
|
-
if (Array.isArray(v)) v = v[Number(seg)];
|
|
11286
|
-
else if (typeof v === "object") v = v[seg];
|
|
11287
|
-
else return;
|
|
11288
|
-
}
|
|
11289
|
-
return v;
|
|
11290
|
-
}
|
|
11291
|
-
/************************************************************
|
|
11292
|
-
* Set a value at absolute segments (mutates root).
|
|
11293
|
-
*
|
|
11294
|
-
* segments == [] means "replace the whole root" — the caller
|
|
11295
|
-
* must use the RETURNED value in that case (a primitive/array
|
|
11296
|
-
* root cannot be mutated in place).
|
|
11297
|
-
************************************************************/
|
|
11298
|
-
function set_by_segments(root, segments, new_value) {
|
|
11299
|
-
if (segments.length === 0) return new_value;
|
|
11300
|
-
let parent = get_by_segments(root, segments.slice(0, -1));
|
|
11301
|
-
if (parent === null || parent === void 0 || typeof parent !== "object") return root;
|
|
11302
|
-
let last = segments[segments.length - 1];
|
|
11303
|
-
if (Array.isArray(parent)) parent[Number(last)] = new_value;
|
|
11304
|
-
else parent[last] = new_value;
|
|
11305
|
-
return root;
|
|
11306
|
-
}
|
|
11307
|
-
/************************************************************
|
|
11308
|
-
* Does `value` (or any loaded descendant / its own key)
|
|
11309
|
-
* contain the lower-cased search term?
|
|
11310
|
-
*
|
|
11311
|
-
* Collapsed (not-yet-loaded) subtrees can't be searched, so
|
|
11312
|
-
* they never match on content — only their key can match,
|
|
11313
|
-
* which the renderer checks separately.
|
|
11314
|
-
************************************************************/
|
|
11315
|
-
function subtree_matches(value, term) {
|
|
11316
|
-
if (!term) return true;
|
|
11317
|
-
if (is_collapsed(value)) return false;
|
|
11318
|
-
let type = json_type(value);
|
|
11319
|
-
if (type === "object") {
|
|
11320
|
-
for (let [k, v] of Object.entries(value)) {
|
|
11321
|
-
if (String(k).toLowerCase().includes(term)) return true;
|
|
11322
|
-
if (subtree_matches(v, term)) return true;
|
|
11323
|
-
}
|
|
11324
|
-
return false;
|
|
11325
|
-
}
|
|
11326
|
-
if (type === "array") {
|
|
11327
|
-
for (let v of value) if (subtree_matches(v, term)) return true;
|
|
11328
|
-
return false;
|
|
11329
|
-
}
|
|
11330
|
-
return String(value).toLowerCase().includes(term);
|
|
11331
|
-
}
|
|
11332
|
-
/************************************************************
|
|
11333
|
-
* Fields whose numeric value is an epoch timestamp.
|
|
11334
|
-
* Mirrors the timestampTag list wired into C_YUI_FORM's
|
|
11335
|
-
* jsoneditor control, so the two viewers agree.
|
|
11336
|
-
************************************************************/
|
|
11337
|
-
function is_time_field(field) {
|
|
11338
|
-
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";
|
|
11339
|
-
}
|
|
11340
|
-
/************************************************************
|
|
11341
|
-
* Format an epoch value as a local wall-clock string.
|
|
11342
|
-
* Accepts seconds or milliseconds; returns null when the
|
|
11343
|
-
* value is not a usable positive timestamp (0 == "unset").
|
|
11344
|
-
************************************************************/
|
|
11345
|
-
function format_epoch(value) {
|
|
11346
|
-
if (typeof value !== "number" || !isFinite(value) || value <= 0) return null;
|
|
11347
|
-
let ms = Math.abs(value) < 0xe8d4a51000 ? value * 1e3 : value;
|
|
11348
|
-
let d = new Date(ms);
|
|
11349
|
-
if (isNaN(d.getTime())) return null;
|
|
11350
|
-
return d.toLocaleString();
|
|
11351
|
-
}
|
|
11352
|
-
/************************************************************
|
|
11353
|
-
* The text view's payload: the document as text, indented
|
|
11354
|
-
* four characters, cut at `max_chars`.
|
|
11355
|
-
*
|
|
11356
|
-
* Returns {text, capped}. `capped` is what the caller
|
|
11357
|
-
* announces under the dump — a cut that is not announced
|
|
11358
|
-
* reads as a document that ends there.
|
|
11359
|
-
************************************************************/
|
|
11360
|
-
function json_text_dump(root, max_chars) {
|
|
11361
|
-
let text;
|
|
11362
|
-
try {
|
|
11363
|
-
text = JSON.stringify(root, null, 4);
|
|
11364
|
-
} catch (e) {
|
|
11365
|
-
return {
|
|
11366
|
-
text: "",
|
|
11367
|
-
capped: false,
|
|
11368
|
-
error: String(e)
|
|
11369
|
-
};
|
|
11370
|
-
}
|
|
11371
|
-
if (text === void 0) return {
|
|
11372
|
-
text: "",
|
|
11373
|
-
capped: false,
|
|
11374
|
-
error: "not representable as JSON"
|
|
11375
|
-
};
|
|
11376
|
-
if (typeof max_chars === "number" && max_chars > 0 && text.length > max_chars) return {
|
|
11377
|
-
text: text.slice(0, max_chars),
|
|
11378
|
-
capped: true
|
|
11379
|
-
};
|
|
11380
|
-
return {
|
|
11381
|
-
text,
|
|
11382
|
-
capped: false
|
|
11383
|
-
};
|
|
11384
|
-
}
|
|
11385
|
-
/************************************************************
|
|
11386
|
-
* Which view a freshly mounted viewer opens on.
|
|
11387
|
-
*
|
|
11388
|
-
* Three answers, in order of authority: what the HOST asked
|
|
11389
|
-
* for, what the READER chose last time, and `fallback`.
|
|
11390
|
-
*
|
|
11391
|
-
* The host wins because a viewer mounted to show a graph has
|
|
11392
|
-
* to show one. The memory comes next, because reopening a
|
|
11393
|
-
* document in the view you just left is the whole point of
|
|
11394
|
-
* remembering it. `fallback` is the floor.
|
|
11395
|
-
*
|
|
11396
|
-
* Anything not in `modes` is treated as unsaid, which covers
|
|
11397
|
-
* an absent value, an empty one, and a stored string from an
|
|
11398
|
-
* older release that named a view no longer offered.
|
|
11399
|
-
************************************************************/
|
|
11400
|
-
function pick_view_mode(asked, remembered, modes, fallback) {
|
|
11401
|
-
let known = (m) => Array.isArray(modes) && modes.indexOf(m) >= 0;
|
|
11402
|
-
if (known(asked)) return asked;
|
|
11403
|
-
if (known(remembered)) return remembered;
|
|
11404
|
-
return fallback;
|
|
11405
|
-
}
|
|
11406
|
-
/************************************************************
|
|
11407
|
-
* The short label a container row carries beside its size.
|
|
11408
|
-
*
|
|
11409
|
-
* An array of dicts that carry an `id` is a list of RECORDS,
|
|
11410
|
-
* and a row that says only `2: {15}` makes you open all
|
|
11411
|
-
* fifteen fields of every one of them to find out WHICH
|
|
11412
|
-
* record it is. The id is the one field that answers that,
|
|
11413
|
-
* so the row says it without being opened.
|
|
11414
|
-
*
|
|
11415
|
-
* Only a SCALAR id, and only when it is really there: an `id`
|
|
11416
|
-
* that is itself a dict or a list is not a name, it is more
|
|
11417
|
-
* document, and printing `[object Object]` beside the size
|
|
11418
|
-
* would be worse than printing nothing.
|
|
11419
|
-
*
|
|
11420
|
-
* Cut at `max_chars`, because this rides on a row whose job
|
|
11421
|
-
* is to stay one line.
|
|
11422
|
-
************************************************************/
|
|
11423
|
-
function container_label(value, max_chars) {
|
|
11424
|
-
if (value === null || typeof value !== "object" || Array.isArray(value)) return "";
|
|
11425
|
-
if (!Object.prototype.hasOwnProperty.call(value, "id")) return "";
|
|
11426
|
-
let id = value.id;
|
|
11427
|
-
let t = typeof id;
|
|
11428
|
-
if (t !== "string" && t !== "number" && t !== "boolean") return "";
|
|
11429
|
-
let text = String(id).trim();
|
|
11430
|
-
if (text === "") return "";
|
|
11431
|
-
let cap = typeof max_chars === "number" && max_chars > 0 ? max_chars : 0;
|
|
11432
|
-
if (cap && text.length > cap) return text.slice(0, cap) + "…";
|
|
11433
|
-
return text;
|
|
11434
|
-
}
|
|
11435
11803
|
//#endregion
|
|
11436
11804
|
//#region src/c_yui_json.js
|
|
11437
11805
|
/***********************************************************************
|
|
@@ -12717,7 +13085,11 @@ var PRIVATE_DATA$9 = {
|
|
|
12717
13085
|
node_by_id: null,
|
|
12718
13086
|
collapse_state: null,
|
|
12719
13087
|
resize_observer: null,
|
|
12720
|
-
_resize_raf: 0
|
|
13088
|
+
_resize_raf: 0,
|
|
13089
|
+
anchor_id: "",
|
|
13090
|
+
anchor_name: "",
|
|
13091
|
+
anchor_state: "off",
|
|
13092
|
+
last_zoom: 1
|
|
12721
13093
|
};
|
|
12722
13094
|
var __gclass__$9 = null;
|
|
12723
13095
|
var __layout_registered__ = false;
|
|
@@ -13110,6 +13482,7 @@ function make_toolbar$1(gobj) {
|
|
|
13110
13482
|
let center_items = [];
|
|
13111
13483
|
let right_items = [];
|
|
13112
13484
|
center_items = yui_graph_camera_items(gobj, priv.graph, toolbar_wide);
|
|
13485
|
+
center_items.push(yui_graph_anchor_item(gobj, toolbar_wide));
|
|
13113
13486
|
center_items.push(yui_graph_refresh_item(gobj, toolbar_wide));
|
|
13114
13487
|
let options = [];
|
|
13115
13488
|
for (let key of Object.keys(LAYOUTS)) {
|
|
@@ -13185,6 +13558,15 @@ function build_graph$2(gobj) {
|
|
|
13185
13558
|
if (priv.theme) graph.setTheme(priv.theme);
|
|
13186
13559
|
graph.on("aftertransform", () => {
|
|
13187
13560
|
yui_graph_update_zoom((0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container"), priv.graph);
|
|
13561
|
+
let z = priv.last_zoom;
|
|
13562
|
+
try {
|
|
13563
|
+
z = priv.graph.getZoom();
|
|
13564
|
+
} catch (e) {
|
|
13565
|
+
return;
|
|
13566
|
+
}
|
|
13567
|
+
if (z === priv.last_zoom) return;
|
|
13568
|
+
priv.last_zoom = z;
|
|
13569
|
+
if (priv.anchor_state === "on" && priv.anchor_id) yui_graph_center_on(priv.graph, priv.anchor_id);
|
|
13188
13570
|
});
|
|
13189
13571
|
graph.on(_antv_g6.NodeEvent.CLICK, (evt) => {
|
|
13190
13572
|
(0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_NODE_CLICK", { evt }, gobj);
|
|
@@ -13472,13 +13854,23 @@ function load_tree(gobj) {
|
|
|
13472
13854
|
let edges = [];
|
|
13473
13855
|
build_gobj_nodes(gobj, yuno, nodes, edges, null, true, layout_cfg.compact);
|
|
13474
13856
|
if (nodes.length > 0) {
|
|
13857
|
+
let root_id = nodes[0].id;
|
|
13475
13858
|
graph.setData({
|
|
13476
13859
|
nodes,
|
|
13477
13860
|
edges
|
|
13478
13861
|
});
|
|
13479
13862
|
graph.render().then(() => {
|
|
13480
|
-
|
|
13481
|
-
|
|
13863
|
+
reanchor(gobj);
|
|
13864
|
+
if (anchor) {
|
|
13865
|
+
restore_view_anchored(gobj, anchor);
|
|
13866
|
+
return;
|
|
13867
|
+
}
|
|
13868
|
+
if (preserve_view) return;
|
|
13869
|
+
Promise.resolve(graph.zoomTo(1)).then(() => {
|
|
13870
|
+
if (!yui_graph_center_on(graph, priv.anchor_id)) yui_graph_center_on(graph, root_id);
|
|
13871
|
+
priv.last_zoom = 1;
|
|
13872
|
+
yui_graph_update_zoom((0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container"), priv.graph);
|
|
13873
|
+
});
|
|
13482
13874
|
});
|
|
13483
13875
|
}
|
|
13484
13876
|
}
|
|
@@ -13980,13 +14372,77 @@ function ac_collapse_all(gobj, event, kw, src) {
|
|
|
13980
14372
|
return 0;
|
|
13981
14373
|
}
|
|
13982
14374
|
/************************************************************
|
|
14375
|
+
* Every camera action ends the same way once an anchor is
|
|
14376
|
+
* set: the thing you were reading goes back to the middle.
|
|
14377
|
+
*
|
|
14378
|
+
* Chained on the promise G6 returns rather than run beside it
|
|
14379
|
+
* -- centring before the zoom has finished centres the OLD
|
|
14380
|
+
* view, and the reader watches the node slide away and come
|
|
14381
|
+
* back.
|
|
14382
|
+
************************************************************/
|
|
14383
|
+
function after_camera$1(gobj, moved) {
|
|
14384
|
+
let priv = gobj.priv;
|
|
14385
|
+
if (priv.anchor_state !== "on" || !priv.anchor_id) return;
|
|
14386
|
+
Promise.resolve(moved).then(function() {
|
|
14387
|
+
yui_graph_center_on(priv.graph, priv.anchor_id);
|
|
14388
|
+
}).catch(function() {});
|
|
14389
|
+
}
|
|
14390
|
+
/************************************************************
|
|
14391
|
+
* Point the anchor back at its node after a rebuild.
|
|
14392
|
+
*
|
|
14393
|
+
* Node ids are generated per build, so the id the reader chose
|
|
14394
|
+
* does not survive an expand, a collapse or a refresh -- the
|
|
14395
|
+
* `full_name` does, and it is what the anchor really means:
|
|
14396
|
+
* "this gobj", not "this card".
|
|
14397
|
+
*
|
|
14398
|
+
* A gobj no longer drawn (its branch got collapsed, or it went
|
|
14399
|
+
* away) releases the anchor rather than leaving a button that
|
|
14400
|
+
* claims to be locked onto something invisible.
|
|
14401
|
+
************************************************************/
|
|
14402
|
+
function reanchor(gobj) {
|
|
14403
|
+
let priv = gobj.priv;
|
|
14404
|
+
if (priv.anchor_state !== "on" || !priv.anchor_name) return;
|
|
14405
|
+
let found = "";
|
|
14406
|
+
if (priv.node_by_id) {
|
|
14407
|
+
for (let id of Object.keys(priv.node_by_id)) if (priv.node_by_id[id].full_name === priv.anchor_name) {
|
|
14408
|
+
found = id;
|
|
14409
|
+
break;
|
|
14410
|
+
}
|
|
14411
|
+
}
|
|
14412
|
+
priv.anchor_id = found;
|
|
14413
|
+
if (!found) {
|
|
14414
|
+
priv.anchor_state = "off";
|
|
14415
|
+
priv.anchor_name = "";
|
|
14416
|
+
}
|
|
14417
|
+
yui_graph_update_anchor((0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container"), priv.anchor_state);
|
|
14418
|
+
}
|
|
14419
|
+
/************************************************************
|
|
14420
|
+
* Arm the anchor, or let it go.
|
|
14421
|
+
*
|
|
14422
|
+
* Three states and one button, so a press ADVANCES: with no
|
|
14423
|
+
* target it starts waiting for one, and with a target it
|
|
14424
|
+
* releases. Pressing it while it waits is a cancel, which is
|
|
14425
|
+
* what somebody who pressed it by mistake will try.
|
|
14426
|
+
************************************************************/
|
|
14427
|
+
function ac_toggle_anchor$1(gobj, event, kw, src) {
|
|
14428
|
+
let priv = gobj.priv;
|
|
14429
|
+
if (priv.anchor_state === "off") priv.anchor_state = "arming";
|
|
14430
|
+
else {
|
|
14431
|
+
priv.anchor_state = "off";
|
|
14432
|
+
priv.anchor_id = "";
|
|
14433
|
+
priv.anchor_name = "";
|
|
14434
|
+
}
|
|
14435
|
+
yui_graph_update_anchor((0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container"), priv.anchor_state);
|
|
14436
|
+
return 0;
|
|
14437
|
+
}
|
|
14438
|
+
/************************************************************
|
|
13983
14439
|
*
|
|
13984
14440
|
************************************************************/
|
|
13985
14441
|
function ac_zoom_in$1(gobj, event, kw, src) {
|
|
13986
14442
|
let graph = gobj.priv.graph;
|
|
13987
14443
|
if (graph) {
|
|
13988
14444
|
let z = graph.getZoom();
|
|
13989
|
-
graph.zoomTo(z * 1.2);
|
|
14445
|
+
after_camera$1(gobj, graph.zoomTo(z * 1.2));
|
|
13990
14446
|
}
|
|
13991
14447
|
return 0;
|
|
13992
14448
|
}
|
|
@@ -13997,7 +14453,7 @@ function ac_zoom_out$1(gobj, event, kw, src) {
|
|
|
13997
14453
|
let graph = gobj.priv.graph;
|
|
13998
14454
|
if (graph) {
|
|
13999
14455
|
let z = graph.getZoom();
|
|
14000
|
-
graph.zoomTo(z * .8);
|
|
14456
|
+
after_camera$1(gobj, graph.zoomTo(z * .8));
|
|
14001
14457
|
}
|
|
14002
14458
|
return 0;
|
|
14003
14459
|
}
|
|
@@ -14005,10 +14461,14 @@ function ac_zoom_out$1(gobj, event, kw, src) {
|
|
|
14005
14461
|
*
|
|
14006
14462
|
************************************************************/
|
|
14007
14463
|
function ac_zoom_reset$1(gobj, event, kw, src) {
|
|
14008
|
-
let
|
|
14464
|
+
let priv = gobj.priv;
|
|
14465
|
+
let graph = priv.graph;
|
|
14009
14466
|
if (graph && graph.rendered) {
|
|
14010
|
-
graph.zoomTo(1);
|
|
14011
|
-
|
|
14467
|
+
if (priv.anchor_state === "on" && priv.anchor_id) after_camera$1(gobj, graph.zoomTo(1));
|
|
14468
|
+
else {
|
|
14469
|
+
graph.zoomTo(1);
|
|
14470
|
+
graph.translateTo([0, 0]);
|
|
14471
|
+
}
|
|
14012
14472
|
}
|
|
14013
14473
|
return 0;
|
|
14014
14474
|
}
|
|
@@ -14036,6 +14496,14 @@ function ac_node_click$3(gobj, event, kw, src) {
|
|
|
14036
14496
|
if (nd && nd.data) node_data = nd.data;
|
|
14037
14497
|
} catch (e) {}
|
|
14038
14498
|
if (!node_data) return 0;
|
|
14499
|
+
if (priv.anchor_state === "arming") {
|
|
14500
|
+
priv.anchor_id = node_id;
|
|
14501
|
+
priv.anchor_name = node_data.full_name || "";
|
|
14502
|
+
priv.anchor_state = "on";
|
|
14503
|
+
yui_graph_update_anchor((0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container"), priv.anchor_state);
|
|
14504
|
+
yui_graph_center_on(graph, node_id);
|
|
14505
|
+
return 0;
|
|
14506
|
+
}
|
|
14039
14507
|
let client_x = kw.evt && kw.evt.client ? kw.evt.client.x : 0;
|
|
14040
14508
|
let client_y = kw.evt && kw.evt.client ? kw.evt.client.y : 0;
|
|
14041
14509
|
show_popover(gobj, node_data, client_x, client_y);
|
|
@@ -14116,6 +14584,7 @@ function create_gclass$9(gclass_name) {
|
|
|
14116
14584
|
["EV_ZOOM_RESET", 0],
|
|
14117
14585
|
["EV_CENTER", 0],
|
|
14118
14586
|
["EV_NODE_CLICK", 0],
|
|
14587
|
+
["EV_TOGGLE_ANCHOR", 0],
|
|
14119
14588
|
["EV_RESIZE", 0],
|
|
14120
14589
|
["EV_SHOW", 0],
|
|
14121
14590
|
["EV_HIDE", 0]
|
|
@@ -14175,6 +14644,11 @@ function create_gclass$9(gclass_name) {
|
|
|
14175
14644
|
ac_node_click$3,
|
|
14176
14645
|
null
|
|
14177
14646
|
],
|
|
14647
|
+
[
|
|
14648
|
+
"EV_TOGGLE_ANCHOR",
|
|
14649
|
+
ac_toggle_anchor$1,
|
|
14650
|
+
null
|
|
14651
|
+
],
|
|
14178
14652
|
[
|
|
14179
14653
|
"EV_RESIZE",
|
|
14180
14654
|
ac_resize$1,
|
|
@@ -20737,131 +21211,6 @@ function register_c_yui_treedb_topic_with_form() {
|
|
|
20737
21211
|
return create_gclass$5(GCLASS_NAME$5);
|
|
20738
21212
|
}
|
|
20739
21213
|
//#endregion
|
|
20740
|
-
//#region src/lib_graph.js
|
|
20741
|
-
/***********************************************************************
|
|
20742
|
-
* lib_graph.js
|
|
20743
|
-
*
|
|
20744
|
-
* Utilities for graph, using Bulma
|
|
20745
|
-
*
|
|
20746
|
-
* Copyright (c) 2025, ArtGins.
|
|
20747
|
-
* All Rights Reserved.
|
|
20748
|
-
***********************************************************************/
|
|
20749
|
-
/************************************************************
|
|
20750
|
-
* Add class to elements selected
|
|
20751
|
-
************************************************************/
|
|
20752
|
-
function addClasses($container, selector, ...classNames) {
|
|
20753
|
-
$container.querySelectorAll(selector).forEach((el) => el.classList.add(...classNames));
|
|
20754
|
-
}
|
|
20755
|
-
/************************************************************
|
|
20756
|
-
* Remove class from elements selected
|
|
20757
|
-
************************************************************/
|
|
20758
|
-
function removeClasses($container, selector, ...classNames) {
|
|
20759
|
-
$container.querySelectorAll(selector).forEach((el) => el.classList.remove(...classNames));
|
|
20760
|
-
}
|
|
20761
|
-
/************************************************************
|
|
20762
|
-
* Toggle class to elements selected
|
|
20763
|
-
************************************************************/
|
|
20764
|
-
function toggleClasses($container, selector, ...classNames) {
|
|
20765
|
-
$container.querySelectorAll(selector).forEach((el) => classNames.forEach((cls) => el.classList.toggle(cls)));
|
|
20766
|
-
}
|
|
20767
|
-
/************************************************************
|
|
20768
|
-
* Remove child elements
|
|
20769
|
-
************************************************************/
|
|
20770
|
-
function removeChildElements($element) {
|
|
20771
|
-
$element.innerHTML = "";
|
|
20772
|
-
}
|
|
20773
|
-
/************************************************************
|
|
20774
|
-
* Disable/Enable elements
|
|
20775
|
-
************************************************************/
|
|
20776
|
-
function disableElements($container, selector) {
|
|
20777
|
-
$container.querySelectorAll(selector).forEach((el) => {
|
|
20778
|
-
el.setAttribute("disabled", "");
|
|
20779
|
-
});
|
|
20780
|
-
}
|
|
20781
|
-
function enableElements($container, selector) {
|
|
20782
|
-
$container.querySelectorAll(selector).forEach((el) => {
|
|
20783
|
-
if (el.hasAttribute("disabled")) el.removeAttribute("disabled");
|
|
20784
|
-
});
|
|
20785
|
-
}
|
|
20786
|
-
/************************************************************
|
|
20787
|
-
* Set or reset the color of 'enable submit action' state
|
|
20788
|
-
* Color green
|
|
20789
|
-
************************************************************/
|
|
20790
|
-
function set_submit_state($container, selector, set) {
|
|
20791
|
-
if (set) addClasses($container, selector, "color_submit_state");
|
|
20792
|
-
else removeClasses($container, selector, "color_submit_state");
|
|
20793
|
-
}
|
|
20794
|
-
/************************************************************
|
|
20795
|
-
* Set or reset the color of 'enable cancel action' state
|
|
20796
|
-
* Color red
|
|
20797
|
-
************************************************************/
|
|
20798
|
-
function set_cancel_state($container, selector, set) {
|
|
20799
|
-
if (set) addClasses($container, selector, "color_cancel_state");
|
|
20800
|
-
else removeClasses($container, selector, "color_cancel_state");
|
|
20801
|
-
}
|
|
20802
|
-
/************************************************************
|
|
20803
|
-
* Set or reset the color of 'enable actived' state
|
|
20804
|
-
* Color orange
|
|
20805
|
-
************************************************************/
|
|
20806
|
-
function set_active_state($container, selector, set) {
|
|
20807
|
-
if (set) addClasses($container, selector, "color_active_state");
|
|
20808
|
-
else removeClasses($container, selector, "color_active_state");
|
|
20809
|
-
}
|
|
20810
|
-
/************************************************************
|
|
20811
|
-
* Set or reset the 'pressed' state of a TOGGLE.
|
|
20812
|
-
*
|
|
20813
|
-
* Not one of the colours above: each of those names a KIND of
|
|
20814
|
-
* action (create, pending, history, destructive), and a toggle
|
|
20815
|
-
* is not an action -- it is a state the control is IN. So it
|
|
20816
|
-
* looks pressed instead of changing category, which also keeps
|
|
20817
|
-
* it out of the way of a neighbour wearing the same hue.
|
|
20818
|
-
************************************************************/
|
|
20819
|
-
function set_pressed_state($container, selector, set) {
|
|
20820
|
-
if (set) addClasses($container, selector, "pressed_state");
|
|
20821
|
-
else removeClasses($container, selector, "pressed_state");
|
|
20822
|
-
}
|
|
20823
|
-
/**
|
|
20824
|
-
* Returns a smart stroke color in `rgba()` format based on:
|
|
20825
|
-
* - The given fill color (hex or rgb[a])
|
|
20826
|
-
* - The current theme ('light' or 'dark')
|
|
20827
|
-
* - The adjustment factor (default 0.2)
|
|
20828
|
-
*
|
|
20829
|
-
* @param {string} fillColor - Fill color string
|
|
20830
|
-
* @param {string} theme - 'light' or 'dark'
|
|
20831
|
-
* @param {number} factor - Lighten/darken factor (default 0.2)
|
|
20832
|
-
* @returns {string} rgba(r, g, b, a)
|
|
20833
|
-
*/
|
|
20834
|
-
function getStrokeColor(fillColor, theme = "light", factor = .2) {
|
|
20835
|
-
let r, g, b, a = 1;
|
|
20836
|
-
if (fillColor.startsWith("#")) {
|
|
20837
|
-
let hex = fillColor.slice(1);
|
|
20838
|
-
if (hex.length === 3 || hex.length === 4) hex = hex.split("").map((c) => c + c).join("");
|
|
20839
|
-
if (hex.length === 6) {
|
|
20840
|
-
r = parseInt(hex.slice(0, 2), 16);
|
|
20841
|
-
g = parseInt(hex.slice(2, 4), 16);
|
|
20842
|
-
b = parseInt(hex.slice(4, 6), 16);
|
|
20843
|
-
} else if (hex.length === 8) {
|
|
20844
|
-
r = parseInt(hex.slice(0, 2), 16);
|
|
20845
|
-
g = parseInt(hex.slice(2, 4), 16);
|
|
20846
|
-
b = parseInt(hex.slice(4, 6), 16);
|
|
20847
|
-
a = parseInt(hex.slice(6, 8), 16) / 255;
|
|
20848
|
-
} else throw new Error("Unsupported hex format");
|
|
20849
|
-
} else if (fillColor.startsWith("rgb")) {
|
|
20850
|
-
const match = fillColor.match(/rgba?\(\s*(\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\s*\)/);
|
|
20851
|
-
if (!match) throw new Error("Invalid rgb()/rgba() format.");
|
|
20852
|
-
r = parseInt(match[1], 10);
|
|
20853
|
-
g = parseInt(match[2], 10);
|
|
20854
|
-
b = parseInt(match[3], 10);
|
|
20855
|
-
if (match[4] !== void 0) a = parseFloat(match[4]);
|
|
20856
|
-
} else throw new Error("Unsupported color format.");
|
|
20857
|
-
const luminance = (.299 * r + .587 * g + .114 * b) / 255;
|
|
20858
|
-
let lighten;
|
|
20859
|
-
if (theme === "dark") lighten = luminance < .7;
|
|
20860
|
-
else lighten = luminance < .3;
|
|
20861
|
-
const adjust = (v) => lighten ? Math.min(255, Math.floor(v + (255 - v) * factor)) : Math.max(0, Math.floor(v * (1 - factor)));
|
|
20862
|
-
return `rgba(${adjust(r)}, ${adjust(g)}, ${adjust(b)}, ${a.toFixed(3)})`;
|
|
20863
|
-
}
|
|
20864
|
-
//#endregion
|
|
20865
21214
|
//#region src/treedb_node_label.js
|
|
20866
21215
|
/***********************************************************************
|
|
20867
21216
|
* treedb_node_label.js
|
|
@@ -47729,6 +48078,13 @@ var ICONS = `<svg xmlns="http://www.w3.org/2000/svg">
|
|
|
47729
48078
|
stroke="currentColor" stroke-width="40"
|
|
47730
48079
|
stroke-dasharray="59 36"/>
|
|
47731
48080
|
</symbol>
|
|
48081
|
+
<!-- location-crosshairs (anchor the camera on one element).
|
|
48082
|
+
The SAME path as the DOM toolbars and their yi-location-crosshairs
|
|
48083
|
+
mask: the two toolbars sit in one console, and a control that
|
|
48084
|
+
does one thing has to be drawn one way. -->
|
|
48085
|
+
<symbol id="g6-icon-anchor" viewBox="0 0 576 512">
|
|
48086
|
+
<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"/>
|
|
48087
|
+
</symbol>
|
|
47732
48088
|
<!-- floppy-disk (save) -->
|
|
47733
48089
|
<symbol id="g6-icon-save" viewBox="0 0 448 512">
|
|
47734
48090
|
<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"/>
|
|
@@ -47922,6 +48278,9 @@ var attrs_table$4 = [
|
|
|
47922
48278
|
(0, _yuneta_gobj_js.SDATA_END)()
|
|
47923
48279
|
];
|
|
47924
48280
|
var PRIVATE_DATA$4 = {
|
|
48281
|
+
anchor_id: "",
|
|
48282
|
+
anchor_state: "off",
|
|
48283
|
+
last_zoom: 1,
|
|
47925
48284
|
_xy: 100,
|
|
47926
48285
|
_edge_seq: 0,
|
|
47927
48286
|
_history_paused: false,
|
|
@@ -48218,6 +48577,15 @@ function configure_events(gobj) {
|
|
|
48218
48577
|
update_node_icon_position(gobj);
|
|
48219
48578
|
update_link_icon_position(gobj);
|
|
48220
48579
|
update_zoom_readout(gobj);
|
|
48580
|
+
let z = priv.last_zoom;
|
|
48581
|
+
try {
|
|
48582
|
+
z = priv.graph.getZoom();
|
|
48583
|
+
} catch (e) {
|
|
48584
|
+
return;
|
|
48585
|
+
}
|
|
48586
|
+
if (z === priv.last_zoom) return;
|
|
48587
|
+
priv.last_zoom = z;
|
|
48588
|
+
if (priv.anchor_state === "on" && priv.anchor_id) yui_graph_center_on(priv.graph, priv.anchor_id);
|
|
48221
48589
|
});
|
|
48222
48590
|
priv._on_language_changed = () => {
|
|
48223
48591
|
update_toolbar(gobj);
|
|
@@ -48455,6 +48823,12 @@ function configure_toolbar(gobj) {
|
|
|
48455
48823
|
value: "reset",
|
|
48456
48824
|
className: "EV_ZOOM_RESET",
|
|
48457
48825
|
title: (0, i18next.t)("actual size")
|
|
48826
|
+
},
|
|
48827
|
+
{
|
|
48828
|
+
id: "g6-icon-anchor",
|
|
48829
|
+
value: "anchor",
|
|
48830
|
+
className: "EV_TOGGLE_ANCHOR" + (priv.anchor_state !== "off" ? " pressed_state" : "") + (priv.anchor_state === "arming" ? " color_pending_state" : ""),
|
|
48831
|
+
title: anchor_title(priv.anchor_state)
|
|
48458
48832
|
}
|
|
48459
48833
|
];
|
|
48460
48834
|
if (priv.edit_mode) items.push({
|
|
@@ -48496,6 +48870,9 @@ function configure_toolbar(gobj) {
|
|
|
48496
48870
|
case "reset":
|
|
48497
48871
|
(0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_ZOOM_RESET", {}, gobj);
|
|
48498
48872
|
break;
|
|
48873
|
+
case "anchor":
|
|
48874
|
+
(0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_TOGGLE_ANCHOR", {}, gobj);
|
|
48875
|
+
break;
|
|
48499
48876
|
case "auto-fit":
|
|
48500
48877
|
(0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_AUTO_FIT", {}, gobj);
|
|
48501
48878
|
break;
|
|
@@ -49506,12 +49883,12 @@ function graph_find_nodes(gobj, term) {
|
|
|
49506
49883
|
async function graph_zoom_in(gobj) {
|
|
49507
49884
|
let graph = gobj.priv.graph;
|
|
49508
49885
|
const z = graph.getZoom();
|
|
49509
|
-
|
|
49886
|
+
after_camera(gobj, graph.zoomTo(z * 1.1));
|
|
49510
49887
|
}
|
|
49511
49888
|
async function graph_zoom_out(gobj) {
|
|
49512
49889
|
let graph = gobj.priv.graph;
|
|
49513
49890
|
const z = graph.getZoom();
|
|
49514
|
-
|
|
49891
|
+
after_camera(gobj, graph.zoomTo(z * .9));
|
|
49515
49892
|
}
|
|
49516
49893
|
async function graph_render(gobj) {
|
|
49517
49894
|
await gobj.priv.graph.render();
|
|
@@ -52512,7 +52889,56 @@ function ac_zoom_out(gobj, event, kw, src) {
|
|
|
52512
52889
|
return 0;
|
|
52513
52890
|
}
|
|
52514
52891
|
function ac_zoom_reset(gobj, event, kw, src) {
|
|
52515
|
-
gobj.priv
|
|
52892
|
+
let priv = gobj.priv;
|
|
52893
|
+
let graph = priv.graph;
|
|
52894
|
+
if (priv.anchor_state === "on" && priv.anchor_id) after_camera(gobj, graph.zoomTo(1));
|
|
52895
|
+
else graph.zoomTo(1);
|
|
52896
|
+
return 0;
|
|
52897
|
+
}
|
|
52898
|
+
/************************************************************
|
|
52899
|
+
* What the anchor button says it will do next.
|
|
52900
|
+
************************************************************/
|
|
52901
|
+
function anchor_title(state) {
|
|
52902
|
+
if (state === "arming") return (0, i18next.t)("click the element to centre on");
|
|
52903
|
+
if (state === "on") return (0, i18next.t)("centred: click to release");
|
|
52904
|
+
return (0, i18next.t)("anchor view");
|
|
52905
|
+
}
|
|
52906
|
+
/************************************************************
|
|
52907
|
+
* Every camera action ends the same way once an anchor is
|
|
52908
|
+
* set: the thing you were reading goes back to the middle.
|
|
52909
|
+
*
|
|
52910
|
+
* Chained on the promise G6 returns rather than run beside it
|
|
52911
|
+
* -- centring before the zoom has finished centres the OLD
|
|
52912
|
+
* view, and the reader watches the node slide away and come
|
|
52913
|
+
* back.
|
|
52914
|
+
************************************************************/
|
|
52915
|
+
function after_camera(gobj, moved) {
|
|
52916
|
+
let priv = gobj.priv;
|
|
52917
|
+
if (priv.anchor_state !== "on" || !priv.anchor_id) return;
|
|
52918
|
+
Promise.resolve(moved).then(function() {
|
|
52919
|
+
yui_graph_center_on(priv.graph, priv.anchor_id);
|
|
52920
|
+
}).catch(function() {});
|
|
52921
|
+
}
|
|
52922
|
+
/************************************************************
|
|
52923
|
+
* Arm the anchor, or let it go.
|
|
52924
|
+
*
|
|
52925
|
+
* Three states and one button, so a press ADVANCES: with no
|
|
52926
|
+
* target it starts waiting for one, and with a target it
|
|
52927
|
+
* releases. Pressing it while it waits is a cancel, which is
|
|
52928
|
+
* what somebody who pressed it by mistake will try.
|
|
52929
|
+
*
|
|
52930
|
+
* Unlike the DOM toolbars, the button is not repainted in
|
|
52931
|
+
* place: this toolbar is a G6 plugin that rebuilds its whole
|
|
52932
|
+
* item list, and the pressed classes are baked into that list.
|
|
52933
|
+
************************************************************/
|
|
52934
|
+
function ac_toggle_anchor(gobj, event, kw, src) {
|
|
52935
|
+
let priv = gobj.priv;
|
|
52936
|
+
if (priv.anchor_state === "off") priv.anchor_state = "arming";
|
|
52937
|
+
else {
|
|
52938
|
+
priv.anchor_state = "off";
|
|
52939
|
+
priv.anchor_id = "";
|
|
52940
|
+
}
|
|
52941
|
+
update_toolbar(gobj);
|
|
52516
52942
|
return 0;
|
|
52517
52943
|
}
|
|
52518
52944
|
/************************************************************
|
|
@@ -52686,6 +53112,13 @@ function ac_node_click$2(gobj, event, kw, src) {
|
|
|
52686
53112
|
let priv = gobj.priv;
|
|
52687
53113
|
let graph = priv.graph;
|
|
52688
53114
|
let node_id = kw.evt.target.id;
|
|
53115
|
+
if (priv.anchor_state === "arming") {
|
|
53116
|
+
priv.anchor_id = node_id;
|
|
53117
|
+
priv.anchor_state = "on";
|
|
53118
|
+
update_toolbar(gobj);
|
|
53119
|
+
yui_graph_center_on(graph, node_id);
|
|
53120
|
+
return 0;
|
|
53121
|
+
}
|
|
52689
53122
|
try {
|
|
52690
53123
|
let nodedata = graph.getNodeData(node_id);
|
|
52691
53124
|
if (nodedata && nodedata.data && nodedata.data.desc) {
|
|
@@ -52947,6 +53380,11 @@ function create_gclass$4(gclass_name) {
|
|
|
52947
53380
|
ac_zoom_reset,
|
|
52948
53381
|
null
|
|
52949
53382
|
],
|
|
53383
|
+
[
|
|
53384
|
+
"EV_TOGGLE_ANCHOR",
|
|
53385
|
+
ac_toggle_anchor,
|
|
53386
|
+
null
|
|
53387
|
+
],
|
|
52950
53388
|
[
|
|
52951
53389
|
"EV_TOGGLE_TOOLBARS",
|
|
52952
53390
|
ac_toggle_toolbars,
|
|
@@ -53060,6 +53498,7 @@ function create_gclass$4(gclass_name) {
|
|
|
53060
53498
|
["EV_ZOOM_IN", 0],
|
|
53061
53499
|
["EV_ZOOM_OUT", 0],
|
|
53062
53500
|
["EV_ZOOM_RESET", 0],
|
|
53501
|
+
["EV_TOGGLE_ANCHOR", 0],
|
|
53063
53502
|
["EV_TOGGLE_TOOLBARS", 0],
|
|
53064
53503
|
["EV_AUTO_FIT", 0],
|
|
53065
53504
|
["EV_ZOOM_SELECTION", 0],
|