@yuneta/gobj-ui 7.16.0 → 7.18.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 +34 -3
- package/dist/gobj-ui.cjs.js +237 -16
- package/dist/gobj-ui.es.js +237 -16
- package/package.json +1 -1
- package/src/c_g6_nodes_tree.js +345 -36
- package/src/lib_icons.js +9 -0
package/README.md
CHANGED
|
@@ -680,6 +680,8 @@ Two implementation notes worth keeping:
|
|
|
680
680
|
| fit | `fitView()`: the whole graph in the viewport |
|
|
681
681
|
| **`1:1`** | `zoomTo(1)`: actual size |
|
|
682
682
|
| ── | |
|
|
683
|
+
| **fit to selection** | `fit`, for the part that is selected (edition only, disabled while nothing is) |
|
|
684
|
+
| ── | |
|
|
683
685
|
| full screen | the container, not the camera |
|
|
684
686
|
|
|
685
687
|
Two of those rows are the answer to a real complaint, and the reasoning
|
|
@@ -695,6 +697,14 @@ generalises:
|
|
|
695
697
|
that offers it, because there is no glyph anybody recognises for it.
|
|
696
698
|
- **the zoom level is shown** because `1:1` is a jump to a number, and a jump
|
|
697
699
|
to a number is only meaningful next to the number you are on.
|
|
700
|
+
- **fit to selection** wears the `fit` icon with a marked object inside it,
|
|
701
|
+
drawn rather than borrowed: the two sit next to each other and are the same
|
|
702
|
+
action at two scopes, so they have to read as a family. It appears only in
|
|
703
|
+
edition — a button that can never be enabled is furniture, not a control —
|
|
704
|
+
and is disabled while nothing is selected. `fitView()` has no subset form,
|
|
705
|
+
so the bounds are measured off the elements and the zoom is clamped to the
|
|
706
|
+
graph's own `zoomRange`, the only limit that is not invented here. One card
|
|
707
|
+
filling the view is not a bug: that is what zooming to it means.
|
|
698
708
|
|
|
699
709
|
The **separators are gaps, not lines**: every item already carries a hairline
|
|
700
710
|
against its neighbour, so one more line would not group anything. Full screen
|
|
@@ -705,8 +715,9 @@ Both toolbars (this one and the edit one) **follow the theme**. They used to be
|
|
|
705
715
|
pinned to a light background in both themes, with the icon colour pinned dark
|
|
706
716
|
so it survived that — two light islands over a dark canvas.
|
|
707
717
|
|
|
708
|
-
**New keys for consumers: `actual size`, `zoom level
|
|
709
|
-
host that has not defined them shows the key on hover and
|
|
718
|
+
**New keys for consumers: `actual size`, `zoom level`, `zoom to selection`**
|
|
719
|
+
(all tooltips, so a host that has not defined them shows the key on hover and
|
|
720
|
+
nothing else breaks).
|
|
710
721
|
|
|
711
722
|
### Selecting several nodes, and moving them together
|
|
712
723
|
|
|
@@ -718,8 +729,17 @@ clicked":
|
|
|
718
729
|
| click a node | selects it **and opens it**: resize handles, ports, popovers |
|
|
719
730
|
| **shift + click** | adds that node to the selection, or takes it out |
|
|
720
731
|
| **shift + drag on the canvas** | rubber band: the selection becomes what it enclosed |
|
|
732
|
+
| **ctrl/cmd + A** | every node |
|
|
733
|
+
| the **fit-to-selection** button | puts the viewport on what is selected |
|
|
721
734
|
| drag any selected node | **moves the whole selection**, as one undo |
|
|
722
|
-
|
|
|
735
|
+
| **Delete** / **Backspace** | deletes the selection, after a confirmation that counts what it takes |
|
|
736
|
+
| **Esc**, or a click on the canvas | clears it |
|
|
737
|
+
|
|
738
|
+
The keys reach the graph only while the **graph has focus** — G6 gives its
|
|
739
|
+
canvas a `tabIndex` of its own — which is what keeps `ctrl+A` inside the find
|
|
740
|
+
box a selection of the TEXT and not of every node: the focus is in the input,
|
|
741
|
+
and the input is not inside the canvas. They arrive as `EV_KEY_DOWN` and the
|
|
742
|
+
action decides, so a key is as visible in the `machine` trace as a click.
|
|
723
743
|
|
|
724
744
|
Three decisions are worth knowing, because each one is where this could have
|
|
725
745
|
gone wrong:
|
|
@@ -752,6 +772,17 @@ predicate), or the canvas would pan under the rubber band — G6 binds
|
|
|
752
772
|
`drag-canvas` straight to the drag events, and its own docs warn that the two
|
|
753
773
|
gestures cannot both be a plain drag.
|
|
754
774
|
|
|
775
|
+
**A delete says what it takes, whether it is one or twenty.** The Delete key
|
|
776
|
+
asks the same question the per-node delete icon asks, built by the same
|
|
777
|
+
function: the record's key when there is one, the count when there are more,
|
|
778
|
+
and then the two lines that are actually at stake — *N children will be
|
|
779
|
+
UNLINKED, not deleted* and *it will be detached from M parents* — where over a
|
|
780
|
+
set the numbers are the sums. These views delete with `force`, so an operator
|
|
781
|
+
pressing Delete over twelve cards has to read eleven detached children, not
|
|
782
|
+
"are you sure". It needs no new keys: the sentence is the one `7.10.0` already
|
|
783
|
+
defined. The question has no icon to hang off, so it is asked in the middle of
|
|
784
|
+
the graph it is about.
|
|
785
|
+
|
|
755
786
|
**A marquee selects, it does not open.** Even when it encloses exactly one
|
|
756
787
|
node, the handles and ports stay away: `_selected_node_id` means *the node
|
|
757
788
|
opened for editing*, and only a click sets it. Everything that hangs off a
|
package/dist/gobj-ui.cjs.js
CHANGED
|
@@ -53617,6 +53617,15 @@ var ICONS = `<svg xmlns="http://www.w3.org/2000/svg">
|
|
|
53617
53617
|
<symbol id="g6-icon-fit" viewBox="0 0 448 512">
|
|
53618
53618
|
<path fill="currentColor" d="M32 32C14.3 32 0 46.3 0 64l0 96c0 17.7 14.3 32 32 32s32-14.3 32-32l0-64 64 0c17.7 0 32-14.3 32-32s-14.3-32-32-32L32 32zM64 352c0-17.7-14.3-32-32-32S0 334.3 0 352l0 96c0 17.7 14.3 32 32 32l96 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-64 0 0-64zM320 32c-17.7 0-32 14.3-32 32s14.3 32 32 32l64 0 0 64c0 17.7 14.3 32 32 32s32-14.3 32-32l0-96c0-17.7-14.3-32-32-32l-96 0zM448 352c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 64-64 0c-17.7 0-32 14.3-32 32s14.3 32 32 32l96 0c17.7 0 32-14.3 32-32l0-96z"/>
|
|
53619
53619
|
</symbol>
|
|
53620
|
+
<!-- fit to selection: the same corner brackets as the fit icon,
|
|
53621
|
+
around a marked object. The two sit side by side in the
|
|
53622
|
+
toolbar and have to read as a family, which a borrowed glyph
|
|
53623
|
+
would not. (No backticks in here: the sprite lives inside a
|
|
53624
|
+
template literal.) -->
|
|
53625
|
+
<symbol id="g6-icon-fit-selection" viewBox="0 0 448 512">
|
|
53626
|
+
<path fill="currentColor" d="M32 32C14.3 32 0 46.3 0 64l0 96c0 17.7 14.3 32 32 32s32-14.3 32-32l0-64 64 0c17.7 0 32-14.3 32-32s-14.3-32-32-32L32 32zM64 352c0-17.7-14.3-32-32-32S0 334.3 0 352l0 96c0 17.7 14.3 32 32 32l96 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-64 0 0-64zM320 32c-17.7 0-32 14.3-32 32s14.3 32 32 32l64 0 0 64c0 17.7 14.3 32 32 32s32-14.3 32-32l0-96c0-17.7-14.3-32-32-32l-96 0zM448 352c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 64-64 0c-17.7 0-32 14.3-32 32s14.3 32 32 32l96 0c17.7 0 32-14.3 32-32l0-96z"/>
|
|
53627
|
+
<rect fill="currentColor" x="144" y="176" width="160" height="160" rx="24"/>
|
|
53628
|
+
</symbol>
|
|
53620
53629
|
<!-- crosshairs (center) -->
|
|
53621
53630
|
<symbol id="g6-icon-center" viewBox="0 0 576 512">
|
|
53622
53631
|
<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-32zM131.2 288c12.7 62.7 62.1 112.1 124.8 124.8l0-12.8c0-17.7 14.3-32 32-32s32 14.3 32 32l0 12.8c62.7-12.7 112.1-62.1 124.8-124.8L432 288c-17.7 0-32-14.3-32-32s14.3-32 32-32l12.8 0C432.1 161.3 382.7 111.9 320 99.2l0 12.8c0 17.7-14.3 32-32 32s-32-14.3-32-32l0-12.8C193.3 111.9 143.9 161.3 131.2 224l12.8 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l-12.8 0zM288 208a48 48 0 1 1 0 96 48 48 0 1 1 0-96z"/>
|
|
@@ -54088,12 +54097,15 @@ function configure_events(gobj) {
|
|
|
54088
54097
|
update_toolbar(gobj);
|
|
54089
54098
|
};
|
|
54090
54099
|
i18next.default.on("languageChanged", priv._on_language_changed);
|
|
54091
|
-
|
|
54092
|
-
|
|
54093
|
-
|
|
54094
|
-
|
|
54095
|
-
|
|
54096
|
-
|
|
54100
|
+
graph.on("keydown", (evt) => {
|
|
54101
|
+
let ctrl = !!(evt.ctrlKey || evt.metaKey);
|
|
54102
|
+
if (ctrl && (evt.key === "a" || evt.key === "A")) {
|
|
54103
|
+
if (typeof evt.preventDefault === "function") evt.preventDefault();
|
|
54104
|
+
}
|
|
54105
|
+
(0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_KEY_DOWN", {
|
|
54106
|
+
key: evt.key,
|
|
54107
|
+
ctrl
|
|
54108
|
+
}, gobj);
|
|
54097
54109
|
});
|
|
54098
54110
|
}
|
|
54099
54111
|
/************************************************************
|
|
@@ -54147,6 +54159,7 @@ function update_toolbar(gobj) {
|
|
|
54147
54159
|
if (graph_get_plugin(gobj, "toolbar")) graph.updatePlugin({ key: "toolbar" });
|
|
54148
54160
|
configure_toolbar_edit(gobj);
|
|
54149
54161
|
if (graph_get_plugin(gobj, "toolbar-edit")) graph.updatePlugin({ key: "toolbar-edit" });
|
|
54162
|
+
update_zoom_selection_button(gobj);
|
|
54150
54163
|
}
|
|
54151
54164
|
/************************************************************
|
|
54152
54165
|
* The floating toolbars follow the theme.
|
|
@@ -54247,6 +54260,13 @@ function configure_toolbar(gobj) {
|
|
|
54247
54260
|
title: (0, i18next.t)("actual size")
|
|
54248
54261
|
}
|
|
54249
54262
|
];
|
|
54263
|
+
if (priv.edit_mode) items.push({
|
|
54264
|
+
id: "g6-icon-fit-selection",
|
|
54265
|
+
value: "fit-selection",
|
|
54266
|
+
className: "EV_ZOOM_SELECTION",
|
|
54267
|
+
title: (0, i18next.t)("zoom to selection"),
|
|
54268
|
+
disabled: true
|
|
54269
|
+
});
|
|
54250
54270
|
if ((0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_fullscreen")) {
|
|
54251
54271
|
items.push({ separator: true });
|
|
54252
54272
|
if (priv.is_fullscreen) items.push({
|
|
@@ -54278,6 +54298,9 @@ function configure_toolbar(gobj) {
|
|
|
54278
54298
|
case "auto-fit":
|
|
54279
54299
|
(0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_AUTO_FIT", {}, gobj);
|
|
54280
54300
|
break;
|
|
54301
|
+
case "fit-selection":
|
|
54302
|
+
(0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_ZOOM_SELECTION", {}, gobj);
|
|
54303
|
+
break;
|
|
54281
54304
|
case "center":
|
|
54282
54305
|
(0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_CENTER", {}, gobj);
|
|
54283
54306
|
break;
|
|
@@ -55083,6 +55106,52 @@ async function graph_fit_readable(gobj) {
|
|
|
55083
55106
|
}
|
|
55084
55107
|
}
|
|
55085
55108
|
/************************************************************
|
|
55109
|
+
* Put the viewport on the selection: what `fit` does, for a part.
|
|
55110
|
+
*
|
|
55111
|
+
* Computed rather than delegated because `fitView()` fits the whole
|
|
55112
|
+
* graph and there is no subset form of it. The bounds come from the
|
|
55113
|
+
* elements themselves, so a node's real card size is what is
|
|
55114
|
+
* measured, not an assumed one; the zoom is clamped to the graph's
|
|
55115
|
+
* own `zoomRange`, which is the only limit that is not invented
|
|
55116
|
+
* here. A selection of one card filling the view is not a bug --
|
|
55117
|
+
* that is what zooming to it means, and it is what every editor
|
|
55118
|
+
* that offers the action does.
|
|
55119
|
+
************************************************************/
|
|
55120
|
+
async function graph_fit_selection(gobj, ids) {
|
|
55121
|
+
let graph = gobj.priv.graph;
|
|
55122
|
+
if (!graph || !ids || !ids.length) return;
|
|
55123
|
+
let minx = Infinity, miny = Infinity, maxx = -Infinity, maxy = -Infinity;
|
|
55124
|
+
let found = 0;
|
|
55125
|
+
for (let id of ids) {
|
|
55126
|
+
let bounds;
|
|
55127
|
+
try {
|
|
55128
|
+
bounds = graph.getElementRenderBounds(id);
|
|
55129
|
+
} catch (e) {
|
|
55130
|
+
continue;
|
|
55131
|
+
}
|
|
55132
|
+
if (!bounds || !bounds.min || !bounds.max) continue;
|
|
55133
|
+
minx = Math.min(minx, bounds.min[0]);
|
|
55134
|
+
miny = Math.min(miny, bounds.min[1]);
|
|
55135
|
+
maxx = Math.max(maxx, bounds.max[0]);
|
|
55136
|
+
maxy = Math.max(maxy, bounds.max[1]);
|
|
55137
|
+
found++;
|
|
55138
|
+
}
|
|
55139
|
+
if (!found) {
|
|
55140
|
+
(0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: the selection has no bounds to fit`);
|
|
55141
|
+
return;
|
|
55142
|
+
}
|
|
55143
|
+
let size = graph.getSize();
|
|
55144
|
+
let vw = Math.max(1, size[0] - 96);
|
|
55145
|
+
let vh = Math.max(1, size[1] - 96);
|
|
55146
|
+
let bw = Math.max(1, maxx - minx);
|
|
55147
|
+
let bh = Math.max(1, maxy - miny);
|
|
55148
|
+
let zoom = Math.min(vw / bw, vh / bh);
|
|
55149
|
+
let range = graph.getZoomRange() || [.2, 4];
|
|
55150
|
+
zoom = Math.max(range[0], Math.min(range[1], zoom));
|
|
55151
|
+
await graph.zoomTo(zoom);
|
|
55152
|
+
await graph.focusElement(ids);
|
|
55153
|
+
}
|
|
55154
|
+
/************************************************************
|
|
55086
55155
|
* Focus the graph on one topic: highlight (amber 'active' state)
|
|
55087
55156
|
* every node of that topic and centre the viewport on them. An
|
|
55088
55157
|
* empty topic clears the highlight. If the graph is not rendered
|
|
@@ -55470,10 +55539,17 @@ function create_form_button_row(parent, buttons) {
|
|
|
55470
55539
|
function show_confirm_popover(gobj, target_el, message, confirm_text, confirm_color, onConfirm, priv_key) {
|
|
55471
55540
|
hide_overlay(gobj, priv_key);
|
|
55472
55541
|
let priv = gobj.priv;
|
|
55473
|
-
if (!target_el) return;
|
|
55474
|
-
let iconRect = target_el.getBoundingClientRect();
|
|
55475
55542
|
let containerRect = priv.$container.getBoundingClientRect();
|
|
55476
|
-
|
|
55543
|
+
let left, top;
|
|
55544
|
+
if (target_el) {
|
|
55545
|
+
let iconRect = target_el.getBoundingClientRect();
|
|
55546
|
+
left = iconRect.right - containerRect.left + 6;
|
|
55547
|
+
top = iconRect.top - containerRect.top - 4;
|
|
55548
|
+
} else {
|
|
55549
|
+
left = Math.round(containerRect.width / 2) - 110;
|
|
55550
|
+
top = Math.round(containerRect.height / 4);
|
|
55551
|
+
}
|
|
55552
|
+
const popover = create_popover_base(left, top, "g6-confirm-popover", "#ff4d4f", 160);
|
|
55477
55553
|
let msg = document.createElement("div");
|
|
55478
55554
|
msg.style.cssText = "margin-bottom:10px;font-weight:500;white-space:pre-line;";
|
|
55479
55555
|
msg.textContent = message;
|
|
@@ -55544,6 +55620,23 @@ function paint_selection(gobj, ids) {
|
|
|
55544
55620
|
let next = ids || [];
|
|
55545
55621
|
priv._selected_paint_ids = next;
|
|
55546
55622
|
repaint_cards(gobj, /* @__PURE__ */ new Set([...prev, ...next]));
|
|
55623
|
+
update_zoom_selection_button(gobj);
|
|
55624
|
+
}
|
|
55625
|
+
/************************************************************
|
|
55626
|
+
* Zoom-to-selection can only act on a selection.
|
|
55627
|
+
*
|
|
55628
|
+
* The attribute is toggled on the element instead of re-rendering
|
|
55629
|
+
* the toolbar, which would rebuild its innerHTML -- and with it
|
|
55630
|
+
* drop the disabled state of every OTHER button -- each time a
|
|
55631
|
+
* card is ticked.
|
|
55632
|
+
************************************************************/
|
|
55633
|
+
function update_zoom_selection_button(gobj) {
|
|
55634
|
+
let priv = gobj.priv;
|
|
55635
|
+
let $container = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container");
|
|
55636
|
+
if (!$container) return;
|
|
55637
|
+
if (!$container.querySelector(".EV_ZOOM_SELECTION")) return;
|
|
55638
|
+
if ((priv._selected_paint_ids || []).length > 0) enableElements($container, ".EV_ZOOM_SELECTION");
|
|
55639
|
+
else disableElements($container, ".EV_ZOOM_SELECTION");
|
|
55547
55640
|
}
|
|
55548
55641
|
/************************************************************
|
|
55549
55642
|
* Select a SET of nodes: what a marquee and a shift-click do.
|
|
@@ -57157,17 +57250,89 @@ function execute_delete_node(gobj, nodeData) {
|
|
|
57157
57250
|
deselect_node(gobj);
|
|
57158
57251
|
}
|
|
57159
57252
|
/************************************************************
|
|
57253
|
+
* Every node in the graph.
|
|
57254
|
+
************************************************************/
|
|
57255
|
+
function select_all_nodes(gobj) {
|
|
57256
|
+
let graph = gobj.priv.graph;
|
|
57257
|
+
if (!graph) return;
|
|
57258
|
+
set_selection(gobj, ((graph.getData() || {}).nodes || []).map((nd) => nd.id));
|
|
57259
|
+
}
|
|
57260
|
+
/************************************************************
|
|
57261
|
+
* The nodes currently selected, as their node data.
|
|
57262
|
+
************************************************************/
|
|
57263
|
+
function selected_nodes_data(gobj) {
|
|
57264
|
+
let priv = gobj.priv;
|
|
57265
|
+
let graph = priv.graph;
|
|
57266
|
+
let ids = new Set(selected_node_ids(gobj));
|
|
57267
|
+
for (let id of priv._selected_paint_ids || []) ids.add(id);
|
|
57268
|
+
if (priv._selected_node_id) ids.add(priv._selected_node_id);
|
|
57269
|
+
let out = [];
|
|
57270
|
+
for (let id of ids) {
|
|
57271
|
+
let nd;
|
|
57272
|
+
try {
|
|
57273
|
+
nd = graph.getNodeData(id);
|
|
57274
|
+
} catch (e) {
|
|
57275
|
+
continue;
|
|
57276
|
+
}
|
|
57277
|
+
if (nd && nd.data && nd.data.desc) out.push(nd);
|
|
57278
|
+
}
|
|
57279
|
+
return out;
|
|
57280
|
+
}
|
|
57281
|
+
/************************************************************
|
|
57282
|
+
* What a delete takes with it, for ONE node or for a set.
|
|
57283
|
+
*
|
|
57284
|
+
* Same sentence in both cases and the same keys, because it is the
|
|
57285
|
+
* same warning: these views delete with `force`, which UNLINKS the
|
|
57286
|
+
* children (they survive, loose) and cleans the node off its
|
|
57287
|
+
* parents. Over a set the two numbers are the sums -- an operator
|
|
57288
|
+
* about to detach eleven records has to read eleven, not "are you
|
|
57289
|
+
* sure".
|
|
57290
|
+
************************************************************/
|
|
57291
|
+
function delete_question(nodes) {
|
|
57292
|
+
let children = 0;
|
|
57293
|
+
let parents = 0;
|
|
57294
|
+
for (let nd of nodes) {
|
|
57295
|
+
let impact = delete_impact(nd.data.desc, nd.data.record || {});
|
|
57296
|
+
children += impact.children;
|
|
57297
|
+
parents += impact.parents;
|
|
57298
|
+
}
|
|
57299
|
+
let question;
|
|
57300
|
+
if (nodes.length === 1) {
|
|
57301
|
+
let nd = nodes[0];
|
|
57302
|
+
question = (0, i18next.t)("delete") + " " + nd.data.desc.topic_name + ": " + (nd.data.record || {}).id + "?";
|
|
57303
|
+
} else question = (0, i18next.t)("delete") + " " + nodes.length + " " + (0, i18next.t)("records") + "?";
|
|
57304
|
+
if (children > 0) question += "\n" + children + " " + (0, i18next.t)("children will be unlinked, not deleted", { count: children });
|
|
57305
|
+
if (parents > 0) question += "\n" + (0, i18next.t)("it will be detached from") + " " + parents + " " + (0, i18next.t)("parents", { count: parents });
|
|
57306
|
+
return question;
|
|
57307
|
+
}
|
|
57308
|
+
/************************************************************
|
|
57309
|
+
* Delete every selected node (the Delete key).
|
|
57310
|
+
************************************************************/
|
|
57311
|
+
function request_delete_selection(gobj) {
|
|
57312
|
+
let nodes = selected_nodes_data(gobj);
|
|
57313
|
+
if (!nodes.length) return;
|
|
57314
|
+
if (!(0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "confirm_delete_node")) {
|
|
57315
|
+
execute_delete_selection(gobj, nodes);
|
|
57316
|
+
return;
|
|
57317
|
+
}
|
|
57318
|
+
show_confirm_popover(gobj, null, delete_question(nodes), "delete", "#ff4d4f", () => execute_delete_selection(gobj, nodes), "_delete_confirm_el");
|
|
57319
|
+
}
|
|
57320
|
+
function execute_delete_selection(gobj, nodes) {
|
|
57321
|
+
let priv = gobj.priv;
|
|
57322
|
+
hide_delete_confirm(gobj);
|
|
57323
|
+
for (let nd of nodes) (0, _yuneta_gobj_js.gobj_publish_event)(gobj, "EV_DELETE_NODE", {
|
|
57324
|
+
treedb_name: priv.treedb_name,
|
|
57325
|
+
topic_name: nd.data.desc.topic_name,
|
|
57326
|
+
record: nd.data.record
|
|
57327
|
+
});
|
|
57328
|
+
deselect_node(gobj);
|
|
57329
|
+
}
|
|
57330
|
+
/************************************************************
|
|
57160
57331
|
* Show a confirmation popover next to the delete icon.
|
|
57161
57332
|
************************************************************/
|
|
57162
57333
|
function show_delete_confirm(gobj, nodeData) {
|
|
57163
57334
|
let priv = gobj.priv;
|
|
57164
|
-
|
|
57165
|
-
let topic_name = nodeData.data.desc.topic_name;
|
|
57166
|
-
let impact = delete_impact(nodeData.data.desc, record);
|
|
57167
|
-
let question = (0, i18next.t)("delete") + " " + topic_name + ": " + record.id + "?";
|
|
57168
|
-
if (impact.children > 0) question += "\n" + impact.children + " " + (0, i18next.t)("children will be unlinked, not deleted", { count: impact.children });
|
|
57169
|
-
if (impact.parents > 0) question += "\n" + (0, i18next.t)("it will be detached from") + " " + impact.parents + " " + (0, i18next.t)("parents", { count: impact.parents });
|
|
57170
|
-
show_confirm_popover(gobj, priv._node_delete_el, question, "delete", "#ff4d4f", () => execute_delete_node(gobj, nodeData), "_delete_confirm_el");
|
|
57335
|
+
show_confirm_popover(gobj, priv._node_delete_el, delete_question([nodeData]), "delete", "#ff4d4f", () => execute_delete_node(gobj, nodeData), "_delete_confirm_el");
|
|
57171
57336
|
}
|
|
57172
57337
|
function hide_delete_confirm(gobj) {
|
|
57173
57338
|
hide_overlay(gobj, "_delete_confirm_el");
|
|
@@ -57976,6 +58141,18 @@ function ac_auto_fit(gobj, event, kw, src) {
|
|
|
57976
58141
|
graph_fitview(gobj);
|
|
57977
58142
|
return 0;
|
|
57978
58143
|
}
|
|
58144
|
+
/************************************************************
|
|
58145
|
+
* Zoom to what is selected.
|
|
58146
|
+
************************************************************/
|
|
58147
|
+
function ac_zoom_selection(gobj, event, kw, src) {
|
|
58148
|
+
let ids = gobj.priv._selected_paint_ids || [];
|
|
58149
|
+
if (!ids.length) {
|
|
58150
|
+
(0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: zoom to selection with nothing selected`);
|
|
58151
|
+
return -1;
|
|
58152
|
+
}
|
|
58153
|
+
graph_fit_selection(gobj, ids);
|
|
58154
|
+
return 0;
|
|
58155
|
+
}
|
|
57979
58156
|
function ac_focus_topic(gobj, event, kw, src) {
|
|
57980
58157
|
graph_focus_topic(gobj, kw && kw.topic);
|
|
57981
58158
|
return 0;
|
|
@@ -58034,6 +58211,38 @@ function ac_node_drag_end(gobj, event, kw, src) {
|
|
|
58034
58211
|
return 0;
|
|
58035
58212
|
}
|
|
58036
58213
|
/************************************************************
|
|
58214
|
+
* A key, while the graph has focus.
|
|
58215
|
+
*
|
|
58216
|
+
* Full screen keeps the two keys it always had. The rest belong to
|
|
58217
|
+
* the selection, so they only mean anything in edition -- outside
|
|
58218
|
+
* it there is nothing selected to clear, select or delete.
|
|
58219
|
+
************************************************************/
|
|
58220
|
+
function ac_key_down(gobj, event, kw, src) {
|
|
58221
|
+
let priv = gobj.priv;
|
|
58222
|
+
let key = kw && kw.key || "";
|
|
58223
|
+
if ((0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_fullscreen")) {
|
|
58224
|
+
let fullscreen = graph_get_plugin(gobj, "fullscreen");
|
|
58225
|
+
if (fullscreen) {
|
|
58226
|
+
if (key === "F" || key === "f") fullscreen.request();
|
|
58227
|
+
else if (key === "Escape") fullscreen.exit();
|
|
58228
|
+
}
|
|
58229
|
+
}
|
|
58230
|
+
if (!priv.edit_mode) return 0;
|
|
58231
|
+
if (key === "Escape") {
|
|
58232
|
+
deselect_node(gobj);
|
|
58233
|
+
return 0;
|
|
58234
|
+
}
|
|
58235
|
+
if (kw.ctrl && (key === "a" || key === "A")) {
|
|
58236
|
+
select_all_nodes(gobj);
|
|
58237
|
+
return 0;
|
|
58238
|
+
}
|
|
58239
|
+
if (key === "Delete" || key === "Backspace") {
|
|
58240
|
+
request_delete_selection(gobj);
|
|
58241
|
+
return 0;
|
|
58242
|
+
}
|
|
58243
|
+
return 0;
|
|
58244
|
+
}
|
|
58245
|
+
/************************************************************
|
|
58037
58246
|
* The rubber band let go: these are the nodes it enclosed.
|
|
58038
58247
|
*
|
|
58039
58248
|
* They arrive in the kw because `onSelect` runs BEFORE G6 writes
|
|
@@ -58292,6 +58501,11 @@ function create_gclass$1(gclass_name) {
|
|
|
58292
58501
|
ac_brush_select,
|
|
58293
58502
|
null
|
|
58294
58503
|
],
|
|
58504
|
+
[
|
|
58505
|
+
"EV_KEY_DOWN",
|
|
58506
|
+
ac_key_down,
|
|
58507
|
+
null
|
|
58508
|
+
],
|
|
58295
58509
|
[
|
|
58296
58510
|
"EV_ZOOM_IN",
|
|
58297
58511
|
ac_zoom_in,
|
|
@@ -58312,6 +58526,11 @@ function create_gclass$1(gclass_name) {
|
|
|
58312
58526
|
ac_auto_fit,
|
|
58313
58527
|
null
|
|
58314
58528
|
],
|
|
58529
|
+
[
|
|
58530
|
+
"EV_ZOOM_SELECTION",
|
|
58531
|
+
ac_zoom_selection,
|
|
58532
|
+
null
|
|
58533
|
+
],
|
|
58315
58534
|
[
|
|
58316
58535
|
"EV_FOCUS_TOPIC",
|
|
58317
58536
|
ac_focus_topic,
|
|
@@ -58401,10 +58620,12 @@ function create_gclass$1(gclass_name) {
|
|
|
58401
58620
|
["EV_CANVAS_CLICK", 0],
|
|
58402
58621
|
["EV_NODE_DRAG_END", 0],
|
|
58403
58622
|
["EV_BRUSH_SELECT", 0],
|
|
58623
|
+
["EV_KEY_DOWN", 0],
|
|
58404
58624
|
["EV_ZOOM_IN", 0],
|
|
58405
58625
|
["EV_ZOOM_OUT", 0],
|
|
58406
58626
|
["EV_ZOOM_RESET", 0],
|
|
58407
58627
|
["EV_AUTO_FIT", 0],
|
|
58628
|
+
["EV_ZOOM_SELECTION", 0],
|
|
58408
58629
|
["EV_FOCUS_TOPIC", 0],
|
|
58409
58630
|
["EV_FIND_NODES", 0],
|
|
58410
58631
|
["EV_FIND_RESULT", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT],
|
package/dist/gobj-ui.es.js
CHANGED
|
@@ -53604,6 +53604,15 @@ var ICONS = `<svg xmlns="http://www.w3.org/2000/svg">
|
|
|
53604
53604
|
<symbol id="g6-icon-fit" viewBox="0 0 448 512">
|
|
53605
53605
|
<path fill="currentColor" d="M32 32C14.3 32 0 46.3 0 64l0 96c0 17.7 14.3 32 32 32s32-14.3 32-32l0-64 64 0c17.7 0 32-14.3 32-32s-14.3-32-32-32L32 32zM64 352c0-17.7-14.3-32-32-32S0 334.3 0 352l0 96c0 17.7 14.3 32 32 32l96 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-64 0 0-64zM320 32c-17.7 0-32 14.3-32 32s14.3 32 32 32l64 0 0 64c0 17.7 14.3 32 32 32s32-14.3 32-32l0-96c0-17.7-14.3-32-32-32l-96 0zM448 352c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 64-64 0c-17.7 0-32 14.3-32 32s14.3 32 32 32l96 0c17.7 0 32-14.3 32-32l0-96z"/>
|
|
53606
53606
|
</symbol>
|
|
53607
|
+
<!-- fit to selection: the same corner brackets as the fit icon,
|
|
53608
|
+
around a marked object. The two sit side by side in the
|
|
53609
|
+
toolbar and have to read as a family, which a borrowed glyph
|
|
53610
|
+
would not. (No backticks in here: the sprite lives inside a
|
|
53611
|
+
template literal.) -->
|
|
53612
|
+
<symbol id="g6-icon-fit-selection" viewBox="0 0 448 512">
|
|
53613
|
+
<path fill="currentColor" d="M32 32C14.3 32 0 46.3 0 64l0 96c0 17.7 14.3 32 32 32s32-14.3 32-32l0-64 64 0c17.7 0 32-14.3 32-32s-14.3-32-32-32L32 32zM64 352c0-17.7-14.3-32-32-32S0 334.3 0 352l0 96c0 17.7 14.3 32 32 32l96 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-64 0 0-64zM320 32c-17.7 0-32 14.3-32 32s14.3 32 32 32l64 0 0 64c0 17.7 14.3 32 32 32s32-14.3 32-32l0-96c0-17.7-14.3-32-32-32l-96 0zM448 352c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 64-64 0c-17.7 0-32 14.3-32 32s14.3 32 32 32l96 0c17.7 0 32-14.3 32-32l0-96z"/>
|
|
53614
|
+
<rect fill="currentColor" x="144" y="176" width="160" height="160" rx="24"/>
|
|
53615
|
+
</symbol>
|
|
53607
53616
|
<!-- crosshairs (center) -->
|
|
53608
53617
|
<symbol id="g6-icon-center" viewBox="0 0 576 512">
|
|
53609
53618
|
<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-32zM131.2 288c12.7 62.7 62.1 112.1 124.8 124.8l0-12.8c0-17.7 14.3-32 32-32s32 14.3 32 32l0 12.8c62.7-12.7 112.1-62.1 124.8-124.8L432 288c-17.7 0-32-14.3-32-32s14.3-32 32-32l12.8 0C432.1 161.3 382.7 111.9 320 99.2l0 12.8c0 17.7-14.3 32-32 32s-32-14.3-32-32l0-12.8C193.3 111.9 143.9 161.3 131.2 224l12.8 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l-12.8 0zM288 208a48 48 0 1 1 0 96 48 48 0 1 1 0-96z"/>
|
|
@@ -54075,12 +54084,15 @@ function configure_events(gobj) {
|
|
|
54075
54084
|
update_toolbar(gobj);
|
|
54076
54085
|
};
|
|
54077
54086
|
i18next.on("languageChanged", priv._on_language_changed);
|
|
54078
|
-
|
|
54079
|
-
|
|
54080
|
-
|
|
54081
|
-
|
|
54082
|
-
|
|
54083
|
-
|
|
54087
|
+
graph.on("keydown", (evt) => {
|
|
54088
|
+
let ctrl = !!(evt.ctrlKey || evt.metaKey);
|
|
54089
|
+
if (ctrl && (evt.key === "a" || evt.key === "A")) {
|
|
54090
|
+
if (typeof evt.preventDefault === "function") evt.preventDefault();
|
|
54091
|
+
}
|
|
54092
|
+
gobj_send_event(gobj, "EV_KEY_DOWN", {
|
|
54093
|
+
key: evt.key,
|
|
54094
|
+
ctrl
|
|
54095
|
+
}, gobj);
|
|
54084
54096
|
});
|
|
54085
54097
|
}
|
|
54086
54098
|
/************************************************************
|
|
@@ -54134,6 +54146,7 @@ function update_toolbar(gobj) {
|
|
|
54134
54146
|
if (graph_get_plugin(gobj, "toolbar")) graph.updatePlugin({ key: "toolbar" });
|
|
54135
54147
|
configure_toolbar_edit(gobj);
|
|
54136
54148
|
if (graph_get_plugin(gobj, "toolbar-edit")) graph.updatePlugin({ key: "toolbar-edit" });
|
|
54149
|
+
update_zoom_selection_button(gobj);
|
|
54137
54150
|
}
|
|
54138
54151
|
/************************************************************
|
|
54139
54152
|
* The floating toolbars follow the theme.
|
|
@@ -54234,6 +54247,13 @@ function configure_toolbar(gobj) {
|
|
|
54234
54247
|
title: t("actual size")
|
|
54235
54248
|
}
|
|
54236
54249
|
];
|
|
54250
|
+
if (priv.edit_mode) items.push({
|
|
54251
|
+
id: "g6-icon-fit-selection",
|
|
54252
|
+
value: "fit-selection",
|
|
54253
|
+
className: "EV_ZOOM_SELECTION",
|
|
54254
|
+
title: t("zoom to selection"),
|
|
54255
|
+
disabled: true
|
|
54256
|
+
});
|
|
54237
54257
|
if (gobj_read_bool_attr(gobj, "with_fullscreen")) {
|
|
54238
54258
|
items.push({ separator: true });
|
|
54239
54259
|
if (priv.is_fullscreen) items.push({
|
|
@@ -54265,6 +54285,9 @@ function configure_toolbar(gobj) {
|
|
|
54265
54285
|
case "auto-fit":
|
|
54266
54286
|
gobj_send_event(gobj, "EV_AUTO_FIT", {}, gobj);
|
|
54267
54287
|
break;
|
|
54288
|
+
case "fit-selection":
|
|
54289
|
+
gobj_send_event(gobj, "EV_ZOOM_SELECTION", {}, gobj);
|
|
54290
|
+
break;
|
|
54268
54291
|
case "center":
|
|
54269
54292
|
gobj_send_event(gobj, "EV_CENTER", {}, gobj);
|
|
54270
54293
|
break;
|
|
@@ -55070,6 +55093,52 @@ async function graph_fit_readable(gobj) {
|
|
|
55070
55093
|
}
|
|
55071
55094
|
}
|
|
55072
55095
|
/************************************************************
|
|
55096
|
+
* Put the viewport on the selection: what `fit` does, for a part.
|
|
55097
|
+
*
|
|
55098
|
+
* Computed rather than delegated because `fitView()` fits the whole
|
|
55099
|
+
* graph and there is no subset form of it. The bounds come from the
|
|
55100
|
+
* elements themselves, so a node's real card size is what is
|
|
55101
|
+
* measured, not an assumed one; the zoom is clamped to the graph's
|
|
55102
|
+
* own `zoomRange`, which is the only limit that is not invented
|
|
55103
|
+
* here. A selection of one card filling the view is not a bug --
|
|
55104
|
+
* that is what zooming to it means, and it is what every editor
|
|
55105
|
+
* that offers the action does.
|
|
55106
|
+
************************************************************/
|
|
55107
|
+
async function graph_fit_selection(gobj, ids) {
|
|
55108
|
+
let graph = gobj.priv.graph;
|
|
55109
|
+
if (!graph || !ids || !ids.length) return;
|
|
55110
|
+
let minx = Infinity, miny = Infinity, maxx = -Infinity, maxy = -Infinity;
|
|
55111
|
+
let found = 0;
|
|
55112
|
+
for (let id of ids) {
|
|
55113
|
+
let bounds;
|
|
55114
|
+
try {
|
|
55115
|
+
bounds = graph.getElementRenderBounds(id);
|
|
55116
|
+
} catch (e) {
|
|
55117
|
+
continue;
|
|
55118
|
+
}
|
|
55119
|
+
if (!bounds || !bounds.min || !bounds.max) continue;
|
|
55120
|
+
minx = Math.min(minx, bounds.min[0]);
|
|
55121
|
+
miny = Math.min(miny, bounds.min[1]);
|
|
55122
|
+
maxx = Math.max(maxx, bounds.max[0]);
|
|
55123
|
+
maxy = Math.max(maxy, bounds.max[1]);
|
|
55124
|
+
found++;
|
|
55125
|
+
}
|
|
55126
|
+
if (!found) {
|
|
55127
|
+
log_error(`${gobj_short_name(gobj)}: the selection has no bounds to fit`);
|
|
55128
|
+
return;
|
|
55129
|
+
}
|
|
55130
|
+
let size = graph.getSize();
|
|
55131
|
+
let vw = Math.max(1, size[0] - 96);
|
|
55132
|
+
let vh = Math.max(1, size[1] - 96);
|
|
55133
|
+
let bw = Math.max(1, maxx - minx);
|
|
55134
|
+
let bh = Math.max(1, maxy - miny);
|
|
55135
|
+
let zoom = Math.min(vw / bw, vh / bh);
|
|
55136
|
+
let range = graph.getZoomRange() || [.2, 4];
|
|
55137
|
+
zoom = Math.max(range[0], Math.min(range[1], zoom));
|
|
55138
|
+
await graph.zoomTo(zoom);
|
|
55139
|
+
await graph.focusElement(ids);
|
|
55140
|
+
}
|
|
55141
|
+
/************************************************************
|
|
55073
55142
|
* Focus the graph on one topic: highlight (amber 'active' state)
|
|
55074
55143
|
* every node of that topic and centre the viewport on them. An
|
|
55075
55144
|
* empty topic clears the highlight. If the graph is not rendered
|
|
@@ -55457,10 +55526,17 @@ function create_form_button_row(parent, buttons) {
|
|
|
55457
55526
|
function show_confirm_popover(gobj, target_el, message, confirm_text, confirm_color, onConfirm, priv_key) {
|
|
55458
55527
|
hide_overlay(gobj, priv_key);
|
|
55459
55528
|
let priv = gobj.priv;
|
|
55460
|
-
if (!target_el) return;
|
|
55461
|
-
let iconRect = target_el.getBoundingClientRect();
|
|
55462
55529
|
let containerRect = priv.$container.getBoundingClientRect();
|
|
55463
|
-
|
|
55530
|
+
let left, top;
|
|
55531
|
+
if (target_el) {
|
|
55532
|
+
let iconRect = target_el.getBoundingClientRect();
|
|
55533
|
+
left = iconRect.right - containerRect.left + 6;
|
|
55534
|
+
top = iconRect.top - containerRect.top - 4;
|
|
55535
|
+
} else {
|
|
55536
|
+
left = Math.round(containerRect.width / 2) - 110;
|
|
55537
|
+
top = Math.round(containerRect.height / 4);
|
|
55538
|
+
}
|
|
55539
|
+
const popover = create_popover_base(left, top, "g6-confirm-popover", "#ff4d4f", 160);
|
|
55464
55540
|
let msg = document.createElement("div");
|
|
55465
55541
|
msg.style.cssText = "margin-bottom:10px;font-weight:500;white-space:pre-line;";
|
|
55466
55542
|
msg.textContent = message;
|
|
@@ -55531,6 +55607,23 @@ function paint_selection(gobj, ids) {
|
|
|
55531
55607
|
let next = ids || [];
|
|
55532
55608
|
priv._selected_paint_ids = next;
|
|
55533
55609
|
repaint_cards(gobj, /* @__PURE__ */ new Set([...prev, ...next]));
|
|
55610
|
+
update_zoom_selection_button(gobj);
|
|
55611
|
+
}
|
|
55612
|
+
/************************************************************
|
|
55613
|
+
* Zoom-to-selection can only act on a selection.
|
|
55614
|
+
*
|
|
55615
|
+
* The attribute is toggled on the element instead of re-rendering
|
|
55616
|
+
* the toolbar, which would rebuild its innerHTML -- and with it
|
|
55617
|
+
* drop the disabled state of every OTHER button -- each time a
|
|
55618
|
+
* card is ticked.
|
|
55619
|
+
************************************************************/
|
|
55620
|
+
function update_zoom_selection_button(gobj) {
|
|
55621
|
+
let priv = gobj.priv;
|
|
55622
|
+
let $container = gobj_read_attr(gobj, "$container");
|
|
55623
|
+
if (!$container) return;
|
|
55624
|
+
if (!$container.querySelector(".EV_ZOOM_SELECTION")) return;
|
|
55625
|
+
if ((priv._selected_paint_ids || []).length > 0) enableElements($container, ".EV_ZOOM_SELECTION");
|
|
55626
|
+
else disableElements($container, ".EV_ZOOM_SELECTION");
|
|
55534
55627
|
}
|
|
55535
55628
|
/************************************************************
|
|
55536
55629
|
* Select a SET of nodes: what a marquee and a shift-click do.
|
|
@@ -57144,17 +57237,89 @@ function execute_delete_node(gobj, nodeData) {
|
|
|
57144
57237
|
deselect_node(gobj);
|
|
57145
57238
|
}
|
|
57146
57239
|
/************************************************************
|
|
57240
|
+
* Every node in the graph.
|
|
57241
|
+
************************************************************/
|
|
57242
|
+
function select_all_nodes(gobj) {
|
|
57243
|
+
let graph = gobj.priv.graph;
|
|
57244
|
+
if (!graph) return;
|
|
57245
|
+
set_selection(gobj, ((graph.getData() || {}).nodes || []).map((nd) => nd.id));
|
|
57246
|
+
}
|
|
57247
|
+
/************************************************************
|
|
57248
|
+
* The nodes currently selected, as their node data.
|
|
57249
|
+
************************************************************/
|
|
57250
|
+
function selected_nodes_data(gobj) {
|
|
57251
|
+
let priv = gobj.priv;
|
|
57252
|
+
let graph = priv.graph;
|
|
57253
|
+
let ids = new Set(selected_node_ids(gobj));
|
|
57254
|
+
for (let id of priv._selected_paint_ids || []) ids.add(id);
|
|
57255
|
+
if (priv._selected_node_id) ids.add(priv._selected_node_id);
|
|
57256
|
+
let out = [];
|
|
57257
|
+
for (let id of ids) {
|
|
57258
|
+
let nd;
|
|
57259
|
+
try {
|
|
57260
|
+
nd = graph.getNodeData(id);
|
|
57261
|
+
} catch (e) {
|
|
57262
|
+
continue;
|
|
57263
|
+
}
|
|
57264
|
+
if (nd && nd.data && nd.data.desc) out.push(nd);
|
|
57265
|
+
}
|
|
57266
|
+
return out;
|
|
57267
|
+
}
|
|
57268
|
+
/************************************************************
|
|
57269
|
+
* What a delete takes with it, for ONE node or for a set.
|
|
57270
|
+
*
|
|
57271
|
+
* Same sentence in both cases and the same keys, because it is the
|
|
57272
|
+
* same warning: these views delete with `force`, which UNLINKS the
|
|
57273
|
+
* children (they survive, loose) and cleans the node off its
|
|
57274
|
+
* parents. Over a set the two numbers are the sums -- an operator
|
|
57275
|
+
* about to detach eleven records has to read eleven, not "are you
|
|
57276
|
+
* sure".
|
|
57277
|
+
************************************************************/
|
|
57278
|
+
function delete_question(nodes) {
|
|
57279
|
+
let children = 0;
|
|
57280
|
+
let parents = 0;
|
|
57281
|
+
for (let nd of nodes) {
|
|
57282
|
+
let impact = delete_impact(nd.data.desc, nd.data.record || {});
|
|
57283
|
+
children += impact.children;
|
|
57284
|
+
parents += impact.parents;
|
|
57285
|
+
}
|
|
57286
|
+
let question;
|
|
57287
|
+
if (nodes.length === 1) {
|
|
57288
|
+
let nd = nodes[0];
|
|
57289
|
+
question = t("delete") + " " + nd.data.desc.topic_name + ": " + (nd.data.record || {}).id + "?";
|
|
57290
|
+
} else question = t("delete") + " " + nodes.length + " " + t("records") + "?";
|
|
57291
|
+
if (children > 0) question += "\n" + children + " " + t("children will be unlinked, not deleted", { count: children });
|
|
57292
|
+
if (parents > 0) question += "\n" + t("it will be detached from") + " " + parents + " " + t("parents", { count: parents });
|
|
57293
|
+
return question;
|
|
57294
|
+
}
|
|
57295
|
+
/************************************************************
|
|
57296
|
+
* Delete every selected node (the Delete key).
|
|
57297
|
+
************************************************************/
|
|
57298
|
+
function request_delete_selection(gobj) {
|
|
57299
|
+
let nodes = selected_nodes_data(gobj);
|
|
57300
|
+
if (!nodes.length) return;
|
|
57301
|
+
if (!gobj_read_bool_attr(gobj, "confirm_delete_node")) {
|
|
57302
|
+
execute_delete_selection(gobj, nodes);
|
|
57303
|
+
return;
|
|
57304
|
+
}
|
|
57305
|
+
show_confirm_popover(gobj, null, delete_question(nodes), "delete", "#ff4d4f", () => execute_delete_selection(gobj, nodes), "_delete_confirm_el");
|
|
57306
|
+
}
|
|
57307
|
+
function execute_delete_selection(gobj, nodes) {
|
|
57308
|
+
let priv = gobj.priv;
|
|
57309
|
+
hide_delete_confirm(gobj);
|
|
57310
|
+
for (let nd of nodes) gobj_publish_event(gobj, "EV_DELETE_NODE", {
|
|
57311
|
+
treedb_name: priv.treedb_name,
|
|
57312
|
+
topic_name: nd.data.desc.topic_name,
|
|
57313
|
+
record: nd.data.record
|
|
57314
|
+
});
|
|
57315
|
+
deselect_node(gobj);
|
|
57316
|
+
}
|
|
57317
|
+
/************************************************************
|
|
57147
57318
|
* Show a confirmation popover next to the delete icon.
|
|
57148
57319
|
************************************************************/
|
|
57149
57320
|
function show_delete_confirm(gobj, nodeData) {
|
|
57150
57321
|
let priv = gobj.priv;
|
|
57151
|
-
|
|
57152
|
-
let topic_name = nodeData.data.desc.topic_name;
|
|
57153
|
-
let impact = delete_impact(nodeData.data.desc, record);
|
|
57154
|
-
let question = t("delete") + " " + topic_name + ": " + record.id + "?";
|
|
57155
|
-
if (impact.children > 0) question += "\n" + impact.children + " " + t("children will be unlinked, not deleted", { count: impact.children });
|
|
57156
|
-
if (impact.parents > 0) question += "\n" + t("it will be detached from") + " " + impact.parents + " " + t("parents", { count: impact.parents });
|
|
57157
|
-
show_confirm_popover(gobj, priv._node_delete_el, question, "delete", "#ff4d4f", () => execute_delete_node(gobj, nodeData), "_delete_confirm_el");
|
|
57322
|
+
show_confirm_popover(gobj, priv._node_delete_el, delete_question([nodeData]), "delete", "#ff4d4f", () => execute_delete_node(gobj, nodeData), "_delete_confirm_el");
|
|
57158
57323
|
}
|
|
57159
57324
|
function hide_delete_confirm(gobj) {
|
|
57160
57325
|
hide_overlay(gobj, "_delete_confirm_el");
|
|
@@ -57963,6 +58128,18 @@ function ac_auto_fit(gobj, event, kw, src) {
|
|
|
57963
58128
|
graph_fitview(gobj);
|
|
57964
58129
|
return 0;
|
|
57965
58130
|
}
|
|
58131
|
+
/************************************************************
|
|
58132
|
+
* Zoom to what is selected.
|
|
58133
|
+
************************************************************/
|
|
58134
|
+
function ac_zoom_selection(gobj, event, kw, src) {
|
|
58135
|
+
let ids = gobj.priv._selected_paint_ids || [];
|
|
58136
|
+
if (!ids.length) {
|
|
58137
|
+
log_error(`${gobj_short_name(gobj)}: zoom to selection with nothing selected`);
|
|
58138
|
+
return -1;
|
|
58139
|
+
}
|
|
58140
|
+
graph_fit_selection(gobj, ids);
|
|
58141
|
+
return 0;
|
|
58142
|
+
}
|
|
57966
58143
|
function ac_focus_topic(gobj, event, kw, src) {
|
|
57967
58144
|
graph_focus_topic(gobj, kw && kw.topic);
|
|
57968
58145
|
return 0;
|
|
@@ -58021,6 +58198,38 @@ function ac_node_drag_end(gobj, event, kw, src) {
|
|
|
58021
58198
|
return 0;
|
|
58022
58199
|
}
|
|
58023
58200
|
/************************************************************
|
|
58201
|
+
* A key, while the graph has focus.
|
|
58202
|
+
*
|
|
58203
|
+
* Full screen keeps the two keys it always had. The rest belong to
|
|
58204
|
+
* the selection, so they only mean anything in edition -- outside
|
|
58205
|
+
* it there is nothing selected to clear, select or delete.
|
|
58206
|
+
************************************************************/
|
|
58207
|
+
function ac_key_down(gobj, event, kw, src) {
|
|
58208
|
+
let priv = gobj.priv;
|
|
58209
|
+
let key = kw && kw.key || "";
|
|
58210
|
+
if (gobj_read_bool_attr(gobj, "with_fullscreen")) {
|
|
58211
|
+
let fullscreen = graph_get_plugin(gobj, "fullscreen");
|
|
58212
|
+
if (fullscreen) {
|
|
58213
|
+
if (key === "F" || key === "f") fullscreen.request();
|
|
58214
|
+
else if (key === "Escape") fullscreen.exit();
|
|
58215
|
+
}
|
|
58216
|
+
}
|
|
58217
|
+
if (!priv.edit_mode) return 0;
|
|
58218
|
+
if (key === "Escape") {
|
|
58219
|
+
deselect_node(gobj);
|
|
58220
|
+
return 0;
|
|
58221
|
+
}
|
|
58222
|
+
if (kw.ctrl && (key === "a" || key === "A")) {
|
|
58223
|
+
select_all_nodes(gobj);
|
|
58224
|
+
return 0;
|
|
58225
|
+
}
|
|
58226
|
+
if (key === "Delete" || key === "Backspace") {
|
|
58227
|
+
request_delete_selection(gobj);
|
|
58228
|
+
return 0;
|
|
58229
|
+
}
|
|
58230
|
+
return 0;
|
|
58231
|
+
}
|
|
58232
|
+
/************************************************************
|
|
58024
58233
|
* The rubber band let go: these are the nodes it enclosed.
|
|
58025
58234
|
*
|
|
58026
58235
|
* They arrive in the kw because `onSelect` runs BEFORE G6 writes
|
|
@@ -58279,6 +58488,11 @@ function create_gclass$1(gclass_name) {
|
|
|
58279
58488
|
ac_brush_select,
|
|
58280
58489
|
null
|
|
58281
58490
|
],
|
|
58491
|
+
[
|
|
58492
|
+
"EV_KEY_DOWN",
|
|
58493
|
+
ac_key_down,
|
|
58494
|
+
null
|
|
58495
|
+
],
|
|
58282
58496
|
[
|
|
58283
58497
|
"EV_ZOOM_IN",
|
|
58284
58498
|
ac_zoom_in,
|
|
@@ -58299,6 +58513,11 @@ function create_gclass$1(gclass_name) {
|
|
|
58299
58513
|
ac_auto_fit,
|
|
58300
58514
|
null
|
|
58301
58515
|
],
|
|
58516
|
+
[
|
|
58517
|
+
"EV_ZOOM_SELECTION",
|
|
58518
|
+
ac_zoom_selection,
|
|
58519
|
+
null
|
|
58520
|
+
],
|
|
58302
58521
|
[
|
|
58303
58522
|
"EV_FOCUS_TOPIC",
|
|
58304
58523
|
ac_focus_topic,
|
|
@@ -58388,10 +58607,12 @@ function create_gclass$1(gclass_name) {
|
|
|
58388
58607
|
["EV_CANVAS_CLICK", 0],
|
|
58389
58608
|
["EV_NODE_DRAG_END", 0],
|
|
58390
58609
|
["EV_BRUSH_SELECT", 0],
|
|
58610
|
+
["EV_KEY_DOWN", 0],
|
|
58391
58611
|
["EV_ZOOM_IN", 0],
|
|
58392
58612
|
["EV_ZOOM_OUT", 0],
|
|
58393
58613
|
["EV_ZOOM_RESET", 0],
|
|
58394
58614
|
["EV_AUTO_FIT", 0],
|
|
58615
|
+
["EV_ZOOM_SELECTION", 0],
|
|
58395
58616
|
["EV_FOCUS_TOPIC", 0],
|
|
58396
58617
|
["EV_FIND_NODES", 0],
|
|
58397
58618
|
["EV_FIND_RESULT", event_flag_t.EVF_OUTPUT_EVENT],
|
package/package.json
CHANGED
package/src/c_g6_nodes_tree.js
CHANGED
|
@@ -787,20 +787,24 @@ function configure_events(gobj)
|
|
|
787
787
|
};
|
|
788
788
|
i18next.on('languageChanged', priv._on_language_changed);
|
|
789
789
|
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
790
|
+
/* The canvas carries a `tabIndex` of its own, so a keydown reaches
|
|
791
|
+
* us only while the GRAPH has focus. That is what keeps Ctrl+A in
|
|
792
|
+
* the find box a text selection and not a selection of every node:
|
|
793
|
+
* the focus is in the input, and the input is not inside the
|
|
794
|
+
* canvas. The callback only translates -- the work is in the
|
|
795
|
+
* action, like every other gesture here. */
|
|
796
|
+
graph.on("keydown", (evt) => {
|
|
797
|
+
let ctrl = !!(evt.ctrlKey || evt.metaKey);
|
|
798
|
+
|
|
799
|
+
if(ctrl && (evt.key === "a" || evt.key === "A")) {
|
|
800
|
+
/* Or the browser selects the page's text underneath. */
|
|
801
|
+
if(typeof evt.preventDefault === "function") {
|
|
802
|
+
evt.preventDefault();
|
|
801
803
|
}
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
gobj_send_event(gobj, "EV_KEY_DOWN", {key: evt.key, ctrl: ctrl}, gobj);
|
|
807
|
+
});
|
|
804
808
|
}
|
|
805
809
|
|
|
806
810
|
/************************************************************
|
|
@@ -908,6 +912,10 @@ function update_toolbar(gobj)
|
|
|
908
912
|
key: 'toolbar-edit',
|
|
909
913
|
});
|
|
910
914
|
}
|
|
915
|
+
|
|
916
|
+
/* A re-render rebuilds the innerHTML, so a button whose enabled
|
|
917
|
+
* state is not part of getItems() has to be told again. */
|
|
918
|
+
update_zoom_selection_button(gobj);
|
|
911
919
|
}
|
|
912
920
|
|
|
913
921
|
/************************************************************
|
|
@@ -1027,6 +1035,20 @@ function configure_toolbar(gobj)
|
|
|
1027
1035
|
{ text: '1:1', value: 'reset', className: 'EV_ZOOM_RESET', title: t('actual size') },
|
|
1028
1036
|
];
|
|
1029
1037
|
|
|
1038
|
+
/* Only in edition, because that is where a selection
|
|
1039
|
+
* can exist: a button that is permanently disabled is
|
|
1040
|
+
* not a control, it is furniture. Disabled while the
|
|
1041
|
+
* selection is empty -- `paint_selection` is the one
|
|
1042
|
+
* funnel every change goes through, so it is the one
|
|
1043
|
+
* place that has to say so. */
|
|
1044
|
+
if(priv.edit_mode) {
|
|
1045
|
+
items.push({
|
|
1046
|
+
id: 'g6-icon-fit-selection', value: 'fit-selection',
|
|
1047
|
+
className: 'EV_ZOOM_SELECTION',
|
|
1048
|
+
title: t('zoom to selection'), disabled: true
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1030
1052
|
if(gobj_read_bool_attr(gobj, "with_fullscreen")) {
|
|
1031
1053
|
/* Full screen is a WINDOW control, not a camera one:
|
|
1032
1054
|
* it goes in its own group. */
|
|
@@ -1058,6 +1080,9 @@ function configure_toolbar(gobj)
|
|
|
1058
1080
|
case 'auto-fit':
|
|
1059
1081
|
gobj_send_event(gobj, "EV_AUTO_FIT", {}, gobj);
|
|
1060
1082
|
break;
|
|
1083
|
+
case 'fit-selection':
|
|
1084
|
+
gobj_send_event(gobj, "EV_ZOOM_SELECTION", {}, gobj);
|
|
1085
|
+
break;
|
|
1061
1086
|
case 'center':
|
|
1062
1087
|
gobj_send_event(gobj, "EV_CENTER", {}, gobj);
|
|
1063
1088
|
break;
|
|
@@ -2286,6 +2311,65 @@ async function graph_fit_readable(gobj)
|
|
|
2286
2311
|
}
|
|
2287
2312
|
}
|
|
2288
2313
|
|
|
2314
|
+
/************************************************************
|
|
2315
|
+
* Put the viewport on the selection: what `fit` does, for a part.
|
|
2316
|
+
*
|
|
2317
|
+
* Computed rather than delegated because `fitView()` fits the whole
|
|
2318
|
+
* graph and there is no subset form of it. The bounds come from the
|
|
2319
|
+
* elements themselves, so a node's real card size is what is
|
|
2320
|
+
* measured, not an assumed one; the zoom is clamped to the graph's
|
|
2321
|
+
* own `zoomRange`, which is the only limit that is not invented
|
|
2322
|
+
* here. A selection of one card filling the view is not a bug --
|
|
2323
|
+
* that is what zooming to it means, and it is what every editor
|
|
2324
|
+
* that offers the action does.
|
|
2325
|
+
************************************************************/
|
|
2326
|
+
async function graph_fit_selection(gobj, ids)
|
|
2327
|
+
{
|
|
2328
|
+
let priv = gobj.priv;
|
|
2329
|
+
let graph = priv.graph;
|
|
2330
|
+
|
|
2331
|
+
if(!graph || !ids || !ids.length) {
|
|
2332
|
+
return;
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
let minx = Infinity, miny = Infinity, maxx = -Infinity, maxy = -Infinity;
|
|
2336
|
+
let found = 0;
|
|
2337
|
+
for(let id of ids) {
|
|
2338
|
+
let bounds;
|
|
2339
|
+
try {
|
|
2340
|
+
bounds = graph.getElementRenderBounds(id);
|
|
2341
|
+
} catch(e) {
|
|
2342
|
+
continue; /* gone since it was selected */
|
|
2343
|
+
}
|
|
2344
|
+
if(!bounds || !bounds.min || !bounds.max) {
|
|
2345
|
+
continue;
|
|
2346
|
+
}
|
|
2347
|
+
minx = Math.min(minx, bounds.min[0]);
|
|
2348
|
+
miny = Math.min(miny, bounds.min[1]);
|
|
2349
|
+
maxx = Math.max(maxx, bounds.max[0]);
|
|
2350
|
+
maxy = Math.max(maxy, bounds.max[1]);
|
|
2351
|
+
found++;
|
|
2352
|
+
}
|
|
2353
|
+
if(!found) {
|
|
2354
|
+
log_error(`${gobj_short_name(gobj)}: the selection has no bounds to fit`);
|
|
2355
|
+
return;
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
const MARGIN = 48;
|
|
2359
|
+
let size = graph.getSize();
|
|
2360
|
+
let vw = Math.max(1, size[0] - MARGIN * 2);
|
|
2361
|
+
let vh = Math.max(1, size[1] - MARGIN * 2);
|
|
2362
|
+
let bw = Math.max(1, maxx - minx);
|
|
2363
|
+
let bh = Math.max(1, maxy - miny);
|
|
2364
|
+
|
|
2365
|
+
let zoom = Math.min(vw / bw, vh / bh);
|
|
2366
|
+
let range = graph.getZoomRange() || [0.2, 4];
|
|
2367
|
+
zoom = Math.max(range[0], Math.min(range[1], zoom));
|
|
2368
|
+
|
|
2369
|
+
await graph.zoomTo(zoom);
|
|
2370
|
+
await graph.focusElement(ids);
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2289
2373
|
/************************************************************
|
|
2290
2374
|
* Focus the graph on one topic: highlight (amber 'active' state)
|
|
2291
2375
|
* every node of that topic and centre the viewport on them. An
|
|
@@ -2925,14 +3009,19 @@ function show_confirm_popover(gobj, target_el, message, confirm_text, confirm_co
|
|
|
2925
3009
|
hide_overlay(gobj, priv_key);
|
|
2926
3010
|
|
|
2927
3011
|
let priv = gobj.priv;
|
|
2928
|
-
if(!target_el) {
|
|
2929
|
-
return;
|
|
2930
|
-
}
|
|
2931
|
-
|
|
2932
|
-
let iconRect = target_el.getBoundingClientRect();
|
|
2933
3012
|
let containerRect = priv.$container.getBoundingClientRect();
|
|
2934
|
-
let left
|
|
2935
|
-
|
|
3013
|
+
let left, top;
|
|
3014
|
+
|
|
3015
|
+
if(target_el) {
|
|
3016
|
+
let iconRect = target_el.getBoundingClientRect();
|
|
3017
|
+
left = iconRect.right - containerRect.left + 6;
|
|
3018
|
+
top = iconRect.top - containerRect.top - 4;
|
|
3019
|
+
} else {
|
|
3020
|
+
/* A question about a SET has no icon to hang off: it is asked
|
|
3021
|
+
* in the middle of the graph it is about. */
|
|
3022
|
+
left = Math.round(containerRect.width / 2) - 110;
|
|
3023
|
+
top = Math.round(containerRect.height / 4);
|
|
3024
|
+
}
|
|
2936
3025
|
|
|
2937
3026
|
const popover = create_popover_base(left, top, 'g6-confirm-popover', '#ff4d4f', 160);
|
|
2938
3027
|
|
|
@@ -3040,6 +3129,36 @@ function paint_selection(gobj, ids)
|
|
|
3040
3129
|
priv._selected_paint_ids = next;
|
|
3041
3130
|
|
|
3042
3131
|
repaint_cards(gobj, new Set([...prev, ...next]));
|
|
3132
|
+
update_zoom_selection_button(gobj);
|
|
3133
|
+
}
|
|
3134
|
+
|
|
3135
|
+
/************************************************************
|
|
3136
|
+
* Zoom-to-selection can only act on a selection.
|
|
3137
|
+
*
|
|
3138
|
+
* The attribute is toggled on the element instead of re-rendering
|
|
3139
|
+
* the toolbar, which would rebuild its innerHTML -- and with it
|
|
3140
|
+
* drop the disabled state of every OTHER button -- each time a
|
|
3141
|
+
* card is ticked.
|
|
3142
|
+
************************************************************/
|
|
3143
|
+
function update_zoom_selection_button(gobj)
|
|
3144
|
+
{
|
|
3145
|
+
let priv = gobj.priv;
|
|
3146
|
+
let $container = gobj_read_attr(gobj, "$container");
|
|
3147
|
+
|
|
3148
|
+
if(!$container) {
|
|
3149
|
+
return;
|
|
3150
|
+
}
|
|
3151
|
+
|
|
3152
|
+
let $btn = $container.querySelector(".EV_ZOOM_SELECTION");
|
|
3153
|
+
if(!$btn) {
|
|
3154
|
+
return; /* no toolbar, or not in edition: nothing to enable */
|
|
3155
|
+
}
|
|
3156
|
+
|
|
3157
|
+
if((priv._selected_paint_ids || []).length > 0) {
|
|
3158
|
+
enableElements($container, ".EV_ZOOM_SELECTION");
|
|
3159
|
+
} else {
|
|
3160
|
+
disableElements($container, ".EV_ZOOM_SELECTION");
|
|
3161
|
+
}
|
|
3043
3162
|
}
|
|
3044
3163
|
|
|
3045
3164
|
/************************************************************
|
|
@@ -5347,31 +5466,151 @@ function execute_delete_node(gobj, nodeData)
|
|
|
5347
5466
|
}
|
|
5348
5467
|
|
|
5349
5468
|
/************************************************************
|
|
5350
|
-
*
|
|
5469
|
+
* Every node in the graph.
|
|
5351
5470
|
************************************************************/
|
|
5352
|
-
function
|
|
5471
|
+
function select_all_nodes(gobj)
|
|
5472
|
+
{
|
|
5473
|
+
let graph = gobj.priv.graph;
|
|
5474
|
+
|
|
5475
|
+
if(!graph) {
|
|
5476
|
+
return;
|
|
5477
|
+
}
|
|
5478
|
+
|
|
5479
|
+
let nodes = (graph.getData() || {}).nodes || [];
|
|
5480
|
+
set_selection(gobj, nodes.map((nd) => nd.id));
|
|
5481
|
+
}
|
|
5482
|
+
|
|
5483
|
+
/************************************************************
|
|
5484
|
+
* The nodes currently selected, as their node data.
|
|
5485
|
+
************************************************************/
|
|
5486
|
+
function selected_nodes_data(gobj)
|
|
5353
5487
|
{
|
|
5354
5488
|
let priv = gobj.priv;
|
|
5355
|
-
let
|
|
5356
|
-
|
|
5489
|
+
let graph = priv.graph;
|
|
5490
|
+
|
|
5491
|
+
let ids = new Set(selected_node_ids(gobj));
|
|
5492
|
+
for(let id of (priv._selected_paint_ids || [])) {
|
|
5493
|
+
ids.add(id);
|
|
5494
|
+
}
|
|
5495
|
+
if(priv._selected_node_id) {
|
|
5496
|
+
ids.add(priv._selected_node_id);
|
|
5497
|
+
}
|
|
5498
|
+
|
|
5499
|
+
let out = [];
|
|
5500
|
+
for(let id of ids) {
|
|
5501
|
+
let nd;
|
|
5502
|
+
try {
|
|
5503
|
+
nd = graph.getNodeData(id);
|
|
5504
|
+
} catch(e) {
|
|
5505
|
+
continue; /* gone since it was selected */
|
|
5506
|
+
}
|
|
5507
|
+
if(nd && nd.data && nd.data.desc) {
|
|
5508
|
+
out.push(nd);
|
|
5509
|
+
}
|
|
5510
|
+
}
|
|
5357
5511
|
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
|
|
5512
|
+
return out;
|
|
5513
|
+
}
|
|
5514
|
+
|
|
5515
|
+
/************************************************************
|
|
5516
|
+
* What a delete takes with it, for ONE node or for a set.
|
|
5517
|
+
*
|
|
5518
|
+
* Same sentence in both cases and the same keys, because it is the
|
|
5519
|
+
* same warning: these views delete with `force`, which UNLINKS the
|
|
5520
|
+
* children (they survive, loose) and cleans the node off its
|
|
5521
|
+
* parents. Over a set the two numbers are the sums -- an operator
|
|
5522
|
+
* about to detach eleven records has to read eleven, not "are you
|
|
5523
|
+
* sure".
|
|
5524
|
+
************************************************************/
|
|
5525
|
+
function delete_question(nodes)
|
|
5526
|
+
{
|
|
5527
|
+
let children = 0;
|
|
5528
|
+
let parents = 0;
|
|
5529
|
+
|
|
5530
|
+
for(let nd of nodes) {
|
|
5531
|
+
let impact = delete_impact(nd.data.desc, nd.data.record || {});
|
|
5532
|
+
children += impact.children;
|
|
5533
|
+
parents += impact.parents;
|
|
5534
|
+
}
|
|
5535
|
+
|
|
5536
|
+
let question;
|
|
5537
|
+
if(nodes.length === 1) {
|
|
5538
|
+
let nd = nodes[0];
|
|
5539
|
+
question = t('delete') + ' ' + nd.data.desc.topic_name + ': ' +
|
|
5540
|
+
(nd.data.record || {}).id + '?';
|
|
5541
|
+
} else {
|
|
5542
|
+
question = t('delete') + ' ' + nodes.length + ' ' + t('records') + '?';
|
|
5543
|
+
}
|
|
5544
|
+
|
|
5545
|
+
if(children > 0) {
|
|
5546
|
+
question += '\n' + children + ' ' +
|
|
5547
|
+
t('children will be unlinked, not deleted', {count: children});
|
|
5367
5548
|
}
|
|
5368
|
-
if(
|
|
5549
|
+
if(parents > 0) {
|
|
5369
5550
|
question += '\n' + t('it will be detached from') + ' ' +
|
|
5370
|
-
|
|
5551
|
+
parents + ' ' + t('parents', {count: parents});
|
|
5552
|
+
}
|
|
5553
|
+
|
|
5554
|
+
return question;
|
|
5555
|
+
}
|
|
5556
|
+
|
|
5557
|
+
/************************************************************
|
|
5558
|
+
* Delete every selected node (the Delete key).
|
|
5559
|
+
************************************************************/
|
|
5560
|
+
function request_delete_selection(gobj)
|
|
5561
|
+
{
|
|
5562
|
+
let nodes = selected_nodes_data(gobj);
|
|
5563
|
+
|
|
5564
|
+
if(!nodes.length) {
|
|
5565
|
+
return; /* nothing selected: the key means nothing here */
|
|
5566
|
+
}
|
|
5567
|
+
|
|
5568
|
+
if(!gobj_read_bool_attr(gobj, "confirm_delete_node")) {
|
|
5569
|
+
execute_delete_selection(gobj, nodes);
|
|
5570
|
+
return;
|
|
5371
5571
|
}
|
|
5372
5572
|
|
|
5573
|
+
/* No anchor: the set has no icon of its own, so the question is
|
|
5574
|
+
* asked in the middle of the graph. */
|
|
5575
|
+
show_confirm_popover(gobj, null,
|
|
5576
|
+
delete_question(nodes),
|
|
5577
|
+
'delete', '#ff4d4f',
|
|
5578
|
+
() => execute_delete_selection(gobj, nodes),
|
|
5579
|
+
'_delete_confirm_el'
|
|
5580
|
+
);
|
|
5581
|
+
}
|
|
5582
|
+
|
|
5583
|
+
function execute_delete_selection(gobj, nodes)
|
|
5584
|
+
{
|
|
5585
|
+
let priv = gobj.priv;
|
|
5586
|
+
|
|
5587
|
+
hide_delete_confirm(gobj);
|
|
5588
|
+
|
|
5589
|
+
/* One event per node: a treedb deletes records, it has no bulk
|
|
5590
|
+
* delete, and the host turns each of these into its own command
|
|
5591
|
+
* exactly as the topic table's bulk delete does. */
|
|
5592
|
+
for(let nd of nodes) {
|
|
5593
|
+
gobj_publish_event(gobj, "EV_DELETE_NODE", {
|
|
5594
|
+
treedb_name: priv.treedb_name,
|
|
5595
|
+
topic_name: nd.data.desc.topic_name,
|
|
5596
|
+
record: nd.data.record,
|
|
5597
|
+
});
|
|
5598
|
+
}
|
|
5599
|
+
|
|
5600
|
+
deselect_node(gobj);
|
|
5601
|
+
}
|
|
5602
|
+
|
|
5603
|
+
/************************************************************
|
|
5604
|
+
* Show a confirmation popover next to the delete icon.
|
|
5605
|
+
************************************************************/
|
|
5606
|
+
function show_delete_confirm(gobj, nodeData)
|
|
5607
|
+
{
|
|
5608
|
+
let priv = gobj.priv;
|
|
5609
|
+
|
|
5610
|
+
/* Same sentence as the Delete key's, from the same place: the
|
|
5611
|
+
* warning does not depend on how the delete was asked for. */
|
|
5373
5612
|
show_confirm_popover(gobj, priv._node_delete_el,
|
|
5374
|
-
|
|
5613
|
+
delete_question([nodeData]),
|
|
5375
5614
|
'delete', '#ff4d4f',
|
|
5376
5615
|
() => execute_delete_node(gobj, nodeData),
|
|
5377
5616
|
'_delete_confirm_el'
|
|
@@ -6645,6 +6884,25 @@ function ac_auto_fit(gobj, event, kw, src)
|
|
|
6645
6884
|
return 0;
|
|
6646
6885
|
}
|
|
6647
6886
|
|
|
6887
|
+
/************************************************************
|
|
6888
|
+
* Zoom to what is selected.
|
|
6889
|
+
************************************************************/
|
|
6890
|
+
function ac_zoom_selection(gobj, event, kw, src)
|
|
6891
|
+
{
|
|
6892
|
+
let ids = gobj.priv._selected_paint_ids || [];
|
|
6893
|
+
|
|
6894
|
+
if(!ids.length) {
|
|
6895
|
+
/* The button is disabled without a selection, so getting here
|
|
6896
|
+
* means the event came from somewhere else. */
|
|
6897
|
+
log_error(`${gobj_short_name(gobj)}: zoom to selection with nothing selected`);
|
|
6898
|
+
return -1;
|
|
6899
|
+
}
|
|
6900
|
+
|
|
6901
|
+
graph_fit_selection(gobj, ids);
|
|
6902
|
+
|
|
6903
|
+
return 0;
|
|
6904
|
+
}
|
|
6905
|
+
|
|
6648
6906
|
function ac_focus_topic(gobj, event, kw, src)
|
|
6649
6907
|
{
|
|
6650
6908
|
graph_focus_topic(gobj, kw && kw.topic);
|
|
@@ -6723,6 +6981,53 @@ function ac_node_drag_end(gobj, event, kw, src)
|
|
|
6723
6981
|
return 0;
|
|
6724
6982
|
}
|
|
6725
6983
|
|
|
6984
|
+
/************************************************************
|
|
6985
|
+
* A key, while the graph has focus.
|
|
6986
|
+
*
|
|
6987
|
+
* Full screen keeps the two keys it always had. The rest belong to
|
|
6988
|
+
* the selection, so they only mean anything in edition -- outside
|
|
6989
|
+
* it there is nothing selected to clear, select or delete.
|
|
6990
|
+
************************************************************/
|
|
6991
|
+
function ac_key_down(gobj, event, kw, src)
|
|
6992
|
+
{
|
|
6993
|
+
let priv = gobj.priv;
|
|
6994
|
+
let key = (kw && kw.key) || "";
|
|
6995
|
+
|
|
6996
|
+
if(gobj_read_bool_attr(gobj, "with_fullscreen")) {
|
|
6997
|
+
let fullscreen = graph_get_plugin(gobj, "fullscreen");
|
|
6998
|
+
if(fullscreen) {
|
|
6999
|
+
if(key === "F" || key === "f") {
|
|
7000
|
+
fullscreen.request();
|
|
7001
|
+
} else if(key === "Escape") {
|
|
7002
|
+
/* Escape also clears the selection below, and both are
|
|
7003
|
+
* right: the browser exits full screen on Escape
|
|
7004
|
+
* whatever we do, so refusing to clear as well would
|
|
7005
|
+
* only make the key do less than it appears to. */
|
|
7006
|
+
fullscreen.exit();
|
|
7007
|
+
}
|
|
7008
|
+
}
|
|
7009
|
+
}
|
|
7010
|
+
|
|
7011
|
+
if(!priv.edit_mode) {
|
|
7012
|
+
return 0;
|
|
7013
|
+
}
|
|
7014
|
+
|
|
7015
|
+
if(key === "Escape") {
|
|
7016
|
+
deselect_node(gobj);
|
|
7017
|
+
return 0;
|
|
7018
|
+
}
|
|
7019
|
+
if(kw.ctrl && (key === "a" || key === "A")) {
|
|
7020
|
+
select_all_nodes(gobj);
|
|
7021
|
+
return 0;
|
|
7022
|
+
}
|
|
7023
|
+
if(key === "Delete" || key === "Backspace") {
|
|
7024
|
+
request_delete_selection(gobj);
|
|
7025
|
+
return 0;
|
|
7026
|
+
}
|
|
7027
|
+
|
|
7028
|
+
return 0;
|
|
7029
|
+
}
|
|
7030
|
+
|
|
6726
7031
|
/************************************************************
|
|
6727
7032
|
* The rubber band let go: these are the nodes it enclosed.
|
|
6728
7033
|
*
|
|
@@ -7049,12 +7354,14 @@ function create_gclass(gclass_name)
|
|
|
7049
7354
|
["EV_CANVAS_CLICK", ac_canvas_click, null],
|
|
7050
7355
|
["EV_NODE_DRAG_END", ac_node_drag_end, null],
|
|
7051
7356
|
["EV_BRUSH_SELECT", ac_brush_select, null],
|
|
7357
|
+
["EV_KEY_DOWN", ac_key_down, null],
|
|
7052
7358
|
|
|
7053
7359
|
/*--- Toolbar events ---*/
|
|
7054
7360
|
["EV_ZOOM_IN", ac_zoom_in, null],
|
|
7055
7361
|
["EV_ZOOM_OUT", ac_zoom_out, null],
|
|
7056
7362
|
["EV_ZOOM_RESET", ac_zoom_reset, null],
|
|
7057
7363
|
["EV_AUTO_FIT", ac_auto_fit, null],
|
|
7364
|
+
["EV_ZOOM_SELECTION", ac_zoom_selection, null],
|
|
7058
7365
|
["EV_FOCUS_TOPIC", ac_focus_topic, null],
|
|
7059
7366
|
["EV_FIND_NODES", ac_find_nodes, null],
|
|
7060
7367
|
["EV_CENTER", ac_center, null],
|
|
@@ -7094,12 +7401,14 @@ function create_gclass(gclass_name)
|
|
|
7094
7401
|
["EV_CANVAS_CLICK", 0],
|
|
7095
7402
|
["EV_NODE_DRAG_END", 0],
|
|
7096
7403
|
["EV_BRUSH_SELECT", 0],
|
|
7404
|
+
["EV_KEY_DOWN", 0],
|
|
7097
7405
|
|
|
7098
7406
|
/*--- Toolbar (internal) ---*/
|
|
7099
7407
|
["EV_ZOOM_IN", 0],
|
|
7100
7408
|
["EV_ZOOM_OUT", 0],
|
|
7101
7409
|
["EV_ZOOM_RESET", 0],
|
|
7102
7410
|
["EV_AUTO_FIT", 0],
|
|
7411
|
+
["EV_ZOOM_SELECTION", 0],
|
|
7103
7412
|
["EV_FOCUS_TOPIC", 0],
|
|
7104
7413
|
["EV_FIND_NODES", 0],
|
|
7105
7414
|
["EV_FIND_RESULT", event_flag_t.EVF_OUTPUT_EVENT],
|
package/src/lib_icons.js
CHANGED
|
@@ -52,6 +52,15 @@ const ICONS = `<svg xmlns="http://www.w3.org/2000/svg">
|
|
|
52
52
|
<symbol id="g6-icon-fit" viewBox="0 0 448 512">
|
|
53
53
|
<path fill="currentColor" d="M32 32C14.3 32 0 46.3 0 64l0 96c0 17.7 14.3 32 32 32s32-14.3 32-32l0-64 64 0c17.7 0 32-14.3 32-32s-14.3-32-32-32L32 32zM64 352c0-17.7-14.3-32-32-32S0 334.3 0 352l0 96c0 17.7 14.3 32 32 32l96 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-64 0 0-64zM320 32c-17.7 0-32 14.3-32 32s14.3 32 32 32l64 0 0 64c0 17.7 14.3 32 32 32s32-14.3 32-32l0-96c0-17.7-14.3-32-32-32l-96 0zM448 352c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 64-64 0c-17.7 0-32 14.3-32 32s14.3 32 32 32l96 0c17.7 0 32-14.3 32-32l0-96z"/>
|
|
54
54
|
</symbol>
|
|
55
|
+
<!-- fit to selection: the same corner brackets as the fit icon,
|
|
56
|
+
around a marked object. The two sit side by side in the
|
|
57
|
+
toolbar and have to read as a family, which a borrowed glyph
|
|
58
|
+
would not. (No backticks in here: the sprite lives inside a
|
|
59
|
+
template literal.) -->
|
|
60
|
+
<symbol id="g6-icon-fit-selection" viewBox="0 0 448 512">
|
|
61
|
+
<path fill="currentColor" d="M32 32C14.3 32 0 46.3 0 64l0 96c0 17.7 14.3 32 32 32s32-14.3 32-32l0-64 64 0c17.7 0 32-14.3 32-32s-14.3-32-32-32L32 32zM64 352c0-17.7-14.3-32-32-32S0 334.3 0 352l0 96c0 17.7 14.3 32 32 32l96 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-64 0 0-64zM320 32c-17.7 0-32 14.3-32 32s14.3 32 32 32l64 0 0 64c0 17.7 14.3 32 32 32s32-14.3 32-32l0-96c0-17.7-14.3-32-32-32l-96 0zM448 352c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 64-64 0c-17.7 0-32 14.3-32 32s14.3 32 32 32l96 0c17.7 0 32-14.3 32-32l0-96z"/>
|
|
62
|
+
<rect fill="currentColor" x="144" y="176" width="160" height="160" rx="24"/>
|
|
63
|
+
</symbol>
|
|
55
64
|
<!-- crosshairs (center) -->
|
|
56
65
|
<symbol id="g6-icon-center" viewBox="0 0 576 512">
|
|
57
66
|
<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-32zM131.2 288c12.7 62.7 62.1 112.1 124.8 124.8l0-12.8c0-17.7 14.3-32 32-32s32 14.3 32 32l0 12.8c62.7-12.7 112.1-62.1 124.8-124.8L432 288c-17.7 0-32-14.3-32-32s14.3-32 32-32l12.8 0C432.1 161.3 382.7 111.9 320 99.2l0 12.8c0 17.7-14.3 32-32 32s-32-14.3-32-32l0-12.8C193.3 111.9 143.9 161.3 131.2 224l12.8 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l-12.8 0zM288 208a48 48 0 1 1 0 96 48 48 0 1 1 0-96z"/>
|