@themarioga/grid-editor 7.2.0 → 7.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +83 -14
  3. package/UPGRADING.md +31 -0
  4. package/dist/grideditor.bundle.min.js +2 -2
  5. package/dist/grideditor.css +102 -0
  6. package/dist/grideditor.d.ts +18 -0
  7. package/dist/grideditor.esm.js +291 -31
  8. package/dist/grideditor.js +291 -31
  9. package/dist/grideditor.min.css +1 -1
  10. package/dist/grideditor.min.css.map +1 -1
  11. package/dist/grideditor.min.js +1 -1
  12. package/dist/grideditor.min.js.map +3 -3
  13. package/dist/locales/grideditor.es.esm.js +67 -0
  14. package/dist/locales/grideditor.es.js +67 -0
  15. package/dist/locales/grideditor.es.min.js +1 -1
  16. package/dist/locales/grideditor.es.min.js.map +2 -2
  17. package/dist/plugins/grideditor.float.esm.js +10 -1
  18. package/dist/plugins/grideditor.float.js +8 -2
  19. package/dist/plugins/grideditor.float.min.js +1 -1
  20. package/dist/plugins/grideditor.float.min.js.map +4 -4
  21. package/dist/plugins/grideditor.spacing.esm.js +12 -3
  22. package/dist/plugins/grideditor.spacing.js +9 -3
  23. package/dist/plugins/grideditor.spacing.min.js +1 -1
  24. package/dist/plugins/grideditor.spacing.min.js.map +4 -4
  25. package/dist/plugins/grideditor.style.esm.js +1593 -0
  26. package/dist/plugins/grideditor.style.js +1592 -0
  27. package/dist/plugins/grideditor.style.min.js +2 -0
  28. package/dist/plugins/grideditor.style.min.js.map +7 -0
  29. package/dist/plugins/grideditor.textalign.esm.js +10 -1
  30. package/dist/plugins/grideditor.textalign.js +8 -2
  31. package/dist/plugins/grideditor.textalign.min.js +1 -1
  32. package/dist/plugins/grideditor.textalign.min.js.map +4 -4
  33. package/dist/plugins/grideditor.visibility.esm.js +12 -3
  34. package/dist/plugins/grideditor.visibility.js +9 -3
  35. package/dist/plugins/grideditor.visibility.min.js +1 -1
  36. package/dist/plugins/grideditor.visibility.min.js.map +4 -4
  37. package/docs/locale-keys.md +86 -7
  38. package/docs/plugins.md +49 -5
  39. package/example/index.html +14 -0
  40. package/example/style.html +151 -0
  41. package/example/utilities.html +15 -11
  42. package/package.json +1 -1
  43. package/src/js/grideditor.js +365 -27
  44. package/src/js/locales/grideditor.es.js +67 -0
  45. package/src/js/plugins/grideditor.float.js +8 -33
  46. package/src/js/plugins/grideditor.spacing.js +8 -273
  47. package/src/js/plugins/grideditor.style.js +179 -0
  48. package/src/js/plugins/grideditor.textalign.js +8 -39
  49. package/src/js/plugins/grideditor.visibility.js +8 -147
  50. package/src/js/style/accordion.js +157 -0
  51. package/src/js/style/catalog.js +76 -0
  52. package/src/js/style/custom.js +84 -0
  53. package/src/js/style/fields.js +382 -0
  54. package/src/js/style/float.js +46 -0
  55. package/src/js/style/options.js +75 -0
  56. package/src/js/style/sections.js +271 -0
  57. package/src/js/style/spacing.js +286 -0
  58. package/src/js/style/textalign.js +52 -0
  59. package/src/js/style/visibility.js +160 -0
  60. package/src/less/grideditor.less +111 -0
  61. package/types/grideditor.d.ts +18 -0
@@ -14,18 +14,19 @@
14
14
  <link rel="stylesheet" type="text/css" href="../dist/grideditor.css" />
15
15
 
16
16
  <!-- The editor, and the utility plugins. Each is a file of its own, and
17
- loading it is what puts its field in the settings panels. -->
17
+ loading it is what puts its fields in the settings panels. The style
18
+ plugin carries visibility, spacing, text alignment and float, which
19
+ were plugins of their own up to 7.2, and edits them in its Style
20
+ accordion; order, alignment and gutters are the grid's, and stay in
21
+ the Responsive section. -->
18
22
  <script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.6/Sortable.min.js"></script>
19
23
  <script src="../dist/grideditor.min.js"></script>
20
- <script src="../dist/plugins/grideditor.visibility.min.js"></script>
21
24
  <script src="../dist/plugins/grideditor.order.min.js"></script>
22
25
  <script src="../dist/plugins/grideditor.alignment.min.js"></script>
23
26
  <script src="../dist/plugins/grideditor.gutters.min.js"></script>
24
- <script src="../dist/plugins/grideditor.spacing.min.js"></script>
25
- <script src="../dist/plugins/grideditor.textalign.min.js"></script>
26
27
  <!-- Float works on elements, so the elements plugin comes first -->
27
28
  <script src="../dist/plugins/grideditor.elements.min.js"></script>
28
- <script src="../dist/plugins/grideditor.float.min.js"></script>
29
+ <script src="../dist/plugins/grideditor.style.min.js"></script>
29
30
 
30
31
  <script>
31
32
  document.addEventListener('DOMContentLoaded', function() {
@@ -89,9 +90,11 @@
89
90
  breakpoint. Pick a breakpoint in the dropdown and every
90
91
  change is written for that breakpoint alone; in
91
92
  <strong>All sizes</strong> it is written once, for every
92
- size. Each drawer's gear has a <strong>Responsive</strong>
93
- section with a field per utility, which says what the
94
- breakpoint inherits and from where.
93
+ size. Each drawer's gear has a <strong>Style</strong>
94
+ accordion, whose sections hold visibility, spacing, text
95
+ alignment and float, and a <strong>Responsive</strong>
96
+ section for the grid's utilities. Every field says what
97
+ the breakpoint inherits and from where.
95
98
  </p>
96
99
  <div class="d-flex align-items-center gap-2 mb-3">
97
100
  <label class="form-label mb-0" for="panel">A gear opens its settings in</label>
@@ -129,13 +132,14 @@
129
132
  </p>
130
133
  <p class="form-text">
131
134
  <strong>Spacing:</strong> a padding and a margin group in
132
- every panel, each a side and a value. The centred column
135
+ the Style accordion's Spacing section, each a side and a
136
+ value. The centred column
133
137
  below gets more padding from small desktop up.
134
138
  </p>
135
139
  <p class="form-text">
136
- <strong>Text alignment:</strong> a field in every panel.
140
+ <strong>Text alignment:</strong> a field in the Text section.
137
141
  The centred column's text is centred from small desktop
138
- up. <strong>Float:</strong> a field in an element's panel;
142
+ up. <strong>Float:</strong> a field in an element's Position section;
139
143
  the quote in the last row floats to the end from tablet up,
140
144
  with the text flowing round it.
141
145
  </p>
package/package.json CHANGED
@@ -33,7 +33,7 @@
33
33
  "comment": "A scoped package publishes restricted unless it is told otherwise.",
34
34
  "access": "public"
35
35
  },
36
- "version": "7.2.0",
36
+ "version": "7.3.0",
37
37
  "main": "dist/grideditor.js",
38
38
  "module": "dist/grideditor.esm.js",
39
39
  "types": "dist/grideditor.d.ts",
@@ -475,6 +475,7 @@ function build(instance, baseElem, optionsOrMethod) {
475
475
 
476
476
  removeConfirmModal();
477
477
  removeSettingsPanels();
478
+ removeDialog();
478
479
  mainControls.remove();
479
480
  createMainControls();
480
481
  reset();
@@ -1753,6 +1754,10 @@ function build(instance, baseElem, optionsOrMethod) {
1753
1754
  if (openSettingsState && !dom.attached(openSettingsState.node)) {
1754
1755
  closeSettings();
1755
1756
  }
1757
+ // And a dialog showing part of a panel that is gone
1758
+ if (dialogState && dialogState.home && !dom.attached(dialogState.home)) {
1759
+ closeDialog();
1760
+ }
1756
1761
 
1757
1762
  runFilter(true);
1758
1763
  dom.addClass(canvas, 'ge-editing');
@@ -1781,7 +1786,10 @@ function build(instance, baseElem, optionsOrMethod) {
1781
1786
  plugins('onBeforeDeinit');
1782
1787
  closeSizePicker();
1783
1788
  hideDropMarker();
1789
+ // In case a dialog was opened with no settings panel open
1790
+ closeDialog();
1784
1791
  dom.all(canvas, '.ge-tools-drawer').forEach(function(drawer) { drawer.remove(); });
1792
+ pluginFields.clear();
1785
1793
  unwrapTexts();
1786
1794
  plugins('onDeinit');
1787
1795
  // After the rich text editors have let go of their content areas:
@@ -1836,6 +1844,7 @@ function build(instance, baseElem, optionsOrMethod) {
1836
1844
  deinit();
1837
1845
  removeConfirmModal();
1838
1846
  removeSettingsPanels();
1847
+ removeDialog();
1839
1848
  mainControls.remove();
1840
1849
  htmlTextArea.remove();
1841
1850
  lifetime.abort();
@@ -1857,9 +1866,36 @@ function build(instance, baseElem, optionsOrMethod) {
1857
1866
 
1858
1867
  registerFamily('grid', {}, widthFamily());
1859
1868
  if (settings.row_cols !== false) { registerFamily('grid', {}, rowColsFamily()); }
1860
- var wanted = function(name) {
1869
+ // A plugin whose factory says it `replaces` others has taken
1870
+ // their place: an old name in the plugins setting asks for it,
1871
+ // and the old plugin, loaded beside it, stands down
1872
+ var replacedBy = {};
1873
+ [GridEditor.containers, GridEditor.features, GridEditor.utilities].forEach(function(registry) {
1874
+ Object.keys(registry).forEach(function(name) {
1875
+ (registry[name].replaces || []).forEach(function(old) {
1876
+ if (!replacedBy[old]) { replacedBy[old] = name; }
1877
+ });
1878
+ });
1879
+ });
1880
+ var named = function(name) {
1861
1881
  return !settings.plugins || settings.plugins.indexOf(name) !== -1;
1862
1882
  };
1883
+ var namedAsOld = function(name) {
1884
+ return !!settings.plugins && settings.plugins.some(function(old) { return replacedBy[old] === name; });
1885
+ };
1886
+ var standsDown = function(name) {
1887
+ return !!replacedBy[name] && wanted(replacedBy[name]);
1888
+ };
1889
+ var wanted = function(name) {
1890
+ return !standsDown(name) && (named(name) || namedAsOld(name));
1891
+ };
1892
+ var replaced = function(name) {
1893
+ if (!standsDown(name) || !named(name)) { return false; }
1894
+
1895
+ warnOnceHere('replaced:' + name, 'the "' + name + '" plugin is part of "' + replacedBy[name] +
1896
+ '", which is loaded: ignoring it');
1897
+ return true;
1898
+ };
1863
1899
 
1864
1900
  // What the plugins setting does not choose: the text editors,
1865
1901
  // which content_types chooses. A page that names its containers
@@ -1869,16 +1905,16 @@ function build(instance, baseElem, optionsOrMethod) {
1869
1905
  };
1870
1906
 
1871
1907
  Object.keys(GridEditor.containers).forEach(function(type) {
1872
- if (wanted(type)) { CONTAINERS[type] = GridEditor.containers[type](api); }
1908
+ if (!replaced(type) && wanted(type)) { CONTAINERS[type] = GridEditor.containers[type](api); }
1873
1909
  });
1874
1910
 
1875
1911
  Object.keys(GridEditor.features).forEach(function(name) {
1876
1912
  var factory = GridEditor.features[name];
1877
- if (featureWanted(name, factory)) { FEATURES[name] = factory(api); }
1913
+ if (!replaced(name) && featureWanted(name, factory)) { FEATURES[name] = factory(api); }
1878
1914
  });
1879
1915
 
1880
1916
  Object.keys(GridEditor.utilities).forEach(function(name) {
1881
- if (!wanted(name)) { return; }
1917
+ if (replaced(name) || !wanted(name)) { return; }
1882
1918
 
1883
1919
  UTILITIES[name] = GridEditor.utilities[name](api);
1884
1920
  (UTILITIES[name].families || []).forEach(function(family) {
@@ -1896,6 +1932,12 @@ function build(instance, baseElem, optionsOrMethod) {
1896
1932
  (settings.plugins || []).forEach(function(name) {
1897
1933
  if (CONTAINERS[name] || FEATURES[name] || UTILITIES[name]) { return; }
1898
1934
 
1935
+ if (standsDown(name)) {
1936
+ warnOnceHere('renamed:' + name, 'the "' + name + '" plugin is part of "' + replacedBy[name] +
1937
+ '" now: name "' + replacedBy[name] + '" in the plugins setting instead');
1938
+ return;
1939
+ }
1940
+
1899
1941
  warnOnceHere('plugin:' + name, 'the "' + name + '" plugin is not loaded: ' +
1900
1942
  'include dist/plugins/grideditor.' + name + '.js after the editor');
1901
1943
  });
@@ -1948,6 +1990,12 @@ function build(instance, baseElem, optionsOrMethod) {
1948
1990
  setUtility: setUtility,
1949
1991
  utilityField: utilityField,
1950
1992
  bareStyle: bareStyle,
1993
+ // The host's own style, under the preview
1994
+ hostStyle: hostStyle,
1995
+ setHostStyle: setHostStyle,
1996
+ // A modal of the editor's own, over the settings
1997
+ openDialog: openDialog,
1998
+ closeDialog: closeDialog,
1951
1999
  rowFromLayout: rowFromLayoutValue,
1952
2000
  nodeHtml: nodeHtml,
1953
2001
  // A text editor has rewritten a content area, so whatever the
@@ -2266,6 +2314,7 @@ function build(instance, baseElem, optionsOrMethod) {
2266
2314
  if (classes) { classes.value = hostClasses(node).join(' '); }
2267
2315
  dom.children(details, '.ge-utilities').forEach(function(section) { renderUtilities(section); });
2268
2316
  }
2317
+ renderPluginFields(node);
2269
2318
  refreshPreviews(node);
2270
2319
  }
2271
2320
 
@@ -2362,6 +2411,84 @@ function build(instance, baseElem, optionsOrMethod) {
2362
2411
  });
2363
2412
  }
2364
2413
 
2414
+ /* The host's own style, under the preview */
2415
+
2416
+ /**
2417
+ * The node's style as the host wrote it: its declarations with every
2418
+ * property the preview set put back to what the preview recorded.
2419
+ * Held in a detached element's style, which parses and serializes it
2420
+ * the way the node's own would.
2421
+ */
2422
+ function hostDeclaration(node) {
2423
+ var scratch = document.createElement('div');
2424
+ var was = {};
2425
+
2426
+ scratch.style.cssText = node.style.cssText;
2427
+ try { was = JSON.parse(node.getAttribute(PREVIEW_ATTR)) || {}; } catch (error) { /* no record */ }
2428
+
2429
+ Object.keys(was).forEach(function(property) {
2430
+ var before = was[property];
2431
+
2432
+ if (before && before[0]) {
2433
+ scratch.style.setProperty(property, before[0], before[1]);
2434
+ } else {
2435
+ scratch.style.removeProperty(property);
2436
+ }
2437
+ });
2438
+
2439
+ return scratch.style;
2440
+ }
2441
+
2442
+ /**
2443
+ * ge.hostStyle(node, property): { value, priority } of one property
2444
+ * of the host's style, never the preview's. With no property, the
2445
+ * host's whole style as css text.
2446
+ */
2447
+ function hostStyle(node, property) {
2448
+ var declaration = hostDeclaration(node);
2449
+
2450
+ if (property === undefined) { return declaration.cssText; }
2451
+
2452
+ return {
2453
+ value: declaration.getPropertyValue(property),
2454
+ priority: declaration.getPropertyPriority(property),
2455
+ };
2456
+ }
2457
+
2458
+ /**
2459
+ * ge.setHostStyle(node, property, value, priority): write one property
2460
+ * of the host's style, or take it off with an empty value. The
2461
+ * preview comes off the node while it is written and goes back on
2462
+ * after, so what the preview put back on deinit is the host's new
2463
+ * value. False, and nothing written, when the browser refuses the
2464
+ * value. `priority` left out keeps the one the property had.
2465
+ */
2466
+ function setHostStyle(node, property, value, priority) {
2467
+ value = value === null || value === undefined ? '' : String(value).trim();
2468
+
2469
+ if (priority === undefined) { priority = hostStyle(node, property).priority; }
2470
+
2471
+ if (value !== '') {
2472
+ var check = document.createElement('div').style;
2473
+ check.setProperty(property, value, priority || '');
2474
+ if (check.getPropertyValue(property) === '') { return false; }
2475
+ }
2476
+
2477
+ var previewed = node.hasAttribute(PREVIEW_ATTR);
2478
+ if (previewed) { clearPreviews(node); }
2479
+
2480
+ if (value === '') {
2481
+ node.style.removeProperty(property);
2482
+ } else {
2483
+ node.style.setProperty(property, value, priority || '');
2484
+ }
2485
+ dom.dropEmptyStyle(node);
2486
+
2487
+ if (previewed) { refreshPreviews(node); }
2488
+
2489
+ return true;
2490
+ }
2491
+
2365
2492
  /* The panel */
2366
2493
 
2367
2494
  /**
@@ -2434,10 +2561,32 @@ function build(instance, baseElem, optionsOrMethod) {
2434
2561
 
2435
2562
  var field = createField(node, family);
2436
2563
  renderField(field, node);
2564
+ pluginFields.set(field, node);
2437
2565
 
2438
2566
  return field;
2439
2567
  }
2440
2568
 
2569
+ /**
2570
+ * The fields a plugin made, wherever it put them: a Responsive
2571
+ * section renders its own, and these are the rest - in a plugin's
2572
+ * section of the panel, or in the dialog. Each one's node's, or
2573
+ * every node's; a field no longer on the page is forgotten.
2574
+ */
2575
+ var pluginFields = new Map(); // field -> the node it edits
2576
+
2577
+ function renderPluginFields(node) {
2578
+ pluginFields.forEach(function(fieldNode, field) {
2579
+ if (!field.isConnected) {
2580
+ pluginFields.delete(field);
2581
+ return;
2582
+ }
2583
+ if (node && fieldNode !== node) { return; }
2584
+ if (dom.closest(field, '.ge-utilities')) { return; }
2585
+
2586
+ renderField(field, fieldNode);
2587
+ });
2588
+ }
2589
+
2441
2590
  /** Fill a section's fields, the families' and the plugins' own, for the view being edited. */
2442
2591
  function renderUtilities(section) {
2443
2592
  var node = sectionNodes.get(section);
@@ -3170,12 +3319,59 @@ function build(instance, baseElem, optionsOrMethod) {
3170
3319
  classGroup.appendChild(btn);
3171
3320
  });
3172
3321
 
3322
+ pluginSections(container).forEach(function(section) { detailsDiv.appendChild(section); });
3323
+
3173
3324
  var utilities = createUtilitiesSection(container);
3174
3325
  if (utilities) { detailsDiv.appendChild(utilities); }
3175
3326
 
3176
3327
  return detailsDiv;
3177
3328
  }
3178
3329
 
3330
+ /**
3331
+ * What plugins add to a node's panel, between its general fields and
3332
+ * the Responsive section: each plugin's panelSection(node, kind), in
3333
+ * the order they were registered. Where the panel has room - the
3334
+ * offcanvas, the modal - the section is in it. A popover or a panel
3335
+ * inline in the drawer gets a button instead, and the section opens
3336
+ * in the dialog.
3337
+ */
3338
+ function pluginSections(node) {
3339
+ var kind = kindOf(node);
3340
+ var mode = panelMode();
3341
+ var sections = [];
3342
+
3343
+ [CONTAINERS, FEATURES, UTILITIES].forEach(function(registry) {
3344
+ Object.keys(registry).forEach(function(name) {
3345
+ var plugin = registry[name];
3346
+ var section = plugin.panelSection ? plugin.panelSection(node, kind) : null;
3347
+ if (!section || !section.body) { return; }
3348
+
3349
+ var holder = dom.element('div', { 'class': 'ge-panel-section', 'data-ge-plugin': name });
3350
+
3351
+ if (mode === 'offcanvas' || mode === 'modal') {
3352
+ holder.appendChild(section.body);
3353
+ } else {
3354
+ var label = t(section.labelKey);
3355
+ var open = holder.appendChild(dom.element('button', {
3356
+ type: 'button',
3357
+ 'class': 'btn btn-sm btn-outline-secondary ge-panel-section-open',
3358
+ }, label));
3359
+
3360
+ dom.hide(section.body);
3361
+ holder.appendChild(section.body);
3362
+ open.addEventListener('click', function() {
3363
+ openDialog(section.titleKey ? t(section.titleKey, { kind: kindLabel(node) }) : label,
3364
+ section.body, open);
3365
+ });
3366
+ }
3367
+
3368
+ sections.push(holder);
3369
+ });
3370
+ });
3371
+
3372
+ return sections;
3373
+ }
3374
+
3179
3375
  /* --------------------------------------------------------------
3180
3376
  * Where a node's settings open.
3181
3377
  *
@@ -3194,7 +3390,7 @@ function build(instance, baseElem, optionsOrMethod) {
3194
3390
  var PANEL_MODES = ['offcanvas', 'popover', 'modal', 'inline'];
3195
3391
  var settingsPanels = {}; // One of each, built on first use, outside the canvas
3196
3392
  var openSettingsState = null; // What is open: { mode, node, details, home, next, gear }
3197
- var modalState = new WeakMap(); // modal panel -> { watched, wanted, busy }
3393
+ var modalState = new WeakMap(); // modal panel -> { watched, wanted, busy, backdrop, dismissed }
3198
3394
 
3199
3395
  function panelMode() {
3200
3396
  if (PANEL_MODES.indexOf(settings.settings_panel) !== -1) { return settings.settings_panel; }
@@ -3251,7 +3447,8 @@ function build(instance, baseElem, optionsOrMethod) {
3251
3447
 
3252
3448
  var signal = { signal: listening.signal };
3253
3449
  document.addEventListener('keydown', function(e) {
3254
- if (e.key === 'Escape') { closeSettings(); }
3450
+ // A dialog over the panel takes the Escape for itself
3451
+ if (e.key === 'Escape' && !dialogState) { closeSettings(); }
3255
3452
  }, signal);
3256
3453
 
3257
3454
  if (mode === 'offcanvas') {
@@ -3274,15 +3471,20 @@ function build(instance, baseElem, optionsOrMethod) {
3274
3471
  }
3275
3472
  // A press anywhere else puts it away, as a popover does
3276
3473
  dom.on(document, 'mousedown touchstart', function(e) {
3277
- if (!panel.contains(e.target) && !gear.contains(e.target)) { closeSettings(); }
3474
+ if (!dialogState && !panel.contains(e.target) && !gear.contains(e.target)) { closeSettings(); }
3278
3475
  }, signal);
3279
3476
  } else {
3280
- showModal(panel);
3477
+ showModal(panel, function() {
3478
+ if (openSettingsState && openSettingsState.mode === 'modal') { closeSettings(); }
3479
+ });
3281
3480
  }
3282
3481
  }
3283
3482
 
3284
3483
  /** Put what is open away, and its panel back in its drawer. */
3285
3484
  function closeSettings() {
3485
+ // A dialog opened from the panel goes with it
3486
+ closeDialog();
3487
+
3286
3488
  var open = openSettingsState;
3287
3489
  if (!open) { return; }
3288
3490
  openSettingsState = null;
@@ -3381,7 +3583,7 @@ function build(instance, baseElem, optionsOrMethod) {
3381
3583
  settingsPanels[mode].remove();
3382
3584
  }
3383
3585
  });
3384
- if (settingsBackdrop) { settingsBackdrop.remove(); settingsBackdrop = null; }
3586
+ Object.keys(settingsPanels).forEach(function(mode) { removeBackdrop(settingsPanels[mode]); });
3385
3587
  settingsPanels = {};
3386
3588
  }
3387
3589
 
@@ -3424,23 +3626,34 @@ function build(instance, baseElem, optionsOrMethod) {
3424
3626
  });
3425
3627
  }
3426
3628
 
3427
- var settingsBackdrop = null; // The modal's backdrop, when Bootstrap's javascript is not there to make one
3428
-
3429
3629
  /**
3430
3630
  * Bootstrap's modal ignores a hide while it is still fading in, and a
3431
3631
  * show while it is fading out, so the editor asks for what it wants
3432
3632
  * and has it done once the running transition ends: a panel closed as
3433
3633
  * it opens - getHtml, a second click - would otherwise stay open, and
3434
3634
  * empty. A hide Bootstrap starts itself, from Escape or the backdrop,
3435
- * closes the settings as the close button does.
3635
+ * calls `dismissed`, as the close button does: the settings modal
3636
+ * closes the settings, the dialog closes the dialog.
3637
+ *
3638
+ * Without Bootstrap's javascript the modal is shown by hand, over a
3639
+ * backdrop of the editor's own that dismisses it when clicked.
3436
3640
  */
3437
- function showModal(panel) {
3641
+ function showModal(panel, dismissed) {
3438
3642
  var Modal = modalLibrary();
3643
+ var state = modalState.get(panel);
3644
+
3645
+ if (!state) {
3646
+ state = { wanted: null, busy: false, backdrop: null, dismissed: dismissed };
3647
+ modalState.set(panel, state);
3648
+ }
3649
+ state.dismissed = dismissed;
3439
3650
 
3440
3651
  if (!Modal) {
3441
- settingsBackdrop = dom.element('div', { 'class': 'modal-backdrop fade show ge-settings-backdrop' });
3442
- settingsBackdrop.addEventListener('click', function() { closeSettings(); });
3443
- document.body.appendChild(settingsBackdrop);
3652
+ if (!state.backdrop) {
3653
+ state.backdrop = dom.element('div', { 'class': 'modal-backdrop fade show ge-settings-backdrop' });
3654
+ state.backdrop.addEventListener('click', function() { state.dismissed(); });
3655
+ document.body.appendChild(state.backdrop);
3656
+ }
3444
3657
  dom.addClass(panel, 'show');
3445
3658
  panel.style.display = 'block';
3446
3659
  panel.removeAttribute('aria-hidden');
@@ -3448,11 +3661,9 @@ function build(instance, baseElem, optionsOrMethod) {
3448
3661
  }
3449
3662
 
3450
3663
  var modal = Modal.getOrCreateInstance(panel);
3451
- var state = modalState.get(panel);
3452
3664
 
3453
- if (!state) {
3454
- state = { wanted: null, busy: false };
3455
- modalState.set(panel, state);
3665
+ if (!state.watched) {
3666
+ state.watched = true;
3456
3667
  trackModal(panel);
3457
3668
 
3458
3669
  panel.addEventListener('hide.bs.modal', function() {
@@ -3469,9 +3680,9 @@ function build(instance, baseElem, optionsOrMethod) {
3469
3680
  panel.addEventListener('hidden.bs.modal', function() {
3470
3681
  state.busy = false;
3471
3682
  if (state.wanted === 'open') {
3472
- showModal(panel);
3473
- } else if (openSettingsState && openSettingsState.mode === 'modal') {
3474
- closeSettings();
3683
+ showModal(panel, state.dismissed);
3684
+ } else {
3685
+ state.dismissed();
3475
3686
  }
3476
3687
  });
3477
3688
  }
@@ -3485,18 +3696,19 @@ function build(instance, baseElem, optionsOrMethod) {
3485
3696
 
3486
3697
  function hideModal(panel) {
3487
3698
  var Modal = modalLibrary();
3699
+ var state = modalState.get(panel);
3700
+
3701
+ if (!state) { return; }
3702
+ state.wanted = 'closed';
3488
3703
 
3489
3704
  if (!Modal) {
3490
- if (settingsBackdrop) { settingsBackdrop.remove(); settingsBackdrop = null; }
3705
+ if (state.backdrop) { state.backdrop.remove(); state.backdrop = null; }
3491
3706
  dom.removeClass(panel, 'show');
3492
3707
  panel.style.removeProperty('display');
3493
3708
  panel.setAttribute('aria-hidden', 'true');
3494
3709
  return;
3495
3710
  }
3496
3711
 
3497
- var state = modalState.get(panel) || { wanted: null, busy: false };
3498
- state.wanted = 'closed';
3499
-
3500
3712
  // Already out of sight - Bootstrap hid it itself, and this is its
3501
3713
  // hidden event closing the settings - and nothing is under way:
3502
3714
  // a hide now would be ignored, and no event would come to say so
@@ -3506,6 +3718,131 @@ function build(instance, baseElem, optionsOrMethod) {
3506
3718
  }
3507
3719
  }
3508
3720
 
3721
+ /** A modal's hand made backdrop goes with it. */
3722
+ function removeBackdrop(panel) {
3723
+ var state = modalState.get(panel);
3724
+ if (state && state.backdrop) { state.backdrop.remove(); state.backdrop = null; }
3725
+ }
3726
+
3727
+ /* --------------------------------------------------------------
3728
+ * The dialog: a modal of the editor's own, over whatever settings
3729
+ * panel is open, for what does not fit in one. A plugin's section
3730
+ * of the panel opens in it when the panel is a popover or inline,
3731
+ * and a plugin can open one itself through ge.openDialog.
3732
+ *
3733
+ * What it shows is borrowed: the body goes into the dialog when it
3734
+ * opens and back where it was when it closes, so everything that
3735
+ * finds it through its node keeps working. One per editor, built
3736
+ * on first use, outside the canvas.
3737
+ * -------------------------------------------------------------- */
3738
+
3739
+ var dialogPanel = null;
3740
+ var dialogState = null; // What is open: { body, home, next, opener, listening }
3741
+
3742
+ function dialog() {
3743
+ if (dialogPanel) { return dialogPanel; }
3744
+
3745
+ dialogPanel = dom.create('<div class="modal fade ge-dialog" tabindex="-1" role="dialog" aria-hidden="true">' +
3746
+ '<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable">' +
3747
+ '<div class="modal-content">' +
3748
+ '<div class="modal-header"><h5 class="modal-title ge-dialog-title"></h5></div>' +
3749
+ '<div class="modal-body ge-dialog-body"></div>' +
3750
+ '<div class="modal-footer"></div>' +
3751
+ '</div>' +
3752
+ '</div>' +
3753
+ '</div>');
3754
+ dom.one(dialogPanel, '.modal-header').appendChild(dom.element('button', {
3755
+ type: 'button',
3756
+ 'class': 'btn-close ge-dialog-close',
3757
+ 'aria-label': t('panel.close'),
3758
+ }));
3759
+ dom.one(dialogPanel, '.modal-footer').appendChild(dom.element('button', {
3760
+ type: 'button',
3761
+ 'class': 'btn btn-primary ge-dialog-close',
3762
+ }, t('panel.done')));
3763
+
3764
+ dom.delegate(dialogPanel, 'click', '.ge-dialog-close', function(e) {
3765
+ e.preventDefault();
3766
+ closeDialog();
3767
+ });
3768
+
3769
+ return document.body.appendChild(dialogPanel);
3770
+ }
3771
+
3772
+ /**
3773
+ * ge.openDialog(title, body, opener): show `body` in the dialog. One
3774
+ * already open closes first. `opener`, when given, has the focus back
3775
+ * when it closes.
3776
+ */
3777
+ function openDialog(title, body, opener) {
3778
+ closeDialog();
3779
+
3780
+ var panel = dialog();
3781
+ var listening = new AbortController();
3782
+
3783
+ dialogState = {
3784
+ body: body,
3785
+ home: body.parentElement,
3786
+ next: body.nextElementSibling,
3787
+ opener: opener || document.activeElement,
3788
+ listening: listening,
3789
+ };
3790
+
3791
+ dom.one(panel, '.ge-dialog-title').textContent = title;
3792
+ dom.one(panel, '.ge-dialog-body').appendChild(body);
3793
+ dom.show(body);
3794
+
3795
+ // Bootstrap's modal takes its own Escape; a hand made one does not
3796
+ if (!modalLibrary()) {
3797
+ document.addEventListener('keydown', function(e) {
3798
+ if (e.key === 'Escape') { closeDialog(); }
3799
+ }, { signal: listening.signal });
3800
+ }
3801
+
3802
+ // Bootstrap puts a popover over a modal: the panel the dialog was
3803
+ // opened from goes under its backdrop while it is up
3804
+ Object.keys(settingsPanels).forEach(function(mode) {
3805
+ dom.addClass(settingsPanels[mode], 'ge-under-dialog');
3806
+ });
3807
+
3808
+ showModal(panel, function() { closeDialog(); });
3809
+ }
3810
+
3811
+ /** ge.closeDialog(): put the dialog away, and what it showed back where it was. */
3812
+ function closeDialog() {
3813
+ var open = dialogState;
3814
+ if (!open) { return; }
3815
+ dialogState = null;
3816
+
3817
+ open.listening.abort();
3818
+ hideModal(dialogPanel);
3819
+ Object.keys(settingsPanels).forEach(function(mode) {
3820
+ dom.removeClass(settingsPanels[mode], 'ge-under-dialog');
3821
+ });
3822
+
3823
+ dom.hide(open.body);
3824
+ if (open.home && dom.attached(open.home)) {
3825
+ if (open.next && open.next.parentElement === open.home) {
3826
+ open.home.insertBefore(open.body, open.next);
3827
+ } else {
3828
+ open.home.appendChild(open.body);
3829
+ }
3830
+ } else {
3831
+ open.body.remove();
3832
+ }
3833
+
3834
+ if (open.opener && dom.attached(open.opener) && open.opener.focus) { open.opener.focus(); }
3835
+ }
3836
+
3837
+ function removeDialog() {
3838
+ closeDialog();
3839
+ if (!dialogPanel) { return; }
3840
+
3841
+ removeBackdrop(dialogPanel);
3842
+ retireModal(dialogPanel);
3843
+ dialogPanel = null;
3844
+ }
3845
+
3509
3846
  /** What a panel's title calls a node. */
3510
3847
  function kindLabel(node) {
3511
3848
  var kind = kindOf(node);
@@ -4835,6 +5172,7 @@ function build(instance, baseElem, optionsOrMethod) {
4835
5172
  settingsScope().forEach(function(scope) {
4836
5173
  dom.all(scope, '.ge-utilities').forEach(function(section) { renderUtilities(section); });
4837
5174
  });
5175
+ renderPluginFields(null);
4838
5176
  refreshPreviews(canvas);
4839
5177
  plugins('onViewChange', key);
4840
5178
  emit('view-change', { canvas: canvas, breakpoint: key, from: from, to: key });