@themarioga/grid-editor 8.3.0 → 8.5.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 (45) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +43 -1
  3. package/dist/grideditor.bundle.min.js +3 -3
  4. package/dist/grideditor.css +49 -0
  5. package/dist/grideditor.d.ts +21 -1
  6. package/dist/grideditor.esm.js +211 -9
  7. package/dist/grideditor.js +211 -9
  8. package/dist/grideditor.min.css +1 -1
  9. package/dist/grideditor.min.css.map +1 -1
  10. package/dist/grideditor.min.js +2 -2
  11. package/dist/grideditor.min.js.map +3 -3
  12. package/dist/locales/grideditor.es.esm.js +4 -0
  13. package/dist/locales/grideditor.es.js +4 -0
  14. package/dist/locales/grideditor.es.min.js +1 -1
  15. package/dist/locales/grideditor.es.min.js.map +2 -2
  16. package/dist/plugins/grideditor.ckeditor.esm.js +1 -0
  17. package/dist/plugins/grideditor.ckeditor.js +1 -0
  18. package/dist/plugins/grideditor.ckeditor.min.js +1 -1
  19. package/dist/plugins/grideditor.ckeditor.min.js.map +2 -2
  20. package/dist/plugins/grideditor.elements.esm.js +72 -1
  21. package/dist/plugins/grideditor.elements.js +72 -1
  22. package/dist/plugins/grideditor.elements.min.js +1 -1
  23. package/dist/plugins/grideditor.elements.min.js.map +3 -3
  24. package/dist/plugins/grideditor.sections.esm.js +1 -0
  25. package/dist/plugins/grideditor.sections.js +1 -0
  26. package/dist/plugins/grideditor.sections.min.js +1 -1
  27. package/dist/plugins/grideditor.sections.min.js.map +2 -2
  28. package/dist/plugins/grideditor.summernote.esm.js +1 -0
  29. package/dist/plugins/grideditor.summernote.js +1 -0
  30. package/dist/plugins/grideditor.summernote.min.js +1 -1
  31. package/dist/plugins/grideditor.summernote.min.js.map +2 -2
  32. package/dist/plugins/grideditor.tinymce.esm.js +1 -0
  33. package/dist/plugins/grideditor.tinymce.js +1 -0
  34. package/dist/plugins/grideditor.tinymce.min.js +1 -1
  35. package/dist/plugins/grideditor.tinymce.min.js.map +2 -2
  36. package/docs/locale-keys.md +5 -1
  37. package/docs/plugins.md +24 -1
  38. package/package.json +1 -1
  39. package/src/js/grideditor.js +252 -8
  40. package/src/js/locales/grideditor.es.js +4 -0
  41. package/src/js/plugins/grideditor.elements.js +94 -1
  42. package/src/js/plugins/grideditor.sections.js +1 -0
  43. package/src/js/text/grideditor.text.js +1 -0
  44. package/src/less/grideditor.less +58 -0
  45. package/types/grideditor.d.ts +21 -1
@@ -46,7 +46,11 @@ The tools in a drawer, and the buttons in the toolbar above the canvas.
46
46
  | `tool.indent_increase` | Increase indent\n(hold shift for max) | Column drawer, adds offset units |
47
47
  | `tool.edit_source` | Edit Source Code | Toolbar, swaps the canvas for its html |
48
48
  | `tool.preview` | Preview | Toolbar, hides the editing furniture while held |
49
- | `tool.more` | More | Toolbar, the button that opens the add buttons that don't fit on its line |
49
+ | `tool.more` | More | Toolbar, the button that opens the add buttons that don't fit on its line; drawers, the tool that unfolds the ones that don't fit on theirs |
50
+ | `group.rows` | Rows | Toolbar, with `toolbar_groups`: the tab of the add row buttons |
51
+ | `group.content` | Content | Toolbar, with `toolbar_groups`: the tab of the containers, the sections and the plugins' buttons that join it |
52
+ | `group.elements` | Elements | Toolbar, with `toolbar_groups`: the tab of the texts, the elements of `elements.types` and the plugins' buttons that join it |
53
+ | `group.select` | Add | Toolbar, with `toolbar_groups`: the accessible name of the tabs |
50
54
  | `tool.id_placeholder` | id | Placeholder of the id input in the settings panel |
51
55
  | `tool.id_title` | Set a unique identifier | Tooltip of that input |
52
56
  | `tool.classes_placeholder` | classes | Placeholder of the css class input beside it |
package/docs/plugins.md CHANGED
@@ -62,6 +62,7 @@ GridEditor.containers.carousel = function(ge) {
62
62
  return {
63
63
  labelKey: 'container.add_carousel', // the toolbar button's label
64
64
  iconClass: 'bi bi-images', // optional: the button shows it in place of the label
65
+ group: 'media', // optional: its toolbar tab, with toolbar_groups
65
66
  addPaneKey: 'container.add_slide', // the drawer's add pane tool
66
67
  paneKind: 'slide', // the kind its panes report
67
68
 
@@ -100,6 +101,10 @@ written for 6.x.
100
101
  - **`iconClass`**, when given, is the toolbar button's face: the icon alone,
101
102
  with the label as its title. Without one the button shows a plus and the
102
103
  label.
104
+ - **`group`**, when given, is the toolbar tab the button goes in when the
105
+ host turns on `toolbar_groups`. Without one a container goes in `content`,
106
+ with the texts. See the `toolbar` item's `group` below for how tabs are
107
+ named and labelled.
103
108
  - **`create(options)`** returns a detached container. It must carry
104
109
  `data-ge-container="<type>"`: that attribute, and never a class, is how the
105
110
  editor recognises one.
@@ -218,7 +223,8 @@ needs to put a new kind of block on the canvas:
218
223
  - **`toolbar`** adds buttons beside the containers'. A click appends what
219
224
  `create()` returns to the canvas, announced as `kind`; a drop puts it where it
220
225
  was dropped, or, when that region will not have it, on the canvas just after
221
- the block it was dropped in.
226
+ the block it was dropped in. A `create()` that returns `null` adds nothing:
227
+ that is how a plugin says it could not make one this time.
222
228
 
223
229
  - **`drawerTools(drawer, node, kind)`** runs for every drawer that has a gear,
224
230
  as a utility plugin's does, so a tool can go on any node. The clipboard
@@ -280,6 +286,23 @@ rather than remembering anything about it.
280
286
  container or a text does: a click puts it in a row and a column of its own
281
287
  at the end of the canvas, and a drop anywhere but in a column does the
282
288
  same where it was dropped.
289
+ - A **`toolbar`** item's **`group`** is the tab it goes in when the host turns
290
+ on `toolbar_groups`. The core has three: `rows` - the add row buttons -,
291
+ `content` - the containers and the sections - and `elements` - the texts
292
+ and the elements of `elements.types` - and an item may join any of them. Any other name is a tab of its own, shared by every item, of
293
+ any plugin, that names it. An item without a `group` goes in a tab named
294
+ after its plugin. A tab's label is the `group.<name>` locale string, which
295
+ the plugin registers with its others, or its first button's label when
296
+ there is none. Tabs come in this order: `rows`, `content`, `elements`, then the others
297
+ as their first button comes. `group` is ignored with `align: 'end'`: the
298
+ buttons on the right are not grouped.
299
+
300
+ ```javascript
301
+ Object.assign(GridEditor.locales.en, { 'group.media': 'Media' });
302
+ GridEditor.features.gallery = function(ge) {
303
+ return { toolbar: [{ labelKey: 'gallery.add', kind: 'gallery', group: 'media', create: … }] };
304
+ };
305
+ ```
283
306
 
284
307
  - **`onSortable(sortable)`** is handed the function the editor makes all of its
285
308
  own lists with. A plugin describes a list; it never touches the drag toolkit
package/package.json CHANGED
@@ -33,7 +33,7 @@
33
33
  "comment": "A scoped package publishes restricted unless it is told otherwise.",
34
34
  "access": "public"
35
35
  },
36
- "version": "8.3.0",
36
+ "version": "8.5.0",
37
37
  "main": "dist/grideditor.js",
38
38
  "module": "dist/grideditor.esm.js",
39
39
  "types": "dist/grideditor.d.ts",
@@ -142,6 +142,7 @@ var NESTED_SETTINGS = {
142
142
  enabled: 'auto', // 'auto' turns them on when the page has any
143
143
  selector: '[data-ge-element]', // What the host marks an element with
144
144
  auto: false, // Treat every child of a content area as an element
145
+ types: [], // The elements the toolbar offers: { type, html, label | labelKey, iconClass, group }
145
146
  },
146
147
  resize: {
147
148
  enabled: true, // The handle on the column's edge
@@ -198,6 +199,15 @@ function translate(settings, key, params) {
198
199
  });
199
200
  }
200
201
 
202
+ /** Whether some locale has a string for the key, asked without the warning a missing one gets. */
203
+ function hasString(settings, key) {
204
+ var locales = GridEditor.locales;
205
+
206
+ return (settings.locale_strings || {})[key] !== undefined ||
207
+ (locales[settings.locale] || {})[key] !== undefined ||
208
+ locales.en[key] !== undefined;
209
+ }
210
+
201
211
  function warn(message) {
202
212
  if (window.console && window.console.warn) {
203
213
  window.console.warn('grid-editor: ' + message);
@@ -367,6 +377,8 @@ function build(instance, baseElem, optionsOrMethod) {
367
377
  'drag_handle' : 'tool', // 'tool' for the move tool, 'drawer' for the whole drawer
368
378
  'toolbar_drag' : 'auto', // Drag the toolbar's buttons onto the canvas. 'auto' follows drag_handle
369
379
  'toolbar_overflow' : 'menu', // What doesn't fit on one line: 'menu' behind a button, 'wrap' onto another line
380
+ 'drawer_overflow' : 'menu', // The same for the drawers' tools: 'menu' behind a button, 'wrap' onto another line
381
+ 'toolbar_groups' : false, // The add buttons in categories, one shown at a time, picked with tabs
370
382
  'active_target' : false, // A click in a column makes it where the toolbar's buttons add
371
383
  'element_tools' : [], // Host tools on element drawers, same shape as row_tools
372
384
  'element_classes' : [], // Preset class toggles on an element's settings panel
@@ -423,6 +435,9 @@ function build(instance, baseElem, optionsOrMethod) {
423
435
  var activeTarget = null; // The column or region the toolbar adds to, with active_target
424
436
  var toolbarLifetime = null; // Aborted when the toolbar is rebuilt or taken away
425
437
  var closeOverflowMenu = function() {}; // Folds the toolbar's overflow menu away, when it has one
438
+ var refitToolbar = function() {}; // Works out again what the toolbar's overflow menu holds, when it has one
439
+ var addButtons = []; // The toolbar's add buttons, in toolbar order: { button, home, group }
440
+ var toolbarGroup = null; // The category shown, with toolbar_groups. Kept when the toolbar is rebuilt
426
441
  var warnedHere = {}; // Some warnings are worth saying once per instance, not once per call
427
442
  var sortables = []; // Every list made sortable, so deinit destroys exactly those
428
443
  var instanceId = ++editorCounter; // Scopes the sortable groups to this editor
@@ -972,6 +987,7 @@ function build(instance, baseElem, optionsOrMethod) {
972
987
  canvas.parentNode.insertBefore(htmlTextArea, canvas);
973
988
 
974
989
  createMainControls();
990
+ watchDrawers();
975
991
 
976
992
  var signal = { signal: lifetime.signal };
977
993
 
@@ -1003,6 +1019,16 @@ function build(instance, baseElem, optionsOrMethod) {
1003
1019
  if (!dom.hasClass(canvas, 'ge-editing')) { return; }
1004
1020
  if (from && Math.abs(e.clientX - from.x) + Math.abs(e.clientY - from.y) > settings.drag.threshold) { return; }
1005
1021
  var region = dom.closest(e.target, targetSelector());
1022
+
1023
+ // The target's own background, or its drawer's, clicked
1024
+ // again takes it back. What is in it - a text, a tool, a
1025
+ // nested column - keeps it, or makes that the target
1026
+ if (region && region === activeTarget &&
1027
+ (e.target === region || e.target === dom.child(region, '.ge-tools-drawer'))) {
1028
+ changeTarget(null);
1029
+ return;
1030
+ }
1031
+
1006
1032
  changeTarget(region && region !== canvas && canvas.contains(region) ? region : null);
1007
1033
  }, signal);
1008
1034
 
@@ -1047,6 +1073,8 @@ function build(instance, baseElem, optionsOrMethod) {
1047
1073
  start = wrapper.appendChild(dom.element('div', { 'class': 'ge-toolbar-start' }));
1048
1074
  }
1049
1075
 
1076
+ addButtons = [];
1077
+
1050
1078
  // Add row
1051
1079
  addRowGroup = start.appendChild(dom.element('div', { 'class': 'ge-addRowGroup btn-group' }));
1052
1080
  addContainerGroup = dom.element('div', { 'class': 'ge-addContainerGroup btn-group ms-1' });
@@ -1077,6 +1105,7 @@ function build(instance, baseElem, optionsOrMethod) {
1077
1105
  }
1078
1106
  });
1079
1107
  addRowGroup.appendChild(btn);
1108
+ addButtons.push({ button: btn, home: addRowGroup, group: 'rows' });
1080
1109
 
1081
1110
  btn.appendChild(dom.create('<i class="bi bi-plus"></i>'));
1082
1111
 
@@ -1113,6 +1142,7 @@ function build(instance, baseElem, optionsOrMethod) {
1113
1142
  }, { parent: canvas, source: 'tool' });
1114
1143
  });
1115
1144
  addContainerGroup.appendChild(button);
1145
+ addButtons.push({ button: button, home: addContainerGroup, group: toolbarGroupOf(definition.group) || 'content' });
1116
1146
  });
1117
1147
 
1118
1148
  // A feature plugin's own buttons, beside the containers': what
@@ -1128,6 +1158,8 @@ function build(instance, baseElem, optionsOrMethod) {
1128
1158
  endItems.push(button);
1129
1159
  } else {
1130
1160
  addContainerGroup.appendChild(button);
1161
+ // One that names no category of its own has its plugin's
1162
+ addButtons.push({ button: button, home: addContainerGroup, group: toolbarGroupOf(item.group) || name });
1131
1163
  }
1132
1164
  });
1133
1165
  });
@@ -1198,16 +1230,97 @@ function build(instance, baseElem, optionsOrMethod) {
1198
1230
  endItems.forEach(function(button) { end.appendChild(button); });
1199
1231
  }
1200
1232
 
1233
+ if (settings.toolbar_groups) { createGroupTabs(start); }
1201
1234
  if (overflowMenu) { createOverflowMenu(start); }
1202
1235
 
1203
1236
  makeToolbarDraggable();
1204
1237
  }
1205
1238
 
1239
+ /** The core's categories, ahead of the plugins' own. */
1240
+ var CORE_GROUPS = [
1241
+ { name: 'rows', labelKey: 'group.rows' },
1242
+ { name: 'content', labelKey: 'group.content' },
1243
+ { name: 'elements', labelKey: 'group.elements' },
1244
+ ];
1245
+
1246
+ /** A plugin's category, when it names one. */
1247
+ function toolbarGroupOf(value) {
1248
+ return typeof value === 'string' && value.trim() !== '' ? value : null;
1249
+ }
1250
+
1251
+ /**
1252
+ * toolbar_groups: tabs at the start of the toolbar, one per category
1253
+ * of add buttons, and only the chosen one's buttons on it. The rest
1254
+ * wait in a hidden stash inside the toolbar, so toolbarItems still
1255
+ * finds them and the btn-groups round the corners of what shows. With
1256
+ * a single category there is nothing to choose, and no tabs.
1257
+ */
1258
+ function createGroupTabs(start) {
1259
+ // The core's two first, then the rest as their first button comes
1260
+ var names = CORE_GROUPS.map(function(group) { return group.name; });
1261
+ addButtons.forEach(function(entry) {
1262
+ if (names.indexOf(entry.group) < 0) { names.push(entry.group); }
1263
+ });
1264
+ names = names.filter(function(name) {
1265
+ return addButtons.some(function(entry) { return entry.group === name; });
1266
+ });
1267
+
1268
+ if (names.length < 2) { return; }
1269
+
1270
+ var tabs = dom.element('div', { 'class': 'ge-toolbar-groups btn-group', role: 'tablist', 'aria-label': t('group.select') });
1271
+ start.insertBefore(tabs, start.firstChild);
1272
+ var stash = mainControls.appendChild(dom.element('div', { 'class': 'ge-toolbar-stash', hidden: 'hidden' }));
1273
+
1274
+ names.forEach(function(name) {
1275
+ var tab = tabs.appendChild(dom.element('button', {
1276
+ type: 'button',
1277
+ 'class': 'btn btn-sm btn-outline-primary',
1278
+ role: 'tab',
1279
+ 'data-ge-group': name,
1280
+ }, groupLabel(name)));
1281
+
1282
+ tab.addEventListener('click', function() { showGroup(name); });
1283
+ });
1284
+
1285
+ showGroup(names.indexOf(toolbarGroup) >= 0 ? toolbarGroup : names[0]);
1286
+
1287
+ function showGroup(name) {
1288
+ toolbarGroup = name;
1289
+
1290
+ addButtons.forEach(function(entry) {
1291
+ (entry.group === name ? entry.home : stash).appendChild(entry.button);
1292
+ });
1293
+
1294
+ dom.children(tabs).forEach(function(tab) {
1295
+ var chosen = tab.getAttribute('data-ge-group') === name;
1296
+ dom.toggleClass(tab, 'active', chosen);
1297
+ tab.setAttribute('aria-selected', chosen ? 'true' : 'false');
1298
+ });
1299
+
1300
+ refitToolbar();
1301
+ }
1302
+ }
1303
+
1304
+ /** A category's label: its group.<name> string, or its first button's when it has none. */
1305
+ function groupLabel(name) {
1306
+ var key = 'group.' + name;
1307
+ if (hasString(settings, key)) { return t(key); }
1308
+
1309
+ var first = addButtons.filter(function(entry) { return entry.group === name; })[0];
1310
+ return first.button.getAttribute('title') || name;
1311
+ }
1312
+
1313
+ /** The add buttons on the toolbar's line: all of them, or the chosen category's. */
1314
+ function shownAddButtons() {
1315
+ return addButtons.filter(function(entry) { return !dom.closest(entry.button, '.ge-toolbar-stash'); });
1316
+ }
1317
+
1206
1318
  /** The toolbar off the page, with what keeps its overflow menu up to date. */
1207
1319
  function removeMainControls() {
1208
1320
  if (toolbarLifetime) { toolbarLifetime.abort(); }
1209
1321
  toolbarLifetime = null;
1210
1322
  closeOverflowMenu = function() {};
1323
+ refitToolbar = function() {};
1211
1324
  mainControls.remove();
1212
1325
  }
1213
1326
 
@@ -1231,13 +1344,10 @@ function build(instance, baseElem, optionsOrMethod) {
1231
1344
  var toggle = dom.one(more, 'button');
1232
1345
  var panel = dom.one(more, '.ge-toolbar-overflow');
1233
1346
  var spares = dom.all(panel, '.btn-group');
1234
- // Every add button with the group it belongs in, in toolbar order
1235
- var homes = [];
1236
- [addRowGroup, addContainerGroup].forEach(function(group, index) {
1237
- Array.prototype.slice.call(group.children).forEach(function(button) {
1238
- homes.push({ button: button, home: group, spare: spares[index] });
1239
- });
1240
- });
1347
+ // The spare group in the menu for each group on the line
1348
+ function spareOf(home) {
1349
+ return home === addRowGroup ? spares[0] : spares[1];
1350
+ }
1241
1351
 
1242
1352
  toggle.setAttribute('title', t('tool.more'));
1243
1353
  toggle.setAttribute('aria-haspopup', 'true');
@@ -1258,6 +1368,8 @@ function build(instance, baseElem, optionsOrMethod) {
1258
1368
  if (e.key === 'Escape') { openOverflow(false); }
1259
1369
  }, signal);
1260
1370
  closeOverflowMenu = function() { openOverflow(false); };
1371
+ // A category chosen: another set of buttons to fit
1372
+ refitToolbar = fit;
1261
1373
 
1262
1374
  fit();
1263
1375
 
@@ -1288,6 +1400,9 @@ function build(instance, baseElem, optionsOrMethod) {
1288
1400
 
1289
1401
  /** Back on the line, then the last ones off it until the rest fit. */
1290
1402
  function fit() {
1403
+ // Only the chosen category's, with toolbar_groups: the others
1404
+ // wait in their stash
1405
+ var homes = shownAddButtons();
1291
1406
  homes.forEach(function(entry) { entry.home.appendChild(entry.button); });
1292
1407
  dom.removeClass(more, 'ge-needed');
1293
1408
 
@@ -1298,7 +1413,8 @@ function build(instance, baseElem, optionsOrMethod) {
1298
1413
 
1299
1414
  dom.addClass(more, 'ge-needed');
1300
1415
  for (var i = homes.length - 1; i >= 0 && start.scrollWidth > start.clientWidth; i--) {
1301
- homes[i].spare.insertBefore(homes[i].button, homes[i].spare.firstChild);
1416
+ var into = spareOf(homes[i].home);
1417
+ into.insertBefore(homes[i].button, into.firstChild);
1302
1418
  }
1303
1419
  }
1304
1420
  }
@@ -1338,6 +1454,8 @@ function build(instance, baseElem, optionsOrMethod) {
1338
1454
  if (activeTarget) { addToTarget(button, item.source || 'tool'); return; }
1339
1455
 
1340
1456
  var made = item.create();
1457
+ // A plugin that could not make one says so with null
1458
+ if (!made) { return; }
1341
1459
 
1342
1460
  // One that belongs in a column brings a row and a column
1343
1461
  // of its own, the way a container does
@@ -1497,6 +1615,7 @@ function build(instance, baseElem, optionsOrMethod) {
1497
1615
  function insertFeatureFromToolbar(button, where, source) {
1498
1616
  var item = FEATURES[button.getAttribute('data-ge-feature')].toolbar[parseInt(button.getAttribute('data-ge-item'), 10)];
1499
1617
  var made = item.create();
1618
+ if (!made) { return null; }
1500
1619
  var placed = made;
1501
1620
 
1502
1621
  if (item.inColumn) {
@@ -3330,6 +3449,127 @@ function build(instance, baseElem, optionsOrMethod) {
3330
3449
  return createTool(drawer, t('tool.move'), 'ge-move', 'bi bi-arrows-move');
3331
3450
  }
3332
3451
 
3452
+ /**
3453
+ * The drawers that span their node - a row's, a column's, a
3454
+ * container's, an element's, a section's - keep their tools to one
3455
+ * line. The tools that don't fit are hidden from the end, and a more
3456
+ * tool at the drawer's end unfolds it to show them all, wrapping as
3457
+ * it would otherwise. They stay where they are, so whatever finds a
3458
+ * tool in its drawer, or hangs a picker off it, still does.
3459
+ *
3460
+ * A text's drawer sits over the text, as wide as its tools, and a
3461
+ * pane's sits by its label: neither is one of them.
3462
+ */
3463
+ var FITTED_DRAWERS = '.ge-tools-drawer:not(.ge-text-drawer):not(.ge-pane-drawer):not(.ge-code-inline)';
3464
+
3465
+ /**
3466
+ * Fit every drawer as it comes onto the canvas, and again when it
3467
+ * changes width or a tool is added to it, shown or hidden. Plugins
3468
+ * add tools at their own moments, so this watches rather than
3469
+ * waiting to be told.
3470
+ */
3471
+ function watchDrawers() {
3472
+ if (settings.drawer_overflow === 'wrap' || !window.ResizeObserver || !window.MutationObserver) { return; }
3473
+
3474
+ var due = new Set();
3475
+ var frame = null;
3476
+
3477
+ // Once a frame, after the layout: moving tools in the observers'
3478
+ // callbacks would resize what they are watching
3479
+ function schedule(drawer) {
3480
+ due.add(drawer);
3481
+ if (frame === null) { frame = window.requestAnimationFrame(flush); }
3482
+ }
3483
+
3484
+ function flush() {
3485
+ frame = null;
3486
+ var drawers = Array.from(due);
3487
+ due.clear();
3488
+ drawers.forEach(function(drawer) {
3489
+ if (!drawer.isConnected) {
3490
+ drawerResizes.unobserve(drawer);
3491
+ return;
3492
+ }
3493
+ fitDrawer(drawer);
3494
+ });
3495
+ }
3496
+
3497
+ var drawerResizes = new window.ResizeObserver(function(entries) {
3498
+ entries.forEach(function(entry) { schedule(entry.target); });
3499
+ });
3500
+
3501
+ var mutations = new window.MutationObserver(function(records) {
3502
+ records.forEach(function(record) {
3503
+ var target = record.target;
3504
+
3505
+ // A tool shown or hidden
3506
+ if (record.type === 'attributes') {
3507
+ var parent = target.parentNode;
3508
+ if (parent && parent.nodeType === 1 && parent.matches(FITTED_DRAWERS)) { schedule(parent); }
3509
+ return;
3510
+ }
3511
+
3512
+ // A tool added
3513
+ if (target.nodeType === 1 && target.matches(FITTED_DRAWERS)) {
3514
+ schedule(target);
3515
+ return;
3516
+ }
3517
+
3518
+ record.addedNodes.forEach(function(node) {
3519
+ if (node.nodeType !== 1) { return; }
3520
+ dom.selfAndAll(node, FITTED_DRAWERS).forEach(function(drawer) {
3521
+ dom.addClass(drawer, 'ge-drawer-fit');
3522
+ drawerResizes.observe(drawer);
3523
+ schedule(drawer);
3524
+ });
3525
+ });
3526
+ });
3527
+ });
3528
+ mutations.observe(canvas, { childList: true, subtree: true, attributes: true, attributeFilter: ['style'] });
3529
+
3530
+ lifetime.signal.addEventListener('abort', function() {
3531
+ mutations.disconnect();
3532
+ drawerResizes.disconnect();
3533
+ if (frame !== null) { window.cancelAnimationFrame(frame); }
3534
+ });
3535
+ }
3536
+
3537
+ /** Hide the tools that don't fit on the drawer's line, from the end, behind its more tool. */
3538
+ function fitDrawer(drawer) {
3539
+ var tools = dom.children(drawer, 'a').filter(function(tool) { return !dom.hasClass(tool, 'ge-drawer-more'); });
3540
+ var expanded = dom.hasClass(drawer, 'ge-drawer-expanded');
3541
+
3542
+ // Measured on one line, with every tool back on it
3543
+ dom.removeClass(drawer, 'ge-drawer-overflow ge-drawer-expanded');
3544
+ tools.forEach(function(tool) { dom.removeClass(tool, 'ge-tool-overflow'); });
3545
+
3546
+ if (!drawerOverflows(drawer, tools)) { return; }
3547
+
3548
+ dom.addClass(drawer, 'ge-drawer-overflow');
3549
+ if (!dom.child(drawer, '.ge-drawer-more')) {
3550
+ createTool(drawer, t('tool.more'), 'ge-drawer-more', 'bi bi-three-dots', function() {
3551
+ dom.toggleClass(drawer, 'ge-drawer-expanded');
3552
+ });
3553
+ }
3554
+
3555
+ for (var i = tools.length - 1; i >= 0 && drawerOverflows(drawer, tools); i--) {
3556
+ dom.addClass(tools[i], 'ge-tool-overflow');
3557
+ }
3558
+
3559
+ dom.toggleClass(drawer, 'ge-drawer-expanded', expanded);
3560
+ }
3561
+
3562
+ function drawerOverflows(drawer, tools) {
3563
+ var style = window.getComputedStyle(drawer);
3564
+ var limit = drawer.getBoundingClientRect().right -
3565
+ parseFloat(style.borderRightWidth) - parseFloat(style.paddingRight);
3566
+
3567
+ return tools.some(function(tool) {
3568
+ var rects = tool.getClientRects();
3569
+ return rects.length > 0 && rects[0].right > limit + 0.5;
3570
+ });
3571
+ }
3572
+
3333
3573
  /**
3334
3574
  * A tool in a drawer: a link with an icon, and what it does. The
3335
3575
  * handlers are a click handler, or { eventName: handler }; each gets
@@ -5533,6 +5773,10 @@ GridEditor.locales = {
5533
5773
  'tool.edit_source': 'Edit Source Code',
5534
5774
  'tool.preview': 'Preview',
5535
5775
  'tool.more': 'More',
5776
+ 'group.rows': 'Rows',
5777
+ 'group.content': 'Content',
5778
+ 'group.elements': 'Elements',
5779
+ 'group.select': 'Add',
5536
5780
  'panel.title': '{kind} settings',
5537
5781
  'panel.close': 'Close',
5538
5782
  'panel.done': 'Done',
@@ -52,6 +52,10 @@ GridEditor.locales.es = {
52
52
  'tool.edit_source': 'Editar el código fuente',
53
53
  'tool.preview': 'Vista previa',
54
54
  'tool.more': 'Más',
55
+ 'group.rows': 'Filas',
56
+ 'group.content': 'Contenido',
57
+ 'group.elements': 'Elementos',
58
+ 'group.select': 'Añadir',
55
59
  'panel.title': 'Ajustes: {kind}',
56
60
  'panel.close': 'Cerrar',
57
61
  'panel.done': 'Hecho',
@@ -28,11 +28,95 @@ var NOT_ELEMENTS = '.row, .ge-content, .ge-text-block, [data-ge-container], .ge-
28
28
  GridEditor.features.elements = function(ge) {
29
29
 
30
30
  var warnedIntoText = false;
31
+ var types = validTypes();
31
32
 
32
33
  function elementsEnabled() {
33
34
  if (ge.settings.elements.enabled !== 'auto') { return !!ge.settings.elements.enabled; }
34
35
 
35
- return ge.settings.elements.auto || !!ge.canvas.querySelector(ge.settings.elements.selector);
36
+ // A host that offers elements on the toolbar uses them, even on a
37
+ // page that has none yet
38
+ return ge.settings.elements.auto || types.length > 0 ||
39
+ !!ge.canvas.querySelector(ge.settings.elements.selector);
40
+ }
41
+
42
+ /** The elements.types the toolbar can offer. One that can't be made is left out, with a word to the host. */
43
+ function validTypes() {
44
+ return (ge.settings.elements.types || []).filter(function(type, index) {
45
+ var named = type && typeof type.type === 'string' && type.type.trim() !== '';
46
+ var html = type && (typeof type.html === 'function' || (typeof type.html === 'string' && type.html.trim() !== ''));
47
+
48
+ if (!named || !html) {
49
+ ge.warn('elements.types[' + index + ']: a type needs a type name and its html, as a string or a ' +
50
+ 'function; it is left off the toolbar.');
51
+ }
52
+
53
+ return named && html;
54
+ });
55
+ }
56
+
57
+ /** A type's toolbar button. */
58
+ function typeItem(type) {
59
+ var mismatched = false;
60
+
61
+ return {
62
+ // Asked again whenever the toolbar is built, so setLocale
63
+ // translates a labelKey
64
+ label: function() { return labelOf(type); },
65
+ iconClass: type.iconClass,
66
+ group: type.group || 'elements',
67
+ kind: 'element',
68
+ inColumn: true,
69
+ create: function() {
70
+ var element = makeElement(type);
71
+
72
+ if (element && !mismatched && !ge.settings.elements.auto && !element.matches(ge.settings.elements.selector)) {
73
+ mismatched = true;
74
+ ge.warn('elements.types "' + type.type + '": what it makes does not match elements.selector "' +
75
+ ge.settings.elements.selector + '", so it will not be an element once the editor looks again.');
76
+ }
77
+
78
+ return element;
79
+ },
80
+ };
81
+ }
82
+
83
+ function labelOf(type) {
84
+ if (type.labelKey) { return ge.t(type.labelKey); }
85
+
86
+ return type.label || type.type;
87
+ }
88
+
89
+ /**
90
+ * What a type's button makes: the root of its html, marked as the host
91
+ * marks its own, or a div around it when it has no single root. Null,
92
+ * with a word to the host, when there is nothing to make.
93
+ */
94
+ function makeElement(type) {
95
+ var made;
96
+
97
+ try {
98
+ made = typeof type.html === 'function' ? type.html() : type.html;
99
+ } catch (error) {
100
+ ge.warn('elements.types "' + type.type + '": its html function threw (' + error.message + '); nothing was added.');
101
+ return null;
102
+ }
103
+
104
+ var nodes = typeof made === 'string' ? dom.parse(made) : (made && made.nodeType ? [made] : []);
105
+ // Whitespace between tags is not content
106
+ nodes = nodes.filter(function(node) { return node.nodeType !== 3 || node.textContent.trim() !== ''; });
107
+
108
+ if (!nodes.length) {
109
+ ge.warn('elements.types "' + type.type + '": its html came out empty; nothing was added.');
110
+ return null;
111
+ }
112
+
113
+ var element = nodes.length === 1 && nodes[0].nodeType === 1 ? nodes[0] : dom.element('div');
114
+ if (element !== nodes[0] || nodes.length > 1) { fill(element, nodes); }
115
+
116
+ element.setAttribute('data-ge-element', type.type);
117
+ if (!element.hasAttribute('data-ge-label')) { element.setAttribute('data-ge-label', labelOf(type)); }
118
+
119
+ return element;
36
120
  }
37
121
 
38
122
  /** Whether a child of a column is an element. */
@@ -121,6 +205,11 @@ GridEditor.features.elements = function(ge) {
121
205
  return placed;
122
206
  }
123
207
 
208
+ /** enabled: false takes the toolbar's element buttons away too. */
209
+ function elementsOff() {
210
+ return ge.settings.elements.enabled === false;
211
+ }
212
+
124
213
  /** A place a caller named: an element, or the first one a selector matches. */
125
214
  function nodeFrom(node) {
126
215
  if (typeof node === 'string') { return document.querySelector(node); }
@@ -163,6 +252,10 @@ GridEditor.features.elements = function(ge) {
163
252
  createElement: apiCreateElement,
164
253
  },
165
254
 
255
+ // A button for each type the host offers, in the elements category
256
+ // unless the type names another
257
+ toolbar: elementsOff() ? [] : types.map(typeItem),
258
+
166
259
  /** A node this plugin marked is an element, whatever else it is. */
167
260
  kindOf: function(node) {
168
261
  return dom.hasClass(node, 'ge-element') ? 'element' : null;
@@ -192,6 +192,7 @@ GridEditor.features.sections = function(ge) {
192
192
  toolbar: [{
193
193
  labelKey: 'section.add',
194
194
  kind: 'section',
195
+ group: 'content',
195
196
  create: function() { return createSection(); },
196
197
  }],
197
198
 
@@ -321,6 +321,7 @@ if (!GridEditor.features.text) {
321
321
  iconClass: TEXTS[type].iconClass,
322
322
  className: 'ge-add-text-button',
323
323
  kind: 'text',
324
+ group: 'elements',
324
325
  inColumn: true,
325
326
  create: function() { return makeText(type); },
326
327
  };