@yuneta/gobj-ui 7.15.0 → 7.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -0
- package/dist/gobj-ui.cjs.js +310 -41
- package/dist/gobj-ui.es.js +310 -41
- package/package.json +1 -1
- package/src/c_g6_nodes_tree.js +508 -62
package/src/c_g6_nodes_tree.js
CHANGED
|
@@ -184,6 +184,15 @@ const GCLASS_NAME = "C_G6_NODES_TREE";
|
|
|
184
184
|
const HIGHLIGHT_COLOR = "#f0a020";
|
|
185
185
|
const HIGHLIGHT_HALO = "rgba(240,160,32,0.35)";
|
|
186
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
|
+
|
|
187
196
|
/***************************************************************
|
|
188
197
|
* Internal layout and operation mode definitions
|
|
189
198
|
***************************************************************/
|
|
@@ -324,6 +333,10 @@ let PRIVATE_DATA = {
|
|
|
324
333
|
_link_saved_styles: [], // saved port styles to restore on cancel
|
|
325
334
|
_focus_topic: null, // topic currently focused (EV_FOCUS_TOPIC)
|
|
326
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)
|
|
327
340
|
_pending_focus_topic: null, // focus requested before data was loaded
|
|
328
341
|
_pending_find: null, // find requested before data was loaded
|
|
329
342
|
_layout_asked: "", // layout the host asked for at create (see mt_create)
|
|
@@ -774,20 +787,24 @@ function configure_events(gobj)
|
|
|
774
787
|
};
|
|
775
788
|
i18next.on('languageChanged', priv._on_language_changed);
|
|
776
789
|
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
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();
|
|
788
803
|
}
|
|
789
|
-
}
|
|
790
|
-
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
gobj_send_event(gobj, "EV_KEY_DOWN", {key: evt.key, ctrl: ctrl}, gobj);
|
|
807
|
+
});
|
|
791
808
|
}
|
|
792
809
|
|
|
793
810
|
/************************************************************
|
|
@@ -1182,9 +1199,46 @@ function configure_behaviour(gobj)
|
|
|
1182
1199
|
case "edition":
|
|
1183
1200
|
priv.edit_mode = true;
|
|
1184
1201
|
behaviors = [
|
|
1185
|
-
|
|
1202
|
+
/* Panning gives way while Shift is held: that is the
|
|
1203
|
+
* marquee's gesture, and G6 binds drag-canvas straight
|
|
1204
|
+
* to the drag events, so without this the canvas pans
|
|
1205
|
+
* under the rubber band. */
|
|
1206
|
+
{
|
|
1207
|
+
type: "drag-canvas",
|
|
1208
|
+
key: "drag-canvas",
|
|
1209
|
+
enable: (event) => !event.shiftKey,
|
|
1210
|
+
},
|
|
1186
1211
|
"zoom-canvas",
|
|
1212
|
+
/* Moves EVERY node in the `selected` state, not just
|
|
1213
|
+
* the one under the pointer, and wraps the whole move
|
|
1214
|
+
* in one history batch -- so a group move is one drag
|
|
1215
|
+
* and one undo. Nothing to configure: `selected` is
|
|
1216
|
+
* already its default `state`. */
|
|
1187
1217
|
"drag-element",
|
|
1218
|
+
/* Shift+drag on the canvas: the rubber band. The
|
|
1219
|
+
* GESTURE is G6's, its RESULT enters the machine --
|
|
1220
|
+
* `onSelect` fires before G6 writes the state, so the
|
|
1221
|
+
* ids travel with the event and the action does not
|
|
1222
|
+
* have to race it. */
|
|
1223
|
+
{
|
|
1224
|
+
type: "brush-select",
|
|
1225
|
+
key: "brush-select",
|
|
1226
|
+
trigger: ["shift"],
|
|
1227
|
+
enableElements: ["node"],
|
|
1228
|
+
/* Left at G6's default (false): the set is read at
|
|
1229
|
+
* pointerup, not on every pointermove. Each answer
|
|
1230
|
+
* repaints the cards it touches, and doing that per
|
|
1231
|
+
* frame of a rubber band over a hundred nodes is
|
|
1232
|
+
* paid for nothing -- the band already shows what
|
|
1233
|
+
* it covers. */
|
|
1234
|
+
immediately: false,
|
|
1235
|
+
onSelect: (states) => {
|
|
1236
|
+
let ids = Object.keys(states || {}).filter(
|
|
1237
|
+
(id) => (states[id] || []).includes("selected")
|
|
1238
|
+
);
|
|
1239
|
+
gobj_send_event(gobj, "EV_BRUSH_SELECT", {ids: ids}, gobj);
|
|
1240
|
+
},
|
|
1241
|
+
},
|
|
1188
1242
|
];
|
|
1189
1243
|
break;
|
|
1190
1244
|
case "operation":
|
|
@@ -2875,14 +2929,19 @@ function show_confirm_popover(gobj, target_el, message, confirm_text, confirm_co
|
|
|
2875
2929
|
hide_overlay(gobj, priv_key);
|
|
2876
2930
|
|
|
2877
2931
|
let priv = gobj.priv;
|
|
2878
|
-
if(!target_el) {
|
|
2879
|
-
return;
|
|
2880
|
-
}
|
|
2881
|
-
|
|
2882
|
-
let iconRect = target_el.getBoundingClientRect();
|
|
2883
2932
|
let containerRect = priv.$container.getBoundingClientRect();
|
|
2884
|
-
let left
|
|
2885
|
-
|
|
2933
|
+
let left, top;
|
|
2934
|
+
|
|
2935
|
+
if(target_el) {
|
|
2936
|
+
let iconRect = target_el.getBoundingClientRect();
|
|
2937
|
+
left = iconRect.right - containerRect.left + 6;
|
|
2938
|
+
top = iconRect.top - containerRect.top - 4;
|
|
2939
|
+
} else {
|
|
2940
|
+
/* A question about a SET has no icon to hang off: it is asked
|
|
2941
|
+
* in the middle of the graph it is about. */
|
|
2942
|
+
left = Math.round(containerRect.width / 2) - 110;
|
|
2943
|
+
top = Math.round(containerRect.height / 4);
|
|
2944
|
+
}
|
|
2886
2945
|
|
|
2887
2946
|
const popover = create_popover_base(left, top, 'g6-confirm-popover', '#ff4d4f', 160);
|
|
2888
2947
|
|
|
@@ -2948,6 +3007,126 @@ function perform_history_op(gobj, is_redo)
|
|
|
2948
3007
|
}
|
|
2949
3008
|
}
|
|
2950
3009
|
|
|
3010
|
+
/************************************************************
|
|
3011
|
+
* The selection.
|
|
3012
|
+
*
|
|
3013
|
+
* G6's `selected` element state IS the selection: `drag-element`
|
|
3014
|
+
* reads it (`getElementDataByState`) to decide what a drag moves,
|
|
3015
|
+
* so keeping the set anywhere else would be a second truth the
|
|
3016
|
+
* drag does not consult. What this gclass keeps is what is
|
|
3017
|
+
* PAINTED -- an html node draws no state style at all (its key
|
|
3018
|
+
* shape is a DOM element), so the ring lives in the card's own
|
|
3019
|
+
* html, exactly like the find highlight, and the painted set
|
|
3020
|
+
* exists to diff the repaint.
|
|
3021
|
+
************************************************************/
|
|
3022
|
+
function selected_node_ids(gobj)
|
|
3023
|
+
{
|
|
3024
|
+
let graph = gobj.priv.graph;
|
|
3025
|
+
|
|
3026
|
+
if(!graph) {
|
|
3027
|
+
return [];
|
|
3028
|
+
}
|
|
3029
|
+
|
|
3030
|
+
let data;
|
|
3031
|
+
try {
|
|
3032
|
+
data = graph.getElementDataByState('node', 'selected') || [];
|
|
3033
|
+
} catch(e) {
|
|
3034
|
+
return []; /* asked before there is a graph to ask */
|
|
3035
|
+
}
|
|
3036
|
+
|
|
3037
|
+
return data.map((nd) => nd.id);
|
|
3038
|
+
}
|
|
3039
|
+
|
|
3040
|
+
/************************************************************
|
|
3041
|
+
* Move the ring to these nodes and off the ones that had it.
|
|
3042
|
+
************************************************************/
|
|
3043
|
+
function paint_selection(gobj, ids)
|
|
3044
|
+
{
|
|
3045
|
+
let priv = gobj.priv;
|
|
3046
|
+
let prev = priv._selected_paint_ids || [];
|
|
3047
|
+
let next = ids || [];
|
|
3048
|
+
|
|
3049
|
+
priv._selected_paint_ids = next;
|
|
3050
|
+
|
|
3051
|
+
repaint_cards(gobj, new Set([...prev, ...next]));
|
|
3052
|
+
}
|
|
3053
|
+
|
|
3054
|
+
/************************************************************
|
|
3055
|
+
* Select a SET of nodes: what a marquee and a shift-click do.
|
|
3056
|
+
*
|
|
3057
|
+
* It leaves `_selected_node_id` null even for a set of one,
|
|
3058
|
+
* because that field is not "the selection", it is "the node
|
|
3059
|
+
* opened for editing" -- the resize handles, the ports and the
|
|
3060
|
+
* popovers hang off it, and none of them means anything over a
|
|
3061
|
+
* set. Clicking a node is what opens one (`select_node`); a
|
|
3062
|
+
* marquee selects, it does not open.
|
|
3063
|
+
************************************************************/
|
|
3064
|
+
function set_selection(gobj, ids)
|
|
3065
|
+
{
|
|
3066
|
+
let priv = gobj.priv;
|
|
3067
|
+
let graph = priv.graph;
|
|
3068
|
+
|
|
3069
|
+
let next = [];
|
|
3070
|
+
for(let id of (ids || [])) {
|
|
3071
|
+
try {
|
|
3072
|
+
if(graph.getNodeData(id)) {
|
|
3073
|
+
next.push(id);
|
|
3074
|
+
}
|
|
3075
|
+
} catch(e) {
|
|
3076
|
+
/* An id the graph does not have: a brush answers with what
|
|
3077
|
+
* it enclosed, and a node can be gone by the time we ask. */
|
|
3078
|
+
}
|
|
3079
|
+
}
|
|
3080
|
+
|
|
3081
|
+
deselect_node(gobj); /* the state, the paint and the affordances */
|
|
3082
|
+
|
|
3083
|
+
if(!next.length) {
|
|
3084
|
+
return;
|
|
3085
|
+
}
|
|
3086
|
+
|
|
3087
|
+
history_pause(gobj);
|
|
3088
|
+
try {
|
|
3089
|
+
let states = {};
|
|
3090
|
+
for(let id of next) {
|
|
3091
|
+
states[id] = ['selected'];
|
|
3092
|
+
}
|
|
3093
|
+
graph.setElementState(states);
|
|
3094
|
+
} catch(e) {
|
|
3095
|
+
log_error(`${gobj_short_name(gobj)}: cannot set the selection: ${e}`);
|
|
3096
|
+
}
|
|
3097
|
+
|
|
3098
|
+
paint_selection(gobj, next);
|
|
3099
|
+
|
|
3100
|
+
graph_draw(gobj).then(() => {
|
|
3101
|
+
history_resume(gobj);
|
|
3102
|
+
});
|
|
3103
|
+
}
|
|
3104
|
+
|
|
3105
|
+
/************************************************************
|
|
3106
|
+
* Add a node to the selection, or take it out of it.
|
|
3107
|
+
************************************************************/
|
|
3108
|
+
function toggle_in_selection(gobj, node_id)
|
|
3109
|
+
{
|
|
3110
|
+
let priv = gobj.priv;
|
|
3111
|
+
|
|
3112
|
+
/* The union of the two views of the same thing. A brush clears
|
|
3113
|
+
* G6's state on pointerdown without telling anybody, so reading
|
|
3114
|
+
* only the state could drop a card that is on screen wearing a
|
|
3115
|
+
* ring. */
|
|
3116
|
+
let current = new Set(selected_node_ids(gobj));
|
|
3117
|
+
for(let id of (priv._selected_paint_ids || [])) {
|
|
3118
|
+
current.add(id);
|
|
3119
|
+
}
|
|
3120
|
+
|
|
3121
|
+
if(current.has(node_id)) {
|
|
3122
|
+
current.delete(node_id);
|
|
3123
|
+
} else {
|
|
3124
|
+
current.add(node_id);
|
|
3125
|
+
}
|
|
3126
|
+
|
|
3127
|
+
set_selection(gobj, [...current]);
|
|
3128
|
+
}
|
|
3129
|
+
|
|
2951
3130
|
function select_node(gobj, node_id)
|
|
2952
3131
|
{
|
|
2953
3132
|
let priv = gobj.priv;
|
|
@@ -2962,6 +3141,7 @@ function select_node(gobj, node_id)
|
|
|
2962
3141
|
graph.setElementState(node_id, ['selected']);
|
|
2963
3142
|
} catch(e) {}
|
|
2964
3143
|
priv._selected_node_id = node_id;
|
|
3144
|
+
paint_selection(gobj, [node_id]);
|
|
2965
3145
|
|
|
2966
3146
|
// Show resize handles and properties icon
|
|
2967
3147
|
show_resize_handles(gobj);
|
|
@@ -2984,12 +3164,28 @@ function deselect_node(gobj)
|
|
|
2984
3164
|
hide_delete_confirm(gobj);
|
|
2985
3165
|
hide_unlink_confirm(gobj);
|
|
2986
3166
|
|
|
3167
|
+
/* Both views of the selection, because either can hold an id the
|
|
3168
|
+
* other lost: G6's state is what a drag moves, the painted set is
|
|
3169
|
+
* what the reader can see. */
|
|
3170
|
+
let clearing = new Set([
|
|
3171
|
+
...selected_node_ids(gobj),
|
|
3172
|
+
...(priv._selected_paint_ids || [])
|
|
3173
|
+
]);
|
|
2987
3174
|
if(priv._selected_node_id) {
|
|
3175
|
+
clearing.add(priv._selected_node_id);
|
|
3176
|
+
}
|
|
3177
|
+
|
|
3178
|
+
if(clearing.size) {
|
|
2988
3179
|
history_pause(gobj);
|
|
2989
3180
|
try {
|
|
2990
|
-
|
|
3181
|
+
let states = {};
|
|
3182
|
+
for(let id of clearing) {
|
|
3183
|
+
states[id] = [];
|
|
3184
|
+
}
|
|
3185
|
+
graph.setElementState(states);
|
|
2991
3186
|
} catch(e) {}
|
|
2992
3187
|
priv._selected_node_id = null;
|
|
3188
|
+
paint_selection(gobj, []);
|
|
2993
3189
|
|
|
2994
3190
|
graph_draw(gobj).then(() => {
|
|
2995
3191
|
history_resume(gobj);
|
|
@@ -4793,7 +4989,7 @@ function refresh_minimap(gobj)
|
|
|
4793
4989
|
* same three-way choice — it lived inline in the theme refresh and is
|
|
4794
4990
|
* shared now. Returns null for a node that carries no desc.
|
|
4795
4991
|
************************************************************/
|
|
4796
|
-
function node_innerHTML_of(nd, theme, highlight)
|
|
4992
|
+
function node_innerHTML_of(nd, theme, highlight, selected)
|
|
4797
4993
|
{
|
|
4798
4994
|
if(!nd || !nd.data || !nd.data.desc) {
|
|
4799
4995
|
return null;
|
|
@@ -4805,45 +5001,49 @@ function node_innerHTML_of(nd, theme, highlight)
|
|
|
4805
5001
|
switch(desc.node_treedb_type) {
|
|
4806
5002
|
case 'child':
|
|
4807
5003
|
return build_chip_innerHTML(
|
|
4808
|
-
desc.color, theme, record.icon, label, record.id,
|
|
5004
|
+
desc.color, theme, record.icon, label, record.id,
|
|
5005
|
+
highlight, selected
|
|
4809
5006
|
);
|
|
4810
5007
|
case 'extended':
|
|
4811
5008
|
return build_node_innerHTML(
|
|
4812
5009
|
desc.color, theme, record.icon, label,
|
|
4813
|
-
desc.topic_name, true, record.id, highlight
|
|
5010
|
+
desc.topic_name, true, record.id, highlight, selected
|
|
4814
5011
|
);
|
|
4815
5012
|
case 'hierarchical':
|
|
4816
5013
|
return build_node_innerHTML(
|
|
4817
5014
|
desc.color, theme, record.icon, label,
|
|
4818
|
-
desc.topic_name, false, record.id, highlight
|
|
5015
|
+
desc.topic_name, false, record.id, highlight, selected
|
|
4819
5016
|
);
|
|
4820
5017
|
}
|
|
4821
5018
|
return null;
|
|
4822
5019
|
}
|
|
4823
5020
|
|
|
4824
5021
|
/************************************************************
|
|
4825
|
-
* Repaint
|
|
4826
|
-
*
|
|
4827
|
-
*
|
|
5022
|
+
* Repaint these cards with the flags they carry RIGHT NOW.
|
|
5023
|
+
*
|
|
5024
|
+
* One place, because the flags are independent and each used to
|
|
5025
|
+
* be written by whoever repainted last: a find that repainted its
|
|
5026
|
+
* matches erased the selection ring off them, and a selection
|
|
5027
|
+
* repainted over a match erased the amber. Both are read here
|
|
5028
|
+
* from where they live.
|
|
4828
5029
|
************************************************************/
|
|
4829
|
-
function
|
|
5030
|
+
function repaint_cards(gobj, ids)
|
|
4830
5031
|
{
|
|
4831
5032
|
let priv = gobj.priv;
|
|
4832
5033
|
let graph = priv.graph;
|
|
4833
|
-
if(!graph) {
|
|
4834
|
-
return;
|
|
4835
|
-
}
|
|
4836
5034
|
|
|
4837
|
-
|
|
4838
|
-
let touched = new Set([...(prev_ids || []), ...next]);
|
|
4839
|
-
if(touched.size === 0) {
|
|
5035
|
+
if(!graph || !ids || !ids.size) {
|
|
4840
5036
|
return;
|
|
4841
5037
|
}
|
|
4842
5038
|
|
|
5039
|
+
let focus = new Set(priv._focus_ids || []);
|
|
5040
|
+
let selected = new Set(priv._selected_paint_ids || []);
|
|
5041
|
+
|
|
4843
5042
|
let updates = [];
|
|
4844
|
-
for(let id of
|
|
4845
|
-
let
|
|
4846
|
-
|
|
5043
|
+
for(let id of ids) {
|
|
5044
|
+
let html = node_innerHTML_of(
|
|
5045
|
+
graph.getNodeData(id), priv.theme, focus.has(id), selected.has(id)
|
|
5046
|
+
);
|
|
4847
5047
|
if(html !== null) {
|
|
4848
5048
|
updates.push({id: id, style: {innerHTML: html}});
|
|
4849
5049
|
}
|
|
@@ -4851,12 +5051,35 @@ function apply_node_highlight(gobj, prev_ids, next_ids)
|
|
|
4851
5051
|
if(!updates.length) {
|
|
4852
5052
|
return;
|
|
4853
5053
|
}
|
|
5054
|
+
|
|
4854
5055
|
try {
|
|
4855
5056
|
graph.updateNodeData(updates);
|
|
4856
5057
|
graph.draw();
|
|
4857
5058
|
} catch(e) {
|
|
4858
|
-
log_error(`${gobj_short_name(gobj)}: cannot repaint the
|
|
5059
|
+
log_error(`${gobj_short_name(gobj)}: cannot repaint the cards: ${e}`);
|
|
5060
|
+
}
|
|
5061
|
+
}
|
|
5062
|
+
|
|
5063
|
+
/************************************************************
|
|
5064
|
+
* Repaint the cards whose highlight state CHANGES: the ones that had it
|
|
5065
|
+
* and lose it, plus the ones that gain it. Only those — a treedb graph
|
|
5066
|
+
* is redrawn per keystroke of the find box otherwise.
|
|
5067
|
+
************************************************************/
|
|
5068
|
+
function apply_node_highlight(gobj, prev_ids, next_ids)
|
|
5069
|
+
{
|
|
5070
|
+
let priv = gobj.priv;
|
|
5071
|
+
let graph = priv.graph;
|
|
5072
|
+
if(!graph) {
|
|
5073
|
+
return;
|
|
5074
|
+
}
|
|
5075
|
+
|
|
5076
|
+
let next = new Set(next_ids || []);
|
|
5077
|
+
let touched = new Set([...(prev_ids || []), ...next]);
|
|
5078
|
+
if(touched.size === 0) {
|
|
5079
|
+
return;
|
|
4859
5080
|
}
|
|
5081
|
+
|
|
5082
|
+
repaint_cards(gobj, touched);
|
|
4860
5083
|
}
|
|
4861
5084
|
|
|
4862
5085
|
function refresh_html_nodes_theme(gobj, theme)
|
|
@@ -4871,10 +5094,13 @@ function refresh_html_nodes_theme(gobj, theme)
|
|
|
4871
5094
|
* rebuilt every card without it would silently CLEAR the focus or the
|
|
4872
5095
|
* find that is on screen. Carry it across. */
|
|
4873
5096
|
let highlighted = new Set(priv._focus_ids || []);
|
|
5097
|
+
let selected = new Set(priv._selected_paint_ids || []);
|
|
4874
5098
|
let updates = [];
|
|
4875
5099
|
for(let i = 0; i < nodes.length; i++) {
|
|
4876
5100
|
let id = nodes[i].id;
|
|
4877
|
-
let html = node_innerHTML_of(
|
|
5101
|
+
let html = node_innerHTML_of(
|
|
5102
|
+
graph.getNodeData(id), theme, highlighted.has(id), selected.has(id)
|
|
5103
|
+
);
|
|
4878
5104
|
if(html !== null) {
|
|
4879
5105
|
updates.push({id: id, style: {innerHTML: html}});
|
|
4880
5106
|
}
|
|
@@ -4913,13 +5139,42 @@ function refresh_default_edges_theme(gobj, theme)
|
|
|
4913
5139
|
}
|
|
4914
5140
|
}
|
|
4915
5141
|
|
|
5142
|
+
/************************************************************
|
|
5143
|
+
* The rings a card can wear, composed into one `box-shadow`.
|
|
5144
|
+
*
|
|
5145
|
+
* A node can be a find match and be selected at the same time, so
|
|
5146
|
+
* neither ring may be written by overwriting the other: the amber
|
|
5147
|
+
* halo hugs the card and the blue selection ring is drawn outside
|
|
5148
|
+
* it, which is also the order that reads correctly when only one
|
|
5149
|
+
* of them is on.
|
|
5150
|
+
************************************************************/
|
|
5151
|
+
function ring_shadow(highlight, selected, base_shadow)
|
|
5152
|
+
{
|
|
5153
|
+
let rings = [];
|
|
5154
|
+
|
|
5155
|
+
if(highlight) {
|
|
5156
|
+
rings.push(`0 0 0 4px ${HIGHLIGHT_HALO}`);
|
|
5157
|
+
}
|
|
5158
|
+
if(selected) {
|
|
5159
|
+
rings.push(`0 0 0 ${highlight? "7px" : "3px"} ${SELECT_RING}`);
|
|
5160
|
+
}
|
|
5161
|
+
if(base_shadow) {
|
|
5162
|
+
rings.push(base_shadow);
|
|
5163
|
+
}
|
|
5164
|
+
if(!rings.length) {
|
|
5165
|
+
return "";
|
|
5166
|
+
}
|
|
5167
|
+
|
|
5168
|
+
return `box-shadow: ${rings.join(", ")};`;
|
|
5169
|
+
}
|
|
5170
|
+
|
|
4916
5171
|
/************************************************************
|
|
4917
5172
|
* Build innerHTML for pure-child (leaf) nodes: a compact
|
|
4918
5173
|
* chip-card. Same colour/typography family as the entity card
|
|
4919
5174
|
* but lighter (1px border, no shadow, single line). The name is
|
|
4920
5175
|
* always legible (ellipsis + native title tooltip on overflow).
|
|
4921
5176
|
************************************************************/
|
|
4922
|
-
function build_chip_innerHTML(color, theme, icon, label, key, highlight)
|
|
5177
|
+
function build_chip_innerHTML(color, theme, icon, label, key, highlight, selected)
|
|
4923
5178
|
{
|
|
4924
5179
|
let title = key || label;
|
|
4925
5180
|
let dark = (theme === "dark");
|
|
@@ -4950,7 +5205,7 @@ function build_chip_innerHTML(color, theme, icon, label, key, highlight)
|
|
|
4950
5205
|
height: 100%;
|
|
4951
5206
|
background: ${bg};
|
|
4952
5207
|
border: ${highlight? "3px" : "1px"} solid ${border};
|
|
4953
|
-
${highlight
|
|
5208
|
+
${ring_shadow(highlight, selected, "")}
|
|
4954
5209
|
border-radius: 8px;
|
|
4955
5210
|
color: ${text_color};
|
|
4956
5211
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
|
@@ -4977,7 +5232,7 @@ function build_chip_innerHTML(color, theme, icon, label, key, highlight)
|
|
|
4977
5232
|
* colour is kept (per-topic differentiation) but softened via
|
|
4978
5233
|
* color-mix instead of a harsh saturated fill. Theme-aware.
|
|
4979
5234
|
************************************************************/
|
|
4980
|
-
function build_node_innerHTML(color, theme, icon, label, topic_name, structural, key, highlight)
|
|
5235
|
+
function build_node_innerHTML(color, theme, icon, label, topic_name, structural, key, highlight, selected)
|
|
4981
5236
|
{
|
|
4982
5237
|
let title = key || label;
|
|
4983
5238
|
let dark = (theme === "dark");
|
|
@@ -5040,7 +5295,7 @@ function build_node_innerHTML(color, theme, icon, label, topic_name, structural,
|
|
|
5040
5295
|
background: ${bg};
|
|
5041
5296
|
border: ${highlight? "3px" : "1.5px"} ${border_style} ${border};
|
|
5042
5297
|
border-radius: 10px;
|
|
5043
|
-
|
|
5298
|
+
${ring_shadow(highlight, selected, shadow)}
|
|
5044
5299
|
color: ${title_color};
|
|
5045
5300
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
|
5046
5301
|
display: flex;
|
|
@@ -5101,31 +5356,151 @@ function execute_delete_node(gobj, nodeData)
|
|
|
5101
5356
|
}
|
|
5102
5357
|
|
|
5103
5358
|
/************************************************************
|
|
5104
|
-
*
|
|
5359
|
+
* Every node in the graph.
|
|
5105
5360
|
************************************************************/
|
|
5106
|
-
function
|
|
5361
|
+
function select_all_nodes(gobj)
|
|
5362
|
+
{
|
|
5363
|
+
let graph = gobj.priv.graph;
|
|
5364
|
+
|
|
5365
|
+
if(!graph) {
|
|
5366
|
+
return;
|
|
5367
|
+
}
|
|
5368
|
+
|
|
5369
|
+
let nodes = (graph.getData() || {}).nodes || [];
|
|
5370
|
+
set_selection(gobj, nodes.map((nd) => nd.id));
|
|
5371
|
+
}
|
|
5372
|
+
|
|
5373
|
+
/************************************************************
|
|
5374
|
+
* The nodes currently selected, as their node data.
|
|
5375
|
+
************************************************************/
|
|
5376
|
+
function selected_nodes_data(gobj)
|
|
5107
5377
|
{
|
|
5108
5378
|
let priv = gobj.priv;
|
|
5109
|
-
let
|
|
5110
|
-
|
|
5379
|
+
let graph = priv.graph;
|
|
5380
|
+
|
|
5381
|
+
let ids = new Set(selected_node_ids(gobj));
|
|
5382
|
+
for(let id of (priv._selected_paint_ids || [])) {
|
|
5383
|
+
ids.add(id);
|
|
5384
|
+
}
|
|
5385
|
+
if(priv._selected_node_id) {
|
|
5386
|
+
ids.add(priv._selected_node_id);
|
|
5387
|
+
}
|
|
5388
|
+
|
|
5389
|
+
let out = [];
|
|
5390
|
+
for(let id of ids) {
|
|
5391
|
+
let nd;
|
|
5392
|
+
try {
|
|
5393
|
+
nd = graph.getNodeData(id);
|
|
5394
|
+
} catch(e) {
|
|
5395
|
+
continue; /* gone since it was selected */
|
|
5396
|
+
}
|
|
5397
|
+
if(nd && nd.data && nd.data.desc) {
|
|
5398
|
+
out.push(nd);
|
|
5399
|
+
}
|
|
5400
|
+
}
|
|
5401
|
+
|
|
5402
|
+
return out;
|
|
5403
|
+
}
|
|
5404
|
+
|
|
5405
|
+
/************************************************************
|
|
5406
|
+
* What a delete takes with it, for ONE node or for a set.
|
|
5407
|
+
*
|
|
5408
|
+
* Same sentence in both cases and the same keys, because it is the
|
|
5409
|
+
* same warning: these views delete with `force`, which UNLINKS the
|
|
5410
|
+
* children (they survive, loose) and cleans the node off its
|
|
5411
|
+
* parents. Over a set the two numbers are the sums -- an operator
|
|
5412
|
+
* about to detach eleven records has to read eleven, not "are you
|
|
5413
|
+
* sure".
|
|
5414
|
+
************************************************************/
|
|
5415
|
+
function delete_question(nodes)
|
|
5416
|
+
{
|
|
5417
|
+
let children = 0;
|
|
5418
|
+
let parents = 0;
|
|
5419
|
+
|
|
5420
|
+
for(let nd of nodes) {
|
|
5421
|
+
let impact = delete_impact(nd.data.desc, nd.data.record || {});
|
|
5422
|
+
children += impact.children;
|
|
5423
|
+
parents += impact.parents;
|
|
5424
|
+
}
|
|
5425
|
+
|
|
5426
|
+
let question;
|
|
5427
|
+
if(nodes.length === 1) {
|
|
5428
|
+
let nd = nodes[0];
|
|
5429
|
+
question = t('delete') + ' ' + nd.data.desc.topic_name + ': ' +
|
|
5430
|
+
(nd.data.record || {}).id + '?';
|
|
5431
|
+
} else {
|
|
5432
|
+
question = t('delete') + ' ' + nodes.length + ' ' + t('records') + '?';
|
|
5433
|
+
}
|
|
5111
5434
|
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
* operator detach eleven records believing they removed one. */
|
|
5116
|
-
let impact = delete_impact(nodeData.data.desc, record);
|
|
5117
|
-
let question = t('delete') + ' ' + topic_name + ': ' + record.id + '?';
|
|
5118
|
-
if(impact.children > 0) {
|
|
5119
|
-
question += '\n' + impact.children + ' ' +
|
|
5120
|
-
t('children will be unlinked, not deleted', {count: impact.children});
|
|
5435
|
+
if(children > 0) {
|
|
5436
|
+
question += '\n' + children + ' ' +
|
|
5437
|
+
t('children will be unlinked, not deleted', {count: children});
|
|
5121
5438
|
}
|
|
5122
|
-
if(
|
|
5439
|
+
if(parents > 0) {
|
|
5123
5440
|
question += '\n' + t('it will be detached from') + ' ' +
|
|
5124
|
-
|
|
5441
|
+
parents + ' ' + t('parents', {count: parents});
|
|
5442
|
+
}
|
|
5443
|
+
|
|
5444
|
+
return question;
|
|
5445
|
+
}
|
|
5446
|
+
|
|
5447
|
+
/************************************************************
|
|
5448
|
+
* Delete every selected node (the Delete key).
|
|
5449
|
+
************************************************************/
|
|
5450
|
+
function request_delete_selection(gobj)
|
|
5451
|
+
{
|
|
5452
|
+
let nodes = selected_nodes_data(gobj);
|
|
5453
|
+
|
|
5454
|
+
if(!nodes.length) {
|
|
5455
|
+
return; /* nothing selected: the key means nothing here */
|
|
5125
5456
|
}
|
|
5126
5457
|
|
|
5458
|
+
if(!gobj_read_bool_attr(gobj, "confirm_delete_node")) {
|
|
5459
|
+
execute_delete_selection(gobj, nodes);
|
|
5460
|
+
return;
|
|
5461
|
+
}
|
|
5462
|
+
|
|
5463
|
+
/* No anchor: the set has no icon of its own, so the question is
|
|
5464
|
+
* asked in the middle of the graph. */
|
|
5465
|
+
show_confirm_popover(gobj, null,
|
|
5466
|
+
delete_question(nodes),
|
|
5467
|
+
'delete', '#ff4d4f',
|
|
5468
|
+
() => execute_delete_selection(gobj, nodes),
|
|
5469
|
+
'_delete_confirm_el'
|
|
5470
|
+
);
|
|
5471
|
+
}
|
|
5472
|
+
|
|
5473
|
+
function execute_delete_selection(gobj, nodes)
|
|
5474
|
+
{
|
|
5475
|
+
let priv = gobj.priv;
|
|
5476
|
+
|
|
5477
|
+
hide_delete_confirm(gobj);
|
|
5478
|
+
|
|
5479
|
+
/* One event per node: a treedb deletes records, it has no bulk
|
|
5480
|
+
* delete, and the host turns each of these into its own command
|
|
5481
|
+
* exactly as the topic table's bulk delete does. */
|
|
5482
|
+
for(let nd of nodes) {
|
|
5483
|
+
gobj_publish_event(gobj, "EV_DELETE_NODE", {
|
|
5484
|
+
treedb_name: priv.treedb_name,
|
|
5485
|
+
topic_name: nd.data.desc.topic_name,
|
|
5486
|
+
record: nd.data.record,
|
|
5487
|
+
});
|
|
5488
|
+
}
|
|
5489
|
+
|
|
5490
|
+
deselect_node(gobj);
|
|
5491
|
+
}
|
|
5492
|
+
|
|
5493
|
+
/************************************************************
|
|
5494
|
+
* Show a confirmation popover next to the delete icon.
|
|
5495
|
+
************************************************************/
|
|
5496
|
+
function show_delete_confirm(gobj, nodeData)
|
|
5497
|
+
{
|
|
5498
|
+
let priv = gobj.priv;
|
|
5499
|
+
|
|
5500
|
+
/* Same sentence as the Delete key's, from the same place: the
|
|
5501
|
+
* warning does not depend on how the delete was asked for. */
|
|
5127
5502
|
show_confirm_popover(gobj, priv._node_delete_el,
|
|
5128
|
-
|
|
5503
|
+
delete_question([nodeData]),
|
|
5129
5504
|
'delete', '#ff4d4f',
|
|
5130
5505
|
() => execute_delete_node(gobj, nodeData),
|
|
5131
5506
|
'_delete_confirm_el'
|
|
@@ -6477,6 +6852,67 @@ function ac_node_drag_end(gobj, event, kw, src)
|
|
|
6477
6852
|
return 0;
|
|
6478
6853
|
}
|
|
6479
6854
|
|
|
6855
|
+
/************************************************************
|
|
6856
|
+
* A key, while the graph has focus.
|
|
6857
|
+
*
|
|
6858
|
+
* Full screen keeps the two keys it always had. The rest belong to
|
|
6859
|
+
* the selection, so they only mean anything in edition -- outside
|
|
6860
|
+
* it there is nothing selected to clear, select or delete.
|
|
6861
|
+
************************************************************/
|
|
6862
|
+
function ac_key_down(gobj, event, kw, src)
|
|
6863
|
+
{
|
|
6864
|
+
let priv = gobj.priv;
|
|
6865
|
+
let key = (kw && kw.key) || "";
|
|
6866
|
+
|
|
6867
|
+
if(gobj_read_bool_attr(gobj, "with_fullscreen")) {
|
|
6868
|
+
let fullscreen = graph_get_plugin(gobj, "fullscreen");
|
|
6869
|
+
if(fullscreen) {
|
|
6870
|
+
if(key === "F" || key === "f") {
|
|
6871
|
+
fullscreen.request();
|
|
6872
|
+
} else if(key === "Escape") {
|
|
6873
|
+
/* Escape also clears the selection below, and both are
|
|
6874
|
+
* right: the browser exits full screen on Escape
|
|
6875
|
+
* whatever we do, so refusing to clear as well would
|
|
6876
|
+
* only make the key do less than it appears to. */
|
|
6877
|
+
fullscreen.exit();
|
|
6878
|
+
}
|
|
6879
|
+
}
|
|
6880
|
+
}
|
|
6881
|
+
|
|
6882
|
+
if(!priv.edit_mode) {
|
|
6883
|
+
return 0;
|
|
6884
|
+
}
|
|
6885
|
+
|
|
6886
|
+
if(key === "Escape") {
|
|
6887
|
+
deselect_node(gobj);
|
|
6888
|
+
return 0;
|
|
6889
|
+
}
|
|
6890
|
+
if(kw.ctrl && (key === "a" || key === "A")) {
|
|
6891
|
+
select_all_nodes(gobj);
|
|
6892
|
+
return 0;
|
|
6893
|
+
}
|
|
6894
|
+
if(key === "Delete" || key === "Backspace") {
|
|
6895
|
+
request_delete_selection(gobj);
|
|
6896
|
+
return 0;
|
|
6897
|
+
}
|
|
6898
|
+
|
|
6899
|
+
return 0;
|
|
6900
|
+
}
|
|
6901
|
+
|
|
6902
|
+
/************************************************************
|
|
6903
|
+
* The rubber band let go: these are the nodes it enclosed.
|
|
6904
|
+
*
|
|
6905
|
+
* They arrive in the kw because `onSelect` runs BEFORE G6 writes
|
|
6906
|
+
* the state, so asking the graph here would answer the previous
|
|
6907
|
+
* selection.
|
|
6908
|
+
************************************************************/
|
|
6909
|
+
function ac_brush_select(gobj, event, kw, src)
|
|
6910
|
+
{
|
|
6911
|
+
set_selection(gobj, (kw && kw.ids) || []);
|
|
6912
|
+
|
|
6913
|
+
return 0;
|
|
6914
|
+
}
|
|
6915
|
+
|
|
6480
6916
|
/************************************************************
|
|
6481
6917
|
* Node click - publish vertex clicked event
|
|
6482
6918
|
************************************************************/
|
|
@@ -6495,7 +6931,13 @@ function ac_node_click(gobj, event, kw, src)
|
|
|
6495
6931
|
record: nodedata.data.record
|
|
6496
6932
|
});
|
|
6497
6933
|
|
|
6498
|
-
if(priv.edit_mode) {
|
|
6934
|
+
if(priv.edit_mode && kw.evt.shiftKey) {
|
|
6935
|
+
/* Shift+click extends the selection, the way it does
|
|
6936
|
+
* everywhere. It never looks for a port: a port is a
|
|
6937
|
+
* one-node affordance, and this gesture is about the
|
|
6938
|
+
* set. */
|
|
6939
|
+
toggle_in_selection(gobj, node_id);
|
|
6940
|
+
} else if(priv.edit_mode) {
|
|
6499
6941
|
// Check if click hits a port
|
|
6500
6942
|
// Convert client coords to viewport (container-relative) then to canvas
|
|
6501
6943
|
let containerRect = priv.$container.getBoundingClientRect();
|
|
@@ -6782,6 +7224,8 @@ function create_gclass(gclass_name)
|
|
|
6782
7224
|
["EV_NODE_CONTEXT_MENU", ac_node_context_menu, null],
|
|
6783
7225
|
["EV_CANVAS_CLICK", ac_canvas_click, null],
|
|
6784
7226
|
["EV_NODE_DRAG_END", ac_node_drag_end, null],
|
|
7227
|
+
["EV_BRUSH_SELECT", ac_brush_select, null],
|
|
7228
|
+
["EV_KEY_DOWN", ac_key_down, null],
|
|
6785
7229
|
|
|
6786
7230
|
/*--- Toolbar events ---*/
|
|
6787
7231
|
["EV_ZOOM_IN", ac_zoom_in, null],
|
|
@@ -6826,6 +7270,8 @@ function create_gclass(gclass_name)
|
|
|
6826
7270
|
["EV_NODE_CONTEXT_MENU", 0],
|
|
6827
7271
|
["EV_CANVAS_CLICK", 0],
|
|
6828
7272
|
["EV_NODE_DRAG_END", 0],
|
|
7273
|
+
["EV_BRUSH_SELECT", 0],
|
|
7274
|
+
["EV_KEY_DOWN", 0],
|
|
6829
7275
|
|
|
6830
7276
|
/*--- Toolbar (internal) ---*/
|
|
6831
7277
|
["EV_ZOOM_IN", 0],
|