@yuneta/gobj-ui 7.26.27 → 7.26.28
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 +458 -22
- package/dist/gobj-ui.cjs.js +1063 -216
- package/dist/gobj-ui.es.js +1064 -217
- package/package.json +1 -1
- package/scripts/validate-locales.mjs +2269 -316
- package/src/c_g6_nodes_tree.js +1086 -253
- package/src/c_yui_form.js +11 -1
- package/src/c_yui_fsm_graph.js +5 -0
- package/src/c_yui_gclass.js +11 -0
- package/src/c_yui_gobj_tree_js.js +17 -0
- package/src/c_yui_json_graph.js +5 -0
- package/src/c_yui_nav.js +9 -6
- package/src/c_yui_pager.js +1 -1
- package/src/c_yui_period.js +11 -4
- package/src/c_yui_schema_editor.js +19 -7
- package/src/c_yui_shell.js +7 -7
- package/src/c_yui_treedb_graph.js +15 -6
- package/src/c_yui_treedb_schema.js +5 -0
- package/src/c_yui_treedb_topic_with_form.js +10 -10
- package/src/c_yui_treedb_topics.js +6 -6
- package/src/c_yui_window.js +2 -2
- package/src/c_yui_wizard.js +8 -1
- package/src/form_json_field.js +7 -0
- package/src/g6_canvas_tab_order.js +84 -0
- package/src/g6_canvas_tab_order.wiring.test.js +232 -0
- package/src/g6_nodes_tree_controls.test.js +547 -0
- package/src/g6_nodes_tree_focus.wiring.test.js +1360 -0
- package/src/g6_nodes_tree_i18n_keys.test.js +241 -5
- package/src/i18n_key_functions.js +146 -0
- package/src/i18n_key_functions.test.js +1204 -0
- package/src/lib_graph.js +38 -1
- package/src/nav_backbar_name.wiring.test.js +82 -0
- package/src/schema_flags.js +47 -0
- package/src/schema_import.js +7 -0
- package/src/shell_modals.js +20 -16
- package/src/shell_route_map.js +12 -2
- package/src/wizard_helpers.js +8 -0
- package/src/yui_dev.js +16 -3
- package/src/yui_dev_controls.test.js +174 -0
- package/src/yui_gclass_view.js +2 -2
- package/src/yui_graph_camera.js +1 -1
- package/src/yui_install.js +10 -8
- package/src/yui_table_select.js +2 -2
- package/src/yui_time.js +18 -3
package/src/c_g6_nodes_tree.js
CHANGED
|
@@ -178,11 +178,29 @@ import {yui_shell_of} from "./c_yui_shell.js";
|
|
|
178
178
|
import {yui_shell_show_error} from "./shell_modals.js";
|
|
179
179
|
import {g6_event_kw} from "./g6_event_kw.js";
|
|
180
180
|
import {css_color_or_null} from "./yui_data_color.js";
|
|
181
|
+
import {keep_canvas_tab_order} from "./g6_canvas_tab_order.js";
|
|
181
182
|
|
|
182
183
|
/***************************************************************
|
|
183
184
|
* The markup of one toolbar item. Everything that goes into it is
|
|
184
185
|
* escaped, the read-out branch too: its text, title and value are
|
|
185
186
|
* values (a zoom figure, a translated title), never markup.
|
|
187
|
+
*
|
|
188
|
+
* An item is a BUTTON for the keyboard too: `role="button"` and
|
|
189
|
+
* `tabindex="0"`, and YuiToolbar presses it on Enter or Space.
|
|
190
|
+
* `title_key` is the i18n key of `title`: written as
|
|
191
|
+
* `data-i18n-title` / `data-i18n-aria-label`, so the name carries
|
|
192
|
+
* its key like every other control's.
|
|
193
|
+
*
|
|
194
|
+
* A `disabled` item is born the way disableElements() (lib_graph.js)
|
|
195
|
+
* leaves one: `aria-disabled`, out of the Tab order, and the
|
|
196
|
+
* tabindex it takes once enabled kept in `data-enabled-tabindex`,
|
|
197
|
+
* where enableElements() finds it. A toggle (`pressed` given, true
|
|
198
|
+
* or false) says its state in `aria-pressed`, as its look says it
|
|
199
|
+
* with `pressed_state`. `data-control` names the CONTROL the item
|
|
200
|
+
* is, whatever state it shows: `control` when the item gives one
|
|
201
|
+
* (full screen is `request-fullscreen` or `exit-fullscreen`, and
|
|
202
|
+
* one control), its `value` otherwise -- it is what a rebuild puts
|
|
203
|
+
* the focus back on.
|
|
186
204
|
***************************************************************/
|
|
187
205
|
function toolbar_item_html(item)
|
|
188
206
|
{
|
|
@@ -192,17 +210,28 @@ function toolbar_item_html(item)
|
|
|
192
210
|
|
|
193
211
|
const extra_class = item.className ? ` ${escapeHtml(String(item.className))}` : '';
|
|
194
212
|
const title = escapeHtml(String(item.title ?? ''));
|
|
213
|
+
let i18n_attrs = '';
|
|
214
|
+
if(item.title_key) {
|
|
215
|
+
const key = escapeHtml(String(item.title_key));
|
|
216
|
+
i18n_attrs = ` data-i18n-title="${key}" data-i18n-aria-label="${key}"`;
|
|
217
|
+
}
|
|
195
218
|
|
|
196
219
|
if(item.readout) {
|
|
197
220
|
return (
|
|
198
221
|
`<div class="g6-toolbar-readout${extra_class}"` +
|
|
199
|
-
` title="${title}" aria-label="${title}">` +
|
|
222
|
+
` title="${title}" aria-label="${title}"${i18n_attrs}>` +
|
|
200
223
|
`${escapeHtml(String(item.text ?? ''))}` +
|
|
201
224
|
`</div>`
|
|
202
225
|
);
|
|
203
226
|
}
|
|
204
227
|
|
|
205
|
-
const
|
|
228
|
+
const state = item.disabled
|
|
229
|
+
? ' tabindex="-1" aria-disabled="true" data-enabled-tabindex="0" disabled'
|
|
230
|
+
: ' tabindex="0"';
|
|
231
|
+
const pressed = (item.pressed === undefined)
|
|
232
|
+
? ''
|
|
233
|
+
: ` aria-pressed="${item.pressed ? 'true' : 'false'}"`;
|
|
234
|
+
const control = escapeHtml(String(item.control ?? item.value));
|
|
206
235
|
const is_text = (item.text !== undefined);
|
|
207
236
|
const kind_class = is_text ? ' g6-toolbar-item-text' : '';
|
|
208
237
|
const body = is_text
|
|
@@ -213,8 +242,9 @@ function toolbar_item_html(item)
|
|
|
213
242
|
|
|
214
243
|
return (
|
|
215
244
|
`<div class="g6-toolbar-item${kind_class}${extra_class}"` +
|
|
216
|
-
`
|
|
217
|
-
`
|
|
245
|
+
` role="button"${state}${pressed}` +
|
|
246
|
+
` value="${escapeHtml(String(item.value))}" data-control="${control}"` +
|
|
247
|
+
` title="${title}" aria-label="${title}"${i18n_attrs}>` +
|
|
218
248
|
`${body}` +
|
|
219
249
|
`</div>`
|
|
220
250
|
);
|
|
@@ -233,6 +263,11 @@ function toolbar_item_html(item)
|
|
|
233
263
|
* readout — a text item that REPORTS instead of acting (the
|
|
234
264
|
* zoom level); it is not a button
|
|
235
265
|
* separator — a group divider, not an item
|
|
266
|
+
* title_key — the i18n key of `title` (see toolbar_item_html)
|
|
267
|
+
* pressed — boolean; the item is a TOGGLE, and this is its
|
|
268
|
+
* state (`aria-pressed`)
|
|
269
|
+
* control — the identity of the control when its `value`
|
|
270
|
+
* changes with its state (full screen enter/exit)
|
|
236
271
|
*
|
|
237
272
|
* The base class fires onClick only for elements whose class list
|
|
238
273
|
* contains `g6-toolbar-item`, so a separator and a readout carry
|
|
@@ -243,15 +278,168 @@ function toolbar_item_html(item)
|
|
|
243
278
|
* functions (set_submit_state, disableElements, …) work on toolbar
|
|
244
279
|
* icons exactly like on regular Bulma/FA buttons — no special
|
|
245
280
|
* icon-specific helpers needed.
|
|
281
|
+
*
|
|
282
|
+
* The items are buttons for the KEYBOARD as well:
|
|
283
|
+
* - Enter or Space CLICKS the focused item, and the click bubbles
|
|
284
|
+
* to the base class's own handler, so the key reaches the same
|
|
285
|
+
* onClick a pointer does. A `disabled` item takes the key and
|
|
286
|
+
* does nothing, as its `pointer-events: none` does for a press.
|
|
287
|
+
* - A POINTER press does not move the focus to an item (it stays
|
|
288
|
+
* on the canvas, where the graph's keys are heard), the way the
|
|
289
|
+
* floating buttons keep it (see keep_focus_on_press).
|
|
290
|
+
* - A disabled item is out of the Tab order and says it is
|
|
291
|
+
* disabled (`aria-disabled`), from its first render
|
|
292
|
+
* (toolbar_item_html) and on every change (disableElements /
|
|
293
|
+
* enableElements in lib_graph.js).
|
|
294
|
+
* - A rebuild (update) replaces the innerHTML, the focused item
|
|
295
|
+
* with it: the focus goes back to the item of the same CONTROL
|
|
296
|
+
* (`data-control`: Enter on "enter full screen" lands on "exit
|
|
297
|
+
* full screen"), or to the canvas when there is none. And a
|
|
298
|
+
* toolbar that goes away (destroy) hands it to the canvas
|
|
299
|
+
* first. A removed element that held the focus leaves it on
|
|
300
|
+
* <body> in Firefox, which fires no focusout for it, and the
|
|
301
|
+
* graph goes deaf.
|
|
246
302
|
***************************************************************/
|
|
247
303
|
class YuiToolbar extends Toolbar
|
|
248
304
|
{
|
|
305
|
+
constructor(context, options)
|
|
306
|
+
{
|
|
307
|
+
super(context, options);
|
|
308
|
+
this.$element.addEventListener('pointerdown', keep_focus_on_toolbar_press);
|
|
309
|
+
this.$element.addEventListener('keydown', press_toolbar_item_on_key);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async update(options)
|
|
313
|
+
{
|
|
314
|
+
const control = focused_toolbar_item_control(this.$element);
|
|
315
|
+
await super.update(options);
|
|
316
|
+
if(control !== null) {
|
|
317
|
+
refocus_toolbar_item(this.$element, control);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
destroy()
|
|
322
|
+
{
|
|
323
|
+
if(!this.destroyed && this.$element) {
|
|
324
|
+
this.$element.removeEventListener('pointerdown', keep_focus_on_toolbar_press);
|
|
325
|
+
this.$element.removeEventListener('keydown', press_toolbar_item_on_key);
|
|
326
|
+
if(this.$element.contains(document.activeElement)) {
|
|
327
|
+
focus_main_canvas(this.$element.parentNode);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
super.destroy();
|
|
331
|
+
}
|
|
332
|
+
|
|
249
333
|
async getDOMContent()
|
|
250
334
|
{
|
|
251
335
|
const items = await this.options.getItems();
|
|
252
336
|
return items.map(toolbar_item_html).join('');
|
|
253
337
|
}
|
|
254
338
|
}
|
|
339
|
+
|
|
340
|
+
/***************************************************************
|
|
341
|
+
* A POINTER press on a button of this view does not take the
|
|
342
|
+
* focus. The container's capture listener has just put it on the
|
|
343
|
+
* canvas, where the graph's keys are heard; a press that moved it
|
|
344
|
+
* on to the button would leave it on something this view removes
|
|
345
|
+
* -- Escape deselects and takes the floating buttons away, the ✕
|
|
346
|
+
* closes its popover, a rebuild replaces the toolbar -- and
|
|
347
|
+
* Firefox fires no focusout for a removed element: the focus falls
|
|
348
|
+
* to <body> and the graph hears nothing until it is clicked again.
|
|
349
|
+
* Cancelling the pointerdown cancels the mousedown that moves the
|
|
350
|
+
* focus; the click still comes, and Tab still reaches the button.
|
|
351
|
+
***************************************************************/
|
|
352
|
+
function keep_focus_on_press(el)
|
|
353
|
+
{
|
|
354
|
+
el.addEventListener('pointerdown', (e) => {
|
|
355
|
+
e.preventDefault();
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/* The same, for the items of a toolbar, which is rebuilt whole:
|
|
360
|
+
* delegated from the toolbar element. */
|
|
361
|
+
function keep_focus_on_toolbar_press(e)
|
|
362
|
+
{
|
|
363
|
+
let target = e.target;
|
|
364
|
+
if(target && typeof target.closest === 'function' &&
|
|
365
|
+
target.closest('.g6-toolbar-item')) {
|
|
366
|
+
e.preventDefault();
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/***************************************************************
|
|
371
|
+
* The keyboard to the graph: the focus on its main canvas.
|
|
372
|
+
***************************************************************/
|
|
373
|
+
function focus_main_canvas($container)
|
|
374
|
+
{
|
|
375
|
+
if(!$container) {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
let $canvas = main_canvas_of($container);
|
|
379
|
+
if($canvas && $canvas.isConnected && typeof $canvas.focus === 'function') {
|
|
380
|
+
$canvas.focus({preventScroll: true});
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/***************************************************************
|
|
385
|
+
* Enter or Space on a toolbar item clicks it; the click bubbles
|
|
386
|
+
* to G6's toolbar handler, which reads the item's `value`.
|
|
387
|
+
***************************************************************/
|
|
388
|
+
function press_toolbar_item_on_key(e)
|
|
389
|
+
{
|
|
390
|
+
if(e.key !== 'Enter' && e.key !== ' ') {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
let target = e.target;
|
|
394
|
+
let item = (target && typeof target.closest === 'function')?
|
|
395
|
+
target.closest('.g6-toolbar-item') : null;
|
|
396
|
+
if(!item) {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
/* The key pressed a button: it is not the graph's (the
|
|
400
|
+
* container forwards every keydown to G6), and Space would
|
|
401
|
+
* scroll the page. */
|
|
402
|
+
e.preventDefault();
|
|
403
|
+
e.stopPropagation();
|
|
404
|
+
if(item.hasAttribute('disabled')) {
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
item.click();
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/***************************************************************
|
|
411
|
+
* The control (`data-control`) of the toolbar item that has the
|
|
412
|
+
* focus, or null.
|
|
413
|
+
***************************************************************/
|
|
414
|
+
function focused_toolbar_item_control($toolbar)
|
|
415
|
+
{
|
|
416
|
+
let active = document.activeElement;
|
|
417
|
+
if(!$toolbar || !active || active === $toolbar || !$toolbar.contains(active)) {
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
if(!active.classList || !active.classList.contains('g6-toolbar-item')) {
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
return active.getAttribute('data-control');
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/***************************************************************
|
|
427
|
+
* After a rebuild: the focus on the item of that control, in
|
|
428
|
+
* whatever state the rebuild shows it, or on the canvas when the
|
|
429
|
+
* rebuild has none (a fold took it away).
|
|
430
|
+
***************************************************************/
|
|
431
|
+
function refocus_toolbar_item($toolbar, control)
|
|
432
|
+
{
|
|
433
|
+
let items = $toolbar.querySelectorAll('.g6-toolbar-item');
|
|
434
|
+
for(let i = 0; i < items.length; i++) {
|
|
435
|
+
if(items[i].getAttribute('data-control') === control) {
|
|
436
|
+
items[i].focus({preventScroll: true});
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
focus_main_canvas($toolbar.parentNode);
|
|
441
|
+
}
|
|
442
|
+
|
|
255
443
|
register(ExtensionCategory.PLUGIN, 'yui-toolbar', YuiToolbar);
|
|
256
444
|
ensure_drag_canvas_patch();
|
|
257
445
|
ensure_brush_select_owned();
|
|
@@ -343,15 +531,19 @@ const NODE_SHAPES = ['square', 'circle', 'diamond', 'triangle', 'hexagon', 'star
|
|
|
343
531
|
|
|
344
532
|
/* The keys of this view that reach t() as DATA or as an ARGUMENT -- the
|
|
345
533
|
* shape tables, the labels of the property popovers (create_form_label),
|
|
346
|
-
* the confirm button (show_confirm_popover)
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
*
|
|
350
|
-
* the
|
|
534
|
+
* the confirm button (show_confirm_popover), the names of the floating
|
|
535
|
+
* buttons (create_floating_icon, show_dual_icons) and the shell's error
|
|
536
|
+
* messages (yui_shell_show_error): read by a consumer's validate-locales,
|
|
537
|
+
* which asks the locales for every key of an exported `*_I18N_KEYS` array
|
|
538
|
+
* of a module the app mounts. A shape added to NODE_SHAPES or PORT_SHAPES,
|
|
539
|
+
* or a new key handed to one of those helpers, goes here too
|
|
540
|
+
* (g6_nodes_tree_i18n_keys.test.js checks every call). */
|
|
351
541
|
export const G6_NODES_TREE_I18N_KEYS = [
|
|
352
542
|
'square', 'circle', 'diamond', 'triangle', 'hexagon', 'star',
|
|
353
543
|
'shape', 'radius', 'apply to', 'line width', 'color', 'fill color', 'stroke color',
|
|
354
|
-
'delete'
|
|
544
|
+
'delete', 'unlink',
|
|
545
|
+
'node properties', 'delete node', 'edge properties', 'link to hook', 'port properties',
|
|
546
|
+
'some records were gone before the delete'
|
|
355
547
|
];
|
|
356
548
|
const DEFAULT_NODE_SHAPE = 'circle';
|
|
357
549
|
|
|
@@ -519,6 +711,7 @@ let PRIVATE_DATA = {
|
|
|
519
711
|
_saved_graph_properties: {}, // the same, as the BACKEND has it: what a Save compares against
|
|
520
712
|
_graphs_writes_owed: {}, // topic_name → true: a __graphs__ write the host said did not land
|
|
521
713
|
_graphs_load: 0, // the LOAD a __graphs__ write belongs to: EV_CLEAR_DATA starts another
|
|
714
|
+
_save_lit: false, // what the Save was last told (light_save_button)
|
|
522
715
|
yet_showed: false,
|
|
523
716
|
edit_mode: false,
|
|
524
717
|
operation_mode: null,
|
|
@@ -553,6 +746,7 @@ let PRIVATE_DATA = {
|
|
|
553
746
|
_detail_node_id: null, // node id the detail popover is showing
|
|
554
747
|
_delete_confirm_el: null, // delete confirmation popover
|
|
555
748
|
_unlink_confirm_el: null, // unlink confirmation popover
|
|
749
|
+
_confirm_questions: {}, // priv key of a confirm popover → the {what, id|ids} it asks
|
|
556
750
|
_create_popover_el: null, // create node popover element
|
|
557
751
|
_context_node_id: null, // node id for context menu target
|
|
558
752
|
_context_port_key: null, // port key for context menu target (null = node body)
|
|
@@ -561,6 +755,7 @@ let PRIVATE_DATA = {
|
|
|
561
755
|
_link_source: null, // {node_id, port_key, col, topic_name} of fkey being linked
|
|
562
756
|
_link_icon_el: null, // floating link icon on fkey port
|
|
563
757
|
_link_drag_svg: null, // SVG overlay for drag line
|
|
758
|
+
_link_drag: null, // the link drag in flight: {left, top, unwire}
|
|
564
759
|
_link_valid_hooks: [], // [{node_id, port_key}] compatible hook targets
|
|
565
760
|
_link_saved_styles: [], // saved port styles to restore on cancel
|
|
566
761
|
_focus_topic: null, // topic currently focused (EV_FOCUS_TOPIC)
|
|
@@ -1047,6 +1242,10 @@ function build_graph(gobj)
|
|
|
1047
1242
|
*/
|
|
1048
1243
|
graph.setTheme(priv.theme);
|
|
1049
1244
|
|
|
1245
|
+
/* Tab goes from the graph to its own buttons: the main canvas in
|
|
1246
|
+
* the Tab order, the other layers out (g6_canvas_tab_order.js). */
|
|
1247
|
+
keep_canvas_tab_order(gobj, graph);
|
|
1248
|
+
|
|
1050
1249
|
/* A card is an HTML node, and G6's HTML nodes do not pass the wheel
|
|
1051
1250
|
* on: over a card the graph neither scrolled nor zoomed. Only a wheel
|
|
1052
1251
|
* over a CARD is handed back to the canvas -- the toolbars and the
|
|
@@ -1423,10 +1622,13 @@ function update_toolbar(gobj)
|
|
|
1423
1622
|
});
|
|
1424
1623
|
}
|
|
1425
1624
|
|
|
1426
|
-
/*
|
|
1427
|
-
*
|
|
1428
|
-
|
|
1429
|
-
|
|
1625
|
+
/* Nothing to tell the items after this: what each one shows --
|
|
1626
|
+
* pressed, lit, disabled -- is computed in its getItems(). The
|
|
1627
|
+
* rebuild lands LATER (G6 reads the list now and writes the
|
|
1628
|
+
* innerHTML once its promise resolves), so an element told here
|
|
1629
|
+
* was the one being replaced: zoom to selection came back
|
|
1630
|
+
* disabled over a selection, and save, undo and redo over
|
|
1631
|
+
* history still pending. */
|
|
1430
1632
|
}
|
|
1431
1633
|
|
|
1432
1634
|
/************************************************************
|
|
@@ -1490,12 +1692,12 @@ function toolbar_fold_item(gobj)
|
|
|
1490
1692
|
if(toolbars_collapsed(gobj)) {
|
|
1491
1693
|
return {
|
|
1492
1694
|
id: 'g6-icon-toolbar-show', value: 'toolbar-toggle',
|
|
1493
|
-
className: 'EV_TOGGLE_TOOLBARS', title: t('show toolbar')
|
|
1695
|
+
className: 'EV_TOGGLE_TOOLBARS', title: t('show toolbar'), title_key: 'show toolbar'
|
|
1494
1696
|
};
|
|
1495
1697
|
}
|
|
1496
1698
|
return {
|
|
1497
1699
|
id: 'g6-icon-toolbar-hide', value: 'toolbar-toggle',
|
|
1498
|
-
className: 'EV_TOGGLE_TOOLBARS', title: t('hide toolbar')
|
|
1700
|
+
className: 'EV_TOGGLE_TOOLBARS', title: t('hide toolbar'), title_key: 'hide toolbar'
|
|
1499
1701
|
};
|
|
1500
1702
|
}
|
|
1501
1703
|
|
|
@@ -1592,15 +1794,15 @@ function configure_toolbar(gobj)
|
|
|
1592
1794
|
}
|
|
1593
1795
|
|
|
1594
1796
|
let items = [
|
|
1595
|
-
{ id: 'g6-icon-zoom-in', value: 'zoom-in', className: 'EV_ZOOM_IN', title: t('zoom in') },
|
|
1596
|
-
{ id: 'g6-icon-zoom-out', value: 'zoom-out', className: 'EV_ZOOM_OUT', title: t('zoom out') },
|
|
1797
|
+
{ id: 'g6-icon-zoom-in', value: 'zoom-in', className: 'EV_ZOOM_IN', title: t('zoom in'), title_key: 'zoom in' },
|
|
1798
|
+
{ id: 'g6-icon-zoom-out', value: 'zoom-out', className: 'EV_ZOOM_OUT', title: t('zoom out'), title_key: 'zoom out' },
|
|
1597
1799
|
/* What the two buttons above change. Every editor
|
|
1598
1800
|
* that offers a zoom shows the number; without it
|
|
1599
1801
|
* `1:1` is a jump to a value nobody was told. */
|
|
1600
1802
|
{ readout: true, className: 'G6_ZOOM_LEVEL',
|
|
1601
|
-
text: zoom_percent_text(gobj), title: t('zoom level') },
|
|
1803
|
+
text: zoom_percent_text(gobj), title: t('zoom level'), title_key: 'zoom level' },
|
|
1602
1804
|
{ separator: true },
|
|
1603
|
-
{ id: 'g6-icon-fit', value: 'auto-fit', className: 'EV_AUTO_FIT', title: t('auto fit') },
|
|
1805
|
+
{ id: 'g6-icon-fit', value: 'auto-fit', className: 'EV_AUTO_FIT', title: t('auto fit'), title_key: 'auto fit' },
|
|
1604
1806
|
/* `1:1`, not a house. The action is `zoomTo(1)`: it
|
|
1605
1807
|
* sets the SCALE and leaves the camera where it was,
|
|
1606
1808
|
* which is neither of the two things a house means
|
|
@@ -1610,7 +1812,7 @@ function configure_toolbar(gobj)
|
|
|
1610
1812
|
* answered with the same corner of it at 100%.
|
|
1611
1813
|
* Actual size is WRITTEN in every editor that offers
|
|
1612
1814
|
* it, never drawn: there is no glyph for it. */
|
|
1613
|
-
{ text: '1:1', value: 'reset', className: 'EV_ZOOM_RESET', title: t('actual size') },
|
|
1815
|
+
{ text: '1:1', value: 'reset', className: 'EV_ZOOM_RESET', title: t('actual size'), title_key: 'actual size' },
|
|
1614
1816
|
/* Pick one node and every zoom leaves it in the
|
|
1615
1817
|
* middle. The pressed classes are baked in HERE and
|
|
1616
1818
|
* not set afterwards, for the same reason the
|
|
@@ -1625,21 +1827,25 @@ function configure_toolbar(gobj)
|
|
|
1625
1827
|
className: 'EV_TOGGLE_ANCHOR' +
|
|
1626
1828
|
(priv.anchor_state === "on"? ' pressed_state' : '') +
|
|
1627
1829
|
(priv.anchor_state === "arming"? ' color_pending_state' : ''),
|
|
1628
|
-
|
|
1830
|
+
pressed: priv.anchor_state === "on",
|
|
1831
|
+
...anchor_title(priv.anchor_state)
|
|
1629
1832
|
},
|
|
1630
1833
|
];
|
|
1631
1834
|
|
|
1632
1835
|
/* Only in edition, because that is where a selection
|
|
1633
1836
|
* can exist: a button that is permanently disabled is
|
|
1634
1837
|
* not a control, it is furniture. Disabled while the
|
|
1635
|
-
* selection is empty
|
|
1636
|
-
* funnel every change goes
|
|
1637
|
-
*
|
|
1838
|
+
* selection is empty, asked HERE as well as by
|
|
1839
|
+
* `paint_selection` (the one funnel every change goes
|
|
1840
|
+
* through, which tells the live item): a rebuild draws
|
|
1841
|
+
* the item from this list, and an item born disabled
|
|
1842
|
+
* came back that way over a selection. */
|
|
1638
1843
|
if(priv.edit_mode) {
|
|
1639
1844
|
items.push({
|
|
1640
1845
|
id: 'g6-icon-fit-selection', value: 'fit-selection',
|
|
1641
1846
|
className: 'EV_ZOOM_SELECTION',
|
|
1642
|
-
title: t('zoom to selection'),
|
|
1847
|
+
title: t('zoom to selection'), title_key: 'zoom to selection',
|
|
1848
|
+
disabled: !selection_is_painted(gobj)
|
|
1643
1849
|
});
|
|
1644
1850
|
}
|
|
1645
1851
|
|
|
@@ -1647,13 +1853,16 @@ function configure_toolbar(gobj)
|
|
|
1647
1853
|
/* Full screen is a WINDOW control, not a camera one:
|
|
1648
1854
|
* it goes in its own group. */
|
|
1649
1855
|
items.push({ separator: true });
|
|
1856
|
+
/* ONE control in two states (`control`): the
|
|
1857
|
+
* focus a rebuild gives back lands on the other
|
|
1858
|
+
* state's button. */
|
|
1650
1859
|
if(priv.is_fullscreen) {
|
|
1651
1860
|
items.push(
|
|
1652
|
-
{ id: 'g6-icon-fullscreen-exit', value: 'exit-fullscreen', className: 'EV_EXIT_FULLSCREEN', title: t('exit full screen') }
|
|
1861
|
+
{ id: 'g6-icon-fullscreen-exit', value: 'exit-fullscreen', control: 'fullscreen', className: 'EV_EXIT_FULLSCREEN', title: t('exit full screen'), title_key: 'exit full screen' }
|
|
1653
1862
|
);
|
|
1654
1863
|
} else {
|
|
1655
1864
|
items.push(
|
|
1656
|
-
{ id: 'g6-icon-fullscreen', value: 'request-fullscreen', className: 'EV_REQUEST_FULLSCREEN', title: t('enter full screen') }
|
|
1865
|
+
{ id: 'g6-icon-fullscreen', value: 'request-fullscreen', control: 'fullscreen', className: 'EV_REQUEST_FULLSCREEN', title: t('enter full screen'), title_key: 'enter full screen' }
|
|
1657
1866
|
);
|
|
1658
1867
|
}
|
|
1659
1868
|
}
|
|
@@ -1744,8 +1953,14 @@ function configure_toolbar_edit(gobj)
|
|
|
1744
1953
|
position: 'left-top',
|
|
1745
1954
|
style: toolbar_style(),
|
|
1746
1955
|
getItems: () => {
|
|
1956
|
+
/* Save, undo and redo bake their state for the same
|
|
1957
|
+
* reason as the selection mode below: told only to
|
|
1958
|
+
* the live element (update_history_buttons), they
|
|
1959
|
+
* came back disabled from every rebuild while the
|
|
1960
|
+
* history still had something to undo. */
|
|
1961
|
+
let lit = edit_buttons_lit(gobj);
|
|
1747
1962
|
return [
|
|
1748
|
-
{ id: 'g6-icon-plus', value: 'create-node', className: 'EV_CREATE_NODE_BTN color_create_state', title: t('create node') },
|
|
1963
|
+
{ id: 'g6-icon-plus', value: 'create-node', className: 'EV_CREATE_NODE_BTN color_create_state', title: t('create node'), title_key: 'create node' },
|
|
1749
1964
|
/* The mode is carried in the class getItems()
|
|
1750
1965
|
* returns, not only pushed onto the element after
|
|
1751
1966
|
* the fact: this toolbar is rebuilt whole (a fold,
|
|
@@ -1757,17 +1972,30 @@ function configure_toolbar_edit(gobj)
|
|
|
1757
1972
|
id: 'g6-icon-select-mode', value: 'selection-mode',
|
|
1758
1973
|
className: 'EV_TOGGLE_SELECTION_MODE' +
|
|
1759
1974
|
(priv.selection_mode? ' pressed_state' : ''),
|
|
1760
|
-
|
|
1975
|
+
pressed: !!priv.selection_mode,
|
|
1976
|
+
title: t('selection mode'), title_key: 'selection mode'
|
|
1977
|
+
},
|
|
1978
|
+
{
|
|
1979
|
+
id: 'g6-icon-save', value: 'save',
|
|
1980
|
+
className: 'EV_SAVE_GRAPH' + (lit.save? ' color_submit_state' : ''),
|
|
1981
|
+
title: t('save'), title_key: 'save', disabled: !lit.save
|
|
1982
|
+
},
|
|
1983
|
+
{
|
|
1984
|
+
id: 'g6-icon-undo', value: 'undo',
|
|
1985
|
+
className: 'EV_HISTORY_UNDO' + (lit.undo? ' color_active_state' : ''),
|
|
1986
|
+
title: t('undo'), title_key: 'undo', disabled: !lit.undo
|
|
1987
|
+
},
|
|
1988
|
+
{
|
|
1989
|
+
id: 'g6-icon-redo', value: 'redo',
|
|
1990
|
+
className: 'EV_HISTORY_REDO' + (lit.redo? ' color_active_state' : ''),
|
|
1991
|
+
title: t('redo'), title_key: 'redo', disabled: !lit.redo
|
|
1761
1992
|
},
|
|
1762
|
-
{ id: 'g6-icon-save', value: 'save', className: 'EV_SAVE_GRAPH', title: t('save'), disabled: true },
|
|
1763
|
-
{ id: 'g6-icon-undo', value: 'undo', className: 'EV_HISTORY_UNDO', title: t('undo'), disabled: true },
|
|
1764
|
-
{ id: 'g6-icon-redo', value: 'redo', className: 'EV_HISTORY_REDO', title: t('redo'), disabled: true },
|
|
1765
1993
|
];
|
|
1766
1994
|
},
|
|
1767
1995
|
onClick: (value) => {
|
|
1768
1996
|
switch(value) {
|
|
1769
1997
|
case 'create-node':
|
|
1770
|
-
|
|
1998
|
+
gobj_send_event(gobj, "EV_TOGGLE_CREATE_POPOVER", {}, gobj);
|
|
1771
1999
|
break;
|
|
1772
2000
|
case 'selection-mode':
|
|
1773
2001
|
gobj_send_event(gobj, "EV_TOGGLE_SELECTION_MODE", {}, gobj);
|
|
@@ -3434,8 +3662,7 @@ function update_history_buttons(gobj)
|
|
|
3434
3662
|
set_active_state($container, ".EV_HISTORY_UNDO", true);
|
|
3435
3663
|
|
|
3436
3664
|
// Pending changes exist, enable save
|
|
3437
|
-
|
|
3438
|
-
set_submit_state($container, ".EV_SAVE_GRAPH", true);
|
|
3665
|
+
light_save_button(gobj, true);
|
|
3439
3666
|
} else {
|
|
3440
3667
|
disableElements($container, ".EV_HISTORY_UNDO");
|
|
3441
3668
|
set_active_state($container, ".EV_HISTORY_UNDO", false);
|
|
@@ -3443,13 +3670,7 @@ function update_history_buttons(gobj)
|
|
|
3443
3670
|
/* No more undos: the graph is back to what was loaded
|
|
3444
3671
|
* or saved -- unless a write of it did not land. That
|
|
3445
3672
|
* one is still owed, and only a Save pays it. */
|
|
3446
|
-
|
|
3447
|
-
enableElements($container, ".EV_SAVE_GRAPH");
|
|
3448
|
-
set_submit_state($container, ".EV_SAVE_GRAPH", true);
|
|
3449
|
-
} else {
|
|
3450
|
-
disableElements($container, ".EV_SAVE_GRAPH");
|
|
3451
|
-
set_submit_state($container, ".EV_SAVE_GRAPH", false);
|
|
3452
|
-
}
|
|
3673
|
+
light_save_button(gobj, graphs_write_is_owed(gobj));
|
|
3453
3674
|
}
|
|
3454
3675
|
} else {
|
|
3455
3676
|
disableElements($container, ".EV_HISTORY_REDO");
|
|
@@ -3457,10 +3678,52 @@ function update_history_buttons(gobj)
|
|
|
3457
3678
|
|
|
3458
3679
|
disableElements($container, ".EV_HISTORY_UNDO");
|
|
3459
3680
|
set_active_state($container, ".EV_HISTORY_UNDO", false);
|
|
3681
|
+
|
|
3682
|
+
/* No history (a clear, a refresh): nothing to save but a
|
|
3683
|
+
* write still owed -- and `_save_lit` is what the next
|
|
3684
|
+
* rebuild of the toolbar draws. */
|
|
3685
|
+
light_save_button(gobj, graphs_write_is_owed(gobj));
|
|
3460
3686
|
}
|
|
3461
3687
|
}
|
|
3462
3688
|
}
|
|
3463
3689
|
|
|
3690
|
+
/************************************************************
|
|
3691
|
+
* What Save, Undo and Redo show when the edit toolbar is
|
|
3692
|
+
* built (its getItems): Undo and Redo what the history can do
|
|
3693
|
+
* now; Save what it was last told (`_save_lit`, see
|
|
3694
|
+
* light_save_button), because more than the history lights
|
|
3695
|
+
* it -- a change the history does not record
|
|
3696
|
+
* (mark_graph_dirty), a write owed -- and update_history_
|
|
3697
|
+
* buttons() is where those are weighed.
|
|
3698
|
+
************************************************************/
|
|
3699
|
+
function edit_buttons_lit(gobj)
|
|
3700
|
+
{
|
|
3701
|
+
let history = graph_get_plugin(gobj, "history");
|
|
3702
|
+
|
|
3703
|
+
return {
|
|
3704
|
+
undo: !!history && history.canUndo(),
|
|
3705
|
+
redo: !!history && history.canRedo(),
|
|
3706
|
+
save: !!gobj.priv._save_lit,
|
|
3707
|
+
};
|
|
3708
|
+
}
|
|
3709
|
+
|
|
3710
|
+
/************************************************************
|
|
3711
|
+
* Light the Save, or put it out: on the live element, and in
|
|
3712
|
+
* `_save_lit`, where the next rebuild of the toolbar reads it.
|
|
3713
|
+
************************************************************/
|
|
3714
|
+
function light_save_button(gobj, lit)
|
|
3715
|
+
{
|
|
3716
|
+
let $container = gobj_read_attr(gobj, "$container");
|
|
3717
|
+
|
|
3718
|
+
gobj.priv._save_lit = !!lit;
|
|
3719
|
+
if(lit) {
|
|
3720
|
+
enableElements($container, ".EV_SAVE_GRAPH");
|
|
3721
|
+
} else {
|
|
3722
|
+
disableElements($container, ".EV_SAVE_GRAPH");
|
|
3723
|
+
}
|
|
3724
|
+
set_submit_state($container, ".EV_SAVE_GRAPH", !!lit);
|
|
3725
|
+
}
|
|
3726
|
+
|
|
3464
3727
|
/************************************************************
|
|
3465
3728
|
* Graph utility functions
|
|
3466
3729
|
*
|
|
@@ -4720,22 +4983,63 @@ function floating_popover_dx()
|
|
|
4720
4983
|
return 4 + floating_icon_size() + 4;
|
|
4721
4984
|
}
|
|
4722
4985
|
|
|
4986
|
+
/************************************************************
|
|
4987
|
+
* A key press IS the click on an element drawn as a button.
|
|
4988
|
+
*
|
|
4989
|
+
* Enter or Space clicks it, so the keyboard reaches the very
|
|
4990
|
+
* handler the pointer does, whatever that handler is. Stopped
|
|
4991
|
+
* here, as the click is: this view's container forwards every
|
|
4992
|
+
* keydown to G6 (its shortcuts, and EV_KEY_DOWN), and a key
|
|
4993
|
+
* that pressed a button is not the graph's. Space would scroll
|
|
4994
|
+
* the page as well. Any other key goes on to the graph, so
|
|
4995
|
+
* Escape and Delete keep doing there what they do.
|
|
4996
|
+
************************************************************/
|
|
4997
|
+
function click_on_enter_or_space(el)
|
|
4998
|
+
{
|
|
4999
|
+
el.addEventListener('keydown', (e) => {
|
|
5000
|
+
if(e.key === 'Enter' || e.key === ' ') {
|
|
5001
|
+
e.preventDefault();
|
|
5002
|
+
e.stopPropagation();
|
|
5003
|
+
el.click();
|
|
5004
|
+
}
|
|
5005
|
+
});
|
|
5006
|
+
}
|
|
5007
|
+
|
|
4723
5008
|
/************************************************************
|
|
4724
5009
|
* Create a floating circular icon button.
|
|
4725
|
-
*
|
|
4726
|
-
*
|
|
4727
|
-
*
|
|
4728
|
-
*
|
|
4729
|
-
*
|
|
4730
|
-
*
|
|
5010
|
+
*
|
|
5011
|
+
* A button for the keyboard too: `role` and `tabindex`, and
|
|
5012
|
+
* Enter or Space click it (click_on_enter_or_space). Its name
|
|
5013
|
+
* carries its KEY, so refresh_language() renames it on a
|
|
5014
|
+
* language switch; a key handed in here goes in
|
|
5015
|
+
* G6_NODES_TREE_I18N_KEYS, because no scan sees it.
|
|
5016
|
+
*
|
|
5017
|
+
* Its click does one thing: it sends `event` to `gobj`, with
|
|
5018
|
+
* `kw` -- the plain identity of what the button belongs to --
|
|
5019
|
+
* and the ACTION does the work, where the `machine` trace sees
|
|
5020
|
+
* it. A pointer press does not take the focus
|
|
5021
|
+
* (keep_focus_on_press).
|
|
5022
|
+
*
|
|
5023
|
+
* @param {string} svgKey - key into SVG_ICONS
|
|
5024
|
+
* @param {string} color - border & text color (e.g. '#1890ff')
|
|
5025
|
+
* @param {number} left - CSS left in px
|
|
5026
|
+
* @param {number} top - CSS top in px
|
|
5027
|
+
* @param {string} title_key - i18n key of its title and aria-label
|
|
5028
|
+
* @param {object} gobj - who hears the click
|
|
5029
|
+
* @param {string} event - an input event of this gclass
|
|
5030
|
+
* @param {object} kw - plain JSON identity, copied per click
|
|
4731
5031
|
* @returns {HTMLElement}
|
|
4732
5032
|
************************************************************/
|
|
4733
|
-
function create_floating_icon(svgKey, color, left, top,
|
|
5033
|
+
function create_floating_icon(svgKey, color, left, top, title_key, gobj, event, kw)
|
|
4734
5034
|
{
|
|
4735
5035
|
const size = floating_icon_size();
|
|
4736
5036
|
const el = document.createElement('div');
|
|
4737
|
-
el.
|
|
4738
|
-
el.setAttribute('
|
|
5037
|
+
el.setAttribute('role', 'button');
|
|
5038
|
+
el.setAttribute('tabindex', '0');
|
|
5039
|
+
el.setAttribute('title', t(title_key));
|
|
5040
|
+
el.setAttribute('data-i18n-title', title_key);
|
|
5041
|
+
el.setAttribute('aria-label', t(title_key));
|
|
5042
|
+
el.setAttribute('data-i18n-aria-label', title_key);
|
|
4739
5043
|
el.innerHTML = SVG_ICONS[svgKey] || '';
|
|
4740
5044
|
el.style.cssText =
|
|
4741
5045
|
'position:absolute;' +
|
|
@@ -4762,13 +5066,25 @@ function create_floating_icon(svgKey, color, left, top, title, onClick)
|
|
|
4762
5066
|
|
|
4763
5067
|
el.addEventListener('click', (e) => {
|
|
4764
5068
|
e.stopPropagation();
|
|
4765
|
-
|
|
5069
|
+
gobj_send_event(gobj, event, json_deep_copy(kw || {}), gobj);
|
|
4766
5070
|
});
|
|
5071
|
+
keep_focus_on_press(el);
|
|
5072
|
+
click_on_enter_or_space(el);
|
|
4767
5073
|
return el;
|
|
4768
5074
|
}
|
|
4769
5075
|
|
|
4770
5076
|
/************************************************************
|
|
4771
5077
|
* Remove one or more overlay elements from priv and the DOM.
|
|
5078
|
+
*
|
|
5079
|
+
* An overlay that holds the focus -- a floating button reached
|
|
5080
|
+
* with Tab, the ✕ of the detail popover, a field of a popover --
|
|
5081
|
+
* gives it back to the canvas BEFORE it goes. Removing the
|
|
5082
|
+
* focused element is a focus loss the browsers do not report
|
|
5083
|
+
* alike: Chromium fires a focusout, which _on_focusout_restore
|
|
5084
|
+
* answers; Firefox fires none and leaves the focus on <body>,
|
|
5085
|
+
* and the graph is deaf to Escape, Delete and ctrl+A until the
|
|
5086
|
+
* next click on it.
|
|
5087
|
+
*
|
|
4772
5088
|
* @param {object} gobj
|
|
4773
5089
|
* @param {...string} keys - priv property names to remove
|
|
4774
5090
|
************************************************************/
|
|
@@ -4777,7 +5093,11 @@ function hide_overlay(gobj, ...keys)
|
|
|
4777
5093
|
let priv = gobj.priv;
|
|
4778
5094
|
for(let key of keys) {
|
|
4779
5095
|
if(priv[key]) {
|
|
4780
|
-
priv[key]
|
|
5096
|
+
let el = priv[key];
|
|
5097
|
+
if(el.contains(document.activeElement) && !gobj_is_destroying(gobj)) {
|
|
5098
|
+
focus_main_canvas(priv.$container);
|
|
5099
|
+
}
|
|
5100
|
+
el.remove();
|
|
4781
5101
|
priv[key] = null;
|
|
4782
5102
|
}
|
|
4783
5103
|
}
|
|
@@ -4835,9 +5155,7 @@ function graphs_write_is_owed(gobj)
|
|
|
4835
5155
|
|
|
4836
5156
|
function mark_graph_dirty(gobj)
|
|
4837
5157
|
{
|
|
4838
|
-
|
|
4839
|
-
enableElements($container, ".EV_SAVE_GRAPH");
|
|
4840
|
-
set_submit_state($container, ".EV_SAVE_GRAPH", true);
|
|
5158
|
+
light_save_button(gobj, true);
|
|
4841
5159
|
}
|
|
4842
5160
|
|
|
4843
5161
|
/************************************************************
|
|
@@ -4854,19 +5172,21 @@ function extract_size(style)
|
|
|
4854
5172
|
|
|
4855
5173
|
/************************************************************
|
|
4856
5174
|
* Show a pair of floating icons (gear + trash) next to an
|
|
4857
|
-
* element.
|
|
5175
|
+
* element. The titles are i18n KEYS (see create_floating_icon);
|
|
5176
|
+
* each click sends its event to `gobj` with the same identity
|
|
5177
|
+
* `kw`. Returns {icon_el, delete_el}.
|
|
4858
5178
|
************************************************************/
|
|
4859
|
-
function show_dual_icons(gobj, x, y,
|
|
5179
|
+
function show_dual_icons(gobj, x, y, gear_key, gear_event, trash_key, trash_event, kw)
|
|
4860
5180
|
{
|
|
4861
5181
|
let priv = gobj.priv;
|
|
4862
5182
|
|
|
4863
5183
|
const icon_el = create_floating_icon(
|
|
4864
|
-
'gear', '#1890ff', x, y,
|
|
5184
|
+
'gear', '#1890ff', x, y, gear_key, gobj, gear_event, kw
|
|
4865
5185
|
);
|
|
4866
5186
|
priv.$container.appendChild(icon_el);
|
|
4867
5187
|
|
|
4868
5188
|
const delete_el = create_floating_icon(
|
|
4869
|
-
'trash', '#ff4d4f', x, y + floating_icon_step(),
|
|
5189
|
+
'trash', '#ff4d4f', x, y + floating_icon_step(), trash_key, gobj, trash_event, kw
|
|
4870
5190
|
);
|
|
4871
5191
|
priv.$container.appendChild(delete_el);
|
|
4872
5192
|
|
|
@@ -5052,11 +5372,23 @@ const BTN_STYLE_CANCEL =
|
|
|
5052
5372
|
'border:1px solid var(--bulma-border-weak, #d9d9d9);' +
|
|
5053
5373
|
'border-radius:4px;cursor:pointer;font-size:13px;font-weight:500;';
|
|
5054
5374
|
|
|
5055
|
-
|
|
5375
|
+
/************************************************************
|
|
5376
|
+
* The buttons of a popover: each one SENDS an input event of
|
|
5377
|
+
* this gclass to `gobj`, and the action does the work, where
|
|
5378
|
+
* the `machine` trace sees it (V20.5 #4). A button is
|
|
5379
|
+
* {i18n, style, event, kw, form}
|
|
5380
|
+
* `kw` is the plain identity of what the popover is about
|
|
5381
|
+
* (copied per click); `form` names the controls whose `value`
|
|
5382
|
+
* goes in the kw too, as the control says it -- a string, the
|
|
5383
|
+
* action parses it. The kw says what was asked for, in the
|
|
5384
|
+
* trace: the identity and what the form held when it was
|
|
5385
|
+
* pressed.
|
|
5386
|
+
************************************************************/
|
|
5387
|
+
function create_form_button_row(parent, buttons, gobj)
|
|
5056
5388
|
{
|
|
5057
5389
|
let btnRow = document.createElement('div');
|
|
5058
5390
|
btnRow.style.cssText = 'display:flex;gap:8px;';
|
|
5059
|
-
for(let {i18n, style,
|
|
5391
|
+
for(let {i18n, style, event, kw, form} of buttons) {
|
|
5060
5392
|
/* `i18n` is the key (named so that validate-locales sees a literal
|
|
5061
5393
|
* one): the button carries it, and its name */
|
|
5062
5394
|
let btn = document.createElement('button');
|
|
@@ -5069,7 +5401,7 @@ function create_form_button_row(parent, buttons)
|
|
|
5069
5401
|
btn.style.cssText = style + touch_control_css();
|
|
5070
5402
|
btn.addEventListener('click', (e) => {
|
|
5071
5403
|
e.stopPropagation();
|
|
5072
|
-
|
|
5404
|
+
gobj_send_event(gobj, event, form_kw(kw, form), gobj);
|
|
5073
5405
|
});
|
|
5074
5406
|
btnRow.appendChild(btn);
|
|
5075
5407
|
}
|
|
@@ -5077,11 +5409,27 @@ function create_form_button_row(parent, buttons)
|
|
|
5077
5409
|
return btnRow;
|
|
5078
5410
|
}
|
|
5079
5411
|
|
|
5412
|
+
/* The kw a popover button sends: its identity, and the value of
|
|
5413
|
+
* each control of its form. */
|
|
5414
|
+
function form_kw(kw, form)
|
|
5415
|
+
{
|
|
5416
|
+
let out = json_deep_copy(kw || {});
|
|
5417
|
+
for(let [name, control] of Object.entries(form || {})) {
|
|
5418
|
+
out[name] = String(control.value);
|
|
5419
|
+
}
|
|
5420
|
+
return out;
|
|
5421
|
+
}
|
|
5422
|
+
|
|
5080
5423
|
/************************************************************
|
|
5081
5424
|
* Show a confirmation popover next to a target element.
|
|
5082
|
-
*
|
|
5425
|
+
*
|
|
5426
|
+
* `question` is the plain identity of what is asked, {what,
|
|
5427
|
+
* id|ids}: the confirm button sends it with EV_CONFIRMED and the
|
|
5428
|
+
* cancel button with EV_CANCEL_CONFIRM, and the actions answer.
|
|
5429
|
+
* `priv_key` is where the popover is kept, and it must be the
|
|
5430
|
+
* one CONFIRM_POPOVER_OF names for `question.what`.
|
|
5083
5431
|
************************************************************/
|
|
5084
|
-
function show_confirm_popover(gobj, target_el, message, confirm_text, confirm_color,
|
|
5432
|
+
function show_confirm_popover(gobj, target_el, message, confirm_text, confirm_color, question, priv_key)
|
|
5085
5433
|
{
|
|
5086
5434
|
hide_overlay(gobj, priv_key);
|
|
5087
5435
|
|
|
@@ -5114,18 +5462,21 @@ function show_confirm_popover(gobj, target_el, message, confirm_text, confirm_co
|
|
|
5114
5462
|
{
|
|
5115
5463
|
i18n: 'cancel',
|
|
5116
5464
|
style: BTN_STYLE_CANCEL,
|
|
5117
|
-
|
|
5465
|
+
event: "EV_CANCEL_CONFIRM",
|
|
5466
|
+
kw: question,
|
|
5118
5467
|
},
|
|
5119
5468
|
{
|
|
5120
5469
|
i18n: confirm_text,
|
|
5121
5470
|
style: 'flex:1;padding:6px;background:' + confirm_color + ';color:#fff;border:none;' +
|
|
5122
5471
|
'border-radius:4px;cursor:pointer;font-size:13px;font-weight:500;',
|
|
5123
|
-
|
|
5472
|
+
event: "EV_CONFIRMED",
|
|
5473
|
+
kw: question,
|
|
5124
5474
|
},
|
|
5125
|
-
]);
|
|
5475
|
+
], gobj);
|
|
5126
5476
|
|
|
5127
5477
|
priv.$container.appendChild(popover);
|
|
5128
5478
|
priv[priv_key] = popover;
|
|
5479
|
+
priv._confirm_questions[priv_key] = json_deep_copy(question);
|
|
5129
5480
|
clamp_popover_position(gobj, popover);
|
|
5130
5481
|
}
|
|
5131
5482
|
|
|
@@ -5237,13 +5588,17 @@ function paint_selection(gobj, ids)
|
|
|
5237
5588
|
* Zoom-to-selection can only act on a selection.
|
|
5238
5589
|
*
|
|
5239
5590
|
* The attribute is toggled on the element instead of re-rendering
|
|
5240
|
-
* the toolbar, which would rebuild its innerHTML
|
|
5241
|
-
*
|
|
5242
|
-
*
|
|
5591
|
+
* the toolbar, which would rebuild its whole innerHTML each time a
|
|
5592
|
+
* card is ticked. A rebuild asks the same question
|
|
5593
|
+
* (selection_is_painted) in the toolbar's getItems().
|
|
5243
5594
|
************************************************************/
|
|
5595
|
+
function selection_is_painted(gobj)
|
|
5596
|
+
{
|
|
5597
|
+
return (gobj.priv._selected_paint_ids || []).length > 0;
|
|
5598
|
+
}
|
|
5599
|
+
|
|
5244
5600
|
function update_zoom_selection_button(gobj)
|
|
5245
5601
|
{
|
|
5246
|
-
let priv = gobj.priv;
|
|
5247
5602
|
let $container = gobj_read_attr(gobj, "$container");
|
|
5248
5603
|
|
|
5249
5604
|
if(!$container) {
|
|
@@ -5255,7 +5610,7 @@ function update_zoom_selection_button(gobj)
|
|
|
5255
5610
|
return; /* no toolbar, or not in edition: nothing to enable */
|
|
5256
5611
|
}
|
|
5257
5612
|
|
|
5258
|
-
if((
|
|
5613
|
+
if(selection_is_painted(gobj)) {
|
|
5259
5614
|
enableElements($container, ".EV_ZOOM_SELECTION");
|
|
5260
5615
|
} else {
|
|
5261
5616
|
disableElements($container, ".EV_ZOOM_SELECTION");
|
|
@@ -5265,12 +5620,13 @@ function update_zoom_selection_button(gobj)
|
|
|
5265
5620
|
/************************************************************
|
|
5266
5621
|
* The selection mode is a state, so it has to LOOK like one:
|
|
5267
5622
|
* a PRESSED toggle, not a button wearing one of the palette's
|
|
5268
|
-
* action colours (see `pressed_state` in lib_graph.css).
|
|
5623
|
+
* action colours (see `pressed_state` in lib_graph.css). And
|
|
5624
|
+
* it SAYS it, in `aria-pressed`, for whoever does not see it.
|
|
5269
5625
|
*
|
|
5270
5626
|
* Toggled on the element rather than re-rendered with the
|
|
5271
5627
|
* toolbar, for the same reason as the button above: a
|
|
5272
|
-
* re-render rebuilds the innerHTML
|
|
5273
|
-
*
|
|
5628
|
+
* re-render rebuilds the whole innerHTML. A rebuild draws the
|
|
5629
|
+
* same state, baked in the edit toolbar's getItems().
|
|
5274
5630
|
************************************************************/
|
|
5275
5631
|
function update_selection_mode_button(gobj)
|
|
5276
5632
|
{
|
|
@@ -5286,6 +5642,9 @@ function update_selection_mode_button(gobj)
|
|
|
5286
5642
|
}
|
|
5287
5643
|
|
|
5288
5644
|
set_pressed_state($container, ".EV_TOGGLE_SELECTION_MODE", priv.selection_mode);
|
|
5645
|
+
$container.querySelectorAll(".EV_TOGGLE_SELECTION_MODE").forEach(($el) => {
|
|
5646
|
+
$el.setAttribute("aria-pressed", priv.selection_mode? "true" : "false");
|
|
5647
|
+
});
|
|
5289
5648
|
}
|
|
5290
5649
|
|
|
5291
5650
|
/************************************************************
|
|
@@ -5980,21 +6339,22 @@ function show_link_icon_if_fkey(gobj)
|
|
|
5980
6339
|
}
|
|
5981
6340
|
let vp = graph.getViewportByCanvas([canvasPos.x, canvasPos.y]);
|
|
5982
6341
|
|
|
6342
|
+
/* A click (no drag) enters the linking mode; a press that drags
|
|
6343
|
+
* enters it on its own (start_link_drag, below). */
|
|
6344
|
+
let identity = {node_id: priv._selected_node_id, port_key: priv._selected_port_key};
|
|
5983
6345
|
let icon = create_floating_icon(
|
|
5984
6346
|
'link', '#52c41a',
|
|
5985
6347
|
vp[0] + floating_icon_size() / 2 + 4, vp[1] + floating_icon_dy(),
|
|
5986
|
-
|
|
5987
|
-
// Click (no drag) toggles linking mode
|
|
5988
|
-
if(!priv._linking_mode) {
|
|
5989
|
-
enter_linking_mode(gobj);
|
|
5990
|
-
}
|
|
5991
|
-
}
|
|
6348
|
+
'link to hook', gobj, "EV_ENTER_LINKING_MODE", identity
|
|
5992
6349
|
);
|
|
5993
|
-
|
|
6350
|
+
/* The press may be a drag: EV_START_LINK_DRAG, with the port's
|
|
6351
|
+
* identity and where the pointer went down. */
|
|
5994
6352
|
icon.addEventListener('pointerdown', (e) => {
|
|
5995
6353
|
e.stopPropagation();
|
|
5996
6354
|
e.preventDefault();
|
|
5997
|
-
|
|
6355
|
+
gobj_send_event(gobj, "EV_START_LINK_DRAG", Object.assign(
|
|
6356
|
+
json_deep_copy(identity), {client_x: e.clientX, client_y: e.clientY}
|
|
6357
|
+
), gobj);
|
|
5998
6358
|
});
|
|
5999
6359
|
priv.$container.appendChild(icon);
|
|
6000
6360
|
priv._link_icon_el = icon;
|
|
@@ -6070,7 +6430,8 @@ function show_port_icon(gobj)
|
|
|
6070
6430
|
}
|
|
6071
6431
|
let icon = create_floating_icon(
|
|
6072
6432
|
'gear', '#1890ff', xy.x, xy.y,
|
|
6073
|
-
|
|
6433
|
+
'port properties', gobj, "EV_TOGGLE_PORT_POPOVER",
|
|
6434
|
+
{node_id: priv._selected_node_id, port_key: priv._selected_port_key}
|
|
6074
6435
|
);
|
|
6075
6436
|
priv.$container.appendChild(icon);
|
|
6076
6437
|
priv._port_icon_el = icon;
|
|
@@ -6203,22 +6564,20 @@ function show_port_popover(gobj)
|
|
|
6203
6564
|
{
|
|
6204
6565
|
i18n: 'cancel',
|
|
6205
6566
|
style: BTN_STYLE_CANCEL,
|
|
6206
|
-
|
|
6207
|
-
|
|
6208
|
-
},
|
|
6567
|
+
event: "EV_CANCEL_PORT_POPOVER",
|
|
6568
|
+
kw: {node_id: node_id, port_key: port_key},
|
|
6209
6569
|
},
|
|
6210
6570
|
{
|
|
6211
6571
|
i18n: 'apply',
|
|
6212
6572
|
style: 'flex:1;padding:6px;background:#1890ff;color:#fff;border:none;' +
|
|
6213
6573
|
'border-radius:4px;cursor:pointer;font-size:13px;font-weight:500;',
|
|
6214
|
-
|
|
6215
|
-
|
|
6216
|
-
|
|
6217
|
-
|
|
6218
|
-
|
|
6219
|
-
},
|
|
6574
|
+
event: "EV_APPLY_PORT_POPOVER",
|
|
6575
|
+
/* `r_before`: what an unreadable radius falls back to; the
|
|
6576
|
+
* preview has already written the field's on the port. */
|
|
6577
|
+
kw: {node_id: node_id, port_key: port_key, r_before: orig_r},
|
|
6578
|
+
form: {shape: shapeSelect, r: rInput, scope: scopeSelect},
|
|
6220
6579
|
},
|
|
6221
|
-
]);
|
|
6580
|
+
], gobj);
|
|
6222
6581
|
|
|
6223
6582
|
priv.$container.appendChild(popover);
|
|
6224
6583
|
priv._port_popover_el = popover;
|
|
@@ -6529,8 +6888,16 @@ function try_complete_link(gobj, clicked_node_id, clicked_port_key)
|
|
|
6529
6888
|
/************************************************************
|
|
6530
6889
|
* Drag-link: drag from link icon to a hook port.
|
|
6531
6890
|
* Draws a temporary SVG line from the fkey port to the cursor.
|
|
6891
|
+
*
|
|
6892
|
+
* Run by ac_start_link_drag. The pointer's MOVES only draw the
|
|
6893
|
+
* line and the cursor -- the drag's own picture, no action, and
|
|
6894
|
+
* one event per move would bury the machine trace -- while the
|
|
6895
|
+
* END of the gesture is an event: EV_LINK_DRAG_END {client_x,
|
|
6896
|
+
* client_y, dragged} on the release, EV_LINK_DRAG_CANCEL when
|
|
6897
|
+
* the browser takes the pointer. Their actions take the drag
|
|
6898
|
+
* down (end_link_drag) and link, or leave the linking mode.
|
|
6532
6899
|
************************************************************/
|
|
6533
|
-
function start_link_drag(gobj,
|
|
6900
|
+
function start_link_drag(gobj, client_x, client_y)
|
|
6534
6901
|
{
|
|
6535
6902
|
let priv = gobj.priv;
|
|
6536
6903
|
let graph = priv.graph;
|
|
@@ -6557,8 +6924,8 @@ function start_link_drag(gobj, e)
|
|
|
6557
6924
|
let line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
|
6558
6925
|
line.setAttribute('x1', startX);
|
|
6559
6926
|
line.setAttribute('y1', startY);
|
|
6560
|
-
line.setAttribute('x2',
|
|
6561
|
-
line.setAttribute('y2',
|
|
6927
|
+
line.setAttribute('x2', client_x - containerRect.left);
|
|
6928
|
+
line.setAttribute('y2', client_y - containerRect.top);
|
|
6562
6929
|
line.setAttribute('stroke', '#52c41a');
|
|
6563
6930
|
line.setAttribute('stroke-width', '2');
|
|
6564
6931
|
line.setAttribute('stroke-dasharray', '6 3');
|
|
@@ -6582,60 +6949,61 @@ function start_link_drag(gobj, e)
|
|
|
6582
6949
|
}
|
|
6583
6950
|
|
|
6584
6951
|
function onPointerUp(ev) {
|
|
6585
|
-
|
|
6586
|
-
|
|
6587
|
-
|
|
6588
|
-
|
|
6589
|
-
|
|
6590
|
-
// Remove drag line
|
|
6591
|
-
if(priv._link_drag_svg) {
|
|
6592
|
-
priv._link_drag_svg.remove();
|
|
6593
|
-
priv._link_drag_svg = null;
|
|
6594
|
-
}
|
|
6595
|
-
|
|
6596
|
-
if(!dragged) {
|
|
6597
|
-
// It was a click, not a drag — let the click handler deal with it
|
|
6598
|
-
return;
|
|
6599
|
-
}
|
|
6600
|
-
|
|
6601
|
-
// Check if dropped on a valid hook port
|
|
6602
|
-
let cx = ev.clientX - containerRect.left;
|
|
6603
|
-
let cy = ev.clientY - containerRect.top;
|
|
6604
|
-
let canvasPt = graph.getCanvasByViewport([cx, cy]);
|
|
6605
|
-
let target = find_hook_at_point(gobj, canvasPt[0], canvasPt[1]);
|
|
6606
|
-
|
|
6607
|
-
if(target) {
|
|
6608
|
-
try_complete_link(gobj, target.node_id, target.port_key);
|
|
6609
|
-
} else {
|
|
6610
|
-
exit_linking_mode(gobj);
|
|
6611
|
-
}
|
|
6952
|
+
gobj_send_event(gobj, "EV_LINK_DRAG_END", {
|
|
6953
|
+
client_x: ev.clientX,
|
|
6954
|
+
client_y: ev.clientY,
|
|
6955
|
+
dragged: dragged,
|
|
6956
|
+
}, gobj);
|
|
6612
6957
|
}
|
|
6613
6958
|
|
|
6614
|
-
/* A cancelled pointer (the browser took the touch) is no drop
|
|
6615
|
-
* listeners go, and what was there stays. Without it they stayed
|
|
6616
|
-
* on `document` until some later pointerup, which then dropped. */
|
|
6959
|
+
/* A cancelled pointer (the browser took the touch) is no drop. */
|
|
6617
6960
|
function onPointerCancel(ev) {
|
|
6961
|
+
gobj_send_event(gobj, "EV_LINK_DRAG_CANCEL", {}, gobj);
|
|
6962
|
+
}
|
|
6963
|
+
|
|
6964
|
+
function unwire() {
|
|
6618
6965
|
document.removeEventListener('pointermove', onPointerMove);
|
|
6619
6966
|
document.removeEventListener('pointerup', onPointerUp);
|
|
6620
6967
|
document.removeEventListener('pointercancel', onPointerCancel);
|
|
6621
|
-
priv.$container.style.cursor = '';
|
|
6622
|
-
if(priv._link_drag_svg) {
|
|
6623
|
-
priv._link_drag_svg.remove();
|
|
6624
|
-
priv._link_drag_svg = null;
|
|
6625
|
-
}
|
|
6626
|
-
exit_linking_mode(gobj);
|
|
6627
6968
|
}
|
|
6628
6969
|
|
|
6629
6970
|
document.addEventListener('pointermove', onPointerMove);
|
|
6630
6971
|
document.addEventListener('pointerup', onPointerUp);
|
|
6631
6972
|
document.addEventListener('pointercancel', onPointerCancel);
|
|
6632
|
-
|
|
6633
|
-
|
|
6634
|
-
|
|
6635
|
-
|
|
6636
|
-
|
|
6637
|
-
|
|
6973
|
+
|
|
6974
|
+
/* Where the line is measured from, for the drop, and how to take
|
|
6975
|
+
* the listeners off. A view destroyed in the middle of the drag
|
|
6976
|
+
* takes them off too (mt_destroy, through `_drag_unwire`): they
|
|
6977
|
+
* are closures over this gobj, on `document`. */
|
|
6978
|
+
priv._link_drag = {
|
|
6979
|
+
left: containerRect.left,
|
|
6980
|
+
top: containerRect.top,
|
|
6981
|
+
unwire: unwire,
|
|
6638
6982
|
};
|
|
6983
|
+
priv._drag_unwire = unwire;
|
|
6984
|
+
}
|
|
6985
|
+
|
|
6986
|
+
/************************************************************
|
|
6987
|
+
* Take a link drag down: its listeners, its line, its cursor.
|
|
6988
|
+
* What was linked, or not, is the caller's.
|
|
6989
|
+
************************************************************/
|
|
6990
|
+
function end_link_drag(gobj)
|
|
6991
|
+
{
|
|
6992
|
+
let priv = gobj.priv;
|
|
6993
|
+
let drag = priv._link_drag;
|
|
6994
|
+
|
|
6995
|
+
priv._link_drag = null;
|
|
6996
|
+
if(drag) {
|
|
6997
|
+
drag.unwire();
|
|
6998
|
+
if(priv._drag_unwire === drag.unwire) {
|
|
6999
|
+
priv._drag_unwire = null;
|
|
7000
|
+
}
|
|
7001
|
+
}
|
|
7002
|
+
priv.$container.style.cursor = '';
|
|
7003
|
+
if(priv._link_drag_svg) {
|
|
7004
|
+
priv._link_drag_svg.remove();
|
|
7005
|
+
priv._link_drag_svg = null;
|
|
7006
|
+
}
|
|
6639
7007
|
}
|
|
6640
7008
|
|
|
6641
7009
|
/************************************************************
|
|
@@ -7015,9 +7383,9 @@ function show_edge_icon(gobj)
|
|
|
7015
7383
|
}
|
|
7016
7384
|
|
|
7017
7385
|
let icons = show_dual_icons(gobj, mid.x + 4, mid.y + floating_icon_dy(),
|
|
7018
|
-
|
|
7019
|
-
|
|
7020
|
-
|
|
7386
|
+
'edge properties', "EV_TOGGLE_EDGE_POPOVER",
|
|
7387
|
+
'unlink', "EV_REQUEST_UNLINK_EDGE",
|
|
7388
|
+
{edge_id: priv._selected_edge_id}
|
|
7021
7389
|
);
|
|
7022
7390
|
priv._edge_icon_el = icons.icon_el;
|
|
7023
7391
|
priv._edge_delete_el = icons.delete_el;
|
|
@@ -7155,24 +7523,18 @@ function show_edge_popover(gobj)
|
|
|
7155
7523
|
{
|
|
7156
7524
|
i18n: 'cancel',
|
|
7157
7525
|
style: BTN_STYLE_CANCEL,
|
|
7158
|
-
|
|
7159
|
-
|
|
7160
|
-
},
|
|
7526
|
+
event: "EV_CANCEL_EDGE_POPOVER",
|
|
7527
|
+
kw: {edge_id: edge_id},
|
|
7161
7528
|
},
|
|
7162
7529
|
{
|
|
7163
7530
|
i18n: 'apply',
|
|
7164
7531
|
style: 'flex:1;padding:6px;background:#52c41a;color:#fff;border:none;' +
|
|
7165
7532
|
'border-radius:4px;cursor:pointer;font-size:13px;font-weight:500;',
|
|
7166
|
-
|
|
7167
|
-
|
|
7168
|
-
|
|
7169
|
-
parseInt(lwInput.value) || 2,
|
|
7170
|
-
colorInput.value,
|
|
7171
|
-
scopeSelect.value
|
|
7172
|
-
);
|
|
7173
|
-
},
|
|
7533
|
+
event: "EV_APPLY_EDGE_POPOVER",
|
|
7534
|
+
kw: {edge_id: edge_id},
|
|
7535
|
+
form: {line_width: lwInput, color: colorInput, scope: scopeSelect},
|
|
7174
7536
|
},
|
|
7175
|
-
]);
|
|
7537
|
+
], gobj);
|
|
7176
7538
|
|
|
7177
7539
|
priv.$container.appendChild(popover);
|
|
7178
7540
|
priv._edge_popover_el = popover;
|
|
@@ -7243,9 +7605,9 @@ function show_node_icon(gobj)
|
|
|
7243
7605
|
}
|
|
7244
7606
|
|
|
7245
7607
|
let icons = show_dual_icons(gobj, rect.right + 4, rect.top + floating_icon_dy(),
|
|
7246
|
-
|
|
7247
|
-
|
|
7248
|
-
|
|
7608
|
+
'node properties', "EV_TOGGLE_NODE_POPOVER",
|
|
7609
|
+
'delete node', "EV_REQUEST_DELETE_NODE",
|
|
7610
|
+
{node_id: priv._selected_node_id}
|
|
7249
7611
|
);
|
|
7250
7612
|
priv._node_icon_el = icons.icon_el;
|
|
7251
7613
|
priv._node_delete_el = icons.delete_el;
|
|
@@ -7404,29 +7766,23 @@ function show_node_popover(gobj)
|
|
|
7404
7766
|
{
|
|
7405
7767
|
i18n: 'cancel',
|
|
7406
7768
|
style: BTN_STYLE_CANCEL,
|
|
7407
|
-
|
|
7408
|
-
|
|
7409
|
-
},
|
|
7769
|
+
event: "EV_CANCEL_NODE_POPOVER",
|
|
7770
|
+
kw: {node_id: node_id},
|
|
7410
7771
|
},
|
|
7411
7772
|
{
|
|
7412
7773
|
i18n: 'apply',
|
|
7413
7774
|
style: 'flex:1;padding:6px;background:#1890ff;color:#fff;border:none;' +
|
|
7414
7775
|
'border-radius:4px;cursor:pointer;font-size:13px;font-weight:500;',
|
|
7415
|
-
|
|
7416
|
-
|
|
7417
|
-
|
|
7418
|
-
|
|
7419
|
-
|
|
7420
|
-
|
|
7421
|
-
|
|
7422
|
-
/* Only a figure somebody CHOSE is written: the
|
|
7423
|
-
* one the select opened on is today's default,
|
|
7424
|
-
* and saving it would freeze it on the node. */
|
|
7425
|
-
(shapeSelect.value !== orig_figure)? shapeSelect.value : ""
|
|
7426
|
-
);
|
|
7776
|
+
event: "EV_APPLY_NODE_POPOVER",
|
|
7777
|
+
/* `shape_before`: the figure the select opened on, which
|
|
7778
|
+
* the action does not write (see ac_apply_node_popover). */
|
|
7779
|
+
kw: {node_id: node_id, shape_before: orig_figure},
|
|
7780
|
+
form: {
|
|
7781
|
+
fill: fillInput, stroke: strokeInput, line_width: lwInput,
|
|
7782
|
+
scope: scopeSelect, shape: shapeSelect
|
|
7427
7783
|
},
|
|
7428
7784
|
},
|
|
7429
|
-
]);
|
|
7785
|
+
], gobj);
|
|
7430
7786
|
|
|
7431
7787
|
priv.$container.appendChild(popover);
|
|
7432
7788
|
priv._node_popover_el = popover;
|
|
@@ -7454,6 +7810,49 @@ function hide_node_detail(gobj)
|
|
|
7454
7810
|
hide_overlay(gobj, '_node_detail_el');
|
|
7455
7811
|
}
|
|
7456
7812
|
|
|
7813
|
+
/************************************************************
|
|
7814
|
+
* The ✕ of the detail popover. Built as an element and not in
|
|
7815
|
+
* the popover's markup, so that it is a whole button: named
|
|
7816
|
+
* with its key (a language switch renames it), reachable by
|
|
7817
|
+
* the keyboard and pressed by Enter or Space as by the pointer.
|
|
7818
|
+
* Its click sends EV_CLOSE_NODE_DETAIL {node_id} and nothing
|
|
7819
|
+
* else; a pointer press does not take the focus.
|
|
7820
|
+
************************************************************/
|
|
7821
|
+
function create_detail_close(gobj, color, node_id)
|
|
7822
|
+
{
|
|
7823
|
+
let el = document.createElement('div');
|
|
7824
|
+
el.className = 'g6-detail-close';
|
|
7825
|
+
el.setAttribute('role', 'button');
|
|
7826
|
+
el.setAttribute('tabindex', '0');
|
|
7827
|
+
el.setAttribute('title', t('close'));
|
|
7828
|
+
el.setAttribute('data-i18n-title', 'close');
|
|
7829
|
+
el.setAttribute('aria-label', t('close'));
|
|
7830
|
+
el.setAttribute('data-i18n-aria-label', 'close');
|
|
7831
|
+
el.style.color = color;
|
|
7832
|
+
el.textContent = '×';
|
|
7833
|
+
el.addEventListener('click', (e) => {
|
|
7834
|
+
e.stopPropagation();
|
|
7835
|
+
gobj_send_event(gobj, "EV_CLOSE_NODE_DETAIL", {node_id: node_id}, gobj);
|
|
7836
|
+
});
|
|
7837
|
+
keep_focus_on_press(el);
|
|
7838
|
+
click_on_enter_or_space(el);
|
|
7839
|
+
return el;
|
|
7840
|
+
}
|
|
7841
|
+
|
|
7842
|
+
/* A `div` of the detail popover: its class, and its text as a
|
|
7843
|
+
* text node. */
|
|
7844
|
+
function detail_div(class_name, text)
|
|
7845
|
+
{
|
|
7846
|
+
let el = document.createElement('div');
|
|
7847
|
+
if(class_name) {
|
|
7848
|
+
el.className = class_name;
|
|
7849
|
+
}
|
|
7850
|
+
if(text !== undefined) {
|
|
7851
|
+
el.textContent = text;
|
|
7852
|
+
}
|
|
7853
|
+
return el;
|
|
7854
|
+
}
|
|
7855
|
+
|
|
7457
7856
|
function show_node_detail_popover(gobj, node_id)
|
|
7458
7857
|
{
|
|
7459
7858
|
let priv = gobj.priv;
|
|
@@ -7501,7 +7900,30 @@ function show_node_detail_popover(gobj, node_id)
|
|
|
7501
7900
|
"-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, " +
|
|
7502
7901
|
"Helvetica, Arial, sans-serif";
|
|
7503
7902
|
|
|
7504
|
-
|
|
7903
|
+
/*
|
|
7904
|
+
* Built as elements, every value a TEXT node: an id, a topic, a
|
|
7905
|
+
* field and its value are data, never markup.
|
|
7906
|
+
*
|
|
7907
|
+
* The head is STICKY: the box scrolls, and with the head scrolling
|
|
7908
|
+
* away the ✕ left the screen -- on a phone, where the box is most
|
|
7909
|
+
* of the screen, that is a popover you cannot close without
|
|
7910
|
+
* scrolling back up for the button. Its background is the
|
|
7911
|
+
* popover's own, or the rows would show through it.
|
|
7912
|
+
*/
|
|
7913
|
+
let head = detail_div('g6-detail-head');
|
|
7914
|
+
head.style.background = bg;
|
|
7915
|
+
let names = detail_div('');
|
|
7916
|
+
names.style.flex = '1 1 auto';
|
|
7917
|
+
names.style.minWidth = '0';
|
|
7918
|
+
names.appendChild(detail_div('g6-detail-id', id));
|
|
7919
|
+
let topic_el = detail_div('g6-detail-topic', topic);
|
|
7920
|
+
topic_el.style.color = sub;
|
|
7921
|
+
names.appendChild(topic_el);
|
|
7922
|
+
head.appendChild(names);
|
|
7923
|
+
head.appendChild(create_detail_close(gobj, sub, node_id));
|
|
7924
|
+
popover.appendChild(head);
|
|
7925
|
+
|
|
7926
|
+
let n_rows = 0;
|
|
7505
7927
|
for(let key of Object.keys(record)) {
|
|
7506
7928
|
if(key.indexOf('__') === 0) { // skip internal metadata
|
|
7507
7929
|
continue;
|
|
@@ -7518,42 +7940,20 @@ function show_node_detail_popover(gobj, node_id)
|
|
|
7518
7940
|
if(vs.length > 240) {
|
|
7519
7941
|
vs = vs.slice(0, 240) + '…';
|
|
7520
7942
|
}
|
|
7521
|
-
|
|
7522
|
-
|
|
7523
|
-
|
|
7524
|
-
|
|
7525
|
-
|
|
7526
|
-
|
|
7943
|
+
let row = detail_div('g6-detail-row');
|
|
7944
|
+
row.style.borderTop = `1px solid ${rowbd}`;
|
|
7945
|
+
let key_el = detail_div('g6-detail-key', key);
|
|
7946
|
+
key_el.style.color = sub;
|
|
7947
|
+
row.appendChild(key_el);
|
|
7948
|
+
row.appendChild(detail_div('g6-detail-val', vs));
|
|
7949
|
+
popover.appendChild(row);
|
|
7950
|
+
n_rows++;
|
|
7527
7951
|
}
|
|
7528
|
-
|
|
7529
|
-
|
|
7530
|
-
|
|
7531
|
-
|
|
7532
|
-
|
|
7533
|
-
* scrolling back up for the button. Its background is the
|
|
7534
|
-
* popover's own, or the rows would show through it.
|
|
7535
|
-
*/
|
|
7536
|
-
popover.innerHTML =
|
|
7537
|
-
`<div class="g6-detail-head" style="background:${bg};">` +
|
|
7538
|
-
`<div style="flex:1 1 auto;min-width:0;">` +
|
|
7539
|
-
`<div class="g6-detail-id">${escapeHtml(id)}</div>` +
|
|
7540
|
-
`<div class="g6-detail-topic" style="color:${sub};">` +
|
|
7541
|
-
`${escapeHtml(topic)}</div>` +
|
|
7542
|
-
`</div>` +
|
|
7543
|
-
`<div class="g6-detail-close" role="button" tabindex="0"` +
|
|
7544
|
-
` aria-label="${escapeHtml(t('close'))}"` +
|
|
7545
|
-
` title="${escapeHtml(t('close'))}"` +
|
|
7546
|
-
` style="color:${sub};">×</div>` +
|
|
7547
|
-
`</div>` +
|
|
7548
|
-
(rows || `<div class="g6-detail-empty" style="color:${sub};">` +
|
|
7549
|
-
`${escapeHtml(t('no fields'))}</div>`);
|
|
7550
|
-
|
|
7551
|
-
let close_el = popover.querySelector('.g6-detail-close');
|
|
7552
|
-
if(close_el) {
|
|
7553
|
-
close_el.addEventListener('click', (e) => {
|
|
7554
|
-
e.stopPropagation();
|
|
7555
|
-
hide_node_detail(gobj);
|
|
7556
|
-
});
|
|
7952
|
+
if(!n_rows) {
|
|
7953
|
+
let empty = detail_div('g6-detail-empty', t('no fields'));
|
|
7954
|
+
empty.setAttribute('data-i18n', 'no fields');
|
|
7955
|
+
empty.style.color = sub;
|
|
7956
|
+
popover.appendChild(empty);
|
|
7557
7957
|
}
|
|
7558
7958
|
|
|
7559
7959
|
priv.$container.appendChild(popover);
|
|
@@ -8373,10 +8773,10 @@ function request_delete_selection(gobj)
|
|
|
8373
8773
|
show_confirm_popover(gobj, null,
|
|
8374
8774
|
delete_question(nodes),
|
|
8375
8775
|
'delete', '#ff4d4f',
|
|
8376
|
-
|
|
8776
|
+
{
|
|
8377
8777
|
what: "delete_selection",
|
|
8378
8778
|
ids: nodes.map((nd) => String(nd.id))
|
|
8379
|
-
},
|
|
8779
|
+
},
|
|
8380
8780
|
'_delete_confirm_el'
|
|
8381
8781
|
);
|
|
8382
8782
|
}
|
|
@@ -8413,10 +8813,10 @@ function show_delete_confirm(gobj, nodeData)
|
|
|
8413
8813
|
show_confirm_popover(gobj, priv._node_delete_el,
|
|
8414
8814
|
delete_question([nodeData]),
|
|
8415
8815
|
'delete', '#ff4d4f',
|
|
8416
|
-
|
|
8816
|
+
{
|
|
8417
8817
|
what: "delete_node",
|
|
8418
8818
|
id: String(nodeData.id)
|
|
8419
|
-
},
|
|
8819
|
+
},
|
|
8420
8820
|
'_delete_confirm_el'
|
|
8421
8821
|
);
|
|
8422
8822
|
}
|
|
@@ -8498,6 +8898,7 @@ function show_create_popover(gobj)
|
|
|
8498
8898
|
popover.appendChild(topicLabel);
|
|
8499
8899
|
|
|
8500
8900
|
let topicSelect = document.createElement('select');
|
|
8901
|
+
topicSelect.className = 'G6_CREATE_TOPIC';
|
|
8501
8902
|
topicSelect.setAttribute('title', t('topic'));
|
|
8502
8903
|
topicSelect.setAttribute('data-i18n-title', 'topic');
|
|
8503
8904
|
topicSelect.setAttribute('aria-label', t('topic'));
|
|
@@ -8547,6 +8948,7 @@ function show_create_popover(gobj)
|
|
|
8547
8948
|
popover.appendChild(idLabel);
|
|
8548
8949
|
|
|
8549
8950
|
let idInput = document.createElement('input');
|
|
8951
|
+
idInput.className = 'G6_CREATE_ID';
|
|
8550
8952
|
idInput.type = 'text';
|
|
8551
8953
|
idInput.placeholder = t('node id');
|
|
8552
8954
|
idInput.setAttribute('data-i18n-placeholder', 'node id');
|
|
@@ -8562,48 +8964,39 @@ function show_create_popover(gobj)
|
|
|
8562
8964
|
|
|
8563
8965
|
// Error message area
|
|
8564
8966
|
let errorEl = document.createElement('div');
|
|
8967
|
+
errorEl.className = 'G6_CREATE_ERROR';
|
|
8565
8968
|
errorEl.style.cssText =
|
|
8566
8969
|
'color:#ff4d4f;font-size:12px;margin-bottom:8px;display:none;';
|
|
8567
8970
|
popover.appendChild(errorEl);
|
|
8568
8971
|
|
|
8569
|
-
|
|
8570
|
-
|
|
8571
|
-
create_form_button_row(popover, [
|
|
8972
|
+
/* Create sends what the form holds; an empty id is answered by
|
|
8973
|
+
* the action (ac_submit_new_node), in the popover. */
|
|
8974
|
+
let btnRow = create_form_button_row(popover, [
|
|
8572
8975
|
{
|
|
8573
8976
|
i18n: 'cancel',
|
|
8574
8977
|
style: BTN_STYLE_CANCEL,
|
|
8575
|
-
|
|
8978
|
+
event: "EV_CLOSE_CREATE_POPOVER",
|
|
8979
|
+
kw: {},
|
|
8576
8980
|
},
|
|
8577
8981
|
{
|
|
8578
8982
|
i18n: 'create',
|
|
8579
8983
|
style: 'flex:1;padding:6px;background:#1890ff;color:#fff;border:none;' +
|
|
8580
8984
|
'border-radius:4px;cursor:pointer;font-size:13px;font-weight:500;',
|
|
8581
|
-
|
|
8582
|
-
|
|
8583
|
-
|
|
8584
|
-
errorEl.textContent = t('node id required');
|
|
8585
|
-
errorEl.setAttribute('data-i18n', 'node id required');
|
|
8586
|
-
errorEl.style.display = 'block';
|
|
8587
|
-
idInput.focus();
|
|
8588
|
-
return;
|
|
8589
|
-
}
|
|
8590
|
-
gobj_send_event(gobj, "EV_SUBMIT_NEW_NODE", {
|
|
8591
|
-
topic_name: String(topicSelect.value),
|
|
8592
|
-
id: node_id
|
|
8593
|
-
}, gobj);
|
|
8594
|
-
},
|
|
8985
|
+
event: "EV_SUBMIT_NEW_NODE",
|
|
8986
|
+
kw: {},
|
|
8987
|
+
form: {topic_name: topicSelect, id: idInput},
|
|
8595
8988
|
},
|
|
8596
|
-
]);
|
|
8597
|
-
createBtn =
|
|
8989
|
+
], gobj);
|
|
8990
|
+
let createBtn = btnRow.lastElementChild; // for the Enter key
|
|
8598
8991
|
|
|
8599
|
-
// Enter key submits
|
|
8992
|
+
// Enter key submits, Escape closes: the two buttons' events
|
|
8600
8993
|
idInput.addEventListener('keydown', (e) => {
|
|
8601
8994
|
if(e.key === 'Enter') {
|
|
8602
8995
|
e.preventDefault();
|
|
8603
8996
|
createBtn.click();
|
|
8604
8997
|
} else if(e.key === 'Escape') {
|
|
8605
8998
|
e.preventDefault();
|
|
8606
|
-
|
|
8999
|
+
gobj_send_event(gobj, "EV_CLOSE_CREATE_POPOVER", {}, gobj);
|
|
8607
9000
|
}
|
|
8608
9001
|
});
|
|
8609
9002
|
|
|
@@ -8696,10 +9089,10 @@ function show_unlink_confirm(gobj, edgeData)
|
|
|
8696
9089
|
t('unlink') + ' ' + d.child_id + ' → ' + d.parent_id + '?' +
|
|
8697
9090
|
'\n' + t('neither record is deleted'),
|
|
8698
9091
|
'unlink', '#ff4d4f',
|
|
8699
|
-
|
|
9092
|
+
{
|
|
8700
9093
|
what: "unlink_edge",
|
|
8701
9094
|
id: String(edgeData.id)
|
|
8702
|
-
},
|
|
9095
|
+
},
|
|
8703
9096
|
'_unlink_confirm_el'
|
|
8704
9097
|
);
|
|
8705
9098
|
}
|
|
@@ -10696,10 +11089,7 @@ function ac_save_graph(gobj, event, kw, src)
|
|
|
10696
11089
|
let priv = gobj.priv;
|
|
10697
11090
|
|
|
10698
11091
|
if(priv.edit_mode) {
|
|
10699
|
-
|
|
10700
|
-
|
|
10701
|
-
disableElements($container, ".EV_SAVE_GRAPH");
|
|
10702
|
-
set_submit_state($container, ".EV_SAVE_GRAPH", false);
|
|
11092
|
+
light_save_button(gobj, false);
|
|
10703
11093
|
|
|
10704
11094
|
let history = graph_get_plugin(gobj, "history");
|
|
10705
11095
|
if(history) {
|
|
@@ -11161,17 +11551,27 @@ function ac_center_anchor(gobj, event, kw, src)
|
|
|
11161
11551
|
}
|
|
11162
11552
|
|
|
11163
11553
|
/************************************************************
|
|
11164
|
-
* What the anchor button says it will do next
|
|
11554
|
+
* What the anchor button says it will do next: its `title`,
|
|
11555
|
+
* and the `title_key` it is translated from.
|
|
11165
11556
|
************************************************************/
|
|
11166
11557
|
function anchor_title(state)
|
|
11167
11558
|
{
|
|
11168
11559
|
if(state === "arming") {
|
|
11169
|
-
return
|
|
11560
|
+
return {
|
|
11561
|
+
title: t('click the element to centre on'),
|
|
11562
|
+
title_key: 'click the element to centre on'
|
|
11563
|
+
};
|
|
11170
11564
|
}
|
|
11171
11565
|
if(state === "on") {
|
|
11172
|
-
return
|
|
11566
|
+
return {
|
|
11567
|
+
title: t('centred: click to release'),
|
|
11568
|
+
title_key: 'centred: click to release'
|
|
11569
|
+
};
|
|
11173
11570
|
}
|
|
11174
|
-
return
|
|
11571
|
+
return {
|
|
11572
|
+
title: t('anchor view'),
|
|
11573
|
+
title_key: 'anchor view'
|
|
11574
|
+
};
|
|
11175
11575
|
}
|
|
11176
11576
|
|
|
11177
11577
|
/************************************************************
|
|
@@ -11702,11 +12102,15 @@ function ac_set_operation_mode(gobj, event, kw, src)
|
|
|
11702
12102
|
{
|
|
11703
12103
|
gobj_write_attr(gobj, "operation_mode", kw.operation_mode);
|
|
11704
12104
|
configure_behaviour(gobj);
|
|
11705
|
-
update_toolbar(gobj);
|
|
11706
12105
|
|
|
11707
12106
|
/* `configure_behaviour` is what decides `edit_mode`, and the
|
|
11708
|
-
* history plugin belongs to that decision.
|
|
12107
|
+
* history plugin belongs to that decision. BEFORE the toolbars:
|
|
12108
|
+
* the edit toolbar entering edition reads its Save from what
|
|
12109
|
+
* update_history_buttons() says of the new history -- after
|
|
12110
|
+
* it, the toolbar was made with what the Save was told in the
|
|
12111
|
+
* LAST edition. */
|
|
11709
12112
|
sync_history_plugin(gobj);
|
|
12113
|
+
update_toolbar(gobj);
|
|
11710
12114
|
return 0;
|
|
11711
12115
|
}
|
|
11712
12116
|
|
|
@@ -11991,31 +12395,181 @@ function ac_context_menu_item(gobj, event, kw, src)
|
|
|
11991
12395
|
}
|
|
11992
12396
|
|
|
11993
12397
|
/************************************************************
|
|
11994
|
-
*
|
|
12398
|
+
* Is the identity a floating button was drawn with still what
|
|
12399
|
+
* is selected?
|
|
12400
|
+
*
|
|
12401
|
+
* The buttons are removed with their selection and drawn again
|
|
12402
|
+
* for the next one, so a click carrying another identity is a
|
|
12403
|
+
* broken invariant -- said loudly, never acted on: it would
|
|
12404
|
+
* delete, unlink or edit what the reader is not looking at.
|
|
12405
|
+
************************************************************/
|
|
12406
|
+
function floating_button_is_current(gobj, event, kw)
|
|
12407
|
+
{
|
|
12408
|
+
let priv = gobj.priv;
|
|
12409
|
+
let want = {
|
|
12410
|
+
node_id: priv._selected_node_id || "",
|
|
12411
|
+
port_key: priv._selected_port_key || "",
|
|
12412
|
+
edge_id: priv._selected_edge_id || "",
|
|
12413
|
+
};
|
|
12414
|
+
let keys = Object.keys(kw || {});
|
|
12415
|
+
let matches = keys.length > 0 && keys.every((k) => {
|
|
12416
|
+
return Object.prototype.hasOwnProperty.call(want, k) &&
|
|
12417
|
+
!!kw[k] && kw[k] === want[k];
|
|
12418
|
+
});
|
|
12419
|
+
if(!matches) {
|
|
12420
|
+
log_error(`${gobj_short_name(gobj)}: ${event} for ${JSON.stringify(kw || {})}, ` +
|
|
12421
|
+
`but the selection is ${JSON.stringify(want)}`);
|
|
12422
|
+
}
|
|
12423
|
+
return matches;
|
|
12424
|
+
}
|
|
12425
|
+
|
|
12426
|
+
/************************************************************
|
|
12427
|
+
* {node_id} -- the delete icon floating beside the selected
|
|
12428
|
+
* node.
|
|
11995
12429
|
************************************************************/
|
|
11996
12430
|
function ac_request_delete_node(gobj, event, kw, src)
|
|
11997
12431
|
{
|
|
12432
|
+
if(!floating_button_is_current(gobj, event, kw)) {
|
|
12433
|
+
return -1; // Error already logged
|
|
12434
|
+
}
|
|
11998
12435
|
request_delete_node(gobj);
|
|
11999
12436
|
return 0;
|
|
12000
12437
|
}
|
|
12001
12438
|
|
|
12002
12439
|
/************************************************************
|
|
12003
|
-
*
|
|
12440
|
+
* {edge_id} -- the unlink icon floating beside the selected
|
|
12441
|
+
* edge.
|
|
12004
12442
|
************************************************************/
|
|
12005
12443
|
function ac_request_unlink_edge(gobj, event, kw, src)
|
|
12006
12444
|
{
|
|
12445
|
+
if(!floating_button_is_current(gobj, event, kw)) {
|
|
12446
|
+
return -1; // Error already logged
|
|
12447
|
+
}
|
|
12007
12448
|
request_unlink_edge(gobj);
|
|
12008
12449
|
return 0;
|
|
12009
12450
|
}
|
|
12010
12451
|
|
|
12452
|
+
/************************************************************
|
|
12453
|
+
* {node_id} -- the gear beside the selected node: its
|
|
12454
|
+
* properties popover, opened or closed.
|
|
12455
|
+
************************************************************/
|
|
12456
|
+
function ac_toggle_node_popover(gobj, event, kw, src)
|
|
12457
|
+
{
|
|
12458
|
+
if(!floating_button_is_current(gobj, event, kw)) {
|
|
12459
|
+
return -1; // Error already logged
|
|
12460
|
+
}
|
|
12461
|
+
toggle_node_popover(gobj);
|
|
12462
|
+
return 0;
|
|
12463
|
+
}
|
|
12464
|
+
|
|
12465
|
+
/************************************************************
|
|
12466
|
+
* {edge_id} -- the gear beside the selected edge.
|
|
12467
|
+
************************************************************/
|
|
12468
|
+
function ac_toggle_edge_popover(gobj, event, kw, src)
|
|
12469
|
+
{
|
|
12470
|
+
if(!floating_button_is_current(gobj, event, kw)) {
|
|
12471
|
+
return -1; // Error already logged
|
|
12472
|
+
}
|
|
12473
|
+
toggle_edge_popover(gobj);
|
|
12474
|
+
return 0;
|
|
12475
|
+
}
|
|
12476
|
+
|
|
12477
|
+
/************************************************************
|
|
12478
|
+
* {node_id, port_key} -- the gear beside the selected port.
|
|
12479
|
+
************************************************************/
|
|
12480
|
+
function ac_toggle_port_popover(gobj, event, kw, src)
|
|
12481
|
+
{
|
|
12482
|
+
if(!floating_button_is_current(gobj, event, kw)) {
|
|
12483
|
+
return -1; // Error already logged
|
|
12484
|
+
}
|
|
12485
|
+
toggle_port_popover(gobj);
|
|
12486
|
+
return 0;
|
|
12487
|
+
}
|
|
12488
|
+
|
|
12489
|
+
/************************************************************
|
|
12490
|
+
* {node_id, port_key} -- the link icon of a selected fkey port,
|
|
12491
|
+
* CLICKED (a press that drags enters the mode on its own, from
|
|
12492
|
+
* start_link_drag): the compatible hooks light up, and the next
|
|
12493
|
+
* click on one of them links.
|
|
12494
|
+
************************************************************/
|
|
12495
|
+
function ac_enter_linking_mode(gobj, event, kw, src)
|
|
12496
|
+
{
|
|
12497
|
+
let priv = gobj.priv;
|
|
12498
|
+
|
|
12499
|
+
if(!floating_button_is_current(gobj, event, kw)) {
|
|
12500
|
+
return -1; // Error already logged
|
|
12501
|
+
}
|
|
12502
|
+
if(!priv._linking_mode) {
|
|
12503
|
+
enter_linking_mode(gobj);
|
|
12504
|
+
}
|
|
12505
|
+
return 0;
|
|
12506
|
+
}
|
|
12507
|
+
|
|
12508
|
+
/************************************************************
|
|
12509
|
+
* {node_id} -- the ✕ of the detail popover.
|
|
12510
|
+
************************************************************/
|
|
12511
|
+
function ac_close_node_detail(gobj, event, kw, src)
|
|
12512
|
+
{
|
|
12513
|
+
let priv = gobj.priv;
|
|
12514
|
+
let node_id = (kw && kw.node_id) || "";
|
|
12515
|
+
|
|
12516
|
+
if(!priv._node_detail_el || !node_id || node_id !== priv._detail_node_id) {
|
|
12517
|
+
log_error(`${gobj_short_name(gobj)}: ${event} for '${node_id}', ` +
|
|
12518
|
+
`but the detail popover shows '${priv._detail_node_id || ""}'`);
|
|
12519
|
+
return -1;
|
|
12520
|
+
}
|
|
12521
|
+
hide_node_detail(gobj);
|
|
12522
|
+
return 0;
|
|
12523
|
+
}
|
|
12524
|
+
|
|
12525
|
+
/************************************************************
|
|
12526
|
+
* {} -- the create button of the edit toolbar: the create
|
|
12527
|
+
* popover, opened or closed. Only in edition, where that
|
|
12528
|
+
* toolbar is.
|
|
12529
|
+
************************************************************/
|
|
12530
|
+
function ac_toggle_create_popover(gobj, event, kw, src)
|
|
12531
|
+
{
|
|
12532
|
+
let priv = gobj.priv;
|
|
12533
|
+
|
|
12534
|
+
if(!priv.edit_mode) {
|
|
12535
|
+
log_error(`${gobj_short_name(gobj)}: ${event} outside edition`);
|
|
12536
|
+
return -1;
|
|
12537
|
+
}
|
|
12538
|
+
toggle_create_popover(gobj);
|
|
12539
|
+
return 0;
|
|
12540
|
+
}
|
|
12541
|
+
|
|
12542
|
+
/************************************************************
|
|
12543
|
+
* {} -- the cancel of the create popover, or Escape in its id.
|
|
12544
|
+
************************************************************/
|
|
12545
|
+
function ac_close_create_popover(gobj, event, kw, src)
|
|
12546
|
+
{
|
|
12547
|
+
let priv = gobj.priv;
|
|
12548
|
+
|
|
12549
|
+
if(!priv._create_popover_el) {
|
|
12550
|
+
log_error(`${gobj_short_name(gobj)}: ${event} with no create popover open`);
|
|
12551
|
+
return -1;
|
|
12552
|
+
}
|
|
12553
|
+
hide_create_popover(gobj);
|
|
12554
|
+
return 0;
|
|
12555
|
+
}
|
|
12556
|
+
|
|
12011
12557
|
/************************************************************
|
|
12012
12558
|
* {topic_name, id} -- the create popover was filled in.
|
|
12559
|
+
*
|
|
12560
|
+
* An empty id with the popover open is the person's to fix,
|
|
12561
|
+
* not a fault: it is said IN the popover, where they typed.
|
|
12013
12562
|
************************************************************/
|
|
12014
12563
|
function ac_submit_new_node(gobj, event, kw, src)
|
|
12015
12564
|
{
|
|
12016
|
-
let
|
|
12017
|
-
let
|
|
12565
|
+
let priv = gobj.priv;
|
|
12566
|
+
let topic_name = (kw && kw.topic_name)? String(kw.topic_name) : "";
|
|
12567
|
+
let node_id = (kw && kw.id)? String(kw.id).trim() : "";
|
|
12018
12568
|
|
|
12569
|
+
if(topic_name && !node_id && priv._create_popover_el) {
|
|
12570
|
+
say_node_id_required(gobj);
|
|
12571
|
+
return 0;
|
|
12572
|
+
}
|
|
12019
12573
|
if(!topic_name || !node_id) {
|
|
12020
12574
|
log_error(`${gobj_short_name(gobj)}: ${event} without topic or id`);
|
|
12021
12575
|
return -1;
|
|
@@ -12024,6 +12578,248 @@ function ac_submit_new_node(gobj, event, kw, src)
|
|
|
12024
12578
|
return 0;
|
|
12025
12579
|
}
|
|
12026
12580
|
|
|
12581
|
+
/* "node id required", under the id of the create popover, and
|
|
12582
|
+
* the focus back on the id. */
|
|
12583
|
+
function say_node_id_required(gobj)
|
|
12584
|
+
{
|
|
12585
|
+
let $popover = gobj.priv._create_popover_el;
|
|
12586
|
+
let $error = $popover.querySelector('.G6_CREATE_ERROR');
|
|
12587
|
+
let $id = $popover.querySelector('.G6_CREATE_ID');
|
|
12588
|
+
|
|
12589
|
+
if(!$error || !$id) {
|
|
12590
|
+
log_error(`${gobj_short_name(gobj)}: the create popover has no id or no error line`);
|
|
12591
|
+
return;
|
|
12592
|
+
}
|
|
12593
|
+
$error.textContent = t('node id required');
|
|
12594
|
+
$error.setAttribute('data-i18n', 'node id required');
|
|
12595
|
+
$error.style.display = 'block';
|
|
12596
|
+
$id.focus();
|
|
12597
|
+
}
|
|
12598
|
+
|
|
12599
|
+
/************************************************************
|
|
12600
|
+
* Is `popover_key` open, and for the selection `kw` names?
|
|
12601
|
+
* `keys` are the identity in `kw` (the rest is what the form
|
|
12602
|
+
* held). A button of a popover that is not the one open is a
|
|
12603
|
+
* broken invariant -- said loudly, never acted on.
|
|
12604
|
+
************************************************************/
|
|
12605
|
+
function popover_is_current(gobj, event, kw, popover_key, keys)
|
|
12606
|
+
{
|
|
12607
|
+
let priv = gobj.priv;
|
|
12608
|
+
|
|
12609
|
+
if(!priv[popover_key]) {
|
|
12610
|
+
log_error(`${gobj_short_name(gobj)}: ${event} for ${JSON.stringify(kw || {})}, ` +
|
|
12611
|
+
`but its popover is not open`);
|
|
12612
|
+
return false;
|
|
12613
|
+
}
|
|
12614
|
+
let identity = {};
|
|
12615
|
+
for(let k of keys) {
|
|
12616
|
+
identity[k] = kw? kw[k] : undefined;
|
|
12617
|
+
}
|
|
12618
|
+
return floating_button_is_current(gobj, event, identity);
|
|
12619
|
+
}
|
|
12620
|
+
|
|
12621
|
+
/************************************************************
|
|
12622
|
+
* {node_id, shape_before, fill, stroke, line_width, scope,
|
|
12623
|
+
* shape} -- apply of the node popover.
|
|
12624
|
+
************************************************************/
|
|
12625
|
+
function ac_apply_node_popover(gobj, event, kw, src)
|
|
12626
|
+
{
|
|
12627
|
+
if(!popover_is_current(gobj, event, kw, '_node_popover_el', ['node_id'])) {
|
|
12628
|
+
return -1; // Error already logged
|
|
12629
|
+
}
|
|
12630
|
+
let shape = String(kw.shape || "");
|
|
12631
|
+
drop_preview_undo(gobj, '_node_preview_undo');
|
|
12632
|
+
apply_node_properties(gobj, kw.node_id,
|
|
12633
|
+
kw.fill,
|
|
12634
|
+
kw.stroke,
|
|
12635
|
+
parseInt(kw.line_width) || 1,
|
|
12636
|
+
kw.scope,
|
|
12637
|
+
/* Only a figure somebody CHOSE is written: the one the
|
|
12638
|
+
* select opened on is today's default, and saving it
|
|
12639
|
+
* would freeze it on the node. */
|
|
12640
|
+
(shape !== kw.shape_before)? shape : ""
|
|
12641
|
+
);
|
|
12642
|
+
return 0;
|
|
12643
|
+
}
|
|
12644
|
+
|
|
12645
|
+
/************************************************************
|
|
12646
|
+
* {node_id} -- cancel of the node popover, which undoes the
|
|
12647
|
+
* preview.
|
|
12648
|
+
************************************************************/
|
|
12649
|
+
function ac_cancel_node_popover(gobj, event, kw, src)
|
|
12650
|
+
{
|
|
12651
|
+
if(!popover_is_current(gobj, event, kw, '_node_popover_el', ['node_id'])) {
|
|
12652
|
+
return -1; // Error already logged
|
|
12653
|
+
}
|
|
12654
|
+
hide_node_popover(gobj);
|
|
12655
|
+
return 0;
|
|
12656
|
+
}
|
|
12657
|
+
|
|
12658
|
+
/************************************************************
|
|
12659
|
+
* {edge_id, line_width, color, scope} -- apply of the edge
|
|
12660
|
+
* popover.
|
|
12661
|
+
************************************************************/
|
|
12662
|
+
function ac_apply_edge_popover(gobj, event, kw, src)
|
|
12663
|
+
{
|
|
12664
|
+
if(!popover_is_current(gobj, event, kw, '_edge_popover_el', ['edge_id'])) {
|
|
12665
|
+
return -1; // Error already logged
|
|
12666
|
+
}
|
|
12667
|
+
drop_preview_undo(gobj, '_edge_preview_undo');
|
|
12668
|
+
apply_edge_properties(gobj, kw.edge_id,
|
|
12669
|
+
parseInt(kw.line_width) || 2,
|
|
12670
|
+
kw.color,
|
|
12671
|
+
kw.scope
|
|
12672
|
+
);
|
|
12673
|
+
return 0;
|
|
12674
|
+
}
|
|
12675
|
+
|
|
12676
|
+
/************************************************************
|
|
12677
|
+
* {edge_id} -- cancel of the edge popover: the edge is let go,
|
|
12678
|
+
* which undoes the preview.
|
|
12679
|
+
************************************************************/
|
|
12680
|
+
function ac_cancel_edge_popover(gobj, event, kw, src)
|
|
12681
|
+
{
|
|
12682
|
+
if(!popover_is_current(gobj, event, kw, '_edge_popover_el', ['edge_id'])) {
|
|
12683
|
+
return -1; // Error already logged
|
|
12684
|
+
}
|
|
12685
|
+
deselect_edge(gobj);
|
|
12686
|
+
return 0;
|
|
12687
|
+
}
|
|
12688
|
+
|
|
12689
|
+
/************************************************************
|
|
12690
|
+
* {node_id, port_key, r_before, shape, r, scope} -- apply of
|
|
12691
|
+
* the port popover.
|
|
12692
|
+
************************************************************/
|
|
12693
|
+
function ac_apply_port_popover(gobj, event, kw, src)
|
|
12694
|
+
{
|
|
12695
|
+
if(!popover_is_current(gobj, event, kw, '_port_popover_el', ['node_id', 'port_key'])) {
|
|
12696
|
+
return -1; // Error already logged
|
|
12697
|
+
}
|
|
12698
|
+
drop_preview_undo(gobj, '_port_preview_undo');
|
|
12699
|
+
apply_port_properties(gobj, kw.node_id, kw.port_key,
|
|
12700
|
+
kw.shape, parseInt(kw.r) || kw.r_before, kw.scope
|
|
12701
|
+
);
|
|
12702
|
+
return 0;
|
|
12703
|
+
}
|
|
12704
|
+
|
|
12705
|
+
/************************************************************
|
|
12706
|
+
* {node_id, port_key} -- cancel of the port popover, which
|
|
12707
|
+
* undoes the preview.
|
|
12708
|
+
************************************************************/
|
|
12709
|
+
function ac_cancel_port_popover(gobj, event, kw, src)
|
|
12710
|
+
{
|
|
12711
|
+
if(!popover_is_current(gobj, event, kw, '_port_popover_el', ['node_id', 'port_key'])) {
|
|
12712
|
+
return -1; // Error already logged
|
|
12713
|
+
}
|
|
12714
|
+
hide_port_popover(gobj);
|
|
12715
|
+
return 0;
|
|
12716
|
+
}
|
|
12717
|
+
|
|
12718
|
+
/* Where the confirm popover of each question is kept. */
|
|
12719
|
+
const CONFIRM_POPOVER_OF = {
|
|
12720
|
+
delete_selection: '_delete_confirm_el',
|
|
12721
|
+
delete_node: '_delete_confirm_el',
|
|
12722
|
+
unlink_edge: '_unlink_confirm_el',
|
|
12723
|
+
};
|
|
12724
|
+
|
|
12725
|
+
function same_question(a, b)
|
|
12726
|
+
{
|
|
12727
|
+
return !!a && !!b && a.what === b.what &&
|
|
12728
|
+
JSON.stringify(a.id ?? null) === JSON.stringify(b.id ?? null) &&
|
|
12729
|
+
JSON.stringify(a.ids ?? null) === JSON.stringify(b.ids ?? null);
|
|
12730
|
+
}
|
|
12731
|
+
|
|
12732
|
+
/************************************************************
|
|
12733
|
+
* {what, id|ids} -- the person said no in a confirm popover:
|
|
12734
|
+
* the question it carries must be the one open.
|
|
12735
|
+
************************************************************/
|
|
12736
|
+
function ac_cancel_confirm(gobj, event, kw, src)
|
|
12737
|
+
{
|
|
12738
|
+
let priv = gobj.priv;
|
|
12739
|
+
let key = CONFIRM_POPOVER_OF[(kw && kw.what) || ""];
|
|
12740
|
+
let asked = key? priv._confirm_questions[key] : null;
|
|
12741
|
+
|
|
12742
|
+
if(!key || !priv[key] || !same_question(asked, kw)) {
|
|
12743
|
+
log_error(`${gobj_short_name(gobj)}: ${event} for ${JSON.stringify(kw || {})}, ` +
|
|
12744
|
+
`but the question open is ${JSON.stringify((priv[key] && asked) || {})}`);
|
|
12745
|
+
return -1;
|
|
12746
|
+
}
|
|
12747
|
+
hide_overlay(gobj, key);
|
|
12748
|
+
return 0;
|
|
12749
|
+
}
|
|
12750
|
+
|
|
12751
|
+
/************************************************************
|
|
12752
|
+
* {node_id, port_key, client_x, client_y} -- a press on the
|
|
12753
|
+
* link icon of the selected fkey port, which may be a drag.
|
|
12754
|
+
************************************************************/
|
|
12755
|
+
function ac_start_link_drag(gobj, event, kw, src)
|
|
12756
|
+
{
|
|
12757
|
+
let priv = gobj.priv;
|
|
12758
|
+
|
|
12759
|
+
if(!floating_button_is_current(gobj, event,
|
|
12760
|
+
{node_id: kw && kw.node_id, port_key: kw && kw.port_key})) {
|
|
12761
|
+
return -1; // Error already logged
|
|
12762
|
+
}
|
|
12763
|
+
if(priv._link_drag) {
|
|
12764
|
+
/* The release of the last one never came: it is dropped,
|
|
12765
|
+
* and this press starts over. */
|
|
12766
|
+
log_warning(`${gobj_short_name(gobj)}: ${event} with a link drag still in flight`);
|
|
12767
|
+
end_link_drag(gobj);
|
|
12768
|
+
}
|
|
12769
|
+
start_link_drag(gobj, Number(kw.client_x) || 0, Number(kw.client_y) || 0);
|
|
12770
|
+
return 0;
|
|
12771
|
+
}
|
|
12772
|
+
|
|
12773
|
+
/************************************************************
|
|
12774
|
+
* {client_x, client_y, dragged} -- the pointer of a link drag
|
|
12775
|
+
* went up. Not dragged, it was a click: the click comes next
|
|
12776
|
+
* (EV_ENTER_LINKING_MODE). Dragged, it links to the hook it was
|
|
12777
|
+
* dropped on, or leaves the linking mode.
|
|
12778
|
+
************************************************************/
|
|
12779
|
+
function ac_link_drag_end(gobj, event, kw, src)
|
|
12780
|
+
{
|
|
12781
|
+
let priv = gobj.priv;
|
|
12782
|
+
let drag = priv._link_drag;
|
|
12783
|
+
|
|
12784
|
+
if(!drag) {
|
|
12785
|
+
log_error(`${gobj_short_name(gobj)}: ${event} with no link drag in flight`);
|
|
12786
|
+
return -1;
|
|
12787
|
+
}
|
|
12788
|
+
end_link_drag(gobj);
|
|
12789
|
+
if(!kw || !kw.dragged) {
|
|
12790
|
+
return 0;
|
|
12791
|
+
}
|
|
12792
|
+
|
|
12793
|
+
let cx = Number(kw.client_x) - drag.left;
|
|
12794
|
+
let cy = Number(kw.client_y) - drag.top;
|
|
12795
|
+
let canvasPt = priv.graph.getCanvasByViewport([cx, cy]);
|
|
12796
|
+
let target = find_hook_at_point(gobj, canvasPt[0], canvasPt[1]);
|
|
12797
|
+
|
|
12798
|
+
if(target) {
|
|
12799
|
+
try_complete_link(gobj, target.node_id, target.port_key);
|
|
12800
|
+
} else {
|
|
12801
|
+
exit_linking_mode(gobj);
|
|
12802
|
+
}
|
|
12803
|
+
return 0;
|
|
12804
|
+
}
|
|
12805
|
+
|
|
12806
|
+
/************************************************************
|
|
12807
|
+
* {} -- the browser took the pointer of a link drag: no drop,
|
|
12808
|
+
* and the linking mode goes.
|
|
12809
|
+
************************************************************/
|
|
12810
|
+
function ac_link_drag_cancel(gobj, event, kw, src)
|
|
12811
|
+
{
|
|
12812
|
+
let priv = gobj.priv;
|
|
12813
|
+
|
|
12814
|
+
if(!priv._link_drag) {
|
|
12815
|
+
log_error(`${gobj_short_name(gobj)}: ${event} with no link drag in flight`);
|
|
12816
|
+
return -1;
|
|
12817
|
+
}
|
|
12818
|
+
end_link_drag(gobj);
|
|
12819
|
+
exit_linking_mode(gobj);
|
|
12820
|
+
return 0;
|
|
12821
|
+
}
|
|
12822
|
+
|
|
12027
12823
|
/************************************************************
|
|
12028
12824
|
* {what, id|ids} -- the person said yes in a confirm popover.
|
|
12029
12825
|
*
|
|
@@ -12316,8 +13112,25 @@ function create_gclass(gclass_name)
|
|
|
12316
13112
|
["EV_CONTEXT_MENU_ITEM", ac_context_menu_item, null],
|
|
12317
13113
|
["EV_REQUEST_DELETE_NODE", ac_request_delete_node, null],
|
|
12318
13114
|
["EV_REQUEST_UNLINK_EDGE", ac_request_unlink_edge, null],
|
|
13115
|
+
["EV_TOGGLE_NODE_POPOVER", ac_toggle_node_popover, null],
|
|
13116
|
+
["EV_TOGGLE_EDGE_POPOVER", ac_toggle_edge_popover, null],
|
|
13117
|
+
["EV_TOGGLE_PORT_POPOVER", ac_toggle_port_popover, null],
|
|
13118
|
+
["EV_ENTER_LINKING_MODE", ac_enter_linking_mode, null],
|
|
13119
|
+
["EV_CLOSE_NODE_DETAIL", ac_close_node_detail, null],
|
|
13120
|
+
["EV_TOGGLE_CREATE_POPOVER", ac_toggle_create_popover, null],
|
|
13121
|
+
["EV_CLOSE_CREATE_POPOVER", ac_close_create_popover, null],
|
|
12319
13122
|
["EV_SUBMIT_NEW_NODE", ac_submit_new_node, null],
|
|
13123
|
+
["EV_APPLY_NODE_POPOVER", ac_apply_node_popover, null],
|
|
13124
|
+
["EV_CANCEL_NODE_POPOVER", ac_cancel_node_popover, null],
|
|
13125
|
+
["EV_APPLY_EDGE_POPOVER", ac_apply_edge_popover, null],
|
|
13126
|
+
["EV_CANCEL_EDGE_POPOVER", ac_cancel_edge_popover, null],
|
|
13127
|
+
["EV_APPLY_PORT_POPOVER", ac_apply_port_popover, null],
|
|
13128
|
+
["EV_CANCEL_PORT_POPOVER", ac_cancel_port_popover, null],
|
|
12320
13129
|
["EV_CONFIRMED", ac_confirmed, null],
|
|
13130
|
+
["EV_CANCEL_CONFIRM", ac_cancel_confirm, null],
|
|
13131
|
+
["EV_START_LINK_DRAG", ac_start_link_drag, null],
|
|
13132
|
+
["EV_LINK_DRAG_END", ac_link_drag_end, null],
|
|
13133
|
+
["EV_LINK_DRAG_CANCEL", ac_link_drag_cancel, null],
|
|
12321
13134
|
["EV_CANVAS_CLICK", ac_canvas_click, null],
|
|
12322
13135
|
["EV_NODE_DRAG_END", ac_node_drag_end, null],
|
|
12323
13136
|
["EV_BRUSH_SELECT", ac_brush_select, null],
|
|
@@ -12386,8 +13199,25 @@ function create_gclass(gclass_name)
|
|
|
12386
13199
|
["EV_CONTEXT_MENU_ITEM", 0],
|
|
12387
13200
|
["EV_REQUEST_DELETE_NODE", 0],
|
|
12388
13201
|
["EV_REQUEST_UNLINK_EDGE", 0],
|
|
13202
|
+
["EV_TOGGLE_NODE_POPOVER", 0],
|
|
13203
|
+
["EV_TOGGLE_EDGE_POPOVER", 0],
|
|
13204
|
+
["EV_TOGGLE_PORT_POPOVER", 0],
|
|
13205
|
+
["EV_ENTER_LINKING_MODE", 0],
|
|
13206
|
+
["EV_CLOSE_NODE_DETAIL", 0],
|
|
13207
|
+
["EV_TOGGLE_CREATE_POPOVER", 0],
|
|
13208
|
+
["EV_CLOSE_CREATE_POPOVER", 0],
|
|
12389
13209
|
["EV_SUBMIT_NEW_NODE", 0],
|
|
13210
|
+
["EV_APPLY_NODE_POPOVER", 0],
|
|
13211
|
+
["EV_CANCEL_NODE_POPOVER", 0],
|
|
13212
|
+
["EV_APPLY_EDGE_POPOVER", 0],
|
|
13213
|
+
["EV_CANCEL_EDGE_POPOVER", 0],
|
|
13214
|
+
["EV_APPLY_PORT_POPOVER", 0],
|
|
13215
|
+
["EV_CANCEL_PORT_POPOVER", 0],
|
|
12390
13216
|
["EV_CONFIRMED", 0],
|
|
13217
|
+
["EV_CANCEL_CONFIRM", 0],
|
|
13218
|
+
["EV_START_LINK_DRAG", 0],
|
|
13219
|
+
["EV_LINK_DRAG_END", 0],
|
|
13220
|
+
["EV_LINK_DRAG_CANCEL", 0],
|
|
12391
13221
|
["EV_CANVAS_CLICK", 0],
|
|
12392
13222
|
["EV_NODE_DRAG_END", 0],
|
|
12393
13223
|
["EV_BRUSH_SELECT", 0],
|
|
@@ -12511,4 +13341,7 @@ function register_c_g6_nodes_tree()
|
|
|
12511
13341
|
export {
|
|
12512
13342
|
register_c_g6_nodes_tree,
|
|
12513
13343
|
toolbar_item_html, // exported for its test
|
|
13344
|
+
YuiToolbar, // exported for its test
|
|
13345
|
+
create_floating_icon, // exported for its test
|
|
13346
|
+
create_detail_close, // exported for its test
|
|
12514
13347
|
};
|