@themarioga/grid-editor 8.1.0 → 8.3.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.
@@ -35,6 +35,8 @@ var METHODS = {
35
35
  setLocale: {},
36
36
  getUtility: { value: true },
37
37
  setUtility: { value: true },
38
+ getActiveTarget: { value: true },
39
+ setActiveTarget: {},
38
40
  };
39
41
 
40
42
  /**
@@ -364,6 +366,8 @@ function build(instance, baseElem, optionsOrMethod) {
364
366
  'row_tools' : [],
365
367
  'drag_handle' : 'tool', // 'tool' for the move tool, 'drawer' for the whole drawer
366
368
  'toolbar_drag' : 'auto', // Drag the toolbar's buttons onto the canvas. 'auto' follows drag_handle
369
+ 'toolbar_overflow' : 'menu', // What doesn't fit on one line: 'menu' behind a button, 'wrap' onto another line
370
+ 'active_target' : false, // A click in a column makes it where the toolbar's buttons add
367
371
  'element_tools' : [], // Host tools on element drawers, same shape as row_tools
368
372
  'element_classes' : [], // Preset class toggles on an element's settings panel
369
373
  // content_types, text_tools and text_classes are the text editor
@@ -416,6 +420,9 @@ function build(instance, baseElem, optionsOrMethod) {
416
420
  var sizePicker = null; // The open column size picker, if there is one
417
421
  var sourceOpen = false; // Whether the canvas is being edited as html
418
422
  var dropMarker = null; // The line showing where a dragged toolbar button would land
423
+ var activeTarget = null; // The column or region the toolbar adds to, with active_target
424
+ var toolbarLifetime = null; // Aborted when the toolbar is rebuilt or taken away
425
+ var closeOverflowMenu = function() {}; // Folds the toolbar's overflow menu away, when it has one
419
426
  var warnedHere = {}; // Some warnings are worth saying once per instance, not once per call
420
427
  var sortables = []; // Every list made sortable, so deinit destroys exactly those
421
428
  var instanceId = ++editorCounter; // Scopes the sortable groups to this editor
@@ -460,7 +467,7 @@ function build(instance, baseElem, optionsOrMethod) {
460
467
  removeConfirmModal();
461
468
  removeSettingsPanels();
462
469
  removeDialog();
463
- mainControls.remove();
470
+ removeMainControls();
464
471
  createMainControls();
465
472
  reset();
466
473
  }
@@ -979,6 +986,35 @@ function build(instance, baseElem, optionsOrMethod) {
979
986
  dom.delegate(canvas, 'click', '.ge-popup-trigger, [data-ge-popup-target]', function(e) {
980
987
  if (dom.hasClass(canvas, 'ge-editing')) { e.preventDefault(); }
981
988
  }, signal);
989
+
990
+ if (settings.active_target) {
991
+ // Whatever the click was for - a text, a tool - it also says
992
+ // where the toolbar adds next. A click on no column or region
993
+ // says nowhere, and the toolbar adds to the canvas again.
994
+ // The click a drag ends in - a resize, a move - is not one:
995
+ // it comes where the pointer let go, not where it pressed
996
+ var pressed = null;
997
+ canvas.addEventListener('pointerdown', function(e) {
998
+ pressed = { x: e.clientX, y: e.clientY };
999
+ }, signal);
1000
+ canvas.addEventListener('click', function(e) {
1001
+ var from = pressed;
1002
+ pressed = null;
1003
+ if (!dom.hasClass(canvas, 'ge-editing')) { return; }
1004
+ if (from && Math.abs(e.clientX - from.x) + Math.abs(e.clientY - from.y) > settings.drag.threshold) { return; }
1005
+ var region = dom.closest(e.target, targetSelector());
1006
+ changeTarget(region && region !== canvas && canvas.contains(region) ? region : null);
1007
+ }, signal);
1008
+
1009
+ // Escape is the panel's, the dialog's, and a field's or a
1010
+ // text's while one has the focus; after those, it is this
1011
+ document.addEventListener('keydown', function(e) {
1012
+ if (e.key !== 'Escape' || !activeTarget || openSettingsState || dialogState) { return; }
1013
+ var focus = document.activeElement;
1014
+ if (focus && (dom.is(focus, 'input, textarea, select') || focus.isContentEditable)) { return; }
1015
+ changeTarget(null);
1016
+ }, signal);
1017
+ }
982
1018
  }
983
1019
 
984
1020
  /**
@@ -1002,8 +1038,17 @@ function build(instance, baseElem, optionsOrMethod) {
1002
1038
  htmlTextArea.parentNode.insertBefore(mainControls, htmlTextArea);
1003
1039
  wrapper = mainControls.appendChild(dom.element('div', { 'class': 'ge-wrapper ge-top' }));
1004
1040
 
1041
+ // With the overflow menu, the add buttons stand in a line of
1042
+ // their own that keeps to one line, beside the menu's button
1043
+ var overflowMenu = settings.toolbar_overflow !== 'wrap';
1044
+ var start = wrapper;
1045
+ if (overflowMenu) {
1046
+ dom.addClass(wrapper, 'ge-toolbar-menu');
1047
+ start = wrapper.appendChild(dom.element('div', { 'class': 'ge-toolbar-start' }));
1048
+ }
1049
+
1005
1050
  // Add row
1006
- addRowGroup = wrapper.appendChild(dom.element('div', { 'class': 'ge-addRowGroup btn-group' }));
1051
+ addRowGroup = start.appendChild(dom.element('div', { 'class': 'ge-addRowGroup btn-group' }));
1007
1052
  addContainerGroup = dom.element('div', { 'class': 'ge-addContainerGroup btn-group ms-1' });
1008
1053
  settings.new_row_layouts.forEach(function(layout) {
1009
1054
  var grouped = !Array.isArray(layout);
@@ -1019,6 +1064,8 @@ function build(instance, baseElem, optionsOrMethod) {
1019
1064
  });
1020
1065
 
1021
1066
  btn.addEventListener('click', function() {
1067
+ if (activeTarget) { addToTarget(btn, 'tool'); return; }
1068
+
1022
1069
  var row = rowFromLayoutValue(layout);
1023
1070
 
1024
1071
  var added = addNode('row', row, function() {
@@ -1044,7 +1091,7 @@ function build(instance, baseElem, optionsOrMethod) {
1044
1091
  btn.appendChild(dom.create(icon));
1045
1092
  });
1046
1093
 
1047
- wrapper.appendChild(addContainerGroup);
1094
+ start.appendChild(addContainerGroup);
1048
1095
 
1049
1096
  // A container starts in a row of its own, the way the add row
1050
1097
  // buttons next to these ones do
@@ -1055,6 +1102,8 @@ function build(instance, baseElem, optionsOrMethod) {
1055
1102
 
1056
1103
  dom.attr(button, { 'data-ge-toolbar': 'container', 'data-ge-container-type': type });
1057
1104
  button.addEventListener('click', function() {
1105
+ if (activeTarget) { addToTarget(button, 'tool'); return; }
1106
+
1058
1107
  var row = createRow();
1059
1108
  var column = row.appendChild(createColumn(MAX_COL_SIZE));
1060
1109
  var container = column.appendChild(definition.create({}));
@@ -1149,9 +1198,111 @@ function build(instance, baseElem, optionsOrMethod) {
1149
1198
  endItems.forEach(function(button) { end.appendChild(button); });
1150
1199
  }
1151
1200
 
1201
+ if (overflowMenu) { createOverflowMenu(start); }
1202
+
1152
1203
  makeToolbarDraggable();
1153
1204
  }
1154
1205
 
1206
+ /** The toolbar off the page, with what keeps its overflow menu up to date. */
1207
+ function removeMainControls() {
1208
+ if (toolbarLifetime) { toolbarLifetime.abort(); }
1209
+ toolbarLifetime = null;
1210
+ closeOverflowMenu = function() {};
1211
+ mainControls.remove();
1212
+ }
1213
+
1214
+ /**
1215
+ * The add buttons that don't fit on the toolbar's line, behind a
1216
+ * button at its end. They are the same buttons, moved rather than
1217
+ * copied, so a click, a drag or a plugin finding them with
1218
+ * toolbarItems works as it does on the line. Which ones go is worked
1219
+ * out again whenever the toolbar or what is on it changes size.
1220
+ */
1221
+ function createOverflowMenu(start) {
1222
+ var more = dom.create('<div class="ge-toolbar-more">' +
1223
+ '<button type="button" class="btn btn-sm btn-primary"><i class="bi bi-three-dots"></i></button>' +
1224
+ '<div class="ge-toolbar-overflow">' +
1225
+ '<div class="ge-addRowGroup btn-group"></div>' +
1226
+ '<div class="ge-addContainerGroup btn-group"></div>' +
1227
+ '</div>' +
1228
+ '</div>');
1229
+ start.parentNode.insertBefore(more, start.nextSibling);
1230
+
1231
+ var toggle = dom.one(more, 'button');
1232
+ var panel = dom.one(more, '.ge-toolbar-overflow');
1233
+ 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
+ });
1241
+
1242
+ toggle.setAttribute('title', t('tool.more'));
1243
+ toggle.setAttribute('aria-haspopup', 'true');
1244
+ toggle.setAttribute('aria-expanded', 'false');
1245
+
1246
+ var menuLifetime = toolbarLifetime = new AbortController();
1247
+ var signal = { signal: menuLifetime.signal };
1248
+
1249
+ toggle.addEventListener('click', function() { openOverflow(!dom.hasClass(more, 'ge-open')); });
1250
+ // A button in the menu has done its job once it is clicked
1251
+ panel.addEventListener('click', function(e) {
1252
+ if (dom.closest(e.target, '[data-ge-toolbar]')) { openOverflow(false); }
1253
+ });
1254
+ document.addEventListener('pointerdown', function(e) {
1255
+ if (!more.contains(e.target)) { openOverflow(false); }
1256
+ }, signal);
1257
+ document.addEventListener('keydown', function(e) {
1258
+ if (e.key === 'Escape') { openOverflow(false); }
1259
+ }, signal);
1260
+ closeOverflowMenu = function() { openOverflow(false); };
1261
+
1262
+ fit();
1263
+
1264
+ if (window.ResizeObserver) {
1265
+ // Not in the callback itself: moving buttons there would
1266
+ // resize what is being observed before the frame is drawn
1267
+ var pending = false;
1268
+ var observer = new window.ResizeObserver(function() {
1269
+ if (pending) { return; }
1270
+ pending = true;
1271
+ window.requestAnimationFrame(function() {
1272
+ pending = false;
1273
+ if (!menuLifetime.signal.aborted) { fit(); }
1274
+ });
1275
+ });
1276
+ // The toolbar's width, and the buttons on the right of it
1277
+ // showing and hiding, as the paste button does
1278
+ observer.observe(wrapper);
1279
+ observer.observe(start);
1280
+ menuLifetime.signal.addEventListener('abort', function() { observer.disconnect(); });
1281
+ }
1282
+
1283
+ function openOverflow(open) {
1284
+ open = open && dom.hasClass(more, 'ge-needed');
1285
+ dom.toggleClass(more, 'ge-open', open);
1286
+ toggle.setAttribute('aria-expanded', open ? 'true' : 'false');
1287
+ }
1288
+
1289
+ /** Back on the line, then the last ones off it until the rest fit. */
1290
+ function fit() {
1291
+ homes.forEach(function(entry) { entry.home.appendChild(entry.button); });
1292
+ dom.removeClass(more, 'ge-needed');
1293
+
1294
+ if (start.scrollWidth <= start.clientWidth) {
1295
+ openOverflow(false);
1296
+ return;
1297
+ }
1298
+
1299
+ dom.addClass(more, 'ge-needed');
1300
+ 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);
1302
+ }
1303
+ }
1304
+ }
1305
+
1155
1306
  /**
1156
1307
  * A toolbar button's face: the plugin's icon alone when it has one,
1157
1308
  * with its label as the title, or a plus and the label otherwise.
@@ -1184,6 +1335,8 @@ function build(instance, baseElem, optionsOrMethod) {
1184
1335
  'data-ge-item': index,
1185
1336
  });
1186
1337
  button.addEventListener('click', function() {
1338
+ if (activeTarget) { addToTarget(button, item.source || 'tool'); return; }
1339
+
1187
1340
  var made = item.create();
1188
1341
 
1189
1342
  // One that belongs in a column brings a row and a column
@@ -1259,6 +1412,8 @@ function build(instance, baseElem, optionsOrMethod) {
1259
1412
 
1260
1413
  helper = dom.addClass(button.cloneNode(true), 'ge-toolbar-helper');
1261
1414
  document.body.appendChild(helper);
1415
+ // The menu a button came from would hide where it lands
1416
+ closeOverflowMenu();
1262
1417
  dom.addClass(canvas, 'ge-dropping');
1263
1418
  }
1264
1419
 
@@ -1339,7 +1494,7 @@ function build(instance, baseElem, optionsOrMethod) {
1339
1494
  * not have what it makes - a section dropped on a column - gives way
1340
1495
  * to the canvas, just after the top level block the pointer was in.
1341
1496
  */
1342
- function insertFeatureFromToolbar(button, where) {
1497
+ function insertFeatureFromToolbar(button, where, source) {
1343
1498
  var item = FEATURES[button.getAttribute('data-ge-feature')].toolbar[parseInt(button.getAttribute('data-ge-item'), 10)];
1344
1499
  var made = item.create();
1345
1500
  var placed = made;
@@ -1360,7 +1515,7 @@ function build(instance, baseElem, optionsOrMethod) {
1360
1515
  } else {
1361
1516
  where.region.appendChild(placed);
1362
1517
  }
1363
- }, { parent: where.region, source: item.source || 'dragdrop' });
1518
+ }, { parent: where.region, source: item.source || source || 'dragdrop' });
1364
1519
  }
1365
1520
 
1366
1521
  /** A detached row with one full width column holding `node`. */
@@ -1396,9 +1551,9 @@ function build(instance, baseElem, optionsOrMethod) {
1396
1551
  * The row or container a toolbar button stands for, made and put where
1397
1552
  * the pointer left it.
1398
1553
  */
1399
- function insertFromToolbar(button, where) {
1554
+ function insertFromToolbar(button, where, source) {
1400
1555
  if (button.getAttribute('data-ge-toolbar') === 'feature') {
1401
- return insertFeatureFromToolbar(button, where);
1556
+ return insertFeatureFromToolbar(button, where, source);
1402
1557
  }
1403
1558
 
1404
1559
  var container = button.getAttribute('data-ge-toolbar') === 'container';
@@ -1418,7 +1573,7 @@ function build(instance, baseElem, optionsOrMethod) {
1418
1573
  } else {
1419
1574
  where.region.appendChild(placed);
1420
1575
  }
1421
- }, { parent: where.region, source: 'dragdrop' });
1576
+ }, { parent: where.region, source: source || 'dragdrop' });
1422
1577
  }
1423
1578
 
1424
1579
  /**
@@ -1710,6 +1865,8 @@ function build(instance, baseElem, optionsOrMethod) {
1710
1865
  */
1711
1866
  function openSource() {
1712
1867
  deinit();
1868
+ // The canvas is made again from the html when it comes back
1869
+ changeTarget(null);
1713
1870
  htmlTextArea.style.height = (0.8 * document.documentElement.clientHeight) + 'px';
1714
1871
  htmlTextArea.value = canvas.innerHTML;
1715
1872
  dom.show(htmlTextArea);
@@ -1732,6 +1889,39 @@ function build(instance, baseElem, optionsOrMethod) {
1732
1889
  dom.hide(htmlTextArea);
1733
1890
  }
1734
1891
 
1892
+ /**
1893
+ * What can be the active target: a column, or a plugin's region - a
1894
+ * section. Not the canvas, which is where the toolbar adds without one.
1895
+ */
1896
+ function targetSelector() {
1897
+ return ['.column'].concat(pluginHooks('regions')).join(', ');
1898
+ }
1899
+
1900
+ function isTarget(node) {
1901
+ return !!node && node !== canvas && canvas.contains(node) && dom.is(node, targetSelector());
1902
+ }
1903
+
1904
+ /** The one way the active target changes, told only when it does. */
1905
+ function changeTarget(node) {
1906
+ if (node === activeTarget) { return; }
1907
+
1908
+ var from = activeTarget;
1909
+ if (from) { dom.removeClass(from, 'ge-active-target'); }
1910
+ activeTarget = node;
1911
+ if (node && dom.hasClass(canvas, 'ge-editing')) { dom.addClass(node, 'ge-active-target'); }
1912
+
1913
+ emit('target-change', { canvas: canvas, target: node, from: from });
1914
+ }
1915
+
1916
+ /**
1917
+ * A toolbar button's click with an active target: what it makes goes
1918
+ * where a drop at the target's end would put it.
1919
+ */
1920
+ function addToTarget(button, source) {
1921
+ var made = insertFromToolbar(button, { region: activeTarget, before: null }, source);
1922
+ if (made && made.scrollIntoView) { made.scrollIntoView({ behavior: 'smooth' }); }
1923
+ }
1924
+
1735
1925
  function init() {
1736
1926
  // The node whose settings were open is gone - deleted, say - and
1737
1927
  // its panel with it
@@ -1759,12 +1949,21 @@ function build(instance, baseElem, optionsOrMethod) {
1759
1949
  makeResizable();
1760
1950
  switchLayout(curView);
1761
1951
  refreshPreviews(canvas);
1952
+
1953
+ // Deleted, or taken out some other way, it is a target no more
1954
+ if (activeTarget && !canvas.contains(activeTarget)) {
1955
+ changeTarget(null);
1956
+ } else if (activeTarget) {
1957
+ dom.addClass(activeTarget, 'ge-active-target');
1958
+ }
1762
1959
  }
1763
1960
 
1764
1961
  function deinit() {
1765
1962
  // Its panel goes home to its drawer first, and both go together
1766
1963
  closeSettings();
1767
1964
  dom.removeClass(canvas, 'ge-editing ge-drag-drawer ge-dropping');
1965
+ // The mark goes, the target stays: getHtml comes straight back
1966
+ if (activeTarget) { dom.removeClass(activeTarget, 'ge-active-target'); }
1768
1967
  // Before the drawers and the text blocks come off: an editor
1769
1968
  // plugin closes its editors here, and finds them where it left them
1770
1969
  plugins('onBeforeDeinit');
@@ -1826,10 +2025,11 @@ function build(instance, baseElem, optionsOrMethod) {
1826
2025
  // The html being edited is what the canvas is left with
1827
2026
  if (sourceOpen) { closeSource(); }
1828
2027
  deinit();
2028
+ activeTarget = null;
1829
2029
  removeConfirmModal();
1830
2030
  removeSettingsPanels();
1831
2031
  removeDialog();
1832
- mainControls.remove();
2032
+ removeMainControls();
1833
2033
  htmlTextArea.remove();
1834
2034
  lifetime.abort();
1835
2035
  instances.delete(canvas);
@@ -5150,6 +5350,23 @@ function build(instance, baseElem, optionsOrMethod) {
5150
5350
  * finishes before the canvas is rebuilt. The ones that do something,
5151
5351
  * rather than answer something, hand back the instance, to chain.
5152
5352
  */
5353
+ function apiSetActiveTarget(value) {
5354
+ if (!settings.active_target) {
5355
+ warnOnceHere('active_target', 'setActiveTarget needs the active_target setting: without it, it does nothing');
5356
+ return;
5357
+ }
5358
+ if (value === null || value === undefined) { changeTarget(null); return; }
5359
+
5360
+ var node = nodeFrom(value);
5361
+ if (!isTarget(node)) {
5362
+ var named = typeof value === 'string' ? value : (node ? '<' + node.tagName.toLowerCase() + '>' : String(value));
5363
+ warn('setActiveTarget: ' + named + ' is not a column or a region of the canvas');
5364
+ return;
5365
+ }
5366
+
5367
+ changeTarget(node);
5368
+ }
5369
+
5153
5370
  var own = {
5154
5371
  getHtml: getHtml,
5155
5372
  getPlainHtml: getPlainHtml,
@@ -5180,6 +5397,8 @@ function build(instance, baseElem, optionsOrMethod) {
5180
5397
  setLocale: setLocale,
5181
5398
  getUtility: getUtility,
5182
5399
  setUtility: setUtility,
5400
+ getActiveTarget: function() { return settings.active_target ? activeTarget : null; },
5401
+ setActiveTarget: apiSetActiveTarget,
5183
5402
  };
5184
5403
 
5185
5404
  Object.keys(own).forEach(function(name) {
@@ -5313,6 +5532,7 @@ GridEditor.locales = {
5313
5532
  'tool.indent_increase': 'Increase indent\n(hold shift for max)',
5314
5533
  'tool.edit_source': 'Edit Source Code',
5315
5534
  'tool.preview': 'Preview',
5535
+ 'tool.more': 'More',
5316
5536
  'panel.title': '{kind} settings',
5317
5537
  'panel.close': 'Close',
5318
5538
  'panel.done': 'Done',
@@ -51,6 +51,7 @@ 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',
54
55
  'panel.title': 'Ajustes: {kind}',
55
56
  'panel.close': 'Cerrar',
56
57
  'panel.done': 'Hecho',
@@ -67,6 +67,82 @@
67
67
  margin-left: 5px;
68
68
  }
69
69
 
70
+ /* toolbar_overflow: 'menu'. The add buttons keep to one line, and the
71
+ ones that don't fit go behind a button at its end. The right-hand
72
+ buttons come before in the markup, so they are ordered back to how
73
+ floating them puts them. */
74
+ .ge-wrapper.ge-toolbar-menu {
75
+ display: flex;
76
+ align-items: flex-start;
77
+
78
+ > .pull-right {
79
+ float: none;
80
+ }
81
+ > .ge-toolbar-end {
82
+ order: 1;
83
+ }
84
+ > .btn-group.pull-right {
85
+ order: 2;
86
+ }
87
+ > .ge-layout-mode {
88
+ order: 3;
89
+ }
90
+ }
91
+
92
+ .ge-toolbar-start {
93
+ display: flex;
94
+ flex: 1 1 auto;
95
+ min-width: 0;
96
+ overflow: hidden;
97
+ }
98
+
99
+ .ge-toolbar-more {
100
+ display: none;
101
+ position: relative;
102
+ flex: none;
103
+ margin: 0 5px 0 4px;
104
+
105
+ &.ge-needed {
106
+ display: block;
107
+ }
108
+ }
109
+
110
+ .ge-toolbar-overflow {
111
+ display: none;
112
+ position: absolute;
113
+ z-index: 30;
114
+ top: ~"calc(100% + 4px)";
115
+ right: 0;
116
+ flex-direction: column;
117
+ gap: 6px;
118
+ width: max-content;
119
+ max-width: ~"min(420px, calc(100vw - 32px))";
120
+ padding: 6px;
121
+ background: #fff;
122
+ border: 1px solid rgba(0, 0, 0, .15);
123
+ border-radius: 6px;
124
+ box-shadow: 0 6px 16px rgba(0, 0, 0, .15);
125
+
126
+ > .btn-group {
127
+ flex-wrap: wrap;
128
+ gap: 4px;
129
+ margin: 0;
130
+
131
+ &:empty {
132
+ display: none;
133
+ }
134
+ }
135
+ /* Wrapped onto several lines, a group's buttons stand apart */
136
+ > .btn-group > .btn.btn {
137
+ margin-left: 0;
138
+ border-radius: 4px;
139
+ }
140
+ }
141
+
142
+ .ge-toolbar-more.ge-open > .ge-toolbar-overflow {
143
+ display: flex;
144
+ }
145
+
70
146
  .ge-toolbar-end {
71
147
  margin-right: 5px;
72
148
 
@@ -516,6 +592,16 @@
516
592
  outline-offset: 1px;
517
593
  }
518
594
 
595
+ /* Where the toolbar's buttons add, with active_target. A node that is both
596
+ shows both: this one a little further out */
597
+ .ge-canvas.ge-editing .ge-active-target {
598
+ outline: 2px dashed var(--bs-success, #198754);
599
+ outline-offset: -2px;
600
+ }
601
+ .ge-canvas.ge-editing .ge-active-target.ge-settings-target {
602
+ box-shadow: 0 0 0 4px var(--bs-success, #198754);
603
+ }
604
+
519
605
  /* While editing */
520
606
  .ge-canvas.ge-editing {
521
607
 
@@ -107,6 +107,10 @@ 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
+ /** A click in a column or a section makes it where the toolbar's buttons add. Default false. */
113
+ active_target?: boolean;
110
114
  /** The plugins to use, of any kind; null is every one loaded. */
111
115
  plugins?: string[] | null;
112
116
  /** The text editors offered, in order. */
@@ -218,6 +222,13 @@ export interface ViewChangePayload {
218
222
  to: View;
219
223
  }
220
224
 
225
+ /** The active target changed: the column or region the toolbar adds to, or null for the canvas. */
226
+ export interface TargetChangePayload {
227
+ canvas: HTMLElement;
228
+ target: HTMLElement | null;
229
+ from: HTMLElement | null;
230
+ }
231
+
221
232
  /** The events, by name, with their payloads. Adding fires a specific name and the generic one. */
222
233
  export interface GridEditorEventMap {
223
234
  'grideditor:before-add': Payload;
@@ -255,6 +266,7 @@ export interface GridEditorEventMap {
255
266
  'grideditor:after-copy': Payload;
256
267
  'grideditor:popup-orphan': PopupOrphanPayload;
257
268
  'grideditor:view-change': ViewChangePayload;
269
+ 'grideditor:target-change': TargetChangePayload;
258
270
  }
259
271
 
260
272
  /** An event the editor dispatches on its canvas: a CustomEvent whose detail is the payload. */
@@ -343,6 +355,10 @@ export declare class GridEditor {
343
355
  /** Take the editor off: the markup stays, everything else goes. */
344
356
  destroy(): this;
345
357
  changeView(view: View): this;
358
+ /** The column or region the toolbar adds to, or null. Null without active_target. */
359
+ getActiveTarget(): HTMLElement | null;
360
+ /** Make a column or region, or the first a selector matches, where the toolbar adds; null for none. */
361
+ setActiveTarget(target: HTMLElement | string | null): this;
346
362
  getView(): View;
347
363
  setLocale(code: string): this;
348
364
  createRow(layout?: RowLayout, options?: Placement): HTMLElement | null;