@yuneta/gobj-ui 7.14.3 → 7.16.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 +90 -0
- package/dist/gobj-ui.cjs.js +260 -47
- package/dist/gobj-ui.es.js +260 -47
- package/package.json +1 -1
- package/src/c_g6_nodes_tree.js +431 -58
- package/src/lib_graph.css +70 -4
package/src/c_g6_nodes_tree.js
CHANGED
|
@@ -105,11 +105,22 @@ import {yui_theme_now, yui_watch_theme} from "./yui_theme.js";
|
|
|
105
105
|
|
|
106
106
|
/***************************************************************
|
|
107
107
|
* YuiToolbar — G6 Toolbar subclass that adds per-item className
|
|
108
|
-
* and disabled support.
|
|
108
|
+
* and disabled support, plus three item kinds G6 does not have.
|
|
109
109
|
*
|
|
110
110
|
* Each item in getItems() may carry:
|
|
111
111
|
* className — CSS class added to the div (e.g. 'EV_SAVE_GRAPH')
|
|
112
112
|
* disabled — boolean; sets the 'disabled' attribute initially
|
|
113
|
+
* text — render this text instead of a sprite symbol; a
|
|
114
|
+
* label is the only readable way to say `1:1`, and
|
|
115
|
+
* every editor that offers actual-size writes it
|
|
116
|
+
* readout — a text item that REPORTS instead of acting (the
|
|
117
|
+
* zoom level); it is not a button
|
|
118
|
+
* separator — a group divider, not an item
|
|
119
|
+
*
|
|
120
|
+
* The base class fires onClick only for elements whose class list
|
|
121
|
+
* contains `g6-toolbar-item`, so a separator and a readout carry
|
|
122
|
+
* their own class and are inert by construction, with no guard in
|
|
123
|
+
* the click handler.
|
|
113
124
|
*
|
|
114
125
|
* With className set to the event name, the lib_graph.js state
|
|
115
126
|
* functions (set_submit_state, disableElements, …) work on toolbar
|
|
@@ -122,14 +133,36 @@ class YuiToolbar extends Toolbar
|
|
|
122
133
|
{
|
|
123
134
|
const items = await this.options.getItems();
|
|
124
135
|
return items.map((item) => {
|
|
136
|
+
if(item.separator) {
|
|
137
|
+
return `<div class="g6-toolbar-sep" aria-hidden="true"></div>`;
|
|
138
|
+
}
|
|
139
|
+
|
|
125
140
|
const extra_class = item.className ? ` ${item.className}` : '';
|
|
126
|
-
const
|
|
141
|
+
const title = item.title ?? '';
|
|
142
|
+
|
|
143
|
+
if(item.readout) {
|
|
144
|
+
return (
|
|
145
|
+
`<div class="g6-toolbar-readout${extra_class}"` +
|
|
146
|
+
` title="${title}" aria-label="${title}">` +
|
|
147
|
+
`${item.text ?? ''}` +
|
|
148
|
+
`</div>`
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const disabled = item.disabled ? ' disabled' : '';
|
|
153
|
+
const is_text = (item.text !== undefined);
|
|
154
|
+
const kind_class = is_text ? ' g6-toolbar-item-text' : '';
|
|
155
|
+
const body = is_text
|
|
156
|
+
? `<span class="g6-toolbar-text">${item.text}</span>`
|
|
157
|
+
: `<svg aria-hidden="true" focusable="false">` +
|
|
158
|
+
`<use xlink:href="#${item.id}"></use>` +
|
|
159
|
+
`</svg>`;
|
|
160
|
+
|
|
127
161
|
return (
|
|
128
|
-
`<div class="g6-toolbar-item${extra_class}"` +
|
|
129
|
-
` value="${item.value}" title="${
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
`</svg>` +
|
|
162
|
+
`<div class="g6-toolbar-item${kind_class}${extra_class}"` +
|
|
163
|
+
` value="${item.value}" title="${title}"` +
|
|
164
|
+
` aria-label="${title}"${disabled}>` +
|
|
165
|
+
`${body}` +
|
|
133
166
|
`</div>`
|
|
134
167
|
);
|
|
135
168
|
}).join('');
|
|
@@ -151,6 +184,15 @@ const GCLASS_NAME = "C_G6_NODES_TREE";
|
|
|
151
184
|
const HIGHLIGHT_COLOR = "#f0a020";
|
|
152
185
|
const HIGHLIGHT_HALO = "rgba(240,160,32,0.35)";
|
|
153
186
|
|
|
187
|
+
/*
|
|
188
|
+
* The selection ring. Deliberately NOT the amber of the highlight:
|
|
189
|
+
* a node can be a find match AND be selected, and two amber rings
|
|
190
|
+
* would say nothing about either. Blue is what a selection is in
|
|
191
|
+
* every editor, and the ring is drawn OUTSIDE the highlight's halo
|
|
192
|
+
* so the two compose instead of overwriting one another.
|
|
193
|
+
*/
|
|
194
|
+
const SELECT_RING = "rgba(59,130,246,0.95)";
|
|
195
|
+
|
|
154
196
|
/***************************************************************
|
|
155
197
|
* Internal layout and operation mode definitions
|
|
156
198
|
***************************************************************/
|
|
@@ -291,6 +333,10 @@ let PRIVATE_DATA = {
|
|
|
291
333
|
_link_saved_styles: [], // saved port styles to restore on cancel
|
|
292
334
|
_focus_topic: null, // topic currently focused (EV_FOCUS_TOPIC)
|
|
293
335
|
_focus_ids: [], // node ids carrying the focus 'active' state
|
|
336
|
+
_selected_paint_ids: [], // node ids whose card is PAINTED selected
|
|
337
|
+
// (G6's 'selected' state is the selection
|
|
338
|
+
// itself; this is what is on screen, and
|
|
339
|
+
// it exists to diff the repaint)
|
|
294
340
|
_pending_focus_topic: null, // focus requested before data was loaded
|
|
295
341
|
_pending_find: null, // find requested before data was loaded
|
|
296
342
|
_layout_asked: "", // layout the host asked for at create (see mt_create)
|
|
@@ -732,6 +778,7 @@ function configure_events(gobj)
|
|
|
732
778
|
update_edge_icon_position(gobj);
|
|
733
779
|
update_node_icon_position(gobj);
|
|
734
780
|
update_link_icon_position(gobj);
|
|
781
|
+
update_zoom_readout(gobj);
|
|
735
782
|
});
|
|
736
783
|
|
|
737
784
|
// Re-render G6 toolbars when language changes
|
|
@@ -863,6 +910,79 @@ function update_toolbar(gobj)
|
|
|
863
910
|
}
|
|
864
911
|
}
|
|
865
912
|
|
|
913
|
+
/************************************************************
|
|
914
|
+
* The floating toolbars follow the theme.
|
|
915
|
+
*
|
|
916
|
+
* They used to be pinned to a light background in BOTH themes,
|
|
917
|
+
* with the icon colour pinned dark so it survived that -- two
|
|
918
|
+
* light islands sitting over a dark canvas. The tokens do the
|
|
919
|
+
* flipping now; the fallbacks are the old forced-light values, so
|
|
920
|
+
* a host without Bulma gets exactly what it had.
|
|
921
|
+
************************************************************/
|
|
922
|
+
function toolbar_style(extra)
|
|
923
|
+
{
|
|
924
|
+
return Object.assign({
|
|
925
|
+
backgroundColor: 'var(--bulma-scheme-main, #f5f5f5)',
|
|
926
|
+
color: 'var(--bulma-text-strong, #333)',
|
|
927
|
+
padding: '8px',
|
|
928
|
+
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
|
|
929
|
+
borderRadius: '8px',
|
|
930
|
+
border: '1px solid var(--bulma-border-weak, #e8e8e8)',
|
|
931
|
+
opacity: '0.85',
|
|
932
|
+
}, extra || {});
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
/************************************************************
|
|
936
|
+
* The zoom level as a percentage, the way every editor that
|
|
937
|
+
* shows one writes it. Empty while the graph cannot be asked --
|
|
938
|
+
* the toolbar is built before the first draw, and a viewport
|
|
939
|
+
* that does not exist yet has no zoom to report.
|
|
940
|
+
************************************************************/
|
|
941
|
+
function zoom_percent_text(gobj)
|
|
942
|
+
{
|
|
943
|
+
let graph = gobj.priv.graph;
|
|
944
|
+
|
|
945
|
+
if(!graph || typeof graph.getZoom !== "function") {
|
|
946
|
+
return "";
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
let zoom;
|
|
950
|
+
try {
|
|
951
|
+
zoom = graph.getZoom();
|
|
952
|
+
} catch(e) {
|
|
953
|
+
return "";
|
|
954
|
+
}
|
|
955
|
+
if(!zoom) {
|
|
956
|
+
return "";
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
return `${Math.round(zoom * 100)}%`;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
/************************************************************
|
|
963
|
+
* Repaint the zoom readout in place.
|
|
964
|
+
*
|
|
965
|
+
* The text node is patched instead of re-rendering the plugin:
|
|
966
|
+
* the readout follows the wheel, and updatePlugin() rebuilds the
|
|
967
|
+
* whole toolbar's innerHTML -- which would also drop the disabled
|
|
968
|
+
* state of the edit buttons on every notch of the wheel.
|
|
969
|
+
************************************************************/
|
|
970
|
+
function update_zoom_readout(gobj)
|
|
971
|
+
{
|
|
972
|
+
let $container = gobj_read_attr(gobj, "$container");
|
|
973
|
+
|
|
974
|
+
if(!$container) {
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
let $readout = $container.querySelector(".G6_ZOOM_LEVEL");
|
|
979
|
+
if(!$readout) {
|
|
980
|
+
return; /* no toolbar (with_toolbar false): nobody to report to */
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
$readout.textContent = zoom_percent_text(gobj);
|
|
984
|
+
}
|
|
985
|
+
|
|
866
986
|
function configure_toolbar(gobj)
|
|
867
987
|
{
|
|
868
988
|
let priv = gobj.priv;
|
|
@@ -880,28 +1000,37 @@ function configure_toolbar(gobj)
|
|
|
880
1000
|
type: 'yui-toolbar',
|
|
881
1001
|
className: 'g6-toolbar-large',
|
|
882
1002
|
position: toolbar_position,
|
|
883
|
-
style: {
|
|
884
|
-
|
|
885
|
-
/* Forced-light bg in both themes → pin dark icon
|
|
886
|
-
* color (currentColor) so it stays visible in dark. */
|
|
887
|
-
color: '#333',
|
|
888
|
-
padding: '8px',
|
|
889
|
-
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
|
|
890
|
-
borderRadius: '8px',
|
|
891
|
-
border: '1px solid #e8e8e8',
|
|
892
|
-
opacity: '0.85',
|
|
893
|
-
marginTop: '12px',
|
|
1003
|
+
style: toolbar_style({
|
|
1004
|
+
marginTop: '12px',
|
|
894
1005
|
marginLeft: '12px',
|
|
895
|
-
},
|
|
1006
|
+
}),
|
|
896
1007
|
getItems: () => {
|
|
897
1008
|
let items = [
|
|
898
1009
|
{ id: 'g6-icon-zoom-in', value: 'zoom-in', className: 'EV_ZOOM_IN', title: t('zoom in') },
|
|
899
1010
|
{ id: 'g6-icon-zoom-out', value: 'zoom-out', className: 'EV_ZOOM_OUT', title: t('zoom out') },
|
|
1011
|
+
/* What the two buttons above change. Every editor
|
|
1012
|
+
* that offers a zoom shows the number; without it
|
|
1013
|
+
* `1:1` is a jump to a value nobody was told. */
|
|
1014
|
+
{ readout: true, className: 'G6_ZOOM_LEVEL',
|
|
1015
|
+
text: zoom_percent_text(gobj), title: t('zoom level') },
|
|
1016
|
+
{ separator: true },
|
|
900
1017
|
{ id: 'g6-icon-fit', value: 'auto-fit', className: 'EV_AUTO_FIT', title: t('auto fit') },
|
|
901
|
-
|
|
1018
|
+
/* `1:1`, not a house. The action is `zoomTo(1)`: it
|
|
1019
|
+
* sets the SCALE and leaves the camera where it was,
|
|
1020
|
+
* which is neither of the two things a house means
|
|
1021
|
+
* anywhere -- a map's initial extent, an editor's
|
|
1022
|
+
* starting view. Sitting under `fit`, the house read
|
|
1023
|
+
* as a second way to get the whole graph back, and
|
|
1024
|
+
* answered with the same corner of it at 100%.
|
|
1025
|
+
* Actual size is WRITTEN in every editor that offers
|
|
1026
|
+
* it, never drawn: there is no glyph for it. */
|
|
1027
|
+
{ text: '1:1', value: 'reset', className: 'EV_ZOOM_RESET', title: t('actual size') },
|
|
902
1028
|
];
|
|
903
1029
|
|
|
904
1030
|
if(gobj_read_bool_attr(gobj, "with_fullscreen")) {
|
|
1031
|
+
/* Full screen is a WINDOW control, not a camera one:
|
|
1032
|
+
* it goes in its own group. */
|
|
1033
|
+
items.push({ separator: true });
|
|
905
1034
|
if(priv.is_fullscreen) {
|
|
906
1035
|
items.push(
|
|
907
1036
|
{ id: 'g6-icon-fullscreen-exit', value: 'exit-fullscreen', className: 'EV_EXIT_FULLSCREEN', title: t('exit full screen') }
|
|
@@ -976,18 +1105,7 @@ function configure_toolbar_edit(gobj)
|
|
|
976
1105
|
type: 'yui-toolbar',
|
|
977
1106
|
className: 'g6-toolbar-large',
|
|
978
1107
|
position: 'left-top',
|
|
979
|
-
style:
|
|
980
|
-
backgroundColor: '#f5f5f5',
|
|
981
|
-
/* Toolbar bg is forced light in BOTH themes; icons
|
|
982
|
-
* use currentColor, so pin a dark color or in dark
|
|
983
|
-
* theme they'd be light-on-light (invisible). */
|
|
984
|
-
color: '#333',
|
|
985
|
-
padding: '8px',
|
|
986
|
-
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
|
|
987
|
-
borderRadius: '8px',
|
|
988
|
-
border: '1px solid #e8e8e8',
|
|
989
|
-
opacity: '0.85',
|
|
990
|
-
},
|
|
1108
|
+
style: toolbar_style(),
|
|
991
1109
|
getItems: () => {
|
|
992
1110
|
return [
|
|
993
1111
|
{ id: 'g6-icon-plus', value: 'create-node', className: 'EV_CREATE_NODE_BTN color_create_state', title: t('create node') },
|
|
@@ -1077,9 +1195,46 @@ function configure_behaviour(gobj)
|
|
|
1077
1195
|
case "edition":
|
|
1078
1196
|
priv.edit_mode = true;
|
|
1079
1197
|
behaviors = [
|
|
1080
|
-
|
|
1198
|
+
/* Panning gives way while Shift is held: that is the
|
|
1199
|
+
* marquee's gesture, and G6 binds drag-canvas straight
|
|
1200
|
+
* to the drag events, so without this the canvas pans
|
|
1201
|
+
* under the rubber band. */
|
|
1202
|
+
{
|
|
1203
|
+
type: "drag-canvas",
|
|
1204
|
+
key: "drag-canvas",
|
|
1205
|
+
enable: (event) => !event.shiftKey,
|
|
1206
|
+
},
|
|
1081
1207
|
"zoom-canvas",
|
|
1208
|
+
/* Moves EVERY node in the `selected` state, not just
|
|
1209
|
+
* the one under the pointer, and wraps the whole move
|
|
1210
|
+
* in one history batch -- so a group move is one drag
|
|
1211
|
+
* and one undo. Nothing to configure: `selected` is
|
|
1212
|
+
* already its default `state`. */
|
|
1082
1213
|
"drag-element",
|
|
1214
|
+
/* Shift+drag on the canvas: the rubber band. The
|
|
1215
|
+
* GESTURE is G6's, its RESULT enters the machine --
|
|
1216
|
+
* `onSelect` fires before G6 writes the state, so the
|
|
1217
|
+
* ids travel with the event and the action does not
|
|
1218
|
+
* have to race it. */
|
|
1219
|
+
{
|
|
1220
|
+
type: "brush-select",
|
|
1221
|
+
key: "brush-select",
|
|
1222
|
+
trigger: ["shift"],
|
|
1223
|
+
enableElements: ["node"],
|
|
1224
|
+
/* Left at G6's default (false): the set is read at
|
|
1225
|
+
* pointerup, not on every pointermove. Each answer
|
|
1226
|
+
* repaints the cards it touches, and doing that per
|
|
1227
|
+
* frame of a rubber band over a hundred nodes is
|
|
1228
|
+
* paid for nothing -- the band already shows what
|
|
1229
|
+
* it covers. */
|
|
1230
|
+
immediately: false,
|
|
1231
|
+
onSelect: (states) => {
|
|
1232
|
+
let ids = Object.keys(states || {}).filter(
|
|
1233
|
+
(id) => (states[id] || []).includes("selected")
|
|
1234
|
+
);
|
|
1235
|
+
gobj_send_event(gobj, "EV_BRUSH_SELECT", {ids: ids}, gobj);
|
|
1236
|
+
},
|
|
1237
|
+
},
|
|
1083
1238
|
];
|
|
1084
1239
|
break;
|
|
1085
1240
|
case "operation":
|
|
@@ -2843,6 +2998,126 @@ function perform_history_op(gobj, is_redo)
|
|
|
2843
2998
|
}
|
|
2844
2999
|
}
|
|
2845
3000
|
|
|
3001
|
+
/************************************************************
|
|
3002
|
+
* The selection.
|
|
3003
|
+
*
|
|
3004
|
+
* G6's `selected` element state IS the selection: `drag-element`
|
|
3005
|
+
* reads it (`getElementDataByState`) to decide what a drag moves,
|
|
3006
|
+
* so keeping the set anywhere else would be a second truth the
|
|
3007
|
+
* drag does not consult. What this gclass keeps is what is
|
|
3008
|
+
* PAINTED -- an html node draws no state style at all (its key
|
|
3009
|
+
* shape is a DOM element), so the ring lives in the card's own
|
|
3010
|
+
* html, exactly like the find highlight, and the painted set
|
|
3011
|
+
* exists to diff the repaint.
|
|
3012
|
+
************************************************************/
|
|
3013
|
+
function selected_node_ids(gobj)
|
|
3014
|
+
{
|
|
3015
|
+
let graph = gobj.priv.graph;
|
|
3016
|
+
|
|
3017
|
+
if(!graph) {
|
|
3018
|
+
return [];
|
|
3019
|
+
}
|
|
3020
|
+
|
|
3021
|
+
let data;
|
|
3022
|
+
try {
|
|
3023
|
+
data = graph.getElementDataByState('node', 'selected') || [];
|
|
3024
|
+
} catch(e) {
|
|
3025
|
+
return []; /* asked before there is a graph to ask */
|
|
3026
|
+
}
|
|
3027
|
+
|
|
3028
|
+
return data.map((nd) => nd.id);
|
|
3029
|
+
}
|
|
3030
|
+
|
|
3031
|
+
/************************************************************
|
|
3032
|
+
* Move the ring to these nodes and off the ones that had it.
|
|
3033
|
+
************************************************************/
|
|
3034
|
+
function paint_selection(gobj, ids)
|
|
3035
|
+
{
|
|
3036
|
+
let priv = gobj.priv;
|
|
3037
|
+
let prev = priv._selected_paint_ids || [];
|
|
3038
|
+
let next = ids || [];
|
|
3039
|
+
|
|
3040
|
+
priv._selected_paint_ids = next;
|
|
3041
|
+
|
|
3042
|
+
repaint_cards(gobj, new Set([...prev, ...next]));
|
|
3043
|
+
}
|
|
3044
|
+
|
|
3045
|
+
/************************************************************
|
|
3046
|
+
* Select a SET of nodes: what a marquee and a shift-click do.
|
|
3047
|
+
*
|
|
3048
|
+
* It leaves `_selected_node_id` null even for a set of one,
|
|
3049
|
+
* because that field is not "the selection", it is "the node
|
|
3050
|
+
* opened for editing" -- the resize handles, the ports and the
|
|
3051
|
+
* popovers hang off it, and none of them means anything over a
|
|
3052
|
+
* set. Clicking a node is what opens one (`select_node`); a
|
|
3053
|
+
* marquee selects, it does not open.
|
|
3054
|
+
************************************************************/
|
|
3055
|
+
function set_selection(gobj, ids)
|
|
3056
|
+
{
|
|
3057
|
+
let priv = gobj.priv;
|
|
3058
|
+
let graph = priv.graph;
|
|
3059
|
+
|
|
3060
|
+
let next = [];
|
|
3061
|
+
for(let id of (ids || [])) {
|
|
3062
|
+
try {
|
|
3063
|
+
if(graph.getNodeData(id)) {
|
|
3064
|
+
next.push(id);
|
|
3065
|
+
}
|
|
3066
|
+
} catch(e) {
|
|
3067
|
+
/* An id the graph does not have: a brush answers with what
|
|
3068
|
+
* it enclosed, and a node can be gone by the time we ask. */
|
|
3069
|
+
}
|
|
3070
|
+
}
|
|
3071
|
+
|
|
3072
|
+
deselect_node(gobj); /* the state, the paint and the affordances */
|
|
3073
|
+
|
|
3074
|
+
if(!next.length) {
|
|
3075
|
+
return;
|
|
3076
|
+
}
|
|
3077
|
+
|
|
3078
|
+
history_pause(gobj);
|
|
3079
|
+
try {
|
|
3080
|
+
let states = {};
|
|
3081
|
+
for(let id of next) {
|
|
3082
|
+
states[id] = ['selected'];
|
|
3083
|
+
}
|
|
3084
|
+
graph.setElementState(states);
|
|
3085
|
+
} catch(e) {
|
|
3086
|
+
log_error(`${gobj_short_name(gobj)}: cannot set the selection: ${e}`);
|
|
3087
|
+
}
|
|
3088
|
+
|
|
3089
|
+
paint_selection(gobj, next);
|
|
3090
|
+
|
|
3091
|
+
graph_draw(gobj).then(() => {
|
|
3092
|
+
history_resume(gobj);
|
|
3093
|
+
});
|
|
3094
|
+
}
|
|
3095
|
+
|
|
3096
|
+
/************************************************************
|
|
3097
|
+
* Add a node to the selection, or take it out of it.
|
|
3098
|
+
************************************************************/
|
|
3099
|
+
function toggle_in_selection(gobj, node_id)
|
|
3100
|
+
{
|
|
3101
|
+
let priv = gobj.priv;
|
|
3102
|
+
|
|
3103
|
+
/* The union of the two views of the same thing. A brush clears
|
|
3104
|
+
* G6's state on pointerdown without telling anybody, so reading
|
|
3105
|
+
* only the state could drop a card that is on screen wearing a
|
|
3106
|
+
* ring. */
|
|
3107
|
+
let current = new Set(selected_node_ids(gobj));
|
|
3108
|
+
for(let id of (priv._selected_paint_ids || [])) {
|
|
3109
|
+
current.add(id);
|
|
3110
|
+
}
|
|
3111
|
+
|
|
3112
|
+
if(current.has(node_id)) {
|
|
3113
|
+
current.delete(node_id);
|
|
3114
|
+
} else {
|
|
3115
|
+
current.add(node_id);
|
|
3116
|
+
}
|
|
3117
|
+
|
|
3118
|
+
set_selection(gobj, [...current]);
|
|
3119
|
+
}
|
|
3120
|
+
|
|
2846
3121
|
function select_node(gobj, node_id)
|
|
2847
3122
|
{
|
|
2848
3123
|
let priv = gobj.priv;
|
|
@@ -2857,6 +3132,7 @@ function select_node(gobj, node_id)
|
|
|
2857
3132
|
graph.setElementState(node_id, ['selected']);
|
|
2858
3133
|
} catch(e) {}
|
|
2859
3134
|
priv._selected_node_id = node_id;
|
|
3135
|
+
paint_selection(gobj, [node_id]);
|
|
2860
3136
|
|
|
2861
3137
|
// Show resize handles and properties icon
|
|
2862
3138
|
show_resize_handles(gobj);
|
|
@@ -2879,12 +3155,28 @@ function deselect_node(gobj)
|
|
|
2879
3155
|
hide_delete_confirm(gobj);
|
|
2880
3156
|
hide_unlink_confirm(gobj);
|
|
2881
3157
|
|
|
3158
|
+
/* Both views of the selection, because either can hold an id the
|
|
3159
|
+
* other lost: G6's state is what a drag moves, the painted set is
|
|
3160
|
+
* what the reader can see. */
|
|
3161
|
+
let clearing = new Set([
|
|
3162
|
+
...selected_node_ids(gobj),
|
|
3163
|
+
...(priv._selected_paint_ids || [])
|
|
3164
|
+
]);
|
|
2882
3165
|
if(priv._selected_node_id) {
|
|
3166
|
+
clearing.add(priv._selected_node_id);
|
|
3167
|
+
}
|
|
3168
|
+
|
|
3169
|
+
if(clearing.size) {
|
|
2883
3170
|
history_pause(gobj);
|
|
2884
3171
|
try {
|
|
2885
|
-
|
|
3172
|
+
let states = {};
|
|
3173
|
+
for(let id of clearing) {
|
|
3174
|
+
states[id] = [];
|
|
3175
|
+
}
|
|
3176
|
+
graph.setElementState(states);
|
|
2886
3177
|
} catch(e) {}
|
|
2887
3178
|
priv._selected_node_id = null;
|
|
3179
|
+
paint_selection(gobj, []);
|
|
2888
3180
|
|
|
2889
3181
|
graph_draw(gobj).then(() => {
|
|
2890
3182
|
history_resume(gobj);
|
|
@@ -4688,7 +4980,7 @@ function refresh_minimap(gobj)
|
|
|
4688
4980
|
* same three-way choice — it lived inline in the theme refresh and is
|
|
4689
4981
|
* shared now. Returns null for a node that carries no desc.
|
|
4690
4982
|
************************************************************/
|
|
4691
|
-
function node_innerHTML_of(nd, theme, highlight)
|
|
4983
|
+
function node_innerHTML_of(nd, theme, highlight, selected)
|
|
4692
4984
|
{
|
|
4693
4985
|
if(!nd || !nd.data || !nd.data.desc) {
|
|
4694
4986
|
return null;
|
|
@@ -4700,45 +4992,49 @@ function node_innerHTML_of(nd, theme, highlight)
|
|
|
4700
4992
|
switch(desc.node_treedb_type) {
|
|
4701
4993
|
case 'child':
|
|
4702
4994
|
return build_chip_innerHTML(
|
|
4703
|
-
desc.color, theme, record.icon, label, record.id,
|
|
4995
|
+
desc.color, theme, record.icon, label, record.id,
|
|
4996
|
+
highlight, selected
|
|
4704
4997
|
);
|
|
4705
4998
|
case 'extended':
|
|
4706
4999
|
return build_node_innerHTML(
|
|
4707
5000
|
desc.color, theme, record.icon, label,
|
|
4708
|
-
desc.topic_name, true, record.id, highlight
|
|
5001
|
+
desc.topic_name, true, record.id, highlight, selected
|
|
4709
5002
|
);
|
|
4710
5003
|
case 'hierarchical':
|
|
4711
5004
|
return build_node_innerHTML(
|
|
4712
5005
|
desc.color, theme, record.icon, label,
|
|
4713
|
-
desc.topic_name, false, record.id, highlight
|
|
5006
|
+
desc.topic_name, false, record.id, highlight, selected
|
|
4714
5007
|
);
|
|
4715
5008
|
}
|
|
4716
5009
|
return null;
|
|
4717
5010
|
}
|
|
4718
5011
|
|
|
4719
5012
|
/************************************************************
|
|
4720
|
-
* Repaint
|
|
4721
|
-
*
|
|
4722
|
-
*
|
|
5013
|
+
* Repaint these cards with the flags they carry RIGHT NOW.
|
|
5014
|
+
*
|
|
5015
|
+
* One place, because the flags are independent and each used to
|
|
5016
|
+
* be written by whoever repainted last: a find that repainted its
|
|
5017
|
+
* matches erased the selection ring off them, and a selection
|
|
5018
|
+
* repainted over a match erased the amber. Both are read here
|
|
5019
|
+
* from where they live.
|
|
4723
5020
|
************************************************************/
|
|
4724
|
-
function
|
|
5021
|
+
function repaint_cards(gobj, ids)
|
|
4725
5022
|
{
|
|
4726
5023
|
let priv = gobj.priv;
|
|
4727
5024
|
let graph = priv.graph;
|
|
4728
|
-
if(!graph) {
|
|
4729
|
-
return;
|
|
4730
|
-
}
|
|
4731
5025
|
|
|
4732
|
-
|
|
4733
|
-
let touched = new Set([...(prev_ids || []), ...next]);
|
|
4734
|
-
if(touched.size === 0) {
|
|
5026
|
+
if(!graph || !ids || !ids.size) {
|
|
4735
5027
|
return;
|
|
4736
5028
|
}
|
|
4737
5029
|
|
|
5030
|
+
let focus = new Set(priv._focus_ids || []);
|
|
5031
|
+
let selected = new Set(priv._selected_paint_ids || []);
|
|
5032
|
+
|
|
4738
5033
|
let updates = [];
|
|
4739
|
-
for(let id of
|
|
4740
|
-
let
|
|
4741
|
-
|
|
5034
|
+
for(let id of ids) {
|
|
5035
|
+
let html = node_innerHTML_of(
|
|
5036
|
+
graph.getNodeData(id), priv.theme, focus.has(id), selected.has(id)
|
|
5037
|
+
);
|
|
4742
5038
|
if(html !== null) {
|
|
4743
5039
|
updates.push({id: id, style: {innerHTML: html}});
|
|
4744
5040
|
}
|
|
@@ -4746,14 +5042,37 @@ function apply_node_highlight(gobj, prev_ids, next_ids)
|
|
|
4746
5042
|
if(!updates.length) {
|
|
4747
5043
|
return;
|
|
4748
5044
|
}
|
|
5045
|
+
|
|
4749
5046
|
try {
|
|
4750
5047
|
graph.updateNodeData(updates);
|
|
4751
5048
|
graph.draw();
|
|
4752
5049
|
} catch(e) {
|
|
4753
|
-
log_error(`${gobj_short_name(gobj)}: cannot repaint the
|
|
5050
|
+
log_error(`${gobj_short_name(gobj)}: cannot repaint the cards: ${e}`);
|
|
4754
5051
|
}
|
|
4755
5052
|
}
|
|
4756
5053
|
|
|
5054
|
+
/************************************************************
|
|
5055
|
+
* Repaint the cards whose highlight state CHANGES: the ones that had it
|
|
5056
|
+
* and lose it, plus the ones that gain it. Only those — a treedb graph
|
|
5057
|
+
* is redrawn per keystroke of the find box otherwise.
|
|
5058
|
+
************************************************************/
|
|
5059
|
+
function apply_node_highlight(gobj, prev_ids, next_ids)
|
|
5060
|
+
{
|
|
5061
|
+
let priv = gobj.priv;
|
|
5062
|
+
let graph = priv.graph;
|
|
5063
|
+
if(!graph) {
|
|
5064
|
+
return;
|
|
5065
|
+
}
|
|
5066
|
+
|
|
5067
|
+
let next = new Set(next_ids || []);
|
|
5068
|
+
let touched = new Set([...(prev_ids || []), ...next]);
|
|
5069
|
+
if(touched.size === 0) {
|
|
5070
|
+
return;
|
|
5071
|
+
}
|
|
5072
|
+
|
|
5073
|
+
repaint_cards(gobj, touched);
|
|
5074
|
+
}
|
|
5075
|
+
|
|
4757
5076
|
function refresh_html_nodes_theme(gobj, theme)
|
|
4758
5077
|
{
|
|
4759
5078
|
let priv = gobj.priv;
|
|
@@ -4766,10 +5085,13 @@ function refresh_html_nodes_theme(gobj, theme)
|
|
|
4766
5085
|
* rebuilt every card without it would silently CLEAR the focus or the
|
|
4767
5086
|
* find that is on screen. Carry it across. */
|
|
4768
5087
|
let highlighted = new Set(priv._focus_ids || []);
|
|
5088
|
+
let selected = new Set(priv._selected_paint_ids || []);
|
|
4769
5089
|
let updates = [];
|
|
4770
5090
|
for(let i = 0; i < nodes.length; i++) {
|
|
4771
5091
|
let id = nodes[i].id;
|
|
4772
|
-
let html = node_innerHTML_of(
|
|
5092
|
+
let html = node_innerHTML_of(
|
|
5093
|
+
graph.getNodeData(id), theme, highlighted.has(id), selected.has(id)
|
|
5094
|
+
);
|
|
4773
5095
|
if(html !== null) {
|
|
4774
5096
|
updates.push({id: id, style: {innerHTML: html}});
|
|
4775
5097
|
}
|
|
@@ -4808,13 +5130,42 @@ function refresh_default_edges_theme(gobj, theme)
|
|
|
4808
5130
|
}
|
|
4809
5131
|
}
|
|
4810
5132
|
|
|
5133
|
+
/************************************************************
|
|
5134
|
+
* The rings a card can wear, composed into one `box-shadow`.
|
|
5135
|
+
*
|
|
5136
|
+
* A node can be a find match and be selected at the same time, so
|
|
5137
|
+
* neither ring may be written by overwriting the other: the amber
|
|
5138
|
+
* halo hugs the card and the blue selection ring is drawn outside
|
|
5139
|
+
* it, which is also the order that reads correctly when only one
|
|
5140
|
+
* of them is on.
|
|
5141
|
+
************************************************************/
|
|
5142
|
+
function ring_shadow(highlight, selected, base_shadow)
|
|
5143
|
+
{
|
|
5144
|
+
let rings = [];
|
|
5145
|
+
|
|
5146
|
+
if(highlight) {
|
|
5147
|
+
rings.push(`0 0 0 4px ${HIGHLIGHT_HALO}`);
|
|
5148
|
+
}
|
|
5149
|
+
if(selected) {
|
|
5150
|
+
rings.push(`0 0 0 ${highlight? "7px" : "3px"} ${SELECT_RING}`);
|
|
5151
|
+
}
|
|
5152
|
+
if(base_shadow) {
|
|
5153
|
+
rings.push(base_shadow);
|
|
5154
|
+
}
|
|
5155
|
+
if(!rings.length) {
|
|
5156
|
+
return "";
|
|
5157
|
+
}
|
|
5158
|
+
|
|
5159
|
+
return `box-shadow: ${rings.join(", ")};`;
|
|
5160
|
+
}
|
|
5161
|
+
|
|
4811
5162
|
/************************************************************
|
|
4812
5163
|
* Build innerHTML for pure-child (leaf) nodes: a compact
|
|
4813
5164
|
* chip-card. Same colour/typography family as the entity card
|
|
4814
5165
|
* but lighter (1px border, no shadow, single line). The name is
|
|
4815
5166
|
* always legible (ellipsis + native title tooltip on overflow).
|
|
4816
5167
|
************************************************************/
|
|
4817
|
-
function build_chip_innerHTML(color, theme, icon, label, key, highlight)
|
|
5168
|
+
function build_chip_innerHTML(color, theme, icon, label, key, highlight, selected)
|
|
4818
5169
|
{
|
|
4819
5170
|
let title = key || label;
|
|
4820
5171
|
let dark = (theme === "dark");
|
|
@@ -4845,7 +5196,7 @@ function build_chip_innerHTML(color, theme, icon, label, key, highlight)
|
|
|
4845
5196
|
height: 100%;
|
|
4846
5197
|
background: ${bg};
|
|
4847
5198
|
border: ${highlight? "3px" : "1px"} solid ${border};
|
|
4848
|
-
${highlight
|
|
5199
|
+
${ring_shadow(highlight, selected, "")}
|
|
4849
5200
|
border-radius: 8px;
|
|
4850
5201
|
color: ${text_color};
|
|
4851
5202
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
|
@@ -4872,7 +5223,7 @@ function build_chip_innerHTML(color, theme, icon, label, key, highlight)
|
|
|
4872
5223
|
* colour is kept (per-topic differentiation) but softened via
|
|
4873
5224
|
* color-mix instead of a harsh saturated fill. Theme-aware.
|
|
4874
5225
|
************************************************************/
|
|
4875
|
-
function build_node_innerHTML(color, theme, icon, label, topic_name, structural, key, highlight)
|
|
5226
|
+
function build_node_innerHTML(color, theme, icon, label, topic_name, structural, key, highlight, selected)
|
|
4876
5227
|
{
|
|
4877
5228
|
let title = key || label;
|
|
4878
5229
|
let dark = (theme === "dark");
|
|
@@ -4935,7 +5286,7 @@ function build_node_innerHTML(color, theme, icon, label, topic_name, structural,
|
|
|
4935
5286
|
background: ${bg};
|
|
4936
5287
|
border: ${highlight? "3px" : "1.5px"} ${border_style} ${border};
|
|
4937
5288
|
border-radius: 10px;
|
|
4938
|
-
|
|
5289
|
+
${ring_shadow(highlight, selected, shadow)}
|
|
4939
5290
|
color: ${title_color};
|
|
4940
5291
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
|
4941
5292
|
display: flex;
|
|
@@ -6372,6 +6723,20 @@ function ac_node_drag_end(gobj, event, kw, src)
|
|
|
6372
6723
|
return 0;
|
|
6373
6724
|
}
|
|
6374
6725
|
|
|
6726
|
+
/************************************************************
|
|
6727
|
+
* The rubber band let go: these are the nodes it enclosed.
|
|
6728
|
+
*
|
|
6729
|
+
* They arrive in the kw because `onSelect` runs BEFORE G6 writes
|
|
6730
|
+
* the state, so asking the graph here would answer the previous
|
|
6731
|
+
* selection.
|
|
6732
|
+
************************************************************/
|
|
6733
|
+
function ac_brush_select(gobj, event, kw, src)
|
|
6734
|
+
{
|
|
6735
|
+
set_selection(gobj, (kw && kw.ids) || []);
|
|
6736
|
+
|
|
6737
|
+
return 0;
|
|
6738
|
+
}
|
|
6739
|
+
|
|
6375
6740
|
/************************************************************
|
|
6376
6741
|
* Node click - publish vertex clicked event
|
|
6377
6742
|
************************************************************/
|
|
@@ -6390,7 +6755,13 @@ function ac_node_click(gobj, event, kw, src)
|
|
|
6390
6755
|
record: nodedata.data.record
|
|
6391
6756
|
});
|
|
6392
6757
|
|
|
6393
|
-
if(priv.edit_mode) {
|
|
6758
|
+
if(priv.edit_mode && kw.evt.shiftKey) {
|
|
6759
|
+
/* Shift+click extends the selection, the way it does
|
|
6760
|
+
* everywhere. It never looks for a port: a port is a
|
|
6761
|
+
* one-node affordance, and this gesture is about the
|
|
6762
|
+
* set. */
|
|
6763
|
+
toggle_in_selection(gobj, node_id);
|
|
6764
|
+
} else if(priv.edit_mode) {
|
|
6394
6765
|
// Check if click hits a port
|
|
6395
6766
|
// Convert client coords to viewport (container-relative) then to canvas
|
|
6396
6767
|
let containerRect = priv.$container.getBoundingClientRect();
|
|
@@ -6677,6 +7048,7 @@ function create_gclass(gclass_name)
|
|
|
6677
7048
|
["EV_NODE_CONTEXT_MENU", ac_node_context_menu, null],
|
|
6678
7049
|
["EV_CANVAS_CLICK", ac_canvas_click, null],
|
|
6679
7050
|
["EV_NODE_DRAG_END", ac_node_drag_end, null],
|
|
7051
|
+
["EV_BRUSH_SELECT", ac_brush_select, null],
|
|
6680
7052
|
|
|
6681
7053
|
/*--- Toolbar events ---*/
|
|
6682
7054
|
["EV_ZOOM_IN", ac_zoom_in, null],
|
|
@@ -6721,6 +7093,7 @@ function create_gclass(gclass_name)
|
|
|
6721
7093
|
["EV_NODE_CONTEXT_MENU", 0],
|
|
6722
7094
|
["EV_CANVAS_CLICK", 0],
|
|
6723
7095
|
["EV_NODE_DRAG_END", 0],
|
|
7096
|
+
["EV_BRUSH_SELECT", 0],
|
|
6724
7097
|
|
|
6725
7098
|
/*--- Toolbar (internal) ---*/
|
|
6726
7099
|
["EV_ZOOM_IN", 0],
|