@yuneta/gobj-ui 7.23.17 → 7.23.19

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.
@@ -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) {
@@ -10075,21 +10273,48 @@ var PRIVATE_DATA$11 = {
10075
10273
  $layout_select: null,
10076
10274
  $find_input: null,
10077
10275
  $find_result: null,
10078
- $find_count: null
10276
+ $find_count: null,
10277
+ anchor_id: "",
10278
+ anchor_path: "",
10279
+ anchor_state: "off",
10280
+ last_zoom: 1
10079
10281
  };
10080
10282
  var __gclass__$11 = null;
10081
10283
  var __layout_registered__$1 = false;
10082
10284
  /***************************************************************
10083
- * Type colors matching the original mx_json_viewer
10084
- ***************************************************************/
10085
- var TYPE_COLORS = {
10285
+ * Type colours, ONE EXPLICIT PALETTE PER THEME and not a
10286
+ * derivation, for the same reason the tree keeps two sets of
10287
+ * CSS tokens: a colour that reads on white is not the same
10288
+ * colour lightened, and deriving one from the other made the
10289
+ * two views of one document disagree about what a string is.
10290
+ * These ARE the tree's tokens (`c_yui_json.css`), so the tree
10291
+ * and the graph now say green with the same green.
10292
+ *
10293
+ * The light values are darker than the `mx_json_viewer` ones
10294
+ * they came from, because those could not be read: `#FF8C00`
10295
+ * is mid-luminance, so it reaches at most **2.33:1 against any
10296
+ * background that exists** -- no card colour and no highlight
10297
+ * could have fixed it, only the colour itself. Orange and blue
10298
+ * moved until every type clears 4.5:1 on all five surfaces this
10299
+ * viewer paints (two light cards, the match chip, two dark
10300
+ * cards); red and green were already there.
10301
+ ***************************************************************/
10302
+ var TYPE_COLORS_LIGHT = {
10086
10303
  string: "#006000",
10087
- number: "#EE422E",
10088
- boolean: "#FF8C00",
10089
- null: "#475ED0",
10304
+ number: "#b5330b",
10305
+ boolean: "#8C4D00",
10306
+ null: "#4359C6",
10090
10307
  list: "#4C0099",
10091
10308
  dict: "#4C0099"
10092
10309
  };
10310
+ var TYPE_COLORS_DARK = {
10311
+ string: "#7ec87e",
10312
+ number: "#ff8a65",
10313
+ boolean: "#ffb74d",
10314
+ null: "#90a4f4",
10315
+ list: "#B799D6",
10316
+ dict: "#B799D6"
10317
+ };
10093
10318
  /***************************************************************
10094
10319
  * Group (card) colours. `fill` is the light-theme card; `tint`
10095
10320
  * is the hue that fill hints at, used to derive the dark card —
@@ -10143,21 +10368,15 @@ function json_card_style(group, dark) {
10143
10368
  };
10144
10369
  }
10145
10370
  /***************************************************************
10146
- * A scalar's colour by type. The palette is tuned for a light
10147
- * card; on a dark one every one of them (dark green #006000,
10148
- * blue #475ED0, …) sinks into the background, so brighten it.
10149
- *
10150
- * 60% white and not the 45% it shipped with. The number is a
10151
- * balance measured both ways: brighter reads better on the dark
10152
- * card (the worst type goes from 2.88:1 to 4.74:1) and washes the
10153
- * six types toward one pastel, and past this point red, orange
10154
- * and purple stop being tellable apart, which is the only reason
10155
- * they are coloured at all.
10371
+ * A scalar's colour by type, straight out of the palette for
10372
+ * the theme. It used to be the light colour mixed with white,
10373
+ * which is how a document ended up green in one view and a
10374
+ * paler green in the other, and how the amount of white became
10375
+ * a knob nobody could set: enough of it to read on a dark card
10376
+ * washed the six types into one pastel.
10156
10377
  ***************************************************************/
10157
10378
  function type_color(type, dark) {
10158
- let color = TYPE_COLORS[type] || (dark ? "#e8eaed" : "black");
10159
- if (!dark) return color;
10160
- return `color-mix(in srgb, ${color} 40%, #ffffff)`;
10379
+ return (dark ? TYPE_COLORS_DARK : TYPE_COLORS_LIGHT)[type] || (dark ? "#e8eaed" : "black");
10161
10380
  }
10162
10381
  /******************************
10163
10382
  * Framework Methods
@@ -10351,6 +10570,7 @@ function make_toolbar$3(gobj) {
10351
10570
  ]]);
10352
10571
  let center_items = [];
10353
10572
  center_items = yui_graph_camera_items(gobj, priv.graph, toolbar_wide);
10573
+ center_items.push(yui_graph_anchor_item(gobj, toolbar_wide));
10354
10574
  center_items.push(yui_graph_refresh_item(gobj, toolbar_wide));
10355
10575
  let current_layout = (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "layout") || DEFAULT_LAYOUT;
10356
10576
  let options = Object.keys(LAYOUTS$1).map(function(key) {
@@ -10433,6 +10653,15 @@ function build_graph$3(gobj) {
10433
10653
  });
10434
10654
  graph.on("aftertransform", () => {
10435
10655
  yui_graph_update_zoom((0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container"), priv.graph);
10656
+ let z = priv.last_zoom;
10657
+ try {
10658
+ z = priv.graph.getZoom();
10659
+ } catch (e) {
10660
+ return;
10661
+ }
10662
+ if (z === priv.last_zoom) return;
10663
+ priv.last_zoom = z;
10664
+ if (priv.anchor_state === "on" && priv.anchor_id) yui_graph_center_on(priv.graph, priv.anchor_id);
10436
10665
  });
10437
10666
  graph.on(_antv_g6.CanvasEvent.CLICK, (evt) => {});
10438
10667
  if (typeof ResizeObserver !== "undefined") {
@@ -10611,14 +10840,17 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id) {
10611
10840
  card_matched = true;
10612
10841
  priv.match_count++;
10613
10842
  }
10614
- let html = build_cell_html(key, value, type, dark, matched);
10615
- lines.push(html);
10616
- if ((0, _yuneta_gobj_js.is_object)(value) && (0, _yuneta_gobj_js.json_object_size)(value) > 0 || (0, _yuneta_gobj_js.is_array)(value) && value.length > 0) pending_complex.push({
10617
- key,
10618
- value
10619
- });
10843
+ if ((0, _yuneta_gobj_js.is_object)(value) && (0, _yuneta_gobj_js.json_object_size)(value) > 0 || (0, _yuneta_gobj_js.is_array)(value) && value.length > 0) {
10844
+ pending_complex.push({
10845
+ key,
10846
+ value
10847
+ });
10848
+ continue;
10849
+ }
10850
+ lines.push(build_cell_html(key, value, type, dark, matched));
10620
10851
  }
10621
10852
  let label = get_group_label(path) || (is_dict ? "{}" : "[]");
10853
+ if (pending_complex.length > 0) label += is_dict ? ` {${pending_complex.length}}` : ` [${pending_complex.length}]`;
10622
10854
  let foldable = pending_complex.length > 0;
10623
10855
  let folded = foldable && priv.collapsed.has(path);
10624
10856
  let colors = json_card_style(is_dict ? GROUP_COLORS.dict : GROUP_COLORS.list, dark);
@@ -10694,13 +10926,22 @@ function load_json(gobj) {
10694
10926
  let preserve_view = !!priv.pending_preserve_view;
10695
10927
  priv.pending_preserve_view = false;
10696
10928
  if (nodes.length > 0) {
10929
+ let root_id = nodes[0].id;
10697
10930
  graph.setData({
10698
10931
  nodes,
10699
10932
  edges
10700
10933
  });
10701
10934
  graph.render().then(() => {
10702
- if (!preserve_view) graph.fitView();
10703
- yui_graph_update_zoom((0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container"), priv.graph);
10935
+ reanchor$1(gobj);
10936
+ if (preserve_view) {
10937
+ yui_graph_update_zoom((0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container"), priv.graph);
10938
+ return;
10939
+ }
10940
+ Promise.resolve(graph.zoomTo(1)).then(() => {
10941
+ if (!yui_graph_center_on(graph, priv.anchor_id)) yui_graph_center_on(graph, root_id);
10942
+ priv.last_zoom = 1;
10943
+ yui_graph_update_zoom((0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container"), priv.graph);
10944
+ });
10704
10945
  });
10705
10946
  }
10706
10947
  }
@@ -10963,13 +11204,79 @@ function ac_theme$3(gobj, event, kw, src) {
10963
11204
  return 0;
10964
11205
  }
10965
11206
  /************************************************************
11207
+ * Every camera action ends the same way once an anchor is
11208
+ * set: the thing you were reading goes back to the middle.
11209
+ *
11210
+ * Chained on the promise G6 returns rather than run beside it
11211
+ * -- centring before the zoom has finished centres the OLD
11212
+ * view, and the reader watches the node slide away and come
11213
+ * back.
11214
+ ************************************************************/
11215
+ function after_camera$2(gobj, moved) {
11216
+ let priv = gobj.priv;
11217
+ if (priv.anchor_state !== "on" || !priv.anchor_id) return;
11218
+ Promise.resolve(moved).then(function() {
11219
+ yui_graph_center_on(priv.graph, priv.anchor_id);
11220
+ }).catch(function() {});
11221
+ }
11222
+ /************************************************************
11223
+ * Point the anchor back at its node after a rebuild.
11224
+ *
11225
+ * Node ids are generated per build, so the id the reader
11226
+ * chose does not survive a refresh, a fold or a layout
11227
+ * change -- the PATH does, and it is what the anchor really
11228
+ * means: "this place in the document", not "this card".
11229
+ *
11230
+ * A path that is no longer drawn (its parent got folded)
11231
+ * releases the anchor rather than leaving a button that says
11232
+ * it is locked onto something invisible.
11233
+ ************************************************************/
11234
+ function reanchor$1(gobj) {
11235
+ let priv = gobj.priv;
11236
+ if (priv.anchor_state !== "on" || !priv.anchor_path) return;
11237
+ let found = "";
11238
+ try {
11239
+ for (let node of priv.graph.getNodeData()) if (node && node.data && node.data.path === priv.anchor_path) {
11240
+ found = node.id;
11241
+ break;
11242
+ }
11243
+ } catch (e) {
11244
+ return;
11245
+ }
11246
+ priv.anchor_id = found;
11247
+ if (!found) {
11248
+ priv.anchor_state = "off";
11249
+ priv.anchor_path = "";
11250
+ }
11251
+ yui_graph_update_anchor((0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container"), priv.anchor_state);
11252
+ }
11253
+ /************************************************************
11254
+ * Arm the anchor, or let it go.
11255
+ *
11256
+ * Three states and one button, so a press ADVANCES: with no
11257
+ * target it starts waiting for one, and with a target it
11258
+ * releases. Pressing it while it waits is a cancel, which is
11259
+ * what somebody who pressed it by mistake will try.
11260
+ ************************************************************/
11261
+ function ac_toggle_anchor$2(gobj, event, kw, src) {
11262
+ let priv = gobj.priv;
11263
+ if (priv.anchor_state === "off") priv.anchor_state = "arming";
11264
+ else {
11265
+ priv.anchor_state = "off";
11266
+ priv.anchor_id = "";
11267
+ priv.anchor_path = "";
11268
+ }
11269
+ yui_graph_update_anchor((0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container"), priv.anchor_state);
11270
+ return 0;
11271
+ }
11272
+ /************************************************************
10966
11273
  *
10967
11274
  ************************************************************/
10968
11275
  function ac_zoom_in$2(gobj, event, kw, src) {
10969
11276
  let graph = gobj.priv.graph;
10970
11277
  if (graph) {
10971
11278
  let z = graph.getZoom();
10972
- graph.zoomTo(z * 1.2);
11279
+ after_camera$2(gobj, graph.zoomTo(z * 1.2));
10973
11280
  }
10974
11281
  return 0;
10975
11282
  }
@@ -10980,7 +11287,7 @@ function ac_zoom_out$2(gobj, event, kw, src) {
10980
11287
  let graph = gobj.priv.graph;
10981
11288
  if (graph) {
10982
11289
  let z = graph.getZoom();
10983
- graph.zoomTo(z * .8);
11290
+ after_camera$2(gobj, graph.zoomTo(z * .8));
10984
11291
  }
10985
11292
  return 0;
10986
11293
  }
@@ -10988,10 +11295,14 @@ function ac_zoom_out$2(gobj, event, kw, src) {
10988
11295
  *
10989
11296
  ************************************************************/
10990
11297
  function ac_zoom_reset$2(gobj, event, kw, src) {
10991
- let graph = gobj.priv.graph;
11298
+ let priv = gobj.priv;
11299
+ let graph = priv.graph;
10992
11300
  if (graph && graph.rendered) {
10993
- graph.zoomTo(1);
10994
- graph.translateTo([0, 0]);
11301
+ if (priv.anchor_state === "on" && priv.anchor_id) after_camera$2(gobj, graph.zoomTo(1));
11302
+ else {
11303
+ graph.zoomTo(1);
11304
+ graph.translateTo([0, 0]);
11305
+ }
10995
11306
  }
10996
11307
  return 0;
10997
11308
  }
@@ -11010,12 +11321,21 @@ function ac_center$2(gobj, event, kw, src) {
11010
11321
  *
11011
11322
  ************************************************************/
11012
11323
  function ac_node_click$4(gobj, event, kw, src) {
11013
- let graph = gobj.priv.graph;
11324
+ let priv = gobj.priv;
11325
+ let graph = priv.graph;
11014
11326
  let node_id = kw.evt.target.id;
11015
11327
  let nodedata;
11016
11328
  try {
11017
11329
  nodedata = graph.getNodeData(node_id);
11018
11330
  } catch (e) {}
11331
+ if (priv.anchor_state === "arming") {
11332
+ priv.anchor_id = node_id;
11333
+ priv.anchor_path = nodedata && nodedata.data ? nodedata.data.path : "";
11334
+ priv.anchor_state = "on";
11335
+ yui_graph_update_anchor((0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container"), priv.anchor_state);
11336
+ yui_graph_center_on(graph, node_id);
11337
+ return 0;
11338
+ }
11019
11339
  if (nodedata && nodedata.data) (0, _yuneta_gobj_js.gobj_publish_event)(gobj, "EV_JSON_ITEM_CLICKED", {
11020
11340
  path: nodedata.data.path,
11021
11341
  id: node_id
@@ -11149,6 +11469,11 @@ function create_gclass$11(gclass_name) {
11149
11469
  ac_node_click$4,
11150
11470
  null
11151
11471
  ],
11472
+ [
11473
+ "EV_TOGGLE_ANCHOR",
11474
+ ac_toggle_anchor$2,
11475
+ null
11476
+ ],
11152
11477
  [
11153
11478
  "EV_RESIZE",
11154
11479
  ac_resize$2,
@@ -11179,6 +11504,7 @@ function create_gclass$11(gclass_name) {
11179
11504
  ["EV_ZOOM_RESET", 0],
11180
11505
  ["EV_CENTER", 0],
11181
11506
  ["EV_NODE_CLICK", 0],
11507
+ ["EV_TOGGLE_ANCHOR", 0],
11182
11508
  ["EV_RESIZE", 0],
11183
11509
  ["EV_SHOW", 0],
11184
11510
  ["EV_HIDE", 0],
@@ -12700,7 +13026,11 @@ var PRIVATE_DATA$9 = {
12700
13026
  node_by_id: null,
12701
13027
  collapse_state: null,
12702
13028
  resize_observer: null,
12703
- _resize_raf: 0
13029
+ _resize_raf: 0,
13030
+ anchor_id: "",
13031
+ anchor_name: "",
13032
+ anchor_state: "off",
13033
+ last_zoom: 1
12704
13034
  };
12705
13035
  var __gclass__$9 = null;
12706
13036
  var __layout_registered__ = false;
@@ -13093,6 +13423,7 @@ function make_toolbar$1(gobj) {
13093
13423
  let center_items = [];
13094
13424
  let right_items = [];
13095
13425
  center_items = yui_graph_camera_items(gobj, priv.graph, toolbar_wide);
13426
+ center_items.push(yui_graph_anchor_item(gobj, toolbar_wide));
13096
13427
  center_items.push(yui_graph_refresh_item(gobj, toolbar_wide));
13097
13428
  let options = [];
13098
13429
  for (let key of Object.keys(LAYOUTS)) {
@@ -13168,6 +13499,15 @@ function build_graph$2(gobj) {
13168
13499
  if (priv.theme) graph.setTheme(priv.theme);
13169
13500
  graph.on("aftertransform", () => {
13170
13501
  yui_graph_update_zoom((0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container"), priv.graph);
13502
+ let z = priv.last_zoom;
13503
+ try {
13504
+ z = priv.graph.getZoom();
13505
+ } catch (e) {
13506
+ return;
13507
+ }
13508
+ if (z === priv.last_zoom) return;
13509
+ priv.last_zoom = z;
13510
+ if (priv.anchor_state === "on" && priv.anchor_id) yui_graph_center_on(priv.graph, priv.anchor_id);
13171
13511
  });
13172
13512
  graph.on(_antv_g6.NodeEvent.CLICK, (evt) => {
13173
13513
  (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_NODE_CLICK", { evt }, gobj);
@@ -13455,13 +13795,23 @@ function load_tree(gobj) {
13455
13795
  let edges = [];
13456
13796
  build_gobj_nodes(gobj, yuno, nodes, edges, null, true, layout_cfg.compact);
13457
13797
  if (nodes.length > 0) {
13798
+ let root_id = nodes[0].id;
13458
13799
  graph.setData({
13459
13800
  nodes,
13460
13801
  edges
13461
13802
  });
13462
13803
  graph.render().then(() => {
13463
- if (anchor) restore_view_anchored(gobj, anchor);
13464
- else if (!preserve_view) graph.fitView();
13804
+ reanchor(gobj);
13805
+ if (anchor) {
13806
+ restore_view_anchored(gobj, anchor);
13807
+ return;
13808
+ }
13809
+ if (preserve_view) return;
13810
+ Promise.resolve(graph.zoomTo(1)).then(() => {
13811
+ if (!yui_graph_center_on(graph, priv.anchor_id)) yui_graph_center_on(graph, root_id);
13812
+ priv.last_zoom = 1;
13813
+ yui_graph_update_zoom((0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container"), priv.graph);
13814
+ });
13465
13815
  });
13466
13816
  }
13467
13817
  }
@@ -13963,13 +14313,77 @@ function ac_collapse_all(gobj, event, kw, src) {
13963
14313
  return 0;
13964
14314
  }
13965
14315
  /************************************************************
14316
+ * Every camera action ends the same way once an anchor is
14317
+ * set: the thing you were reading goes back to the middle.
14318
+ *
14319
+ * Chained on the promise G6 returns rather than run beside it
14320
+ * -- centring before the zoom has finished centres the OLD
14321
+ * view, and the reader watches the node slide away and come
14322
+ * back.
14323
+ ************************************************************/
14324
+ function after_camera$1(gobj, moved) {
14325
+ let priv = gobj.priv;
14326
+ if (priv.anchor_state !== "on" || !priv.anchor_id) return;
14327
+ Promise.resolve(moved).then(function() {
14328
+ yui_graph_center_on(priv.graph, priv.anchor_id);
14329
+ }).catch(function() {});
14330
+ }
14331
+ /************************************************************
14332
+ * Point the anchor back at its node after a rebuild.
14333
+ *
14334
+ * Node ids are generated per build, so the id the reader chose
14335
+ * does not survive an expand, a collapse or a refresh -- the
14336
+ * `full_name` does, and it is what the anchor really means:
14337
+ * "this gobj", not "this card".
14338
+ *
14339
+ * A gobj no longer drawn (its branch got collapsed, or it went
14340
+ * away) releases the anchor rather than leaving a button that
14341
+ * claims to be locked onto something invisible.
14342
+ ************************************************************/
14343
+ function reanchor(gobj) {
14344
+ let priv = gobj.priv;
14345
+ if (priv.anchor_state !== "on" || !priv.anchor_name) return;
14346
+ let found = "";
14347
+ if (priv.node_by_id) {
14348
+ for (let id of Object.keys(priv.node_by_id)) if (priv.node_by_id[id].full_name === priv.anchor_name) {
14349
+ found = id;
14350
+ break;
14351
+ }
14352
+ }
14353
+ priv.anchor_id = found;
14354
+ if (!found) {
14355
+ priv.anchor_state = "off";
14356
+ priv.anchor_name = "";
14357
+ }
14358
+ yui_graph_update_anchor((0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container"), priv.anchor_state);
14359
+ }
14360
+ /************************************************************
14361
+ * Arm the anchor, or let it go.
14362
+ *
14363
+ * Three states and one button, so a press ADVANCES: with no
14364
+ * target it starts waiting for one, and with a target it
14365
+ * releases. Pressing it while it waits is a cancel, which is
14366
+ * what somebody who pressed it by mistake will try.
14367
+ ************************************************************/
14368
+ function ac_toggle_anchor$1(gobj, event, kw, src) {
14369
+ let priv = gobj.priv;
14370
+ if (priv.anchor_state === "off") priv.anchor_state = "arming";
14371
+ else {
14372
+ priv.anchor_state = "off";
14373
+ priv.anchor_id = "";
14374
+ priv.anchor_name = "";
14375
+ }
14376
+ yui_graph_update_anchor((0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container"), priv.anchor_state);
14377
+ return 0;
14378
+ }
14379
+ /************************************************************
13966
14380
  *
13967
14381
  ************************************************************/
13968
14382
  function ac_zoom_in$1(gobj, event, kw, src) {
13969
14383
  let graph = gobj.priv.graph;
13970
14384
  if (graph) {
13971
14385
  let z = graph.getZoom();
13972
- graph.zoomTo(z * 1.2);
14386
+ after_camera$1(gobj, graph.zoomTo(z * 1.2));
13973
14387
  }
13974
14388
  return 0;
13975
14389
  }
@@ -13980,7 +14394,7 @@ function ac_zoom_out$1(gobj, event, kw, src) {
13980
14394
  let graph = gobj.priv.graph;
13981
14395
  if (graph) {
13982
14396
  let z = graph.getZoom();
13983
- graph.zoomTo(z * .8);
14397
+ after_camera$1(gobj, graph.zoomTo(z * .8));
13984
14398
  }
13985
14399
  return 0;
13986
14400
  }
@@ -13988,10 +14402,14 @@ function ac_zoom_out$1(gobj, event, kw, src) {
13988
14402
  *
13989
14403
  ************************************************************/
13990
14404
  function ac_zoom_reset$1(gobj, event, kw, src) {
13991
- let graph = gobj.priv.graph;
14405
+ let priv = gobj.priv;
14406
+ let graph = priv.graph;
13992
14407
  if (graph && graph.rendered) {
13993
- graph.zoomTo(1);
13994
- graph.translateTo([0, 0]);
14408
+ if (priv.anchor_state === "on" && priv.anchor_id) after_camera$1(gobj, graph.zoomTo(1));
14409
+ else {
14410
+ graph.zoomTo(1);
14411
+ graph.translateTo([0, 0]);
14412
+ }
13995
14413
  }
13996
14414
  return 0;
13997
14415
  }
@@ -14019,6 +14437,14 @@ function ac_node_click$3(gobj, event, kw, src) {
14019
14437
  if (nd && nd.data) node_data = nd.data;
14020
14438
  } catch (e) {}
14021
14439
  if (!node_data) return 0;
14440
+ if (priv.anchor_state === "arming") {
14441
+ priv.anchor_id = node_id;
14442
+ priv.anchor_name = node_data.full_name || "";
14443
+ priv.anchor_state = "on";
14444
+ yui_graph_update_anchor((0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container"), priv.anchor_state);
14445
+ yui_graph_center_on(graph, node_id);
14446
+ return 0;
14447
+ }
14022
14448
  let client_x = kw.evt && kw.evt.client ? kw.evt.client.x : 0;
14023
14449
  let client_y = kw.evt && kw.evt.client ? kw.evt.client.y : 0;
14024
14450
  show_popover(gobj, node_data, client_x, client_y);
@@ -14099,6 +14525,7 @@ function create_gclass$9(gclass_name) {
14099
14525
  ["EV_ZOOM_RESET", 0],
14100
14526
  ["EV_CENTER", 0],
14101
14527
  ["EV_NODE_CLICK", 0],
14528
+ ["EV_TOGGLE_ANCHOR", 0],
14102
14529
  ["EV_RESIZE", 0],
14103
14530
  ["EV_SHOW", 0],
14104
14531
  ["EV_HIDE", 0]
@@ -14158,6 +14585,11 @@ function create_gclass$9(gclass_name) {
14158
14585
  ac_node_click$3,
14159
14586
  null
14160
14587
  ],
14588
+ [
14589
+ "EV_TOGGLE_ANCHOR",
14590
+ ac_toggle_anchor$1,
14591
+ null
14592
+ ],
14161
14593
  [
14162
14594
  "EV_RESIZE",
14163
14595
  ac_resize$1,
@@ -20720,131 +21152,6 @@ function register_c_yui_treedb_topic_with_form() {
20720
21152
  return create_gclass$5(GCLASS_NAME$5);
20721
21153
  }
20722
21154
  //#endregion
20723
- //#region src/lib_graph.js
20724
- /***********************************************************************
20725
- * lib_graph.js
20726
- *
20727
- * Utilities for graph, using Bulma
20728
- *
20729
- * Copyright (c) 2025, ArtGins.
20730
- * All Rights Reserved.
20731
- ***********************************************************************/
20732
- /************************************************************
20733
- * Add class to elements selected
20734
- ************************************************************/
20735
- function addClasses($container, selector, ...classNames) {
20736
- $container.querySelectorAll(selector).forEach((el) => el.classList.add(...classNames));
20737
- }
20738
- /************************************************************
20739
- * Remove class from elements selected
20740
- ************************************************************/
20741
- function removeClasses($container, selector, ...classNames) {
20742
- $container.querySelectorAll(selector).forEach((el) => el.classList.remove(...classNames));
20743
- }
20744
- /************************************************************
20745
- * Toggle class to elements selected
20746
- ************************************************************/
20747
- function toggleClasses($container, selector, ...classNames) {
20748
- $container.querySelectorAll(selector).forEach((el) => classNames.forEach((cls) => el.classList.toggle(cls)));
20749
- }
20750
- /************************************************************
20751
- * Remove child elements
20752
- ************************************************************/
20753
- function removeChildElements($element) {
20754
- $element.innerHTML = "";
20755
- }
20756
- /************************************************************
20757
- * Disable/Enable elements
20758
- ************************************************************/
20759
- function disableElements($container, selector) {
20760
- $container.querySelectorAll(selector).forEach((el) => {
20761
- el.setAttribute("disabled", "");
20762
- });
20763
- }
20764
- function enableElements($container, selector) {
20765
- $container.querySelectorAll(selector).forEach((el) => {
20766
- if (el.hasAttribute("disabled")) el.removeAttribute("disabled");
20767
- });
20768
- }
20769
- /************************************************************
20770
- * Set or reset the color of 'enable submit action' state
20771
- * Color green
20772
- ************************************************************/
20773
- function set_submit_state($container, selector, set) {
20774
- if (set) addClasses($container, selector, "color_submit_state");
20775
- else removeClasses($container, selector, "color_submit_state");
20776
- }
20777
- /************************************************************
20778
- * Set or reset the color of 'enable cancel action' state
20779
- * Color red
20780
- ************************************************************/
20781
- function set_cancel_state($container, selector, set) {
20782
- if (set) addClasses($container, selector, "color_cancel_state");
20783
- else removeClasses($container, selector, "color_cancel_state");
20784
- }
20785
- /************************************************************
20786
- * Set or reset the color of 'enable actived' state
20787
- * Color orange
20788
- ************************************************************/
20789
- function set_active_state($container, selector, set) {
20790
- if (set) addClasses($container, selector, "color_active_state");
20791
- else removeClasses($container, selector, "color_active_state");
20792
- }
20793
- /************************************************************
20794
- * Set or reset the 'pressed' state of a TOGGLE.
20795
- *
20796
- * Not one of the colours above: each of those names a KIND of
20797
- * action (create, pending, history, destructive), and a toggle
20798
- * is not an action -- it is a state the control is IN. So it
20799
- * looks pressed instead of changing category, which also keeps
20800
- * it out of the way of a neighbour wearing the same hue.
20801
- ************************************************************/
20802
- function set_pressed_state($container, selector, set) {
20803
- if (set) addClasses($container, selector, "pressed_state");
20804
- else removeClasses($container, selector, "pressed_state");
20805
- }
20806
- /**
20807
- * Returns a smart stroke color in `rgba()` format based on:
20808
- * - The given fill color (hex or rgb[a])
20809
- * - The current theme ('light' or 'dark')
20810
- * - The adjustment factor (default 0.2)
20811
- *
20812
- * @param {string} fillColor - Fill color string
20813
- * @param {string} theme - 'light' or 'dark'
20814
- * @param {number} factor - Lighten/darken factor (default 0.2)
20815
- * @returns {string} rgba(r, g, b, a)
20816
- */
20817
- function getStrokeColor(fillColor, theme = "light", factor = .2) {
20818
- let r, g, b, a = 1;
20819
- if (fillColor.startsWith("#")) {
20820
- let hex = fillColor.slice(1);
20821
- if (hex.length === 3 || hex.length === 4) hex = hex.split("").map((c) => c + c).join("");
20822
- if (hex.length === 6) {
20823
- r = parseInt(hex.slice(0, 2), 16);
20824
- g = parseInt(hex.slice(2, 4), 16);
20825
- b = parseInt(hex.slice(4, 6), 16);
20826
- } else if (hex.length === 8) {
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
- a = parseInt(hex.slice(6, 8), 16) / 255;
20831
- } else throw new Error("Unsupported hex format");
20832
- } else if (fillColor.startsWith("rgb")) {
20833
- const match = fillColor.match(/rgba?\(\s*(\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\s*\)/);
20834
- if (!match) throw new Error("Invalid rgb()/rgba() format.");
20835
- r = parseInt(match[1], 10);
20836
- g = parseInt(match[2], 10);
20837
- b = parseInt(match[3], 10);
20838
- if (match[4] !== void 0) a = parseFloat(match[4]);
20839
- } else throw new Error("Unsupported color format.");
20840
- const luminance = (.299 * r + .587 * g + .114 * b) / 255;
20841
- let lighten;
20842
- if (theme === "dark") lighten = luminance < .7;
20843
- else lighten = luminance < .3;
20844
- const adjust = (v) => lighten ? Math.min(255, Math.floor(v + (255 - v) * factor)) : Math.max(0, Math.floor(v * (1 - factor)));
20845
- return `rgba(${adjust(r)}, ${adjust(g)}, ${adjust(b)}, ${a.toFixed(3)})`;
20846
- }
20847
- //#endregion
20848
21155
  //#region src/treedb_node_label.js
20849
21156
  /***********************************************************************
20850
21157
  * treedb_node_label.js
@@ -47712,6 +48019,13 @@ var ICONS = `<svg xmlns="http://www.w3.org/2000/svg">
47712
48019
  stroke="currentColor" stroke-width="40"
47713
48020
  stroke-dasharray="59 36"/>
47714
48021
  </symbol>
48022
+ <!-- location-crosshairs (anchor the camera on one element).
48023
+ The SAME path as the DOM toolbars and their yi-location-crosshairs
48024
+ mask: the two toolbars sit in one console, and a control that
48025
+ does one thing has to be drawn one way. -->
48026
+ <symbol id="g6-icon-anchor" viewBox="0 0 576 512">
48027
+ <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"/>
48028
+ </symbol>
47715
48029
  <!-- floppy-disk (save) -->
47716
48030
  <symbol id="g6-icon-save" viewBox="0 0 448 512">
47717
48031
  <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"/>
@@ -47905,6 +48219,9 @@ var attrs_table$4 = [
47905
48219
  (0, _yuneta_gobj_js.SDATA_END)()
47906
48220
  ];
47907
48221
  var PRIVATE_DATA$4 = {
48222
+ anchor_id: "",
48223
+ anchor_state: "off",
48224
+ last_zoom: 1,
47908
48225
  _xy: 100,
47909
48226
  _edge_seq: 0,
47910
48227
  _history_paused: false,
@@ -48201,6 +48518,15 @@ function configure_events(gobj) {
48201
48518
  update_node_icon_position(gobj);
48202
48519
  update_link_icon_position(gobj);
48203
48520
  update_zoom_readout(gobj);
48521
+ let z = priv.last_zoom;
48522
+ try {
48523
+ z = priv.graph.getZoom();
48524
+ } catch (e) {
48525
+ return;
48526
+ }
48527
+ if (z === priv.last_zoom) return;
48528
+ priv.last_zoom = z;
48529
+ if (priv.anchor_state === "on" && priv.anchor_id) yui_graph_center_on(priv.graph, priv.anchor_id);
48204
48530
  });
48205
48531
  priv._on_language_changed = () => {
48206
48532
  update_toolbar(gobj);
@@ -48438,6 +48764,12 @@ function configure_toolbar(gobj) {
48438
48764
  value: "reset",
48439
48765
  className: "EV_ZOOM_RESET",
48440
48766
  title: (0, i18next.t)("actual size")
48767
+ },
48768
+ {
48769
+ id: "g6-icon-anchor",
48770
+ value: "anchor",
48771
+ className: "EV_TOGGLE_ANCHOR" + (priv.anchor_state !== "off" ? " pressed_state" : "") + (priv.anchor_state === "arming" ? " color_pending_state" : ""),
48772
+ title: anchor_title(priv.anchor_state)
48441
48773
  }
48442
48774
  ];
48443
48775
  if (priv.edit_mode) items.push({
@@ -48479,6 +48811,9 @@ function configure_toolbar(gobj) {
48479
48811
  case "reset":
48480
48812
  (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_ZOOM_RESET", {}, gobj);
48481
48813
  break;
48814
+ case "anchor":
48815
+ (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_TOGGLE_ANCHOR", {}, gobj);
48816
+ break;
48482
48817
  case "auto-fit":
48483
48818
  (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_AUTO_FIT", {}, gobj);
48484
48819
  break;
@@ -49489,12 +49824,12 @@ function graph_find_nodes(gobj, term) {
49489
49824
  async function graph_zoom_in(gobj) {
49490
49825
  let graph = gobj.priv.graph;
49491
49826
  const z = graph.getZoom();
49492
- await graph.zoomTo(z * 1.1);
49827
+ after_camera(gobj, graph.zoomTo(z * 1.1));
49493
49828
  }
49494
49829
  async function graph_zoom_out(gobj) {
49495
49830
  let graph = gobj.priv.graph;
49496
49831
  const z = graph.getZoom();
49497
- await graph.zoomTo(z * .9);
49832
+ after_camera(gobj, graph.zoomTo(z * .9));
49498
49833
  }
49499
49834
  async function graph_render(gobj) {
49500
49835
  await gobj.priv.graph.render();
@@ -52495,7 +52830,56 @@ function ac_zoom_out(gobj, event, kw, src) {
52495
52830
  return 0;
52496
52831
  }
52497
52832
  function ac_zoom_reset(gobj, event, kw, src) {
52498
- gobj.priv.graph.zoomTo(1);
52833
+ let priv = gobj.priv;
52834
+ let graph = priv.graph;
52835
+ if (priv.anchor_state === "on" && priv.anchor_id) after_camera(gobj, graph.zoomTo(1));
52836
+ else graph.zoomTo(1);
52837
+ return 0;
52838
+ }
52839
+ /************************************************************
52840
+ * What the anchor button says it will do next.
52841
+ ************************************************************/
52842
+ function anchor_title(state) {
52843
+ if (state === "arming") return (0, i18next.t)("click the element to centre on");
52844
+ if (state === "on") return (0, i18next.t)("centred: click to release");
52845
+ return (0, i18next.t)("anchor view");
52846
+ }
52847
+ /************************************************************
52848
+ * Every camera action ends the same way once an anchor is
52849
+ * set: the thing you were reading goes back to the middle.
52850
+ *
52851
+ * Chained on the promise G6 returns rather than run beside it
52852
+ * -- centring before the zoom has finished centres the OLD
52853
+ * view, and the reader watches the node slide away and come
52854
+ * back.
52855
+ ************************************************************/
52856
+ function after_camera(gobj, moved) {
52857
+ let priv = gobj.priv;
52858
+ if (priv.anchor_state !== "on" || !priv.anchor_id) return;
52859
+ Promise.resolve(moved).then(function() {
52860
+ yui_graph_center_on(priv.graph, priv.anchor_id);
52861
+ }).catch(function() {});
52862
+ }
52863
+ /************************************************************
52864
+ * Arm the anchor, or let it go.
52865
+ *
52866
+ * Three states and one button, so a press ADVANCES: with no
52867
+ * target it starts waiting for one, and with a target it
52868
+ * releases. Pressing it while it waits is a cancel, which is
52869
+ * what somebody who pressed it by mistake will try.
52870
+ *
52871
+ * Unlike the DOM toolbars, the button is not repainted in
52872
+ * place: this toolbar is a G6 plugin that rebuilds its whole
52873
+ * item list, and the pressed classes are baked into that list.
52874
+ ************************************************************/
52875
+ function ac_toggle_anchor(gobj, event, kw, src) {
52876
+ let priv = gobj.priv;
52877
+ if (priv.anchor_state === "off") priv.anchor_state = "arming";
52878
+ else {
52879
+ priv.anchor_state = "off";
52880
+ priv.anchor_id = "";
52881
+ }
52882
+ update_toolbar(gobj);
52499
52883
  return 0;
52500
52884
  }
52501
52885
  /************************************************************
@@ -52669,6 +53053,13 @@ function ac_node_click$2(gobj, event, kw, src) {
52669
53053
  let priv = gobj.priv;
52670
53054
  let graph = priv.graph;
52671
53055
  let node_id = kw.evt.target.id;
53056
+ if (priv.anchor_state === "arming") {
53057
+ priv.anchor_id = node_id;
53058
+ priv.anchor_state = "on";
53059
+ update_toolbar(gobj);
53060
+ yui_graph_center_on(graph, node_id);
53061
+ return 0;
53062
+ }
52672
53063
  try {
52673
53064
  let nodedata = graph.getNodeData(node_id);
52674
53065
  if (nodedata && nodedata.data && nodedata.data.desc) {
@@ -52930,6 +53321,11 @@ function create_gclass$4(gclass_name) {
52930
53321
  ac_zoom_reset,
52931
53322
  null
52932
53323
  ],
53324
+ [
53325
+ "EV_TOGGLE_ANCHOR",
53326
+ ac_toggle_anchor,
53327
+ null
53328
+ ],
52933
53329
  [
52934
53330
  "EV_TOGGLE_TOOLBARS",
52935
53331
  ac_toggle_toolbars,
@@ -53043,6 +53439,7 @@ function create_gclass$4(gclass_name) {
53043
53439
  ["EV_ZOOM_IN", 0],
53044
53440
  ["EV_ZOOM_OUT", 0],
53045
53441
  ["EV_ZOOM_RESET", 0],
53442
+ ["EV_TOGGLE_ANCHOR", 0],
53046
53443
  ["EV_TOGGLE_TOOLBARS", 0],
53047
53444
  ["EV_AUTO_FIT", 0],
53048
53445
  ["EV_ZOOM_SELECTION", 0],