@themarioga/grid-editor 6.0.0 → 7.0.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 (147) hide show
  1. package/AUTO_SAVE.md +2 -2
  2. package/BUILDING.md +45 -29
  3. package/CHANGELOG.md +47 -0
  4. package/README.md +137 -85
  5. package/UPGRADING.md +119 -0
  6. package/dist/grideditor.bundle.min.js +14 -0
  7. package/dist/grideditor.d.ts +335 -0
  8. package/dist/grideditor.esm.js +3977 -0
  9. package/dist/grideditor.jquery.js +253 -0
  10. package/dist/grideditor.jquery.min.js +1 -0
  11. package/dist/grideditor.jquery.min.js.map +7 -0
  12. package/dist/grideditor.js +3983 -0
  13. package/dist/grideditor.min.js +6 -0
  14. package/dist/grideditor.min.js.map +7 -0
  15. package/dist/locales/grideditor.es.esm.js +158 -0
  16. package/dist/locales/grideditor.es.js +164 -168
  17. package/dist/locales/grideditor.es.min.js +6 -2
  18. package/dist/locales/grideditor.es.min.js.map +7 -1
  19. package/dist/plugins/grideditor.accordion.esm.js +294 -0
  20. package/dist/plugins/grideditor.accordion.js +286 -180
  21. package/dist/plugins/grideditor.accordion.min.js +1 -2
  22. package/dist/plugins/grideditor.accordion.min.js.map +7 -1
  23. package/dist/plugins/grideditor.alignment.esm.js +55 -0
  24. package/dist/plugins/grideditor.alignment.js +58 -77
  25. package/dist/plugins/grideditor.alignment.min.js +1 -2
  26. package/dist/plugins/grideditor.alignment.min.js.map +7 -1
  27. package/dist/plugins/grideditor.card.esm.js +73 -0
  28. package/dist/plugins/grideditor.card.js +74 -64
  29. package/dist/plugins/grideditor.card.min.js +1 -2
  30. package/dist/plugins/grideditor.card.min.js.map +7 -1
  31. package/dist/plugins/grideditor.ckeditor.esm.js +500 -0
  32. package/dist/plugins/grideditor.ckeditor.js +484 -449
  33. package/dist/plugins/grideditor.ckeditor.min.js +1 -2
  34. package/dist/plugins/grideditor.ckeditor.min.js.map +7 -1
  35. package/dist/plugins/grideditor.clipboard.esm.js +326 -0
  36. package/dist/plugins/grideditor.clipboard.js +298 -235
  37. package/dist/plugins/grideditor.clipboard.min.js +1 -2
  38. package/dist/plugins/grideditor.clipboard.min.js.map +7 -1
  39. package/dist/plugins/grideditor.codemirror-inline.esm.js +205 -0
  40. package/dist/plugins/grideditor.codemirror-inline.js +188 -168
  41. package/dist/plugins/grideditor.codemirror-inline.min.js +1 -2
  42. package/dist/plugins/grideditor.codemirror-inline.min.js.map +7 -1
  43. package/dist/plugins/grideditor.codemirror.esm.js +63 -0
  44. package/dist/plugins/grideditor.codemirror.js +60 -71
  45. package/dist/plugins/grideditor.codemirror.min.js +1 -2
  46. package/dist/plugins/grideditor.codemirror.min.js.map +7 -1
  47. package/dist/plugins/grideditor.elements.esm.js +271 -0
  48. package/dist/plugins/grideditor.elements.js +256 -149
  49. package/dist/plugins/grideditor.elements.min.js +1 -2
  50. package/dist/plugins/grideditor.elements.min.js.map +7 -1
  51. package/dist/plugins/grideditor.float.esm.js +34 -0
  52. package/dist/plugins/grideditor.float.js +37 -52
  53. package/dist/plugins/grideditor.float.min.js +1 -2
  54. package/dist/plugins/grideditor.float.min.js.map +7 -1
  55. package/dist/plugins/grideditor.gutters.esm.js +143 -0
  56. package/dist/plugins/grideditor.gutters.js +134 -131
  57. package/dist/plugins/grideditor.gutters.min.js +1 -2
  58. package/dist/plugins/grideditor.gutters.min.js.map +7 -1
  59. package/dist/plugins/grideditor.order.esm.js +189 -0
  60. package/dist/plugins/grideditor.order.js +170 -182
  61. package/dist/plugins/grideditor.order.min.js +1 -2
  62. package/dist/plugins/grideditor.order.min.js.map +7 -1
  63. package/dist/plugins/grideditor.popup.esm.js +187 -0
  64. package/dist/plugins/grideditor.popup.js +183 -134
  65. package/dist/plugins/grideditor.popup.min.js +1 -2
  66. package/dist/plugins/grideditor.popup.min.js.map +7 -1
  67. package/dist/plugins/grideditor.sections.esm.js +271 -0
  68. package/dist/plugins/grideditor.sections.js +250 -173
  69. package/dist/plugins/grideditor.sections.min.js +1 -2
  70. package/dist/plugins/grideditor.sections.min.js.map +7 -1
  71. package/dist/plugins/grideditor.spacing.esm.js +318 -0
  72. package/dist/plugins/grideditor.spacing.js +296 -272
  73. package/dist/plugins/grideditor.spacing.min.js +1 -2
  74. package/dist/plugins/grideditor.spacing.min.js.map +7 -1
  75. package/dist/plugins/grideditor.summernote.esm.js +509 -0
  76. package/dist/plugins/grideditor.summernote.js +493 -427
  77. package/dist/plugins/grideditor.summernote.min.js +1 -2
  78. package/dist/plugins/grideditor.summernote.min.js.map +7 -1
  79. package/dist/plugins/grideditor.tabs.esm.js +237 -0
  80. package/dist/plugins/grideditor.tabs.js +233 -139
  81. package/dist/plugins/grideditor.tabs.min.js +1 -2
  82. package/dist/plugins/grideditor.tabs.min.js.map +7 -1
  83. package/dist/plugins/grideditor.textalign.esm.js +40 -0
  84. package/dist/plugins/grideditor.textalign.js +43 -56
  85. package/dist/plugins/grideditor.textalign.min.js +1 -2
  86. package/dist/plugins/grideditor.textalign.min.js.map +7 -1
  87. package/dist/plugins/grideditor.tinymce.esm.js +519 -0
  88. package/dist/plugins/grideditor.tinymce.js +502 -490
  89. package/dist/plugins/grideditor.tinymce.min.js +1 -2
  90. package/dist/plugins/grideditor.tinymce.min.js.map +7 -1
  91. package/dist/plugins/grideditor.visibility.esm.js +158 -0
  92. package/dist/plugins/grideditor.visibility.js +140 -149
  93. package/dist/plugins/grideditor.visibility.min.js +1 -2
  94. package/dist/plugins/grideditor.visibility.min.js.map +7 -1
  95. package/docs/events.md +47 -21
  96. package/docs/locale-keys.md +6 -0
  97. package/docs/plugins.md +47 -25
  98. package/example/adapter.html +112 -0
  99. package/example/attributes.html +85 -61
  100. package/example/autocols.html +9 -9
  101. package/example/autosave.html +13 -14
  102. package/example/basic.html +4 -5
  103. package/example/breakpoints.html +5 -6
  104. package/example/ckeditor.html +4 -5
  105. package/example/clipboard.html +11 -10
  106. package/example/codemirror.html +9 -7
  107. package/example/containers.html +7 -8
  108. package/example/custom_editor.html +53 -34
  109. package/example/elements.html +10 -11
  110. package/example/index.html +16 -2
  111. package/example/locale.html +6 -7
  112. package/example/plugins.html +44 -29
  113. package/example/summernote.html +6 -5
  114. package/example/utilities.html +17 -13
  115. package/example/wrap_content.html +9 -9
  116. package/package.json +43 -12
  117. package/src/js/dom.js +401 -0
  118. package/src/js/grideditor.classic.js +16 -0
  119. package/src/js/grideditor.jquery.js +325 -0
  120. package/src/js/{jquery.grideditor.js → grideditor.js} +1284 -1124
  121. package/src/js/locales/grideditor.es.js +163 -156
  122. package/src/js/plugins/grideditor.accordion.js +97 -77
  123. package/src/js/plugins/grideditor.alignment.js +5 -7
  124. package/src/js/plugins/grideditor.card.js +23 -23
  125. package/src/js/plugins/grideditor.ckeditor.js +107 -106
  126. package/src/js/plugins/grideditor.clipboard.js +49 -37
  127. package/src/js/plugins/grideditor.codemirror-inline.js +50 -37
  128. package/src/js/plugins/grideditor.codemirror.js +9 -10
  129. package/src/js/plugins/grideditor.elements.js +57 -33
  130. package/src/js/plugins/grideditor.float.js +4 -6
  131. package/src/js/plugins/grideditor.gutters.js +20 -15
  132. package/src/js/plugins/grideditor.order.js +28 -25
  133. package/src/js/plugins/grideditor.popup.js +63 -51
  134. package/src/js/plugins/grideditor.sections.js +39 -32
  135. package/src/js/plugins/grideditor.spacing.js +42 -45
  136. package/src/js/plugins/grideditor.summernote.js +112 -76
  137. package/src/js/plugins/grideditor.tabs.js +65 -57
  138. package/src/js/plugins/grideditor.textalign.js +4 -6
  139. package/src/js/plugins/grideditor.tinymce.js +147 -139
  140. package/src/js/plugins/grideditor.visibility.js +24 -25
  141. package/src/js/register.js +18 -0
  142. package/src/js/text/grideditor.text.js +77 -56
  143. package/types/grideditor.d.ts +335 -0
  144. package/dist/jquery.grideditor.bundle.min.js +0 -10
  145. package/dist/jquery.grideditor.js +0 -4927
  146. package/dist/jquery.grideditor.min.js +0 -2
  147. package/dist/jquery.grideditor.min.js.map +0 -1
@@ -1,29 +1,28 @@
1
1
  /**
2
- * Grid editor plugin.
2
+ * Grid editor.
3
3
  *
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
+ *
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.
6
10
  */
7
- (function( $ ){
11
+ import * as dom from './dom.js';
8
12
 
9
13
  /**
10
- * Every method the plugin dispatches, and how to dispatch it.
11
- *
12
- * `value` marks a method that hands back something other than the jQuery set
13
- * - html, a breakpoint key, a created node - so it runs against the first
14
- * element of the set only and does not chain. `noInstance` is the answer for
15
- * an element that carries no editor; every other method is a no-op returning
16
- * the set. `unimplemented` registers a method a later phase fills in, so a
17
- * host that calls it early gets told rather than ignored.
14
+ * The methods an instance has, and which of them hand back something other
15
+ * than the instance: html, a view key, a created node. After destroy() every
16
+ * one of them is a no-op that says so once, and the value methods answer
17
+ * null - but for getHtml and getPlainHtml, which read the element as it is.
18
18
  */
19
19
  var METHODS = {
20
- getHtml: { value: true, noInstance: function(element) { return element.html(); } },
21
- getPlainHtml: { value: true, noInstance: function(element) { return plainHtml(element.html()); } },
20
+ getHtml: { value: true },
21
+ getPlainHtml: { value: true },
22
22
  init: {},
23
23
  deinit: {},
24
24
  reset: {},
25
25
  destroy: {},
26
- remove: {},
27
26
  changeView: {},
28
27
  getView: { value: true },
29
28
  createRow: { value: true },
@@ -170,6 +169,9 @@ var warned = {};
170
169
  /** Editors on the page, counted so each one's sortable groups are its own. */
171
170
  var editorCounter = 0;
172
171
 
172
+ /** The editor on each canvas element, so there is never a second one. */
173
+ var instances = new WeakMap();
174
+
173
175
  /**
174
176
  * Translate one key.
175
177
  *
@@ -177,12 +179,11 @@ var editorCounter = 0;
177
179
  * then the key itself, so a missing string is a visible key and never an
178
180
  * empty tooltip. `params` fills {name} placeholders.
179
181
  *
180
- * Exposed as $.fn.gridEditor.t for the editor integrations in the other
181
- * source files, which are handed the settings and have no instance of their
182
- * own.
182
+ * Exposed as GridEditor.t for the plugins that have settings and no
183
+ * instance of their own.
183
184
  */
184
185
  function translate(settings, key, params) {
185
- var locales = $.fn.gridEditor.locales;
186
+ var locales = GridEditor.locales;
186
187
  var locale = locales[settings.locale] || {};
187
188
  var overrides = settings.locale_strings || {};
188
189
  var string = overrides[key];
@@ -213,40 +214,46 @@ function warnOnce(key, message) {
213
214
  warn(message);
214
215
  }
215
216
 
217
+ /** A string in English, for the few things said before an editor has settings. */
218
+ function english(key, params) {
219
+ return translate({ locale: 'en' }, key, params);
220
+ }
221
+
216
222
  /**
217
- * Run a string method against a set of elements.
218
- *
219
- * An element with no editor on it is not an error: the method is a no-op and
220
- * the set comes back for chaining, so host code does not have to check first.
221
- * `getHtml` is the exception, because reading an element's html makes sense
222
- * whether or not it is being edited.
223
+ * The element an editor goes on: an element, or the first one a selector
224
+ * matches. Anything else is a mistake worth throwing for, since there is no
225
+ * editor to hand back.
223
226
  */
224
- function dispatch(set, name, args) {
225
- var descriptor = METHODS[name];
226
-
227
- if (!descriptor) {
228
- warnOnce('method:' + name, 'unknown method "' + name + '"');
229
- return set;
227
+ function targetElement(target) {
228
+ if (typeof target === 'string') {
229
+ var found = document.querySelector(target);
230
+ if (!found) { throw new TypeError('grid-editor: no element matches ' + JSON.stringify(target)); }
231
+ return found;
230
232
  }
231
233
 
232
- if (descriptor.value) {
233
- var element = set.first();
234
- if (!element.length) { return null; }
234
+ if (target && target.nodeType === 1) { return target; }
235
235
 
236
- var instance = element.data('grideditor');
237
- if (!instance) {
238
- return descriptor.noInstance ? descriptor.noInstance(element) : null;
239
- }
236
+ throw new TypeError('grid-editor: the target is an element or a selector, not ' +
237
+ (target === null ? 'null' : typeof target));
238
+ }
240
239
 
241
- return instance[name].apply(instance, args);
242
- }
240
+ /** A node a caller handed in, as an element: an element, a selector's first match, or null. */
241
+ function nodeFrom(node) {
242
+ if (typeof node === 'string') { return document.querySelector(node); }
243
+ return node && node.nodeType === 1 ? node : null;
244
+ }
243
245
 
244
- set.each(function() {
245
- var found = $(this).data('grideditor');
246
- if (found) { found[name].apply(found, args); }
247
- });
246
+ /**
247
+ * SortableJS, and Bootstrap's Modal, as the host provided them: assigned to
248
+ * GridEditor by a page that imports them as modules, or the page's globals.
249
+ */
250
+ function sortableLibrary() {
251
+ return GridEditor.Sortable || window.Sortable || null;
252
+ }
248
253
 
249
- return set;
254
+ function modalLibrary() {
255
+ var bootstrap = GridEditor.bootstrap || window.bootstrap;
256
+ return bootstrap && bootstrap.Modal ? bootstrap.Modal : null;
250
257
  }
251
258
 
252
259
  /**
@@ -261,60 +268,85 @@ function dispatch(set, name, args) {
261
268
  function plainHtml(html) {
262
269
  // Parsed in a document of its own, which is inert: no script in the
263
270
  // markup runs and no image starts loading while it is being cleaned
264
- var root = $(document.implementation.createHTMLDocument('').body);
265
- root[0].innerHTML = html;
271
+ var root = document.implementation.createHTMLDocument('').body;
272
+ root.innerHTML = html;
266
273
  var emptied = [];
267
274
 
268
- root.find('*').each(function() {
269
- var node = $(this);
275
+ dom.all(root, '*').forEach(function(node) {
270
276
  var marked = false;
271
277
 
272
- $.each($.makeArray(this.attributes), function(i, attribute) {
278
+ Array.prototype.slice.call(node.attributes).forEach(function(attribute) {
273
279
  if (attribute.name.indexOf('data-ge-') === 0) {
274
- node.removeAttr(attribute.name);
280
+ node.removeAttribute(attribute.name);
275
281
  marked = true;
276
282
  }
277
283
  });
278
284
 
279
- var classes = (node.attr('class') || '').split(/\s+/).filter(Boolean);
285
+ var classes = (node.getAttribute('class') || '').split(/\s+/).filter(Boolean);
280
286
  var kept = classes.filter(function(name) {
281
287
  return name !== 'column' && name.indexOf('ge-') !== 0;
282
288
  });
283
289
  if (kept.length !== classes.length) { marked = true; }
284
290
  if (kept.length) {
285
- node.attr('class', kept.join(' '));
291
+ node.setAttribute('class', kept.join(' '));
286
292
  } else {
287
- node.removeAttr('class');
293
+ node.removeAttribute('class');
288
294
  }
289
295
 
290
- if (marked && this.tagName === 'DIV' && !this.attributes.length) {
291
- emptied.push(this);
296
+ if (marked && node.tagName === 'DIV' && !node.attributes.length) {
297
+ emptied.push(node);
292
298
  }
293
299
  });
294
300
 
295
301
  emptied.forEach(function(div) {
296
- $(div).replaceWith($(div).contents());
302
+ dom.unwrap(div);
297
303
  });
298
304
 
299
- return root[0].innerHTML;
305
+ return root.innerHTML;
300
306
  }
301
307
 
302
- $.fn.gridEditor = function( optionsOrMethod ) {
308
+ /**
309
+ * An editor on `target`: an element, or the first one a selector matches.
310
+ *
311
+ * var ge = new GridEditor('#myGrid', { new_row_layouts: [[12], [6, 6]] });
312
+ * var html = ge.getHtml();
313
+ *
314
+ * An element carries one editor at most: asked for a second, this hands back
315
+ * the one it has, with the options it was made with, and says so once.
316
+ */
317
+ function GridEditor(target, options) {
318
+ var element = targetElement(target);
319
+ var existing = instances.get(element);
303
320
 
304
- var self = this;
321
+ if (existing) {
322
+ if (!doubleWarned.has(element)) {
323
+ doubleWarned.add(element);
324
+ warn(english('warning.already_editing'));
325
+ }
326
+ return existing;
327
+ }
305
328
 
306
- /** Methods **/
329
+ if (!(this instanceof GridEditor)) { return new GridEditor(element, options); }
307
330
 
308
- if (typeof optionsOrMethod == 'string') {
309
- return dispatch(self, optionsOrMethod, Array.prototype.slice.call(arguments, 1));
310
- }
331
+ build(this, element, options || {});
332
+ }
333
+
334
+ /** The elements a second editor was asked for on, so each is told about once. */
335
+ var doubleWarned = new WeakSet();
336
+
337
+ GridEditor.create = function(target, options) {
338
+ return new GridEditor(target, options);
339
+ };
311
340
 
312
- /** Initialize plugin */
341
+ /** The editor on `target`, or null. */
342
+ GridEditor.get = function(target) {
343
+ var element = nodeFrom(target);
344
+ return element ? instances.get(element) || null : null;
345
+ };
313
346
 
314
- self.each(function(baseIndex, baseElem) {
315
- baseElem = $(baseElem);
347
+ function build(instance, baseElem, optionsOrMethod) {
316
348
 
317
- var settings = $.extend({
349
+ var settings = Object.assign({
318
350
  'new_row_layouts' : [ // Column layouts for add row buttons
319
351
  [12],
320
352
  [6, 6],
@@ -330,8 +362,8 @@ $.fn.gridEditor = function( optionsOrMethod ) {
330
362
  'col_tools' : [], /* Example:
331
363
  [ {
332
364
  title: 'Set background image',
333
- iconClass: 'glyphicon-picture',
334
- on: { click: function() {} }
365
+ iconClass: 'bi bi-image',
366
+ on: { click: function(event) {} }
335
367
  } ]
336
368
  */
337
369
  'row_tools' : [],
@@ -359,7 +391,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
359
391
  'resize' : NESTED_SETTINGS.resize, // Resizing a column by dragging its edge
360
392
  'source_textarea' : '',
361
393
  'edit_source' : true, // The toolbar's button to edit the canvas as html
362
- 'locale' : 'en', // Code of a locale in $.fn.gridEditor.locales
394
+ 'locale' : 'en', // Code of a locale in GridEditor.locales
363
395
  'locale_strings' : {}, // Overrides for individual keys
364
396
  'callbacks' : {}, // before_*/after_* functions, the events by another route
365
397
  'confirm_delete' : true, // Ask before deleting a row or a column
@@ -369,18 +401,18 @@ $.fn.gridEditor = function( optionsOrMethod ) {
369
401
 
370
402
  // Merged rather than replaced, so `elements: { auto: true }` keeps the
371
403
  // default selector instead of losing it
372
- $.each(NESTED_SETTINGS, function(name, defaults) {
373
- settings[name] = $.extend({}, defaults, settings[name]);
404
+ Object.keys(NESTED_SETTINGS).forEach(function(name) {
405
+ settings[name] = Object.assign({}, NESTED_SETTINGS[name], settings[name]);
374
406
  });
375
407
 
376
408
  // Both handed out the drag toolkit's own options, which 4.0 stops
377
409
  // promising: there is no widget underneath a host should be reaching
378
410
  // for. What they were used for is a setting of the editor's now.
379
- $.each(REMOVED_SETTINGS, function(name, replacement) {
411
+ Object.keys(REMOVED_SETTINGS).forEach(function(name) {
380
412
  if (optionsOrMethod && optionsOrMethod[name] !== undefined) {
381
- warn($.fn.gridEditor.t(settings, 'warning.setting_removed', {
413
+ warn(translate(settings, 'warning.setting_removed', {
382
414
  setting: name,
383
- replacement: replacement,
415
+ replacement: REMOVED_SETTINGS[name],
384
416
  }));
385
417
  }
386
418
  });
@@ -403,10 +435,24 @@ $.fn.gridEditor = function( optionsOrMethod ) {
403
435
  var warnedHere = {}; // Deprecations are worth saying once per instance, not once per call
404
436
  var sortables = []; // Every list made sortable, so deinit destroys exactly those
405
437
  var instanceId = ++editorCounter; // Scopes the sortable groups to this editor
438
+ var destroyed = false;
439
+
440
+ // What the editor remembers about the nodes it edits, which used to
441
+ // be jQuery data: a node's settings panel, a drag or a resize in
442
+ // progress. Kept off the markup, and gone with the node.
443
+ var detailsFor = new WeakMap(); // node -> its settings panel
444
+ var moves = new WeakMap(); // dragged node -> { from, canceled }
445
+ var resizes = new WeakMap(); // column -> { from, units }
446
+ var sectionNodes = new WeakMap(); // Responsive section -> the node it edits
447
+ var editableLabels = new WeakSet(); // labels made editable in place
406
448
 
407
- // Before anything else, because the instance handle hands the canvas
408
- // to hosts and the rest of setup() runs at the end of this function
409
- canvas = baseElem.addClass('ge-canvas');
449
+ // Listeners the editor puts on things it does not own - the window,
450
+ // the document, the canvas - taken off together by destroy()
451
+ var lifetime = new AbortController();
452
+
453
+ // Before anything else, because the instance hands the canvas to
454
+ // hosts and the rest of setup() runs at the end of this function
455
+ canvas = dom.addClass(baseElem, 'ge-canvas');
410
456
 
411
457
  function warnOnceHere(key, message) {
412
458
  if (warnedHere[key]) { return; }
@@ -425,7 +471,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
425
471
  */
426
472
  function setLocale(code) {
427
473
  settings.locale = code;
428
- handle.settings = settingsCopy();
474
+ instance.settings = settingsCopy();
429
475
 
430
476
  removeConfirmModal();
431
477
  removeSettingsPanels();
@@ -442,14 +488,14 @@ $.fn.gridEditor = function( optionsOrMethod ) {
442
488
  * so no caller assembles one by hand and gets a field wrong.
443
489
  *
444
490
  * `parent` is where the node is going, or where it is coming from on a
445
- * delete, which is not the same as node.parent() while the node is
491
+ * delete, which is not the same as the node's parent while the node is
446
492
  * still detached - so an add passes it in.
447
493
  */
448
494
  function payloadFor(kind, node, extra) {
449
- return $.extend({
495
+ return Object.assign({
450
496
  kind: kind,
451
497
  node: node,
452
- parent: node.parent(),
498
+ parent: node.parentElement,
453
499
  canvas: canvas,
454
500
  breakpoint: getView(),
455
501
  source: 'api',
@@ -457,11 +503,14 @@ $.fn.gridEditor = function( optionsOrMethod ) {
457
503
  }
458
504
 
459
505
  /**
460
- * Deliver one notification twice: as a jQuery event on the canvas -
461
- * the specific name first, then the generic one - and as the matching
506
+ * Deliver one notification twice: as a DOM event on the canvas - the
507
+ * specific name first, then the generic one - and as the matching
462
508
  * settings.callbacks entries. Everything is delivered whatever the
463
509
  * first listener says, and the answer is whether any of them canceled,
464
510
  * which only means something for a before-* notification.
511
+ *
512
+ * A listener that throws is the browser's to report: the other
513
+ * listeners, the callbacks and the operation go on.
465
514
  */
466
515
  function emit(name, payload) {
467
516
  var names = [name];
@@ -471,9 +520,12 @@ $.fn.gridEditor = function( optionsOrMethod ) {
471
520
  var canceled = false;
472
521
 
473
522
  names.forEach(function(eventName) {
474
- var event = $.Event('grideditor:' + eventName);
475
- canvas.trigger(event, [payload]);
476
- if (event.isDefaultPrevented()) { canceled = true; }
523
+ var event = new CustomEvent('grideditor:' + eventName, {
524
+ detail: payload,
525
+ bubbles: true,
526
+ cancelable: true,
527
+ });
528
+ if (!canvas.dispatchEvent(event)) { canceled = true; }
477
529
  });
478
530
 
479
531
  names.forEach(function(eventName) {
@@ -548,7 +600,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
548
600
  * containers binds one name, not three.
549
601
  */
550
602
  function addEventName(kind) {
551
- return $.fn.gridEditor.containers[kind] ? 'container' : kind;
603
+ return GridEditor.containers[kind] ? 'container' : kind;
552
604
  }
553
605
 
554
606
  /**
@@ -560,7 +612,8 @@ $.fn.gridEditor = function( optionsOrMethod ) {
560
612
  * so it is never part of what getHtml returns.
561
613
  *
562
614
  * A page that loaded Bootstrap's css but not its javascript still gets
563
- * asked - by the browser, as before.
615
+ * asked - by the browser, as before. A page that imports Bootstrap as
616
+ * a module hands its Modal over as GridEditor.bootstrap.
564
617
  */
565
618
  function askToDelete(message, whenConfirmed) {
566
619
  if (!settings.confirm_delete) {
@@ -568,7 +621,9 @@ $.fn.gridEditor = function( optionsOrMethod ) {
568
621
  return;
569
622
  }
570
623
 
571
- if (!window.bootstrap || !window.bootstrap.Modal) {
624
+ var Modal = modalLibrary();
625
+
626
+ if (!Modal) {
572
627
  if (window.confirm(message)) { whenConfirmed(); }
573
628
  return;
574
629
  }
@@ -576,30 +631,34 @@ $.fn.gridEditor = function( optionsOrMethod ) {
576
631
  var modal = confirmModal();
577
632
  var confirmed = false;
578
633
 
579
- modal.find('.ge-confirm-message').text(message);
580
- modal.find('.ge-confirm-ok').off('click').on('click', function() {
634
+ dom.one(modal, '.ge-confirm-message').textContent = message;
635
+ dom.one(modal, '.ge-confirm-ok').onclick = function() {
581
636
  confirmed = true;
582
- window.bootstrap.Modal.getInstance(modal[0]).hide();
583
- });
637
+ Modal.getInstance(modal).hide();
638
+ };
584
639
 
585
- modal.off('hidden.bs.modal').on('hidden.bs.modal', function() {
640
+ confirmHandlers.hidden = function() {
586
641
  // After the modal is out of the way, so the backdrop is not
587
642
  // sitting over the animation the delete runs
588
643
  if (confirmed) { whenConfirmed(); }
589
- });
644
+ };
590
645
 
591
- modal.off('shown.bs.modal').on('shown.bs.modal', function() {
592
- modal.find('.ge-confirm-ok').trigger('focus');
593
- });
646
+ confirmHandlers.shown = function() {
647
+ dom.one(modal, '.ge-confirm-ok').focus();
648
+ };
594
649
 
595
- window.bootstrap.Modal.getOrCreateInstance(modal[0]).show();
650
+ Modal.getOrCreateInstance(modal).show();
596
651
  }
597
652
 
653
+ // What the confirm modal does when Bootstrap says it is shown or
654
+ // hidden: this question's answer, replaced by the next question's
655
+ var confirmHandlers = { hidden: null, shown: null };
656
+
598
657
  /** Built once per instance, and taken away again by destroy(). */
599
658
  function confirmModal() {
600
659
  if (confirmDialog) { return confirmDialog; }
601
660
 
602
- confirmDialog = $(
661
+ confirmDialog = dom.create(
603
662
  '<div class="modal fade ge-confirm" tabindex="-1" aria-hidden="true">' +
604
663
  '<div class="modal-dialog modal-dialog-centered">' +
605
664
  '<div class="modal-content">' +
@@ -615,12 +674,21 @@ $.fn.gridEditor = function( optionsOrMethod ) {
615
674
  '</div>' +
616
675
  '</div>' +
617
676
  '</div>'
618
- ).appendTo('body');
677
+ );
678
+ document.body.appendChild(confirmDialog);
619
679
 
620
- confirmDialog.find('.modal-title').text(t('confirm.title'));
621
- confirmDialog.find('.btn-close').attr('aria-label', t('confirm.cancel'));
622
- confirmDialog.find('.ge-confirm-cancel').text(t('confirm.cancel'));
623
- confirmDialog.find('.ge-confirm-ok').text(t('confirm.ok'));
680
+ dom.one(confirmDialog, '.modal-title').textContent = t('confirm.title');
681
+ dom.one(confirmDialog, '.btn-close').setAttribute('aria-label', t('confirm.cancel'));
682
+ dom.one(confirmDialog, '.ge-confirm-cancel').textContent = t('confirm.cancel');
683
+ dom.one(confirmDialog, '.ge-confirm-ok').textContent = t('confirm.ok');
684
+
685
+ trackModal(confirmDialog);
686
+ confirmDialog.addEventListener('hidden.bs.modal', function() {
687
+ if (confirmHandlers.hidden) { confirmHandlers.hidden(); }
688
+ });
689
+ confirmDialog.addEventListener('shown.bs.modal', function() {
690
+ if (confirmHandlers.shown) { confirmHandlers.shown(); }
691
+ });
624
692
 
625
693
  return confirmDialog;
626
694
  }
@@ -629,13 +697,48 @@ $.fn.gridEditor = function( optionsOrMethod ) {
629
697
  function removeConfirmModal() {
630
698
  if (!confirmDialog) { return; }
631
699
 
632
- if (window.bootstrap && window.bootstrap.Modal) {
633
- var instance = window.bootstrap.Modal.getInstance(confirmDialog[0]);
634
- if (instance) { instance.dispose(); }
700
+ // Its answer is not wanted any more: the editor it asked for is going
701
+ confirmHandlers.hidden = null;
702
+ confirmHandlers.shown = null;
703
+ retireModal(confirmDialog);
704
+ confirmDialog = null;
705
+ }
706
+
707
+ /**
708
+ * Take a Bootstrap modal away. One out of sight goes at once. One that
709
+ * is showing, or on its way in or out, is asked to hide and goes when
710
+ * Bootstrap says it is hidden: Bootstrap finishes a transition on the
711
+ * node it started it on, and a node taken away under it throws, and
712
+ * leaves the page's body locked, with no scrolling.
713
+ */
714
+ function retireModal(panel) {
715
+ var Modal = modalLibrary();
716
+ var modal = Modal ? Modal.getInstance(panel) : null;
717
+ var gone = false;
718
+ var finish = function() {
719
+ if (gone) { return; }
720
+ gone = true;
721
+ if (modal) { modal.dispose(); }
722
+ panel.remove();
723
+ };
724
+
725
+ if (!modal || (!modalMoving.get(panel) && !dom.hasClass(panel, 'show'))) {
726
+ finish();
727
+ return;
635
728
  }
636
729
 
637
- confirmDialog.remove();
638
- confirmDialog = null;
730
+ panel.addEventListener('hidden.bs.modal', finish, { once: true });
731
+ // Still on its way in, it ignores a hide: once it is in, it takes one
732
+ panel.addEventListener('shown.bs.modal', function() { modal.hide(); }, { once: true });
733
+ modal.hide();
734
+ }
735
+
736
+ /** Whether a modal is between Bootstrap's show and shown, or hide and hidden. */
737
+ var modalMoving = new WeakMap();
738
+
739
+ function trackModal(panel) {
740
+ dom.on(panel, 'show.bs.modal hide.bs.modal', function() { modalMoving.set(panel, true); });
741
+ dom.on(panel, 'shown.bs.modal hidden.bs.modal', function() { modalMoving.set(panel, false); });
639
742
  }
640
743
 
641
744
  /**
@@ -815,51 +918,57 @@ $.fn.gridEditor = function( optionsOrMethod ) {
815
918
  * counted, so the index is the one a host would recognize.
816
919
  */
817
920
  function positionOf(node) {
818
- var parent = node.parent();
921
+ var parent = node.parentElement;
819
922
 
820
923
  return {
821
924
  parent: parent,
822
- index: parent.children().not('.ge-tools-drawer').index(node),
925
+ index: dom.children(parent).filter(function(child) {
926
+ return !dom.hasClass(child, 'ge-tools-drawer');
927
+ }).indexOf(node),
823
928
  };
824
929
  }
825
930
 
826
931
  function kindOf(node) {
827
932
  var fromPlugin = null;
828
933
 
829
- $.each(FEATURES, function(name, feature) {
934
+ Object.keys(FEATURES).forEach(function(name) {
935
+ var feature = FEATURES[name];
830
936
  if (!fromPlugin && feature.kindOf) { fromPlugin = feature.kindOf(node); }
831
937
  });
832
938
  if (fromPlugin) { return fromPlugin; }
833
939
 
834
- if (node.attr('data-ge-container')) { return node.attr('data-ge-container'); }
835
- if (node.hasClass('ge-tab')) { return 'tab'; }
836
- if (node.hasClass('ge-accordion-item')) { return 'accordion-item'; }
837
- if (node.hasClass('row')) { return 'row'; }
838
- if (node.hasClass('column')) { return 'column'; }
839
- if (node.hasClass('ge-element')) { return 'element'; }
940
+ if (node.getAttribute('data-ge-container')) { return node.getAttribute('data-ge-container'); }
941
+ if (dom.hasClass(node, 'ge-tab')) { return 'tab'; }
942
+ if (dom.hasClass(node, 'ge-accordion-item')) { return 'accordion-item'; }
943
+ if (dom.hasClass(node, 'row')) { return 'row'; }
944
+ if (dom.hasClass(node, 'column')) { return 'column'; }
945
+ if (dom.hasClass(node, 'ge-element')) { return 'element'; }
840
946
  // A content area is a text when an editor's type is on it, and the
841
947
  // host's plain content when there is none
842
- if (node.hasClass('ge-text-block')) { node = node.children('.ge-content'); }
843
- if (node.hasClass('ge-content')) { return node.attr('data-ge-content-type') ? 'text' : 'plain'; }
948
+ if (dom.hasClass(node, 'ge-text-block')) { node = dom.child(node, '.ge-content') || node; }
949
+ if (dom.hasClass(node, 'ge-content')) { return node.getAttribute('data-ge-content-type') ? 'text' : 'plain'; }
844
950
  return 'node';
845
951
  }
846
-
952
+
847
953
  // Copy html to sourceElement if a source textarea is given
848
954
  if (settings.source_textarea) {
849
- var sourceHtml = $(settings.source_textarea).val();
955
+ var sourceHtml = nodeFrom(settings.source_textarea).value;
850
956
  // Html with no grid in it goes into a row and a full width column
851
957
  // of its own, where init wraps it as the host's plain content
852
- if (sourceHtml.length > 0 && $('<div>' + sourceHtml + '</div>').find('.row').addBack('.row').length == 0) {
958
+ var probe = dom.element('div');
959
+ probe.innerHTML = sourceHtml;
960
+ if (sourceHtml.length > 0 && !probe.querySelector('.row')) {
853
961
  sourceHtml = '<div class="row"><div class="col-lg-12">' + sourceHtml + '</div></div>';
854
962
  }
855
- baseElem.html(sourceHtml);
963
+ dom.setHtml(baseElem, sourceHtml);
856
964
  }
857
-
965
+
858
966
  // Wrap content if it is non-bootstrap
859
- if (baseElem.children().length && !baseElem.find('div.row').length) {
860
- var children = baseElem.children();
861
- var newRow = $('<div class="row"><div class="col-lg-12"/></div>').appendTo(baseElem);
862
- newRow.find('.col-lg-12').append(children);
967
+ if (baseElem.children.length && !baseElem.querySelector('div.row')) {
968
+ var children = dom.children(baseElem);
969
+ var newRow = dom.create('<div class="row"><div class="col-lg-12"></div></div>');
970
+ baseElem.appendChild(newRow);
971
+ children.forEach(function(child) { newRow.firstChild.appendChild(child); });
863
972
  }
864
973
 
865
974
  // setup() and init() run at the end of this function, once every
@@ -867,29 +976,36 @@ $.fn.gridEditor = function( optionsOrMethod ) {
867
976
  // the container registry, and a var declared later is not there yet.
868
977
 
869
978
  function setup() {
870
- htmlTextArea = $('<textarea class="ge-html-output"/>').insertBefore(canvas);
979
+ htmlTextArea = dom.element('textarea', { 'class': 'ge-html-output' });
980
+ canvas.parentNode.insertBefore(htmlTextArea, canvas);
871
981
 
872
982
  createMainControls();
873
983
 
984
+ var signal = { signal: lifetime.signal };
985
+
874
986
  // Make controls fixed on scroll
875
- $(window).on('scroll', onScroll);
987
+ window.addEventListener('scroll', onScroll, signal);
876
988
 
877
989
  /* A click on the host's plain content offers to make it a text */
878
- canvas.on('click', '.ge-content', onContentClick);
990
+ dom.delegate(canvas, 'click', '.ge-content', onContentClick, signal);
879
991
 
880
992
  /* A trigger is often a link, and a link still navigates even
881
993
  with its Bootstrap attributes suspended */
882
- canvas.on('click', '.ge-popup-trigger, [data-ge-popup-target]', function(e) {
883
- if (canvas.hasClass('ge-editing')) { e.preventDefault(); }
884
- });
994
+ dom.delegate(canvas, 'click', '.ge-popup-trigger, [data-ge-popup-target]', function(e) {
995
+ if (dom.hasClass(canvas, 'ge-editing')) { e.preventDefault(); }
996
+ }, signal);
997
+ }
885
998
 
886
- // A rich text editor rewrites the content area as it takes over,
887
- // which costs the element drawers inside it. The integrations say
888
- // when their editor is ready, and the drawers go back in.
889
- canvas.on('ge-rte-ready', '.ge-content', function() {
890
- plugins('onContentReady', $(this));
891
- refreshPreviews($(this));
892
- });
999
+ /**
1000
+ * A rich text editor rewrites the content area as it takes over,
1001
+ * which costs the element drawers inside it. The integrations say
1002
+ * when their editor is ready, and the drawers go back in.
1003
+ */
1004
+ function textReady(block) {
1005
+ if (!dom.is(block, '.ge-content') || !canvas.contains(block)) { return; }
1006
+
1007
+ plugins('onContentReady', block);
1008
+ refreshPreviews(block);
893
1009
  }
894
1010
 
895
1011
  /**
@@ -897,70 +1013,72 @@ $.fn.gridEditor = function( optionsOrMethod ) {
897
1013
  * string in it comes from the locale, so setLocale() rebuilds it.
898
1014
  */
899
1015
  function createMainControls() {
900
- mainControls = $('<div class="ge-mainControls" />').insertBefore(htmlTextArea);
901
- wrapper = $('<div class="ge-wrapper ge-top" />').appendTo(mainControls);
1016
+ mainControls = dom.element('div', { 'class': 'ge-mainControls' });
1017
+ htmlTextArea.parentNode.insertBefore(mainControls, htmlTextArea);
1018
+ wrapper = mainControls.appendChild(dom.element('div', { 'class': 'ge-wrapper ge-top' }));
902
1019
 
903
1020
  // Add row
904
- addRowGroup = $('<div class="ge-addRowGroup btn-group" />').appendTo(wrapper);
905
- addContainerGroup = $('<div class="ge-addContainerGroup btn-group ms-1" />');
906
- $.each(settings.new_row_layouts, function(j, layout) {
1021
+ addRowGroup = wrapper.appendChild(dom.element('div', { 'class': 'ge-addRowGroup btn-group' }));
1022
+ addContainerGroup = dom.element('div', { 'class': 'ge-addContainerGroup btn-group ms-1' });
1023
+ settings.new_row_layouts.forEach(function(layout) {
907
1024
  var grouped = !Array.isArray(layout);
908
- var btn = $('<a class="btn btn-sm btn-primary" />')
909
- .attr('title', grouped
1025
+ var btn = dom.element('a', {
1026
+ 'class': 'btn btn-sm btn-primary',
1027
+ title: grouped
910
1028
  ? t('row.add_row_cols', { columns: layout.columns, counts: rowColsText(layout.row_cols) })
911
- : t('row.add', { layout: layout.join('-') }))
1029
+ : t('row.add', { layout: layout.join('-') }),
912
1030
  // What this button makes, in the markup rather than in
913
- // jQuery data: a drag works on a clone of it
914
- .attr('data-ge-toolbar', 'row')
915
- .attr('data-ge-layout', grouped ? JSON.stringify(layout) : layout.join(','))
916
- .on('click', function() {
917
- var row = rowFromLayoutValue(layout);
918
-
919
- var added = addNode('row', row, function() {
920
- row.appendTo(canvas);
921
- }, { parent: canvas, source: 'tool' });
922
-
923
- if (added && row[0].scrollIntoView) {
924
- row[0].scrollIntoView({behavior: 'smooth'});
925
- }
926
- })
927
- .appendTo(addRowGroup)
928
- ;
1031
+ // memory: a drag works on a clone of it
1032
+ 'data-ge-toolbar': 'row',
1033
+ 'data-ge-layout': grouped ? JSON.stringify(layout) : layout.join(','),
1034
+ });
929
1035
 
930
- btn.append('<i class="bi bi-plus"></i>');
1036
+ btn.addEventListener('click', function() {
1037
+ var row = rowFromLayoutValue(layout);
1038
+
1039
+ var added = addNode('row', row, function() {
1040
+ canvas.appendChild(row);
1041
+ }, { parent: canvas, source: 'tool' });
1042
+
1043
+ if (added && row.scrollIntoView) {
1044
+ row.scrollIntoView({behavior: 'smooth'});
1045
+ }
1046
+ });
1047
+ addRowGroup.appendChild(btn);
1048
+
1049
+ btn.appendChild(dom.create('<i class="bi bi-plus"></i>'));
931
1050
 
932
1051
  // A row of columns shares out the icon as they share the row;
933
1052
  // a row with row-cols draws one line of its widest count
934
1053
  var sizes = grouped ? rowColsIcon(layout) : layout;
935
1054
  var icon = '<div class="row ge-row-icon">';
936
1055
  sizes.forEach(function(size) {
937
- // Closed explicitly: jQuery 4 no longer expands <div/>, and
938
- // each column would be parsed inside the one before
939
1056
  icon += '<div class="column ' + sizeClass(BREAKPOINTS[0], size) + '"></div>';
940
1057
  });
941
1058
  icon += '</div>';
942
- btn.append(icon);
1059
+ btn.appendChild(dom.create(icon));
943
1060
  });
944
1061
 
945
- addContainerGroup.appendTo(wrapper);
1062
+ wrapper.appendChild(addContainerGroup);
946
1063
 
947
1064
  // A container starts in a row of its own, the way the add row
948
1065
  // buttons next to these ones do
949
- $.each(CONTAINERS, function(type, definition) {
950
- labelButton($('<a class="btn btn-sm btn-primary ge-add-container" />'), t(definition.labelKey), definition.iconClass)
951
- .attr('data-ge-toolbar', 'container')
952
- .attr('data-ge-container-type', type)
953
- .on('click', function() {
954
- var row = createRow();
955
- var column = createColumn(MAX_COL_SIZE).appendTo(row);
956
- var container = definition.create({}).appendTo(column);
957
-
958
- addNode(type, container, function() {
959
- row.appendTo(canvas);
960
- }, { parent: canvas, source: 'tool' });
961
- })
962
- .appendTo(addContainerGroup)
963
- ;
1066
+ Object.keys(CONTAINERS).forEach(function(type) {
1067
+ var definition = CONTAINERS[type];
1068
+ var button = labelButton(dom.element('a', { 'class': 'btn btn-sm btn-primary ge-add-container' }),
1069
+ t(definition.labelKey), definition.iconClass);
1070
+
1071
+ dom.attr(button, { 'data-ge-toolbar': 'container', 'data-ge-container-type': type });
1072
+ button.addEventListener('click', function() {
1073
+ var row = createRow();
1074
+ var column = row.appendChild(createColumn(MAX_COL_SIZE));
1075
+ var container = column.appendChild(definition.create({}));
1076
+
1077
+ addNode(type, container, function() {
1078
+ canvas.appendChild(row);
1079
+ }, { parent: canvas, source: 'tool' });
1080
+ });
1081
+ addContainerGroup.appendChild(button);
964
1082
  });
965
1083
 
966
1084
  // A feature plugin's own buttons, beside the containers': what
@@ -968,85 +1086,82 @@ $.fn.gridEditor = function( optionsOrMethod ) {
968
1086
  // One with align 'end' goes on the right instead, as an icon,
969
1087
  // beside the source and preview buttons.
970
1088
  var endItems = [];
971
- $.each(FEATURES, function(name, feature) {
972
- (feature.toolbar || []).forEach(function(item, index) {
1089
+ Object.keys(FEATURES).forEach(function(name) {
1090
+ (FEATURES[name].toolbar || []).forEach(function(item, index) {
973
1091
  var button = featureButton(name, item, index);
974
1092
 
975
1093
  if (item.align === 'end') {
976
1094
  endItems.push(button);
977
1095
  } else {
978
- button.appendTo(addContainerGroup);
1096
+ addContainerGroup.appendChild(button);
979
1097
  }
980
1098
  });
981
1099
  });
982
1100
 
983
1101
  // Buttons on right
984
- layoutDropdown = $('<div class="dropdown pull-right ge-layout-mode">' +
1102
+ layoutDropdown = dom.create('<div class="dropdown pull-right ge-layout-mode">' +
985
1103
  '<button type="button" class="btn btn-sm btn-primary dropdown-toggle" data-bs-toggle="dropdown"></button>' +
986
1104
  '<div class="dropdown-menu" role="menu"></div>' +
987
- '</div>')
988
- .on('click', 'a', function() {
989
- // Through changeView, so the dropdown and the method are
990
- // one path rather than two that have to agree
991
- changeView($(this).attr('data-ge-view'));
992
- })
993
- .appendTo(wrapper)
994
- ;
1105
+ '</div>');
1106
+ dom.delegate(layoutDropdown, 'click', 'a', function() {
1107
+ // Through changeView, so the dropdown and the method are
1108
+ // one path rather than two that have to agree
1109
+ changeView(this.getAttribute('data-ge-view'));
1110
+ });
1111
+ wrapper.appendChild(layoutDropdown);
1112
+
995
1113
  settings.layout_modes.forEach(function(view) {
996
- $('<a class="dropdown-item" />')
997
- .attr('data-ge-view', view)
998
- .attr('title', t(labelKeyFor(view)))
999
- .text(t(labelKeyFor(view)))
1000
- .appendTo(layoutDropdown.find('.dropdown-menu'))
1001
- ;
1114
+ dom.one(layoutDropdown, '.dropdown-menu').appendChild(dom.element('a', {
1115
+ 'class': 'dropdown-item',
1116
+ 'data-ge-view': view,
1117
+ title: t(labelKeyFor(view)),
1118
+ }, t(labelKeyFor(view))));
1002
1119
  });
1003
- layoutDropdown.find('button').text(t(labelKeyFor(curView)));
1120
+ dom.one(layoutDropdown, 'button').textContent = t(labelKeyFor(curView));
1121
+
1122
+ var btnGroup = wrapper.appendChild(dom.element('div', { 'class': 'btn-group pull-right' }));
1004
1123
 
1005
- var btnGroup = $('<div class="btn-group pull-right"/>')
1006
- .appendTo(wrapper)
1007
- ;
1008
1124
  if (settings.edit_source) {
1009
1125
  // Built again by setLocale, maybe with the source open
1010
- var htmlButton = $('<button type="button" class="btn btn-sm btn-primary gm-edit-mode"><i class="bi bi-code-slash"></i></button>')
1011
- .attr('title', t('tool.edit_source'))
1012
- .toggleClass('active btn-danger', sourceOpen)
1013
- .on('click', function() {
1014
- if (sourceOpen) {
1015
- closeSource();
1016
- } else {
1017
- openSource();
1018
- }
1126
+ var htmlButton = dom.create('<button type="button" class="btn btn-sm btn-primary gm-edit-mode"><i class="bi bi-code-slash"></i></button>');
1127
+ htmlButton.setAttribute('title', t('tool.edit_source'));
1128
+ dom.toggleClass(htmlButton, 'active btn-danger', sourceOpen);
1129
+ htmlButton.addEventListener('click', function() {
1130
+ if (sourceOpen) {
1131
+ closeSource();
1132
+ } else {
1133
+ openSource();
1134
+ }
1019
1135
 
1020
- htmlButton.toggleClass('active btn-danger', sourceOpen);
1021
- })
1022
- .appendTo(btnGroup)
1023
- ;
1136
+ dom.toggleClass(htmlButton, 'active btn-danger', sourceOpen);
1137
+ });
1138
+ btnGroup.appendChild(htmlButton);
1024
1139
  }
1025
- var previewButton = $('<button type="button" class="btn btn-sm btn-primary gm-preview"><i class="bi bi-eye-fill"></i></button>')
1026
- .attr('title', t('tool.preview'))
1027
- .on('mouseenter', function() {
1028
- canvas.removeClass('ge-editing');
1029
- })
1030
- .on('click', function() {
1031
- previewButton.toggleClass('active btn-danger').trigger('mouseleave');
1032
- })
1033
- .on('mouseleave', function() {
1034
- if (!previewButton.hasClass('active')) {
1035
- canvas.addClass('ge-editing');
1036
- }
1037
- })
1038
- .appendTo(btnGroup)
1039
- ;
1140
+
1141
+ var previewButton = dom.create('<button type="button" class="btn btn-sm btn-primary gm-preview"><i class="bi bi-eye-fill"></i></button>');
1142
+ var endPreview = function() {
1143
+ if (!dom.hasClass(previewButton, 'active')) {
1144
+ dom.addClass(canvas, 'ge-editing');
1145
+ }
1146
+ };
1147
+ previewButton.setAttribute('title', t('tool.preview'));
1148
+ previewButton.addEventListener('mouseenter', function() {
1149
+ dom.removeClass(canvas, 'ge-editing');
1150
+ });
1151
+ previewButton.addEventListener('click', function() {
1152
+ dom.toggleClass(previewButton, 'active btn-danger');
1153
+ endPreview();
1154
+ });
1155
+ previewButton.addEventListener('mouseleave', endPreview);
1156
+ btnGroup.appendChild(previewButton);
1040
1157
 
1041
1158
  // Floated right after the source and preview buttons, so it
1042
1159
  // stands to their left. Not a btn-group: its buttons show one at
1043
1160
  // a time, and a hidden one still counts as a neighbour to
1044
1161
  // Bootstrap, which squares off the corners they would share.
1045
1162
  if (endItems.length) {
1046
- $('<div class="pull-right ge-toolbar-end" />')
1047
- .append(endItems)
1048
- .appendTo(wrapper)
1049
- ;
1163
+ var end = wrapper.appendChild(dom.element('div', { 'class': 'pull-right ge-toolbar-end' }));
1164
+ endItems.forEach(function(button) { end.appendChild(button); });
1050
1165
  }
1051
1166
 
1052
1167
  makeToolbarDraggable();
@@ -1057,14 +1172,16 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1057
1172
  * with its label as the title, or a plus and the label otherwise.
1058
1173
  */
1059
1174
  function labelButton(button, label, iconClass) {
1060
- button.attr('title', label);
1175
+ button.setAttribute('title', label);
1061
1176
 
1062
- if (iconClass) { return button.append($('<i />').addClass(iconClass)); }
1177
+ if (iconClass) {
1178
+ button.appendChild(dom.element('i', { 'class': iconClass }));
1179
+ return button;
1180
+ }
1063
1181
 
1064
- return button
1065
- .append('<i class="bi bi-plus"></i>')
1066
- .append($('<span />').text(label))
1067
- ;
1182
+ button.appendChild(dom.create('<i class="bi bi-plus"></i>'));
1183
+ button.appendChild(dom.element('span', {}, label));
1184
+ return button;
1068
1185
  }
1069
1186
 
1070
1187
  /** A feature plugin's toolbar button. At the end it is always an icon, `bi bi-plus` if it has none of its own. */
@@ -1073,31 +1190,33 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1073
1190
  // A label of its own, worked out when the toolbar is built - and
1074
1191
  // built again by setLocale - or the string its labelKey names
1075
1192
  var label = typeof item.label === 'function' ? item.label() : (item.label || t(item.labelKey));
1076
- var button = labelButton($('<a class="btn btn-sm btn-primary ge-add-container ge-add-feature" />'), label, iconClass)
1077
- .addClass(item.className || '')
1078
- .attr('data-ge-toolbar', 'feature')
1079
- .attr('data-ge-feature', name)
1080
- .attr('data-ge-item', index)
1081
- .on('click', function() {
1082
- var made = item.create();
1083
-
1084
- // One that belongs in a column brings a row and a column
1085
- // of its own, the way a container does
1086
- var placed = item.inColumn ? inRowOfItsOwn(made) : made;
1087
-
1088
- var added = addNode(item.kind, made, function() {
1089
- placed.appendTo(canvas);
1090
- }, { parent: canvas, source: item.source || 'tool' });
1091
-
1092
- if (added && item.inColumn && placed[0].scrollIntoView) {
1093
- placed[0].scrollIntoView({behavior: 'smooth'});
1094
- }
1095
- })
1096
- ;
1193
+ var button = labelButton(dom.element('a', { 'class': 'btn btn-sm btn-primary ge-add-container ge-add-feature' }), label, iconClass);
1194
+
1195
+ dom.addClass(button, item.className || '');
1196
+ dom.attr(button, {
1197
+ 'data-ge-toolbar': 'feature',
1198
+ 'data-ge-feature': name,
1199
+ 'data-ge-item': index,
1200
+ });
1201
+ button.addEventListener('click', function() {
1202
+ var made = item.create();
1203
+
1204
+ // One that belongs in a column brings a row and a column
1205
+ // of its own, the way a container does
1206
+ var placed = item.inColumn ? inRowOfItsOwn(made) : made;
1207
+
1208
+ var added = addNode(item.kind, made, function() {
1209
+ canvas.appendChild(placed);
1210
+ }, { parent: canvas, source: item.source || 'tool' });
1211
+
1212
+ if (added && item.inColumn && placed.scrollIntoView) {
1213
+ placed.scrollIntoView({behavior: 'smooth'});
1214
+ }
1215
+ });
1097
1216
 
1098
1217
  return button;
1099
1218
  }
1100
-
1219
+
1101
1220
  /**
1102
1221
  * The toolbar's buttons as a palette: drag one onto the canvas and
1103
1222
  * what it makes is created where it lands, rather than at the end.
@@ -1113,14 +1232,19 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1113
1232
  }
1114
1233
 
1115
1234
  function makeToolbarDraggable() {
1116
- var buttons = mainControls.find('[data-ge-toolbar]')
1117
- .removeClass('ge-palette-button')
1118
- .off('pointerdown.ge-palette')
1119
- ;
1235
+ var buttons = dom.all(mainControls, '[data-ge-toolbar]');
1236
+
1237
+ buttons.forEach(function(button) {
1238
+ dom.removeClass(button, 'ge-palette-button');
1239
+ button.removeEventListener('pointerdown', startToolbarDrag);
1240
+ });
1120
1241
 
1121
1242
  if (!toolbarDrags()) { return; }
1122
1243
 
1123
- buttons.addClass('ge-palette-button').on('pointerdown.ge-palette', startToolbarDrag);
1244
+ buttons.forEach(function(button) {
1245
+ dom.addClass(button, 'ge-palette-button');
1246
+ button.addEventListener('pointerdown', startToolbarDrag);
1247
+ });
1124
1248
  }
1125
1249
 
1126
1250
  /**
@@ -1132,7 +1256,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1132
1256
  * also what keeps the add column tool's hold-to-pick working.
1133
1257
  */
1134
1258
  function startToolbarDrag(e) {
1135
- var button = $(e.currentTarget);
1259
+ var button = e.currentTarget;
1136
1260
  var startX = e.pageX;
1137
1261
  var startY = e.pageY;
1138
1262
  var helper = null;
@@ -1148,35 +1272,37 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1148
1272
  if (!helper) {
1149
1273
  if (!far(move)) { return; }
1150
1274
 
1151
- helper = button.clone()
1152
- .addClass('ge-toolbar-helper')
1153
- .appendTo('body')
1154
- ;
1155
- canvas.addClass('ge-dropping');
1275
+ helper = dom.addClass(button.cloneNode(true), 'ge-toolbar-helper');
1276
+ document.body.appendChild(helper);
1277
+ dom.addClass(canvas, 'ge-dropping');
1156
1278
  }
1157
1279
 
1158
- helper.css({ left: move.pageX - 14, top: move.pageY - 14 });
1280
+ dom.css(helper, { left: move.pageX - 14, top: move.pageY - 14 });
1159
1281
  showDropMarker(move.pageX, move.pageY);
1160
1282
  }
1161
1283
 
1162
1284
  function onUp(up) {
1163
- $(document)
1164
- .off('pointermove.ge-toolbar', onMove)
1165
- .off('pointerup.ge-toolbar pointercancel.ge-toolbar', onUp)
1166
- ;
1285
+ document.removeEventListener('pointermove', onMove);
1286
+ dom.off(document, 'pointerup pointercancel', onUp);
1167
1287
 
1168
1288
  if (!helper) { return; }
1169
1289
 
1170
1290
  helper.remove();
1171
- canvas.removeClass('ge-dropping');
1291
+ dom.removeClass(canvas, 'ge-dropping');
1172
1292
  hideDropMarker();
1173
1293
 
1174
1294
  // The click that follows a drag would add the block a second
1175
- // time, at the end of the canvas
1176
- button.one('click', function(click) {
1295
+ // time, at the end of the canvas. Caught on the way down, so
1296
+ // it never reaches the button's own handler.
1297
+ var swallow = function(click) {
1298
+ if (click.target !== button && !button.contains(click.target)) { return; }
1177
1299
  click.preventDefault();
1178
1300
  click.stopImmediatePropagation();
1179
- });
1301
+ };
1302
+ window.addEventListener('click', swallow, { capture: true, once: true });
1303
+ // A drag the browser does not follow with a click leaves
1304
+ // nothing to swallow: the next click is a click
1305
+ window.setTimeout(function() { window.removeEventListener('click', swallow, { capture: true }); }, 0);
1180
1306
 
1181
1307
  var where = dropPlaceAt(up.pageX, up.pageY);
1182
1308
  if (where) { insertFromToolbar(button, where); }
@@ -1185,10 +1311,8 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1185
1311
  // On the document, not on the button: until the gesture is far
1186
1312
  // enough along to be a drag there is nothing to capture the
1187
1313
  // pointer with, and the pointer has left the button by then
1188
- $(document)
1189
- .on('pointermove.ge-toolbar', onMove)
1190
- .on('pointerup.ge-toolbar pointercancel.ge-toolbar', onUp)
1191
- ;
1314
+ document.addEventListener('pointermove', onMove);
1315
+ dom.on(document, 'pointerup pointercancel', onUp);
1192
1316
  }
1193
1317
 
1194
1318
  /**
@@ -1208,18 +1332,18 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1208
1332
 
1209
1333
  if (!under) { return null; }
1210
1334
 
1211
- var region = $(under).closest(['.column', '.ge-canvas'].concat(pluginHooks('regions')).join(', '));
1212
- if (!region.length || (region[0] !== canvas[0] && !canvas[0].contains(region[0]))) {
1335
+ var region = dom.closest(under, ['.column', '.ge-canvas'].concat(pluginHooks('regions')).join(', '));
1336
+ if (!region || (region !== canvas && !canvas.contains(region))) {
1213
1337
  return null;
1214
1338
  }
1215
1339
 
1216
1340
  var before = null;
1217
1341
 
1218
- region.children(blockSelector()).each(function() {
1342
+ dom.children(region, blockSelector()).forEach(function(block) {
1219
1343
  if (before) { return; }
1220
1344
 
1221
- var box = this.getBoundingClientRect();
1222
- if (y < box.top + box.height / 2) { before = $(this); }
1345
+ var box = block.getBoundingClientRect();
1346
+ if (y < box.top + box.height / 2) { before = block; }
1223
1347
  });
1224
1348
 
1225
1349
  return { region: region, before: before };
@@ -1231,25 +1355,25 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1231
1355
  * to the canvas, just after the top level block the pointer was in.
1232
1356
  */
1233
1357
  function insertFeatureFromToolbar(button, where) {
1234
- var item = FEATURES[button.attr('data-ge-feature')].toolbar[parseInt(button.attr('data-ge-item'), 10)];
1358
+ var item = FEATURES[button.getAttribute('data-ge-feature')].toolbar[parseInt(button.getAttribute('data-ge-item'), 10)];
1235
1359
  var made = item.create();
1236
1360
  var placed = made;
1237
1361
 
1238
1362
  if (item.inColumn) {
1239
1363
  // Where it lands in a column, or in a row and a column of its
1240
1364
  // own anywhere else
1241
- if (!where.region.is('.column')) { placed = inRowOfItsOwn(made); }
1365
+ if (!dom.is(where.region, '.column')) { placed = inRowOfItsOwn(made); }
1242
1366
  } else if (!acceptsBlock(where.region, made)) {
1243
- var top = where.region.parentsUntil(canvas).addBack().first();
1244
- where = { region: canvas, before: top.length ? top.next() : null };
1245
- if (where.before && !where.before.length) { where.before = null; }
1367
+ var ancestors = where.region === canvas ? [] : dom.parentsUntil(where.region, canvas).reverse().concat([where.region]);
1368
+ var top = ancestors[0] || null;
1369
+ where = { region: canvas, before: top ? top.nextElementSibling : null };
1246
1370
  }
1247
1371
 
1248
1372
  return addNode(item.kind, made, function() {
1249
1373
  if (where.before) {
1250
- placed.insertBefore(where.before);
1374
+ where.before.parentNode.insertBefore(placed, where.before);
1251
1375
  } else {
1252
- placed.appendTo(where.region);
1376
+ where.region.appendChild(placed);
1253
1377
  }
1254
1378
  }, { parent: where.region, source: item.source || 'dragdrop' });
1255
1379
  }
@@ -1257,7 +1381,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1257
1381
  /** A detached row with one full width column holding `node`. */
1258
1382
  function inRowOfItsOwn(node) {
1259
1383
  var row = createRow();
1260
- createColumn(MAX_COL_SIZE).appendTo(row).append(node);
1384
+ row.appendChild(createColumn(MAX_COL_SIZE)).appendChild(node);
1261
1385
 
1262
1386
  return row;
1263
1387
  }
@@ -1268,12 +1392,12 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1268
1392
 
1269
1393
  if (!where) { return hideDropMarker(); }
1270
1394
 
1271
- if (!dropMarker) { dropMarker = $('<div class="ge-drop-marker" />'); }
1395
+ if (!dropMarker) { dropMarker = dom.element('div', { 'class': 'ge-drop-marker' }); }
1272
1396
 
1273
1397
  if (where.before) {
1274
- dropMarker.insertBefore(where.before);
1398
+ where.before.parentNode.insertBefore(dropMarker, where.before);
1275
1399
  } else {
1276
- dropMarker.appendTo(where.region);
1400
+ where.region.appendChild(dropMarker);
1277
1401
  }
1278
1402
 
1279
1403
  return undefined;
@@ -1288,26 +1412,26 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1288
1412
  * the pointer left it.
1289
1413
  */
1290
1414
  function insertFromToolbar(button, where) {
1291
- if (button.attr('data-ge-toolbar') === 'feature') {
1415
+ if (button.getAttribute('data-ge-toolbar') === 'feature') {
1292
1416
  return insertFeatureFromToolbar(button, where);
1293
1417
  }
1294
1418
 
1295
- var container = button.attr('data-ge-toolbar') === 'container';
1296
- var type = button.attr('data-ge-container-type');
1419
+ var container = button.getAttribute('data-ge-toolbar') === 'container';
1420
+ var type = button.getAttribute('data-ge-container-type');
1297
1421
  var made = container
1298
1422
  ? CONTAINERS[type].create({})
1299
- : rowFromLayout(button.attr('data-ge-layout'));
1423
+ : rowFromLayout(button.getAttribute('data-ge-layout'));
1300
1424
 
1301
1425
  // A container belongs in a column: dropped straight onto the
1302
1426
  // canvas it brings a row and a column of its own
1303
1427
  var placed = made;
1304
- if (container && !where.region.is('.column')) { placed = inRowOfItsOwn(made); }
1428
+ if (container && !dom.is(where.region, '.column')) { placed = inRowOfItsOwn(made); }
1305
1429
 
1306
1430
  return addNode(container ? type : 'row', made, function() {
1307
1431
  if (where.before) {
1308
- placed.insertBefore(where.before);
1432
+ where.before.parentNode.insertBefore(placed, where.before);
1309
1433
  } else {
1310
- placed.appendTo(where.region);
1434
+ where.region.appendChild(placed);
1311
1435
  }
1312
1436
  }, { parent: where.region, source: 'dragdrop' });
1313
1437
  }
@@ -1369,16 +1493,17 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1369
1493
  var row = createRow();
1370
1494
 
1371
1495
  if (Array.isArray(layout)) {
1372
- layout.forEach(function(size) { createColumn(size).appendTo(row); });
1496
+ layout.forEach(function(size) { row.appendChild(createColumn(size)); });
1373
1497
  return row;
1374
1498
  }
1375
1499
 
1376
- $.each(layout.row_cols || {}, function(key, value) {
1500
+ Object.keys(layout.row_cols || {}).forEach(function(key) {
1501
+ var value = layout.row_cols[key];
1377
1502
  var tier = breakpoint(key);
1378
- if (tier && ROW_COLS_VALUES.indexOf(String(value)) !== -1) { row.addClass(rowColsClass(tier, value)); }
1503
+ if (tier && ROW_COLS_VALUES.indexOf(String(value)) !== -1) { dom.addClass(row, rowColsClass(tier, value)); }
1379
1504
  });
1380
1505
 
1381
- for (var i = 0; i < (layout.columns || 0); i++) { createColumn(null).appendTo(row); }
1506
+ for (var i = 0; i < (layout.columns || 0); i++) { row.appendChild(createColumn(null)); }
1382
1507
 
1383
1508
  return row;
1384
1509
  }
@@ -1393,49 +1518,46 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1393
1518
  /** The icon of a row-cols layout: one line of equal columns, as many as its widest count. */
1394
1519
  function rowColsIcon(layout) {
1395
1520
  var widest = 1;
1396
- $.each(layout.row_cols || {}, function(key, value) {
1521
+ Object.keys(layout.row_cols || {}).forEach(function(key) {
1522
+ var value = layout.row_cols[key];
1397
1523
  if (value !== 'auto') { widest = Math.max(widest, parseInt(value, 10) || 1); }
1398
1524
  });
1399
1525
 
1400
1526
  return Array.apply(null, Array(Math.min(widest, layout.columns || widest))).map(function() { return 'equal'; });
1401
1527
  }
1402
1528
 
1403
- function onScroll(e) {
1404
- var $window = $(window);
1405
-
1529
+ function onScroll() {
1530
+ var scrollTop = window.pageYOffset;
1531
+
1406
1532
  if (
1407
- $window.scrollTop() > mainControls.offset().top &&
1408
- $window.scrollTop() < canvas.offset().top + canvas.height()
1533
+ scrollTop > dom.offset(mainControls).top &&
1534
+ scrollTop < dom.offset(canvas).top + dom.contentHeight(canvas)
1409
1535
  ) {
1410
- if (wrapper.hasClass('ge-top')) {
1411
- wrapper
1412
- .css({
1413
- left: wrapper.offset().left,
1414
- width: wrapper.outerWidth(),
1415
- })
1416
- .removeClass('ge-top')
1417
- .addClass('ge-fixed')
1418
- ;
1536
+ if (dom.hasClass(wrapper, 'ge-top')) {
1537
+ dom.css(wrapper, {
1538
+ left: dom.offset(wrapper).left,
1539
+ width: dom.outerWidth(wrapper),
1540
+ });
1541
+ dom.removeClass(wrapper, 'ge-top');
1542
+ dom.addClass(wrapper, 'ge-fixed');
1419
1543
  }
1420
1544
  } else {
1421
- if (wrapper.hasClass('ge-fixed')) {
1422
- wrapper
1423
- .css({ left: '', width: '' })
1424
- .removeClass('ge-fixed')
1425
- .addClass('ge-top')
1426
- ;
1545
+ if (dom.hasClass(wrapper, 'ge-fixed')) {
1546
+ dom.css(wrapper, { left: '', width: '' });
1547
+ dom.removeClass(wrapper, 'ge-fixed');
1548
+ dom.addClass(wrapper, 'ge-top');
1427
1549
  }
1428
1550
  }
1429
1551
  }
1430
-
1552
+
1431
1553
  /**
1432
1554
  * A click on the host's plain content makes it a text: of the one
1433
1555
  * editor offered, or of the one chosen when there are several. What
1434
1556
  * a click on a text does is its editor plugin's business.
1435
1557
  */
1436
1558
  function onContentClick() {
1437
- var block = $(this);
1438
- if (block.attr('data-ge-content-type')) { return; }
1559
+ var block = this;
1560
+ if (block.getAttribute('data-ge-content-type')) { return; }
1439
1561
 
1440
1562
  // A second click, with the choice still open, withdraws it
1441
1563
  if (closeSizePicker()) { return; }
@@ -1443,7 +1565,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1443
1565
  // Content nobody can see - a tab that is not the open one, a
1444
1566
  // closed accordion item - has no geometry for an editor to lay
1445
1567
  // its toolbar out against, and nothing anyone can type into
1446
- if (!block.is(':visible')) { return; }
1568
+ if (!dom.visible(block)) { return; }
1447
1569
 
1448
1570
  var offers = textOffers();
1449
1571
  if (!offers.length) { return; }
@@ -1474,11 +1596,16 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1474
1596
 
1475
1597
  if (!emit('before-convert', payload)) { return false; }
1476
1598
 
1477
- block.addClass('ge-content-type-' + offer.type).attr('data-ge-content-type', offer.type);
1599
+ dom.addClass(block, 'ge-content-type-' + offer.type);
1600
+ block.setAttribute('data-ge-content-type', offer.type);
1478
1601
 
1479
1602
  // Its plain drawer goes: the block is the plugin's now, and
1480
1603
  // the plugin gives it the drawer a text has
1481
- block.parent('.ge-text-block').removeClass('ge-plain-block').children('.ge-tools-drawer').remove();
1604
+ var textBlock = block.parentElement;
1605
+ if (dom.hasClass(textBlock, 'ge-text-block')) {
1606
+ dom.removeClass(textBlock, 'ge-plain-block');
1607
+ dom.children(textBlock, '.ge-tools-drawer').forEach(function(drawer) { drawer.remove(); });
1608
+ }
1482
1609
 
1483
1610
  emit('after-convert', payload);
1484
1611
  offer.edit(block);
@@ -1492,9 +1619,10 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1492
1619
  * drawer, when there is more than one to choose from.
1493
1620
  */
1494
1621
  function openConvertPicker(block, offers) {
1495
- var textBlock = block.parent('.ge-text-block');
1622
+ var textBlock = dom.hasClass(block.parentElement, 'ge-text-block') ? block.parentElement : null;
1623
+ if (!textBlock) { return; }
1496
1624
 
1497
- openPicker(textBlock.children('.ge-tools-drawer'), offers.map(function(offer) {
1625
+ openPicker(dom.child(textBlock, '.ge-tools-drawer'), offers.map(function(offer) {
1498
1626
  return {
1499
1627
  label: offer.label,
1500
1628
  title: t('tool.convert_type', { editor: offer.label }),
@@ -1511,31 +1639,37 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1511
1639
  */
1512
1640
  function openPicker(anchor, choices, className, hover) {
1513
1641
  closeSizePicker();
1642
+ if (!anchor) { return; }
1514
1643
 
1515
- anchor.closest('.ge-tools-drawer').addClass('ge-picker-open');
1516
- sizePicker = $('<div class="ge-size-picker" />').addClass(className || '').appendTo(anchor);
1644
+ var drawer = dom.closest(anchor, '.ge-tools-drawer');
1645
+ if (drawer) { dom.addClass(drawer, 'ge-picker-open'); }
1646
+ sizePicker = anchor.appendChild(dom.addClass(dom.element('div', { 'class': 'ge-size-picker' }), className || ''));
1517
1647
 
1518
1648
  choices.forEach(function(choice) {
1519
- $('<a class="ge-size ge-size-flex" />')
1520
- .attr(choice.attributes || {})
1521
- .attr('title', choice.title || choice.label)
1522
- .text(choice.label)
1523
- .on('click', function(e) {
1524
- e.preventDefault();
1525
- e.stopPropagation();
1526
-
1527
- closeSizePicker();
1528
- choice.choose();
1529
- })
1530
- .appendTo(sizePicker)
1531
- ;
1649
+ var button = dom.attr(dom.element('a', { 'class': 'ge-size ge-size-flex' }), choice.attributes || {});
1650
+
1651
+ button.setAttribute('title', choice.title || choice.label);
1652
+ button.textContent = choice.label;
1653
+ button.addEventListener('click', function(e) {
1654
+ e.preventDefault();
1655
+ e.stopPropagation();
1656
+
1657
+ closeSizePicker();
1658
+ choice.choose();
1659
+ });
1660
+ sizePicker.appendChild(button);
1532
1661
  });
1533
1662
 
1534
- (hover || anchor).one('mouseleave', function() {
1663
+ withdrawOnLeave(hover || anchor);
1664
+ }
1665
+
1666
+ /** Leaving `node` withdraws the picker open under it, unless the pointer went into the picker. */
1667
+ function withdrawOnLeave(node) {
1668
+ node.addEventListener('mouseleave', function() {
1535
1669
  window.setTimeout(function() {
1536
- if (sizePicker && !sizePicker.is(':hover')) { closeSizePicker(); }
1670
+ if (sizePicker && !sizePicker.matches(':hover')) { closeSizePicker(); }
1537
1671
  }, 400);
1538
- });
1672
+ }, { once: true });
1539
1673
  }
1540
1674
 
1541
1675
  /**
@@ -1546,7 +1680,8 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1546
1680
  var owners = {};
1547
1681
  var entries = [];
1548
1682
 
1549
- $.each(FEATURES, function(name, feature) {
1683
+ Object.keys(FEATURES).forEach(function(name) {
1684
+ var feature = FEATURES[name];
1550
1685
  if (!feature.textTypes) { return; }
1551
1686
 
1552
1687
  (feature.textTypes() || []).forEach(function(entry) {
@@ -1590,34 +1725,38 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1590
1725
  */
1591
1726
  function openSource() {
1592
1727
  deinit();
1593
- htmlTextArea
1594
- .height(0.8 * $(window).height())
1595
- .val(canvas.html())
1596
- .show()
1597
- ;
1598
- canvas.hide();
1728
+ htmlTextArea.style.height = (0.8 * document.documentElement.clientHeight) + 'px';
1729
+ htmlTextArea.value = canvas.innerHTML;
1730
+ dom.show(htmlTextArea);
1731
+ dom.hide(canvas);
1599
1732
  sourceOpen = true;
1600
1733
  plugins('onSourceOpen', htmlTextArea);
1601
1734
  }
1602
1735
 
1736
+ /**
1737
+ * The textarea's html back into the canvas. innerHTML, so a <script>
1738
+ * typed into the source is markup like any other and does not run in
1739
+ * the editor; the page it is published on runs it.
1740
+ */
1603
1741
  function closeSource() {
1604
1742
  plugins('onSourceClose', htmlTextArea);
1605
1743
  sourceOpen = false;
1606
- canvas.empty().html(htmlTextArea.val()).show();
1744
+ dom.setHtml(canvas, htmlTextArea.value);
1745
+ dom.show(canvas);
1607
1746
  init();
1608
- htmlTextArea.hide();
1747
+ dom.hide(htmlTextArea);
1609
1748
  }
1610
1749
 
1611
1750
  function init() {
1612
1751
  // The node whose settings were open is gone - deleted, say - and
1613
1752
  // its panel with it
1614
- if (openSettingsState && !$.contains(document.documentElement, openSettingsState.node[0])) {
1753
+ if (openSettingsState && !dom.attached(openSettingsState.node)) {
1615
1754
  closeSettings();
1616
1755
  }
1617
1756
 
1618
1757
  runFilter(true);
1619
- canvas.addClass('ge-editing');
1620
- canvas.toggleClass('ge-drag-drawer', settings.drag_handle === 'drawer');
1758
+ dom.addClass(canvas, 'ge-editing');
1759
+ dom.toggleClass(canvas, 'ge-drag-drawer', settings.drag_handle === 'drawer');
1621
1760
  addAllColClasses();
1622
1761
  var cutter = textCutter();
1623
1762
  splitTexts(cutter);
@@ -1636,26 +1775,26 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1636
1775
  function deinit() {
1637
1776
  // Its panel goes home to its drawer first, and both go together
1638
1777
  closeSettings();
1639
- canvas.removeClass('ge-editing ge-drag-drawer ge-dropping');
1778
+ dom.removeClass(canvas, 'ge-editing ge-drag-drawer ge-dropping');
1640
1779
  // Before the drawers and the text blocks come off: an editor
1641
1780
  // plugin closes its editors here, and finds them where it left them
1642
1781
  plugins('onBeforeDeinit');
1643
1782
  closeSizePicker();
1644
1783
  hideDropMarker();
1645
- canvas.find('.ge-tools-drawer').remove();
1784
+ dom.all(canvas, '.ge-tools-drawer').forEach(function(drawer) { drawer.remove(); });
1646
1785
  unwrapTexts();
1647
1786
  plugins('onDeinit');
1648
1787
  // After the rich text editors have let go of their content areas:
1649
1788
  // one that rebuilt its area's DOM brought the preview styles back
1650
1789
  // with it, and the attribute recording them came back too
1651
1790
  clearPreviews(canvas);
1652
- canvas.find('[data-ge-row-cols]').removeAttr('data-ge-row-cols');
1791
+ dom.all(canvas, '[data-ge-row-cols]').forEach(function(row) { row.removeAttribute('data-ge-row-cols'); });
1653
1792
  unmarkContainers();
1654
1793
  removeSortable();
1655
1794
  removeResizable();
1656
1795
  runFilter(false);
1657
1796
  }
1658
-
1797
+
1659
1798
  /**
1660
1799
  * The markup as a host would save it: no drawers, no editor, no
1661
1800
  * sortables. The canvas goes back to editing afterwards.
@@ -1663,7 +1802,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1663
1802
  function getHtml() {
1664
1803
  deinit();
1665
1804
  stripPixelWidths(canvas);
1666
- var html = canvas.html();
1805
+ var html = canvas.innerHTML;
1667
1806
  init();
1668
1807
  return html;
1669
1808
  }
@@ -1675,7 +1814,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1675
1814
  function nodeHtml(node) {
1676
1815
  deinit();
1677
1816
  stripPixelWidths(node);
1678
- var html = node[0].outerHTML;
1817
+ var html = node.outerHTML;
1679
1818
  init();
1680
1819
  return html;
1681
1820
  }
@@ -1685,6 +1824,12 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1685
1824
  return plainHtml(getHtml());
1686
1825
  }
1687
1826
 
1827
+ /**
1828
+ * Take the editor off the canvas: the markup stays, as getHtml would
1829
+ * give it, and everything the editor added - controls, panels,
1830
+ * listeners - goes. Safe on a canvas already taken out of the page,
1831
+ * as a framework tearing a component down does.
1832
+ */
1688
1833
  function destroy() {
1689
1834
  // The html being edited is what the canvas is left with
1690
1835
  if (sourceOpen) { closeSource(); }
@@ -1693,43 +1838,14 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1693
1838
  removeSettingsPanels();
1694
1839
  mainControls.remove();
1695
1840
  htmlTextArea.remove();
1696
- $(window).off('scroll', onScroll);
1697
- canvas.off('click', '.ge-content', onContentClick);
1698
- canvas.off('ge-rte-ready', '.ge-content');
1699
- canvas.removeData('grideditor');
1841
+ lifetime.abort();
1842
+ instances.delete(canvas);
1843
+ destroyed = true;
1700
1844
  }
1701
1845
 
1702
- function deprecatedRemove() {
1703
- warnOnceHere('remove', 'remove() is deprecated and will be removed in a later ' +
1704
- 'release. Use destroy(), which does the same thing.');
1705
- destroy();
1706
- }
1707
-
1708
-
1709
- /** The elements of one content area: its marked children, or all of them. */
1710
-
1711
- /**
1712
- * Give every element its class, its drawer and, while editing, the
1713
- * contenteditable="false" that makes a rich text editor treat it as
1714
- * one atomic thing rather than as text it may rewrite.
1715
- *
1716
- * The class is re-applied on every init rather than trusted to
1717
- * survive: an editor that snapshots and restores the markup inside a
1718
- * content area can drop it, and the marking that identifies an
1719
- * element lives in a data attribute for exactly that reason.
1720
- */
1721
-
1722
-
1723
-
1724
- /**
1725
- * What the info tool calls this element: its label, its type, or both.
1726
- * An element found by elements.auto has neither, so it is named after
1727
- * its tag, which is the only thing it has said about itself.
1728
- */
1729
-
1730
1846
  /**
1731
- * The container plugins this editor is using: the ones registered by
1732
- * the files the page loaded, narrowed by the plugins setting.
1847
+ * The plugins this editor is using: the ones registered by the files
1848
+ * the page loaded, narrowed by the plugins setting.
1733
1849
  *
1734
1850
  * Each is a factory, called once here with the handle it works
1735
1851
  * through. Everything a plugin needs from the editor goes through
@@ -1752,33 +1868,28 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1752
1868
  return factory.always === true || wanted(name);
1753
1869
  };
1754
1870
 
1755
- $.each($.fn.gridEditor.containers, function(type, factory) {
1756
- if (wanted(type)) { CONTAINERS[type] = factory(api); }
1871
+ Object.keys(GridEditor.containers).forEach(function(type) {
1872
+ if (wanted(type)) { CONTAINERS[type] = GridEditor.containers[type](api); }
1757
1873
  });
1758
1874
 
1759
- $.each($.fn.gridEditor.features, function(name, factory) {
1875
+ Object.keys(GridEditor.features).forEach(function(name) {
1876
+ var factory = GridEditor.features[name];
1760
1877
  if (featureWanted(name, factory)) { FEATURES[name] = factory(api); }
1761
1878
  });
1762
1879
 
1763
- // 5.x's registry of text editors is gone: an editor of a host's
1764
- // own is a plugin of its own since 6.0
1765
- if (!$.isEmptyObject($.fn.gridEditor.RTEs || {})) {
1766
- warnOnceHere('rtes', '$.fn.gridEditor.RTEs was removed in 6.0 and what is registered there ' +
1767
- 'is ignored: write the editor as a plugin, see docs/plugins.md');
1768
- }
1769
-
1770
- $.each($.fn.gridEditor.utilities, function(name, factory) {
1880
+ Object.keys(GridEditor.utilities).forEach(function(name) {
1771
1881
  if (!wanted(name)) { return; }
1772
1882
 
1773
- UTILITIES[name] = factory(api);
1883
+ UTILITIES[name] = GridEditor.utilities[name](api);
1774
1884
  (UTILITIES[name].families || []).forEach(function(family) {
1775
1885
  registerFamily(name, UTILITIES[name], family);
1776
1886
  });
1777
1887
  });
1778
1888
 
1779
- $.each(FEATURES, function(name, feature) {
1780
- $.each(feature.methods || {}, function(method, implementation) {
1781
- featureMethods[method] = implementation;
1889
+ Object.keys(FEATURES).forEach(function(name) {
1890
+ var methods = FEATURES[name].methods || {};
1891
+ Object.keys(methods).forEach(function(method) {
1892
+ featureMethods[method] = methods[method];
1782
1893
  });
1783
1894
  });
1784
1895
 
@@ -1797,14 +1908,10 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1797
1908
  * nodes the other two may have just made.
1798
1909
  */
1799
1910
  function plugins(hook, argument) {
1800
- $.each(CONTAINERS, function(type, definition) {
1801
- if (definition[hook]) { definition[hook](argument); }
1802
- });
1803
- $.each(FEATURES, function(name, feature) {
1804
- if (feature[hook]) { feature[hook](argument); }
1805
- });
1806
- $.each(UTILITIES, function(name, utility) {
1807
- if (utility[hook]) { utility[hook](argument); }
1911
+ [CONTAINERS, FEATURES, UTILITIES].forEach(function(registry) {
1912
+ Object.keys(registry).forEach(function(name) {
1913
+ if (registry[name][hook]) { registry[name][hook](argument); }
1914
+ });
1808
1915
  });
1809
1916
  }
1810
1917
 
@@ -1845,7 +1952,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1845
1952
  nodeHtml: nodeHtml,
1846
1953
  // A text editor has rewritten a content area, so whatever the
1847
1954
  // editor and its plugins had put in there goes back in
1848
- textReady: function(block) { block.trigger('ge-rte-ready'); },
1955
+ textReady: textReady,
1849
1956
  // A choice under a tool, offered by holding it
1850
1957
  attachPicker: attachPicker,
1851
1958
  openPicker: openPicker,
@@ -1854,19 +1961,24 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1854
1961
  detailsOf: detailsOf,
1855
1962
  // A node's drawer: its first child, or for a content area the
1856
1963
  // one beside it in its text block
1857
- drawerOf: function(node) {
1858
- return node.hasClass('ge-content')
1859
- ? node.parent('.ge-text-block').children('.ge-tools-drawer')
1860
- : node.children('.ge-tools-drawer');
1861
- },
1964
+ drawerOf: drawerOf,
1862
1965
  toolbarItems: function(name) {
1863
1966
  return mainControls
1864
- ? mainControls.find('[data-ge-toolbar="feature"][data-ge-feature="' + name + '"]')
1865
- : $();
1967
+ ? dom.all(mainControls, '[data-ge-toolbar="feature"][data-ge-feature="' + name + '"]')
1968
+ : [];
1866
1969
  },
1867
1970
  };
1868
1971
  }
1869
1972
 
1973
+ function drawerOf(node) {
1974
+ if (dom.hasClass(node, 'ge-content')) {
1975
+ var textBlock = node.parentElement;
1976
+ return dom.hasClass(textBlock, 'ge-text-block') ? dom.child(textBlock, '.ge-tools-drawer') : null;
1977
+ }
1978
+
1979
+ return dom.child(node, '.ge-tools-drawer');
1980
+ }
1981
+
1870
1982
  /* --------------------------------------------------------------
1871
1983
  * Utilities: Bootstrap's responsive utility classes.
1872
1984
  *
@@ -1900,7 +2012,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1900
2012
 
1901
2013
  FAMILIES[name] = {
1902
2014
  plugin: pluginName,
1903
- family: $.extend({}, family, {
2015
+ family: Object.assign({}, family, {
1904
2016
  name: name,
1905
2017
  values: family.values.map(String),
1906
2018
  appliesTo: family.appliesTo || utility.appliesTo || ['row', 'column'],
@@ -1932,8 +2044,10 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1932
2044
  }
1933
2045
 
1934
2046
  function familiesFor(kind) {
1935
- return $.map(FAMILIES, function(entry) {
1936
- return appliesTo(entry.family, kind) ? entry.family : null;
2047
+ return Object.keys(FAMILIES).map(function(name) {
2048
+ return FAMILIES[name].family;
2049
+ }).filter(function(family) {
2050
+ return appliesTo(family, kind);
1937
2051
  });
1938
2052
  }
1939
2053
 
@@ -1945,7 +2059,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1945
2059
 
1946
2060
  /** The value a node carries a class for at exactly this tier, or null. */
1947
2061
  function ownUtility(node, family, tier) {
1948
- var classes = (node.attr('class') || '').split(/\s+/);
2062
+ var classes = (node.getAttribute('class') || '').split(/\s+/);
1949
2063
 
1950
2064
  for (var i = 0; i < family.values.length; i++) {
1951
2065
  if (classes.indexOf(utilityClass(family, tier, family.values[i])) !== -1) {
@@ -1988,11 +2102,11 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1988
2102
 
1989
2103
  function writeUtility(node, family, tier, value) {
1990
2104
  family.values.forEach(function(candidate) {
1991
- node.removeClass(utilityClass(family, tier, candidate));
2105
+ dom.removeClass(node, utilityClass(family, tier, candidate));
1992
2106
  });
1993
2107
 
1994
- if (value !== null) { node.addClass(utilityClass(family, tier, value)); }
1995
- if (!node.attr('class')) { node.removeAttr('class'); }
2108
+ if (value !== null) { dom.addClass(node, utilityClass(family, tier, value)); }
2109
+ dom.dropEmptyClass(node);
1996
2110
  }
1997
2111
 
1998
2112
  /**
@@ -2040,10 +2154,10 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2040
2154
 
2041
2155
  /** getUtility(node, family, view?): the value that applies, or null. */
2042
2156
  function getUtility(node, name, view) {
2043
- node = $(node).first();
2157
+ node = nodeFrom(node);
2044
2158
 
2045
2159
  var family = familyNamed(name, 'getUtility');
2046
- if (!family || !node.length) { return null; }
2160
+ if (!family || !node) { return null; }
2047
2161
 
2048
2162
  var key = view === undefined ? curView : viewKey(view);
2049
2163
  if (key === null) {
@@ -2062,10 +2176,10 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2062
2176
  */
2063
2177
  function setUtility(node, name, value, options) {
2064
2178
  options = typeof options == 'string' ? { view: options } : (options || {});
2065
- node = $(node).first();
2179
+ node = nodeFrom(node);
2066
2180
 
2067
2181
  var family = familyNamed(name, 'setUtility');
2068
- if (!family || !node.length) { return false; }
2182
+ if (!family || !node) { return false; }
2069
2183
 
2070
2184
  var view = options.view === undefined ? curView : viewKey(options.view);
2071
2185
  if (view === null) {
@@ -2130,15 +2244,15 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2130
2244
  */
2131
2245
  function bareStyle(node, name, property) {
2132
2246
  var family = familyNamed(name, 'bareStyle');
2133
- node = $(node).first();
2134
- if (!family || !node.length) { return null; }
2247
+ node = nodeFrom(node);
2248
+ if (!family || !node) { return null; }
2135
2249
 
2136
- var original = node.attr('class');
2250
+ var original = node.getAttribute('class');
2137
2251
  BREAKPOINTS.forEach(function(tier) { writeUtility(node, family, tier, null); });
2138
2252
 
2139
- var value = getComputedStyle(node[0]).getPropertyValue(property);
2253
+ var value = getComputedStyle(node).getPropertyValue(property);
2140
2254
 
2141
- if (original === undefined) { node.removeAttr('class'); } else { node.attr('class', original); }
2255
+ if (original === null) { node.removeAttribute('class'); } else { node.setAttribute('class', original); }
2142
2256
 
2143
2257
  return value;
2144
2258
  }
@@ -2147,15 +2261,18 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2147
2261
  function refreshUtilities(node) {
2148
2262
  var details = detailsOf(node);
2149
2263
 
2150
- details.find('.ge-classes').val(hostClasses(node).join(' '));
2151
- details.children('.ge-utilities').each(function() { renderUtilities($(this)); });
2264
+ if (details) {
2265
+ var classes = dom.one(details, '.ge-classes');
2266
+ if (classes) { classes.value = hostClasses(node).join(' '); }
2267
+ dom.children(details, '.ge-utilities').forEach(function(section) { renderUtilities(section); });
2268
+ }
2152
2269
  refreshPreviews(node);
2153
2270
  }
2154
2271
 
2155
2272
  /* Preview */
2156
2273
 
2157
2274
  function utilityNodes(scope) {
2158
- return scope.find(UTILITY_NODES).addBack(UTILITY_NODES);
2275
+ return dom.selfAndAll(scope, UTILITY_NODES);
2159
2276
  }
2160
2277
 
2161
2278
  /**
@@ -2181,57 +2298,58 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2181
2298
  }
2182
2299
 
2183
2300
  function previewTier(scope, tier) {
2184
- utilityNodes(scope).each(function() {
2185
- var node = $(this);
2301
+ utilityNodes(scope).forEach(function(node) {
2186
2302
  var kind = kindOf(node);
2187
2303
  var styles = {};
2188
2304
 
2189
2305
  familiesFor(kind).forEach(function(family) {
2190
2306
  if (!family.preview || !utilityTiers(node, family).length) { return; }
2191
2307
 
2192
- $.extend(styles, family.preview(effectiveUtility(node, family, tier), node, kind));
2308
+ Object.assign(styles, family.preview(effectiveUtility(node, family, tier), node, kind));
2193
2309
  });
2194
2310
 
2195
- if (kind === 'column') { $.extend(styles, rowColsPreview(node, tier)); }
2311
+ if (kind === 'column') { Object.assign(styles, rowColsPreview(node, tier)); }
2196
2312
 
2197
2313
  // A plugin whose families settle one property between them
2198
2314
  // previews the node as a whole
2199
- $.each(UTILITIES, function(name, utility) {
2200
- if (utility.preview) { $.extend(styles, utility.preview(node, kind, tier.key)); }
2315
+ Object.keys(UTILITIES).forEach(function(name) {
2316
+ var utility = UTILITIES[name];
2317
+ if (utility.preview) { Object.assign(styles, utility.preview(node, kind, tier.key)); }
2201
2318
  });
2202
2319
 
2203
- if (!$.isEmptyObject(styles)) { applyPreview(node, styles); }
2320
+ if (Object.keys(styles).length) { applyPreview(node, styles); }
2204
2321
  });
2205
2322
  }
2206
2323
 
2207
2324
  /**
2208
2325
  * Set inline !important styles, which is the one thing that beats
2209
2326
  * Bootstrap's own !important, and remember what each property was so
2210
- * it can be put back. The record is an attribute rather than jQuery
2211
- * data because a rich text editor rebuilds the DOM of the area it
2212
- * edits, and the record has to come back with the node.
2327
+ * it can be put back. The record is an attribute because a rich text
2328
+ * editor rebuilds the DOM of the area it edits, and the record has to
2329
+ * come back with the node.
2213
2330
  */
2214
2331
  function applyPreview(node, styles) {
2215
- var style = node[0].style;
2332
+ var style = node.style;
2216
2333
  var was = {};
2217
2334
 
2218
- $.each(styles, function(property, value) {
2335
+ Object.keys(styles).forEach(function(property) {
2219
2336
  was[property] = [style.getPropertyValue(property), style.getPropertyPriority(property)];
2220
- style.setProperty(property, String(value), 'important');
2337
+ style.setProperty(property, String(styles[property]), 'important');
2221
2338
  });
2222
2339
 
2223
- node.attr(PREVIEW_ATTR, JSON.stringify(was));
2340
+ node.setAttribute(PREVIEW_ATTR, JSON.stringify(was));
2224
2341
  }
2225
2342
 
2226
2343
  function clearPreviews(scope) {
2227
- scope.find('[' + PREVIEW_ATTR + ']').addBack('[' + PREVIEW_ATTR + ']').each(function() {
2228
- var node = $(this);
2229
- var style = this.style;
2344
+ dom.selfAndAll(scope, '[' + PREVIEW_ATTR + ']').forEach(function(node) {
2345
+ var style = node.style;
2230
2346
  var was = {};
2231
2347
 
2232
- try { was = JSON.parse(node.attr(PREVIEW_ATTR)) || {}; } catch (error) { /* a mangled record: drop it */ }
2348
+ try { was = JSON.parse(node.getAttribute(PREVIEW_ATTR)) || {}; } catch (error) { /* a mangled record: drop it */ }
2349
+
2350
+ Object.keys(was).forEach(function(property) {
2351
+ var before = was[property];
2233
2352
 
2234
- $.each(was, function(property, before) {
2235
2353
  if (before && before[0]) {
2236
2354
  style.setProperty(property, before[0], before[1]);
2237
2355
  } else {
@@ -2239,8 +2357,8 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2239
2357
  }
2240
2358
  });
2241
2359
 
2242
- node.removeAttr(PREVIEW_ATTR);
2243
- if (!node.attr('style')) { node.removeAttr('style'); }
2360
+ node.removeAttribute(PREVIEW_ATTR);
2361
+ dom.dropEmptyStyle(node);
2244
2362
  });
2245
2363
  }
2246
2364
 
@@ -2259,28 +2377,29 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2259
2377
  .filter(function(family) { return family.panel !== false; })
2260
2378
  .map(function(family) { return createField(node, family); });
2261
2379
 
2262
- $.each(UTILITIES, function(name, utility) {
2380
+ Object.keys(UTILITIES).forEach(function(name) {
2381
+ var utility = UTILITIES[name];
2263
2382
  var own = utility.panel ? utility.panel(node, kind) : null;
2264
- if (own && own.length) { fields.push(own); }
2383
+ if (own) { fields.push(own); }
2265
2384
  });
2266
2385
 
2267
2386
  if (!fields.length) { return null; }
2268
2387
 
2269
- var section = $('<div class="ge-utilities" />')
2270
- .toggleClass('ge-open', utilitiesOpen)
2271
- .data('ge-node', node)
2272
- ;
2388
+ var section = dom.toggleClass(dom.element('div', { 'class': 'ge-utilities' }), 'ge-open', utilitiesOpen);
2389
+ sectionNodes.set(section, node);
2273
2390
 
2274
- $('<a class="ge-utilities-toggle" />')
2275
- .appendTo(section)
2276
- .on('click', function() {
2277
- utilitiesOpen = !section.hasClass('ge-open');
2278
- settingsScope().find('.ge-utilities').toggleClass('ge-open', utilitiesOpen);
2279
- })
2280
- ;
2391
+ var toggle = section.appendChild(dom.element('a', { 'class': 'ge-utilities-toggle' }));
2392
+ toggle.addEventListener('click', function() {
2393
+ utilitiesOpen = !dom.hasClass(section, 'ge-open');
2394
+ settingsScope().forEach(function(scope) {
2395
+ dom.all(scope, '.ge-utilities').forEach(function(each) {
2396
+ dom.toggleClass(each, 'ge-open', utilitiesOpen);
2397
+ });
2398
+ });
2399
+ });
2281
2400
 
2282
- var body = $('<div class="ge-utilities-body" />').appendTo(section);
2283
- fields.forEach(function(field) { field.appendTo(body); });
2401
+ var body = section.appendChild(dom.element('div', { 'class': 'ge-utilities-body' }));
2402
+ fields.forEach(function(field) { body.appendChild(field); });
2284
2403
 
2285
2404
  renderUtilities(section);
2286
2405
 
@@ -2293,19 +2412,17 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2293
2412
  * view or the node's classes change.
2294
2413
  */
2295
2414
  function createField(node, family) {
2296
- var field = $('<label class="ge-utility" />').attr('data-ge-family', family.name);
2297
-
2298
- $('<span class="ge-utility-label" />')
2299
- .text(family.labelKey ? t(family.labelKey) : family.name)
2300
- .appendTo(field)
2301
- ;
2302
- $('<select class="form-select form-select-sm" />')
2303
- .appendTo(field)
2304
- .on('change', function() {
2305
- setUtility(node, family.name, this.value, { source: 'panel' });
2306
- })
2307
- ;
2308
- $('<small class="ge-utility-note" />').appendTo(field);
2415
+ var field = dom.element('label', { 'class': 'ge-utility', 'data-ge-family': family.name });
2416
+
2417
+ field.appendChild(dom.element('span', { 'class': 'ge-utility-label' },
2418
+ family.labelKey ? t(family.labelKey) : family.name));
2419
+
2420
+ var select = field.appendChild(dom.element('select', { 'class': 'form-select form-select-sm' }));
2421
+ select.addEventListener('change', function() {
2422
+ setUtility(node, family.name, this.value, { source: 'panel' });
2423
+ });
2424
+
2425
+ field.appendChild(dom.element('small', { 'class': 'ge-utility-note' }));
2309
2426
 
2310
2427
  return field;
2311
2428
  }
@@ -2313,7 +2430,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2313
2430
  /** ge.utilityField(node, family): a field a plugin places in a panel of its own. */
2314
2431
  function utilityField(node, name) {
2315
2432
  var family = familyNamed(name, 'utilityField');
2316
- if (!family) { return $(); }
2433
+ if (!family) { return null; }
2317
2434
 
2318
2435
  var field = createField(node, family);
2319
2436
  renderField(field, node);
@@ -2323,20 +2440,23 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2323
2440
 
2324
2441
  /** Fill a section's fields, the families' and the plugins' own, for the view being edited. */
2325
2442
  function renderUtilities(section) {
2326
- var node = section.data('ge-node');
2443
+ var node = sectionNodes.get(section);
2327
2444
 
2328
- section.children('.ge-utilities-toggle')
2329
- .text(t('utility.section', { view: t(labelKeyFor(curView)) }));
2445
+ dom.children(section, '.ge-utilities-toggle').forEach(function(toggle) {
2446
+ toggle.textContent = t('utility.section', { view: t(labelKeyFor(curView)) });
2447
+ });
2330
2448
 
2331
- section.find('.ge-utility').each(function() { renderField($(this), node); });
2449
+ dom.all(section, '.ge-utility').forEach(function(field) { renderField(field, node); });
2332
2450
  }
2333
2451
 
2334
2452
  function renderField(field, node) {
2335
- var family = FAMILIES[field.attr('data-ge-family')].family;
2336
- var select = field.children('select').empty();
2453
+ var family = FAMILIES[field.getAttribute('data-ge-family')].family;
2454
+ var select = dom.child(field, 'select');
2337
2455
  var choices = family.choices ? family.choices(node, kindOf(node)) : family.values;
2338
2456
  var own, blank, note = '';
2339
2457
 
2458
+ select.innerHTML = '';
2459
+
2340
2460
  if (curView === ALL_VIEW) {
2341
2461
  own = ownUtility(node, family, BREAKPOINTS[0]);
2342
2462
  blank = t('utility.default');
@@ -2368,13 +2488,16 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2368
2488
  // would not offer it here, rather than shown as something else
2369
2489
  if (own !== null && choices.indexOf(own) === -1) { choices = choices.concat([own]); }
2370
2490
 
2371
- $('<option value="" />').text(blank).appendTo(select);
2491
+ select.appendChild(dom.element('option', { value: '' }, blank));
2372
2492
  choices.forEach(function(value) {
2373
- $('<option />').attr('value', value).text(labelOf(family, value)).appendTo(select);
2493
+ select.appendChild(dom.element('option', { value: value }, labelOf(family, value)));
2374
2494
  });
2375
2495
 
2376
- select.val(own === null ? '' : own);
2377
- field.children('.ge-utility-note').text(note).toggle(note !== '');
2496
+ select.value = own === null ? '' : own;
2497
+
2498
+ var noteNode = dom.child(field, '.ge-utility-note');
2499
+ noteNode.textContent = note;
2500
+ dom.toggle(noteNode, note !== '');
2378
2501
  }
2379
2502
 
2380
2503
  function labelOf(family, value) {
@@ -2401,9 +2524,9 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2401
2524
  var containerCounter = 0;
2402
2525
 
2403
2526
  /**
2404
- * Ids go into the markup rather than into jQuery data: Bootstrap's
2405
- * toggles are written in terms of them, and the markup has to survive
2406
- * getHtml with those toggles still pointing at the right panes.
2527
+ * Ids go into the markup rather than into memory: Bootstrap's toggles
2528
+ * are written in terms of them, and the markup has to survive getHtml
2529
+ * with those toggles still pointing at the right panes.
2407
2530
  */
2408
2531
  function containerId(type) {
2409
2532
  containerCounter++;
@@ -2415,17 +2538,16 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2415
2538
  /** A pane's starting content: one full width column, ready to edit. */
2416
2539
  function defaultRegion() {
2417
2540
  var row = createRow();
2418
- createColumn(MAX_COL_SIZE).appendTo(row);
2541
+ row.appendChild(createColumn(MAX_COL_SIZE));
2419
2542
  return row;
2420
2543
  }
2421
2544
 
2422
2545
  function containerTypeOf(container) {
2423
- return container.attr('data-ge-container');
2546
+ return container ? container.getAttribute('data-ge-container') : null;
2424
2547
  }
2425
2548
 
2426
2549
  function markContainers() {
2427
- canvas.find('[data-ge-container]').each(function() {
2428
- var container = $(this);
2550
+ dom.all(canvas, '[data-ge-container]').forEach(function(container) {
2429
2551
  var type = containerTypeOf(container);
2430
2552
  var definition = CONTAINERS[type];
2431
2553
 
@@ -2434,43 +2556,54 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2434
2556
  return;
2435
2557
  }
2436
2558
 
2437
- container.addClass('ge-container ge-container-' + type);
2559
+ dom.addClass(container, 'ge-container ge-container-' + type);
2438
2560
  definition.mark(container);
2439
2561
 
2440
- if (!container.find('> .ge-tools-drawer').length) {
2562
+ if (!dom.child(container, '.ge-tools-drawer')) {
2441
2563
  createContainerControls(container, type, definition);
2442
2564
  }
2443
2565
  });
2444
2566
  }
2445
2567
 
2446
2568
  function unmarkContainers() {
2447
- canvas.find('[data-ge-container]').each(function() {
2448
- var container = $(this);
2569
+ dom.all(canvas, '[data-ge-container]').forEach(function(container) {
2449
2570
  var definition = CONTAINERS[containerTypeOf(container)];
2450
2571
 
2451
2572
  if (definition) { definition.unmark(container); }
2452
2573
 
2453
- container.removeClass('ge-container ge-container-' + containerTypeOf(container));
2454
- if (!container.attr('class')) { container.removeAttr('class'); }
2574
+ dom.removeClass(container, 'ge-container ge-container-' + containerTypeOf(container));
2575
+ dom.dropEmptyClass(container);
2576
+ });
2577
+ }
2578
+
2579
+ /** A new drawer as the first child of `node`. */
2580
+ function prependDrawer(node, className) {
2581
+ var drawer = dom.element('div', { 'class': className });
2582
+ node.insertBefore(drawer, node.firstChild);
2583
+ return drawer;
2584
+ }
2585
+
2586
+ /** The host's own tools, from a *_tools setting, in a drawer. */
2587
+ function hostTools(drawer, tools) {
2588
+ (tools || []).forEach(function(hostTool) {
2589
+ createTool(drawer, hostTool.title || '', hostTool.className || '',
2590
+ hostTool.iconClass || 'bi bi-wrench', hostTool.on);
2455
2591
  });
2456
2592
  }
2457
2593
 
2458
2594
  function createContainerControls(container, type, definition) {
2459
- var drawer = $('<div class="ge-tools-drawer ge-container-drawer" />').prependTo(container);
2595
+ var drawer = prependDrawer(container, 'ge-tools-drawer ge-container-drawer');
2460
2596
 
2461
2597
  createMoveTool(drawer);
2462
2598
  addSettingsTool(drawer, container, settings.container_classes);
2463
2599
 
2464
- settings.container_tools.forEach(function(hostTool) {
2465
- createTool(drawer, hostTool.title || '', hostTool.className || '',
2466
- hostTool.iconClass || 'bi bi-wrench', hostTool.on);
2467
- });
2600
+ hostTools(drawer, settings.container_tools);
2468
2601
 
2469
2602
  if (definition.tools) { definition.tools(drawer, container); }
2470
2603
 
2471
2604
  createTool(drawer, t('tool.delete_container'), 'ge-delete-container', 'bi bi-trash', function() {
2472
2605
  deleteNode(type, container, t('confirm.delete_container'), function(removed) {
2473
- container.slideUp(removed);
2606
+ dom.slideUp(container, removed);
2474
2607
  });
2475
2608
  });
2476
2609
 
@@ -2492,16 +2625,13 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2492
2625
  * A pane drawer: small, inline, and made the same way for every
2493
2626
  * container type so a tab and an accordion item behave alike.
2494
2627
  */
2495
- function createPaneControls(pane, kind, hostTools, confirmText, remove) {
2496
- var drawer = $('<div class="ge-tools-drawer ge-pane-drawer" />').prependTo(pane);
2628
+ function createPaneControls(pane, kind, tools, confirmText, remove) {
2629
+ var drawer = prependDrawer(pane, 'ge-tools-drawer ge-pane-drawer');
2497
2630
 
2498
2631
  createMoveTool(drawer);
2499
2632
  addSettingsTool(drawer, pane, settings.pane_classes);
2500
2633
 
2501
- hostTools.forEach(function(hostTool) {
2502
- createTool(drawer, hostTool.title || '', hostTool.className || '',
2503
- hostTool.iconClass || 'bi bi-wrench', hostTool.on);
2504
- });
2634
+ hostTools(drawer, tools);
2505
2635
 
2506
2636
  createTool(drawer, t('tool.delete_pane'), 'ge-delete-pane', 'bi bi-trash', function() {
2507
2637
  deleteNode(kind, pane, confirmText, function(removed) {
@@ -2523,31 +2653,31 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2523
2653
  * way out.
2524
2654
  */
2525
2655
  function suspendToggles(scope) {
2526
- scope.find('[data-bs-toggle], [data-bs-dismiss]').addBack('[data-bs-toggle], [data-bs-dismiss]')
2527
- .each(function() {
2528
- var node = $(this);
2656
+ if (!scope) { return; }
2529
2657
 
2530
- ['toggle', 'dismiss'].forEach(function(name) {
2531
- var value = node.attr('data-bs-' + name);
2532
- if (value === undefined) { return; }
2658
+ dom.selfAndAll(scope, '[data-bs-toggle], [data-bs-dismiss]').forEach(function(node) {
2659
+ ['toggle', 'dismiss'].forEach(function(name) {
2660
+ var value = node.getAttribute('data-bs-' + name);
2661
+ if (value === null) { return; }
2533
2662
 
2534
- node.attr('data-ge-bs-' + name, value).removeAttr('data-bs-' + name);
2535
- });
2663
+ node.setAttribute('data-ge-bs-' + name, value);
2664
+ node.removeAttribute('data-bs-' + name);
2536
2665
  });
2666
+ });
2537
2667
  }
2538
2668
 
2539
2669
  function resumeToggles(scope) {
2540
- scope.find('[data-ge-bs-toggle], [data-ge-bs-dismiss]').addBack('[data-ge-bs-toggle], [data-ge-bs-dismiss]')
2541
- .each(function() {
2542
- var node = $(this);
2670
+ if (!scope) { return; }
2543
2671
 
2544
- ['toggle', 'dismiss'].forEach(function(name) {
2545
- var value = node.attr('data-ge-bs-' + name);
2546
- if (value === undefined) { return; }
2672
+ dom.selfAndAll(scope, '[data-ge-bs-toggle], [data-ge-bs-dismiss]').forEach(function(node) {
2673
+ ['toggle', 'dismiss'].forEach(function(name) {
2674
+ var value = node.getAttribute('data-ge-bs-' + name);
2675
+ if (value === null) { return; }
2547
2676
 
2548
- node.attr('data-bs-' + name, value).removeAttr('data-ge-bs-' + name);
2549
- });
2677
+ node.setAttribute('data-bs-' + name, value);
2678
+ node.removeAttribute('data-ge-bs-' + name);
2550
2679
  });
2680
+ });
2551
2681
  }
2552
2682
 
2553
2683
  /**
@@ -2556,100 +2686,52 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2556
2686
  * edited: typing in a tab's name must not switch tabs.
2557
2687
  */
2558
2688
  function makeLabelEditable(label) {
2559
- if (label.data('ge-editable')) { return; }
2689
+ if (editableLabels.has(label)) { return; }
2560
2690
 
2561
- var toggle = label.closest('[data-bs-toggle], [data-ge-bs-toggle]');
2691
+ var toggle = dom.closest(label, '[data-bs-toggle], [data-ge-bs-toggle]');
2562
2692
 
2563
- label.data('ge-editable', true)
2564
- .attr('title', t('tool.rename'))
2565
- .on('dblclick', function(e) {
2566
- e.preventDefault();
2567
- e.stopPropagation();
2693
+ editableLabels.add(label);
2694
+ label.setAttribute('title', t('tool.rename'));
2695
+ label.addEventListener('dblclick', function(e) {
2696
+ e.preventDefault();
2697
+ e.stopPropagation();
2568
2698
 
2569
- suspendToggles(toggle);
2570
- label.attr('contenteditable', 'true').trigger('focus');
2571
- window.getSelection().selectAllChildren(label[0]);
2572
- })
2573
- .on('keydown', function(e) {
2574
- if (label.attr('contenteditable') !== 'true') { return; }
2699
+ suspendToggles(toggle);
2700
+ label.setAttribute('contenteditable', 'true');
2701
+ label.focus();
2702
+ window.getSelection().selectAllChildren(label);
2703
+ });
2704
+ label.addEventListener('keydown', function(e) {
2705
+ if (label.getAttribute('contenteditable') !== 'true') { return; }
2575
2706
 
2576
- if (e.key === 'Enter') {
2577
- e.preventDefault();
2578
- label.trigger('blur');
2579
- }
2580
- })
2581
- .on('blur', function() {
2582
- label.removeAttr('contenteditable');
2583
- resumeToggles(toggle);
2584
- })
2585
- ;
2707
+ if (e.key === 'Enter') {
2708
+ e.preventDefault();
2709
+ label.blur();
2710
+ }
2711
+ });
2712
+ label.addEventListener('blur', function() {
2713
+ label.removeAttribute('contenteditable');
2714
+ resumeToggles(toggle);
2715
+ });
2586
2716
  }
2587
2717
 
2588
-
2589
- /** Exactly one tab is the active one, in the strip and in the content. */
2590
-
2591
- /**
2592
- * An accordion whose items carry no data-bs-parent is one Bootstrap
2593
- * lets you open several items in at once. The markup is the state:
2594
- * there is nothing else to remember it in.
2595
- */
2596
-
2597
-
2598
- /**
2599
- * An item dropped into another accordion collapses against the one it
2600
- * landed in, and takes on that accordion's idea of whether several
2601
- * items may be open at once.
2602
- */
2603
- /**
2604
- * Open or close an item while editing.
2605
- *
2606
- * The editor does this itself rather than letting Bootstrap's collapse
2607
- * run over the canvas, and it writes what it does to data-ge-open, so
2608
- * what is left open here is what the authored page opens with. An
2609
- * accordion that closes its siblings - one without stay_open - closes
2610
- * them here too, because the canvas is meant to look like the page.
2611
- */
2612
-
2613
- /** One item's state, in the attribute and in Bootstrap's own classes. */
2614
-
2615
-
2616
-
2617
-
2618
- /**
2619
- * A trigger is any node the host marked with data-ge-popup-target,
2620
- * plus the button the container makes for itself. Ids go stale - a
2621
- * popup deleted, a trigger pasted from another page - so what can be
2622
- * repaired is repaired, and the rest is reported rather than removed.
2623
- * Grid-editor never deletes a node the host wrote.
2624
- */
2625
-
2626
- /**
2627
- * On the way out, every trigger gets the attributes that make
2628
- * Bootstrap open the modal in the authored page. An orphan gets
2629
- * nothing, because there is nothing to point it at.
2630
- */
2631
-
2632
-
2633
- /**
2634
- * What each container type is made of, and what has to happen to one
2635
- * while it is being edited. Everything type specific lives here; the
2636
- * core above treats them all the same.
2637
- */
2638
2718
  /** The label inside a pane's button, wrapped so it can be edited alone. */
2639
2719
  function labelIn(button) {
2640
- var label = button.find('> .ge-pane-label');
2720
+ var label = dom.child(button, '.ge-pane-label');
2641
2721
 
2642
- if (!label.length) {
2643
- label = $('<span class="ge-pane-label" />').text(button.text().trim());
2644
- button.empty().append(label);
2722
+ if (!label) {
2723
+ label = dom.element('span', { 'class': 'ge-pane-label' }, button.textContent.trim());
2724
+ button.innerHTML = '';
2725
+ button.appendChild(label);
2645
2726
  }
2646
2727
 
2647
2728
  return label;
2648
2729
  }
2649
2730
 
2650
2731
  function unwrapLabels(scope) {
2651
- scope.find('.ge-pane-label').each(function() {
2652
- $(this).removeData('ge-editable').contents().unwrap();
2732
+ dom.all(scope, '.ge-pane-label').forEach(function(label) {
2733
+ editableLabels.delete(label);
2734
+ dom.unwrap(label);
2653
2735
  });
2654
2736
  }
2655
2737
 
@@ -2660,7 +2742,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2660
2742
  var column = createColumn(size);
2661
2743
 
2662
2744
  return addNode('column', column, function() {
2663
- row.append(column);
2745
+ row.appendChild(column);
2664
2746
  }, { parent: row, source: 'tool' });
2665
2747
  }
2666
2748
 
@@ -2673,7 +2755,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2673
2755
  * see is a gesture nobody finds.
2674
2756
  */
2675
2757
  function attachSizePicker(tool, row) {
2676
- if (!settings.add_column.picker) { return; }
2758
+ if (!settings.add_column.picker || !tool) { return; }
2677
2759
 
2678
2760
  attachPicker(tool, function() { openSizePicker(tool, row); });
2679
2761
  }
@@ -2690,7 +2772,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2690
2772
  timer = null;
2691
2773
  };
2692
2774
 
2693
- tool.on('mouseenter mousedown', function() {
2775
+ dom.on(tool, 'mouseenter mousedown', function() {
2694
2776
  if (timer || sizePicker) { return; }
2695
2777
 
2696
2778
  timer = window.setTimeout(function() {
@@ -2699,8 +2781,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2699
2781
  }, settings.add_column.delay);
2700
2782
  });
2701
2783
 
2702
- tool.on('mouseleave', cancel);
2703
- tool.on('mouseup', cancel);
2784
+ dom.on(tool, 'mouseleave mouseup', cancel);
2704
2785
  }
2705
2786
 
2706
2787
  /**
@@ -2716,37 +2797,34 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2716
2797
  // The drawer it hangs off is raised while it is open: drawers sit
2717
2798
  // below the resize handles, so without this the drawer of the
2718
2799
  // column below takes the clicks meant for the picker
2719
- tool.closest('.ge-tools-drawer').addClass('ge-picker-open');
2800
+ var drawer = dom.closest(tool, '.ge-tools-drawer');
2801
+ if (drawer) { dom.addClass(drawer, 'ge-picker-open'); }
2720
2802
 
2721
- sizePicker = $('<div class="ge-size-picker" />').appendTo(tool);
2803
+ sizePicker = tool.appendChild(dom.element('div', { 'class': 'ge-size-picker' }));
2722
2804
 
2723
2805
  settings.valid_col_sizes.forEach(function(size) {
2724
- $('<a class="ge-size" />')
2725
- .attr('data-ge-size', size)
2726
- .attr('title', sizeTitle(size))
2727
- .toggleClass('ge-size-tight', isUnits(size) && size > room)
2728
- .toggleClass('ge-size-flex', !isUnits(size))
2729
- .text(isUnits(size) ? size : sizeClass(BREAKPOINTS[0], size))
2730
- .on('click', function(e) {
2731
- e.preventDefault();
2732
- e.stopPropagation();
2733
-
2734
- closeSizePicker();
2735
- addColumnTo(row, size);
2736
- })
2737
- .appendTo(sizePicker)
2738
- ;
2806
+ var choice = dom.element('a', {
2807
+ 'class': 'ge-size',
2808
+ 'data-ge-size': size,
2809
+ title: sizeTitle(size),
2810
+ }, isUnits(size) ? size : sizeClass(BREAKPOINTS[0], size));
2811
+
2812
+ dom.toggleClass(choice, 'ge-size-tight', isUnits(size) && size > room);
2813
+ dom.toggleClass(choice, 'ge-size-flex', !isUnits(size));
2814
+ choice.addEventListener('click', function(e) {
2815
+ e.preventDefault();
2816
+ e.stopPropagation();
2817
+
2818
+ closeSizePicker();
2819
+ addColumnTo(row, size);
2820
+ });
2821
+ sizePicker.appendChild(choice);
2739
2822
  });
2740
2823
 
2741
2824
  // Anywhere else, and the question is withdrawn
2742
- tool.one('mouseleave', function() {
2743
- window.setTimeout(function() {
2744
- if (sizePicker && !sizePicker.is(':hover')) { closeSizePicker(); }
2745
- }, 400);
2746
- });
2825
+ withdrawOnLeave(tool);
2747
2826
  }
2748
2827
 
2749
- /** True when there was one to close, which is also a click's answer. */
2750
2828
  function sizeTitle(size) {
2751
2829
  if (size === 'equal') { return t('tool.column_equal'); }
2752
2830
  if (size === 'auto') { return t('tool.column_auto'); }
@@ -2754,10 +2832,12 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2754
2832
  return t('tool.column_size', { size: size });
2755
2833
  }
2756
2834
 
2835
+ /** True when there was one to close, which is also a click's answer. */
2757
2836
  function closeSizePicker() {
2758
2837
  if (!sizePicker) { return false; }
2759
2838
 
2760
- sizePicker.closest('.ge-tools-drawer').removeClass('ge-picker-open');
2839
+ var drawer = dom.closest(sizePicker, '.ge-tools-drawer');
2840
+ if (drawer) { dom.removeClass(drawer, 'ge-picker-open'); }
2761
2841
  sizePicker.remove();
2762
2842
  sizePicker = null;
2763
2843
 
@@ -2765,21 +2845,17 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2765
2845
  }
2766
2846
 
2767
2847
  function createRowControls() {
2768
- canvas.find('.row').each(function() {
2769
- var row = $(this);
2770
- if (row.find('> .ge-tools-drawer').length) { return; }
2848
+ dom.all(canvas, '.row').forEach(function(row) {
2849
+ if (dom.child(row, '.ge-tools-drawer')) { return; }
2771
2850
 
2772
- var drawer = $('<div class="ge-tools-drawer" />').prependTo(row);
2851
+ var drawer = prependDrawer(row, 'ge-tools-drawer');
2773
2852
  createMoveTool(drawer);
2774
2853
  addSettingsTool(drawer, row, settings.row_classes);
2775
2854
 
2776
- settings.row_tools.forEach(function(hostTool) {
2777
- createTool(drawer, hostTool.title || '', hostTool.className || '',
2778
- hostTool.iconClass || 'bi bi-wrench', hostTool.on);
2779
- });
2855
+ hostTools(drawer, settings.row_tools);
2780
2856
  createTool(drawer, t('tool.delete_row'), 'ge-delete-row', 'bi bi-trash', function() {
2781
2857
  deleteNode('row', row, t('confirm.delete_row'), function(removed) {
2782
- row.slideUp(removed);
2858
+ dom.slideUp(row, removed);
2783
2859
  });
2784
2860
  });
2785
2861
  createTool(drawer, t('tool.add_column'), 'ge-add-column', 'bi bi-plus-circle', function() {
@@ -2789,17 +2865,16 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2789
2865
  addColumnTo(row, rowColsSource(row, leadingTier()) ? null : settings.add_column.size);
2790
2866
  });
2791
2867
 
2792
- attachSizePicker(drawer.find('> .ge-add-column'), row);
2868
+ attachSizePicker(dom.child(drawer, '.ge-add-column'), row);
2793
2869
 
2794
2870
  });
2795
2871
  }
2796
2872
 
2797
2873
  function createColControls() {
2798
- canvas.find('.column').each(function() {
2799
- var col = $(this);
2800
- if (col.find('> .ge-tools-drawer').length) { return; }
2874
+ dom.all(canvas, '.column').forEach(function(col) {
2875
+ if (dom.child(col, '.ge-tools-drawer')) { return; }
2801
2876
 
2802
- var drawer = $('<div class="ge-tools-drawer" />').prependTo(col);
2877
+ var drawer = prependDrawer(col, 'ge-tools-drawer');
2803
2878
 
2804
2879
  createMoveTool(drawer);
2805
2880
 
@@ -2830,18 +2905,11 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2830
2905
 
2831
2906
  addSettingsTool(drawer, col, settings.col_classes);
2832
2907
 
2833
- settings.col_tools.forEach(function(hostTool) {
2834
- createTool(drawer, hostTool.title || '', hostTool.className || '',
2835
- hostTool.iconClass || 'bi bi-wrench', hostTool.on);
2836
- });
2908
+ hostTools(drawer, settings.col_tools);
2837
2909
 
2838
2910
  createTool(drawer, t('tool.delete_column'), 'ge-delete-column', 'bi bi-trash', function() {
2839
2911
  deleteNode('column', col, t('confirm.delete_column'), function(removed) {
2840
- col.animate({
2841
- opacity: 'hide',
2842
- width: 'hide',
2843
- height: 'hide'
2844
- }, 400, removed);
2912
+ dom.shrinkAway(col, 400, removed, true);
2845
2913
  });
2846
2914
  });
2847
2915
 
@@ -2851,7 +2919,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2851
2919
  var row = createRow();
2852
2920
 
2853
2921
  addNode('row', row, function() {
2854
- col.append(row);
2922
+ col.appendChild(row);
2855
2923
  }, { parent: col, source: 'tool' });
2856
2924
  });
2857
2925
 
@@ -2892,14 +2960,14 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2892
2960
  var size = currentSize(col);
2893
2961
  if (isUnits(size)) { return size; }
2894
2962
 
2895
- var units = Math.round(col.outerWidth() / rowContentWidth(col.parent()) * MAX_COL_SIZE);
2963
+ var units = Math.round(dom.outerWidth(col) / rowContentWidth(col.parentElement) * MAX_COL_SIZE);
2896
2964
  return Math.min(Math.max(units, 1), MAX_COL_SIZE);
2897
2965
  }
2898
2966
 
2899
2967
  function rowContentWidth(row) {
2900
- var style = window.getComputedStyle(row[0]);
2968
+ var style = window.getComputedStyle(row);
2901
2969
 
2902
- return row[0].clientWidth - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight);
2970
+ return row.clientWidth - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight);
2903
2971
  }
2904
2972
 
2905
2973
  function currentOffset(col) {
@@ -2933,7 +3001,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2933
3001
  * its own indent. What "hold shift for max" means.
2934
3002
  */
2935
3003
  function widestFor(col) {
2936
- var room = spare(col.parent(), leadingTier(), col) - currentOffset(col);
3004
+ var room = spare(col.parentElement, leadingTier(), col) - currentOffset(col);
2937
3005
 
2938
3006
  return Math.min(largest(settings.valid_col_sizes), Math.max(room, 1));
2939
3007
  }
@@ -2943,7 +3011,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2943
3011
  * itself left inside the row.
2944
3012
  */
2945
3013
  function deepestFor(col) {
2946
- var room = spare(col.parent(), leadingTier(), col) - currentUnits(col);
3014
+ var room = spare(col.parentElement, leadingTier(), col) - currentUnits(col);
2947
3015
 
2948
3016
  return Math.min(largest(settings.valid_col_offsets), Math.max(room, 0));
2949
3017
  }
@@ -2953,29 +3021,34 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2953
3021
  * a tool that only says "drag from here" is one tool too many.
2954
3022
  */
2955
3023
  function createMoveTool(drawer) {
2956
- if (settings.drag_handle === 'drawer') { return; }
3024
+ if (settings.drag_handle === 'drawer') { return null; }
2957
3025
 
2958
- createTool(drawer, t('tool.move'), 'ge-move', 'bi bi-arrows-move');
3026
+ return createTool(drawer, t('tool.move'), 'ge-move', 'bi bi-arrows-move');
2959
3027
  }
2960
3028
 
3029
+ /**
3030
+ * A tool in a drawer: a link with an icon, and what it does. The
3031
+ * handlers are a click handler, or { eventName: handler }; each gets
3032
+ * the DOM event, with `this` the tool. Hands back the tool.
3033
+ */
2961
3034
  function createTool(drawer, title, className, iconClass, eventHandlers) {
2962
- // Through attr rather than spliced into markup: a title is text,
2963
- // and a quote in it - a type name, a host's own title - would
2964
- // end the attribute early
2965
- var tool = $('<a />')
2966
- .attr('title', title)
2967
- .attr('class', className)
2968
- .append($('<i />').attr('class', iconClass))
2969
- .appendTo(drawer)
2970
- ;
3035
+ // Through setAttribute rather than spliced into markup: a title is
3036
+ // text, and a quote in it - a type name, a host's own title -
3037
+ // would end the attribute early
3038
+ var tool = dom.element('a', { title: title, 'class': className });
3039
+ tool.appendChild(dom.element('i', { 'class': iconClass }));
3040
+ drawer.appendChild(tool);
3041
+
2971
3042
  if (typeof eventHandlers == 'function') {
2972
- tool.on('click', eventHandlers);
3043
+ tool.addEventListener('click', eventHandlers);
2973
3044
  }
2974
- if (typeof eventHandlers == 'object') {
2975
- $.each(eventHandlers, function(name, func) {
2976
- tool.on(name, func);
3045
+ if (eventHandlers && typeof eventHandlers == 'object') {
3046
+ Object.keys(eventHandlers).forEach(function(name) {
3047
+ dom.on(tool, name, eventHandlers[name]);
2977
3048
  });
2978
3049
  }
3050
+
3051
+ return tool;
2979
3052
  }
2980
3053
 
2981
3054
  /**
@@ -2984,7 +3057,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2984
3057
  * and the only ones it is allowed to take away.
2985
3058
  */
2986
3059
  function hostClasses(node) {
2987
- return (node.attr('class') || '').split(/\s+/).filter(function(name) {
3060
+ return (node.getAttribute('class') || '').split(/\s+/).filter(function(name) {
2988
3061
  return name !== '' && !isEditorClass(name);
2989
3062
  });
2990
3063
  }
@@ -3000,13 +3073,13 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3000
3073
  }
3001
3074
 
3002
3075
  function setHostClasses(node, value) {
3003
- hostClasses(node).forEach(function(name) { node.removeClass(name); });
3076
+ hostClasses(node).forEach(function(name) { dom.removeClass(node, name); });
3004
3077
 
3005
3078
  value.split(/\s+/).forEach(function(name) {
3006
- if (name !== '') { node.addClass(name); }
3079
+ if (name !== '') { dom.addClass(node, name); }
3007
3080
  });
3008
3081
 
3009
- if (!node.attr('class')) { node.removeAttr('class'); }
3082
+ dom.dropEmptyClass(node);
3010
3083
  }
3011
3084
 
3012
3085
  /**
@@ -3015,10 +3088,10 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3015
3088
  */
3016
3089
  function addSettingsTool(drawer, node, presets) {
3017
3090
  var details = createDetails(node, presets || []);
3018
- node.data('ge-details', details);
3091
+ detailsFor.set(node, details);
3019
3092
 
3020
3093
  createTool(drawer, t('tool.settings'), 'ge-settings', 'bi bi-gear-fill', function() {
3021
- toggleSettings(node, details, $(this));
3094
+ toggleSettings(node, details, this);
3022
3095
  });
3023
3096
 
3024
3097
  // Beside the gear, because every node that has one is a node a
@@ -3026,71 +3099,79 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3026
3099
  // feature plugin's tools go there too, for the same reason: the
3027
3100
  // gear is the one tool every drawer has.
3028
3101
  var kind = kindOf(node);
3029
- $.each(UTILITIES, function(name, utility) {
3030
- if (utility.drawerTools) { utility.drawerTools(drawer, node, kind); }
3102
+ Object.keys(UTILITIES).forEach(function(name) {
3103
+ if (UTILITIES[name].drawerTools) { UTILITIES[name].drawerTools(drawer, node, kind); }
3031
3104
  });
3032
- $.each(FEATURES, function(name, feature) {
3033
- if (feature.drawerTools) { feature.drawerTools(drawer, node, kind); }
3105
+ Object.keys(FEATURES).forEach(function(name) {
3106
+ if (FEATURES[name].drawerTools) { FEATURES[name].drawerTools(drawer, node, kind); }
3034
3107
  });
3035
3108
 
3036
- return details.appendTo(drawer);
3109
+ return drawer.appendChild(details);
3037
3110
  }
3038
3111
 
3039
3112
  function createDetails(container, cssClasses) {
3040
- var detailsDiv = $('<div class="ge-details" />');
3041
- var general = $('<div class="ge-details-general" />').appendTo(detailsDiv);
3113
+ var detailsDiv = dom.element('div', { 'class': 'ge-details' });
3114
+ var general = detailsDiv.appendChild(dom.element('div', { 'class': 'ge-details-general' }));
3042
3115
  var field = function(label) {
3043
- return $('<label class="ge-field" />')
3044
- .append($('<span class="ge-field-label" />').text(label))
3045
- .appendTo(general);
3116
+ var holder = dom.element('label', { 'class': 'ge-field' });
3117
+ holder.appendChild(dom.element('span', { 'class': 'ge-field-label' }, label));
3118
+ return general.appendChild(holder);
3046
3119
  };
3047
3120
 
3048
- $('<input class="ge-id form-control form-control-sm" />')
3049
- .attr('placeholder', t('tool.id_placeholder'))
3050
- .val(container.attr('id'))
3051
- .attr('title', t('tool.id_title'))
3052
- .appendTo(field(t('panel.id')))
3053
- .on('change', function() {
3054
- // An empty field means no id, not an empty one
3055
- if (this.value === '') {
3056
- container.removeAttr('id');
3057
- } else {
3058
- container.attr('id', this.value);
3059
- }
3060
- })
3061
- ;
3062
-
3063
- $('<input class="ge-classes form-control form-control-sm" />')
3064
- .attr('placeholder', t('tool.classes_placeholder'))
3065
- .attr('title', t('tool.classes_title'))
3066
- .val(hostClasses(container).join(' '))
3067
- .appendTo(field(t('panel.classes')))
3068
- .on('change', function() {
3069
- setHostClasses(container, this.value);
3070
- refreshUtilities(container);
3071
- })
3072
- ;
3121
+ var id = dom.element('input', {
3122
+ 'class': 'ge-id form-control form-control-sm',
3123
+ placeholder: t('tool.id_placeholder'),
3124
+ title: t('tool.id_title'),
3125
+ });
3126
+ id.value = container.getAttribute('id') || '';
3127
+ field(t('panel.id')).appendChild(id);
3128
+ id.addEventListener('change', function() {
3129
+ // An empty field means no id, not an empty one
3130
+ if (this.value === '') {
3131
+ container.removeAttribute('id');
3132
+ } else {
3133
+ container.setAttribute('id', this.value);
3134
+ }
3135
+ });
3136
+
3137
+ var classes = dom.element('input', {
3138
+ 'class': 'ge-classes form-control form-control-sm',
3139
+ placeholder: t('tool.classes_placeholder'),
3140
+ title: t('tool.classes_title'),
3141
+ });
3142
+ classes.value = hostClasses(container).join(' ');
3143
+ field(t('panel.classes')).appendChild(classes);
3144
+ classes.addEventListener('change', function() {
3145
+ setHostClasses(container, this.value);
3146
+ refreshUtilities(container);
3147
+ });
3073
3148
 
3074
3149
  // Bootstrap 5's outline buttons, filled while their class is on;
3075
3150
  // up to 5.x they carried Bootstrap 3's btn-default, which 5 lacks
3076
- var classGroup = $('<div class="btn-group btn-group-sm ge-presets" role="group" />').appendTo(general);
3151
+ var classGroup = general.appendChild(dom.element('div', { 'class': 'btn-group btn-group-sm ge-presets', role: 'group' }));
3077
3152
  cssClasses.forEach(function(rowClass) {
3078
- var btn = $('<a role="button" class="btn btn-sm btn-outline-secondary" />')
3079
- .html(rowClass.label)
3080
- .attr('title', rowClass.title ? rowClass.title : t('tool.toggle_class', { label: rowClass.label }))
3081
- .toggleClass('active', container.hasClass(rowClass.cssClass))
3082
- .attr('aria-pressed', container.hasClass(rowClass.cssClass) ? 'true' : 'false')
3083
- .on('click', function() {
3084
- btn.toggleClass('active').attr('aria-pressed', btn.hasClass('active') ? 'true' : 'false');
3085
- container.toggleClass(rowClass.cssClass, btn.hasClass('active'));
3086
- refreshUtilities(container);
3087
- })
3088
- .appendTo(classGroup)
3089
- ;
3153
+ var on = dom.hasClass(container, rowClass.cssClass);
3154
+ var btn = dom.element('a', {
3155
+ role: 'button',
3156
+ 'class': 'btn btn-sm btn-outline-secondary',
3157
+ title: rowClass.title ? rowClass.title : t('tool.toggle_class', { label: rowClass.label }),
3158
+ 'aria-pressed': on ? 'true' : 'false',
3159
+ });
3160
+
3161
+ // A preset's label is the host's markup, as it was in 6.x
3162
+ btn.innerHTML = rowClass.label;
3163
+ dom.toggleClass(btn, 'active', on);
3164
+ btn.addEventListener('click', function() {
3165
+ dom.toggleClass(btn, 'active');
3166
+ btn.setAttribute('aria-pressed', dom.hasClass(btn, 'active') ? 'true' : 'false');
3167
+ dom.toggleClass(container, rowClass.cssClass, dom.hasClass(btn, 'active'));
3168
+ refreshUtilities(container);
3169
+ });
3170
+ classGroup.appendChild(btn);
3090
3171
  });
3091
3172
 
3092
3173
  var utilities = createUtilitiesSection(container);
3093
- if (utilities) { utilities.appendTo(detailsDiv); }
3174
+ if (utilities) { detailsDiv.appendChild(utilities); }
3094
3175
 
3095
3176
  return detailsDiv;
3096
3177
  }
@@ -3113,6 +3194,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3113
3194
  var PANEL_MODES = ['offcanvas', 'popover', 'modal', 'inline'];
3114
3195
  var settingsPanels = {}; // One of each, built on first use, outside the canvas
3115
3196
  var openSettingsState = null; // What is open: { mode, node, details, home, next, gear }
3197
+ var modalState = new WeakMap(); // modal panel -> { watched, wanted, busy }
3116
3198
 
3117
3199
  function panelMode() {
3118
3200
  if (PANEL_MODES.indexOf(settings.settings_panel) !== -1) { return settings.settings_panel; }
@@ -3122,17 +3204,16 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3122
3204
  return 'offcanvas';
3123
3205
  }
3124
3206
 
3125
- /** A node's panel, wherever it is at the moment. */
3207
+ /** A node's panel, wherever it is at the moment, or null. */
3126
3208
  function detailsOf(node) {
3127
- var details = node.data('ge-details');
3128
- return details && details.length ? details : $();
3209
+ return (node && detailsFor.get(node)) || null;
3129
3210
  }
3130
3211
 
3131
3212
  /** Where settings fields are found: the canvas, and the panel open outside it. */
3132
3213
  function settingsScope() {
3133
3214
  return openSettingsState && openSettingsState.mode !== 'inline'
3134
- ? canvas.add(openSettingsState.details)
3135
- : canvas;
3215
+ ? [canvas, openSettingsState.details]
3216
+ : [canvas];
3136
3217
  }
3137
3218
 
3138
3219
  /** The gear: its node's panel, opened or closed; another node's closes first. */
@@ -3140,57 +3221,61 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3140
3221
  var mode = panelMode();
3141
3222
 
3142
3223
  if (mode === 'inline') {
3143
- details.toggle();
3224
+ dom.toggle(details);
3144
3225
  return;
3145
3226
  }
3146
3227
 
3147
- var same = openSettingsState && openSettingsState.details[0] === details[0];
3228
+ var same = openSettingsState && openSettingsState.details === details;
3148
3229
  closeSettings();
3149
3230
  if (!same) { openSettings(mode, node, details, gear); }
3150
3231
  }
3151
3232
 
3152
3233
  function openSettings(mode, node, details, gear) {
3153
3234
  var panel = settingsPanel(mode);
3235
+ var listening = new AbortController();
3154
3236
 
3155
3237
  openSettingsState = {
3156
3238
  mode: mode,
3157
3239
  node: node,
3158
3240
  details: details,
3159
- home: details.parent(),
3160
- next: details.next(),
3241
+ home: details.parentElement,
3242
+ next: details.nextElementSibling,
3161
3243
  gear: gear,
3244
+ listening: listening,
3162
3245
  };
3163
3246
 
3164
- panel.find('.ge-settings-title').text(t('panel.title', { kind: kindLabel(node) }));
3165
- details.appendTo(panel.find('.ge-settings-body')).show();
3166
- node.addClass('ge-settings-target');
3247
+ dom.one(panel, '.ge-settings-title').textContent = t('panel.title', { kind: kindLabel(node) });
3248
+ dom.one(panel, '.ge-settings-body').appendChild(details);
3249
+ dom.show(details);
3250
+ dom.addClass(node, 'ge-settings-target');
3167
3251
 
3168
- var ns = '.ge-settings-' + instanceId;
3169
- $(document).on('keydown' + ns, function(e) {
3252
+ var signal = { signal: listening.signal };
3253
+ document.addEventListener('keydown', function(e) {
3170
3254
  if (e.key === 'Escape') { closeSettings(); }
3171
- });
3255
+ }, signal);
3172
3256
 
3173
3257
  if (mode === 'offcanvas') {
3174
3258
  // From the bottom on a phone, from the side anywhere wider
3175
3259
  var narrow = window.innerWidth < 576;
3176
- panel.toggleClass('offcanvas-end', !narrow).toggleClass('offcanvas-bottom', narrow);
3177
- panel.removeClass('hiding');
3178
- panel[0].getBoundingClientRect(); // So the slide in is a transition, not a jump
3179
- panel.addClass('show');
3260
+ dom.toggleClass(panel, 'offcanvas-end', !narrow);
3261
+ dom.toggleClass(panel, 'offcanvas-bottom', narrow);
3262
+ dom.removeClass(panel, 'hiding');
3263
+ panel.getBoundingClientRect(); // So the slide in is a transition, not a jump
3264
+ dom.addClass(panel, 'show');
3180
3265
  } else if (mode === 'popover') {
3181
3266
  placePopover(panel, gear);
3182
- $(window).on('scroll' + ns + ' resize' + ns, function() { placePopover(panel, gear); });
3267
+ dom.on(window, 'scroll resize', function() { placePopover(panel, gear); }, signal);
3183
3268
 
3184
3269
  // And again whenever what is in it changes size: the
3185
3270
  // Responsive section unfolding, a plugin swapping a field
3186
3271
  if (window.ResizeObserver) {
3187
3272
  openSettingsState.observer = new window.ResizeObserver(function() { placePopover(panel, gear); });
3188
- openSettingsState.observer.observe(details[0]);
3273
+ openSettingsState.observer.observe(details);
3189
3274
  }
3190
3275
  // A press anywhere else puts it away, as a popover does
3191
- $(document).on('mousedown' + ns + ' touchstart' + ns, function(e) {
3192
- if (!$(e.target).closest(panel).length && !$(e.target).closest(gear).length) { closeSettings(); }
3193
- });
3276
+ dom.on(document, 'mousedown touchstart', function(e) {
3277
+ if (!panel.contains(e.target) && !gear.contains(e.target)) { closeSettings(); }
3278
+ }, signal);
3194
3279
  } else {
3195
3280
  showModal(panel);
3196
3281
  }
@@ -3203,29 +3288,30 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3203
3288
  openSettingsState = null;
3204
3289
 
3205
3290
  var panel = settingsPanels[open.mode];
3206
- $(document).off('.ge-settings-' + instanceId);
3207
- $(window).off('.ge-settings-' + instanceId);
3291
+ open.listening.abort();
3208
3292
  if (open.observer) { open.observer.disconnect(); }
3209
3293
 
3210
3294
  if (open.mode === 'offcanvas') {
3211
- panel.removeClass('show').addClass('hiding');
3212
- window.setTimeout(function() { panel.removeClass('hiding'); }, 300);
3295
+ dom.removeClass(panel, 'show');
3296
+ dom.addClass(panel, 'hiding');
3297
+ window.setTimeout(function() { dom.removeClass(panel, 'hiding'); }, 300);
3213
3298
  } else if (open.mode === 'popover') {
3214
- panel.hide();
3299
+ dom.hide(panel);
3215
3300
  } else {
3216
3301
  hideModal(panel);
3217
3302
  }
3218
3303
 
3219
- open.node.removeClass('ge-settings-target');
3220
- open.details.css('display', '');
3304
+ dom.removeClass(open.node, 'ge-settings-target');
3305
+ open.details.style.removeProperty('display');
3306
+ dom.dropEmptyStyle(open.details);
3221
3307
 
3222
3308
  // Where it was in its drawer, if the drawer is still there: a
3223
3309
  // node deleted while its settings were open took it with it
3224
- if (open.home.length && $.contains(document.documentElement, open.home[0])) {
3225
- if (open.next.length && open.next.parent()[0] === open.home[0]) {
3226
- open.details.insertBefore(open.next);
3310
+ if (open.home && dom.attached(open.home)) {
3311
+ if (open.next && open.next.parentElement === open.home) {
3312
+ open.home.insertBefore(open.details, open.next);
3227
3313
  } else {
3228
- open.details.appendTo(open.home);
3314
+ open.home.appendChild(open.details);
3229
3315
  }
3230
3316
  } else {
3231
3317
  open.details.remove();
@@ -3236,58 +3322,64 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3236
3322
  function settingsPanel(mode) {
3237
3323
  if (settingsPanels[mode]) { return settingsPanels[mode]; }
3238
3324
 
3239
- var closeButton = function(extra) {
3240
- return $('<button type="button" class="btn-close ge-settings-close" />')
3241
- .addClass(extra || '')
3242
- .attr('aria-label', t('panel.close'));
3325
+ var closeButton = function() {
3326
+ return dom.element('button', {
3327
+ type: 'button',
3328
+ 'class': 'btn-close ge-settings-close',
3329
+ 'aria-label': t('panel.close'),
3330
+ });
3243
3331
  };
3244
3332
  var panel;
3245
3333
 
3246
3334
  if (mode === 'offcanvas') {
3247
- panel = $('<div class="offcanvas offcanvas-end ge-settings-panel ge-settings-offcanvas" tabindex="-1" role="dialog" />')
3248
- .append($('<div class="offcanvas-header" />')
3249
- .append('<h5 class="offcanvas-title ge-settings-title"></h5>')
3250
- .append(closeButton()))
3251
- .append('<div class="offcanvas-body ge-settings-body"></div>');
3335
+ panel = dom.create('<div class="offcanvas offcanvas-end ge-settings-panel ge-settings-offcanvas" tabindex="-1" role="dialog">' +
3336
+ '<div class="offcanvas-header"><h5 class="offcanvas-title ge-settings-title"></h5></div>' +
3337
+ '<div class="offcanvas-body ge-settings-body"></div>' +
3338
+ '</div>');
3339
+ dom.one(panel, '.offcanvas-header').appendChild(closeButton());
3252
3340
  } else if (mode === 'popover') {
3253
- panel = $('<div class="popover bs-popover-bottom ge-settings-panel ge-settings-popover" role="dialog" />')
3254
- .append('<div class="popover-arrow"></div>')
3255
- .append($('<div class="popover-header" />')
3256
- .append('<span class="ge-settings-title"></span>')
3257
- .append(closeButton()))
3258
- .append('<div class="popover-body ge-settings-body"></div>')
3259
- .hide();
3341
+ panel = dom.create('<div class="popover bs-popover-bottom ge-settings-panel ge-settings-popover" role="dialog">' +
3342
+ '<div class="popover-arrow"></div>' +
3343
+ '<div class="popover-header"><span class="ge-settings-title"></span></div>' +
3344
+ '<div class="popover-body ge-settings-body"></div>' +
3345
+ '</div>');
3346
+ dom.one(panel, '.popover-header').appendChild(closeButton());
3347
+ dom.hide(panel);
3260
3348
  } else {
3261
- panel = $('<div class="modal fade ge-settings-panel ge-settings-modal" tabindex="-1" role="dialog" aria-hidden="true" />')
3262
- .append($('<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" />')
3263
- .append($('<div class="modal-content" />')
3264
- .append($('<div class="modal-header" />')
3265
- .append('<h5 class="modal-title ge-settings-title"></h5>')
3266
- .append(closeButton()))
3267
- .append('<div class="modal-body ge-settings-body"></div>')
3268
- .append($('<div class="modal-footer" />')
3269
- .append($('<button type="button" class="btn btn-primary ge-settings-close" />')
3270
- .text(t('panel.done'))))));
3349
+ panel = dom.create('<div class="modal fade ge-settings-panel ge-settings-modal" tabindex="-1" role="dialog" aria-hidden="true">' +
3350
+ '<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable">' +
3351
+ '<div class="modal-content">' +
3352
+ '<div class="modal-header"><h5 class="modal-title ge-settings-title"></h5></div>' +
3353
+ '<div class="modal-body ge-settings-body"></div>' +
3354
+ '<div class="modal-footer"></div>' +
3355
+ '</div>' +
3356
+ '</div>' +
3357
+ '</div>');
3358
+ dom.one(panel, '.modal-header').appendChild(closeButton());
3359
+ dom.one(panel, '.modal-footer').appendChild(dom.element('button', {
3360
+ type: 'button',
3361
+ 'class': 'btn btn-primary ge-settings-close',
3362
+ }, t('panel.done')));
3271
3363
  }
3272
3364
 
3273
- panel.on('click', '.ge-settings-close', function(e) {
3365
+ dom.delegate(panel, 'click', '.ge-settings-close', function(e) {
3274
3366
  e.preventDefault();
3275
3367
  closeSettings();
3276
3368
  });
3277
3369
 
3278
- settingsPanels[mode] = panel.appendTo('body');
3370
+ settingsPanels[mode] = document.body.appendChild(panel);
3279
3371
  return panel;
3280
3372
  }
3281
3373
 
3282
3374
  function removeSettingsPanels() {
3283
3375
  closeSettings();
3284
3376
 
3285
- $.each(settingsPanels, function(mode, panel) {
3286
- if (mode === 'modal' && window.bootstrap && window.bootstrap.Modal) {
3287
- var instance = window.bootstrap.Modal.getInstance(panel[0]);
3288
- if (instance) { instance.dispose(); }
3377
+ Object.keys(settingsPanels).forEach(function(mode) {
3378
+ if (mode === 'modal') {
3379
+ retireModal(settingsPanels[mode]);
3380
+ } else {
3381
+ settingsPanels[mode].remove();
3289
3382
  }
3290
- panel.remove();
3291
3383
  });
3292
3384
  if (settingsBackdrop) { settingsBackdrop.remove(); settingsBackdrop = null; }
3293
3385
  settingsPanels = {};
@@ -3299,37 +3391,37 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3299
3391
  * scrolls.
3300
3392
  */
3301
3393
  function placePopover(panel, gear) {
3302
- if (!$.contains(document.documentElement, gear[0])) { return; }
3394
+ if (!dom.attached(gear)) { return; }
3303
3395
 
3304
- panel.show();
3396
+ dom.show(panel);
3305
3397
 
3306
- var tool = gear[0].getBoundingClientRect();
3398
+ var tool = gear.getBoundingClientRect();
3307
3399
  var gap = 8;
3308
- var body = panel.children('.popover-body').css('max-height', '');
3309
- var width = panel.outerWidth();
3310
- var height = panel.outerHeight();
3400
+ var body = dom.child(panel, '.popover-body');
3401
+ body.style.removeProperty('max-height');
3402
+ var width = dom.outerWidth(panel);
3403
+ var height = dom.outerHeight(panel);
3311
3404
  var roomBelow = window.innerHeight - tool.bottom - 2 * gap;
3312
3405
  var roomAbove = tool.top - 2 * gap;
3313
3406
  var below = height <= roomBelow || roomBelow >= roomAbove;
3314
3407
  var room = below ? roomBelow : roomAbove;
3315
3408
 
3316
3409
  if (height > room) {
3317
- body.css('max-height', Math.max(120, room - (height - body.outerHeight())));
3318
- height = panel.outerHeight();
3410
+ dom.css(body, { 'max-height': Math.max(120, room - (height - dom.outerHeight(body))) });
3411
+ height = dom.outerHeight(panel);
3319
3412
  }
3320
3413
 
3321
3414
  var top = below ? tool.bottom + gap : tool.top - gap - height;
3322
3415
  var left = Math.max(gap, Math.min(tool.left + tool.width / 2 - 24, window.innerWidth - width - gap));
3323
3416
 
3324
- panel
3325
- .toggleClass('bs-popover-bottom', below)
3326
- .toggleClass('bs-popover-top', !below)
3327
- .css({ top: top + window.pageYOffset, left: left + window.pageXOffset })
3328
- ;
3417
+ dom.toggleClass(panel, 'bs-popover-bottom', below);
3418
+ dom.toggleClass(panel, 'bs-popover-top', !below);
3419
+ dom.css(panel, { top: top + window.pageYOffset, left: left + window.pageXOffset });
3329
3420
 
3330
3421
  // The arrow points at the gear, wherever the popover had to go
3331
- panel.children('.popover-arrow').css('left',
3332
- Math.max(gap, Math.min(tool.left + tool.width / 2 - left - 8, width - 24)));
3422
+ dom.css(dom.child(panel, '.popover-arrow'), {
3423
+ left: Math.max(gap, Math.min(tool.left + tool.width / 2 - left - 8, width - 24)),
3424
+ });
3333
3425
  }
3334
3426
 
3335
3427
  var settingsBackdrop = null; // The modal's backdrop, when Bootstrap's javascript is not there to make one
@@ -3343,32 +3435,40 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3343
3435
  * closes the settings as the close button does.
3344
3436
  */
3345
3437
  function showModal(panel) {
3346
- if (!window.bootstrap || !window.bootstrap.Modal) {
3347
- settingsBackdrop = $('<div class="modal-backdrop fade show ge-settings-backdrop" />')
3348
- .appendTo('body')
3349
- .on('click', function() { closeSettings(); });
3350
- panel.addClass('show').css('display', 'block').removeAttr('aria-hidden');
3438
+ var Modal = modalLibrary();
3439
+
3440
+ 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);
3444
+ dom.addClass(panel, 'show');
3445
+ panel.style.display = 'block';
3446
+ panel.removeAttribute('aria-hidden');
3351
3447
  return;
3352
3448
  }
3353
3449
 
3354
- var modal = window.bootstrap.Modal.getOrCreateInstance(panel[0]);
3450
+ var modal = Modal.getOrCreateInstance(panel);
3451
+ var state = modalState.get(panel);
3355
3452
 
3356
- if (!panel.data('ge-watched')) {
3357
- panel.data('ge-watched', true);
3358
- panel.on('hide.bs.modal', function() {
3453
+ if (!state) {
3454
+ state = { wanted: null, busy: false };
3455
+ modalState.set(panel, state);
3456
+ trackModal(panel);
3457
+
3458
+ panel.addEventListener('hide.bs.modal', function() {
3359
3459
  // Not asked for: Escape or the backdrop
3360
- if (panel.data('ge-wanted') === 'open') {
3361
- panel.data('ge-wanted', 'closed');
3362
- panel.data('ge-busy', true);
3460
+ if (state.wanted === 'open') {
3461
+ state.wanted = 'closed';
3462
+ state.busy = true;
3363
3463
  }
3364
3464
  });
3365
- panel.on('shown.bs.modal', function() {
3366
- panel.data('ge-busy', false);
3367
- if (panel.data('ge-wanted') === 'closed') { hideModal(panel); }
3465
+ panel.addEventListener('shown.bs.modal', function() {
3466
+ state.busy = false;
3467
+ if (state.wanted === 'closed') { hideModal(panel); }
3368
3468
  });
3369
- panel.on('hidden.bs.modal', function() {
3370
- panel.data('ge-busy', false);
3371
- if (panel.data('ge-wanted') === 'open') {
3469
+ panel.addEventListener('hidden.bs.modal', function() {
3470
+ state.busy = false;
3471
+ if (state.wanted === 'open') {
3372
3472
  showModal(panel);
3373
3473
  } else if (openSettingsState && openSettingsState.mode === 'modal') {
3374
3474
  closeSettings();
@@ -3376,28 +3476,33 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3376
3476
  });
3377
3477
  }
3378
3478
 
3379
- panel.data('ge-wanted', 'open');
3380
- if (!panel.data('ge-busy')) {
3381
- panel.data('ge-busy', true);
3479
+ state.wanted = 'open';
3480
+ if (!state.busy) {
3481
+ state.busy = true;
3382
3482
  modal.show();
3383
3483
  }
3384
3484
  }
3385
3485
 
3386
3486
  function hideModal(panel) {
3387
- if (!window.bootstrap || !window.bootstrap.Modal) {
3487
+ var Modal = modalLibrary();
3488
+
3489
+ if (!Modal) {
3388
3490
  if (settingsBackdrop) { settingsBackdrop.remove(); settingsBackdrop = null; }
3389
- panel.removeClass('show').css('display', '').attr('aria-hidden', 'true');
3491
+ dom.removeClass(panel, 'show');
3492
+ panel.style.removeProperty('display');
3493
+ panel.setAttribute('aria-hidden', 'true');
3390
3494
  return;
3391
3495
  }
3392
3496
 
3393
- panel.data('ge-wanted', 'closed');
3497
+ var state = modalState.get(panel) || { wanted: null, busy: false };
3498
+ state.wanted = 'closed';
3394
3499
 
3395
3500
  // Already out of sight - Bootstrap hid it itself, and this is its
3396
3501
  // hidden event closing the settings - and nothing is under way:
3397
3502
  // a hide now would be ignored, and no event would come to say so
3398
- if (!panel.data('ge-busy') && panel.hasClass('show')) {
3399
- panel.data('ge-busy', true);
3400
- window.bootstrap.Modal.getOrCreateInstance(panel[0]).hide();
3503
+ if (!state.busy && dom.hasClass(panel, 'show')) {
3504
+ state.busy = true;
3505
+ Modal.getOrCreateInstance(panel).hide();
3401
3506
  }
3402
3507
  }
3403
3508
 
@@ -3430,11 +3535,11 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3430
3535
  * column that had none.
3431
3536
  */
3432
3537
  function addAllColClasses() {
3433
- canvas.find('.column, div[class*="col-"], div.col').each(function() {
3434
- var col = $(this).addClass('column');
3538
+ dom.all(canvas, '.column, div[class*="col-"], div.col').forEach(function(col) {
3539
+ dom.addClass(col, 'column');
3435
3540
 
3436
3541
  // A column in a row with row-cols is sized by its row
3437
- if (sizedTiers(col).length || hasRowCols(col.parent())) { return; }
3542
+ if (sizedTiers(col).length || hasRowCols(col.parentElement)) { return; }
3438
3543
 
3439
3544
  setSize(col, BREAKPOINTS[0], MAX_COL_SIZE);
3440
3545
  });
@@ -3452,18 +3557,18 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3452
3557
  * -------------------------------------------------------------- */
3453
3558
 
3454
3559
  function hasRowCols(row) {
3455
- return row.hasClass('row') && BREAKPOINTS.some(function(tier) {
3456
- return ROW_COLS_VALUES.some(function(value) { return row.hasClass(rowColsClass(tier, value)); });
3560
+ return dom.hasClass(row, 'row') && BREAKPOINTS.some(function(tier) {
3561
+ return ROW_COLS_VALUES.some(function(value) { return dom.hasClass(row, rowColsClass(tier, value)); });
3457
3562
  });
3458
3563
  }
3459
3564
 
3460
3565
  /** The row-cols class that applies to a row at a tier, as { tier, value }, or null. */
3461
3566
  function rowColsSource(row, tier) {
3462
- if (!row.hasClass('row')) { return null; }
3567
+ if (!dom.hasClass(row, 'row')) { return null; }
3463
3568
 
3464
3569
  for (var i = BREAKPOINTS.indexOf(tier); i >= 0; i--) {
3465
3570
  for (var v = 0; v < ROW_COLS_VALUES.length; v++) {
3466
- if (row.hasClass(rowColsClass(BREAKPOINTS[i], ROW_COLS_VALUES[v]))) {
3571
+ if (dom.hasClass(row, rowColsClass(BREAKPOINTS[i], ROW_COLS_VALUES[v]))) {
3467
3572
  return { tier: i, value: ROW_COLS_VALUES[v] };
3468
3573
  }
3469
3574
  }
@@ -3487,7 +3592,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3487
3592
  * the column's own size does.
3488
3593
  */
3489
3594
  function rowColsWinner(col, tier) {
3490
- var row = rowColsSource(col.parent(), tier);
3595
+ var row = rowColsSource(col.parentElement, tier);
3491
3596
  if (!row) { return null; }
3492
3597
 
3493
3598
  var own = columnSource(col, tier);
@@ -3520,7 +3625,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3520
3625
 
3521
3626
  // A wider breakpoint's row-cols is live in a wide window; a
3522
3627
  // column with no size of its own here is a full width one
3523
- if (hasRowCols(col.parent()) && !columnSource(col, tier)) {
3628
+ if (hasRowCols(col.parentElement) && !columnSource(col, tier)) {
3524
3629
  return { flex: '0 0 auto', width: '100%', 'max-width': '100%' };
3525
3630
  }
3526
3631
 
@@ -3532,16 +3637,15 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3532
3637
  * and in the all view by its row-cols with no breakpoint.
3533
3638
  */
3534
3639
  function markRowCols(scope) {
3535
- scope.find('.row').addBack('.row').each(function() {
3536
- var row = $(this);
3640
+ dom.selfAndAll(scope, '.row').forEach(function(row) {
3537
3641
  var source = settings.row_cols === false ? null : (curView === ALL_VIEW
3538
3642
  ? (rowColsSource(row, BREAKPOINTS[0]) || null)
3539
3643
  : rowColsSource(row, breakpoint(curView)));
3540
3644
 
3541
3645
  if (!source) {
3542
- row.removeAttr('data-ge-row-cols');
3646
+ row.removeAttribute('data-ge-row-cols');
3543
3647
  } else {
3544
- row.attr('data-ge-row-cols', rowColsLabel(source.value));
3648
+ row.setAttribute('data-ge-row-cols', rowColsLabel(source.value));
3545
3649
  }
3546
3650
  });
3547
3651
  }
@@ -3578,8 +3682,8 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3578
3682
  var units = readUnits(col, tier.colPrefix);
3579
3683
  if (units !== null) { return units; }
3580
3684
 
3581
- if (col.hasClass(sizeClass(tier, 'auto'))) { return 'auto'; }
3582
- if (col.hasClass(sizeClass(tier, 'equal'))) { return 'equal'; }
3685
+ if (dom.hasClass(col, sizeClass(tier, 'auto'))) { return 'auto'; }
3686
+ if (dom.hasClass(col, sizeClass(tier, 'equal'))) { return 'equal'; }
3583
3687
 
3584
3688
  return null;
3585
3689
  }
@@ -3617,26 +3721,26 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3617
3721
  }
3618
3722
 
3619
3723
  function readUnits(col, prefix) {
3620
- var match = new RegExp('(?:^|\\s)' + prefix + '(\\d+)(?:\\s|$)').exec(col.attr('class') || '');
3724
+ var match = new RegExp('(?:^|\\s)' + prefix + '(\\d+)(?:\\s|$)').exec(col.getAttribute('class') || '');
3621
3725
  return match ? parseInt(match[1], 10) : null;
3622
3726
  }
3623
3727
 
3624
3728
  function writeUnits(col, prefix, units) {
3625
- var classes = (col.attr('class') || '').split(/\s+/).filter(function(name) {
3729
+ var classes = (col.getAttribute('class') || '').split(/\s+/).filter(function(name) {
3626
3730
  return name !== '' && !new RegExp('^' + prefix + '\\d+$').test(name);
3627
3731
  });
3628
3732
 
3629
3733
  if (units !== null) { classes.push(prefix + units); }
3630
3734
 
3631
- col.attr('class', classes.join(' '));
3735
+ col.setAttribute('class', classes.join(' '));
3632
3736
  }
3633
3737
 
3634
3738
  /** One size per tier: writing a size takes whatever size the tier had off. */
3635
3739
  function setSize(col, tier, size) {
3636
3740
  writeUnits(col, tier.colPrefix, null);
3637
- FLEX_SIZES.forEach(function(flex) { col.removeClass(sizeClass(tier, flex)); });
3741
+ FLEX_SIZES.forEach(function(flex) { dom.removeClass(col, sizeClass(tier, flex)); });
3638
3742
 
3639
- if (size !== null && size !== undefined) { col.addClass(sizeClass(tier, size)); }
3743
+ if (size !== null && size !== undefined) { dom.addClass(col, sizeClass(tier, size)); }
3640
3744
  }
3641
3745
 
3642
3746
  /** An offset of 0 is written as no class at all, which is what it means. */
@@ -3658,9 +3762,8 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3658
3762
  function spare(row, tier, ignore) {
3659
3763
  var used = 0;
3660
3764
 
3661
- row.children('.column').each(function() {
3662
- var sibling = $(this);
3663
- if (ignore && sibling[0] === ignore[0]) { return; }
3765
+ dom.children(row, '.column').forEach(function(sibling) {
3766
+ if (ignore && sibling === ignore) { return; }
3664
3767
 
3665
3768
  // An equal or auto column takes what is left, so it uses none;
3666
3769
  // one its row sizes uses its share of the line
@@ -3709,14 +3812,12 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3709
3812
  * been written.
3710
3813
  */
3711
3814
  function stripPixelWidths(scope) {
3712
- scope.find('.column').addBack('.column').each(function() {
3713
- var col = $(this);
3714
-
3715
- col.css({ width: '', height: '', left: '', top: '' });
3815
+ dom.selfAndAll(scope, '.column').forEach(function(col) {
3816
+ dom.css(col, { width: '', height: '', left: '', top: '' });
3716
3817
 
3717
3818
  // Clearing the last property leaves style="" behind, which is
3718
3819
  // an editor leftover like any other
3719
- if (!col.attr('style')) { col.removeAttr('style'); }
3820
+ dom.dropEmptyStyle(col);
3720
3821
  });
3721
3822
  }
3722
3823
 
@@ -3744,7 +3845,9 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3744
3845
 
3745
3846
  /** A selector every feature plugin may contribute to, under one name. */
3746
3847
  function pluginHooks(name) {
3747
- return $.map(FEATURES, function(feature) { return feature[name] || null; });
3848
+ return Object.keys(FEATURES).map(function(key) {
3849
+ return FEATURES[key][name] || null;
3850
+ }).filter(function(hook) { return hook !== null; });
3748
3851
  }
3749
3852
 
3750
3853
  /**
@@ -3755,7 +3858,8 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3755
3858
  function acceptsBlock(region, node) {
3756
3859
  var accepted = true;
3757
3860
 
3758
- $.each(FEATURES, function(name, feature) {
3861
+ Object.keys(FEATURES).forEach(function(name) {
3862
+ var feature = FEATURES[name];
3759
3863
  if (accepted && feature.accepts && feature.accepts(region, node) === false) { accepted = false; }
3760
3864
  });
3761
3865
 
@@ -3786,17 +3890,21 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3786
3890
  * function's business. That is the whole point of it: the toolkit
3787
3891
  * underneath is named in one place, so replacing it is one function
3788
3892
  * and not thirty call sites.
3893
+ *
3894
+ * `lists` is an element or an array of them; `options.accepts`, when
3895
+ * given, is asked (list, item) with elements.
3789
3896
  */
3790
3897
  function sortable(lists, options) {
3791
- if (!lists.length || !window.Sortable) { return; }
3898
+ var Sortable = sortableLibrary();
3899
+
3900
+ lists = Array.isArray(lists) ? lists : (lists ? [lists] : []);
3901
+ if (!lists.length || !Sortable) { return; }
3792
3902
 
3793
3903
  var wholeDrawer = settings.drag_handle === 'drawer';
3794
3904
  var cancel = dragCancelSelector();
3795
3905
  var soloGroup = 0;
3796
3906
 
3797
- lists.each(function() {
3798
- var list = this;
3799
-
3907
+ lists.forEach(function(list) {
3800
3908
  // A list with no group sorts only within itself, which is what
3801
3909
  // a tab strip wants: a name of its own, closed both ways
3802
3910
  var group = options.group
@@ -3809,11 +3917,11 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3809
3917
  // a column would drop into the canvas, or into another editor
3810
3918
  if (options.group && options.accepts) {
3811
3919
  group.put = function(to, from, dragged) {
3812
- return from.options.group.name === group.name && options.accepts($(to.el), $(dragged));
3920
+ return from.options.group.name === group.name && options.accepts(to.el, dragged);
3813
3921
  };
3814
3922
  }
3815
3923
 
3816
- sortables.push(window.Sortable.create(list, $.extend({
3924
+ sortables.push(Sortable.create(list, Object.assign({
3817
3925
  group: group,
3818
3926
  draggable: options.draggable,
3819
3927
  handle: wholeDrawer ? '.ge-tools-drawer' : '.ge-tools-drawer .ge-move',
@@ -3828,7 +3936,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3828
3936
  * row nested three columns down and drag that.
3829
3937
  */
3830
3938
  filter: function(e, item) {
3831
- if ($(e.target).closest(cancel, list).length) { return true; }
3939
+ if (dom.closest(e.target, cancel, list)) { return true; }
3832
3940
 
3833
3941
  return !item || item.parentNode !== list;
3834
3942
  },
@@ -3873,17 +3981,17 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3873
3981
  }
3874
3982
 
3875
3983
  function makeSortable() {
3876
- if (!window.Sortable) {
3984
+ if (!sortableLibrary()) {
3877
3985
  warnOnceHere('sortable_missing', t('error.sortable_missing'));
3878
3986
  return;
3879
3987
  }
3880
3988
 
3881
- sortable(canvas.find('.row'), {
3989
+ sortable(dom.all(canvas, '.row'), {
3882
3990
  draggable: '.column',
3883
3991
  group: 'column',
3884
3992
  });
3885
3993
 
3886
- sortable(canvas.add(canvas.find('.column')), {
3994
+ sortable([canvas].concat(dom.all(canvas, '.column')), {
3887
3995
  draggable: blockSelector(),
3888
3996
  group: 'block',
3889
3997
  accepts: acceptsBlock,
@@ -3896,7 +4004,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3896
4004
  // SortableJS for "direct children only", which the list is not.
3897
4005
  var regions = pluginHooks('regions');
3898
4006
  if (regions.length) {
3899
- sortable(canvas.find(regions.join(', ')), {
4007
+ sortable(dom.all(canvas, regions.join(', ')), {
3900
4008
  draggable: '>' + blockSelector(),
3901
4009
  group: 'block',
3902
4010
  accepts: acceptsBlock,
@@ -3914,12 +4022,12 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3914
4022
  * always reported by the list that started the drag.
3915
4023
  */
3916
4024
  function sortStart(e) {
3917
- var node = $(e.item);
4025
+ var node = e.item;
3918
4026
  var from = positionOf(node);
3919
4027
  var subject = moveSubject(node);
4028
+ var move = { from: from, canceled: false };
3920
4029
 
3921
- node.data('ge-move-from', from);
3922
- node.removeData('ge-move-canceled');
4030
+ moves.set(node, move);
3923
4031
 
3924
4032
  operate(function() {
3925
4033
  var moving = emit('before-move', payloadFor(subject.kind, subject.node, {
@@ -3928,7 +4036,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3928
4036
  from: from,
3929
4037
  }));
3930
4038
 
3931
- if (!moving) { node.data('ge-move-canceled', true); }
4039
+ if (!moving) { move.canceled = true; }
3932
4040
  });
3933
4041
  }
3934
4042
 
@@ -3937,40 +4045,42 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3937
4045
  * wrapper, so what moved is the content area inside it.
3938
4046
  */
3939
4047
  function moveSubject(item) {
3940
- var node = item.hasClass('ge-text-block') ? item.children('.ge-content') : item;
4048
+ var node = dom.hasClass(item, 'ge-text-block') ? (dom.child(item, '.ge-content') || item) : item;
3941
4049
 
3942
4050
  return { kind: kindOf(node), node: node };
3943
4051
  }
3944
4052
 
3945
4053
  /** Put a node back where a refused drag found it. */
3946
4054
  function putBack(node, from) {
3947
- var siblings = from.parent.children().not('.ge-tools-drawer').not(node);
4055
+ var siblings = dom.children(from.parent).filter(function(child) {
4056
+ return child !== node && !dom.hasClass(child, 'ge-tools-drawer');
4057
+ });
3948
4058
 
3949
4059
  if (!siblings.length || from.index >= siblings.length) {
3950
- node.appendTo(from.parent);
4060
+ from.parent.appendChild(node);
3951
4061
  } else {
3952
- node.insertBefore(siblings.eq(from.index));
4062
+ from.parent.insertBefore(node, siblings[from.index]);
3953
4063
  }
3954
4064
  }
3955
4065
 
3956
4066
  function sortEnd(e) {
3957
- var node = $(e.item);
3958
- var from = node.data('ge-move-from') || positionOf(node);
4067
+ var node = e.item;
4068
+ var move = moves.get(node);
4069
+ var from = move ? move.from : positionOf(node);
3959
4070
 
3960
- node.removeData('ge-move-from');
4071
+ moves.delete(node);
3961
4072
 
3962
- if (node.data('ge-move-canceled')) {
3963
- node.removeData('ge-move-canceled');
4073
+ if (move && move.canceled) {
3964
4074
  putBack(node, from);
3965
4075
  return;
3966
4076
  }
3967
4077
 
3968
4078
  var to = positionOf(node);
3969
- if (to.parent[0] === from.parent[0] && to.index === from.index) {
4079
+ if (to.parent === from.parent && to.index === from.index) {
3970
4080
  return; // A drag that went nowhere is not a move
3971
4081
  }
3972
4082
 
3973
- var container = node.closest('[data-ge-container]');
4083
+ var container = dom.closest(node, '[data-ge-container]');
3974
4084
  var definition = CONTAINERS[containerTypeOf(container)];
3975
4085
  if (definition && definition.afterPaneMove) {
3976
4086
  definition.afterPaneMove(container, node, from);
@@ -3987,7 +4097,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3987
4097
  source: 'dragdrop',
3988
4098
  from: from,
3989
4099
  to: to,
3990
- container: container.length ? container : undefined,
4100
+ container: container || undefined,
3991
4101
  }));
3992
4102
  });
3993
4103
  }
@@ -4004,19 +4114,19 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4004
4114
  function makeResizable() {
4005
4115
  if (!settings.resize.enabled) { return; }
4006
4116
 
4007
- canvas.find('.column').each(function() {
4008
- var col = $(this);
4009
- if (col.find('> .ge-resize-handle').length) { return; }
4117
+ dom.all(canvas, '.column').forEach(function(col) {
4118
+ if (dom.child(col, '.ge-resize-handle')) { return; }
4010
4119
 
4011
- $('<span class="ge-resize-size" />').appendTo(col.find('> .ge-tools-drawer'));
4120
+ var drawer = dom.child(col, '.ge-tools-drawer');
4121
+ if (drawer) { drawer.appendChild(dom.element('span', { 'class': 'ge-resize-size' })); }
4012
4122
 
4013
- $.each(resizeEdges(), function(_, edge) {
4014
- $('<span class="ge-resize-handle" />')
4015
- .addClass('ge-resize-' + edge)
4016
- .attr('data-ge-edge', edge)
4017
- .on('pointerdown', startResizeDrag)
4018
- .appendTo(col)
4019
- ;
4123
+ resizeEdges().forEach(function(edge) {
4124
+ var handle = dom.element('span', {
4125
+ 'class': 'ge-resize-handle ge-resize-' + edge,
4126
+ 'data-ge-edge': edge,
4127
+ });
4128
+ handle.addEventListener('pointerdown', startResizeDrag);
4129
+ col.appendChild(handle);
4020
4130
  });
4021
4131
  });
4022
4132
  }
@@ -4037,19 +4147,19 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4037
4147
  * starts: nothing is written and the column does not move.
4038
4148
  */
4039
4149
  function startResizeDrag(e) {
4040
- var handle = $(e.currentTarget);
4041
- var col = handle.parent();
4042
- var west = handle.attr('data-ge-edge') === 'w';
4150
+ var handle = e.currentTarget;
4151
+ var col = handle.parentElement;
4152
+ var west = handle.getAttribute('data-ge-edge') === 'w';
4043
4153
  var startX = e.pageX;
4044
- var startWidth = col.outerWidth();
4154
+ var startWidth = dom.outerWidth(col);
4045
4155
 
4046
4156
  e.preventDefault();
4047
4157
 
4048
4158
  if (!resizeStart(col)) { return; }
4049
4159
 
4050
- col.addClass('ge-resizing');
4051
- if (e.pointerId !== undefined && handle[0].setPointerCapture) {
4052
- handle[0].setPointerCapture(e.pointerId);
4160
+ dom.addClass(col, 'ge-resizing');
4161
+ if (e.pointerId !== undefined && handle.setPointerCapture) {
4162
+ handle.setPointerCapture(e.pointerId);
4053
4163
  }
4054
4164
 
4055
4165
  function widthAt(move) {
@@ -4059,23 +4169,25 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4059
4169
  }
4060
4170
 
4061
4171
  function onMove(move) {
4062
- col.css('width', widthAt(move) + 'px');
4172
+ col.style.width = widthAt(move) + 'px';
4063
4173
  resizeMove(col, widthAt(move));
4064
4174
  }
4065
4175
 
4066
4176
  function onUp(up) {
4067
- handle.off('pointermove', onMove).off('pointerup pointercancel', onUp);
4068
- col.removeClass('ge-resizing');
4177
+ handle.removeEventListener('pointermove', onMove);
4178
+ dom.off(handle, 'pointerup pointercancel', onUp);
4179
+ dom.removeClass(col, 'ge-resizing');
4069
4180
  resizeStop(col, widthAt(up));
4070
4181
  }
4071
4182
 
4072
- handle.on('pointermove', onMove).on('pointerup pointercancel', onUp);
4183
+ handle.addEventListener('pointermove', onMove);
4184
+ dom.on(handle, 'pointerup pointercancel', onUp);
4073
4185
  }
4074
4186
 
4075
4187
  function removeResizable() {
4076
- canvas.find('.ge-resize-handle').remove();
4188
+ dom.all(canvas, '.ge-resize-handle').forEach(function(handle) { handle.remove(); });
4077
4189
 
4078
- canvas.find('.ge-resize-size').remove();
4190
+ dom.all(canvas, '.ge-resize-size').forEach(function(readout) { readout.remove(); });
4079
4191
  stripPixelWidths(canvas);
4080
4192
  }
4081
4193
 
@@ -4086,13 +4198,14 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4086
4198
  * divider between two columns looks like it should do.
4087
4199
  */
4088
4200
  function snapUnits(col, pixels) {
4089
- var units = Math.round(pixels / rowContentWidth(col.parent()) * MAX_COL_SIZE);
4201
+ var units = Math.round(pixels / rowContentWidth(col.parentElement) * MAX_COL_SIZE);
4090
4202
  var next = balanceSibling(col);
4091
4203
  var nextSize = next ? currentSize(next) : null;
4092
4204
 
4093
4205
  // Measured when the drag started: the column is the width of the
4094
4206
  // pointer now, which is not what it was
4095
- var own = col.data('ge-resize-units');
4207
+ var resize = resizes.get(col);
4208
+ var own = resize ? resize.units : currentUnits(col);
4096
4209
 
4097
4210
  // With a sibling to balance against, the drag may take that
4098
4211
  // column's units but not its last one. Without one, the row is
@@ -4114,12 +4227,13 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4114
4227
  function balanceSibling(col) {
4115
4228
  if (settings.resize.balance !== 'next') { return null; }
4116
4229
 
4117
- var next = col.nextAll('.column').first();
4118
- return next.length ? next : null;
4230
+ return dom.nextAll(col, '.column')[0] || null;
4119
4231
  }
4120
4232
 
4121
4233
  function resizeReadout(col, text) {
4122
- col.find('> .ge-tools-drawer > .ge-resize-size').text(text);
4234
+ var drawer = dom.child(col, '.ge-tools-drawer');
4235
+ var readout = drawer ? dom.child(drawer, '.ge-resize-size') : null;
4236
+ if (readout) { readout.textContent = text; }
4123
4237
  }
4124
4238
 
4125
4239
  function sizeLabel(units) {
@@ -4145,9 +4259,9 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4145
4259
 
4146
4260
  if (!allowed) { return false; }
4147
4261
 
4148
- col.data('ge-resize-from', from);
4149
- col.data('ge-resize-units', currentUnits(col));
4150
- resizeReadout(col, sizeLabel(col.data('ge-resize-units')));
4262
+ var resize = { from: from, units: currentUnits(col) };
4263
+ resizes.set(col, resize);
4264
+ resizeReadout(col, sizeLabel(resize.units));
4151
4265
 
4152
4266
  return true;
4153
4267
  }
@@ -4157,44 +4271,40 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4157
4271
  }
4158
4272
 
4159
4273
  function resizeStop(col, width) {
4160
- var from = col.data('ge-resize-from');
4161
- var started = col.data('ge-resize-units');
4274
+ var resize = resizes.get(col);
4162
4275
  var units = snapUnits(col, width);
4163
4276
 
4164
- col.removeData('ge-resize-from').removeData('ge-resize-units');
4277
+ resizes.delete(col);
4165
4278
  resizeReadout(col, '');
4166
4279
  stripPixelWidths(col);
4167
4280
 
4168
- if (from === undefined) { return; }
4281
+ if (!resize) { return; }
4169
4282
 
4170
4283
  // A drag of a couple of pixels lands on the size it started from,
4171
4284
  // and is not a resize - not even of an equal or auto column,
4172
4285
  // which it would otherwise turn into a number
4173
- if (units === started) { return; }
4286
+ if (units === resize.units) { return; }
4174
4287
 
4175
4288
  var plan = planSize(col, units);
4176
4289
  if (!plan) { return; }
4177
4290
 
4178
4291
  operate(function() {
4179
4292
  writeSize(col, plan);
4180
- balanceAfterResize(col, units - started);
4293
+ balanceAfterResize(col, units - resize.units);
4181
4294
  refreshUtilities(col);
4182
4295
 
4183
4296
  emit('after-resize', payloadFor('column', col, withCleared({
4184
4297
  source: 'dragdrop',
4185
- from: from,
4298
+ from: resize.from,
4186
4299
  to: plan.size,
4187
4300
  }, plan.cleared)));
4188
4301
  });
4189
4302
  }
4190
4303
 
4191
- /**
4192
- * Move the delta into the following column, so a full row stays full.
4193
- * It is part of the same gesture, so it is not announced separately.
4194
- */
4195
4304
  /**
4196
4305
  * The column after takes what the resized one gave or took, unless it
4197
- * is equal or auto: those take what is left by themselves.
4306
+ * is equal or auto: those take what is left by themselves. It is part
4307
+ * of the same gesture, so it is not announced separately.
4198
4308
  */
4199
4309
  function balanceAfterResize(col, delta) {
4200
4310
  var next = balanceSibling(col);
@@ -4218,13 +4328,13 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4218
4328
  * is none of the editor's business.
4219
4329
  */
4220
4330
  function removeSortable() {
4221
- $.each(sortables, function(_, instance) { instance.destroy(); });
4331
+ sortables.forEach(function(sortableInstance) { sortableInstance.destroy(); });
4222
4332
 
4223
4333
  sortables = [];
4224
4334
  }
4225
4335
 
4226
4336
  function createRow() {
4227
- return $('<div class="row" />');
4337
+ return dom.element('div', { 'class': 'row' });
4228
4338
  }
4229
4339
 
4230
4340
  /**
@@ -4234,6 +4344,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4234
4344
  * detached, and placing it and calling reset() is the host's job
4235
4345
  * (spec 1.2).
4236
4346
  *
4347
+ * The place is an element, or the first one a selector matches.
4237
4348
  * Returns the node, or null when a handler canceled the add. A call
4238
4349
  * made from inside an event handler is queued, and then returns the
4239
4350
  * node without knowing yet whether the add will be canceled.
@@ -4249,14 +4360,27 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4249
4360
 
4250
4361
  if (placement === null) { return node; }
4251
4362
 
4252
- var target = $(options[placement]);
4363
+ var target = nodeFrom(options[placement]);
4364
+ if (!target) {
4365
+ warn(kind + ': ' + placement + ' matches no element; the ' + kind + ' is left detached');
4366
+ return node;
4367
+ }
4368
+
4253
4369
  var parent = (placement === 'appendTo' || placement === 'prependTo')
4254
4370
  ? target
4255
- : target.parent();
4371
+ : target.parentElement;
4256
4372
 
4257
4373
  var add = function() {
4258
4374
  return addNode(kind, node, function() {
4259
- node[placement](options[placement]);
4375
+ if (placement === 'appendTo') {
4376
+ target.appendChild(node);
4377
+ } else if (placement === 'prependTo') {
4378
+ target.insertBefore(node, target.firstChild);
4379
+ } else if (placement === 'insertBefore') {
4380
+ target.parentNode.insertBefore(node, target);
4381
+ } else {
4382
+ dom.insertAfter(node, target);
4383
+ }
4260
4384
  }, { parent: parent, source: options.source || 'api' });
4261
4385
  };
4262
4386
 
@@ -4283,7 +4407,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4283
4407
  if (layout && !Array.isArray(layout)) { row = rowFromLayoutValue(layout); }
4284
4408
 
4285
4409
  (Array.isArray(layout) ? layout : []).forEach(function(size) {
4286
- createColumn(size).appendTo(row);
4410
+ row.appendChild(createColumn(size));
4287
4411
  });
4288
4412
 
4289
4413
  return place(row, 'row', options);
@@ -4296,9 +4420,9 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4296
4420
  options = options || {};
4297
4421
 
4298
4422
  // Going into a row with row-cols, no size is a size: the row's share
4299
- var into = $(options.appendTo || options.prependTo || []).first();
4423
+ var into = nodeFrom(options.appendTo || options.prependTo || null);
4300
4424
 
4301
- if (size === undefined && into.length && rowColsSource(into, leadingTier())) {
4425
+ if (size === undefined && into && rowColsSource(into, leadingTier())) {
4302
4426
  size = null;
4303
4427
  } else if (!isUnits(size) && FLEX_SIZES.indexOf(size) === -1) {
4304
4428
  warn('createColumn: no column size given, using ' + MAX_COL_SIZE);
@@ -4307,7 +4431,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4307
4431
 
4308
4432
  var column = createColumn(size, options.offset);
4309
4433
  if (options.content !== undefined) {
4310
- column.append(contentFor(options.content));
4434
+ column.appendChild(contentFor(options.content));
4311
4435
  }
4312
4436
 
4313
4437
  return place(column, 'column', options);
@@ -4330,7 +4454,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4330
4454
 
4331
4455
  /** A pane appended to a container, through the add events. */
4332
4456
  function addPaneTo(container, type, options) {
4333
- container = $(container);
4457
+ container = nodeFrom(container);
4334
4458
  options = options || {};
4335
4459
 
4336
4460
  var definition = CONTAINERS[containerTypeOf(container)];
@@ -4350,20 +4474,16 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4350
4474
  }
4351
4475
 
4352
4476
  /**
4353
- * Host markup wrapped as a grid-editor element (spec 4.5). What is
4354
- * inside stays the host's; grid-editor owns the wrapper only.
4355
- */
4356
-
4357
- /**
4358
- * A shallow frozen copy of the settings for the instance handle, so a
4359
- * host can read what the editor is running with without changing it
4477
+ * A shallow frozen copy of the settings for the instance, so a host
4478
+ * can read what the editor is running with without changing it
4360
4479
  * behind the editor's back. Arrays are copied; the objects inside them
4361
4480
  * are the host's own and stay shared.
4362
4481
  */
4363
4482
  function settingsCopy() {
4364
4483
  var copy = {};
4365
4484
 
4366
- $.each(settings, function(key, value) {
4485
+ Object.keys(settings).forEach(function(key) {
4486
+ var value = settings[key];
4367
4487
  copy[key] = Array.isArray(value) ? value.slice() : value;
4368
4488
  });
4369
4489
 
@@ -4377,7 +4497,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4377
4497
  function createColumn(size, offset) {
4378
4498
  // Empty: what goes in it is the next decision, and the tools of
4379
4499
  // the column and the toolbar are where it is made
4380
- var column = $('<div class="column"/>');
4500
+ var column = dom.element('div', { 'class': 'column' });
4381
4501
 
4382
4502
  // The tier being edited, or the base class in the all view
4383
4503
  var tier = tiersFor(curView)[0];
@@ -4390,18 +4510,24 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4390
4510
  }
4391
4511
 
4392
4512
  /**
4393
- * Run custom content filter on init and deinit
4513
+ * Run custom content filter on init and deinit. A filter is a
4514
+ * function, or the name of one on window, and gets the canvas and
4515
+ * whether this is init.
4394
4516
  */
4395
4517
  function runFilter(isInit) {
4396
- if (settings.custom_filter.length) {
4397
- $.each(settings.custom_filter, function(key, func) {
4398
- if (typeof func == 'string') {
4399
- func = window[func];
4400
- }
4518
+ if (!settings.custom_filter || !settings.custom_filter.length) { return; }
4401
4519
 
4402
- func(canvas, isInit);
4403
- });
4404
- }
4520
+ var filters = typeof settings.custom_filter === 'string' || typeof settings.custom_filter === 'function'
4521
+ ? [settings.custom_filter]
4522
+ : settings.custom_filter;
4523
+
4524
+ Array.prototype.forEach.call(filters, function(func) {
4525
+ if (typeof func == 'string') {
4526
+ func = window[func];
4527
+ }
4528
+
4529
+ func(canvas, isInit);
4530
+ });
4405
4531
  }
4406
4532
 
4407
4533
  /**
@@ -4412,7 +4538,8 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4412
4538
  function textCutter() {
4413
4539
  var cuts = ['.row', '[data-ge-container]'];
4414
4540
 
4415
- $.each(FEATURES, function(name, feature) {
4541
+ Object.keys(FEATURES).forEach(function(name) {
4542
+ var feature = FEATURES[name];
4416
4543
  var cut = typeof feature.cuts === 'function' ? feature.cuts() : feature.cuts;
4417
4544
  if (cut) { cuts.push(cut); }
4418
4545
  });
@@ -4438,17 +4565,16 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4438
4565
  * content area its editor is open on is left for the next init.
4439
4566
  */
4440
4567
  function splitTexts(cutter) {
4441
- canvas.find('.column > .ge-content, .column > .ge-text-block > .ge-content').each(function() {
4442
- var area = $(this);
4443
- if (area.hasClass('ge-rte-active') || !area.children().filter(cutter).length) { return; }
4568
+ dom.all(canvas, '.column > .ge-content, .column > .ge-text-block > .ge-content').forEach(function(area) {
4569
+ if (dom.hasClass(area, 'ge-rte-active') || !dom.children(area, cutter).length) { return; }
4444
4570
 
4445
- var type = area.attr('data-ge-content-type');
4446
- var anchor = area.parent('.ge-text-block').length ? area.parent() : area;
4571
+ var type = area.getAttribute('data-ge-content-type');
4572
+ var anchor = dom.hasClass(area.parentElement, 'ge-text-block') ? area.parentElement : area;
4447
4573
  var pieces = [];
4448
4574
  var run = [];
4449
4575
 
4450
- $.each($.makeArray(area[0].childNodes), function(i, node) {
4451
- if (node.nodeType === 1 && $(node).is(cutter)) {
4576
+ Array.prototype.slice.call(area.childNodes).forEach(function(node) {
4577
+ if (node.nodeType === 1 && node.matches(cutter)) {
4452
4578
  pieces.push({ text: run }, { block: node });
4453
4579
  run = [];
4454
4580
  } else {
@@ -4457,7 +4583,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4457
4583
  });
4458
4584
  pieces.push({ text: run });
4459
4585
 
4460
- $(area[0].childNodes).detach();
4586
+ while (area.firstChild) { area.removeChild(area.firstChild); }
4461
4587
 
4462
4588
  var kept = false;
4463
4589
  var last = anchor;
@@ -4466,21 +4592,22 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4466
4592
  var placed;
4467
4593
 
4468
4594
  if (piece.block) {
4469
- placed = $(piece.block);
4595
+ placed = piece.block;
4470
4596
  } else if (!hasContent(piece.text)) {
4471
4597
  return;
4472
4598
  } else if (kept) {
4473
4599
  // Of the same type, or of none if it had none
4474
- placed = createDefaultContentWrapper(type).append(piece.text);
4600
+ placed = createDefaultContentWrapper(type);
4601
+ piece.text.forEach(function(node) { placed.appendChild(node); });
4475
4602
  } else {
4476
4603
  // The content area itself, moved along if an
4477
4604
  // element came before the first text
4478
- area.append(piece.text);
4605
+ piece.text.forEach(function(node) { area.appendChild(node); });
4479
4606
  placed = anchor;
4480
4607
  kept = true;
4481
4608
  }
4482
4609
 
4483
- if (placed[0] !== last[0]) { placed.insertAfter(last); }
4610
+ if (placed !== last) { dom.insertAfter(placed, last); }
4484
4611
  last = placed;
4485
4612
  });
4486
4613
 
@@ -4499,26 +4626,23 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4499
4626
  * Wrap column content in <div class="ge-content"> where neccesary
4500
4627
  */
4501
4628
  function wrapContent(cutter) {
4502
- canvas.find('.column').each(function() {
4503
- var col = $(this);
4504
- var contents = $();
4505
-
4506
- col.children().each(function() {
4507
- var child = $(this);
4629
+ dom.all(canvas, '.column').forEach(function(col) {
4630
+ var contents = [];
4508
4631
 
4632
+ dom.children(col).forEach(function(child) {
4509
4633
  // The editor's own furniture is not content and not a
4510
4634
  // boundary either. The resize handle used to be treated as
4511
4635
  // content and wrapped into a content area of its own on
4512
4636
  // the next init.
4513
- if (child.is('.ge-tools-drawer, .ge-resize-handle')) { return; }
4637
+ if (dom.is(child, '.ge-tools-drawer, .ge-resize-handle')) { return; }
4514
4638
 
4515
4639
  // A container, or an element, sits in the column beside
4516
4640
  // the content areas, not inside one, so it ends a run of
4517
4641
  // loose content rather than joining it
4518
- if (child.is('.ge-content, .ge-text-block') || child.is(cutter)) {
4642
+ if (dom.is(child, '.ge-content, .ge-text-block') || child.matches(cutter)) {
4519
4643
  contents = doWrap(contents);
4520
4644
  } else {
4521
- contents = contents.add(child);
4645
+ contents.push(child);
4522
4646
  }
4523
4647
  });
4524
4648
 
@@ -4529,17 +4653,17 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4529
4653
  /**
4530
4654
  * Wrap a run of loose column content in a content area of no type -
4531
4655
  * the host's plain content - and hand back
4532
- * an empty set: the caller has to forget what it just wrapped, or the
4656
+ * an empty run: the caller has to forget what it just wrapped, or the
4533
4657
  * next boundary wraps the same nodes again and leaves the first
4534
4658
  * wrapper behind, empty.
4535
4659
  */
4536
4660
  function doWrap(contents) {
4537
4661
  if (contents.length) {
4538
- var contentArea = createDefaultContentWrapper().insertAfter(contents.last());
4539
- contents.appendTo(contentArea);
4662
+ var contentArea = dom.insertAfter(createDefaultContentWrapper(), contents[contents.length - 1]);
4663
+ contents.forEach(function(node) { contentArea.appendChild(node); });
4540
4664
  }
4541
4665
 
4542
- return $();
4666
+ return [];
4543
4667
  }
4544
4668
 
4545
4669
  /**
@@ -4547,10 +4671,11 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4547
4671
  * content: a content area and nothing more.
4548
4672
  */
4549
4673
  function createDefaultContentWrapper(type) {
4550
- var contentArea = $('<div class="ge-content"/>');
4674
+ var contentArea = dom.element('div', { 'class': 'ge-content' });
4551
4675
 
4552
4676
  if (type) {
4553
- contentArea.addClass('ge-content-type-' + type).attr('data-ge-content-type', type);
4677
+ dom.addClass(contentArea, 'ge-content-type-' + type);
4678
+ contentArea.setAttribute('data-ge-content-type', type);
4554
4679
  }
4555
4680
 
4556
4681
  return contentArea;
@@ -4575,15 +4700,15 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4575
4700
  * -------------------------------------------------------------- */
4576
4701
 
4577
4702
  function wrapTexts() {
4578
- canvas.find('.column > .ge-content').each(function() {
4579
- $(this).wrap('<div class="ge-text-block" />');
4703
+ dom.all(canvas, '.column > .ge-content').forEach(function(area) {
4704
+ dom.wrap(area, dom.element('div', { 'class': 'ge-text-block' }));
4580
4705
  });
4581
4706
 
4582
- canvas.find('.ge-text-block').each(function() {
4583
- var textBlock = $(this);
4584
- if (textBlock.children('.ge-tools-drawer').length) { return; }
4707
+ dom.all(canvas, '.ge-text-block').forEach(function(textBlock) {
4708
+ if (dom.child(textBlock, '.ge-tools-drawer')) { return; }
4585
4709
 
4586
- var type = textBlock.children('.ge-content').attr('data-ge-content-type');
4710
+ var area = dom.child(textBlock, '.ge-content');
4711
+ var type = area ? area.getAttribute('data-ge-content-type') : null;
4587
4712
 
4588
4713
  if (!type) {
4589
4714
  createPlainControls(textBlock);
@@ -4595,28 +4720,28 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4595
4720
 
4596
4721
  /** After the drawers are gone, so a wrapper holds its content area and nothing else. */
4597
4722
  function unwrapTexts() {
4598
- canvas.find('.ge-text-block').each(function() {
4599
- $(this).replaceWith($(this).contents());
4723
+ dom.all(canvas, '.ge-text-block').forEach(function(textBlock) {
4724
+ dom.unwrap(textBlock);
4600
4725
  });
4601
4726
  }
4602
4727
 
4603
4728
  /** The host's plain content: moved and deleted here, edited in the source. */
4604
4729
  function createPlainControls(textBlock) {
4605
- var block = textBlock.children('.ge-content');
4606
- var drawer = $('<div class="ge-tools-drawer ge-text-drawer ge-plain-drawer" />').prependTo(textBlock);
4607
- textBlock.addClass('ge-plain-block');
4730
+ var block = dom.child(textBlock, '.ge-content');
4731
+ var drawer = prependDrawer(textBlock, 'ge-tools-drawer ge-text-drawer ge-plain-drawer');
4732
+ dom.addClass(textBlock, 'ge-plain-block');
4608
4733
 
4609
4734
  createMoveTool(drawer);
4610
4735
 
4611
4736
  // The tools a feature plugin gives plain content, which is not
4612
4737
  // every plugin's drawerTools: no gear, no utilities, no copy
4613
- $.each(FEATURES, function(name, feature) {
4614
- if (feature.plainTools) { feature.plainTools(drawer, block); }
4738
+ Object.keys(FEATURES).forEach(function(name) {
4739
+ if (FEATURES[name].plainTools) { FEATURES[name].plainTools(drawer, block); }
4615
4740
  });
4616
4741
 
4617
4742
  createTool(drawer, t('tool.delete_plain'), 'ge-delete-plain', 'bi bi-trash', function() {
4618
4743
  deleteNode('plain', block, t('confirm.delete_plain'), function(removed) {
4619
- textBlock.slideUp(function() {
4744
+ dom.slideUp(textBlock, function() {
4620
4745
  textBlock.remove();
4621
4746
  removed();
4622
4747
  });
@@ -4629,15 +4754,15 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4629
4754
  * and its drawer says why it cannot be edited.
4630
4755
  */
4631
4756
  function createOrphanControls(textBlock, type) {
4632
- var block = textBlock.children('.ge-content');
4633
- var drawer = $('<div class="ge-tools-drawer ge-text-drawer" />').prependTo(textBlock);
4757
+ var block = dom.child(textBlock, '.ge-content');
4758
+ var drawer = prependDrawer(textBlock, 'ge-tools-drawer ge-text-drawer');
4634
4759
 
4635
4760
  createMoveTool(drawer);
4636
4761
  createTool(drawer, t('text.no_editor', { type: type }), 'ge-text-info ge-text-missing',
4637
4762
  'bi bi-exclamation-triangle');
4638
4763
  createTool(drawer, t('tool.delete_text'), 'ge-delete-text', 'bi bi-trash', function() {
4639
4764
  deleteNode('text', block, t('confirm.delete_text'), function(removed) {
4640
- textBlock.slideUp(function() {
4765
+ dom.slideUp(textBlock, function() {
4641
4766
  textBlock.remove();
4642
4767
  removed();
4643
4768
  });
@@ -4646,13 +4771,21 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4646
4771
  }
4647
4772
 
4648
4773
  /**
4649
- * A detached content area holding `content`: a text of the first type
4650
- * offered, or the host's plain content when no editor is offered.
4774
+ * A detached content area holding `content` - html, or a node - as a
4775
+ * text of the first type offered, or the host's plain content when no
4776
+ * editor is offered.
4651
4777
  */
4652
4778
  function contentFor(content) {
4653
4779
  var offers = textOffers();
4780
+ var area = createDefaultContentWrapper(offers.length ? offers[0].type : null);
4781
+
4782
+ if (content && content.nodeType) {
4783
+ area.appendChild(content);
4784
+ } else {
4785
+ dom.setHtml(area, content);
4786
+ }
4654
4787
 
4655
- return createDefaultContentWrapper(offers.length ? offers[0].type : null).html(content);
4788
+ return area;
4656
4789
  }
4657
4790
 
4658
4791
  /**
@@ -4664,9 +4797,9 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4664
4797
  curView = view;
4665
4798
 
4666
4799
  VIEW_KEYS.forEach(function(key) {
4667
- canvas.toggleClass('ge-layout-' + key, key === view);
4800
+ dom.toggleClass(canvas, 'ge-layout-' + key, key === view);
4668
4801
  });
4669
- layoutDropdown.find('button').text(t(labelKeyFor(view)));
4802
+ dom.one(layoutDropdown, 'button').textContent = t(labelKeyFor(view));
4670
4803
  }
4671
4804
 
4672
4805
  /**
@@ -4699,7 +4832,9 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4699
4832
 
4700
4833
  if (key === from) { return; }
4701
4834
 
4702
- settingsScope().find('.ge-utilities').each(function() { renderUtilities($(this)); });
4835
+ settingsScope().forEach(function(scope) {
4836
+ dom.all(scope, '.ge-utilities').forEach(function(section) { renderUtilities(section); });
4837
+ });
4703
4838
  refreshPreviews(canvas);
4704
4839
  plugins('onViewChange', key);
4705
4840
  emit('view-change', { canvas: canvas, breakpoint: key, from: from, to: key });
@@ -4708,22 +4843,33 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4708
4843
  function getView() {
4709
4844
  return curView;
4710
4845
  }
4711
-
4846
+
4847
+ /** A method a feature plugin contributes, or a warning that the plugin is not loaded. */
4848
+ function featureMethod(name, plugin, file) {
4849
+ return function() {
4850
+ if (!featureMethods[name]) {
4851
+ warnOnceHere('plugin:' + plugin, name + ' needs the ' + plugin + ' plugin: ' +
4852
+ 'include dist/plugins/grideditor.' + file + '.js after the editor');
4853
+ return null;
4854
+ }
4855
+
4856
+ return featureMethods[name].apply(null, arguments);
4857
+ };
4858
+ }
4859
+
4712
4860
  /**
4713
- * The instance handle, documented API as of 3.0: the methods the
4714
- * plugin dispatches, plus the settings and the canvas. A host holding
4715
- * this can call several methods without dispatching each one.
4861
+ * The instance's own methods: the documented API. init and reset are
4862
+ * deferred when a handler calls them, so an operation in flight
4863
+ * finishes before the canvas is rebuilt. The ones that do something,
4864
+ * rather than answer something, hand back the instance, to chain.
4716
4865
  */
4717
- var handle = {
4866
+ var own = {
4718
4867
  getHtml: getHtml,
4719
4868
  getPlainHtml: getPlainHtml,
4720
- // init and reset are deferred when a handler calls them, so an
4721
- // operation in flight finishes before the canvas is rebuilt
4722
4869
  init: function() { defer(init); },
4723
4870
  reset: function() { defer(reset); },
4724
4871
  deinit: deinit,
4725
4872
  destroy: destroy,
4726
- remove: deprecatedRemove,
4727
4873
  changeView: changeView,
4728
4874
  getView: getView,
4729
4875
  createRow: apiCreateRow,
@@ -4737,24 +4883,8 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4737
4883
 
4738
4884
  return featureMethods.createText(type, options);
4739
4885
  },
4740
- createElement: function(content, options) {
4741
- if (!featureMethods.createElement) {
4742
- warnOnceHere('plugin:elements', 'createElement needs the elements plugin: ' +
4743
- 'include dist/plugins/grideditor.elements.js after the editor');
4744
- return null;
4745
- }
4746
-
4747
- return featureMethods.createElement(content, options);
4748
- },
4749
- createSection: function(options) {
4750
- if (!featureMethods.createSection) {
4751
- warnOnceHere('plugin:sections', 'createSection needs the sections plugin: ' +
4752
- 'include dist/plugins/grideditor.sections.js after the editor');
4753
- return null;
4754
- }
4755
-
4756
- return featureMethods.createSection(options);
4757
- },
4886
+ createElement: featureMethod('createElement', 'elements', 'elements'),
4887
+ createSection: featureMethod('createSection', 'sections', 'sections'),
4758
4888
  createContainer: apiCreateContainer,
4759
4889
  addTab: function(container, options) { return addPaneTo(container, 'tabs', options); },
4760
4890
  addAccordionItem: function(container, options) {
@@ -4763,42 +4893,45 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4763
4893
  setLocale: setLocale,
4764
4894
  getUtility: getUtility,
4765
4895
  setUtility: setUtility,
4766
- canvas: canvas,
4767
- settings: settingsCopy(),
4768
4896
  };
4769
4897
 
4770
- // Methods a later phase fills in: registered, so calling one gets a
4771
- // warning and null rather than silence.
4772
- $.each(METHODS, function(name, descriptor) {
4773
- if (!descriptor.unimplemented) { return; }
4898
+ Object.keys(own).forEach(function(name) {
4899
+ var value = !!(METHODS[name] && METHODS[name].value);
4774
4900
 
4775
- handle[name] = function() {
4776
- warnOnceHere(name, name + ' is registered but not implemented in this build yet');
4777
- return null;
4901
+ instance[name] = function() {
4902
+ if (destroyed) {
4903
+ warnOnceHere('destroyed:' + name, t('warning.destroyed', { method: name }));
4904
+ if (name === 'getHtml') { return canvas.innerHTML; }
4905
+ if (name === 'getPlainHtml') { return plainHtml(canvas.innerHTML); }
4906
+ return value ? null : instance;
4907
+ }
4908
+
4909
+ var result = own[name].apply(instance, arguments);
4910
+ return value ? result : instance;
4778
4911
  };
4779
4912
  });
4780
4913
 
4781
- baseElem.data('grideditor', handle);
4914
+ instance.canvas = canvas;
4915
+ instance.settings = settingsCopy();
4916
+
4917
+ instances.set(canvas, instance);
4782
4918
 
4783
4919
  loadPlugins();
4784
4920
  // Again, with what the plugins filled in: the text editors'
4785
4921
  // content_types when the host gave none
4786
- handle.settings = settingsCopy();
4922
+ instance.settings = settingsCopy();
4787
4923
  setup();
4788
4924
  init();
4789
4925
 
4790
- });
4791
-
4792
- return self;
4793
-
4794
- };
4926
+ GridEditor._created.forEach(function(hook) { hook(instance); });
4927
+ }
4795
4928
 
4796
4929
  /**
4797
- * 5.x's registry of text editors, removed in 6.0: still there to register
4798
- * into, so a page written for 5.x does not throw, but what it holds is
4799
- * ignored with a warning. The text editors are plugins since 6.0.
4930
+ * What a module that builds on the editor - the jQuery adapter - is told:
4931
+ * each editor as it is made. Not the plugin contract; a plugin is handed
4932
+ * the editor through its factory.
4800
4933
  */
4801
- $.fn.gridEditor.RTEs = {};
4934
+ GridEditor._created = [];
4802
4935
 
4803
4936
  /**
4804
4937
  * Container plugins: tabs, accordions, popups, and whatever a host writes.
@@ -4807,11 +4940,11 @@ $.fn.gridEditor.RTEs = {};
4807
4940
  * editor with the handle described in docs/plugins.md. Loading its file is
4808
4941
  * what makes the type available; the `plugins` setting narrows that list.
4809
4942
  *
4810
- * $.fn.gridEditor.containers.carousel = function(ge) {
4943
+ * GridEditor.containers.carousel = function(ge) {
4811
4944
  * return { labelKey: ..., create: ..., mark: ..., unmark: ... };
4812
4945
  * };
4813
4946
  */
4814
- $.fn.gridEditor.containers = {};
4947
+ GridEditor.containers = {};
4815
4948
 
4816
4949
  /**
4817
4950
  * Feature plugins: a piece of the editor that is not a container type, in a
@@ -4820,7 +4953,7 @@ $.fn.gridEditor.containers = {};
4820
4953
  * the handle in docs/plugins.md - and the same `plugins` setting decides
4821
4954
  * which of the loaded ones are used.
4822
4955
  */
4823
- $.fn.gridEditor.features = {};
4956
+ GridEditor.features = {};
4824
4957
 
4825
4958
  /**
4826
4959
  * Utility plugins: Bootstrap's responsive utility classes - order-md-2,
@@ -4828,14 +4961,35 @@ $.fn.gridEditor.features = {};
4828
4961
  * and the editor reads them, writes them, puts them in the settings panel and
4829
4962
  * previews them in each view. Same factory, same `plugins` setting.
4830
4963
  *
4831
- * $.fn.gridEditor.utilities.order = function(ge) {
4964
+ * GridEditor.utilities.order = function(ge) {
4832
4965
  * return { families: [{ name: 'order', prefix: 'order', values: [...] }] };
4833
4966
  * };
4834
4967
  */
4835
- $.fn.gridEditor.utilities = {};
4968
+ GridEditor.utilities = {};
4969
+
4970
+ /**
4971
+ * The text editors, by the content type each edits: what the text plugins
4972
+ * (tinymce, ckeditor, summernote, a host's own) register, and the text
4973
+ * feature, src/js/text/grideditor.text.js, reads.
4974
+ */
4975
+ GridEditor.texts = {};
4976
+
4977
+ /** Translator for the plugins, which get settings and no instance. */
4978
+ GridEditor.t = translate;
4979
+
4980
+ /** getPlainHtml for html that is not on a canvas, for the jQuery adapter. Not the public API. */
4981
+ GridEditor._plainHtml = plainHtml;
4982
+
4983
+ /**
4984
+ * SortableJS and Bootstrap's javascript, for a page that imports them as
4985
+ * modules and so has no window.Sortable or window.bootstrap. Looked up
4986
+ * before the globals; only Bootstrap's Modal is used.
4987
+ */
4988
+ GridEditor.Sortable = null;
4989
+ GridEditor.bootstrap = null;
4836
4990
 
4837
- /** Translator for the editor integrations, which get settings and no instance. */
4838
- $.fn.gridEditor.t = translate;
4991
+ /** The version of the build, filled in by build/build.js. */
4992
+ GridEditor.version = typeof __GRIDEDITOR_VERSION__ === 'undefined' ? 'dev' : __GRIDEDITOR_VERSION__;
4839
4993
 
4840
4994
  /**
4841
4995
  * Locale registry: code -> { key: string }.
@@ -4848,7 +5002,7 @@ $.fn.gridEditor.t = translate;
4848
5002
  * Every key is listed in docs/locale-keys.md, which test/locales.js holds to
4849
5003
  * this catalogue in both directions.
4850
5004
  */
4851
- $.fn.gridEditor.locales = {
5005
+ GridEditor.locales = {
4852
5006
  en: {
4853
5007
  'tool.move': 'Move',
4854
5008
  'tool.settings': 'Settings',
@@ -4918,10 +5072,16 @@ $.fn.gridEditor.locales = {
4918
5072
  'utility.varies': 'Changes at {breakpoints}; choosing here replaces that',
4919
5073
  'error.sortable_missing': 'SortableJS not available! Make sure you loaded the Sortable js file; dragging is off without it.',
4920
5074
  'warning.setting_removed': 'The {setting} setting was removed in 4.0. Use {replacement} instead.',
5075
+ 'warning.already_editing': 'This element already has an editor: that one is handed back, with the options it was made with.',
5076
+ 'warning.destroyed': '{method}() was called on an editor that has been destroyed, and does nothing.',
5077
+ '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
+ 'warning.adapter_no_jquery': 'grideditor.jquery.js needs jQuery 4, and there is no jQuery on the page: the jQuery API is not there.',
4921
5080
  'error.tinymce_missing': 'tinyMCE not available! Make sure you loaded the tinyMCE js file.',
4922
- 'error.ckeditor_missing': 'CKEditor not available! Make sure you loaded the ckeditor and jquery adapter js files.',
4923
- 'error.summernote_missing': 'Summernote not available! Make sure you loaded the Summernote js file.',
5081
+ 'error.ckeditor_missing': 'CKEditor not available! Make sure you loaded the CKEditor js file.',
5082
+ 'error.summernote_missing': 'Summernote not available! Make sure you loaded jQuery and the Summernote js file.',
4924
5083
  },
4925
5084
  };
4926
5085
 
4927
- })( jQuery );
5086
+ export { GridEditor };
5087
+ export default GridEditor;