@yuneta/gobj-ui 7.23.8 → 7.23.10

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.
@@ -5,7 +5,7 @@ import { createJSONEditor } from "vanilla-jsoneditor";
5
5
  import { TabulatorFull } from "tabulator-tables";
6
6
  import * as maplibregl from "maplibre-gl";
7
7
  import uPlot from "uplot";
8
- import { BaseLayout, CanvasEvent, Circle, CommonEvent, DragCanvas, EdgeEvent, ExtensionCategory, Graph, HistoryEvent, NodeEvent, Toolbar, register } from "@antv/g6";
8
+ import { BaseLayout, BrushSelect, CanvasEvent, Circle, CommonEvent, DragCanvas, EdgeEvent, ExtensionCategory, Graph, HistoryEvent, NodeEvent, Toolbar, ZoomCanvas, register } from "@antv/g6";
9
9
  //#region \0rolldown/runtime.js
10
10
  var __create = Object.create;
11
11
  var __defProp = Object.defineProperty;
@@ -9499,6 +9499,275 @@ function yui_graph_refresh_item(gobj, wide) {
9499
9499
  return camera_button(gobj, "yi-arrows-rotate", "EV_REFRESH", "refresh", wide);
9500
9500
  }
9501
9501
  //#endregion
9502
+ //#region src/g6_touch_gestures.js
9503
+ /***********************************************************************
9504
+ * g6_touch_gestures.js
9505
+ *
9506
+ * The two gestures a G6 graph needs on a touch screen and
9507
+ * does not have: PINCH to zoom, and LONG PRESS to open the
9508
+ * context menu.
9509
+ *
9510
+ * Why this exists
9511
+ * ----------------
9512
+ * G6 v5 draws a graph that reads fine on a phone and cannot
9513
+ * be OPERATED on one. Two gaps, both structural:
9514
+ *
9515
+ * 1. `zoom-canvas` binds the WHEEL and nothing else
9516
+ * (`trigger: []`). There is no wheel on a phone, so the
9517
+ * only zoom is the toolbar's +/- buttons. G6 does ship a
9518
+ * pinch recogniser, and `zoom-canvas` will use it -- with
9519
+ * `trigger: ['pinch']`, which its `bindEvents` treats as
9520
+ * an ALTERNATIVE to the wheel (`if pinch ... else wheel`),
9521
+ * so opting in costs the mouse. Worse, its `PinchHandler`
9522
+ * keeps its instance and its callback list in STATICS: on
9523
+ * a page with two graphs the second one registers its
9524
+ * callback against the FIRST one's emitter, so pinching
9525
+ * graph A zooms both and pinching graph B does nothing.
9526
+ * We recognise the gesture ourselves, per graph.
9527
+ *
9528
+ * 2. The context menu is bound to `contextmenu`, and G6 does
9529
+ * not take that from the DOM: its `BehaviorController`
9530
+ * SYNTHESISES it from `pointerdown` with `button === 2`.
9531
+ * A long press therefore never reaches the plugin on ANY
9532
+ * platform, whatever the browser does with the gesture.
9533
+ * So we synthesise it the same way G6 does, from the same
9534
+ * forwarded event object -- which is why the plugin's
9535
+ * `getItems(e)` receives exactly the shape it expects.
9536
+ *
9537
+ * Both are built on ONE per-graph record of the fingers
9538
+ * on the glass, read from the NATIVE touch events -- see
9539
+ * `touch_state_of` for why the pointer stream, which looks
9540
+ * like the right source, is not. That record is also what
9541
+ * tells `drag-canvas` to stop panning while two fingers are
9542
+ * down (`is_pinching`), the guard G6's own drag-canvas has
9543
+ * and our replacement dropped.
9544
+ *
9545
+ * Copyright (c) 2026, ArtGins.
9546
+ * All Rights Reserved.
9547
+ ***********************************************************************/
9548
+ /***************************************************************
9549
+ * Constants
9550
+ ***************************************************************/
9551
+ var LONG_PRESS_MS = 500;
9552
+ var LONG_PRESS_SLOP = 10;
9553
+ /***************************************************************
9554
+ * Per-graph record of the fingers on the glass
9555
+ ***************************************************************/
9556
+ var __touch_state__ = /* @__PURE__ */ new WeakMap();
9557
+ /************************************************************
9558
+ * The touch record of one graph, installing its listeners
9559
+ * the first time it is asked for.
9560
+ *
9561
+ * Read from the NATIVE touch events, not from G6's
9562
+ * forwarded pointer stream.
9563
+ *
9564
+ * The pointer stream looks like the right source and is
9565
+ * not: @antv/g re-issues pointer ids in the middle of a
9566
+ * two-finger gesture (measured: a pinch that started on
9567
+ * ids 2 and 3 finished on id 1), so anything that keys a
9568
+ * Map on `pointerId` loses one of the two fingers halfway
9569
+ * and reads the gesture as a fraction of what it was.
9570
+ * G6's own `PinchHandler` is built exactly that way.
9571
+ *
9572
+ * `event.touches` needs no bookkeeping at all: it IS the
9573
+ * list of fingers currently down, re-stated on every event.
9574
+ ************************************************************/
9575
+ function touch_state_of(graph) {
9576
+ let state = __touch_state__.get(graph);
9577
+ if (state) return state;
9578
+ state = {
9579
+ count: 0,
9580
+ container: null
9581
+ };
9582
+ __touch_state__.set(graph, state);
9583
+ let container = graph.getCanvas?.()?.getContainer?.();
9584
+ if (!container) return state;
9585
+ state.container = container;
9586
+ let read = (event) => {
9587
+ state.count = event.touches ? event.touches.length : 0;
9588
+ };
9589
+ container.addEventListener("touchstart", read, { passive: true });
9590
+ container.addEventListener("touchmove", read, { passive: true });
9591
+ container.addEventListener("touchend", read, { passive: true });
9592
+ container.addEventListener("touchcancel", read, { passive: true });
9593
+ return state;
9594
+ }
9595
+ /************************************************************
9596
+ * Is a two-finger gesture in progress on this graph?
9597
+ *
9598
+ * Read by our `drag-canvas` replacement: panning and zooming
9599
+ * the same drag at the same time fights the user for the
9600
+ * camera.
9601
+ ************************************************************/
9602
+ function is_pinching(graph) {
9603
+ if (!graph) return false;
9604
+ let state = __touch_state__.get(graph);
9605
+ if (!state) return false;
9606
+ return state.count >= 2;
9607
+ }
9608
+ /***************************
9609
+ * Pinch to zoom
9610
+ ***************************/
9611
+ /************************************************************
9612
+ * `zoom-canvas` that also answers the pinch.
9613
+ *
9614
+ * The base class keeps the wheel; the pinch is added on top
9615
+ * instead of replacing it, so one build serves the mouse and
9616
+ * the finger. The scale is ABSOLUTE against the zoom the
9617
+ * gesture started at -- G6's own pinch feeds `zoom()` an
9618
+ * increment per frame, which drifts away from the fingers
9619
+ * over a long gesture.
9620
+ ************************************************************/
9621
+ var ZoomCanvasPinch = class extends ZoomCanvas {
9622
+ constructor(context, options) {
9623
+ super(context, options);
9624
+ this._pinch_dist0 = 0;
9625
+ this._pinch_zoom0 = 1;
9626
+ this._on_touch_move = null;
9627
+ let { trigger } = this.options;
9628
+ if (Array.isArray(trigger) && trigger.includes(CommonEvent.PINCH)) return;
9629
+ let graph = this.context.graph;
9630
+ let state = touch_state_of(graph);
9631
+ if (!state.container) return;
9632
+ this._on_touch_move = (event) => {
9633
+ let touches = event.touches;
9634
+ if (!touches || touches.length !== 2) {
9635
+ this._pinch_dist0 = 0;
9636
+ return;
9637
+ }
9638
+ let dx = touches[0].clientX - touches[1].clientX;
9639
+ let dy = touches[0].clientY - touches[1].clientY;
9640
+ let dist = Math.sqrt(dx * dx + dy * dy);
9641
+ if (dist <= 0) return;
9642
+ if (this._pinch_dist0 === 0) {
9643
+ this._pinch_dist0 = dist;
9644
+ this._pinch_zoom0 = graph.getZoom();
9645
+ return;
9646
+ }
9647
+ this._pinch_to(dist / this._pinch_dist0, touches, event);
9648
+ };
9649
+ state.container.addEventListener("touchmove", this._on_touch_move, { passive: true });
9650
+ }
9651
+ /************************************************************
9652
+ * Zoom to `scale` times the zoom the pinch started at,
9653
+ * about the midpoint between the two fingers.
9654
+ *
9655
+ * ABSOLUTE against the start of the gesture, where G6's
9656
+ * own pinch feeds `zoom()` an increment per frame: an
9657
+ * increment drifts away from the fingers over a long
9658
+ * gesture, and cannot come back when they close again.
9659
+ ************************************************************/
9660
+ _pinch_to(scale, touches, event) {
9661
+ if (!this.validate(event)) return;
9662
+ let graph = this.context.graph;
9663
+ let state = touch_state_of(graph);
9664
+ let origin = null;
9665
+ let rect = state.container?.getBoundingClientRect();
9666
+ if (rect) origin = [(touches[0].clientX + touches[1].clientX) / 2 - rect.left, (touches[0].clientY + touches[1].clientY) / 2 - rect.top];
9667
+ graph.zoomTo(this._pinch_zoom0 * scale, false, origin);
9668
+ this.options.onFinish?.();
9669
+ }
9670
+ destroy() {
9671
+ if (this._on_touch_move) {
9672
+ __touch_state__.get(this.context.graph)?.container?.removeEventListener("touchmove", this._on_touch_move);
9673
+ this._on_touch_move = null;
9674
+ }
9675
+ super.destroy();
9676
+ }
9677
+ };
9678
+ /************************************************************
9679
+ * Register once, over the built-in `zoom-canvas`, so every
9680
+ * graph (tree, json, treedb, editor) pinches without
9681
+ * touching its behaviors list.
9682
+ ************************************************************/
9683
+ var __zoom_canvas_patched__ = false;
9684
+ function ensure_pinch_zoom_patch() {
9685
+ if (__zoom_canvas_patched__) return;
9686
+ register(ExtensionCategory.BEHAVIOR, "zoom-canvas", ZoomCanvasPinch);
9687
+ __zoom_canvas_patched__ = true;
9688
+ }
9689
+ /***************************
9690
+ * Long press
9691
+ ***************************/
9692
+ /************************************************************
9693
+ * Make a long press open the context menu.
9694
+ *
9695
+ * Returns the function that uninstalls it.
9696
+ *
9697
+ * The menu the press opens is whatever the `contextmenu`
9698
+ * plugin builds: we re-emit G6's own forwarded event under
9699
+ * the name the plugin listens for, so `getItems(e)` and
9700
+ * `enable(e)` see the same object a right-click would give
9701
+ * them -- same `target`, same `targetType`, same `client`.
9702
+ ************************************************************/
9703
+ function install_long_press_contextmenu(graph) {
9704
+ if (!graph) return () => {};
9705
+ let state = touch_state_of(graph);
9706
+ let pending = null;
9707
+ let cancel = () => {
9708
+ if (pending) {
9709
+ clearTimeout(pending.timer);
9710
+ pending = null;
9711
+ }
9712
+ };
9713
+ let fire = (event) => {
9714
+ pending = null;
9715
+ let swallow = (evt) => {
9716
+ evt.stopPropagation();
9717
+ document.removeEventListener("click", swallow, true);
9718
+ };
9719
+ document.addEventListener("click", swallow, true);
9720
+ setTimeout(() => {
9721
+ document.removeEventListener("click", swallow, true);
9722
+ }, 1e3);
9723
+ let container = graph.getCanvas?.()?.getContainer?.();
9724
+ if (container) container.addEventListener("contextmenu", (evt) => {
9725
+ evt.preventDefault();
9726
+ }, {
9727
+ once: true,
9728
+ capture: true
9729
+ });
9730
+ graph.emit(`${event.targetType}:${CommonEvent.CONTEXT_MENU}`, event);
9731
+ graph.emit(CommonEvent.CONTEXT_MENU, event);
9732
+ };
9733
+ let on_down = (event) => {
9734
+ cancel();
9735
+ if (event.pointerType !== "touch") return;
9736
+ if (state.count > 1) return;
9737
+ let x = event.client?.x;
9738
+ let y = event.client?.y;
9739
+ if (x === void 0 || y === void 0) return;
9740
+ let frozen = Object.assign({}, event);
9741
+ pending = {
9742
+ x,
9743
+ y,
9744
+ timer: setTimeout(() => {
9745
+ fire(frozen);
9746
+ }, LONG_PRESS_MS)
9747
+ };
9748
+ };
9749
+ let on_move = (event) => {
9750
+ if (!pending) return;
9751
+ if (state.count > 1) {
9752
+ cancel();
9753
+ return;
9754
+ }
9755
+ let x = event.client?.x;
9756
+ let y = event.client?.y;
9757
+ if (x === void 0 || y === void 0) return;
9758
+ if (Math.abs(x - pending.x) > LONG_PRESS_SLOP || Math.abs(y - pending.y) > LONG_PRESS_SLOP) cancel();
9759
+ };
9760
+ graph.on(CommonEvent.POINTER_DOWN, on_down);
9761
+ graph.on(CommonEvent.POINTER_MOVE, on_move);
9762
+ graph.on(CommonEvent.POINTER_UP, cancel);
9763
+ return () => {
9764
+ cancel();
9765
+ graph.off(CommonEvent.POINTER_DOWN, on_down);
9766
+ graph.off(CommonEvent.POINTER_MOVE, on_move);
9767
+ graph.off(CommonEvent.POINTER_UP, cancel);
9768
+ };
9769
+ }
9770
+ //#endregion
9502
9771
  //#region src/g6_drag_canvas_touch.js
9503
9772
  /***********************************************************************
9504
9773
  * g6_drag_canvas_touch.js
@@ -9543,6 +9812,12 @@ function yui_graph_refresh_item(gobj, wide) {
9543
9812
  * every graph (tree, json, treedb, editor) is fixed without
9544
9813
  * touching each consumer's behaviors list.
9545
9814
  *
9815
+ * It also has to keep the ONE guard the base class has and
9816
+ * this replacement dropped: G6's `onDrag` refuses to pan
9817
+ * while a pinch is in progress. See `is_pinching` in
9818
+ * `g6_touch_gestures.js`, which is where the pinch that
9819
+ * matters is now recognised.
9820
+ *
9546
9821
  * Copyright (c) 2026, ArtGins.
9547
9822
  * All Rights Reserved.
9548
9823
  ***********************************************************************/
@@ -9558,6 +9833,10 @@ var DragCanvasTouch = class extends DragCanvas {
9558
9833
  this._lastVP = null;
9559
9834
  return;
9560
9835
  }
9836
+ if (is_pinching(this.context.graph)) {
9837
+ this._lastVP = null;
9838
+ return;
9839
+ }
9561
9840
  let vp = event.viewport;
9562
9841
  if (!vp || typeof vp.x !== "number" || typeof vp.y !== "number") return;
9563
9842
  if (this._lastVP === null) {
@@ -9787,6 +10066,7 @@ function mt_create$11(gobj) {
9787
10066
  priv.theme_observer = yui_watch_theme(gobj);
9788
10067
  build_ui$10(gobj);
9789
10068
  ensure_drag_canvas_patch();
10069
+ ensure_pinch_zoom_patch();
9790
10070
  if (!__layout_registered__$1) {
9791
10071
  register(ExtensionCategory.LAYOUT, "json-tree-layout", JsonTreeLayout);
9792
10072
  __layout_registered__$1 = true;
@@ -12415,6 +12695,7 @@ function mt_create$9(gobj) {
12415
12695
  priv.theme_observer = yui_watch_theme(gobj);
12416
12696
  build_ui$8(gobj);
12417
12697
  ensure_drag_canvas_patch();
12698
+ ensure_pinch_zoom_patch();
12418
12699
  if (!__layout_registered__) {
12419
12700
  register(ExtensionCategory.LAYOUT, "gobj-tree-v", GobjTreeVLayout);
12420
12701
  register(ExtensionCategory.LAYOUT, "gobj-tree-h", GobjTreeHLayout);
@@ -47185,6 +47466,20 @@ var ICONS = `<svg xmlns="http://www.w3.org/2000/svg">
47185
47466
  <symbol id="g6-icon-plus" viewBox="0 0 512 512">
47186
47467
  <path fill="currentColor" d="M316 32 L316 196 L480 196 L480 316 L316 316 L316 480 L196 480 L196 316 L32 316 L32 196 L196 196 L196 32 Z"/>
47187
47468
  </symbol>
47469
+ <!-- ellipsis-vertical (unfold the floating toolbar).
47470
+ The strip it stands for is a vertical stack, and a vertical
47471
+ ellipsis is what "there is more of this here" is drawn as
47472
+ everywhere else. -->
47473
+ <symbol id="g6-icon-toolbar-show" viewBox="0 0 128 512">
47474
+ <path fill="currentColor" d="M64 360a56 56 0 1 0 0 112 56 56 0 1 0 0-112zm0-160a56 56 0 1 0 0 112 56 56 0 1 0 0-112zM120 96A56 56 0 1 0 8 96a56 56 0 1 0 112 0z"/>
47475
+ </symbol>
47476
+ <!-- xmark (fold the floating toolbar away).
47477
+ Not a chevron: the two toolbars sit on OPPOSITE edges, so a
47478
+ direction would point the wrong way on one of them. What the
47479
+ button does is close an overlay, and that is an x. -->
47480
+ <symbol id="g6-icon-toolbar-hide" viewBox="0 0 384 512">
47481
+ <path fill="currentColor" d="M342.6 150.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L192 210.7 86.6 105.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L146.7 256 41.4 361.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192 301.3 297.4 406.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L237.3 256 342.6 150.6z"/>
47482
+ </symbol>
47188
47483
  <!-- floppy-disk (save) -->
47189
47484
  <symbol id="g6-icon-save" viewBox="0 0 448 512">
47190
47485
  <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"/>
@@ -47199,6 +47494,58 @@ function inject_svg_icons() {
47199
47494
  document.body.appendChild(div);
47200
47495
  }
47201
47496
  //#endregion
47497
+ //#region src/g6_brush_select_owned.js
47498
+ /***********************************************************************
47499
+ * g6_brush_select_owned.js
47500
+ *
47501
+ * The rubber band, for a graph whose FSM owns the selection.
47502
+ *
47503
+ * Why this exists
47504
+ * ----------------
47505
+ * G6 v5 `brush-select` does two things: it draws the band and
47506
+ * it keeps the `selected` element state. The second one is
47507
+ * not ours to give away -- `C_G6_NODES_TREE` owns the
47508
+ * selection, sets it in `set_selection()` and clears it in
47509
+ * `deselect_node()`, both with the history recorder paused,
47510
+ * because a selection is not an edit.
47511
+ *
47512
+ * The built-in disagrees on one path: on EVERY canvas click
47513
+ * it calls `clearElementsStates()`, which writes the state of
47514
+ * every node and every edge of the graph -- whether or not
47515
+ * any of them carried it -- outside our pause. G6's history
47516
+ * then records the redraw as a command whose `original` and
47517
+ * `current` are identical: an undo entry that undoes nothing,
47518
+ * and a Save button lit on a graph nobody has touched. One
47519
+ * click on the background was enough.
47520
+ *
47521
+ * So this subclass drops that half and keeps the band. The
47522
+ * selection still clears on a canvas click -- through
47523
+ * `EV_CANVAS_CLICK`, where the gclass has always cleared it.
47524
+ *
47525
+ * Registered under its OWN id: the built-in `brush-select` is
47526
+ * right for a graph that does not own its selection, and no
47527
+ * other consumer of this library asked for this.
47528
+ *
47529
+ * Copyright (c) 2026, ArtGins.
47530
+ * All Rights Reserved.
47531
+ ***********************************************************************/
47532
+ /************************************************************
47533
+ * Same band, no state bookkeeping behind the gclass's back
47534
+ ************************************************************/
47535
+ var BrushSelectOwned = class extends BrushSelect {
47536
+ clearElementsStates() {}
47537
+ };
47538
+ /************************************************************
47539
+ * Register once, under our own id
47540
+ ************************************************************/
47541
+ var BRUSH_SELECT_OWNED = "yui-brush-select";
47542
+ var __brush_select_registered__ = false;
47543
+ function ensure_brush_select_owned() {
47544
+ if (__brush_select_registered__) return;
47545
+ register(ExtensionCategory.BEHAVIOR, BRUSH_SELECT_OWNED, BrushSelectOwned);
47546
+ __brush_select_registered__ = true;
47547
+ }
47548
+ //#endregion
47202
47549
  //#region src/c_g6_nodes_tree.js
47203
47550
  /***********************************************************************
47204
47551
  * c_g6_nodes_tree.js
@@ -47257,6 +47604,8 @@ var YuiToolbar = class extends Toolbar {
47257
47604
  };
47258
47605
  register(ExtensionCategory.PLUGIN, "yui-toolbar", YuiToolbar);
47259
47606
  ensure_drag_canvas_patch();
47607
+ ensure_brush_select_owned();
47608
+ ensure_pinch_zoom_patch();
47260
47609
  /***************************************************************
47261
47610
  * Constants
47262
47611
  ***************************************************************/
@@ -47376,6 +47725,9 @@ var PRIVATE_DATA$4 = {
47376
47725
  _focus_topic: null,
47377
47726
  _focus_ids: [],
47378
47727
  _on_pointerdown_focus: null,
47728
+ _uninstall_long_press: null,
47729
+ toolbar_collapsed: true,
47730
+ _toolbars_could_collapse: null,
47379
47731
  _on_focusout_restore: null,
47380
47732
  _selected_paint_ids: [],
47381
47733
  _pending_focus_topic: null,
@@ -47461,6 +47813,10 @@ function mt_destroy$4(gobj) {
47461
47813
  cancelAnimationFrame(priv._resize_raf);
47462
47814
  priv._resize_raf = 0;
47463
47815
  }
47816
+ if (priv._uninstall_long_press) {
47817
+ priv._uninstall_long_press();
47818
+ priv._uninstall_long_press = null;
47819
+ }
47464
47820
  if (priv.graph) {
47465
47821
  priv.graph.destroy();
47466
47822
  priv.graph = null;
@@ -47675,6 +48031,8 @@ function configure_plugins(gobj) {
47675
48031
  return e.targetType === "node" || e.targetType === "edge";
47676
48032
  }
47677
48033
  });
48034
+ if (priv._uninstall_long_press) priv._uninstall_long_press();
48035
+ priv._uninstall_long_press = install_long_press_contextmenu(priv.graph);
47678
48036
  if (gobj_read_bool_attr(gobj, "with_toolbar")) {
47679
48037
  configure_toolbar(gobj);
47680
48038
  configure_toolbar_edit(gobj);
@@ -47704,6 +48062,55 @@ function update_toolbar(gobj) {
47704
48062
  * flipping now; the fallbacks are the old forced-light values, so
47705
48063
  * a host without Bulma gets exactly what it had.
47706
48064
  ************************************************************/
48065
+ /************************************************************
48066
+ * Is the canvas too narrow to give both floating toolbars a
48067
+ * strip of it?
48068
+ *
48069
+ * They are drawn INSIDE the canvas, one on each edge. On a
48070
+ * desktop that is a corner; on a 356px-wide phone canvas the
48071
+ * two of them take a third of the drawing area and stand on
48072
+ * top of the nodes -- and a node under a toolbar cannot be
48073
+ * read, tapped or dragged out from under it.
48074
+ *
48075
+ * Measured on the CONTAINER, not on the window: the same
48076
+ * graph is a full page in one app and a card in a column in
48077
+ * another, and it is the box it actually got that decides.
48078
+ ************************************************************/
48079
+ var TOOLBAR_COLLAPSE_WIDTH = 480;
48080
+ function toolbars_can_collapse(gobj) {
48081
+ let $container = gobj.priv.$container;
48082
+ if (!$container) return false;
48083
+ let width = $container.getBoundingClientRect().width;
48084
+ if (!width) return false;
48085
+ return width < TOOLBAR_COLLAPSE_WIDTH;
48086
+ }
48087
+ /************************************************************
48088
+ * Are they folded RIGHT NOW? Wide enough and the question
48089
+ * does not arise -- the desktop toolbar never grows a button
48090
+ * it does not need.
48091
+ ************************************************************/
48092
+ function toolbars_collapsed(gobj) {
48093
+ if (!toolbars_can_collapse(gobj)) return false;
48094
+ return gobj.priv.toolbar_collapsed !== false;
48095
+ }
48096
+ /************************************************************
48097
+ * The one item a folded toolbar shows, and the one an
48098
+ * unfolded narrow toolbar adds to fold itself again.
48099
+ ************************************************************/
48100
+ function toolbar_fold_item(gobj) {
48101
+ if (toolbars_collapsed(gobj)) return {
48102
+ id: "g6-icon-toolbar-show",
48103
+ value: "toolbar-toggle",
48104
+ className: "EV_TOGGLE_TOOLBARS",
48105
+ title: t("show toolbar")
48106
+ };
48107
+ return {
48108
+ id: "g6-icon-toolbar-hide",
48109
+ value: "toolbar-toggle",
48110
+ className: "EV_TOGGLE_TOOLBARS",
48111
+ title: t("hide toolbar")
48112
+ };
48113
+ }
47707
48114
  function toolbar_style(extra) {
47708
48115
  return Object.assign({
47709
48116
  backgroundColor: "var(--bulma-scheme-main, #f5f5f5)",
@@ -47761,6 +48168,7 @@ function configure_toolbar(gobj) {
47761
48168
  marginLeft: "12px"
47762
48169
  }),
47763
48170
  getItems: () => {
48171
+ if (toolbars_collapsed(gobj)) return [toolbar_fold_item(gobj)];
47764
48172
  let items = [
47765
48173
  {
47766
48174
  id: "g6-icon-zoom-in",
@@ -47816,6 +48224,10 @@ function configure_toolbar(gobj) {
47816
48224
  title: t("enter full screen")
47817
48225
  });
47818
48226
  }
48227
+ if (toolbars_can_collapse(gobj)) {
48228
+ items.push({ separator: true });
48229
+ items.push(toolbar_fold_item(gobj));
48230
+ }
47819
48231
  return items;
47820
48232
  },
47821
48233
  onClick: (value) => {
@@ -47841,7 +48253,10 @@ function configure_toolbar(gobj) {
47841
48253
  case "request-fullscreen":
47842
48254
  gobj_send_event(gobj, "EV_REQUEST_FULLSCREEN", {}, gobj);
47843
48255
  break;
47844
- case "exit-fullscreen": gobj_send_event(gobj, "EV_EXIT_FULLSCREEN", {}, gobj);
48256
+ case "exit-fullscreen":
48257
+ gobj_send_event(gobj, "EV_EXIT_FULLSCREEN", {}, gobj);
48258
+ break;
48259
+ case "toolbar-toggle": gobj_send_event(gobj, "EV_TOGGLE_TOOLBARS", {}, gobj);
47845
48260
  }
47846
48261
  }
47847
48262
  });
@@ -47851,7 +48266,7 @@ function configure_toolbar(gobj) {
47851
48266
  * Contains edit-mode buttons: Undo, Redo, Save
47852
48267
  ************************************************************/
47853
48268
  function configure_toolbar_edit(gobj) {
47854
- if (!gobj.priv.edit_mode) {
48269
+ if (!gobj.priv.edit_mode || toolbars_collapsed(gobj)) {
47855
48270
  graph_remove_plugin(gobj, "toolbar-edit");
47856
48271
  return;
47857
48272
  }
@@ -47940,12 +48355,16 @@ function configure_behaviour(gobj) {
47940
48355
  {
47941
48356
  type: "drag-canvas",
47942
48357
  key: "drag-canvas",
47943
- enable: (event) => !event.shiftKey
48358
+ enable: (event) => {
48359
+ if (event.shiftKey) return false;
48360
+ if ("targetType" in event) return event.targetType === "canvas";
48361
+ return true;
48362
+ }
47944
48363
  },
47945
48364
  "zoom-canvas",
47946
48365
  "drag-element",
47947
48366
  {
47948
- type: "brush-select",
48367
+ type: BRUSH_SELECT_OWNED,
47949
48368
  key: "brush-select",
47950
48369
  trigger: ["shift"],
47951
48370
  enableElements: ["node"],
@@ -47959,6 +48378,7 @@ function configure_behaviour(gobj) {
47959
48378
  break;
47960
48379
  case "operation":
47961
48380
  priv.edit_mode = false;
48381
+ behaviors = ["drag-canvas", "zoom-canvas"];
47962
48382
  break;
47963
48383
  default: log_error(`operation mode unknown: ${operation_mode}`);
47964
48384
  }
@@ -48528,6 +48948,30 @@ function history_resume(gobj) {
48528
48948
  gobj.priv._history_paused = false;
48529
48949
  }
48530
48950
  /************************************************************
48951
+ * The history plugin exists while the graph is in EDITION,
48952
+ * and only there.
48953
+ *
48954
+ * It used to be installed at one MOMENT -- the arrival of the
48955
+ * last topic of the load -- and only if the graph happened to
48956
+ * be in edition right then. Entering edition afterwards, which
48957
+ * is the ordinary way in (the graph opens in reading and the
48958
+ * mode selector is next to it), left no history at all: Undo
48959
+ * and Redo were dead buttons that never even lit, `history_
48960
+ * pause()` had no `beforeAddCommand` to answer, and the Save
48961
+ * button was told the graph was dirty by `mark_graph_dirty()`
48962
+ * alone -- which is why a move offered Save without Undo.
48963
+ *
48964
+ * So it follows the MODE, and every place that changes the
48965
+ * mode calls this.
48966
+ ************************************************************/
48967
+ function sync_history_plugin(gobj) {
48968
+ let priv = gobj.priv;
48969
+ if (!priv.graph || !priv.graph_rendered) return;
48970
+ if (priv.edit_mode) graph_add_plugin(gobj, "history");
48971
+ else graph_remove_plugin(gobj, "history");
48972
+ update_history_buttons(gobj);
48973
+ }
48974
+ /************************************************************
48531
48975
  *
48532
48976
  ************************************************************/
48533
48977
  function update_history_buttons(gobj) {
@@ -48818,6 +49262,7 @@ function graph_add_plugin(gobj, plugin_key, options) {
48818
49262
  let priv = gobj.priv;
48819
49263
  let graph = priv.graph;
48820
49264
  let plugin = graph_get_plugin(gobj, plugin_key);
49265
+ let existed = !!plugin;
48821
49266
  if (!plugin) {
48822
49267
  let plugin_def = {
48823
49268
  key: plugin_key,
@@ -48828,7 +49273,7 @@ function graph_add_plugin(gobj, plugin_key, options) {
48828
49273
  }
48829
49274
  plugin = graph_get_plugin(gobj, plugin_key);
48830
49275
  switch (plugin_key) {
48831
- case "history": if (plugin) {
49276
+ case "history": if (plugin && !existed) {
48832
49277
  plugin.on(HistoryEvent.CHANGE, () => {
48833
49278
  update_history_buttons(gobj);
48834
49279
  });
@@ -48921,6 +49366,14 @@ var ManualLayout = class extends BaseLayout {
48921
49366
  ************************************************************/
48922
49367
  var RESIZE_HANDLE_SIZE = 8;
48923
49368
  var RESIZE_MIN_VP = 20;
49369
+ var RESIZE_HANDLE_VISUAL_COARSE = 14;
49370
+ var RESIZE_HANDLE_HIT_COARSE = 44;
49371
+ function resize_handle_visual() {
49372
+ return coarse_pointer() ? RESIZE_HANDLE_VISUAL_COARSE : RESIZE_HANDLE_SIZE;
49373
+ }
49374
+ function resize_handle_hit() {
49375
+ return coarse_pointer() ? RESIZE_HANDLE_HIT_COARSE : RESIZE_HANDLE_SIZE;
49376
+ }
48924
49377
  /************************************************************
48925
49378
  * SVG icons for floating action buttons (28×28 circles).
48926
49379
  * Each value is a raw SVG string sized 18×18 in a 24×24 viewBox.
@@ -48933,7 +49386,50 @@ var SVG_ICONS = {
48933
49386
  plus: "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"12\" y1=\"5\" x2=\"12\" y2=\"19\"/><line x1=\"5\" y1=\"12\" x2=\"19\" y2=\"12\"/></svg>"
48934
49387
  };
48935
49388
  /************************************************************
48936
- * Create a 28×28 floating circular icon button.
49389
+ * Is the primary pointer a FINGER?
49390
+ *
49391
+ * Everything this gclass draws over the canvas is sized from
49392
+ * this answer. A mouse is a pixel and can hit an 8px square;
49393
+ * a fingertip covers about 40, and the overlay it lands on
49394
+ * decides between "open the properties" and "delete the node".
49395
+ *
49396
+ * Asked per call, not cached: a tablet with a keyboard dock
49397
+ * changes the answer without reloading the page.
49398
+ ************************************************************/
49399
+ function coarse_pointer() {
49400
+ if (typeof window.matchMedia !== "function") return false;
49401
+ try {
49402
+ return window.matchMedia("(pointer: coarse)").matches === true;
49403
+ } catch (e) {
49404
+ return false;
49405
+ }
49406
+ }
49407
+ /************************************************************
49408
+ * The size of the round floating buttons (properties,
49409
+ * delete, link), and the GAP between two of them.
49410
+ *
49411
+ * The gap matters as much as the size: `node properties` and
49412
+ * `delete node` are drawn one under the other, and at 28px
49413
+ * with 4px between them a fingertip covers both -- with the
49414
+ * destructive one underneath.
49415
+ ************************************************************/
49416
+ function floating_icon_size() {
49417
+ return coarse_pointer() ? 44 : 28;
49418
+ }
49419
+ function floating_icon_gap() {
49420
+ return coarse_pointer() ? 12 : 4;
49421
+ }
49422
+ function floating_icon_dy() {
49423
+ return -floating_icon_size() / 2;
49424
+ }
49425
+ function floating_icon_step() {
49426
+ return floating_icon_size() + floating_icon_gap();
49427
+ }
49428
+ function floating_popover_dx() {
49429
+ return 4 + floating_icon_size() + 4;
49430
+ }
49431
+ /************************************************************
49432
+ * Create a floating circular icon button.
48937
49433
  * @param {string} svgKey - key into SVG_ICONS
48938
49434
  * @param {string} color - border & text color (e.g. '#1890ff')
48939
49435
  * @param {number} left - CSS left in px
@@ -48943,10 +49439,18 @@ var SVG_ICONS = {
48943
49439
  * @returns {HTMLElement}
48944
49440
  ************************************************************/
48945
49441
  function create_floating_icon(svgKey, color, left, top, title, onClick) {
49442
+ const size = floating_icon_size();
48946
49443
  const el = document.createElement("div");
48947
49444
  el.title = title;
49445
+ el.setAttribute("aria-label", title);
48948
49446
  el.innerHTML = SVG_ICONS[svgKey] || "";
48949
- el.style.cssText = "position:absolute;left:" + left + "px;top:" + top + "px;width:28px;height:28px;display:flex;align-items:center;justify-content:center;background:#fff;border:1px solid " + color + ";border-radius:50%;cursor:pointer;pointer-events:all;z-index:11;box-shadow:0 2px 6px rgba(0,0,0,0.15);color:" + color + ";";
49447
+ el.style.cssText = "position:absolute;left:" + left + "px;top:" + top + "px;width:" + size + "px;height:" + size + "px;display:flex;align-items:center;justify-content:center;background:#fff;border:1px solid " + color + ";border-radius:50%;cursor:pointer;pointer-events:all;z-index:11;touch-action:none;box-shadow:0 2px 6px rgba(0,0,0,0.15);color:" + color + ";";
49448
+ const $svg = el.querySelector("svg");
49449
+ if ($svg) {
49450
+ const glyph = Math.round(size * .5);
49451
+ $svg.setAttribute("width", String(glyph));
49452
+ $svg.setAttribute("height", String(glyph));
49453
+ }
48950
49454
  el.addEventListener("click", (e) => {
48951
49455
  e.stopPropagation();
48952
49456
  onClick();
@@ -48992,7 +49496,7 @@ function show_dual_icons(gobj, x, y, gear_title, gear_click, trash_title, trash_
48992
49496
  let priv = gobj.priv;
48993
49497
  const icon_el = create_floating_icon("gear", "#1890ff", x, y, gear_title, gear_click);
48994
49498
  priv.$container.appendChild(icon_el);
48995
- const delete_el = create_floating_icon("trash", "#ff4d4f", x, y + 32, trash_title, trash_click);
49499
+ const delete_el = create_floating_icon("trash", "#ff4d4f", x, y + floating_icon_step(), trash_title, trash_click);
48996
49500
  priv.$container.appendChild(delete_el);
48997
49501
  return {
48998
49502
  icon_el,
@@ -49010,6 +49514,21 @@ function create_popover_base(left, top, className, borderColor, minWidth) {
49010
49514
  popover.addEventListener("pointerdown", (e) => e.stopPropagation());
49011
49515
  return popover;
49012
49516
  }
49517
+ /************************************************************
49518
+ * Extra CSS every control inside a popover carries on a
49519
+ * touch screen.
49520
+ *
49521
+ * A popover is where the graph is TYPED into -- a colour, a
49522
+ * radius, an id -- and its controls were sized by their
49523
+ * padding alone, which lands them at 25-30px. A finger needs
49524
+ * 44, and the number is not ours: it is the floor both Apple
49525
+ * and Google publish, and the one every native control on the
49526
+ * device already meets.
49527
+ ************************************************************/
49528
+ function touch_control_css() {
49529
+ if (!coarse_pointer()) return "";
49530
+ return "min-height:44px;font-size:16px;";
49531
+ }
49013
49532
  function create_form_label(parent, text) {
49014
49533
  let label = document.createElement("label");
49015
49534
  label.textContent = t(text);
@@ -49021,7 +49540,7 @@ function create_form_color_input(parent, value, onInput) {
49021
49540
  let input = document.createElement("input");
49022
49541
  input.type = "color";
49023
49542
  input.value = value;
49024
- input.style.cssText = "width:100%;height:30px;padding:0;border:1px solid var(--bulma-border-weak, #d9d9d9);border-radius:4px;cursor:pointer;margin-bottom:10px;";
49543
+ input.style.cssText = "width:100%;height:30px;padding:0;border:1px solid var(--bulma-border-weak, #d9d9d9);border-radius:4px;cursor:pointer;margin-bottom:10px;" + touch_control_css();
49025
49544
  if (onInput) input.addEventListener("input", onInput);
49026
49545
  parent.appendChild(input);
49027
49546
  return input;
@@ -49032,14 +49551,14 @@ function create_form_number_input(parent, value, min, max, onInput) {
49032
49551
  input.min = String(min);
49033
49552
  input.max = String(max);
49034
49553
  input.value = value;
49035
- input.style.cssText = "width:100%;padding:4px 6px;background:var(--bulma-scheme-main-bis, #fff);color:var(--bulma-text-strong, #333);border:1px solid var(--bulma-border-weak, #d9d9d9);border-radius:4px;box-sizing:border-box;margin-bottom:10px;";
49554
+ input.style.cssText = "width:100%;padding:4px 6px;background:var(--bulma-scheme-main-bis, #fff);color:var(--bulma-text-strong, #333);border:1px solid var(--bulma-border-weak, #d9d9d9);border-radius:4px;box-sizing:border-box;margin-bottom:10px;" + touch_control_css();
49036
49555
  if (onInput) input.addEventListener("input", onInput);
49037
49556
  parent.appendChild(input);
49038
49557
  return input;
49039
49558
  }
49040
49559
  function create_form_select(parent, options, marginBottom) {
49041
49560
  let select = document.createElement("select");
49042
- select.style.cssText = "width:100%;padding:4px 6px;background:var(--bulma-scheme-main-bis, #fff);color:var(--bulma-text-strong, #333);border:1px solid var(--bulma-border-weak, #d9d9d9);border-radius:4px;box-sizing:border-box;margin-bottom:" + (marginBottom || "12px") + ";";
49561
+ select.style.cssText = "width:100%;padding:4px 6px;background:var(--bulma-scheme-main-bis, #fff);color:var(--bulma-text-strong, #333);border:1px solid var(--bulma-border-weak, #d9d9d9);border-radius:4px;box-sizing:border-box;margin-bottom:" + (marginBottom || "12px") + ";" + touch_control_css();
49043
49562
  for (let opt of options) {
49044
49563
  let o = document.createElement("option");
49045
49564
  o.value = opt.value;
@@ -49056,7 +49575,7 @@ function create_form_button_row(parent, buttons) {
49056
49575
  for (let { text, style, onClick } of buttons) {
49057
49576
  let btn = document.createElement("button");
49058
49577
  btn.textContent = t(text);
49059
- btn.style.cssText = style;
49578
+ btn.style.cssText = style + touch_control_css();
49060
49579
  btn.addEventListener("click", (e) => {
49061
49580
  e.stopPropagation();
49062
49581
  onClick();
@@ -49294,7 +49813,7 @@ function show_resize_handles(gobj) {
49294
49813
  container.style.cssText = "position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:10;";
49295
49814
  const selRect = document.createElement("div");
49296
49815
  container.appendChild(selRect);
49297
- const handle_defs = [
49816
+ let handle_defs = [
49298
49817
  {
49299
49818
  cursor: "nw-resize",
49300
49819
  mx: -1,
@@ -49336,10 +49855,16 @@ function show_resize_handles(gobj) {
49336
49855
  my: 1
49337
49856
  }
49338
49857
  ];
49858
+ const hit = resize_handle_hit();
49859
+ const visual = resize_handle_visual();
49860
+ if (coarse_pointer()) handle_defs = handle_defs.filter((def) => def.mx !== 0 && def.my !== 0);
49339
49861
  const handles = [];
49340
49862
  for (const def of handle_defs) {
49341
49863
  const el = document.createElement("div");
49342
- el.style.cssText = "position:absolute;width:8px;height:8px;background:#fff;border:1px solid #1890ff;cursor:" + def.cursor + ";pointer-events:all;box-sizing:border-box;";
49864
+ el.style.cssText = "position:absolute;width:" + hit + "px;height:" + hit + "px;display:flex;align-items:center;justify-content:center;cursor:" + def.cursor + ";pointer-events:all;touch-action:none;box-sizing:border-box;";
49865
+ const mark = document.createElement("div");
49866
+ mark.style.cssText = "width:" + visual + "px;height:" + visual + "px;background:#fff;border:1px solid #1890ff;pointer-events:none;box-sizing:border-box;";
49867
+ el.appendChild(mark);
49343
49868
  el.addEventListener("pointerdown", (e) => {
49344
49869
  start_node_resize(gobj, e, def.mx, def.my);
49345
49870
  });
@@ -49379,52 +49904,18 @@ function update_resize_handles_position(gobj) {
49379
49904
  }
49380
49905
  function apply_handles_to_rect(gobj, rect) {
49381
49906
  let priv = gobj.priv;
49382
- const HALF = RESIZE_HANDLE_SIZE / 2;
49907
+ const HALF = resize_handle_hit() / 2;
49383
49908
  const left = rect.left;
49384
49909
  const top = rect.top;
49385
49910
  const w = rect.right - rect.left;
49386
49911
  const h = rect.bottom - rect.top;
49387
49912
  const selRect = priv._resize_sel_rect;
49388
49913
  if (selRect) selRect.style.cssText = "position:absolute;left:" + left + "px;top:" + top + "px;width:" + w + "px;height:" + h + "px;border:1px dashed #1890ff;pointer-events:none;box-sizing:border-box;";
49389
- const positions = [
49390
- {
49391
- x: left,
49392
- y: top
49393
- },
49394
- {
49395
- x: left + w / 2,
49396
- y: top
49397
- },
49398
- {
49399
- x: left + w,
49400
- y: top
49401
- },
49402
- {
49403
- x: left,
49404
- y: top + h / 2
49405
- },
49406
- {
49407
- x: left + w,
49408
- y: top + h / 2
49409
- },
49410
- {
49411
- x: left,
49412
- y: top + h
49413
- },
49414
- {
49415
- x: left + w / 2,
49416
- y: top + h
49417
- },
49418
- {
49419
- x: left + w,
49420
- y: top + h
49421
- }
49422
- ];
49423
- for (let i = 0; i < priv._resize_handles.length; i++) {
49424
- const handle = priv._resize_handles[i];
49425
- const p = positions[i];
49426
- handle.el.style.left = p.x - HALF + "px";
49427
- handle.el.style.top = p.y - HALF + "px";
49914
+ for (const handle of priv._resize_handles) {
49915
+ const x = left + (handle.mx + 1) / 2 * w;
49916
+ const y = top + (handle.my + 1) / 2 * h;
49917
+ handle.el.style.left = x - HALF + "px";
49918
+ handle.el.style.top = y - HALF + "px";
49428
49919
  }
49429
49920
  }
49430
49921
  function start_node_resize(gobj, e, mx, my) {
@@ -49500,6 +49991,14 @@ function start_node_resize(gobj, e, mx, my) {
49500
49991
  * Port selection and individual port resizing
49501
49992
  ************************************************************/
49502
49993
  var PORT_HANDLE_SIZE = 8;
49994
+ var PORT_HANDLE_VISUAL_COARSE = 12;
49995
+ var PORT_HANDLE_HIT_COARSE = 32;
49996
+ function port_handle_visual() {
49997
+ return coarse_pointer() ? PORT_HANDLE_VISUAL_COARSE : PORT_HANDLE_SIZE;
49998
+ }
49999
+ function port_handle_hit() {
50000
+ return coarse_pointer() ? PORT_HANDLE_HIT_COARSE : PORT_HANDLE_SIZE;
50001
+ }
49503
50002
  /************************************************************
49504
50003
  * Compute the canvas (world) position of a port on a node.
49505
50004
  * Returns {x, y} in world coordinates.
@@ -49532,6 +50031,26 @@ function get_port_radius(gobj, node_id, port_key) {
49532
50031
  return style.portR || 6;
49533
50032
  }
49534
50033
  /************************************************************
50034
+ * How much wider than the port itself its hit area is,
50035
+ * measured on the SCREEN and converted to world units.
50036
+ *
50037
+ * It used to be a flat `+4` in WORLD units, which is a
50038
+ * different target on every zoom: at the 50% a phone lands on
50039
+ * after fit, a 6-unit port plus 4 is a 5px radius -- one
50040
+ * fifth of a fingertip, and picking a port is the first step
50041
+ * of every link.
50042
+ ************************************************************/
50043
+ function port_hit_slop_world(graph) {
50044
+ let slop = coarse_pointer() ? 22 : 6;
50045
+ let zoom = 1;
50046
+ try {
50047
+ zoom = graph.getZoom() || 1;
50048
+ } catch (e) {
50049
+ zoom = 1;
50050
+ }
50051
+ return slop / zoom;
50052
+ }
50053
+ /************************************************************
49535
50054
  * Detect if a click in canvas coordinates hits a port.
49536
50055
  * Returns the port key string, or null if no port hit.
49537
50056
  ************************************************************/
@@ -49544,6 +50063,7 @@ function detect_port_click(gobj, node_id, canvasX, canvasY) {
49544
50063
  const defaultR = style.portR || 6;
49545
50064
  let best_key = null;
49546
50065
  let best_dist = Infinity;
50066
+ let slop = port_hit_slop_world(graph);
49547
50067
  for (let i = 0; i < ports.length; i++) {
49548
50068
  let pl = ports[i].placement || [.5, .5];
49549
50069
  let px = pos[0] + (pl[0] - .5) * w;
@@ -49552,7 +50072,7 @@ function detect_port_click(gobj, node_id, canvasX, canvasY) {
49552
50072
  let dx = canvasX - px;
49553
50073
  let dy = canvasY - py;
49554
50074
  let dist = Math.sqrt(dx * dx + dy * dy);
49555
- if (dist <= r + 4 && dist < best_dist) {
50075
+ if (dist <= r + slop && dist < best_dist) {
49556
50076
  best_dist = dist;
49557
50077
  best_key = ports[i].key;
49558
50078
  }
@@ -49600,7 +50120,7 @@ function show_link_icon_if_fkey(gobj) {
49600
50120
  let canvasPos = get_port_canvas_position(gobj, priv._selected_node_id, priv._selected_port_key);
49601
50121
  if (!canvasPos) return;
49602
50122
  let vp = graph.getViewportByCanvas([canvasPos.x, canvasPos.y]);
49603
- let icon = create_floating_icon("link", "#52c41a", vp[0] + 18, vp[1] - 14, t("link to hook"), () => {
50123
+ let icon = create_floating_icon("link", "#52c41a", vp[0] + floating_icon_size() / 2 + 4, vp[1] + floating_icon_dy(), t("link to hook"), () => {
49604
50124
  if (!priv._linking_mode) enter_linking_mode(gobj);
49605
50125
  });
49606
50126
  icon.addEventListener("pointerdown", (e) => {
@@ -49625,8 +50145,8 @@ function update_link_icon_position(gobj) {
49625
50145
  return;
49626
50146
  }
49627
50147
  let vp = graph.getViewportByCanvas([canvasPos.x, canvasPos.y]);
49628
- priv._link_icon_el.style.left = vp[0] + 18 + "px";
49629
- priv._link_icon_el.style.top = vp[1] - 14 + "px";
50148
+ priv._link_icon_el.style.left = vp[0] + floating_icon_size() / 2 + 4 + "px";
50149
+ priv._link_icon_el.style.top = vp[1] + floating_icon_dy() + "px";
49630
50150
  } catch (e) {
49631
50151
  hide_link_icon(gobj);
49632
50152
  }
@@ -49894,10 +50414,15 @@ function show_port_resize_handles(gobj) {
49894
50414
  dy: 0
49895
50415
  }
49896
50416
  ];
50417
+ const hit = port_handle_hit();
50418
+ const visual = port_handle_visual();
49897
50419
  const handles = [];
49898
50420
  for (const def of handle_defs) {
49899
50421
  const el = document.createElement("div");
49900
- el.style.cssText = "position:absolute;width:8px;height:8px;background:#fff;border:1px solid #fa8c16;border-radius:50%;cursor:" + def.cursor + ";pointer-events:all;box-sizing:border-box;";
50422
+ el.style.cssText = "position:absolute;width:" + hit + "px;height:" + hit + "px;display:flex;align-items:center;justify-content:center;cursor:" + def.cursor + ";pointer-events:all;touch-action:none;box-sizing:border-box;";
50423
+ const mark = document.createElement("div");
50424
+ mark.style.cssText = "width:" + visual + "px;height:" + visual + "px;background:#fff;border:1px solid #fa8c16;border-radius:50%;pointer-events:none;box-sizing:border-box;";
50425
+ el.appendChild(mark);
49901
50426
  el.addEventListener("pointerdown", (e) => {
49902
50427
  start_port_resize(gobj, e);
49903
50428
  });
@@ -49951,7 +50476,7 @@ function update_port_resize_handles_position(gobj) {
49951
50476
  ************************************************************/
49952
50477
  function apply_port_handles(gobj, cx, cy, vpR) {
49953
50478
  let priv = gobj.priv;
49954
- const HALF = PORT_HANDLE_SIZE / 2;
50479
+ const HALF = port_handle_hit() / 2;
49955
50480
  const ring = priv._port_ring;
49956
50481
  if (ring) {
49957
50482
  const d = vpR * 2;
@@ -50096,7 +50621,7 @@ function show_edge_icon(gobj) {
50096
50621
  if (!priv._selected_edge_id) return;
50097
50622
  let mid = get_edge_viewport_midpoint(gobj, priv._selected_edge_id);
50098
50623
  if (!mid) return;
50099
- let icons = show_dual_icons(gobj, mid.x + 4, mid.y - 14, t("edge properties"), () => toggle_edge_popover(gobj), t("unlink"), () => request_unlink_edge(gobj));
50624
+ let icons = show_dual_icons(gobj, mid.x + 4, mid.y + floating_icon_dy(), t("edge properties"), () => toggle_edge_popover(gobj), t("unlink"), () => request_unlink_edge(gobj));
50100
50625
  priv._edge_icon_el = icons.icon_el;
50101
50626
  priv._edge_delete_el = icons.delete_el;
50102
50627
  }
@@ -50114,14 +50639,14 @@ function update_edge_icon_position(gobj) {
50114
50639
  return;
50115
50640
  }
50116
50641
  priv._edge_icon_el.style.left = mid.x + 4 + "px";
50117
- priv._edge_icon_el.style.top = mid.y - 14 + "px";
50642
+ priv._edge_icon_el.style.top = mid.y + floating_icon_dy() + "px";
50118
50643
  if (priv._edge_delete_el) {
50119
50644
  priv._edge_delete_el.style.left = mid.x + 4 + "px";
50120
- priv._edge_delete_el.style.top = mid.y + 18 + "px";
50645
+ priv._edge_delete_el.style.top = mid.y + floating_icon_dy() + floating_icon_step() + "px";
50121
50646
  }
50122
50647
  if (priv._edge_popover_el) {
50123
- priv._edge_popover_el.style.left = mid.x + 36 + "px";
50124
- priv._edge_popover_el.style.top = mid.y - 14 + "px";
50648
+ priv._edge_popover_el.style.left = mid.x + floating_popover_dx() + "px";
50649
+ priv._edge_popover_el.style.top = mid.y + floating_icon_dy() + "px";
50125
50650
  clamp_popover_position(gobj, priv._edge_popover_el);
50126
50651
  }
50127
50652
  } catch (e) {
@@ -50159,7 +50684,7 @@ function show_edge_popover(gobj) {
50159
50684
  });
50160
50685
  let mid = get_edge_viewport_midpoint(gobj, edge_id);
50161
50686
  if (!mid) return;
50162
- const popover = create_popover_base(mid.x + 36, mid.y - 14, "g6-edge-popover", "#d9d9d9", 180);
50687
+ const popover = create_popover_base(mid.x + floating_popover_dx(), mid.y + floating_icon_dy(), "g6-edge-popover", "#d9d9d9", 180);
50163
50688
  function preview_edge() {
50164
50689
  let lw = parseInt(lwInput.value) || 2;
50165
50690
  let color = colorInput.value;
@@ -50246,7 +50771,7 @@ function show_node_icon(gobj) {
50246
50771
  if (!priv._selected_node_id || priv._selected_port_key) return;
50247
50772
  let rect = get_node_viewport_rect(gobj, priv._selected_node_id);
50248
50773
  if (!rect) return;
50249
- let icons = show_dual_icons(gobj, rect.right + 4, rect.top - 14, t("node properties"), () => toggle_node_popover(gobj), t("delete node"), () => request_delete_node(gobj));
50774
+ let icons = show_dual_icons(gobj, rect.right + 4, rect.top + floating_icon_dy(), t("node properties"), () => toggle_node_popover(gobj), t("delete node"), () => request_delete_node(gobj));
50250
50775
  priv._node_icon_el = icons.icon_el;
50251
50776
  priv._node_delete_el = icons.delete_el;
50252
50777
  }
@@ -50268,14 +50793,14 @@ function update_node_icon_position(gobj) {
50268
50793
  return;
50269
50794
  }
50270
50795
  priv._node_icon_el.style.left = rect.right + 4 + "px";
50271
- priv._node_icon_el.style.top = rect.top - 14 + "px";
50796
+ priv._node_icon_el.style.top = rect.top + floating_icon_dy() + "px";
50272
50797
  if (priv._node_delete_el) {
50273
50798
  priv._node_delete_el.style.left = rect.right + 4 + "px";
50274
- priv._node_delete_el.style.top = rect.top + 18 + "px";
50799
+ priv._node_delete_el.style.top = rect.top + floating_icon_dy() + floating_icon_step() + "px";
50275
50800
  }
50276
50801
  if (priv._node_popover_el) {
50277
- priv._node_popover_el.style.left = rect.right + 36 + "px";
50278
- priv._node_popover_el.style.top = rect.top - 14 + "px";
50802
+ priv._node_popover_el.style.left = rect.right + floating_popover_dx() + "px";
50803
+ priv._node_popover_el.style.top = rect.top + floating_icon_dy() + "px";
50279
50804
  clamp_popover_position(gobj, priv._node_popover_el);
50280
50805
  }
50281
50806
  } catch (e) {
@@ -50308,7 +50833,7 @@ function show_node_popover(gobj) {
50308
50833
  let origLW = currentLW;
50309
50834
  let rect = get_node_viewport_rect(gobj, node_id);
50310
50835
  if (!rect) return;
50311
- const popover = create_popover_base(rect.right + 36, rect.top - 14, "g6-node-popover", "#d9d9d9", 180);
50836
+ const popover = create_popover_base(rect.right + floating_popover_dx(), rect.top + floating_icon_dy(), "g6-node-popover", "#d9d9d9", 180);
50312
50837
  let node_graph_type = nodeData.data && nodeData.data.desc ? nodeData.data.desc.node_treedb_type : null;
50313
50838
  function preview_node() {
50314
50839
  let fill = fillInput.value;
@@ -50939,7 +51464,7 @@ function show_create_popover(gobj) {
50939
51464
  topicLabel.textContent = t("topic") + ":";
50940
51465
  popover.appendChild(topicLabel);
50941
51466
  let topicSelect = document.createElement("select");
50942
- topicSelect.style.cssText = "width:100%;padding:6px;border:1px solid #d9d9d9;border-radius:4px;font-size:13px;margin-bottom:8px;";
51467
+ topicSelect.style.cssText = "width:100%;padding:6px;border:1px solid #d9d9d9;border-radius:4px;font-size:13px;margin-bottom:8px;" + touch_control_css();
50943
51468
  for (let i = 0; i < topics.length; i++) {
50944
51469
  let opt = document.createElement("option");
50945
51470
  opt.value = topics[i].topic_name;
@@ -50972,7 +51497,7 @@ function show_create_popover(gobj) {
50972
51497
  let idInput = document.createElement("input");
50973
51498
  idInput.type = "text";
50974
51499
  idInput.placeholder = t("node id");
50975
- idInput.style.cssText = "width:100%;padding:6px;border:1px solid #d9d9d9;border-radius:4px;font-size:13px;margin-bottom:10px;box-sizing:border-box;";
51500
+ idInput.style.cssText = "width:100%;padding:6px;border:1px solid #d9d9d9;border-radius:4px;font-size:13px;margin-bottom:10px;box-sizing:border-box;" + touch_control_css();
50976
51501
  popover.appendChild(idInput);
50977
51502
  let errorEl = document.createElement("div");
50978
51503
  errorEl.style.cssText = "color:#ff4d4f;font-size:12px;margin-bottom:8px;display:none;";
@@ -51479,8 +52004,7 @@ function ac_load_data(gobj, event, kw, src) {
51479
52004
  if (auto_laid) graph_fit_readable(gobj).catch((e) => {
51480
52005
  log_error(`${gobj_short_name(gobj)}: cannot fit the graph: ${e}`);
51481
52006
  });
51482
- if (priv.edit_mode) graph_add_plugin(gobj, "history");
51483
- else graph_remove_plugin(gobj, "history");
52007
+ sync_history_plugin(gobj);
51484
52008
  if (priv._pending_focus_topic !== null) {
51485
52009
  let ft = priv._pending_focus_topic;
51486
52010
  priv._pending_focus_topic = null;
@@ -51662,6 +52186,11 @@ function ac_resize(gobj, event, kw, src) {
51662
52186
  if (h < 0) h = 80;
51663
52187
  graph_resize(gobj, rect.width, h);
51664
52188
  }
52189
+ let can_collapse = toolbars_can_collapse(gobj);
52190
+ if (can_collapse !== priv._toolbars_could_collapse) {
52191
+ priv._toolbars_could_collapse = can_collapse;
52192
+ update_toolbar(gobj);
52193
+ }
51665
52194
  return 0;
51666
52195
  }
51667
52196
  /************************************************************
@@ -51700,6 +52229,19 @@ function ac_zoom_reset(gobj, event, kw, src) {
51700
52229
  gobj.priv.graph.zoomTo(1);
51701
52230
  return 0;
51702
52231
  }
52232
+ /************************************************************
52233
+ * Fold / unfold the floating toolbars.
52234
+ *
52235
+ * One flag for BOTH: they are one wall over the canvas, and
52236
+ * the button that unfolds them has to be the same button
52237
+ * whichever edge the user reaches for.
52238
+ ************************************************************/
52239
+ function ac_toggle_toolbars(gobj, event, kw, src) {
52240
+ let priv = gobj.priv;
52241
+ priv.toolbar_collapsed = !toolbars_collapsed(gobj);
52242
+ update_toolbar(gobj);
52243
+ return 0;
52244
+ }
51703
52245
  function ac_auto_fit(gobj, event, kw, src) {
51704
52246
  graph_fitview(gobj);
51705
52247
  return 0;
@@ -51764,6 +52306,7 @@ function ac_set_operation_mode$1(gobj, event, kw, src) {
51764
52306
  gobj_write_attr(gobj, "operation_mode", kw.operation_mode);
51765
52307
  configure_behaviour(gobj);
51766
52308
  update_toolbar(gobj);
52309
+ sync_history_plugin(gobj);
51767
52310
  return 0;
51768
52311
  }
51769
52312
  /************************************************************
@@ -52084,6 +52627,11 @@ function create_gclass$4(gclass_name) {
52084
52627
  ac_zoom_reset,
52085
52628
  null
52086
52629
  ],
52630
+ [
52631
+ "EV_TOGGLE_TOOLBARS",
52632
+ ac_toggle_toolbars,
52633
+ null
52634
+ ],
52087
52635
  [
52088
52636
  "EV_AUTO_FIT",
52089
52637
  ac_auto_fit,
@@ -52187,6 +52735,7 @@ function create_gclass$4(gclass_name) {
52187
52735
  ["EV_ZOOM_IN", 0],
52188
52736
  ["EV_ZOOM_OUT", 0],
52189
52737
  ["EV_ZOOM_RESET", 0],
52738
+ ["EV_TOGGLE_TOOLBARS", 0],
52190
52739
  ["EV_AUTO_FIT", 0],
52191
52740
  ["EV_ZOOM_SELECTION", 0],
52192
52741
  ["EV_FOCUS_TOPIC", 0],