@yuneta/gobj-ui 7.15.0 → 7.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -708,6 +708,75 @@ so it survived that — two light islands over a dark canvas.
708
708
  **New keys for consumers: `actual size`, `zoom level`** (both tooltips, so a
709
709
  host that has not defined them shows the key on hover and nothing else breaks).
710
710
 
711
+ ### Selecting several nodes, and moving them together
712
+
713
+ In **edition** mode the graph has a real selection, not just "the node you
714
+ clicked":
715
+
716
+ | gesture | what it does |
717
+ |---|---|
718
+ | click a node | selects it **and opens it**: resize handles, ports, popovers |
719
+ | **shift + click** | adds that node to the selection, or takes it out |
720
+ | **shift + drag on the canvas** | rubber band: the selection becomes what it enclosed |
721
+ | **ctrl/cmd + A** | every node |
722
+ | drag any selected node | **moves the whole selection**, as one undo |
723
+ | **Delete** / **Backspace** | deletes the selection, after a confirmation that counts what it takes |
724
+ | **Esc**, or a click on the canvas | clears it |
725
+
726
+ The keys reach the graph only while the **graph has focus** — G6 gives its
727
+ canvas a `tabIndex` of its own — which is what keeps `ctrl+A` inside the find
728
+ box a selection of the TEXT and not of every node: the focus is in the input,
729
+ and the input is not inside the canvas. They arrive as `EV_KEY_DOWN` and the
730
+ action decides, so a key is as visible in the `machine` trace as a click.
731
+
732
+ Three decisions are worth knowing, because each one is where this could have
733
+ gone wrong:
734
+
735
+ - **G6's `selected` element state IS the selection.** `drag-element` decides
736
+ what a drag moves by asking the graph for it
737
+ (`getElementDataByState('node', 'selected')`), so a set kept anywhere else
738
+ would be a second truth the drag never consults — the ring would say five and
739
+ one would move. It also batches the move, so a group drag is one history
740
+ entry rather than one per node.
741
+
742
+ - **The ring is painted into the card's own html**, and it had to be. A state
743
+ style paints on a node's KEY SHAPE, and every node here is an `html` node
744
+ whose key shape is a DOM element — the same reason the amber highlight had
745
+ never appeared before `7.3.0`. Selecting with `brush-select` and nothing else
746
+ would have selected correctly and shown **nothing**. The ring is blue and
747
+ drawn OUTSIDE the amber halo, so a node that is both a find match and
748
+ selected wears both; one function composes them (`ring_shadow`), because
749
+ before it each repaint wrote its own flag and erased the other's.
750
+
751
+ - **The gesture is G6's, the result is an event.** `brush-select` gets an
752
+ `onSelect` that sends `EV_BRUSH_SELECT` with the ids, and the action does the
753
+ work — so a marquee shows up in the `machine` trace like every other action.
754
+ Shift+click is not G6's `click-select` at all: this gclass already owns
755
+ `EV_NODE_CLICK`, and adding a second selection owner outside the FSM is how
756
+ the two end up disagreeing.
757
+
758
+ Panning gives way while **Shift** is held (`drag-canvas` takes an `enable`
759
+ predicate), or the canvas would pan under the rubber band — G6 binds
760
+ `drag-canvas` straight to the drag events, and its own docs warn that the two
761
+ gestures cannot both be a plain drag.
762
+
763
+ **A delete says what it takes, whether it is one or twenty.** The Delete key
764
+ asks the same question the per-node delete icon asks, built by the same
765
+ function: the record's key when there is one, the count when there are more,
766
+ and then the two lines that are actually at stake — *N children will be
767
+ UNLINKED, not deleted* and *it will be detached from M parents* — where over a
768
+ set the numbers are the sums. These views delete with `force`, so an operator
769
+ pressing Delete over twelve cards has to read eleven detached children, not
770
+ "are you sure". It needs no new keys: the sentence is the one `7.10.0` already
771
+ defined. The question has no icon to hang off, so it is asked in the middle of
772
+ the graph it is about.
773
+
774
+ **A marquee selects, it does not open.** Even when it encloses exactly one
775
+ node, the handles and ports stay away: `_selected_node_id` means *the node
776
+ opened for editing*, and only a click sets it. Everything that hangs off a
777
+ single node reads that field, so a multiple selection puts all of it away by
778
+ construction rather than by a check in twenty places.
779
+
711
780
  ### Finding a node in the graph
712
781
 
713
782
  `C_YUI_TREEDB_GRAPH` carries a find box in the middle of its toolbar. It
@@ -53747,6 +53747,7 @@ ensure_drag_canvas_patch();
53747
53747
  var GCLASS_NAME$1 = "C_G6_NODES_TREE";
53748
53748
  var HIGHLIGHT_COLOR = "#f0a020";
53749
53749
  var HIGHLIGHT_HALO = "rgba(240,160,32,0.35)";
53750
+ var SELECT_RING = "rgba(59,130,246,0.95)";
53750
53751
  /***************************************************************
53751
53752
  * Internal layout and operation mode definitions
53752
53753
  ***************************************************************/
@@ -53858,6 +53859,7 @@ var PRIVATE_DATA$1 = {
53858
53859
  _link_saved_styles: [],
53859
53860
  _focus_topic: null,
53860
53861
  _focus_ids: [],
53862
+ _selected_paint_ids: [],
53861
53863
  _pending_focus_topic: null,
53862
53864
  _pending_find: null,
53863
53865
  _layout_asked: "",
@@ -54086,12 +54088,15 @@ function configure_events(gobj) {
54086
54088
  update_toolbar(gobj);
54087
54089
  };
54088
54090
  i18next.default.on("languageChanged", priv._on_language_changed);
54089
- if ((0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_fullscreen")) graph.on("keydown", (evt) => {
54090
- const key = evt.key;
54091
- const fullscreen = graph_get_plugin(gobj, "fullscreen");
54092
- if (!fullscreen) return;
54093
- if (key === "F" || key === "f") fullscreen.request();
54094
- else if (key === "Escape") fullscreen.exit();
54091
+ graph.on("keydown", (evt) => {
54092
+ let ctrl = !!(evt.ctrlKey || evt.metaKey);
54093
+ if (ctrl && (evt.key === "a" || evt.key === "A")) {
54094
+ if (typeof evt.preventDefault === "function") evt.preventDefault();
54095
+ }
54096
+ (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_KEY_DOWN", {
54097
+ key: evt.key,
54098
+ ctrl
54099
+ }, gobj);
54095
54100
  });
54096
54101
  }
54097
54102
  /************************************************************
@@ -54378,9 +54383,24 @@ function configure_behaviour(gobj) {
54378
54383
  case "edition":
54379
54384
  priv.edit_mode = true;
54380
54385
  behaviors = [
54381
- "drag-canvas",
54386
+ {
54387
+ type: "drag-canvas",
54388
+ key: "drag-canvas",
54389
+ enable: (event) => !event.shiftKey
54390
+ },
54382
54391
  "zoom-canvas",
54383
- "drag-element"
54392
+ "drag-element",
54393
+ {
54394
+ type: "brush-select",
54395
+ key: "brush-select",
54396
+ trigger: ["shift"],
54397
+ enableElements: ["node"],
54398
+ immediately: false,
54399
+ onSelect: (states) => {
54400
+ let ids = Object.keys(states || {}).filter((id) => (states[id] || []).includes("selected"));
54401
+ (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_BRUSH_SELECT", { ids }, gobj);
54402
+ }
54403
+ }
54384
54404
  ];
54385
54405
  break;
54386
54406
  case "operation":
@@ -55453,10 +55473,17 @@ function create_form_button_row(parent, buttons) {
55453
55473
  function show_confirm_popover(gobj, target_el, message, confirm_text, confirm_color, onConfirm, priv_key) {
55454
55474
  hide_overlay(gobj, priv_key);
55455
55475
  let priv = gobj.priv;
55456
- if (!target_el) return;
55457
- let iconRect = target_el.getBoundingClientRect();
55458
55476
  let containerRect = priv.$container.getBoundingClientRect();
55459
- const popover = create_popover_base(iconRect.right - containerRect.left + 6, iconRect.top - containerRect.top - 4, "g6-confirm-popover", "#ff4d4f", 160);
55477
+ let left, top;
55478
+ if (target_el) {
55479
+ let iconRect = target_el.getBoundingClientRect();
55480
+ left = iconRect.right - containerRect.left + 6;
55481
+ top = iconRect.top - containerRect.top - 4;
55482
+ } else {
55483
+ left = Math.round(containerRect.width / 2) - 110;
55484
+ top = Math.round(containerRect.height / 4);
55485
+ }
55486
+ const popover = create_popover_base(left, top, "g6-confirm-popover", "#ff4d4f", 160);
55460
55487
  let msg = document.createElement("div");
55461
55488
  msg.style.cssText = "margin-bottom:10px;font-weight:500;white-space:pre-line;";
55462
55489
  msg.textContent = message;
@@ -55495,6 +55522,81 @@ function perform_history_op(gobj, is_redo) {
55495
55522
  sync_history_to_backend(gobj, cmd ? cmd.original : null);
55496
55523
  }
55497
55524
  }
55525
+ /************************************************************
55526
+ * The selection.
55527
+ *
55528
+ * G6's `selected` element state IS the selection: `drag-element`
55529
+ * reads it (`getElementDataByState`) to decide what a drag moves,
55530
+ * so keeping the set anywhere else would be a second truth the
55531
+ * drag does not consult. What this gclass keeps is what is
55532
+ * PAINTED -- an html node draws no state style at all (its key
55533
+ * shape is a DOM element), so the ring lives in the card's own
55534
+ * html, exactly like the find highlight, and the painted set
55535
+ * exists to diff the repaint.
55536
+ ************************************************************/
55537
+ function selected_node_ids(gobj) {
55538
+ let graph = gobj.priv.graph;
55539
+ if (!graph) return [];
55540
+ let data;
55541
+ try {
55542
+ data = graph.getElementDataByState("node", "selected") || [];
55543
+ } catch (e) {
55544
+ return [];
55545
+ }
55546
+ return data.map((nd) => nd.id);
55547
+ }
55548
+ /************************************************************
55549
+ * Move the ring to these nodes and off the ones that had it.
55550
+ ************************************************************/
55551
+ function paint_selection(gobj, ids) {
55552
+ let priv = gobj.priv;
55553
+ let prev = priv._selected_paint_ids || [];
55554
+ let next = ids || [];
55555
+ priv._selected_paint_ids = next;
55556
+ repaint_cards(gobj, /* @__PURE__ */ new Set([...prev, ...next]));
55557
+ }
55558
+ /************************************************************
55559
+ * Select a SET of nodes: what a marquee and a shift-click do.
55560
+ *
55561
+ * It leaves `_selected_node_id` null even for a set of one,
55562
+ * because that field is not "the selection", it is "the node
55563
+ * opened for editing" -- the resize handles, the ports and the
55564
+ * popovers hang off it, and none of them means anything over a
55565
+ * set. Clicking a node is what opens one (`select_node`); a
55566
+ * marquee selects, it does not open.
55567
+ ************************************************************/
55568
+ function set_selection(gobj, ids) {
55569
+ let graph = gobj.priv.graph;
55570
+ let next = [];
55571
+ for (let id of ids || []) try {
55572
+ if (graph.getNodeData(id)) next.push(id);
55573
+ } catch (e) {}
55574
+ deselect_node(gobj);
55575
+ if (!next.length) return;
55576
+ history_pause(gobj);
55577
+ try {
55578
+ let states = {};
55579
+ for (let id of next) states[id] = ["selected"];
55580
+ graph.setElementState(states);
55581
+ } catch (e) {
55582
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: cannot set the selection: ${e}`);
55583
+ }
55584
+ paint_selection(gobj, next);
55585
+ graph_draw(gobj).then(() => {
55586
+ history_resume(gobj);
55587
+ });
55588
+ }
55589
+ /************************************************************
55590
+ * Add a node to the selection, or take it out of it.
55591
+ ************************************************************/
55592
+ function toggle_in_selection(gobj, node_id) {
55593
+ let priv = gobj.priv;
55594
+ let current = new Set(selected_node_ids(gobj));
55595
+ for (let id of priv._selected_paint_ids || []) current.add(id);
55596
+ if (current.has(node_id)) current.delete(node_id);
55597
+ else current.add(node_id);
55598
+ set_selection(gobj, [...current]);
55599
+ }
55498
55600
  function select_node(gobj, node_id) {
55499
55601
  let priv = gobj.priv;
55500
55602
  let graph = priv.graph;
@@ -55504,6 +55606,7 @@ function select_node(gobj, node_id) {
55504
55606
  graph.setElementState(node_id, ["selected"]);
55505
55607
  } catch (e) {}
55506
55608
  priv._selected_node_id = node_id;
55609
+ paint_selection(gobj, [node_id]);
55507
55610
  show_resize_handles(gobj);
55508
55611
  show_node_icon(gobj);
55509
55612
  graph_draw(gobj).then(() => {
@@ -55519,12 +55622,17 @@ function deselect_node(gobj) {
55519
55622
  hide_node_popover(gobj);
55520
55623
  hide_delete_confirm(gobj);
55521
55624
  hide_unlink_confirm(gobj);
55522
- if (priv._selected_node_id) {
55625
+ let clearing = /* @__PURE__ */ new Set([...selected_node_ids(gobj), ...priv._selected_paint_ids || []]);
55626
+ if (priv._selected_node_id) clearing.add(priv._selected_node_id);
55627
+ if (clearing.size) {
55523
55628
  history_pause(gobj);
55524
55629
  try {
55525
- graph.setElementState(priv._selected_node_id, []);
55630
+ let states = {};
55631
+ for (let id of clearing) states[id] = [];
55632
+ graph.setElementState(states);
55526
55633
  } catch (e) {}
55527
55634
  priv._selected_node_id = null;
55635
+ paint_selection(gobj, []);
55528
55636
  graph_draw(gobj).then(() => {
55529
55637
  history_resume(gobj);
55530
55638
  });
@@ -56811,33 +56919,36 @@ function refresh_minimap(gobj) {
56811
56919
  * same three-way choice — it lived inline in the theme refresh and is
56812
56920
  * shared now. Returns null for a node that carries no desc.
56813
56921
  ************************************************************/
56814
- function node_innerHTML_of(nd, theme, highlight) {
56922
+ function node_innerHTML_of(nd, theme, highlight, selected) {
56815
56923
  if (!nd || !nd.data || !nd.data.desc) return null;
56816
56924
  let desc = nd.data.desc;
56817
56925
  let record = nd.data.record || {};
56818
56926
  let label = node_label(desc, record);
56819
56927
  switch (desc.node_treedb_type) {
56820
- case "child": return build_chip_innerHTML(desc.color, theme, record.icon, label, record.id, highlight);
56821
- case "extended": return build_node_innerHTML(desc.color, theme, record.icon, label, desc.topic_name, true, record.id, highlight);
56822
- case "hierarchical": return build_node_innerHTML(desc.color, theme, record.icon, label, desc.topic_name, false, record.id, highlight);
56928
+ case "child": return build_chip_innerHTML(desc.color, theme, record.icon, label, record.id, highlight, selected);
56929
+ case "extended": return build_node_innerHTML(desc.color, theme, record.icon, label, desc.topic_name, true, record.id, highlight, selected);
56930
+ case "hierarchical": return build_node_innerHTML(desc.color, theme, record.icon, label, desc.topic_name, false, record.id, highlight, selected);
56823
56931
  }
56824
56932
  return null;
56825
56933
  }
56826
56934
  /************************************************************
56827
- * Repaint the cards whose highlight state CHANGES: the ones that had it
56828
- * and lose it, plus the ones that gain it. Only those — a treedb graph
56829
- * is redrawn per keystroke of the find box otherwise.
56935
+ * Repaint these cards with the flags they carry RIGHT NOW.
56936
+ *
56937
+ * One place, because the flags are independent and each used to
56938
+ * be written by whoever repainted last: a find that repainted its
56939
+ * matches erased the selection ring off them, and a selection
56940
+ * repainted over a match erased the amber. Both are read here
56941
+ * from where they live.
56830
56942
  ************************************************************/
56831
- function apply_node_highlight(gobj, prev_ids, next_ids) {
56943
+ function repaint_cards(gobj, ids) {
56832
56944
  let priv = gobj.priv;
56833
56945
  let graph = priv.graph;
56834
- if (!graph) return;
56835
- let next = new Set(next_ids || []);
56836
- let touched = /* @__PURE__ */ new Set([...prev_ids || [], ...next]);
56837
- if (touched.size === 0) return;
56946
+ if (!graph || !ids || !ids.size) return;
56947
+ let focus = new Set(priv._focus_ids || []);
56948
+ let selected = new Set(priv._selected_paint_ids || []);
56838
56949
  let updates = [];
56839
- for (let id of touched) {
56840
- let html = node_innerHTML_of(graph.getNodeData(id), priv.theme, next.has(id));
56950
+ for (let id of ids) {
56951
+ let html = node_innerHTML_of(graph.getNodeData(id), priv.theme, focus.has(id), selected.has(id));
56841
56952
  if (html !== null) updates.push({
56842
56953
  id,
56843
56954
  style: { innerHTML: html }
@@ -56848,19 +56959,32 @@ function apply_node_highlight(gobj, prev_ids, next_ids) {
56848
56959
  graph.updateNodeData(updates);
56849
56960
  graph.draw();
56850
56961
  } catch (e) {
56851
- (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: cannot repaint the highlight: ${e}`);
56962
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: cannot repaint the cards: ${e}`);
56852
56963
  }
56853
56964
  }
56965
+ /************************************************************
56966
+ * Repaint the cards whose highlight state CHANGES: the ones that had it
56967
+ * and lose it, plus the ones that gain it. Only those — a treedb graph
56968
+ * is redrawn per keystroke of the find box otherwise.
56969
+ ************************************************************/
56970
+ function apply_node_highlight(gobj, prev_ids, next_ids) {
56971
+ if (!gobj.priv.graph) return;
56972
+ let next = new Set(next_ids || []);
56973
+ let touched = /* @__PURE__ */ new Set([...prev_ids || [], ...next]);
56974
+ if (touched.size === 0) return;
56975
+ repaint_cards(gobj, touched);
56976
+ }
56854
56977
  function refresh_html_nodes_theme(gobj, theme) {
56855
56978
  let priv = gobj.priv;
56856
56979
  let graph = priv.graph;
56857
56980
  if (!graph) return;
56858
56981
  let nodes = graph.getData().nodes || [];
56859
56982
  let highlighted = new Set(priv._focus_ids || []);
56983
+ let selected = new Set(priv._selected_paint_ids || []);
56860
56984
  let updates = [];
56861
56985
  for (let i = 0; i < nodes.length; i++) {
56862
56986
  let id = nodes[i].id;
56863
- let html = node_innerHTML_of(graph.getNodeData(id), theme, highlighted.has(id));
56987
+ let html = node_innerHTML_of(graph.getNodeData(id), theme, highlighted.has(id), selected.has(id));
56864
56988
  if (html !== null) updates.push({
56865
56989
  id,
56866
56990
  style: { innerHTML: html }
@@ -56890,12 +57014,29 @@ function refresh_default_edges_theme(gobj, theme) {
56890
57014
  if (updates.length > 0) graph.updateEdgeData(updates);
56891
57015
  }
56892
57016
  /************************************************************
57017
+ * The rings a card can wear, composed into one `box-shadow`.
57018
+ *
57019
+ * A node can be a find match and be selected at the same time, so
57020
+ * neither ring may be written by overwriting the other: the amber
57021
+ * halo hugs the card and the blue selection ring is drawn outside
57022
+ * it, which is also the order that reads correctly when only one
57023
+ * of them is on.
57024
+ ************************************************************/
57025
+ function ring_shadow(highlight, selected, base_shadow) {
57026
+ let rings = [];
57027
+ if (highlight) rings.push(`0 0 0 4px ${HIGHLIGHT_HALO}`);
57028
+ if (selected) rings.push(`0 0 0 ${highlight ? "7px" : "3px"} ${SELECT_RING}`);
57029
+ if (base_shadow) rings.push(base_shadow);
57030
+ if (!rings.length) return "";
57031
+ return `box-shadow: ${rings.join(", ")};`;
57032
+ }
57033
+ /************************************************************
56893
57034
  * Build innerHTML for pure-child (leaf) nodes: a compact
56894
57035
  * chip-card. Same colour/typography family as the entity card
56895
57036
  * but lighter (1px border, no shadow, single line). The name is
56896
57037
  * always legible (ellipsis + native title tooltip on overflow).
56897
57038
  ************************************************************/
56898
- function build_chip_innerHTML(color, theme, icon, label, key, highlight) {
57039
+ function build_chip_innerHTML(color, theme, icon, label, key, highlight, selected) {
56899
57040
  let title = key || label;
56900
57041
  let dark = theme === "dark";
56901
57042
  let bg = dark ? `color-mix(in srgb, ${color} 30%, #2c3542)` : `color-mix(in srgb, ${color} 10%, ${dark ? "#1b2230" : "#ffffff"})`;
@@ -56914,7 +57055,7 @@ function build_chip_innerHTML(color, theme, icon, label, key, highlight) {
56914
57055
  height: 100%;
56915
57056
  background: ${bg};
56916
57057
  border: ${highlight ? "3px" : "1px"} solid ${border};
56917
- ${highlight ? `box-shadow: 0 0 0 4px ${HIGHLIGHT_HALO};` : ""}
57058
+ ${ring_shadow(highlight, selected, "")}
56918
57059
  border-radius: 8px;
56919
57060
  color: ${text_color};
56920
57061
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
@@ -56940,7 +57081,7 @@ function build_chip_innerHTML(color, theme, icon, label, key, highlight) {
56940
57081
  * colour is kept (per-topic differentiation) but softened via
56941
57082
  * color-mix instead of a harsh saturated fill. Theme-aware.
56942
57083
  ************************************************************/
56943
- function build_node_innerHTML(color, theme, icon, label, topic_name, structural, key, highlight) {
57084
+ function build_node_innerHTML(color, theme, icon, label, topic_name, structural, key, highlight, selected) {
56944
57085
  let title = key || label;
56945
57086
  let dark = theme === "dark";
56946
57087
  let surface = dark ? "#1b2230" : "#ffffff";
@@ -56982,7 +57123,7 @@ function build_node_innerHTML(color, theme, icon, label, topic_name, structural,
56982
57123
  background: ${bg};
56983
57124
  border: ${highlight ? "3px" : "1.5px"} ${border_style} ${border};
56984
57125
  border-radius: 10px;
56985
- box-shadow: ${highlight ? `0 0 0 4px ${HIGHLIGHT_HALO}, ${shadow}` : shadow};
57126
+ ${ring_shadow(highlight, selected, shadow)}
56986
57127
  color: ${title_color};
56987
57128
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
56988
57129
  display: flex;
@@ -57026,17 +57167,89 @@ function execute_delete_node(gobj, nodeData) {
57026
57167
  deselect_node(gobj);
57027
57168
  }
57028
57169
  /************************************************************
57170
+ * Every node in the graph.
57171
+ ************************************************************/
57172
+ function select_all_nodes(gobj) {
57173
+ let graph = gobj.priv.graph;
57174
+ if (!graph) return;
57175
+ set_selection(gobj, ((graph.getData() || {}).nodes || []).map((nd) => nd.id));
57176
+ }
57177
+ /************************************************************
57178
+ * The nodes currently selected, as their node data.
57179
+ ************************************************************/
57180
+ function selected_nodes_data(gobj) {
57181
+ let priv = gobj.priv;
57182
+ let graph = priv.graph;
57183
+ let ids = new Set(selected_node_ids(gobj));
57184
+ for (let id of priv._selected_paint_ids || []) ids.add(id);
57185
+ if (priv._selected_node_id) ids.add(priv._selected_node_id);
57186
+ let out = [];
57187
+ for (let id of ids) {
57188
+ let nd;
57189
+ try {
57190
+ nd = graph.getNodeData(id);
57191
+ } catch (e) {
57192
+ continue;
57193
+ }
57194
+ if (nd && nd.data && nd.data.desc) out.push(nd);
57195
+ }
57196
+ return out;
57197
+ }
57198
+ /************************************************************
57199
+ * What a delete takes with it, for ONE node or for a set.
57200
+ *
57201
+ * Same sentence in both cases and the same keys, because it is the
57202
+ * same warning: these views delete with `force`, which UNLINKS the
57203
+ * children (they survive, loose) and cleans the node off its
57204
+ * parents. Over a set the two numbers are the sums -- an operator
57205
+ * about to detach eleven records has to read eleven, not "are you
57206
+ * sure".
57207
+ ************************************************************/
57208
+ function delete_question(nodes) {
57209
+ let children = 0;
57210
+ let parents = 0;
57211
+ for (let nd of nodes) {
57212
+ let impact = delete_impact(nd.data.desc, nd.data.record || {});
57213
+ children += impact.children;
57214
+ parents += impact.parents;
57215
+ }
57216
+ let question;
57217
+ if (nodes.length === 1) {
57218
+ let nd = nodes[0];
57219
+ question = (0, i18next.t)("delete") + " " + nd.data.desc.topic_name + ": " + (nd.data.record || {}).id + "?";
57220
+ } else question = (0, i18next.t)("delete") + " " + nodes.length + " " + (0, i18next.t)("records") + "?";
57221
+ if (children > 0) question += "\n" + children + " " + (0, i18next.t)("children will be unlinked, not deleted", { count: children });
57222
+ if (parents > 0) question += "\n" + (0, i18next.t)("it will be detached from") + " " + parents + " " + (0, i18next.t)("parents", { count: parents });
57223
+ return question;
57224
+ }
57225
+ /************************************************************
57226
+ * Delete every selected node (the Delete key).
57227
+ ************************************************************/
57228
+ function request_delete_selection(gobj) {
57229
+ let nodes = selected_nodes_data(gobj);
57230
+ if (!nodes.length) return;
57231
+ if (!(0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "confirm_delete_node")) {
57232
+ execute_delete_selection(gobj, nodes);
57233
+ return;
57234
+ }
57235
+ show_confirm_popover(gobj, null, delete_question(nodes), "delete", "#ff4d4f", () => execute_delete_selection(gobj, nodes), "_delete_confirm_el");
57236
+ }
57237
+ function execute_delete_selection(gobj, nodes) {
57238
+ let priv = gobj.priv;
57239
+ hide_delete_confirm(gobj);
57240
+ for (let nd of nodes) (0, _yuneta_gobj_js.gobj_publish_event)(gobj, "EV_DELETE_NODE", {
57241
+ treedb_name: priv.treedb_name,
57242
+ topic_name: nd.data.desc.topic_name,
57243
+ record: nd.data.record
57244
+ });
57245
+ deselect_node(gobj);
57246
+ }
57247
+ /************************************************************
57029
57248
  * Show a confirmation popover next to the delete icon.
57030
57249
  ************************************************************/
57031
57250
  function show_delete_confirm(gobj, nodeData) {
57032
57251
  let priv = gobj.priv;
57033
- let record = nodeData.data.record || {};
57034
- let topic_name = nodeData.data.desc.topic_name;
57035
- let impact = delete_impact(nodeData.data.desc, record);
57036
- let question = (0, i18next.t)("delete") + " " + topic_name + ": " + record.id + "?";
57037
- if (impact.children > 0) question += "\n" + impact.children + " " + (0, i18next.t)("children will be unlinked, not deleted", { count: impact.children });
57038
- if (impact.parents > 0) question += "\n" + (0, i18next.t)("it will be detached from") + " " + impact.parents + " " + (0, i18next.t)("parents", { count: impact.parents });
57039
- show_confirm_popover(gobj, priv._node_delete_el, question, "delete", "#ff4d4f", () => execute_delete_node(gobj, nodeData), "_delete_confirm_el");
57252
+ show_confirm_popover(gobj, priv._node_delete_el, delete_question([nodeData]), "delete", "#ff4d4f", () => execute_delete_node(gobj, nodeData), "_delete_confirm_el");
57040
57253
  }
57041
57254
  function hide_delete_confirm(gobj) {
57042
57255
  hide_overlay(gobj, "_delete_confirm_el");
@@ -57903,6 +58116,49 @@ function ac_node_drag_end(gobj, event, kw, src) {
57903
58116
  return 0;
57904
58117
  }
57905
58118
  /************************************************************
58119
+ * A key, while the graph has focus.
58120
+ *
58121
+ * Full screen keeps the two keys it always had. The rest belong to
58122
+ * the selection, so they only mean anything in edition -- outside
58123
+ * it there is nothing selected to clear, select or delete.
58124
+ ************************************************************/
58125
+ function ac_key_down(gobj, event, kw, src) {
58126
+ let priv = gobj.priv;
58127
+ let key = kw && kw.key || "";
58128
+ if ((0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_fullscreen")) {
58129
+ let fullscreen = graph_get_plugin(gobj, "fullscreen");
58130
+ if (fullscreen) {
58131
+ if (key === "F" || key === "f") fullscreen.request();
58132
+ else if (key === "Escape") fullscreen.exit();
58133
+ }
58134
+ }
58135
+ if (!priv.edit_mode) return 0;
58136
+ if (key === "Escape") {
58137
+ deselect_node(gobj);
58138
+ return 0;
58139
+ }
58140
+ if (kw.ctrl && (key === "a" || key === "A")) {
58141
+ select_all_nodes(gobj);
58142
+ return 0;
58143
+ }
58144
+ if (key === "Delete" || key === "Backspace") {
58145
+ request_delete_selection(gobj);
58146
+ return 0;
58147
+ }
58148
+ return 0;
58149
+ }
58150
+ /************************************************************
58151
+ * The rubber band let go: these are the nodes it enclosed.
58152
+ *
58153
+ * They arrive in the kw because `onSelect` runs BEFORE G6 writes
58154
+ * the state, so asking the graph here would answer the previous
58155
+ * selection.
58156
+ ************************************************************/
58157
+ function ac_brush_select(gobj, event, kw, src) {
58158
+ set_selection(gobj, kw && kw.ids || []);
58159
+ return 0;
58160
+ }
58161
+ /************************************************************
57906
58162
  * Node click - publish vertex clicked event
57907
58163
  ************************************************************/
57908
58164
  function ac_node_click(gobj, event, kw, src) {
@@ -57917,7 +58173,8 @@ function ac_node_click(gobj, event, kw, src) {
57917
58173
  topic_name: nodedata.data.desc.topic_name,
57918
58174
  record: nodedata.data.record
57919
58175
  });
57920
- if (priv.edit_mode) {
58176
+ if (priv.edit_mode && kw.evt.shiftKey) toggle_in_selection(gobj, node_id);
58177
+ else if (priv.edit_mode) {
57921
58178
  let containerRect = priv.$container.getBoundingClientRect();
57922
58179
  let canvasPoint = graph.getCanvasByViewport([kw.evt.client.x - containerRect.left, kw.evt.client.y - containerRect.top]);
57923
58180
  let port_key = detect_port_click(gobj, node_id, canvasPoint[0], canvasPoint[1]);
@@ -58144,6 +58401,16 @@ function create_gclass$1(gclass_name) {
58144
58401
  ac_node_drag_end,
58145
58402
  null
58146
58403
  ],
58404
+ [
58405
+ "EV_BRUSH_SELECT",
58406
+ ac_brush_select,
58407
+ null
58408
+ ],
58409
+ [
58410
+ "EV_KEY_DOWN",
58411
+ ac_key_down,
58412
+ null
58413
+ ],
58147
58414
  [
58148
58415
  "EV_ZOOM_IN",
58149
58416
  ac_zoom_in,
@@ -58252,6 +58519,8 @@ function create_gclass$1(gclass_name) {
58252
58519
  ["EV_NODE_CONTEXT_MENU", 0],
58253
58520
  ["EV_CANVAS_CLICK", 0],
58254
58521
  ["EV_NODE_DRAG_END", 0],
58522
+ ["EV_BRUSH_SELECT", 0],
58523
+ ["EV_KEY_DOWN", 0],
58255
58524
  ["EV_ZOOM_IN", 0],
58256
58525
  ["EV_ZOOM_OUT", 0],
58257
58526
  ["EV_ZOOM_RESET", 0],