@themarioga/grid-editor 7.2.0 → 8.0.0-beta.2

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 (97) hide show
  1. package/CHANGELOG.md +101 -0
  2. package/README.md +96 -30
  3. package/UPGRADING.md +86 -0
  4. package/dist/grideditor.bundle.min.js +3 -3
  5. package/dist/grideditor.css +149 -6
  6. package/dist/grideditor.d.ts +18 -0
  7. package/dist/grideditor.esm.js +257 -48
  8. package/dist/grideditor.jquery.js +0 -13
  9. package/dist/grideditor.jquery.min.js +1 -1
  10. package/dist/grideditor.jquery.min.js.map +3 -3
  11. package/dist/grideditor.js +257 -48
  12. package/dist/grideditor.min.css +1 -1
  13. package/dist/grideditor.min.css.map +1 -1
  14. package/dist/grideditor.min.js +2 -2
  15. package/dist/grideditor.min.js.map +3 -3
  16. package/dist/locales/grideditor.es.esm.js +80 -6
  17. package/dist/locales/grideditor.es.js +80 -6
  18. package/dist/locales/grideditor.es.min.js +1 -1
  19. package/dist/locales/grideditor.es.min.js.map +2 -2
  20. package/dist/plugins/grideditor.accordion.min.js.map +2 -2
  21. package/dist/plugins/grideditor.alignment.esm.js +11 -38
  22. package/dist/plugins/grideditor.alignment.js +11 -38
  23. package/dist/plugins/grideditor.alignment.min.js +1 -1
  24. package/dist/plugins/grideditor.alignment.min.js.map +3 -3
  25. package/dist/plugins/grideditor.card.min.js.map +1 -1
  26. package/dist/plugins/grideditor.ckeditor.esm.js +0 -5
  27. package/dist/plugins/grideditor.ckeditor.js +0 -5
  28. package/dist/plugins/grideditor.ckeditor.min.js +1 -1
  29. package/dist/plugins/grideditor.ckeditor.min.js.map +3 -3
  30. package/dist/plugins/grideditor.clipboard.min.js.map +1 -1
  31. package/dist/plugins/grideditor.codemirror-inline.min.js.map +1 -1
  32. package/dist/plugins/grideditor.codemirror.min.js.map +1 -1
  33. package/dist/plugins/grideditor.elements.min.js.map +2 -2
  34. package/dist/plugins/grideditor.gutters.min.js.map +1 -1
  35. package/dist/plugins/grideditor.order.min.js.map +1 -1
  36. package/dist/plugins/grideditor.popup.min.js.map +1 -1
  37. package/dist/plugins/grideditor.sections.min.js.map +2 -2
  38. package/dist/plugins/grideditor.style.esm.js +1926 -0
  39. package/dist/plugins/grideditor.style.js +1923 -0
  40. package/dist/plugins/grideditor.style.min.js +4 -0
  41. package/dist/plugins/grideditor.style.min.js.map +7 -0
  42. package/dist/plugins/grideditor.summernote.min.js.map +2 -2
  43. package/dist/plugins/grideditor.tabs.min.js.map +2 -2
  44. package/dist/plugins/grideditor.tinymce.esm.js +1 -8
  45. package/dist/plugins/grideditor.tinymce.js +1 -8
  46. package/dist/plugins/grideditor.tinymce.min.js +1 -1
  47. package/dist/plugins/grideditor.tinymce.min.js.map +3 -3
  48. package/docs/events.md +4 -6
  49. package/docs/locale-keys.md +99 -15
  50. package/docs/plugins.md +47 -23
  51. package/example/ckeditor.html +0 -5
  52. package/example/index.html +17 -2
  53. package/example/style.html +163 -0
  54. package/example/utilities.html +21 -16
  55. package/package.json +1 -1
  56. package/src/js/dom.js +1 -2
  57. package/src/js/grideditor.jquery.js +7 -28
  58. package/src/js/grideditor.js +336 -67
  59. package/src/js/locales/grideditor.es.js +80 -6
  60. package/src/js/plugins/grideditor.alignment.js +21 -50
  61. package/src/js/plugins/grideditor.ckeditor.js +10 -23
  62. package/src/js/plugins/grideditor.style.js +185 -0
  63. package/src/js/plugins/grideditor.summernote.js +4 -5
  64. package/src/js/plugins/grideditor.tinymce.js +4 -17
  65. package/src/js/style/accordion.js +168 -0
  66. package/src/js/style/catalog.js +76 -0
  67. package/src/js/style/custom.js +84 -0
  68. package/src/js/style/display.js +229 -0
  69. package/src/js/style/drawerflow.js +118 -0
  70. package/src/js/style/fields.js +382 -0
  71. package/src/js/style/flex.js +156 -0
  72. package/src/js/{plugins/grideditor.float.js → style/float.js} +6 -16
  73. package/src/js/style/options.js +60 -0
  74. package/src/js/style/sections.js +298 -0
  75. package/src/js/{plugins/grideditor.spacing.js → style/spacing.js} +7 -19
  76. package/src/js/style/sticky.js +39 -0
  77. package/src/js/{plugins/grideditor.textalign.js → style/textalign.js} +6 -14
  78. package/src/js/text/grideditor.text.js +1 -1
  79. package/src/less/grideditor.less +151 -10
  80. package/types/grideditor.d.ts +18 -0
  81. package/dist/plugins/grideditor.float.esm.js +0 -34
  82. package/dist/plugins/grideditor.float.js +0 -41
  83. package/dist/plugins/grideditor.float.min.js +0 -1
  84. package/dist/plugins/grideditor.float.min.js.map +0 -7
  85. package/dist/plugins/grideditor.spacing.esm.js +0 -318
  86. package/dist/plugins/grideditor.spacing.js +0 -323
  87. package/dist/plugins/grideditor.spacing.min.js +0 -1
  88. package/dist/plugins/grideditor.spacing.min.js.map +0 -7
  89. package/dist/plugins/grideditor.textalign.esm.js +0 -40
  90. package/dist/plugins/grideditor.textalign.js +0 -47
  91. package/dist/plugins/grideditor.textalign.min.js +0 -1
  92. package/dist/plugins/grideditor.textalign.min.js.map +0 -7
  93. package/dist/plugins/grideditor.visibility.esm.js +0 -158
  94. package/dist/plugins/grideditor.visibility.js +0 -163
  95. package/dist/plugins/grideditor.visibility.min.js +0 -1
  96. package/dist/plugins/grideditor.visibility.min.js.map +0 -7
  97. package/src/js/plugins/grideditor.visibility.js +0 -171
@@ -4,9 +4,8 @@
4
4
  * A fork of https://github.com/Friendly-Pixel/grid-editor by Simon Epskamp,
5
5
  * maintained at https://github.com/themarioga/grid-editor.
6
6
  *
7
- * Plain DOM since 7.0: nothing here needs jQuery. A page written for the 6.x
8
- * jQuery API loads grideditor.jquery.js beside this file and keeps working;
9
- * see UPGRADING.md.
7
+ * Plain DOM: nothing here needs jQuery. A page written against the jQuery
8
+ * API loads grideditor.jquery.js beside this file.
10
9
  */
11
10
  import * as dom from './dom.js';
12
11
 
@@ -73,8 +72,6 @@ var ALL_VIEW_LABEL_KEY = 'view.all';
73
72
  /** Every view key the dropdown can offer, in the order it offers them. */
74
73
  var VIEW_KEYS = [ALL_VIEW].concat(BREAKPOINTS.map(function(tier) { return tier.key; }));
75
74
 
76
- /** What the three layout mode indexes of 2.x meant. */
77
- var LEGACY_VIEW_INDEXES = ['lg', 'sm', 'xs'];
78
75
 
79
76
  var MAX_COL_SIZE = 12;
80
77
  var MAX_COL_OFFSET = 11;
@@ -158,12 +155,6 @@ var NESTED_SETTINGS = {
158
155
  },
159
156
  };
160
157
 
161
- /** Settings 4.0 took away, and what a host should reach for instead. */
162
- var REMOVED_SETTINGS = {
163
- sortable_options: 'drag',
164
- resizable_options: 'resize',
165
- };
166
-
167
158
  var warned = {};
168
159
 
169
160
  /** Editors on the page, counted so each one's sortable groups are its own. */
@@ -372,7 +363,7 @@ function build(instance, baseElem, optionsOrMethod) {
372
363
  'element_tools' : [], // Host tools on element drawers, same shape as row_tools
373
364
  'element_classes' : [], // Preset class toggles on an element's settings panel
374
365
  // content_types, text_tools and text_classes are the text editor
375
- // plugins' settings since 6.0: see grideditor.text.js
366
+ // plugins' settings: see grideditor.text.js
376
367
  'container_classes' : [], // The same, on a container's panel
377
368
  'pane_classes' : [], // And on a tab's or an accordion item's
378
369
  'container_tools' : [], // Host tools on container drawers
@@ -405,18 +396,6 @@ function build(instance, baseElem, optionsOrMethod) {
405
396
  settings[name] = Object.assign({}, NESTED_SETTINGS[name], settings[name]);
406
397
  });
407
398
 
408
- // Both handed out the drag toolkit's own options, which 4.0 stops
409
- // promising: there is no widget underneath a host should be reaching
410
- // for. What they were used for is a setting of the editor's now.
411
- Object.keys(REMOVED_SETTINGS).forEach(function(name) {
412
- if (optionsOrMethod && optionsOrMethod[name] !== undefined) {
413
- warn(translate(settings, 'warning.setting_removed', {
414
- setting: name,
415
- replacement: REMOVED_SETTINGS[name],
416
- }));
417
- }
418
- });
419
-
420
399
 
421
400
  // Elems
422
401
  var canvas,
@@ -432,7 +411,7 @@ function build(instance, baseElem, optionsOrMethod) {
432
411
  var sizePicker = null; // The open column size picker, if there is one
433
412
  var sourceOpen = false; // Whether the canvas is being edited as html
434
413
  var dropMarker = null; // The line showing where a dragged toolbar button would land
435
- var warnedHere = {}; // Deprecations are worth saying once per instance, not once per call
414
+ var warnedHere = {}; // Some warnings are worth saying once per instance, not once per call
436
415
  var sortables = []; // Every list made sortable, so deinit destroys exactly those
437
416
  var instanceId = ++editorCounter; // Scopes the sortable groups to this editor
438
417
  var destroyed = false;
@@ -475,6 +454,7 @@ function build(instance, baseElem, optionsOrMethod) {
475
454
 
476
455
  removeConfirmModal();
477
456
  removeSettingsPanels();
457
+ removeDialog();
478
458
  mainControls.remove();
479
459
  createMainControls();
480
460
  reset();
@@ -1753,6 +1733,10 @@ function build(instance, baseElem, optionsOrMethod) {
1753
1733
  if (openSettingsState && !dom.attached(openSettingsState.node)) {
1754
1734
  closeSettings();
1755
1735
  }
1736
+ // And a dialog showing part of a panel that is gone
1737
+ if (dialogState && dialogState.home && !dom.attached(dialogState.home)) {
1738
+ closeDialog();
1739
+ }
1756
1740
 
1757
1741
  runFilter(true);
1758
1742
  dom.addClass(canvas, 'ge-editing');
@@ -1781,7 +1765,10 @@ function build(instance, baseElem, optionsOrMethod) {
1781
1765
  plugins('onBeforeDeinit');
1782
1766
  closeSizePicker();
1783
1767
  hideDropMarker();
1768
+ // In case a dialog was opened with no settings panel open
1769
+ closeDialog();
1784
1770
  dom.all(canvas, '.ge-tools-drawer').forEach(function(drawer) { drawer.remove(); });
1771
+ pluginFields.clear();
1785
1772
  unwrapTexts();
1786
1773
  plugins('onDeinit');
1787
1774
  // After the rich text editors have let go of their content areas:
@@ -1836,6 +1823,7 @@ function build(instance, baseElem, optionsOrMethod) {
1836
1823
  deinit();
1837
1824
  removeConfirmModal();
1838
1825
  removeSettingsPanels();
1826
+ removeDialog();
1839
1827
  mainControls.remove();
1840
1828
  htmlTextArea.remove();
1841
1829
  lifetime.abort();
@@ -1948,6 +1936,12 @@ function build(instance, baseElem, optionsOrMethod) {
1948
1936
  setUtility: setUtility,
1949
1937
  utilityField: utilityField,
1950
1938
  bareStyle: bareStyle,
1939
+ // The host's own style, under the preview
1940
+ hostStyle: hostStyle,
1941
+ setHostStyle: setHostStyle,
1942
+ // A modal of the editor's own, over the settings
1943
+ openDialog: openDialog,
1944
+ closeDialog: closeDialog,
1951
1945
  rowFromLayout: rowFromLayoutValue,
1952
1946
  nodeHtml: nodeHtml,
1953
1947
  // A text editor has rewritten a content area, so whatever the
@@ -2266,6 +2260,7 @@ function build(instance, baseElem, optionsOrMethod) {
2266
2260
  if (classes) { classes.value = hostClasses(node).join(' '); }
2267
2261
  dom.children(details, '.ge-utilities').forEach(function(section) { renderUtilities(section); });
2268
2262
  }
2263
+ renderPluginFields(node);
2269
2264
  refreshPreviews(node);
2270
2265
  }
2271
2266
 
@@ -2362,6 +2357,84 @@ function build(instance, baseElem, optionsOrMethod) {
2362
2357
  });
2363
2358
  }
2364
2359
 
2360
+ /* The host's own style, under the preview */
2361
+
2362
+ /**
2363
+ * The node's style as the host wrote it: its declarations with every
2364
+ * property the preview set put back to what the preview recorded.
2365
+ * Held in a detached element's style, which parses and serializes it
2366
+ * the way the node's own would.
2367
+ */
2368
+ function hostDeclaration(node) {
2369
+ var scratch = document.createElement('div');
2370
+ var was = {};
2371
+
2372
+ scratch.style.cssText = node.style.cssText;
2373
+ try { was = JSON.parse(node.getAttribute(PREVIEW_ATTR)) || {}; } catch (error) { /* no record */ }
2374
+
2375
+ Object.keys(was).forEach(function(property) {
2376
+ var before = was[property];
2377
+
2378
+ if (before && before[0]) {
2379
+ scratch.style.setProperty(property, before[0], before[1]);
2380
+ } else {
2381
+ scratch.style.removeProperty(property);
2382
+ }
2383
+ });
2384
+
2385
+ return scratch.style;
2386
+ }
2387
+
2388
+ /**
2389
+ * ge.hostStyle(node, property): { value, priority } of one property
2390
+ * of the host's style, never the preview's. With no property, the
2391
+ * host's whole style as css text.
2392
+ */
2393
+ function hostStyle(node, property) {
2394
+ var declaration = hostDeclaration(node);
2395
+
2396
+ if (property === undefined) { return declaration.cssText; }
2397
+
2398
+ return {
2399
+ value: declaration.getPropertyValue(property),
2400
+ priority: declaration.getPropertyPriority(property),
2401
+ };
2402
+ }
2403
+
2404
+ /**
2405
+ * ge.setHostStyle(node, property, value, priority): write one property
2406
+ * of the host's style, or take it off with an empty value. The
2407
+ * preview comes off the node while it is written and goes back on
2408
+ * after, so what the preview put back on deinit is the host's new
2409
+ * value. False, and nothing written, when the browser refuses the
2410
+ * value. `priority` left out keeps the one the property had.
2411
+ */
2412
+ function setHostStyle(node, property, value, priority) {
2413
+ value = value === null || value === undefined ? '' : String(value).trim();
2414
+
2415
+ if (priority === undefined) { priority = hostStyle(node, property).priority; }
2416
+
2417
+ if (value !== '') {
2418
+ var check = document.createElement('div').style;
2419
+ check.setProperty(property, value, priority || '');
2420
+ if (check.getPropertyValue(property) === '') { return false; }
2421
+ }
2422
+
2423
+ var previewed = node.hasAttribute(PREVIEW_ATTR);
2424
+ if (previewed) { clearPreviews(node); }
2425
+
2426
+ if (value === '') {
2427
+ node.style.removeProperty(property);
2428
+ } else {
2429
+ node.style.setProperty(property, value, priority || '');
2430
+ }
2431
+ dom.dropEmptyStyle(node);
2432
+
2433
+ if (previewed) { refreshPreviews(node); }
2434
+
2435
+ return true;
2436
+ }
2437
+
2365
2438
  /* The panel */
2366
2439
 
2367
2440
  /**
@@ -2434,10 +2507,32 @@ function build(instance, baseElem, optionsOrMethod) {
2434
2507
 
2435
2508
  var field = createField(node, family);
2436
2509
  renderField(field, node);
2510
+ pluginFields.set(field, node);
2437
2511
 
2438
2512
  return field;
2439
2513
  }
2440
2514
 
2515
+ /**
2516
+ * The fields a plugin made, wherever it put them: a Responsive
2517
+ * section renders its own, and these are the rest - in a plugin's
2518
+ * section of the panel, or in the dialog. Each one's node's, or
2519
+ * every node's; a field no longer on the page is forgotten.
2520
+ */
2521
+ var pluginFields = new Map(); // field -> the node it edits
2522
+
2523
+ function renderPluginFields(node) {
2524
+ pluginFields.forEach(function(fieldNode, field) {
2525
+ if (!field.isConnected) {
2526
+ pluginFields.delete(field);
2527
+ return;
2528
+ }
2529
+ if (node && fieldNode !== node) { return; }
2530
+ if (dom.closest(field, '.ge-utilities')) { return; }
2531
+
2532
+ renderField(field, fieldNode);
2533
+ });
2534
+ }
2535
+
2441
2536
  /** Fill a section's fields, the families' and the plugins' own, for the view being edited. */
2442
2537
  function renderUtilities(section) {
2443
2538
  var node = sectionNodes.get(section);
@@ -3146,8 +3241,7 @@ function build(instance, baseElem, optionsOrMethod) {
3146
3241
  refreshUtilities(container);
3147
3242
  });
3148
3243
 
3149
- // Bootstrap 5's outline buttons, filled while their class is on;
3150
- // up to 5.x they carried Bootstrap 3's btn-default, which 5 lacks
3244
+ // Bootstrap's outline buttons, filled while their class is on
3151
3245
  var classGroup = general.appendChild(dom.element('div', { 'class': 'btn-group btn-group-sm ge-presets', role: 'group' }));
3152
3246
  cssClasses.forEach(function(rowClass) {
3153
3247
  var on = dom.hasClass(container, rowClass.cssClass);
@@ -3158,7 +3252,7 @@ function build(instance, baseElem, optionsOrMethod) {
3158
3252
  'aria-pressed': on ? 'true' : 'false',
3159
3253
  });
3160
3254
 
3161
- // A preset's label is the host's markup, as it was in 6.x
3255
+ // A preset's label is the host's markup
3162
3256
  btn.innerHTML = rowClass.label;
3163
3257
  dom.toggleClass(btn, 'active', on);
3164
3258
  btn.addEventListener('click', function() {
@@ -3170,12 +3264,59 @@ function build(instance, baseElem, optionsOrMethod) {
3170
3264
  classGroup.appendChild(btn);
3171
3265
  });
3172
3266
 
3267
+ pluginSections(container).forEach(function(section) { detailsDiv.appendChild(section); });
3268
+
3173
3269
  var utilities = createUtilitiesSection(container);
3174
3270
  if (utilities) { detailsDiv.appendChild(utilities); }
3175
3271
 
3176
3272
  return detailsDiv;
3177
3273
  }
3178
3274
 
3275
+ /**
3276
+ * What plugins add to a node's panel, between its general fields and
3277
+ * the Responsive section: each plugin's panelSection(node, kind), in
3278
+ * the order they were registered. Where the panel has room - the
3279
+ * offcanvas, the modal - the section is in it. A popover or a panel
3280
+ * inline in the drawer gets a button instead, and the section opens
3281
+ * in the dialog.
3282
+ */
3283
+ function pluginSections(node) {
3284
+ var kind = kindOf(node);
3285
+ var mode = panelMode();
3286
+ var sections = [];
3287
+
3288
+ [CONTAINERS, FEATURES, UTILITIES].forEach(function(registry) {
3289
+ Object.keys(registry).forEach(function(name) {
3290
+ var plugin = registry[name];
3291
+ var section = plugin.panelSection ? plugin.panelSection(node, kind) : null;
3292
+ if (!section || !section.body) { return; }
3293
+
3294
+ var holder = dom.element('div', { 'class': 'ge-panel-section', 'data-ge-plugin': name });
3295
+
3296
+ if (mode === 'offcanvas' || mode === 'modal') {
3297
+ holder.appendChild(section.body);
3298
+ } else {
3299
+ var label = t(section.labelKey);
3300
+ var open = holder.appendChild(dom.element('button', {
3301
+ type: 'button',
3302
+ 'class': 'btn btn-sm btn-outline-secondary ge-panel-section-open',
3303
+ }, label));
3304
+
3305
+ dom.hide(section.body);
3306
+ holder.appendChild(section.body);
3307
+ open.addEventListener('click', function() {
3308
+ openDialog(section.titleKey ? t(section.titleKey, { kind: kindLabel(node) }) : label,
3309
+ section.body, open);
3310
+ });
3311
+ }
3312
+
3313
+ sections.push(holder);
3314
+ });
3315
+ });
3316
+
3317
+ return sections;
3318
+ }
3319
+
3179
3320
  /* --------------------------------------------------------------
3180
3321
  * Where a node's settings open.
3181
3322
  *
@@ -3194,7 +3335,7 @@ function build(instance, baseElem, optionsOrMethod) {
3194
3335
  var PANEL_MODES = ['offcanvas', 'popover', 'modal', 'inline'];
3195
3336
  var settingsPanels = {}; // One of each, built on first use, outside the canvas
3196
3337
  var openSettingsState = null; // What is open: { mode, node, details, home, next, gear }
3197
- var modalState = new WeakMap(); // modal panel -> { watched, wanted, busy }
3338
+ var modalState = new WeakMap(); // modal panel -> { watched, wanted, busy, backdrop, dismissed }
3198
3339
 
3199
3340
  function panelMode() {
3200
3341
  if (PANEL_MODES.indexOf(settings.settings_panel) !== -1) { return settings.settings_panel; }
@@ -3251,7 +3392,8 @@ function build(instance, baseElem, optionsOrMethod) {
3251
3392
 
3252
3393
  var signal = { signal: listening.signal };
3253
3394
  document.addEventListener('keydown', function(e) {
3254
- if (e.key === 'Escape') { closeSettings(); }
3395
+ // A dialog over the panel takes the Escape for itself
3396
+ if (e.key === 'Escape' && !dialogState) { closeSettings(); }
3255
3397
  }, signal);
3256
3398
 
3257
3399
  if (mode === 'offcanvas') {
@@ -3274,15 +3416,20 @@ function build(instance, baseElem, optionsOrMethod) {
3274
3416
  }
3275
3417
  // A press anywhere else puts it away, as a popover does
3276
3418
  dom.on(document, 'mousedown touchstart', function(e) {
3277
- if (!panel.contains(e.target) && !gear.contains(e.target)) { closeSettings(); }
3419
+ if (!dialogState && !panel.contains(e.target) && !gear.contains(e.target)) { closeSettings(); }
3278
3420
  }, signal);
3279
3421
  } else {
3280
- showModal(panel);
3422
+ showModal(panel, function() {
3423
+ if (openSettingsState && openSettingsState.mode === 'modal') { closeSettings(); }
3424
+ });
3281
3425
  }
3282
3426
  }
3283
3427
 
3284
3428
  /** Put what is open away, and its panel back in its drawer. */
3285
3429
  function closeSettings() {
3430
+ // A dialog opened from the panel goes with it
3431
+ closeDialog();
3432
+
3286
3433
  var open = openSettingsState;
3287
3434
  if (!open) { return; }
3288
3435
  openSettingsState = null;
@@ -3381,7 +3528,7 @@ function build(instance, baseElem, optionsOrMethod) {
3381
3528
  settingsPanels[mode].remove();
3382
3529
  }
3383
3530
  });
3384
- if (settingsBackdrop) { settingsBackdrop.remove(); settingsBackdrop = null; }
3531
+ Object.keys(settingsPanels).forEach(function(mode) { removeBackdrop(settingsPanels[mode]); });
3385
3532
  settingsPanels = {};
3386
3533
  }
3387
3534
 
@@ -3424,23 +3571,34 @@ function build(instance, baseElem, optionsOrMethod) {
3424
3571
  });
3425
3572
  }
3426
3573
 
3427
- var settingsBackdrop = null; // The modal's backdrop, when Bootstrap's javascript is not there to make one
3428
-
3429
3574
  /**
3430
3575
  * Bootstrap's modal ignores a hide while it is still fading in, and a
3431
3576
  * show while it is fading out, so the editor asks for what it wants
3432
3577
  * and has it done once the running transition ends: a panel closed as
3433
3578
  * it opens - getHtml, a second click - would otherwise stay open, and
3434
3579
  * empty. A hide Bootstrap starts itself, from Escape or the backdrop,
3435
- * closes the settings as the close button does.
3580
+ * calls `dismissed`, as the close button does: the settings modal
3581
+ * closes the settings, the dialog closes the dialog.
3582
+ *
3583
+ * Without Bootstrap's javascript the modal is shown by hand, over a
3584
+ * backdrop of the editor's own that dismisses it when clicked.
3436
3585
  */
3437
- function showModal(panel) {
3586
+ function showModal(panel, dismissed) {
3438
3587
  var Modal = modalLibrary();
3588
+ var state = modalState.get(panel);
3589
+
3590
+ if (!state) {
3591
+ state = { wanted: null, busy: false, backdrop: null, dismissed: dismissed };
3592
+ modalState.set(panel, state);
3593
+ }
3594
+ state.dismissed = dismissed;
3439
3595
 
3440
3596
  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);
3597
+ if (!state.backdrop) {
3598
+ state.backdrop = dom.element('div', { 'class': 'modal-backdrop fade show ge-settings-backdrop' });
3599
+ state.backdrop.addEventListener('click', function() { state.dismissed(); });
3600
+ document.body.appendChild(state.backdrop);
3601
+ }
3444
3602
  dom.addClass(panel, 'show');
3445
3603
  panel.style.display = 'block';
3446
3604
  panel.removeAttribute('aria-hidden');
@@ -3448,11 +3606,9 @@ function build(instance, baseElem, optionsOrMethod) {
3448
3606
  }
3449
3607
 
3450
3608
  var modal = Modal.getOrCreateInstance(panel);
3451
- var state = modalState.get(panel);
3452
3609
 
3453
- if (!state) {
3454
- state = { wanted: null, busy: false };
3455
- modalState.set(panel, state);
3610
+ if (!state.watched) {
3611
+ state.watched = true;
3456
3612
  trackModal(panel);
3457
3613
 
3458
3614
  panel.addEventListener('hide.bs.modal', function() {
@@ -3469,9 +3625,9 @@ function build(instance, baseElem, optionsOrMethod) {
3469
3625
  panel.addEventListener('hidden.bs.modal', function() {
3470
3626
  state.busy = false;
3471
3627
  if (state.wanted === 'open') {
3472
- showModal(panel);
3473
- } else if (openSettingsState && openSettingsState.mode === 'modal') {
3474
- closeSettings();
3628
+ showModal(panel, state.dismissed);
3629
+ } else {
3630
+ state.dismissed();
3475
3631
  }
3476
3632
  });
3477
3633
  }
@@ -3485,18 +3641,19 @@ function build(instance, baseElem, optionsOrMethod) {
3485
3641
 
3486
3642
  function hideModal(panel) {
3487
3643
  var Modal = modalLibrary();
3644
+ var state = modalState.get(panel);
3645
+
3646
+ if (!state) { return; }
3647
+ state.wanted = 'closed';
3488
3648
 
3489
3649
  if (!Modal) {
3490
- if (settingsBackdrop) { settingsBackdrop.remove(); settingsBackdrop = null; }
3650
+ if (state.backdrop) { state.backdrop.remove(); state.backdrop = null; }
3491
3651
  dom.removeClass(panel, 'show');
3492
3652
  panel.style.removeProperty('display');
3493
3653
  panel.setAttribute('aria-hidden', 'true');
3494
3654
  return;
3495
3655
  }
3496
3656
 
3497
- var state = modalState.get(panel) || { wanted: null, busy: false };
3498
- state.wanted = 'closed';
3499
-
3500
3657
  // Already out of sight - Bootstrap hid it itself, and this is its
3501
3658
  // hidden event closing the settings - and nothing is under way:
3502
3659
  // a hide now would be ignored, and no event would come to say so
@@ -3506,6 +3663,131 @@ function build(instance, baseElem, optionsOrMethod) {
3506
3663
  }
3507
3664
  }
3508
3665
 
3666
+ /** A modal's hand made backdrop goes with it. */
3667
+ function removeBackdrop(panel) {
3668
+ var state = modalState.get(panel);
3669
+ if (state && state.backdrop) { state.backdrop.remove(); state.backdrop = null; }
3670
+ }
3671
+
3672
+ /* --------------------------------------------------------------
3673
+ * The dialog: a modal of the editor's own, over whatever settings
3674
+ * panel is open, for what does not fit in one. A plugin's section
3675
+ * of the panel opens in it when the panel is a popover or inline,
3676
+ * and a plugin can open one itself through ge.openDialog.
3677
+ *
3678
+ * What it shows is borrowed: the body goes into the dialog when it
3679
+ * opens and back where it was when it closes, so everything that
3680
+ * finds it through its node keeps working. One per editor, built
3681
+ * on first use, outside the canvas.
3682
+ * -------------------------------------------------------------- */
3683
+
3684
+ var dialogPanel = null;
3685
+ var dialogState = null; // What is open: { body, home, next, opener, listening }
3686
+
3687
+ function dialog() {
3688
+ if (dialogPanel) { return dialogPanel; }
3689
+
3690
+ dialogPanel = dom.create('<div class="modal fade ge-dialog" tabindex="-1" role="dialog" aria-hidden="true">' +
3691
+ '<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable">' +
3692
+ '<div class="modal-content">' +
3693
+ '<div class="modal-header"><h5 class="modal-title ge-dialog-title"></h5></div>' +
3694
+ '<div class="modal-body ge-dialog-body"></div>' +
3695
+ '<div class="modal-footer"></div>' +
3696
+ '</div>' +
3697
+ '</div>' +
3698
+ '</div>');
3699
+ dom.one(dialogPanel, '.modal-header').appendChild(dom.element('button', {
3700
+ type: 'button',
3701
+ 'class': 'btn-close ge-dialog-close',
3702
+ 'aria-label': t('panel.close'),
3703
+ }));
3704
+ dom.one(dialogPanel, '.modal-footer').appendChild(dom.element('button', {
3705
+ type: 'button',
3706
+ 'class': 'btn btn-primary ge-dialog-close',
3707
+ }, t('panel.done')));
3708
+
3709
+ dom.delegate(dialogPanel, 'click', '.ge-dialog-close', function(e) {
3710
+ e.preventDefault();
3711
+ closeDialog();
3712
+ });
3713
+
3714
+ return document.body.appendChild(dialogPanel);
3715
+ }
3716
+
3717
+ /**
3718
+ * ge.openDialog(title, body, opener): show `body` in the dialog. One
3719
+ * already open closes first. `opener`, when given, has the focus back
3720
+ * when it closes.
3721
+ */
3722
+ function openDialog(title, body, opener) {
3723
+ closeDialog();
3724
+
3725
+ var panel = dialog();
3726
+ var listening = new AbortController();
3727
+
3728
+ dialogState = {
3729
+ body: body,
3730
+ home: body.parentElement,
3731
+ next: body.nextElementSibling,
3732
+ opener: opener || document.activeElement,
3733
+ listening: listening,
3734
+ };
3735
+
3736
+ dom.one(panel, '.ge-dialog-title').textContent = title;
3737
+ dom.one(panel, '.ge-dialog-body').appendChild(body);
3738
+ dom.show(body);
3739
+
3740
+ // Bootstrap's modal takes its own Escape; a hand made one does not
3741
+ if (!modalLibrary()) {
3742
+ document.addEventListener('keydown', function(e) {
3743
+ if (e.key === 'Escape') { closeDialog(); }
3744
+ }, { signal: listening.signal });
3745
+ }
3746
+
3747
+ // Bootstrap puts a popover over a modal: the panel the dialog was
3748
+ // opened from goes under its backdrop while it is up
3749
+ Object.keys(settingsPanels).forEach(function(mode) {
3750
+ dom.addClass(settingsPanels[mode], 'ge-under-dialog');
3751
+ });
3752
+
3753
+ showModal(panel, function() { closeDialog(); });
3754
+ }
3755
+
3756
+ /** ge.closeDialog(): put the dialog away, and what it showed back where it was. */
3757
+ function closeDialog() {
3758
+ var open = dialogState;
3759
+ if (!open) { return; }
3760
+ dialogState = null;
3761
+
3762
+ open.listening.abort();
3763
+ hideModal(dialogPanel);
3764
+ Object.keys(settingsPanels).forEach(function(mode) {
3765
+ dom.removeClass(settingsPanels[mode], 'ge-under-dialog');
3766
+ });
3767
+
3768
+ dom.hide(open.body);
3769
+ if (open.home && dom.attached(open.home)) {
3770
+ if (open.next && open.next.parentElement === open.home) {
3771
+ open.home.insertBefore(open.body, open.next);
3772
+ } else {
3773
+ open.home.appendChild(open.body);
3774
+ }
3775
+ } else {
3776
+ open.body.remove();
3777
+ }
3778
+
3779
+ if (open.opener && dom.attached(open.opener) && open.opener.focus) { open.opener.focus(); }
3780
+ }
3781
+
3782
+ function removeDialog() {
3783
+ closeDialog();
3784
+ if (!dialogPanel) { return; }
3785
+
3786
+ removeBackdrop(dialogPanel);
3787
+ retireModal(dialogPanel);
3788
+ dialogPanel = null;
3789
+ }
3790
+
3509
3791
  /** What a panel's title calls a node. */
3510
3792
  function kindLabel(node) {
3511
3793
  var kind = kindOf(node);
@@ -4802,20 +5084,8 @@ function build(instance, baseElem, optionsOrMethod) {
4802
5084
  dom.one(layoutDropdown, 'button').textContent = t(labelKeyFor(view));
4803
5085
  }
4804
5086
 
4805
- /**
4806
- * The view key a caller asked for, or null. 2.x callers passed a
4807
- * layout mode index, which still works and says so once.
4808
- */
5087
+ /** The view key a caller asked for, or null. */
4809
5088
  function viewKey(view) {
4810
- if (typeof view == 'number') {
4811
- warnOnceHere('changeView-index', 'changeView(' + view + '): layout modes are ' +
4812
- 'identified by breakpoint key now, so pass one of ' +
4813
- JSON.stringify(VIEW_KEYS) + '. Numeric indexes still work, ' +
4814
- 'but they mean what they meant in 2.x (' +
4815
- LEGACY_VIEW_INDEXES.join(', ') + ') and will be dropped.');
4816
- return LEGACY_VIEW_INDEXES[view] || null;
4817
- }
4818
-
4819
5089
  return VIEW_KEYS.indexOf(view) === -1 ? null : view;
4820
5090
  }
4821
5091
 
@@ -4835,6 +5105,7 @@ function build(instance, baseElem, optionsOrMethod) {
4835
5105
  settingsScope().forEach(function(scope) {
4836
5106
  dom.all(scope, '.ge-utilities').forEach(function(section) { renderUtilities(section); });
4837
5107
  });
5108
+ renderPluginFields(null);
4838
5109
  refreshPreviews(canvas);
4839
5110
  plugins('onViewChange', key);
4840
5111
  emit('view-change', { canvas: canvas, breakpoint: key, from: from, to: key });
@@ -5071,14 +5342,12 @@ GridEditor.locales = {
5071
5342
  'utility.inherit': 'Inherit: {value} (from {breakpoint})',
5072
5343
  'utility.varies': 'Changes at {breakpoints}; choosing here replaces that',
5073
5344
  'error.sortable_missing': 'SortableJS not available! Make sure you loaded the Sortable js file; dragging is off without it.',
5074
- 'warning.setting_removed': 'The {setting} setting was removed in 4.0. Use {replacement} instead.',
5075
5345
  'warning.already_editing': 'This element already has an editor: that one is handed back, with the options it was made with.',
5076
5346
  'warning.destroyed': '{method}() was called on an editor that has been destroyed, and does nothing.',
5077
5347
  'warning.duplicate_build': 'grideditor.js was loaded twice: the first GridEditor is kept.',
5078
- 'warning.plugin_6x': 'The "{name}" plugin is written for grid-editor 6 and is not loaded. Plugins register on GridEditor since 7.0: see UPGRADING.md.',
5079
5348
  'warning.adapter_no_jquery': 'grideditor.jquery.js needs jQuery 4, and there is no jQuery on the page: the jQuery API is not there.',
5080
5349
  'error.tinymce_missing': 'tinyMCE not available! Make sure you loaded the tinyMCE js file.',
5081
- 'error.ckeditor_missing': 'CKEditor 5 not available! Make sure you loaded its ckeditor5.umd.js file: CKEditor 4 is no longer supported.',
5350
+ 'error.ckeditor_missing': 'CKEditor 5 not available! Make sure you loaded its ckeditor5.umd.js file.',
5082
5351
  'error.summernote_missing': 'Summernote not available! Make sure you loaded jQuery and the Summernote js file.',
5083
5352
  },
5084
5353
  };