@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.
@@ -9345,6 +9345,123 @@ function yui_toolbar(attrs = {}, items = []) {
9345
9345
  return $toolbar;
9346
9346
  }
9347
9347
  //#endregion
9348
+ //#region src/lib_graph.js
9349
+ /************************************************************
9350
+ * Add class to elements selected
9351
+ ************************************************************/
9352
+ function addClasses($container, selector, ...classNames) {
9353
+ $container.querySelectorAll(selector).forEach((el) => el.classList.add(...classNames));
9354
+ }
9355
+ /************************************************************
9356
+ * Remove class from elements selected
9357
+ ************************************************************/
9358
+ function removeClasses($container, selector, ...classNames) {
9359
+ $container.querySelectorAll(selector).forEach((el) => el.classList.remove(...classNames));
9360
+ }
9361
+ /************************************************************
9362
+ * Toggle class to elements selected
9363
+ ************************************************************/
9364
+ function toggleClasses($container, selector, ...classNames) {
9365
+ $container.querySelectorAll(selector).forEach((el) => classNames.forEach((cls) => el.classList.toggle(cls)));
9366
+ }
9367
+ /************************************************************
9368
+ * Remove child elements
9369
+ ************************************************************/
9370
+ function removeChildElements($element) {
9371
+ $element.innerHTML = "";
9372
+ }
9373
+ /************************************************************
9374
+ * Disable/Enable elements
9375
+ ************************************************************/
9376
+ function disableElements($container, selector) {
9377
+ $container.querySelectorAll(selector).forEach((el) => {
9378
+ el.setAttribute("disabled", "");
9379
+ });
9380
+ }
9381
+ function enableElements($container, selector) {
9382
+ $container.querySelectorAll(selector).forEach((el) => {
9383
+ if (el.hasAttribute("disabled")) el.removeAttribute("disabled");
9384
+ });
9385
+ }
9386
+ /************************************************************
9387
+ * Set or reset the color of 'enable submit action' state
9388
+ * Color green
9389
+ ************************************************************/
9390
+ function set_submit_state($container, selector, set) {
9391
+ if (set) addClasses($container, selector, "color_submit_state");
9392
+ else removeClasses($container, selector, "color_submit_state");
9393
+ }
9394
+ /************************************************************
9395
+ * Set or reset the color of 'enable cancel action' state
9396
+ * Color red
9397
+ ************************************************************/
9398
+ function set_cancel_state($container, selector, set) {
9399
+ if (set) addClasses($container, selector, "color_cancel_state");
9400
+ else removeClasses($container, selector, "color_cancel_state");
9401
+ }
9402
+ /************************************************************
9403
+ * Set or reset the color of 'enable actived' state
9404
+ * Color orange
9405
+ ************************************************************/
9406
+ function set_active_state($container, selector, set) {
9407
+ if (set) addClasses($container, selector, "color_active_state");
9408
+ else removeClasses($container, selector, "color_active_state");
9409
+ }
9410
+ /************************************************************
9411
+ * Set or reset the 'pressed' state of a TOGGLE.
9412
+ *
9413
+ * Not one of the colours above: each of those names a KIND of
9414
+ * action (create, pending, history, destructive), and a toggle
9415
+ * is not an action -- it is a state the control is IN. So it
9416
+ * looks pressed instead of changing category, which also keeps
9417
+ * it out of the way of a neighbour wearing the same hue.
9418
+ ************************************************************/
9419
+ function set_pressed_state($container, selector, set) {
9420
+ if (set) addClasses($container, selector, "pressed_state");
9421
+ else removeClasses($container, selector, "pressed_state");
9422
+ }
9423
+ /**
9424
+ * Returns a smart stroke color in `rgba()` format based on:
9425
+ * - The given fill color (hex or rgb[a])
9426
+ * - The current theme ('light' or 'dark')
9427
+ * - The adjustment factor (default 0.2)
9428
+ *
9429
+ * @param {string} fillColor - Fill color string
9430
+ * @param {string} theme - 'light' or 'dark'
9431
+ * @param {number} factor - Lighten/darken factor (default 0.2)
9432
+ * @returns {string} rgba(r, g, b, a)
9433
+ */
9434
+ function getStrokeColor(fillColor, theme = "light", factor = .2) {
9435
+ let r, g, b, a = 1;
9436
+ if (fillColor.startsWith("#")) {
9437
+ let hex = fillColor.slice(1);
9438
+ if (hex.length === 3 || hex.length === 4) hex = hex.split("").map((c) => c + c).join("");
9439
+ if (hex.length === 6) {
9440
+ r = parseInt(hex.slice(0, 2), 16);
9441
+ g = parseInt(hex.slice(2, 4), 16);
9442
+ b = parseInt(hex.slice(4, 6), 16);
9443
+ } else if (hex.length === 8) {
9444
+ r = parseInt(hex.slice(0, 2), 16);
9445
+ g = parseInt(hex.slice(2, 4), 16);
9446
+ b = parseInt(hex.slice(4, 6), 16);
9447
+ a = parseInt(hex.slice(6, 8), 16) / 255;
9448
+ } else throw new Error("Unsupported hex format");
9449
+ } else if (fillColor.startsWith("rgb")) {
9450
+ const match = fillColor.match(/rgba?\(\s*(\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\s*\)/);
9451
+ if (!match) throw new Error("Invalid rgb()/rgba() format.");
9452
+ r = parseInt(match[1], 10);
9453
+ g = parseInt(match[2], 10);
9454
+ b = parseInt(match[3], 10);
9455
+ if (match[4] !== void 0) a = parseFloat(match[4]);
9456
+ } else throw new Error("Unsupported color format.");
9457
+ const luminance = (.299 * r + .587 * g + .114 * b) / 255;
9458
+ let lighten;
9459
+ if (theme === "dark") lighten = luminance < .7;
9460
+ else lighten = luminance < .3;
9461
+ const adjust = (v) => lighten ? Math.min(255, Math.floor(v + (255 - v) * factor)) : Math.max(0, Math.floor(v * (1 - factor)));
9462
+ return `rgba(${adjust(r)}, ${adjust(g)}, ${adjust(b)}, ${a.toFixed(3)})`;
9463
+ }
9464
+ //#endregion
9348
9465
  //#region src/yui_graph_camera.js
9349
9466
  /***********************************************************************
9350
9467
  * yui_graph_camera.js
@@ -9493,6 +9610,79 @@ function yui_graph_fold_items(gobj, wide) {
9493
9610
  return [camera_button(gobj, "yi-chevron-right", "EV_EXPAND_ALL", "expand all", wide, " transform: rotate(90deg);"), camera_button(gobj, "yi-chevron-right", "EV_COLLAPSE_ALL", "collapse all", wide)];
9494
9611
  }
9495
9612
  /************************************************************
9613
+ * The ANCHOR toggle: pick one element and every camera move
9614
+ * from then on leaves it in the MIDDLE.
9615
+ *
9616
+ * Why it exists: a graph that fits on screen is unreadable at
9617
+ * the zoom that makes it fit, and the zoom that makes it
9618
+ * readable does not fit -- so the useful view is always a
9619
+ * fraction of the document, and which fraction is the whole
9620
+ * question. Without an anchor, `1:1` lands wherever the
9621
+ * layout's origin happens to be and the reader hunts for the
9622
+ * node they were looking at.
9623
+ *
9624
+ * Crosshairs and not a thumbtack. A tack says "do not move
9625
+ * this", which is what a pinned NODE would mean in a graph you
9626
+ * can drag nodes around in; this pins the CAMERA to a target,
9627
+ * which is what a crosshair says and what every map that
9628
+ * offers it draws. It is also in the icon set already, and a
9629
+ * `yi-*` class that is not is a black square.
9630
+ *
9631
+ * Three states, because two would not say what a press does:
9632
+ *
9633
+ * off no target; the camera behaves as it always did
9634
+ * arming waiting for you to click the element you mean
9635
+ * on locked; every zoom re-centres on it
9636
+ ************************************************************/
9637
+ function yui_graph_anchor_item(gobj, wide) {
9638
+ return camera_button(gobj, "yi-location-crosshairs", "EV_TOGGLE_ANCHOR", "anchor view", wide);
9639
+ }
9640
+ /************************************************************
9641
+ * Paint the anchor button. `state` is "off" | "arming" | "on".
9642
+ *
9643
+ * Both live states look PRESSED, because both are states the
9644
+ * control is IN rather than actions; what separates them is the
9645
+ * colour, and only the waiting one takes a palette colour --
9646
+ * it is the one that needs the reader to do something next.
9647
+ ************************************************************/
9648
+ function yui_graph_update_anchor($container, state) {
9649
+ if (!$container) return;
9650
+ let $btn = $container.querySelector(".EV_TOGGLE_ANCHOR");
9651
+ if (!$btn) return;
9652
+ set_pressed_state($container, ".EV_TOGGLE_ANCHOR", state !== "off");
9653
+ $btn.classList.toggle("color_pending_state", state === "arming");
9654
+ $btn.setAttribute("aria-pressed", state === "on" ? "true" : "false");
9655
+ let key = state === "arming" ? "click the element to centre on" : state === "on" ? "centred: click to release" : "anchor view";
9656
+ $btn.setAttribute("title", t(key));
9657
+ $btn.setAttribute("data-i18n-title", key);
9658
+ $btn.setAttribute("aria-label", t(key));
9659
+ $btn.setAttribute("data-i18n-aria-label", key);
9660
+ }
9661
+ /************************************************************
9662
+ * Put `node_id` back in the middle of the viewport.
9663
+ *
9664
+ * Called AFTER the camera moved, never instead of moving it:
9665
+ * the zoom is what the reader asked for, the centring is what
9666
+ * keeps the thing they are reading under their eyes.
9667
+ *
9668
+ * Guarded on every side because it runs on every wheel notch:
9669
+ * a graph mid-rebuild, an anchor whose node went away with a
9670
+ * fold, a G6 version without `focusElement` -- none of those is
9671
+ * an error worth a log line per notch, they are all "there is
9672
+ * nothing to centre right now".
9673
+ ************************************************************/
9674
+ function yui_graph_center_on(graph, node_id) {
9675
+ if (!graph || !node_id) return false;
9676
+ try {
9677
+ if (typeof graph.getElementData === "function" && !graph.getElementData(node_id)) return false;
9678
+ if (typeof graph.focusElement !== "function") return false;
9679
+ graph.focusElement(node_id);
9680
+ return true;
9681
+ } catch (e) {
9682
+ return false;
9683
+ }
9684
+ }
9685
+ /************************************************************
9496
9686
  * A refresh button, for the graphs that offer one.
9497
9687
  ************************************************************/
9498
9688
  function yui_graph_refresh_item(gobj, wide) {
@@ -10070,21 +10260,48 @@ var PRIVATE_DATA$11 = {
10070
10260
  $layout_select: null,
10071
10261
  $find_input: null,
10072
10262
  $find_result: null,
10073
- $find_count: null
10263
+ $find_count: null,
10264
+ anchor_id: "",
10265
+ anchor_path: "",
10266
+ anchor_state: "off",
10267
+ last_zoom: 1
10074
10268
  };
10075
10269
  var __gclass__$11 = null;
10076
10270
  var __layout_registered__$1 = false;
10077
10271
  /***************************************************************
10078
- * Type colors matching the original mx_json_viewer
10079
- ***************************************************************/
10080
- var TYPE_COLORS = {
10272
+ * Type colours, ONE EXPLICIT PALETTE PER THEME and not a
10273
+ * derivation, for the same reason the tree keeps two sets of
10274
+ * CSS tokens: a colour that reads on white is not the same
10275
+ * colour lightened, and deriving one from the other made the
10276
+ * two views of one document disagree about what a string is.
10277
+ * These ARE the tree's tokens (`c_yui_json.css`), so the tree
10278
+ * and the graph now say green with the same green.
10279
+ *
10280
+ * The light values are darker than the `mx_json_viewer` ones
10281
+ * they came from, because those could not be read: `#FF8C00`
10282
+ * is mid-luminance, so it reaches at most **2.33:1 against any
10283
+ * background that exists** -- no card colour and no highlight
10284
+ * could have fixed it, only the colour itself. Orange and blue
10285
+ * moved until every type clears 4.5:1 on all five surfaces this
10286
+ * viewer paints (two light cards, the match chip, two dark
10287
+ * cards); red and green were already there.
10288
+ ***************************************************************/
10289
+ var TYPE_COLORS_LIGHT = {
10081
10290
  string: "#006000",
10082
- number: "#EE422E",
10083
- boolean: "#FF8C00",
10084
- null: "#475ED0",
10291
+ number: "#b5330b",
10292
+ boolean: "#8C4D00",
10293
+ null: "#4359C6",
10085
10294
  list: "#4C0099",
10086
10295
  dict: "#4C0099"
10087
10296
  };
10297
+ var TYPE_COLORS_DARK = {
10298
+ string: "#7ec87e",
10299
+ number: "#ff8a65",
10300
+ boolean: "#ffb74d",
10301
+ null: "#90a4f4",
10302
+ list: "#B799D6",
10303
+ dict: "#B799D6"
10304
+ };
10088
10305
  /***************************************************************
10089
10306
  * Group (card) colours. `fill` is the light-theme card; `tint`
10090
10307
  * is the hue that fill hints at, used to derive the dark card —
@@ -10138,21 +10355,15 @@ function json_card_style(group, dark) {
10138
10355
  };
10139
10356
  }
10140
10357
  /***************************************************************
10141
- * A scalar's colour by type. The palette is tuned for a light
10142
- * card; on a dark one every one of them (dark green #006000,
10143
- * blue #475ED0, …) sinks into the background, so brighten it.
10144
- *
10145
- * 60% white and not the 45% it shipped with. The number is a
10146
- * balance measured both ways: brighter reads better on the dark
10147
- * card (the worst type goes from 2.88:1 to 4.74:1) and washes the
10148
- * six types toward one pastel, and past this point red, orange
10149
- * and purple stop being tellable apart, which is the only reason
10150
- * they are coloured at all.
10358
+ * A scalar's colour by type, straight out of the palette for
10359
+ * the theme. It used to be the light colour mixed with white,
10360
+ * which is how a document ended up green in one view and a
10361
+ * paler green in the other, and how the amount of white became
10362
+ * a knob nobody could set: enough of it to read on a dark card
10363
+ * washed the six types into one pastel.
10151
10364
  ***************************************************************/
10152
10365
  function type_color(type, dark) {
10153
- let color = TYPE_COLORS[type] || (dark ? "#e8eaed" : "black");
10154
- if (!dark) return color;
10155
- return `color-mix(in srgb, ${color} 40%, #ffffff)`;
10366
+ return (dark ? TYPE_COLORS_DARK : TYPE_COLORS_LIGHT)[type] || (dark ? "#e8eaed" : "black");
10156
10367
  }
10157
10368
  /******************************
10158
10369
  * Framework Methods
@@ -10346,6 +10557,7 @@ function make_toolbar$3(gobj) {
10346
10557
  ]]);
10347
10558
  let center_items = [];
10348
10559
  center_items = yui_graph_camera_items(gobj, priv.graph, toolbar_wide);
10560
+ center_items.push(yui_graph_anchor_item(gobj, toolbar_wide));
10349
10561
  center_items.push(yui_graph_refresh_item(gobj, toolbar_wide));
10350
10562
  let current_layout = gobj_read_str_attr(gobj, "layout") || DEFAULT_LAYOUT;
10351
10563
  let options = Object.keys(LAYOUTS$1).map(function(key) {
@@ -10428,6 +10640,15 @@ function build_graph$3(gobj) {
10428
10640
  });
10429
10641
  graph.on("aftertransform", () => {
10430
10642
  yui_graph_update_zoom(gobj_read_attr(gobj, "$container"), priv.graph);
10643
+ let z = priv.last_zoom;
10644
+ try {
10645
+ z = priv.graph.getZoom();
10646
+ } catch (e) {
10647
+ return;
10648
+ }
10649
+ if (z === priv.last_zoom) return;
10650
+ priv.last_zoom = z;
10651
+ if (priv.anchor_state === "on" && priv.anchor_id) yui_graph_center_on(priv.graph, priv.anchor_id);
10431
10652
  });
10432
10653
  graph.on(CanvasEvent.CLICK, (evt) => {});
10433
10654
  if (typeof ResizeObserver !== "undefined") {
@@ -10606,14 +10827,17 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id) {
10606
10827
  card_matched = true;
10607
10828
  priv.match_count++;
10608
10829
  }
10609
- let html = build_cell_html(key, value, type, dark, matched);
10610
- lines.push(html);
10611
- if (is_object(value) && json_object_size(value) > 0 || is_array(value) && value.length > 0) pending_complex.push({
10612
- key,
10613
- value
10614
- });
10830
+ if (is_object(value) && json_object_size(value) > 0 || is_array(value) && value.length > 0) {
10831
+ pending_complex.push({
10832
+ key,
10833
+ value
10834
+ });
10835
+ continue;
10836
+ }
10837
+ lines.push(build_cell_html(key, value, type, dark, matched));
10615
10838
  }
10616
10839
  let label = get_group_label(path) || (is_dict ? "{}" : "[]");
10840
+ if (pending_complex.length > 0) label += is_dict ? ` {${pending_complex.length}}` : ` [${pending_complex.length}]`;
10617
10841
  let foldable = pending_complex.length > 0;
10618
10842
  let folded = foldable && priv.collapsed.has(path);
10619
10843
  let colors = json_card_style(is_dict ? GROUP_COLORS.dict : GROUP_COLORS.list, dark);
@@ -10689,13 +10913,22 @@ function load_json(gobj) {
10689
10913
  let preserve_view = !!priv.pending_preserve_view;
10690
10914
  priv.pending_preserve_view = false;
10691
10915
  if (nodes.length > 0) {
10916
+ let root_id = nodes[0].id;
10692
10917
  graph.setData({
10693
10918
  nodes,
10694
10919
  edges
10695
10920
  });
10696
10921
  graph.render().then(() => {
10697
- if (!preserve_view) graph.fitView();
10698
- yui_graph_update_zoom(gobj_read_attr(gobj, "$container"), priv.graph);
10922
+ reanchor$1(gobj);
10923
+ if (preserve_view) {
10924
+ yui_graph_update_zoom(gobj_read_attr(gobj, "$container"), priv.graph);
10925
+ return;
10926
+ }
10927
+ Promise.resolve(graph.zoomTo(1)).then(() => {
10928
+ if (!yui_graph_center_on(graph, priv.anchor_id)) yui_graph_center_on(graph, root_id);
10929
+ priv.last_zoom = 1;
10930
+ yui_graph_update_zoom(gobj_read_attr(gobj, "$container"), priv.graph);
10931
+ });
10699
10932
  });
10700
10933
  }
10701
10934
  }
@@ -10958,13 +11191,79 @@ function ac_theme$3(gobj, event, kw, src) {
10958
11191
  return 0;
10959
11192
  }
10960
11193
  /************************************************************
11194
+ * Every camera action ends the same way once an anchor is
11195
+ * set: the thing you were reading goes back to the middle.
11196
+ *
11197
+ * Chained on the promise G6 returns rather than run beside it
11198
+ * -- centring before the zoom has finished centres the OLD
11199
+ * view, and the reader watches the node slide away and come
11200
+ * back.
11201
+ ************************************************************/
11202
+ function after_camera$2(gobj, moved) {
11203
+ let priv = gobj.priv;
11204
+ if (priv.anchor_state !== "on" || !priv.anchor_id) return;
11205
+ Promise.resolve(moved).then(function() {
11206
+ yui_graph_center_on(priv.graph, priv.anchor_id);
11207
+ }).catch(function() {});
11208
+ }
11209
+ /************************************************************
11210
+ * Point the anchor back at its node after a rebuild.
11211
+ *
11212
+ * Node ids are generated per build, so the id the reader
11213
+ * chose does not survive a refresh, a fold or a layout
11214
+ * change -- the PATH does, and it is what the anchor really
11215
+ * means: "this place in the document", not "this card".
11216
+ *
11217
+ * A path that is no longer drawn (its parent got folded)
11218
+ * releases the anchor rather than leaving a button that says
11219
+ * it is locked onto something invisible.
11220
+ ************************************************************/
11221
+ function reanchor$1(gobj) {
11222
+ let priv = gobj.priv;
11223
+ if (priv.anchor_state !== "on" || !priv.anchor_path) return;
11224
+ let found = "";
11225
+ try {
11226
+ for (let node of priv.graph.getNodeData()) if (node && node.data && node.data.path === priv.anchor_path) {
11227
+ found = node.id;
11228
+ break;
11229
+ }
11230
+ } catch (e) {
11231
+ return;
11232
+ }
11233
+ priv.anchor_id = found;
11234
+ if (!found) {
11235
+ priv.anchor_state = "off";
11236
+ priv.anchor_path = "";
11237
+ }
11238
+ yui_graph_update_anchor(gobj_read_attr(gobj, "$container"), priv.anchor_state);
11239
+ }
11240
+ /************************************************************
11241
+ * Arm the anchor, or let it go.
11242
+ *
11243
+ * Three states and one button, so a press ADVANCES: with no
11244
+ * target it starts waiting for one, and with a target it
11245
+ * releases. Pressing it while it waits is a cancel, which is
11246
+ * what somebody who pressed it by mistake will try.
11247
+ ************************************************************/
11248
+ function ac_toggle_anchor$2(gobj, event, kw, src) {
11249
+ let priv = gobj.priv;
11250
+ if (priv.anchor_state === "off") priv.anchor_state = "arming";
11251
+ else {
11252
+ priv.anchor_state = "off";
11253
+ priv.anchor_id = "";
11254
+ priv.anchor_path = "";
11255
+ }
11256
+ yui_graph_update_anchor(gobj_read_attr(gobj, "$container"), priv.anchor_state);
11257
+ return 0;
11258
+ }
11259
+ /************************************************************
10961
11260
  *
10962
11261
  ************************************************************/
10963
11262
  function ac_zoom_in$2(gobj, event, kw, src) {
10964
11263
  let graph = gobj.priv.graph;
10965
11264
  if (graph) {
10966
11265
  let z = graph.getZoom();
10967
- graph.zoomTo(z * 1.2);
11266
+ after_camera$2(gobj, graph.zoomTo(z * 1.2));
10968
11267
  }
10969
11268
  return 0;
10970
11269
  }
@@ -10975,7 +11274,7 @@ function ac_zoom_out$2(gobj, event, kw, src) {
10975
11274
  let graph = gobj.priv.graph;
10976
11275
  if (graph) {
10977
11276
  let z = graph.getZoom();
10978
- graph.zoomTo(z * .8);
11277
+ after_camera$2(gobj, graph.zoomTo(z * .8));
10979
11278
  }
10980
11279
  return 0;
10981
11280
  }
@@ -10983,10 +11282,14 @@ function ac_zoom_out$2(gobj, event, kw, src) {
10983
11282
  *
10984
11283
  ************************************************************/
10985
11284
  function ac_zoom_reset$2(gobj, event, kw, src) {
10986
- let graph = gobj.priv.graph;
11285
+ let priv = gobj.priv;
11286
+ let graph = priv.graph;
10987
11287
  if (graph && graph.rendered) {
10988
- graph.zoomTo(1);
10989
- graph.translateTo([0, 0]);
11288
+ if (priv.anchor_state === "on" && priv.anchor_id) after_camera$2(gobj, graph.zoomTo(1));
11289
+ else {
11290
+ graph.zoomTo(1);
11291
+ graph.translateTo([0, 0]);
11292
+ }
10990
11293
  }
10991
11294
  return 0;
10992
11295
  }
@@ -11005,12 +11308,21 @@ function ac_center$2(gobj, event, kw, src) {
11005
11308
  *
11006
11309
  ************************************************************/
11007
11310
  function ac_node_click$4(gobj, event, kw, src) {
11008
- let graph = gobj.priv.graph;
11311
+ let priv = gobj.priv;
11312
+ let graph = priv.graph;
11009
11313
  let node_id = kw.evt.target.id;
11010
11314
  let nodedata;
11011
11315
  try {
11012
11316
  nodedata = graph.getNodeData(node_id);
11013
11317
  } catch (e) {}
11318
+ if (priv.anchor_state === "arming") {
11319
+ priv.anchor_id = node_id;
11320
+ priv.anchor_path = nodedata && nodedata.data ? nodedata.data.path : "";
11321
+ priv.anchor_state = "on";
11322
+ yui_graph_update_anchor(gobj_read_attr(gobj, "$container"), priv.anchor_state);
11323
+ yui_graph_center_on(graph, node_id);
11324
+ return 0;
11325
+ }
11014
11326
  if (nodedata && nodedata.data) gobj_publish_event(gobj, "EV_JSON_ITEM_CLICKED", {
11015
11327
  path: nodedata.data.path,
11016
11328
  id: node_id
@@ -11144,6 +11456,11 @@ function create_gclass$11(gclass_name) {
11144
11456
  ac_node_click$4,
11145
11457
  null
11146
11458
  ],
11459
+ [
11460
+ "EV_TOGGLE_ANCHOR",
11461
+ ac_toggle_anchor$2,
11462
+ null
11463
+ ],
11147
11464
  [
11148
11465
  "EV_RESIZE",
11149
11466
  ac_resize$2,
@@ -11174,6 +11491,7 @@ function create_gclass$11(gclass_name) {
11174
11491
  ["EV_ZOOM_RESET", 0],
11175
11492
  ["EV_CENTER", 0],
11176
11493
  ["EV_NODE_CLICK", 0],
11494
+ ["EV_TOGGLE_ANCHOR", 0],
11177
11495
  ["EV_RESIZE", 0],
11178
11496
  ["EV_SHOW", 0],
11179
11497
  ["EV_HIDE", 0],
@@ -12695,7 +13013,11 @@ var PRIVATE_DATA$9 = {
12695
13013
  node_by_id: null,
12696
13014
  collapse_state: null,
12697
13015
  resize_observer: null,
12698
- _resize_raf: 0
13016
+ _resize_raf: 0,
13017
+ anchor_id: "",
13018
+ anchor_name: "",
13019
+ anchor_state: "off",
13020
+ last_zoom: 1
12699
13021
  };
12700
13022
  var __gclass__$9 = null;
12701
13023
  var __layout_registered__ = false;
@@ -13088,6 +13410,7 @@ function make_toolbar$1(gobj) {
13088
13410
  let center_items = [];
13089
13411
  let right_items = [];
13090
13412
  center_items = yui_graph_camera_items(gobj, priv.graph, toolbar_wide);
13413
+ center_items.push(yui_graph_anchor_item(gobj, toolbar_wide));
13091
13414
  center_items.push(yui_graph_refresh_item(gobj, toolbar_wide));
13092
13415
  let options = [];
13093
13416
  for (let key of Object.keys(LAYOUTS)) {
@@ -13163,6 +13486,15 @@ function build_graph$2(gobj) {
13163
13486
  if (priv.theme) graph.setTheme(priv.theme);
13164
13487
  graph.on("aftertransform", () => {
13165
13488
  yui_graph_update_zoom(gobj_read_attr(gobj, "$container"), priv.graph);
13489
+ let z = priv.last_zoom;
13490
+ try {
13491
+ z = priv.graph.getZoom();
13492
+ } catch (e) {
13493
+ return;
13494
+ }
13495
+ if (z === priv.last_zoom) return;
13496
+ priv.last_zoom = z;
13497
+ if (priv.anchor_state === "on" && priv.anchor_id) yui_graph_center_on(priv.graph, priv.anchor_id);
13166
13498
  });
13167
13499
  graph.on(NodeEvent.CLICK, (evt) => {
13168
13500
  gobj_send_event(gobj, "EV_NODE_CLICK", { evt }, gobj);
@@ -13450,13 +13782,23 @@ function load_tree(gobj) {
13450
13782
  let edges = [];
13451
13783
  build_gobj_nodes(gobj, yuno, nodes, edges, null, true, layout_cfg.compact);
13452
13784
  if (nodes.length > 0) {
13785
+ let root_id = nodes[0].id;
13453
13786
  graph.setData({
13454
13787
  nodes,
13455
13788
  edges
13456
13789
  });
13457
13790
  graph.render().then(() => {
13458
- if (anchor) restore_view_anchored(gobj, anchor);
13459
- else if (!preserve_view) graph.fitView();
13791
+ reanchor(gobj);
13792
+ if (anchor) {
13793
+ restore_view_anchored(gobj, anchor);
13794
+ return;
13795
+ }
13796
+ if (preserve_view) return;
13797
+ Promise.resolve(graph.zoomTo(1)).then(() => {
13798
+ if (!yui_graph_center_on(graph, priv.anchor_id)) yui_graph_center_on(graph, root_id);
13799
+ priv.last_zoom = 1;
13800
+ yui_graph_update_zoom(gobj_read_attr(gobj, "$container"), priv.graph);
13801
+ });
13460
13802
  });
13461
13803
  }
13462
13804
  }
@@ -13958,13 +14300,77 @@ function ac_collapse_all(gobj, event, kw, src) {
13958
14300
  return 0;
13959
14301
  }
13960
14302
  /************************************************************
14303
+ * Every camera action ends the same way once an anchor is
14304
+ * set: the thing you were reading goes back to the middle.
14305
+ *
14306
+ * Chained on the promise G6 returns rather than run beside it
14307
+ * -- centring before the zoom has finished centres the OLD
14308
+ * view, and the reader watches the node slide away and come
14309
+ * back.
14310
+ ************************************************************/
14311
+ function after_camera$1(gobj, moved) {
14312
+ let priv = gobj.priv;
14313
+ if (priv.anchor_state !== "on" || !priv.anchor_id) return;
14314
+ Promise.resolve(moved).then(function() {
14315
+ yui_graph_center_on(priv.graph, priv.anchor_id);
14316
+ }).catch(function() {});
14317
+ }
14318
+ /************************************************************
14319
+ * Point the anchor back at its node after a rebuild.
14320
+ *
14321
+ * Node ids are generated per build, so the id the reader chose
14322
+ * does not survive an expand, a collapse or a refresh -- the
14323
+ * `full_name` does, and it is what the anchor really means:
14324
+ * "this gobj", not "this card".
14325
+ *
14326
+ * A gobj no longer drawn (its branch got collapsed, or it went
14327
+ * away) releases the anchor rather than leaving a button that
14328
+ * claims to be locked onto something invisible.
14329
+ ************************************************************/
14330
+ function reanchor(gobj) {
14331
+ let priv = gobj.priv;
14332
+ if (priv.anchor_state !== "on" || !priv.anchor_name) return;
14333
+ let found = "";
14334
+ if (priv.node_by_id) {
14335
+ for (let id of Object.keys(priv.node_by_id)) if (priv.node_by_id[id].full_name === priv.anchor_name) {
14336
+ found = id;
14337
+ break;
14338
+ }
14339
+ }
14340
+ priv.anchor_id = found;
14341
+ if (!found) {
14342
+ priv.anchor_state = "off";
14343
+ priv.anchor_name = "";
14344
+ }
14345
+ yui_graph_update_anchor(gobj_read_attr(gobj, "$container"), priv.anchor_state);
14346
+ }
14347
+ /************************************************************
14348
+ * Arm the anchor, or let it go.
14349
+ *
14350
+ * Three states and one button, so a press ADVANCES: with no
14351
+ * target it starts waiting for one, and with a target it
14352
+ * releases. Pressing it while it waits is a cancel, which is
14353
+ * what somebody who pressed it by mistake will try.
14354
+ ************************************************************/
14355
+ function ac_toggle_anchor$1(gobj, event, kw, src) {
14356
+ let priv = gobj.priv;
14357
+ if (priv.anchor_state === "off") priv.anchor_state = "arming";
14358
+ else {
14359
+ priv.anchor_state = "off";
14360
+ priv.anchor_id = "";
14361
+ priv.anchor_name = "";
14362
+ }
14363
+ yui_graph_update_anchor(gobj_read_attr(gobj, "$container"), priv.anchor_state);
14364
+ return 0;
14365
+ }
14366
+ /************************************************************
13961
14367
  *
13962
14368
  ************************************************************/
13963
14369
  function ac_zoom_in$1(gobj, event, kw, src) {
13964
14370
  let graph = gobj.priv.graph;
13965
14371
  if (graph) {
13966
14372
  let z = graph.getZoom();
13967
- graph.zoomTo(z * 1.2);
14373
+ after_camera$1(gobj, graph.zoomTo(z * 1.2));
13968
14374
  }
13969
14375
  return 0;
13970
14376
  }
@@ -13975,7 +14381,7 @@ function ac_zoom_out$1(gobj, event, kw, src) {
13975
14381
  let graph = gobj.priv.graph;
13976
14382
  if (graph) {
13977
14383
  let z = graph.getZoom();
13978
- graph.zoomTo(z * .8);
14384
+ after_camera$1(gobj, graph.zoomTo(z * .8));
13979
14385
  }
13980
14386
  return 0;
13981
14387
  }
@@ -13983,10 +14389,14 @@ function ac_zoom_out$1(gobj, event, kw, src) {
13983
14389
  *
13984
14390
  ************************************************************/
13985
14391
  function ac_zoom_reset$1(gobj, event, kw, src) {
13986
- let graph = gobj.priv.graph;
14392
+ let priv = gobj.priv;
14393
+ let graph = priv.graph;
13987
14394
  if (graph && graph.rendered) {
13988
- graph.zoomTo(1);
13989
- graph.translateTo([0, 0]);
14395
+ if (priv.anchor_state === "on" && priv.anchor_id) after_camera$1(gobj, graph.zoomTo(1));
14396
+ else {
14397
+ graph.zoomTo(1);
14398
+ graph.translateTo([0, 0]);
14399
+ }
13990
14400
  }
13991
14401
  return 0;
13992
14402
  }
@@ -14014,6 +14424,14 @@ function ac_node_click$3(gobj, event, kw, src) {
14014
14424
  if (nd && nd.data) node_data = nd.data;
14015
14425
  } catch (e) {}
14016
14426
  if (!node_data) return 0;
14427
+ if (priv.anchor_state === "arming") {
14428
+ priv.anchor_id = node_id;
14429
+ priv.anchor_name = node_data.full_name || "";
14430
+ priv.anchor_state = "on";
14431
+ yui_graph_update_anchor(gobj_read_attr(gobj, "$container"), priv.anchor_state);
14432
+ yui_graph_center_on(graph, node_id);
14433
+ return 0;
14434
+ }
14017
14435
  let client_x = kw.evt && kw.evt.client ? kw.evt.client.x : 0;
14018
14436
  let client_y = kw.evt && kw.evt.client ? kw.evt.client.y : 0;
14019
14437
  show_popover(gobj, node_data, client_x, client_y);
@@ -14094,6 +14512,7 @@ function create_gclass$9(gclass_name) {
14094
14512
  ["EV_ZOOM_RESET", 0],
14095
14513
  ["EV_CENTER", 0],
14096
14514
  ["EV_NODE_CLICK", 0],
14515
+ ["EV_TOGGLE_ANCHOR", 0],
14097
14516
  ["EV_RESIZE", 0],
14098
14517
  ["EV_SHOW", 0],
14099
14518
  ["EV_HIDE", 0]
@@ -14153,6 +14572,11 @@ function create_gclass$9(gclass_name) {
14153
14572
  ac_node_click$3,
14154
14573
  null
14155
14574
  ],
14575
+ [
14576
+ "EV_TOGGLE_ANCHOR",
14577
+ ac_toggle_anchor$1,
14578
+ null
14579
+ ],
14156
14580
  [
14157
14581
  "EV_RESIZE",
14158
14582
  ac_resize$1,
@@ -20715,123 +21139,6 @@ function register_c_yui_treedb_topic_with_form() {
20715
21139
  return create_gclass$5(GCLASS_NAME$5);
20716
21140
  }
20717
21141
  //#endregion
20718
- //#region src/lib_graph.js
20719
- /************************************************************
20720
- * Add class to elements selected
20721
- ************************************************************/
20722
- function addClasses($container, selector, ...classNames) {
20723
- $container.querySelectorAll(selector).forEach((el) => el.classList.add(...classNames));
20724
- }
20725
- /************************************************************
20726
- * Remove class from elements selected
20727
- ************************************************************/
20728
- function removeClasses($container, selector, ...classNames) {
20729
- $container.querySelectorAll(selector).forEach((el) => el.classList.remove(...classNames));
20730
- }
20731
- /************************************************************
20732
- * Toggle class to elements selected
20733
- ************************************************************/
20734
- function toggleClasses($container, selector, ...classNames) {
20735
- $container.querySelectorAll(selector).forEach((el) => classNames.forEach((cls) => el.classList.toggle(cls)));
20736
- }
20737
- /************************************************************
20738
- * Remove child elements
20739
- ************************************************************/
20740
- function removeChildElements($element) {
20741
- $element.innerHTML = "";
20742
- }
20743
- /************************************************************
20744
- * Disable/Enable elements
20745
- ************************************************************/
20746
- function disableElements($container, selector) {
20747
- $container.querySelectorAll(selector).forEach((el) => {
20748
- el.setAttribute("disabled", "");
20749
- });
20750
- }
20751
- function enableElements($container, selector) {
20752
- $container.querySelectorAll(selector).forEach((el) => {
20753
- if (el.hasAttribute("disabled")) el.removeAttribute("disabled");
20754
- });
20755
- }
20756
- /************************************************************
20757
- * Set or reset the color of 'enable submit action' state
20758
- * Color green
20759
- ************************************************************/
20760
- function set_submit_state($container, selector, set) {
20761
- if (set) addClasses($container, selector, "color_submit_state");
20762
- else removeClasses($container, selector, "color_submit_state");
20763
- }
20764
- /************************************************************
20765
- * Set or reset the color of 'enable cancel action' state
20766
- * Color red
20767
- ************************************************************/
20768
- function set_cancel_state($container, selector, set) {
20769
- if (set) addClasses($container, selector, "color_cancel_state");
20770
- else removeClasses($container, selector, "color_cancel_state");
20771
- }
20772
- /************************************************************
20773
- * Set or reset the color of 'enable actived' state
20774
- * Color orange
20775
- ************************************************************/
20776
- function set_active_state($container, selector, set) {
20777
- if (set) addClasses($container, selector, "color_active_state");
20778
- else removeClasses($container, selector, "color_active_state");
20779
- }
20780
- /************************************************************
20781
- * Set or reset the 'pressed' state of a TOGGLE.
20782
- *
20783
- * Not one of the colours above: each of those names a KIND of
20784
- * action (create, pending, history, destructive), and a toggle
20785
- * is not an action -- it is a state the control is IN. So it
20786
- * looks pressed instead of changing category, which also keeps
20787
- * it out of the way of a neighbour wearing the same hue.
20788
- ************************************************************/
20789
- function set_pressed_state($container, selector, set) {
20790
- if (set) addClasses($container, selector, "pressed_state");
20791
- else removeClasses($container, selector, "pressed_state");
20792
- }
20793
- /**
20794
- * Returns a smart stroke color in `rgba()` format based on:
20795
- * - The given fill color (hex or rgb[a])
20796
- * - The current theme ('light' or 'dark')
20797
- * - The adjustment factor (default 0.2)
20798
- *
20799
- * @param {string} fillColor - Fill color string
20800
- * @param {string} theme - 'light' or 'dark'
20801
- * @param {number} factor - Lighten/darken factor (default 0.2)
20802
- * @returns {string} rgba(r, g, b, a)
20803
- */
20804
- function getStrokeColor(fillColor, theme = "light", factor = .2) {
20805
- let r, g, b, a = 1;
20806
- if (fillColor.startsWith("#")) {
20807
- let hex = fillColor.slice(1);
20808
- if (hex.length === 3 || hex.length === 4) hex = hex.split("").map((c) => c + c).join("");
20809
- if (hex.length === 6) {
20810
- r = parseInt(hex.slice(0, 2), 16);
20811
- g = parseInt(hex.slice(2, 4), 16);
20812
- b = parseInt(hex.slice(4, 6), 16);
20813
- } else if (hex.length === 8) {
20814
- r = parseInt(hex.slice(0, 2), 16);
20815
- g = parseInt(hex.slice(2, 4), 16);
20816
- b = parseInt(hex.slice(4, 6), 16);
20817
- a = parseInt(hex.slice(6, 8), 16) / 255;
20818
- } else throw new Error("Unsupported hex format");
20819
- } else if (fillColor.startsWith("rgb")) {
20820
- const match = fillColor.match(/rgba?\(\s*(\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\s*\)/);
20821
- if (!match) throw new Error("Invalid rgb()/rgba() format.");
20822
- r = parseInt(match[1], 10);
20823
- g = parseInt(match[2], 10);
20824
- b = parseInt(match[3], 10);
20825
- if (match[4] !== void 0) a = parseFloat(match[4]);
20826
- } else throw new Error("Unsupported color format.");
20827
- const luminance = (.299 * r + .587 * g + .114 * b) / 255;
20828
- let lighten;
20829
- if (theme === "dark") lighten = luminance < .7;
20830
- else lighten = luminance < .3;
20831
- const adjust = (v) => lighten ? Math.min(255, Math.floor(v + (255 - v) * factor)) : Math.max(0, Math.floor(v * (1 - factor)));
20832
- return `rgba(${adjust(r)}, ${adjust(g)}, ${adjust(b)}, ${a.toFixed(3)})`;
20833
- }
20834
- //#endregion
20835
21142
  //#region src/treedb_node_label.js
20836
21143
  /***********************************************************************
20837
21144
  * treedb_node_label.js
@@ -47699,6 +48006,13 @@ var ICONS = `<svg xmlns="http://www.w3.org/2000/svg">
47699
48006
  stroke="currentColor" stroke-width="40"
47700
48007
  stroke-dasharray="59 36"/>
47701
48008
  </symbol>
48009
+ <!-- location-crosshairs (anchor the camera on one element).
48010
+ The SAME path as the DOM toolbars and their yi-location-crosshairs
48011
+ mask: the two toolbars sit in one console, and a control that
48012
+ does one thing has to be drawn one way. -->
48013
+ <symbol id="g6-icon-anchor" viewBox="0 0 576 512">
48014
+ <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"/>
48015
+ </symbol>
47702
48016
  <!-- floppy-disk (save) -->
47703
48017
  <symbol id="g6-icon-save" viewBox="0 0 448 512">
47704
48018
  <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"/>
@@ -47892,6 +48206,9 @@ var attrs_table$4 = [
47892
48206
  SDATA_END()
47893
48207
  ];
47894
48208
  var PRIVATE_DATA$4 = {
48209
+ anchor_id: "",
48210
+ anchor_state: "off",
48211
+ last_zoom: 1,
47895
48212
  _xy: 100,
47896
48213
  _edge_seq: 0,
47897
48214
  _history_paused: false,
@@ -48188,6 +48505,15 @@ function configure_events(gobj) {
48188
48505
  update_node_icon_position(gobj);
48189
48506
  update_link_icon_position(gobj);
48190
48507
  update_zoom_readout(gobj);
48508
+ let z = priv.last_zoom;
48509
+ try {
48510
+ z = priv.graph.getZoom();
48511
+ } catch (e) {
48512
+ return;
48513
+ }
48514
+ if (z === priv.last_zoom) return;
48515
+ priv.last_zoom = z;
48516
+ if (priv.anchor_state === "on" && priv.anchor_id) yui_graph_center_on(priv.graph, priv.anchor_id);
48191
48517
  });
48192
48518
  priv._on_language_changed = () => {
48193
48519
  update_toolbar(gobj);
@@ -48425,6 +48751,12 @@ function configure_toolbar(gobj) {
48425
48751
  value: "reset",
48426
48752
  className: "EV_ZOOM_RESET",
48427
48753
  title: t("actual size")
48754
+ },
48755
+ {
48756
+ id: "g6-icon-anchor",
48757
+ value: "anchor",
48758
+ className: "EV_TOGGLE_ANCHOR" + (priv.anchor_state !== "off" ? " pressed_state" : "") + (priv.anchor_state === "arming" ? " color_pending_state" : ""),
48759
+ title: anchor_title(priv.anchor_state)
48428
48760
  }
48429
48761
  ];
48430
48762
  if (priv.edit_mode) items.push({
@@ -48466,6 +48798,9 @@ function configure_toolbar(gobj) {
48466
48798
  case "reset":
48467
48799
  gobj_send_event(gobj, "EV_ZOOM_RESET", {}, gobj);
48468
48800
  break;
48801
+ case "anchor":
48802
+ gobj_send_event(gobj, "EV_TOGGLE_ANCHOR", {}, gobj);
48803
+ break;
48469
48804
  case "auto-fit":
48470
48805
  gobj_send_event(gobj, "EV_AUTO_FIT", {}, gobj);
48471
48806
  break;
@@ -49476,12 +49811,12 @@ function graph_find_nodes(gobj, term) {
49476
49811
  async function graph_zoom_in(gobj) {
49477
49812
  let graph = gobj.priv.graph;
49478
49813
  const z = graph.getZoom();
49479
- await graph.zoomTo(z * 1.1);
49814
+ after_camera(gobj, graph.zoomTo(z * 1.1));
49480
49815
  }
49481
49816
  async function graph_zoom_out(gobj) {
49482
49817
  let graph = gobj.priv.graph;
49483
49818
  const z = graph.getZoom();
49484
- await graph.zoomTo(z * .9);
49819
+ after_camera(gobj, graph.zoomTo(z * .9));
49485
49820
  }
49486
49821
  async function graph_render(gobj) {
49487
49822
  await gobj.priv.graph.render();
@@ -52482,7 +52817,56 @@ function ac_zoom_out(gobj, event, kw, src) {
52482
52817
  return 0;
52483
52818
  }
52484
52819
  function ac_zoom_reset(gobj, event, kw, src) {
52485
- gobj.priv.graph.zoomTo(1);
52820
+ let priv = gobj.priv;
52821
+ let graph = priv.graph;
52822
+ if (priv.anchor_state === "on" && priv.anchor_id) after_camera(gobj, graph.zoomTo(1));
52823
+ else graph.zoomTo(1);
52824
+ return 0;
52825
+ }
52826
+ /************************************************************
52827
+ * What the anchor button says it will do next.
52828
+ ************************************************************/
52829
+ function anchor_title(state) {
52830
+ if (state === "arming") return t("click the element to centre on");
52831
+ if (state === "on") return t("centred: click to release");
52832
+ return t("anchor view");
52833
+ }
52834
+ /************************************************************
52835
+ * Every camera action ends the same way once an anchor is
52836
+ * set: the thing you were reading goes back to the middle.
52837
+ *
52838
+ * Chained on the promise G6 returns rather than run beside it
52839
+ * -- centring before the zoom has finished centres the OLD
52840
+ * view, and the reader watches the node slide away and come
52841
+ * back.
52842
+ ************************************************************/
52843
+ function after_camera(gobj, moved) {
52844
+ let priv = gobj.priv;
52845
+ if (priv.anchor_state !== "on" || !priv.anchor_id) return;
52846
+ Promise.resolve(moved).then(function() {
52847
+ yui_graph_center_on(priv.graph, priv.anchor_id);
52848
+ }).catch(function() {});
52849
+ }
52850
+ /************************************************************
52851
+ * Arm the anchor, or let it go.
52852
+ *
52853
+ * Three states and one button, so a press ADVANCES: with no
52854
+ * target it starts waiting for one, and with a target it
52855
+ * releases. Pressing it while it waits is a cancel, which is
52856
+ * what somebody who pressed it by mistake will try.
52857
+ *
52858
+ * Unlike the DOM toolbars, the button is not repainted in
52859
+ * place: this toolbar is a G6 plugin that rebuilds its whole
52860
+ * item list, and the pressed classes are baked into that list.
52861
+ ************************************************************/
52862
+ function ac_toggle_anchor(gobj, event, kw, src) {
52863
+ let priv = gobj.priv;
52864
+ if (priv.anchor_state === "off") priv.anchor_state = "arming";
52865
+ else {
52866
+ priv.anchor_state = "off";
52867
+ priv.anchor_id = "";
52868
+ }
52869
+ update_toolbar(gobj);
52486
52870
  return 0;
52487
52871
  }
52488
52872
  /************************************************************
@@ -52656,6 +53040,13 @@ function ac_node_click$2(gobj, event, kw, src) {
52656
53040
  let priv = gobj.priv;
52657
53041
  let graph = priv.graph;
52658
53042
  let node_id = kw.evt.target.id;
53043
+ if (priv.anchor_state === "arming") {
53044
+ priv.anchor_id = node_id;
53045
+ priv.anchor_state = "on";
53046
+ update_toolbar(gobj);
53047
+ yui_graph_center_on(graph, node_id);
53048
+ return 0;
53049
+ }
52659
53050
  try {
52660
53051
  let nodedata = graph.getNodeData(node_id);
52661
53052
  if (nodedata && nodedata.data && nodedata.data.desc) {
@@ -52917,6 +53308,11 @@ function create_gclass$4(gclass_name) {
52917
53308
  ac_zoom_reset,
52918
53309
  null
52919
53310
  ],
53311
+ [
53312
+ "EV_TOGGLE_ANCHOR",
53313
+ ac_toggle_anchor,
53314
+ null
53315
+ ],
52920
53316
  [
52921
53317
  "EV_TOGGLE_TOOLBARS",
52922
53318
  ac_toggle_toolbars,
@@ -53030,6 +53426,7 @@ function create_gclass$4(gclass_name) {
53030
53426
  ["EV_ZOOM_IN", 0],
53031
53427
  ["EV_ZOOM_OUT", 0],
53032
53428
  ["EV_ZOOM_RESET", 0],
53429
+ ["EV_TOGGLE_ANCHOR", 0],
53033
53430
  ["EV_TOGGLE_TOOLBARS", 0],
53034
53431
  ["EV_AUTO_FIT", 0],
53035
53432
  ["EV_ZOOM_SELECTION", 0],