@themarioga/grid-editor 8.5.0 → 8.7.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 (105) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +78 -4
  3. package/dist/grideditor.bundle.min.js +3 -3
  4. package/dist/grideditor.css +15 -4
  5. package/dist/grideditor.d.ts +59 -3
  6. package/dist/grideditor.esm.js +226 -38
  7. package/dist/grideditor.js +226 -38
  8. package/dist/grideditor.min.css +1 -1
  9. package/dist/grideditor.min.css.map +1 -1
  10. package/dist/grideditor.min.js +2 -2
  11. package/dist/grideditor.min.js.map +3 -3
  12. package/dist/locales/grideditor.es.esm.js +4 -0
  13. package/dist/locales/grideditor.es.js +4 -0
  14. package/dist/locales/grideditor.es.min.js +1 -1
  15. package/dist/locales/grideditor.es.min.js.map +2 -2
  16. package/dist/plugins/grideditor.accordion.esm.js +7 -0
  17. package/dist/plugins/grideditor.accordion.js +7 -0
  18. package/dist/plugins/grideditor.accordion.min.js +1 -1
  19. package/dist/plugins/grideditor.accordion.min.js.map +3 -3
  20. package/dist/plugins/grideditor.autosave.esm.js +309 -0
  21. package/dist/plugins/grideditor.autosave.js +316 -0
  22. package/dist/plugins/grideditor.autosave.min.js +1 -0
  23. package/dist/plugins/grideditor.autosave.min.js.map +7 -0
  24. package/dist/plugins/grideditor.card.esm.js +10 -0
  25. package/dist/plugins/grideditor.card.js +10 -0
  26. package/dist/plugins/grideditor.card.min.js +1 -1
  27. package/dist/plugins/grideditor.card.min.js.map +3 -3
  28. package/dist/plugins/grideditor.ckeditor.esm.js +29 -0
  29. package/dist/plugins/grideditor.ckeditor.js +29 -0
  30. package/dist/plugins/grideditor.ckeditor.min.js +1 -1
  31. package/dist/plugins/grideditor.ckeditor.min.js.map +3 -3
  32. package/dist/plugins/grideditor.clipboard.esm.js +3 -0
  33. package/dist/plugins/grideditor.clipboard.js +3 -0
  34. package/dist/plugins/grideditor.clipboard.min.js +1 -1
  35. package/dist/plugins/grideditor.clipboard.min.js.map +2 -2
  36. package/dist/plugins/grideditor.codemirror-inline.esm.js +11 -0
  37. package/dist/plugins/grideditor.codemirror-inline.js +11 -0
  38. package/dist/plugins/grideditor.codemirror-inline.min.js +1 -1
  39. package/dist/plugins/grideditor.codemirror-inline.min.js.map +3 -3
  40. package/dist/plugins/grideditor.elements.esm.js +6 -3
  41. package/dist/plugins/grideditor.elements.js +6 -3
  42. package/dist/plugins/grideditor.elements.min.js +1 -1
  43. package/dist/plugins/grideditor.elements.min.js.map +3 -3
  44. package/dist/plugins/grideditor.gutters.esm.js +5 -0
  45. package/dist/plugins/grideditor.gutters.js +5 -0
  46. package/dist/plugins/grideditor.gutters.min.js +1 -1
  47. package/dist/plugins/grideditor.gutters.min.js.map +3 -3
  48. package/dist/plugins/grideditor.inline-style.esm.js +18 -10
  49. package/dist/plugins/grideditor.inline-style.js +18 -10
  50. package/dist/plugins/grideditor.inline-style.min.js +3 -3
  51. package/dist/plugins/grideditor.inline-style.min.js.map +3 -3
  52. package/dist/plugins/grideditor.order.esm.js +5 -0
  53. package/dist/plugins/grideditor.order.js +5 -0
  54. package/dist/plugins/grideditor.order.min.js +1 -1
  55. package/dist/plugins/grideditor.order.min.js.map +3 -3
  56. package/dist/plugins/grideditor.popup.esm.js +17 -6
  57. package/dist/plugins/grideditor.popup.js +17 -6
  58. package/dist/plugins/grideditor.popup.min.js +1 -1
  59. package/dist/plugins/grideditor.popup.min.js.map +3 -3
  60. package/dist/plugins/grideditor.sections.esm.js +6 -3
  61. package/dist/plugins/grideditor.sections.js +6 -3
  62. package/dist/plugins/grideditor.sections.min.js +1 -1
  63. package/dist/plugins/grideditor.sections.min.js.map +3 -3
  64. package/dist/plugins/grideditor.summernote.esm.js +39 -0
  65. package/dist/plugins/grideditor.summernote.js +39 -0
  66. package/dist/plugins/grideditor.summernote.min.js +1 -1
  67. package/dist/plugins/grideditor.summernote.min.js.map +3 -3
  68. package/dist/plugins/grideditor.tabs.esm.js +7 -0
  69. package/dist/plugins/grideditor.tabs.js +7 -0
  70. package/dist/plugins/grideditor.tabs.min.js +1 -1
  71. package/dist/plugins/grideditor.tabs.min.js.map +3 -3
  72. package/dist/plugins/grideditor.tinymce.esm.js +31 -0
  73. package/dist/plugins/grideditor.tinymce.js +31 -0
  74. package/dist/plugins/grideditor.tinymce.min.js +1 -1
  75. package/dist/plugins/grideditor.tinymce.min.js.map +3 -3
  76. package/docs/events.md +8 -0
  77. package/docs/locale-keys.md +14 -0
  78. package/docs/plugins.md +86 -2
  79. package/example/autosave.html +39 -22
  80. package/example/index.html +1 -1
  81. package/example/plugins.html +10 -0
  82. package/package.json +1 -3
  83. package/src/js/grideditor.js +280 -30
  84. package/src/js/inline-style/display.js +4 -3
  85. package/src/js/inline-style/drawerflow.js +10 -5
  86. package/src/js/locales/grideditor.es.js +4 -0
  87. package/src/js/plugins/grideditor.accordion.js +6 -0
  88. package/src/js/plugins/grideditor.autosave.js +349 -0
  89. package/src/js/plugins/grideditor.card.js +6 -0
  90. package/src/js/plugins/grideditor.ckeditor.js +6 -0
  91. package/src/js/plugins/grideditor.clipboard.js +3 -0
  92. package/src/js/plugins/grideditor.codemirror-inline.js +7 -0
  93. package/src/js/plugins/grideditor.elements.js +4 -3
  94. package/src/js/plugins/grideditor.gutters.js +4 -0
  95. package/src/js/plugins/grideditor.inline-style.js +2 -0
  96. package/src/js/plugins/grideditor.order.js +4 -0
  97. package/src/js/plugins/grideditor.popup.js +15 -6
  98. package/src/js/plugins/grideditor.sections.js +4 -3
  99. package/src/js/plugins/grideditor.summernote.js +14 -0
  100. package/src/js/plugins/grideditor.tabs.js +6 -0
  101. package/src/js/plugins/grideditor.tinymce.js +8 -0
  102. package/src/js/text/grideditor.text.js +26 -0
  103. package/src/less/grideditor.less +16 -8
  104. package/types/grideditor.d.ts +59 -3
  105. package/AUTO_SAVE.md +0 -30
@@ -437,6 +437,7 @@ function build(instance, baseElem, optionsOrMethod) {
437
437
  var closeOverflowMenu = function() {}; // Folds the toolbar's overflow menu away, when it has one
438
438
  var refitToolbar = function() {}; // Works out again what the toolbar's overflow menu holds, when it has one
439
439
  var addButtons = []; // The toolbar's add buttons, in toolbar order: { button, home, group }
440
+ var shownToolbar = {}; // Each feature plugin's toolbar items, as the toolbar was last built with them
440
441
  var toolbarGroup = null; // The category shown, with toolbar_groups. Kept when the toolbar is rebuilt
441
442
  var warnedHere = {}; // Some warnings are worth saying once per instance, not once per call
442
443
  var sortables = []; // Every list made sortable, so deinit destroys exactly those
@@ -628,30 +629,61 @@ function build(instance, baseElem, optionsOrMethod) {
628
629
  return;
629
630
  }
630
631
 
632
+ confirmWith(message, { danger: true }, function(confirmed) {
633
+ if (confirmed) { whenConfirmed(); }
634
+ });
635
+ }
636
+
637
+ /**
638
+ * ge.confirm(): a question of a plugin's own, in the same modal.
639
+ * `answer` is called once: true for the ok button, false for cancel,
640
+ * the close button or Escape, and null when the question goes
641
+ * unanswered because the modal is taken away - setLocale, destroy, or
642
+ * another question asked over it.
643
+ */
644
+ function confirmWith(message, options, answer) {
645
+ options = options || {};
631
646
  var Modal = modalLibrary();
632
647
 
633
648
  if (!Modal) {
634
- if (window.confirm(message)) { whenConfirmed(); }
649
+ answer(window.confirm(message));
635
650
  return;
636
651
  }
637
652
 
638
653
  var modal = confirmModal();
639
654
  var confirmed = false;
655
+ var ok = dom.one(modal, '.ge-confirm-ok');
656
+
657
+ // The question before this one, if it is still up, goes unanswered
658
+ if (pendingAnswer) { pendingAnswer(null); }
659
+
660
+ var settle = function(value) {
661
+ if (pendingAnswer !== settle) { return; }
662
+ pendingAnswer = null;
663
+ answer(value);
664
+ };
665
+ pendingAnswer = settle;
666
+
667
+ dom.one(modal, '.modal-title').textContent = options.title || t('confirm.title');
668
+ dom.one(modal, '.ge-confirm-cancel').textContent = options.cancel || t('confirm.cancel');
669
+ ok.textContent = options.ok || t('confirm.ok');
670
+ dom.toggleClass(ok, 'btn-danger', !!options.danger);
671
+ dom.toggleClass(ok, 'btn-primary', !options.danger);
640
672
 
641
673
  dom.one(modal, '.ge-confirm-message').textContent = message;
642
- dom.one(modal, '.ge-confirm-ok').onclick = function() {
674
+ ok.onclick = function() {
643
675
  confirmed = true;
644
676
  Modal.getInstance(modal).hide();
645
677
  };
646
678
 
647
679
  confirmHandlers.hidden = function() {
648
680
  // After the modal is out of the way, so the backdrop is not
649
- // sitting over the animation the delete runs
650
- if (confirmed) { whenConfirmed(); }
681
+ // sitting over the animation a delete runs
682
+ settle(confirmed);
651
683
  };
652
684
 
653
685
  confirmHandlers.shown = function() {
654
- dom.one(modal, '.ge-confirm-ok').focus();
686
+ ok.focus();
655
687
  };
656
688
 
657
689
  Modal.getOrCreateInstance(modal).show();
@@ -660,6 +692,7 @@ function build(instance, baseElem, optionsOrMethod) {
660
692
  // What the confirm modal does when Bootstrap says it is shown or
661
693
  // hidden: this question's answer, replaced by the next question's
662
694
  var confirmHandlers = { hidden: null, shown: null };
695
+ var pendingAnswer = null; // The open question's answer, until it is given
663
696
 
664
697
  /** Built once per instance, and taken away again by destroy(). */
665
698
  function confirmModal() {
@@ -690,11 +723,15 @@ function build(instance, baseElem, optionsOrMethod) {
690
723
  dom.one(confirmDialog, '.ge-confirm-ok').textContent = t('confirm.ok');
691
724
 
692
725
  trackModal(confirmDialog);
726
+ // Only while it is the modal asking: one being taken away -
727
+ // setLocale builds another - finishes hiding after the next
728
+ // question is up, and that answer is not its to give
729
+ var asking = confirmDialog;
693
730
  confirmDialog.addEventListener('hidden.bs.modal', function() {
694
- if (confirmHandlers.hidden) { confirmHandlers.hidden(); }
731
+ if (asking === confirmDialog && confirmHandlers.hidden) { confirmHandlers.hidden(); }
695
732
  });
696
733
  confirmDialog.addEventListener('shown.bs.modal', function() {
697
- if (confirmHandlers.shown) { confirmHandlers.shown(); }
734
+ if (asking === confirmDialog && confirmHandlers.shown) { confirmHandlers.shown(); }
698
735
  });
699
736
 
700
737
  return confirmDialog;
@@ -709,6 +746,9 @@ function build(instance, baseElem, optionsOrMethod) {
709
746
  confirmHandlers.shown = null;
710
747
  retireModal(confirmDialog);
711
748
  confirmDialog = null;
749
+
750
+ // A plugin's question goes unanswered, and the plugin is told so
751
+ if (pendingAnswer) { pendingAnswer(null); }
712
752
  }
713
753
 
714
754
  /**
@@ -1081,7 +1121,7 @@ function build(instance, baseElem, optionsOrMethod) {
1081
1121
  settings.new_row_layouts.forEach(function(layout) {
1082
1122
  var grouped = !Array.isArray(layout);
1083
1123
  var btn = dom.element('a', {
1084
- 'class': 'btn btn-sm btn-primary',
1124
+ 'class': 'btn btn-sm btn-light',
1085
1125
  title: grouped
1086
1126
  ? t('row.add_row_cols', { columns: layout.columns, counts: rowColsText(layout.row_cols) })
1087
1127
  : t('row.add', { layout: layout.join('-') }),
@@ -1126,7 +1166,7 @@ function build(instance, baseElem, optionsOrMethod) {
1126
1166
  // buttons next to these ones do
1127
1167
  Object.keys(CONTAINERS).forEach(function(type) {
1128
1168
  var definition = CONTAINERS[type];
1129
- var button = labelButton(dom.element('a', { 'class': 'btn btn-sm btn-primary ge-add-container' }),
1169
+ var button = labelButton(dom.element('a', { 'class': 'btn btn-sm btn-light ge-add-container' }),
1130
1170
  t(definition.labelKey), definition.iconClass);
1131
1171
 
1132
1172
  dom.attr(button, { 'data-ge-toolbar': 'container', 'data-ge-container-type': type });
@@ -1150,8 +1190,10 @@ function build(instance, baseElem, optionsOrMethod) {
1150
1190
  // One with align 'end' goes on the right instead, as an icon,
1151
1191
  // beside the source and preview buttons.
1152
1192
  var endItems = [];
1193
+ shownToolbar = {};
1153
1194
  Object.keys(FEATURES).forEach(function(name) {
1154
- (FEATURES[name].toolbar || []).forEach(function(item, index) {
1195
+ shownToolbar[name] = toolbarOf(FEATURES[name]);
1196
+ shownToolbar[name].forEach(function(item, index) {
1155
1197
  var button = featureButton(name, item, index);
1156
1198
 
1157
1199
  if (item.align === 'end') {
@@ -1274,7 +1316,7 @@ function build(instance, baseElem, optionsOrMethod) {
1274
1316
  names.forEach(function(name) {
1275
1317
  var tab = tabs.appendChild(dom.element('button', {
1276
1318
  type: 'button',
1277
- 'class': 'btn btn-sm btn-outline-primary',
1319
+ 'class': 'btn btn-sm btn-outline-secondary',
1278
1320
  role: 'tab',
1279
1321
  'data-ge-group': name,
1280
1322
  }, groupLabel(name)));
@@ -1316,6 +1358,34 @@ function build(instance, baseElem, optionsOrMethod) {
1316
1358
  }
1317
1359
 
1318
1360
  /** The toolbar off the page, with what keeps its overflow menu up to date. */
1361
+ /** A feature plugin's toolbar items: its array, or what its function returns now. */
1362
+ function toolbarOf(feature) {
1363
+ var items = typeof feature.toolbar === 'function' ? feature.toolbar() : feature.toolbar;
1364
+ return Array.isArray(items) ? items : [];
1365
+ }
1366
+
1367
+ /**
1368
+ * ge.refreshToolbar(): the toolbar built again, from what the plugins'
1369
+ * toolbars hold now - a plugin whose buttons come and go calls it when
1370
+ * they have. The category shown, the preview and the source view stay
1371
+ * as they are; an open overflow menu folds away.
1372
+ */
1373
+ function refreshToolbar() {
1374
+ // Not built yet - the first one reads the toolbars as they are
1375
+ // then - or taken away for good
1376
+ if (!mainControls || destroyed) { return; }
1377
+
1378
+ var previewing = !!dom.one(mainControls, '.gm-preview.active');
1379
+
1380
+ removeMainControls();
1381
+ createMainControls();
1382
+
1383
+ if (previewing) {
1384
+ dom.addClass(dom.one(mainControls, '.gm-preview'), 'active btn-danger');
1385
+ }
1386
+ onScroll();
1387
+ }
1388
+
1319
1389
  function removeMainControls() {
1320
1390
  if (toolbarLifetime) { toolbarLifetime.abort(); }
1321
1391
  toolbarLifetime = null;
@@ -1333,7 +1403,7 @@ function build(instance, baseElem, optionsOrMethod) {
1333
1403
  */
1334
1404
  function createOverflowMenu(start) {
1335
1405
  var more = dom.create('<div class="ge-toolbar-more">' +
1336
- '<button type="button" class="btn btn-sm btn-primary"><i class="bi bi-three-dots"></i></button>' +
1406
+ '<button type="button" class="btn btn-sm btn-light"><i class="bi bi-three-dots"></i></button>' +
1337
1407
  '<div class="ge-toolbar-overflow">' +
1338
1408
  '<div class="ge-addRowGroup btn-group"></div>' +
1339
1409
  '<div class="ge-addContainerGroup btn-group"></div>' +
@@ -1442,7 +1512,10 @@ function build(instance, baseElem, optionsOrMethod) {
1442
1512
  // A label of its own, worked out when the toolbar is built - and
1443
1513
  // built again by setLocale - or the string its labelKey names
1444
1514
  var label = typeof item.label === 'function' ? item.label() : (item.label || t(item.labelKey));
1445
- var button = labelButton(dom.element('a', { 'class': 'btn btn-sm btn-primary ge-add-container ge-add-feature' }), label, iconClass);
1515
+ // Grey with the add buttons on the left, blue with the source and
1516
+ // preview buttons on the right
1517
+ var colour = item.align === 'end' ? 'btn-primary' : 'btn-light';
1518
+ var button = labelButton(dom.element('a', { 'class': 'btn btn-sm ' + colour + ' ge-add-container ge-add-feature' }), label, iconClass);
1446
1519
 
1447
1520
  dom.addClass(button, item.className || '');
1448
1521
  dom.attr(button, {
@@ -1613,7 +1686,10 @@ function build(instance, baseElem, optionsOrMethod) {
1613
1686
  * to the canvas, just after the top level block the pointer was in.
1614
1687
  */
1615
1688
  function insertFeatureFromToolbar(button, where, source) {
1616
- var item = FEATURES[button.getAttribute('data-ge-feature')].toolbar[parseInt(button.getAttribute('data-ge-item'), 10)];
1689
+ // The item the button was built from, whatever the plugin's list
1690
+ // says now: it may have changed since, and the toolbar not yet
1691
+ var item = (shownToolbar[button.getAttribute('data-ge-feature')] || [])[parseInt(button.getAttribute('data-ge-item'), 10)];
1692
+ if (!item) { return null; }
1617
1693
  var made = item.create();
1618
1694
  if (!made) { return null; }
1619
1695
  var placed = made;
@@ -2090,26 +2166,144 @@ function build(instance, baseElem, optionsOrMethod) {
2090
2166
  hideDropMarker();
2091
2167
  // In case a dialog was opened with no settings panel open
2092
2168
  closeDialog();
2093
- dom.all(canvas, '.ge-tools-drawer').forEach(function(drawer) { drawer.remove(); });
2169
+ removeDrawers(canvas);
2094
2170
  pluginFields.clear();
2095
- unwrapTexts();
2171
+ unwrapTexts(canvas);
2096
2172
  plugins('onDeinit');
2097
2173
  // After the rich text editors have let go of their content areas:
2098
2174
  // one that rebuilt its area's DOM brought the preview styles back
2099
2175
  // with it, and the attribute recording them came back too
2100
2176
  clearPreviews(canvas);
2101
- dom.all(canvas, '[data-ge-row-cols]').forEach(function(row) { row.removeAttribute('data-ge-row-cols'); });
2177
+ clearRowCols(canvas);
2102
2178
  unmarkContainers();
2103
2179
  removeSortable();
2104
2180
  removeResizable();
2105
- runFilter(false);
2181
+ runFilter(false, canvas);
2182
+ }
2183
+
2184
+ function removeDrawers(root) {
2185
+ dom.all(root, '.ge-tools-drawer').forEach(function(drawer) { drawer.remove(); });
2186
+ }
2187
+
2188
+ function clearRowCols(root) {
2189
+ dom.all(root, '[data-ge-row-cols]').forEach(function(row) { row.removeAttribute('data-ge-row-cols'); });
2190
+ }
2191
+
2192
+ /**
2193
+ * The plugins that cannot be read without the canvas leaving editing:
2194
+ * the ones that put something on the canvas - an unmark, an onDeinit
2195
+ * or an onBeforeDeinit takes it off - and say nothing of how to take
2196
+ * it off a copy.
2197
+ */
2198
+ function pluginsWithoutCleanMarkup() {
2199
+ var names = [];
2200
+
2201
+ [CONTAINERS, FEATURES, UTILITIES].forEach(function(registry) {
2202
+ Object.keys(registry).forEach(function(name) {
2203
+ var plugin = registry[name];
2204
+ var marks = plugin.unmark || plugin.onDeinit || plugin.onBeforeDeinit;
2205
+ if (marks && !plugin.cleanMarkup) { names.push(name); }
2206
+ });
2207
+ });
2208
+
2209
+ return names;
2210
+ }
2211
+
2212
+ /**
2213
+ * A copy of the canvas, and the way from each of its elements back to
2214
+ * the one it was copied from. The two trees are walked side by side,
2215
+ * which works because a deep clone has the same shape.
2216
+ */
2217
+ function copyCanvas() {
2218
+ var copy = canvas.cloneNode(true);
2219
+ var toLive = new Map();
2220
+ var toCopy = new Map();
2221
+ var live = document.createTreeWalker(canvas, NodeFilter.SHOW_ELEMENT);
2222
+ var copied = document.createTreeWalker(copy, NodeFilter.SHOW_ELEMENT);
2223
+ var a = live.currentNode;
2224
+ var b = copied.currentNode;
2225
+
2226
+ while (a && b) {
2227
+ toLive.set(b, a);
2228
+ toCopy.set(a, b);
2229
+ a = live.nextNode();
2230
+ b = copied.nextNode();
2231
+ }
2232
+
2233
+ return {
2234
+ root: copy,
2235
+ liveOf: function(node) { return toLive.get(node) || null; },
2236
+ copyOf: function(node) { return toCopy.get(node) || null; },
2237
+ };
2238
+ }
2239
+
2240
+ /**
2241
+ * getHtml without the canvas leaving editing: what deinit takes off,
2242
+ * taken off a copy, so the text being typed into, the settings panel
2243
+ * and the focus are where they were. The same markup getHtml gives,
2244
+ * or getHtml itself - with a word, once - when a plugin can only take
2245
+ * its marking off the canvas, or will not this time.
2246
+ */
2247
+ function snapshotHtml() {
2248
+ // Nothing is being edited to keep: the canvas is out of editing
2249
+ if (sourceOpen) { return getHtml(); }
2250
+
2251
+ var missing = pluginsWithoutCleanMarkup();
2252
+ if (missing.length) {
2253
+ missing.forEach(function(name) {
2254
+ warnOnceHere('snapshot:' + name, 'the "' + name + '" plugin has no cleanMarkup, so the canvas ' +
2255
+ 'leaves editing to be read: open texts and settings panels close');
2256
+ });
2257
+ return getHtml();
2258
+ }
2259
+
2260
+ var copy = copyCanvas();
2261
+ var root = copy.root;
2262
+
2263
+ [root].concat(dom.all(root, '.ge-active-target, .ge-settings-target')).forEach(function(node) {
2264
+ dom.removeClass(node, 'ge-active-target ge-settings-target');
2265
+ dom.dropEmptyClass(node);
2266
+ });
2267
+ dom.all(root, '.ge-drop-marker').forEach(function(marker) { marker.remove(); });
2268
+ removeDrawers(root);
2269
+ unwrapTexts(root);
2270
+
2271
+ var refused = null;
2272
+ [CONTAINERS, FEATURES, UTILITIES].forEach(function(registry) {
2273
+ Object.keys(registry).forEach(function(name) {
2274
+ var plugin = registry[name];
2275
+ if (refused || !plugin.cleanMarkup) { return; }
2276
+ if (plugin.cleanMarkup(root, copy.liveOf) === false) { refused = name; }
2277
+ });
2278
+ });
2279
+
2280
+ if (refused) {
2281
+ warnOnceHere('snapshot-refused:' + refused, 'the "' + refused + '" plugin cannot take its marking off ' +
2282
+ 'a copy right now, so the canvas leaves editing to be read');
2283
+ return getHtml();
2284
+ }
2285
+
2286
+ clearPreviews(root);
2287
+ clearRowCols(root);
2288
+ dom.all(root, '[data-ge-container]').forEach(unmarkContainerClasses);
2289
+ sortables.forEach(function(sortableInstance) {
2290
+ var list = copy.copyOf(sortableInstance.el);
2291
+ if (list) { dom.all(list, '[draggable]').forEach(function(node) { node.removeAttribute('draggable'); }); }
2292
+ });
2293
+ removeResizeFurniture(root);
2294
+ runFilter(false, root);
2295
+ stripPixelWidths(root);
2296
+
2297
+ return root.innerHTML;
2106
2298
  }
2107
2299
 
2108
2300
  /**
2109
2301
  * The markup as a host would save it: no drawers, no editor, no
2110
2302
  * sortables. The canvas goes back to editing afterwards.
2111
2303
  */
2112
- function getHtml() {
2304
+ function getHtml(options) {
2305
+ if (options && options.keepEditing) { return snapshotHtml(); }
2306
+
2113
2307
  deinit();
2114
2308
  stripPixelWidths(canvas);
2115
2309
  var html = canvas.innerHTML;
@@ -2130,8 +2324,31 @@ function build(instance, baseElem, optionsOrMethod) {
2130
2324
  }
2131
2325
 
2132
2326
  /** getHtml with grid-editor's own classes and attributes taken off. */
2133
- function getPlainHtml() {
2134
- return plainHtml(getHtml());
2327
+ function getPlainHtml(options) {
2328
+ return plainHtml(getHtml(options));
2329
+ }
2330
+
2331
+ /**
2332
+ * ge.setHtml(): the canvas made of other markup, and edited again. Not
2333
+ * an add or a delete of anything, so no events: what was there is
2334
+ * replaced as a whole. innerHTML, as the source view does, so a
2335
+ * <script> in it is markup and does not run in the editor.
2336
+ */
2337
+ function setHtml(html) {
2338
+ // What is in the textarea is not wanted - html replaces it - but
2339
+ // the source view closes the one way it always does
2340
+ if (sourceOpen) {
2341
+ closeSource();
2342
+ if (mainControls) {
2343
+ dom.all(mainControls, '.gm-edit-mode').forEach(function(button) {
2344
+ dom.removeClass(button, 'active btn-danger');
2345
+ });
2346
+ }
2347
+ }
2348
+
2349
+ deinit();
2350
+ dom.setHtml(canvas, html);
2351
+ init();
2135
2352
  }
2136
2353
 
2137
2354
  /**
@@ -2141,6 +2358,8 @@ function build(instance, baseElem, optionsOrMethod) {
2141
2358
  * as a framework tearing a component down does.
2142
2359
  */
2143
2360
  function destroy() {
2361
+ // First, while the editor is whole: a plugin may still read it
2362
+ plugins('onDestroy');
2144
2363
  // The html being edited is what the canvas is left with
2145
2364
  if (sourceOpen) { closeSource(); }
2146
2365
  deinit();
@@ -2268,6 +2487,11 @@ function build(instance, baseElem, optionsOrMethod) {
2268
2487
  closeDialog: closeDialog,
2269
2488
  rowFromLayout: rowFromLayoutValue,
2270
2489
  nodeHtml: nodeHtml,
2490
+ // The markup as getHtml gives it, with the canvas left editing
2491
+ snapshotHtml: snapshotHtml,
2492
+ setHtml: setHtml,
2493
+ // A question in the editor's confirm modal
2494
+ confirm: confirmWith,
2271
2495
  // A text editor has rewritten a content area, so whatever the
2272
2496
  // editor and its plugins had put in there goes back in
2273
2497
  textReady: textReady,
@@ -2280,6 +2504,8 @@ function build(instance, baseElem, optionsOrMethod) {
2280
2504
  // A node's drawer: its first child, or for a content area the
2281
2505
  // one beside it in its text block
2282
2506
  drawerOf: drawerOf,
2507
+ // The toolbar built again, from the plugins' toolbars as they are now
2508
+ refreshToolbar: refreshToolbar,
2283
2509
  toolbarItems: function(name) {
2284
2510
  return mainControls
2285
2511
  ? dom.all(mainControls, '[data-ge-toolbar="feature"][data-ge-feature="' + name + '"]')
@@ -2990,11 +3216,15 @@ function build(instance, baseElem, optionsOrMethod) {
2990
3216
 
2991
3217
  if (definition) { definition.unmark(container); }
2992
3218
 
2993
- dom.removeClass(container, 'ge-container ge-container-' + containerTypeOf(container));
2994
- dom.dropEmptyClass(container);
3219
+ unmarkContainerClasses(container);
2995
3220
  });
2996
3221
  }
2997
3222
 
3223
+ function unmarkContainerClasses(container) {
3224
+ dom.removeClass(container, 'ge-container ge-container-' + containerTypeOf(container));
3225
+ dom.dropEmptyClass(container);
3226
+ }
3227
+
2998
3228
  /** A new drawer as the first child of `node`. */
2999
3229
  function prependDrawer(node, className) {
3000
3230
  var drawer = dom.element('div', { 'class': className });
@@ -4923,10 +5153,14 @@ function build(instance, baseElem, optionsOrMethod) {
4923
5153
  }
4924
5154
 
4925
5155
  function removeResizable() {
4926
- dom.all(canvas, '.ge-resize-handle').forEach(function(handle) { handle.remove(); });
5156
+ removeResizeFurniture(canvas);
5157
+ }
4927
5158
 
4928
- dom.all(canvas, '.ge-resize-size').forEach(function(readout) { readout.remove(); });
4929
- stripPixelWidths(canvas);
5159
+ function removeResizeFurniture(root) {
5160
+ dom.all(root, '.ge-resize-handle').forEach(function(handle) { handle.remove(); });
5161
+
5162
+ dom.all(root, '.ge-resize-size').forEach(function(readout) { readout.remove(); });
5163
+ stripPixelWidths(root);
4930
5164
  }
4931
5165
 
4932
5166
  /**
@@ -5252,7 +5486,7 @@ function build(instance, baseElem, optionsOrMethod) {
5252
5486
  * function, or the name of one on window, and gets the canvas and
5253
5487
  * whether this is init.
5254
5488
  */
5255
- function runFilter(isInit) {
5489
+ function runFilter(isInit, root) {
5256
5490
  if (!settings.custom_filter || !settings.custom_filter.length) { return; }
5257
5491
 
5258
5492
  var filters = typeof settings.custom_filter === 'string' || typeof settings.custom_filter === 'function'
@@ -5264,7 +5498,7 @@ function build(instance, baseElem, optionsOrMethod) {
5264
5498
  func = window[func];
5265
5499
  }
5266
5500
 
5267
- func(canvas, isInit);
5501
+ func(root || canvas, isInit);
5268
5502
  });
5269
5503
  }
5270
5504
 
@@ -5457,8 +5691,8 @@ function build(instance, baseElem, optionsOrMethod) {
5457
5691
  }
5458
5692
 
5459
5693
  /** After the drawers are gone, so a wrapper holds its content area and nothing else. */
5460
- function unwrapTexts() {
5461
- dom.all(canvas, '.ge-text-block').forEach(function(textBlock) {
5694
+ function unwrapTexts(root) {
5695
+ dom.all(root, '.ge-text-block').forEach(function(textBlock) {
5462
5696
  dom.unwrap(textBlock);
5463
5697
  });
5464
5698
  }
@@ -5666,6 +5900,22 @@ function build(instance, baseElem, optionsOrMethod) {
5666
5900
  // Again, with what the plugins filled in: the text editors'
5667
5901
  // content_types when the host gave none
5668
5902
  instance.settings = settingsCopy();
5903
+
5904
+ // A feature plugin's methods that are not the editor's own documented
5905
+ // ones - the autosave plugin's - are the instance's too, while the
5906
+ // plugin is there
5907
+ Object.keys(featureMethods).forEach(function(name) {
5908
+ if (Object.prototype.hasOwnProperty.call(own, name) || name in instance) { return; }
5909
+
5910
+ instance[name] = function() {
5911
+ if (destroyed) {
5912
+ warnOnceHere('destroyed:' + name, t('warning.destroyed', { method: name }));
5913
+ return null;
5914
+ }
5915
+
5916
+ return featureMethods[name].apply(null, arguments);
5917
+ };
5918
+ });
5669
5919
  setup();
5670
5920
  init();
5671
5921
 
@@ -177,8 +177,8 @@ export function displayPart(ge, given) {
177
177
  });
178
178
  }
179
179
 
180
- function unmark() {
181
- dom.all(ge.canvas, '.ge-hidden-in-view, [data-ge-hidden-in], [' + SHOWN_ATTR + ']').forEach(function(node) {
180
+ function unmark(root) {
181
+ dom.all(root || ge.canvas, '.ge-hidden-in-view, [data-ge-hidden-in], [' + SHOWN_ATTR + ']').forEach(function(node) {
182
182
  dom.removeClass(node, 'ge-hidden-in-view');
183
183
  node.removeAttribute('data-ge-hidden-in');
184
184
  node.removeAttribute(SHOWN_ATTR);
@@ -224,6 +224,7 @@ export function displayPart(ge, given) {
224
224
  },
225
225
 
226
226
  onRefresh: mark,
227
- onDeinit: unmark,
227
+ onDeinit: function() { unmark(); },
228
+ cleanMarkup: unmark,
228
229
  };
229
230
  }
@@ -95,15 +95,19 @@ export function drawerflowPart(ge) {
95
95
  });
96
96
  }
97
97
 
98
- function unmark() {
99
- if (observer) { observer.disconnect(); }
100
- observer = null;
101
-
102
- dom.all(ge.canvas, '.' + OUT_CLASS).forEach(function(node) {
98
+ function unmarkNodes(root) {
99
+ dom.all(root, '.' + OUT_CLASS).forEach(function(node) {
103
100
  dom.removeClass(node, OUT_CLASS);
104
101
  node.removeAttribute(OUT_ATTR);
105
102
  dom.dropEmptyClass(node);
106
103
  });
104
+ }
105
+
106
+ function unmark() {
107
+ if (observer) { observer.disconnect(); }
108
+ observer = null;
109
+
110
+ unmarkNodes(ge.canvas);
107
111
 
108
112
  if (sheet) { sheet.remove(); }
109
113
  sheet = null;
@@ -114,5 +118,6 @@ export function drawerflowPart(ge) {
114
118
  families: [],
115
119
  onRefresh: mark,
116
120
  onDeinit: unmark,
121
+ cleanMarkup: unmarkNodes,
117
122
  };
118
123
  }
@@ -140,6 +140,10 @@ GridEditor.locales.es = {
140
140
  'tool.paste': 'Pegar',
141
141
  'clipboard.paste_row': 'Pegar fila',
142
142
  'clipboard.paste_section': 'Pegar sección',
143
+ 'autosave.restore_title': '¿Recuperar el borrador?',
144
+ 'autosave.restore_message': 'Hay un borrador de esta página guardado el {date}, con cambios que no se publicaron. ¿Quieres recuperarlo?',
145
+ 'autosave.restore': 'Recuperar',
146
+ 'autosave.discard': 'Descartar',
143
147
  'utility.col_from_row': 'De la fila: {count}',
144
148
  'utility.row_cols': 'Columnas por fila',
145
149
  'badge.row_cols': '{count} por fila',
@@ -206,6 +206,12 @@ GridEditor.containers.accordion = function(ge) {
206
206
  ge.unwrapLabels(container);
207
207
  },
208
208
 
209
+ // The same, on a copy of the canvas: unmark only touches markup
210
+ cleanMarkup: function(root) {
211
+ var definition = this;
212
+ dom.all(root, '[data-ge-container="accordion"]').forEach(function(container) { definition.unmark(container); });
213
+ },
214
+
209
215
  afterPaneMove: function(container, item) {
210
216
  reparentAccordionItem(container, item);
211
217
  },