@themarioga/grid-editor 8.2.0 → 8.4.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 (40) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +25 -1
  3. package/dist/grideditor.bundle.min.js +3 -3
  4. package/dist/grideditor.css +116 -0
  5. package/dist/grideditor.d.ts +6 -0
  6. package/dist/grideditor.esm.js +301 -5
  7. package/dist/grideditor.js +301 -5
  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.sections.esm.js +1 -0
  21. package/dist/plugins/grideditor.sections.js +1 -0
  22. package/dist/plugins/grideditor.sections.min.js +1 -1
  23. package/dist/plugins/grideditor.sections.min.js.map +2 -2
  24. package/dist/plugins/grideditor.summernote.esm.js +1 -0
  25. package/dist/plugins/grideditor.summernote.js +1 -0
  26. package/dist/plugins/grideditor.summernote.min.js +1 -1
  27. package/dist/plugins/grideditor.summernote.min.js.map +2 -2
  28. package/dist/plugins/grideditor.tinymce.esm.js +1 -0
  29. package/dist/plugins/grideditor.tinymce.js +1 -0
  30. package/dist/plugins/grideditor.tinymce.min.js +1 -1
  31. package/dist/plugins/grideditor.tinymce.min.js.map +2 -2
  32. package/docs/locale-keys.md +4 -0
  33. package/docs/plugins.md +22 -0
  34. package/package.json +1 -1
  35. package/src/js/grideditor.js +359 -4
  36. package/src/js/locales/grideditor.es.js +4 -0
  37. package/src/js/plugins/grideditor.sections.js +1 -0
  38. package/src/js/text/grideditor.text.js +1 -0
  39. package/src/less/grideditor.less +134 -0
  40. package/types/grideditor.d.ts +6 -0
@@ -198,6 +198,15 @@ function translate(settings, key, params) {
198
198
  });
199
199
  }
200
200
 
201
+ /** Whether some locale has a string for the key, asked without the warning a missing one gets. */
202
+ function hasString(settings, key) {
203
+ var locales = GridEditor.locales;
204
+
205
+ return (settings.locale_strings || {})[key] !== undefined ||
206
+ (locales[settings.locale] || {})[key] !== undefined ||
207
+ locales.en[key] !== undefined;
208
+ }
209
+
201
210
  function warn(message) {
202
211
  if (window.console && window.console.warn) {
203
212
  window.console.warn('grid-editor: ' + message);
@@ -366,6 +375,9 @@ function build(instance, baseElem, optionsOrMethod) {
366
375
  'row_tools' : [],
367
376
  'drag_handle' : 'tool', // 'tool' for the move tool, 'drawer' for the whole drawer
368
377
  'toolbar_drag' : 'auto', // Drag the toolbar's buttons onto the canvas. 'auto' follows drag_handle
378
+ 'toolbar_overflow' : 'menu', // What doesn't fit on one line: 'menu' behind a button, 'wrap' onto another line
379
+ 'drawer_overflow' : 'menu', // The same for the drawers' tools: 'menu' behind a button, 'wrap' onto another line
380
+ 'toolbar_groups' : false, // The add buttons in categories, one shown at a time, picked with tabs
369
381
  'active_target' : false, // A click in a column makes it where the toolbar's buttons add
370
382
  'element_tools' : [], // Host tools on element drawers, same shape as row_tools
371
383
  'element_classes' : [], // Preset class toggles on an element's settings panel
@@ -420,6 +432,11 @@ function build(instance, baseElem, optionsOrMethod) {
420
432
  var sourceOpen = false; // Whether the canvas is being edited as html
421
433
  var dropMarker = null; // The line showing where a dragged toolbar button would land
422
434
  var activeTarget = null; // The column or region the toolbar adds to, with active_target
435
+ var toolbarLifetime = null; // Aborted when the toolbar is rebuilt or taken away
436
+ var closeOverflowMenu = function() {}; // Folds the toolbar's overflow menu away, when it has one
437
+ var refitToolbar = function() {}; // Works out again what the toolbar's overflow menu holds, when it has one
438
+ var addButtons = []; // The toolbar's add buttons, in toolbar order: { button, home, group }
439
+ var toolbarGroup = null; // The category shown, with toolbar_groups. Kept when the toolbar is rebuilt
423
440
  var warnedHere = {}; // Some warnings are worth saying once per instance, not once per call
424
441
  var sortables = []; // Every list made sortable, so deinit destroys exactly those
425
442
  var instanceId = ++editorCounter; // Scopes the sortable groups to this editor
@@ -464,7 +481,7 @@ function build(instance, baseElem, optionsOrMethod) {
464
481
  removeConfirmModal();
465
482
  removeSettingsPanels();
466
483
  removeDialog();
467
- mainControls.remove();
484
+ removeMainControls();
468
485
  createMainControls();
469
486
  reset();
470
487
  }
@@ -969,6 +986,7 @@ function build(instance, baseElem, optionsOrMethod) {
969
986
  canvas.parentNode.insertBefore(htmlTextArea, canvas);
970
987
 
971
988
  createMainControls();
989
+ watchDrawers();
972
990
 
973
991
  var signal = { signal: lifetime.signal };
974
992
 
@@ -1000,6 +1018,16 @@ function build(instance, baseElem, optionsOrMethod) {
1000
1018
  if (!dom.hasClass(canvas, 'ge-editing')) { return; }
1001
1019
  if (from && Math.abs(e.clientX - from.x) + Math.abs(e.clientY - from.y) > settings.drag.threshold) { return; }
1002
1020
  var region = dom.closest(e.target, targetSelector());
1021
+
1022
+ // The target's own background, or its drawer's, clicked
1023
+ // again takes it back. What is in it - a text, a tool, a
1024
+ // nested column - keeps it, or makes that the target
1025
+ if (region && region === activeTarget &&
1026
+ (e.target === region || e.target === dom.child(region, '.ge-tools-drawer'))) {
1027
+ changeTarget(null);
1028
+ return;
1029
+ }
1030
+
1003
1031
  changeTarget(region && region !== canvas && canvas.contains(region) ? region : null);
1004
1032
  }, signal);
1005
1033
 
@@ -1035,8 +1063,19 @@ function build(instance, baseElem, optionsOrMethod) {
1035
1063
  htmlTextArea.parentNode.insertBefore(mainControls, htmlTextArea);
1036
1064
  wrapper = mainControls.appendChild(dom.element('div', { 'class': 'ge-wrapper ge-top' }));
1037
1065
 
1066
+ // With the overflow menu, the add buttons stand in a line of
1067
+ // their own that keeps to one line, beside the menu's button
1068
+ var overflowMenu = settings.toolbar_overflow !== 'wrap';
1069
+ var start = wrapper;
1070
+ if (overflowMenu) {
1071
+ dom.addClass(wrapper, 'ge-toolbar-menu');
1072
+ start = wrapper.appendChild(dom.element('div', { 'class': 'ge-toolbar-start' }));
1073
+ }
1074
+
1075
+ addButtons = [];
1076
+
1038
1077
  // Add row
1039
- addRowGroup = wrapper.appendChild(dom.element('div', { 'class': 'ge-addRowGroup btn-group' }));
1078
+ addRowGroup = start.appendChild(dom.element('div', { 'class': 'ge-addRowGroup btn-group' }));
1040
1079
  addContainerGroup = dom.element('div', { 'class': 'ge-addContainerGroup btn-group ms-1' });
1041
1080
  settings.new_row_layouts.forEach(function(layout) {
1042
1081
  var grouped = !Array.isArray(layout);
@@ -1065,6 +1104,7 @@ function build(instance, baseElem, optionsOrMethod) {
1065
1104
  }
1066
1105
  });
1067
1106
  addRowGroup.appendChild(btn);
1107
+ addButtons.push({ button: btn, home: addRowGroup, group: 'rows' });
1068
1108
 
1069
1109
  btn.appendChild(dom.create('<i class="bi bi-plus"></i>'));
1070
1110
 
@@ -1079,7 +1119,7 @@ function build(instance, baseElem, optionsOrMethod) {
1079
1119
  btn.appendChild(dom.create(icon));
1080
1120
  });
1081
1121
 
1082
- wrapper.appendChild(addContainerGroup);
1122
+ start.appendChild(addContainerGroup);
1083
1123
 
1084
1124
  // A container starts in a row of its own, the way the add row
1085
1125
  // buttons next to these ones do
@@ -1101,6 +1141,7 @@ function build(instance, baseElem, optionsOrMethod) {
1101
1141
  }, { parent: canvas, source: 'tool' });
1102
1142
  });
1103
1143
  addContainerGroup.appendChild(button);
1144
+ addButtons.push({ button: button, home: addContainerGroup, group: toolbarGroupOf(definition.group) || 'content' });
1104
1145
  });
1105
1146
 
1106
1147
  // A feature plugin's own buttons, beside the containers': what
@@ -1116,6 +1157,8 @@ function build(instance, baseElem, optionsOrMethod) {
1116
1157
  endItems.push(button);
1117
1158
  } else {
1118
1159
  addContainerGroup.appendChild(button);
1160
+ // One that names no category of its own has its plugin's
1161
+ addButtons.push({ button: button, home: addContainerGroup, group: toolbarGroupOf(item.group) || name });
1119
1162
  }
1120
1163
  });
1121
1164
  });
@@ -1186,9 +1229,194 @@ function build(instance, baseElem, optionsOrMethod) {
1186
1229
  endItems.forEach(function(button) { end.appendChild(button); });
1187
1230
  }
1188
1231
 
1232
+ if (settings.toolbar_groups) { createGroupTabs(start); }
1233
+ if (overflowMenu) { createOverflowMenu(start); }
1234
+
1189
1235
  makeToolbarDraggable();
1190
1236
  }
1191
1237
 
1238
+ /** The core's categories, ahead of the plugins' own. */
1239
+ var CORE_GROUPS = [
1240
+ { name: 'rows', labelKey: 'group.rows' },
1241
+ { name: 'content', labelKey: 'group.content' },
1242
+ ];
1243
+
1244
+ /** A plugin's category, when it names one. */
1245
+ function toolbarGroupOf(value) {
1246
+ return typeof value === 'string' && value.trim() !== '' ? value : null;
1247
+ }
1248
+
1249
+ /**
1250
+ * toolbar_groups: tabs at the start of the toolbar, one per category
1251
+ * of add buttons, and only the chosen one's buttons on it. The rest
1252
+ * wait in a hidden stash inside the toolbar, so toolbarItems still
1253
+ * finds them and the btn-groups round the corners of what shows. With
1254
+ * a single category there is nothing to choose, and no tabs.
1255
+ */
1256
+ function createGroupTabs(start) {
1257
+ // The core's two first, then the rest as their first button comes
1258
+ var names = CORE_GROUPS.map(function(group) { return group.name; });
1259
+ addButtons.forEach(function(entry) {
1260
+ if (names.indexOf(entry.group) < 0) { names.push(entry.group); }
1261
+ });
1262
+ names = names.filter(function(name) {
1263
+ return addButtons.some(function(entry) { return entry.group === name; });
1264
+ });
1265
+
1266
+ if (names.length < 2) { return; }
1267
+
1268
+ var tabs = dom.element('div', { 'class': 'ge-toolbar-groups btn-group', role: 'tablist', 'aria-label': t('group.select') });
1269
+ start.insertBefore(tabs, start.firstChild);
1270
+ var stash = mainControls.appendChild(dom.element('div', { 'class': 'ge-toolbar-stash', hidden: 'hidden' }));
1271
+
1272
+ names.forEach(function(name) {
1273
+ var tab = tabs.appendChild(dom.element('button', {
1274
+ type: 'button',
1275
+ 'class': 'btn btn-sm btn-outline-primary',
1276
+ role: 'tab',
1277
+ 'data-ge-group': name,
1278
+ }, groupLabel(name)));
1279
+
1280
+ tab.addEventListener('click', function() { showGroup(name); });
1281
+ });
1282
+
1283
+ showGroup(names.indexOf(toolbarGroup) >= 0 ? toolbarGroup : names[0]);
1284
+
1285
+ function showGroup(name) {
1286
+ toolbarGroup = name;
1287
+
1288
+ addButtons.forEach(function(entry) {
1289
+ (entry.group === name ? entry.home : stash).appendChild(entry.button);
1290
+ });
1291
+
1292
+ dom.children(tabs).forEach(function(tab) {
1293
+ var chosen = tab.getAttribute('data-ge-group') === name;
1294
+ dom.toggleClass(tab, 'active', chosen);
1295
+ tab.setAttribute('aria-selected', chosen ? 'true' : 'false');
1296
+ });
1297
+
1298
+ refitToolbar();
1299
+ }
1300
+ }
1301
+
1302
+ /** A category's label: its group.<name> string, or its first button's when it has none. */
1303
+ function groupLabel(name) {
1304
+ var key = 'group.' + name;
1305
+ if (hasString(settings, key)) { return t(key); }
1306
+
1307
+ var first = addButtons.filter(function(entry) { return entry.group === name; })[0];
1308
+ return first.button.getAttribute('title') || name;
1309
+ }
1310
+
1311
+ /** The add buttons on the toolbar's line: all of them, or the chosen category's. */
1312
+ function shownAddButtons() {
1313
+ return addButtons.filter(function(entry) { return !dom.closest(entry.button, '.ge-toolbar-stash'); });
1314
+ }
1315
+
1316
+ /** The toolbar off the page, with what keeps its overflow menu up to date. */
1317
+ function removeMainControls() {
1318
+ if (toolbarLifetime) { toolbarLifetime.abort(); }
1319
+ toolbarLifetime = null;
1320
+ closeOverflowMenu = function() {};
1321
+ refitToolbar = function() {};
1322
+ mainControls.remove();
1323
+ }
1324
+
1325
+ /**
1326
+ * The add buttons that don't fit on the toolbar's line, behind a
1327
+ * button at its end. They are the same buttons, moved rather than
1328
+ * copied, so a click, a drag or a plugin finding them with
1329
+ * toolbarItems works as it does on the line. Which ones go is worked
1330
+ * out again whenever the toolbar or what is on it changes size.
1331
+ */
1332
+ function createOverflowMenu(start) {
1333
+ var more = dom.create('<div class="ge-toolbar-more">' +
1334
+ '<button type="button" class="btn btn-sm btn-primary"><i class="bi bi-three-dots"></i></button>' +
1335
+ '<div class="ge-toolbar-overflow">' +
1336
+ '<div class="ge-addRowGroup btn-group"></div>' +
1337
+ '<div class="ge-addContainerGroup btn-group"></div>' +
1338
+ '</div>' +
1339
+ '</div>');
1340
+ start.parentNode.insertBefore(more, start.nextSibling);
1341
+
1342
+ var toggle = dom.one(more, 'button');
1343
+ var panel = dom.one(more, '.ge-toolbar-overflow');
1344
+ var spares = dom.all(panel, '.btn-group');
1345
+ // The spare group in the menu for each group on the line
1346
+ function spareOf(home) {
1347
+ return home === addRowGroup ? spares[0] : spares[1];
1348
+ }
1349
+
1350
+ toggle.setAttribute('title', t('tool.more'));
1351
+ toggle.setAttribute('aria-haspopup', 'true');
1352
+ toggle.setAttribute('aria-expanded', 'false');
1353
+
1354
+ var menuLifetime = toolbarLifetime = new AbortController();
1355
+ var signal = { signal: menuLifetime.signal };
1356
+
1357
+ toggle.addEventListener('click', function() { openOverflow(!dom.hasClass(more, 'ge-open')); });
1358
+ // A button in the menu has done its job once it is clicked
1359
+ panel.addEventListener('click', function(e) {
1360
+ if (dom.closest(e.target, '[data-ge-toolbar]')) { openOverflow(false); }
1361
+ });
1362
+ document.addEventListener('pointerdown', function(e) {
1363
+ if (!more.contains(e.target)) { openOverflow(false); }
1364
+ }, signal);
1365
+ document.addEventListener('keydown', function(e) {
1366
+ if (e.key === 'Escape') { openOverflow(false); }
1367
+ }, signal);
1368
+ closeOverflowMenu = function() { openOverflow(false); };
1369
+ // A category chosen: another set of buttons to fit
1370
+ refitToolbar = fit;
1371
+
1372
+ fit();
1373
+
1374
+ if (window.ResizeObserver) {
1375
+ // Not in the callback itself: moving buttons there would
1376
+ // resize what is being observed before the frame is drawn
1377
+ var pending = false;
1378
+ var observer = new window.ResizeObserver(function() {
1379
+ if (pending) { return; }
1380
+ pending = true;
1381
+ window.requestAnimationFrame(function() {
1382
+ pending = false;
1383
+ if (!menuLifetime.signal.aborted) { fit(); }
1384
+ });
1385
+ });
1386
+ // The toolbar's width, and the buttons on the right of it
1387
+ // showing and hiding, as the paste button does
1388
+ observer.observe(wrapper);
1389
+ observer.observe(start);
1390
+ menuLifetime.signal.addEventListener('abort', function() { observer.disconnect(); });
1391
+ }
1392
+
1393
+ function openOverflow(open) {
1394
+ open = open && dom.hasClass(more, 'ge-needed');
1395
+ dom.toggleClass(more, 'ge-open', open);
1396
+ toggle.setAttribute('aria-expanded', open ? 'true' : 'false');
1397
+ }
1398
+
1399
+ /** Back on the line, then the last ones off it until the rest fit. */
1400
+ function fit() {
1401
+ // Only the chosen category's, with toolbar_groups: the others
1402
+ // wait in their stash
1403
+ var homes = shownAddButtons();
1404
+ homes.forEach(function(entry) { entry.home.appendChild(entry.button); });
1405
+ dom.removeClass(more, 'ge-needed');
1406
+
1407
+ if (start.scrollWidth <= start.clientWidth) {
1408
+ openOverflow(false);
1409
+ return;
1410
+ }
1411
+
1412
+ dom.addClass(more, 'ge-needed');
1413
+ for (var i = homes.length - 1; i >= 0 && start.scrollWidth > start.clientWidth; i--) {
1414
+ var into = spareOf(homes[i].home);
1415
+ into.insertBefore(homes[i].button, into.firstChild);
1416
+ }
1417
+ }
1418
+ }
1419
+
1192
1420
  /**
1193
1421
  * A toolbar button's face: the plugin's icon alone when it has one,
1194
1422
  * with its label as the title, or a plus and the label otherwise.
@@ -1298,6 +1526,8 @@ function build(instance, baseElem, optionsOrMethod) {
1298
1526
 
1299
1527
  helper = dom.addClass(button.cloneNode(true), 'ge-toolbar-helper');
1300
1528
  document.body.appendChild(helper);
1529
+ // The menu a button came from would hide where it lands
1530
+ closeOverflowMenu();
1301
1531
  dom.addClass(canvas, 'ge-dropping');
1302
1532
  }
1303
1533
 
@@ -1913,7 +2143,7 @@ function build(instance, baseElem, optionsOrMethod) {
1913
2143
  removeConfirmModal();
1914
2144
  removeSettingsPanels();
1915
2145
  removeDialog();
1916
- mainControls.remove();
2146
+ removeMainControls();
1917
2147
  htmlTextArea.remove();
1918
2148
  lifetime.abort();
1919
2149
  instances.delete(canvas);
@@ -3214,6 +3444,127 @@ function build(instance, baseElem, optionsOrMethod) {
3214
3444
  return createTool(drawer, t('tool.move'), 'ge-move', 'bi bi-arrows-move');
3215
3445
  }
3216
3446
 
3447
+ /**
3448
+ * The drawers that span their node - a row's, a column's, a
3449
+ * container's, an element's, a section's - keep their tools to one
3450
+ * line. The tools that don't fit are hidden from the end, and a more
3451
+ * tool at the drawer's end unfolds it to show them all, wrapping as
3452
+ * it would otherwise. They stay where they are, so whatever finds a
3453
+ * tool in its drawer, or hangs a picker off it, still does.
3454
+ *
3455
+ * A text's drawer sits over the text, as wide as its tools, and a
3456
+ * pane's sits by its label: neither is one of them.
3457
+ */
3458
+ var FITTED_DRAWERS = '.ge-tools-drawer:not(.ge-text-drawer):not(.ge-pane-drawer):not(.ge-code-inline)';
3459
+
3460
+ /**
3461
+ * Fit every drawer as it comes onto the canvas, and again when it
3462
+ * changes width or a tool is added to it, shown or hidden. Plugins
3463
+ * add tools at their own moments, so this watches rather than
3464
+ * waiting to be told.
3465
+ */
3466
+ function watchDrawers() {
3467
+ if (settings.drawer_overflow === 'wrap' || !window.ResizeObserver || !window.MutationObserver) { return; }
3468
+
3469
+ var due = new Set();
3470
+ var frame = null;
3471
+
3472
+ // Once a frame, after the layout: moving tools in the observers'
3473
+ // callbacks would resize what they are watching
3474
+ function schedule(drawer) {
3475
+ due.add(drawer);
3476
+ if (frame === null) { frame = window.requestAnimationFrame(flush); }
3477
+ }
3478
+
3479
+ function flush() {
3480
+ frame = null;
3481
+ var drawers = Array.from(due);
3482
+ due.clear();
3483
+ drawers.forEach(function(drawer) {
3484
+ if (!drawer.isConnected) {
3485
+ drawerResizes.unobserve(drawer);
3486
+ return;
3487
+ }
3488
+ fitDrawer(drawer);
3489
+ });
3490
+ }
3491
+
3492
+ var drawerResizes = new window.ResizeObserver(function(entries) {
3493
+ entries.forEach(function(entry) { schedule(entry.target); });
3494
+ });
3495
+
3496
+ var mutations = new window.MutationObserver(function(records) {
3497
+ records.forEach(function(record) {
3498
+ var target = record.target;
3499
+
3500
+ // A tool shown or hidden
3501
+ if (record.type === 'attributes') {
3502
+ var parent = target.parentNode;
3503
+ if (parent && parent.nodeType === 1 && parent.matches(FITTED_DRAWERS)) { schedule(parent); }
3504
+ return;
3505
+ }
3506
+
3507
+ // A tool added
3508
+ if (target.nodeType === 1 && target.matches(FITTED_DRAWERS)) {
3509
+ schedule(target);
3510
+ return;
3511
+ }
3512
+
3513
+ record.addedNodes.forEach(function(node) {
3514
+ if (node.nodeType !== 1) { return; }
3515
+ dom.selfAndAll(node, FITTED_DRAWERS).forEach(function(drawer) {
3516
+ dom.addClass(drawer, 'ge-drawer-fit');
3517
+ drawerResizes.observe(drawer);
3518
+ schedule(drawer);
3519
+ });
3520
+ });
3521
+ });
3522
+ });
3523
+ mutations.observe(canvas, { childList: true, subtree: true, attributes: true, attributeFilter: ['style'] });
3524
+
3525
+ lifetime.signal.addEventListener('abort', function() {
3526
+ mutations.disconnect();
3527
+ drawerResizes.disconnect();
3528
+ if (frame !== null) { window.cancelAnimationFrame(frame); }
3529
+ });
3530
+ }
3531
+
3532
+ /** Hide the tools that don't fit on the drawer's line, from the end, behind its more tool. */
3533
+ function fitDrawer(drawer) {
3534
+ var tools = dom.children(drawer, 'a').filter(function(tool) { return !dom.hasClass(tool, 'ge-drawer-more'); });
3535
+ var expanded = dom.hasClass(drawer, 'ge-drawer-expanded');
3536
+
3537
+ // Measured on one line, with every tool back on it
3538
+ dom.removeClass(drawer, 'ge-drawer-overflow ge-drawer-expanded');
3539
+ tools.forEach(function(tool) { dom.removeClass(tool, 'ge-tool-overflow'); });
3540
+
3541
+ if (!drawerOverflows(drawer, tools)) { return; }
3542
+
3543
+ dom.addClass(drawer, 'ge-drawer-overflow');
3544
+ if (!dom.child(drawer, '.ge-drawer-more')) {
3545
+ createTool(drawer, t('tool.more'), 'ge-drawer-more', 'bi bi-three-dots', function() {
3546
+ dom.toggleClass(drawer, 'ge-drawer-expanded');
3547
+ });
3548
+ }
3549
+
3550
+ for (var i = tools.length - 1; i >= 0 && drawerOverflows(drawer, tools); i--) {
3551
+ dom.addClass(tools[i], 'ge-tool-overflow');
3552
+ }
3553
+
3554
+ dom.toggleClass(drawer, 'ge-drawer-expanded', expanded);
3555
+ }
3556
+
3557
+ function drawerOverflows(drawer, tools) {
3558
+ var style = window.getComputedStyle(drawer);
3559
+ var limit = drawer.getBoundingClientRect().right -
3560
+ parseFloat(style.borderRightWidth) - parseFloat(style.paddingRight);
3561
+
3562
+ return tools.some(function(tool) {
3563
+ var rects = tool.getClientRects();
3564
+ return rects.length > 0 && rects[0].right > limit + 0.5;
3565
+ });
3566
+ }
3567
+
3217
3568
  /**
3218
3569
  * A tool in a drawer: a link with an icon, and what it does. The
3219
3570
  * handlers are a click handler, or { eventName: handler }; each gets
@@ -5416,6 +5767,10 @@ GridEditor.locales = {
5416
5767
  'tool.indent_increase': 'Increase indent\n(hold shift for max)',
5417
5768
  'tool.edit_source': 'Edit Source Code',
5418
5769
  'tool.preview': 'Preview',
5770
+ 'tool.more': 'More',
5771
+ 'group.rows': 'Rows',
5772
+ 'group.content': 'Content',
5773
+ 'group.select': 'Add',
5419
5774
  'panel.title': '{kind} settings',
5420
5775
  'panel.close': 'Close',
5421
5776
  'panel.done': 'Done',
@@ -51,6 +51,10 @@ GridEditor.locales.es = {
51
51
  'tool.indent_increase': 'Aumentar sangría\n(Mayús para el máximo)',
52
52
  'tool.edit_source': 'Editar el código fuente',
53
53
  'tool.preview': 'Vista previa',
54
+ 'tool.more': 'Más',
55
+ 'group.rows': 'Filas',
56
+ 'group.content': 'Contenido',
57
+ 'group.select': 'Añadir',
54
58
  'panel.title': 'Ajustes: {kind}',
55
59
  'panel.close': 'Cerrar',
56
60
  'panel.done': 'Hecho',
@@ -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: 'content',
324
325
  inColumn: true,
325
326
  create: function() { return makeText(type); },
326
327
  };
@@ -67,6 +67,103 @@
67
67
  margin-left: 5px;
68
68
  }
69
69
 
70
+ /* toolbar_groups: the tabs choosing which category of add buttons
71
+ shows, at the start of the line. Never squeezed, and never behind the
72
+ more button */
73
+ .ge-toolbar-groups {
74
+ flex: none;
75
+ margin-right: 8px;
76
+
77
+ > .btn {
78
+ white-space: nowrap;
79
+ }
80
+ }
81
+
82
+ /* toolbar_overflow: 'menu'. The add buttons keep to one line, and the
83
+ ones that don't fit go behind a button at its end. The right-hand
84
+ buttons come before in the markup, so they are ordered back to how
85
+ floating them puts them. */
86
+ .ge-wrapper.ge-toolbar-menu {
87
+ display: flex;
88
+ align-items: flex-start;
89
+
90
+ > .pull-right {
91
+ float: none;
92
+ }
93
+ > .ge-toolbar-end {
94
+ order: 1;
95
+ }
96
+ > .btn-group.pull-right {
97
+ order: 2;
98
+ }
99
+ > .ge-layout-mode {
100
+ order: 3;
101
+ }
102
+ }
103
+
104
+ .ge-toolbar-start {
105
+ display: flex;
106
+ flex: 1 1 auto;
107
+ min-width: 0;
108
+ overflow: hidden;
109
+
110
+ /* A group as wide as its buttons and a button as wide as its label:
111
+ squeezed, they would wrap their labels rather than overflow */
112
+ > .btn-group {
113
+ flex: none;
114
+ }
115
+ .btn {
116
+ white-space: nowrap;
117
+ }
118
+ }
119
+
120
+ .ge-toolbar-more {
121
+ display: none;
122
+ position: relative;
123
+ flex: none;
124
+ margin: 0 5px 0 4px;
125
+
126
+ &.ge-needed {
127
+ display: block;
128
+ }
129
+ }
130
+
131
+ .ge-toolbar-overflow {
132
+ display: none;
133
+ position: absolute;
134
+ z-index: 30;
135
+ top: ~"calc(100% + 4px)";
136
+ right: 0;
137
+ flex-direction: column;
138
+ gap: 6px;
139
+ width: max-content;
140
+ max-width: ~"min(420px, calc(100vw - 32px))";
141
+ padding: 6px;
142
+ background: #fff;
143
+ border: 1px solid rgba(0, 0, 0, .15);
144
+ border-radius: 6px;
145
+ box-shadow: 0 6px 16px rgba(0, 0, 0, .15);
146
+
147
+ > .btn-group {
148
+ flex-wrap: wrap;
149
+ gap: 4px;
150
+ margin: 0;
151
+
152
+ &:empty {
153
+ display: none;
154
+ }
155
+ }
156
+ /* Wrapped onto several lines, a group's buttons stand apart */
157
+ > .btn-group > .btn.btn {
158
+ margin-left: 0;
159
+ border-radius: 4px;
160
+ }
161
+ }
162
+
163
+ .ge-toolbar-more.ge-open > .ge-toolbar-overflow {
164
+ display: flex;
165
+ }
166
+
70
167
  .ge-toolbar-end {
71
168
  margin-right: 5px;
72
169
 
@@ -1038,6 +1135,43 @@
1038
1135
  }
1039
1136
  }
1040
1137
 
1138
+ /* drawer_overflow: 'menu'. A drawer keeps its tools to one line; the ones
1139
+ that don't fit are hidden behind a more tool at its end, which unfolds it
1140
+ to show them all */
1141
+ .ge-canvas.ge-editing .ge-tools-drawer.ge-drawer-fit {
1142
+ white-space: nowrap;
1143
+
1144
+ > .ge-details {
1145
+ white-space: normal;
1146
+ }
1147
+ > a.ge-tool-overflow,
1148
+ > a.ge-drawer-more {
1149
+ display: none;
1150
+ }
1151
+
1152
+ &.ge-drawer-overflow {
1153
+ padding-right: 26px;
1154
+
1155
+ > a.ge-drawer-more {
1156
+ display: inline-block;
1157
+ position: absolute;
1158
+ top: 0;
1159
+ right: 0;
1160
+ }
1161
+ }
1162
+
1163
+ &.ge-drawer-expanded {
1164
+ white-space: normal;
1165
+
1166
+ > a.ge-tool-overflow {
1167
+ display: inline-block;
1168
+ }
1169
+ > a.ge-drawer-more {
1170
+ background: rgba(0, 0, 0, 0.08);
1171
+ }
1172
+ }
1173
+ }
1174
+
1041
1175
  /* The handle belongs to editing, and never to the output */
1042
1176
  .ge-canvas:not(.ge-editing) .ge-resize-handle {
1043
1177
  display: none;
@@ -107,6 +107,12 @@ export interface GridEditorOptions {
107
107
  text_tools?: HostTool[];
108
108
  drag_handle?: 'tool' | 'drawer';
109
109
  toolbar_drag?: 'auto' | boolean;
110
+ /** The add buttons that don't fit on the toolbar's line: behind a more button, or onto another line. Default 'menu'. */
111
+ toolbar_overflow?: 'menu' | 'wrap';
112
+ /** The drawers' tools that don't fit on one line: behind a more tool, or onto more lines. Default 'menu'. */
113
+ drawer_overflow?: 'menu' | 'wrap';
114
+ /** The add buttons in categories - rows, content, and a plugin's own - one shown at a time, chosen with tabs. Default false. */
115
+ toolbar_groups?: boolean;
110
116
  /** A click in a column or a section makes it where the toolbar's buttons add. Default false. */
111
117
  active_target?: boolean;
112
118
  /** The plugins to use, of any kind; null is every one loaded. */