@themarioga/grid-editor 8.1.0 → 8.2.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.
package/docs/events.md CHANGED
@@ -89,6 +89,7 @@ about every insertion can bind `grideditor:before-add` and switch on
89
89
  | `grideditor:before-utility` | — | yes | before a utility class is written, from the panel, a plugin's tool or `setUtility` |
90
90
  | `grideditor:after-utility` | — | no | after the class is written and the preview redrawn, only if something changed |
91
91
  | `grideditor:view-change` | — | no | after the view changes, only if it actually changed |
92
+ | `grideditor:target-change` | — | no | with `active_target`, after the column or section the toolbar adds to changes, only if it actually changed |
92
93
  | `grideditor:after-copy` | — | no | with the clipboard plugin, after a node is copied |
93
94
 
94
95
  Two things here are not in the 3.0 specification's catalogue. The indent pair,
@@ -160,6 +161,12 @@ view), `from` and `to`. A utility's `breakpoint` is the view it was written
160
161
  in, which `setUtility` can be asked to make a different one from the view on
161
162
  screen, and its `source` is `panel`, `tool` or `api`.
162
163
 
164
+ `target-change` carries no node either: its payload is `canvas`, `target` - the
165
+ column or section the toolbar adds to now, or `null` for the end of the canvas
166
+ - and `from`, the one before. What a toolbar button then adds fires its add
167
+ events as any click does, with `source: 'tool'` (or `'paste'`) and the target,
168
+ or the canvas for a section, as the `parent`.
169
+
163
170
  `source` matters to a host that both drives the editor from its own palette and
164
171
  listens for what the user does: `api` is your own call coming back to you,
165
172
  `tool` is a click in the editor, `dragdrop` is a gesture, and `paste` is a node
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.1.0",
36
+ "version": "8.2.0",
37
37
  "main": "dist/grideditor.js",
38
38
  "module": "dist/grideditor.esm.js",
39
39
  "types": "dist/grideditor.d.ts",
@@ -67,6 +67,8 @@ function install() {
67
67
  setLocale: {},
68
68
  getUtility: { value: true },
69
69
  setUtility: { value: true },
70
+ getActiveTarget: { value: true },
71
+ setActiveTarget: {},
70
72
  };
71
73
 
72
74
  /** The *_tools settings, whose handlers 6.x bound with jQuery. */
@@ -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,7 @@ 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
+ 'active_target' : false, // A click in a column makes it where the toolbar's buttons add
367
370
  'element_tools' : [], // Host tools on element drawers, same shape as row_tools
368
371
  'element_classes' : [], // Preset class toggles on an element's settings panel
369
372
  // content_types, text_tools and text_classes are the text editor
@@ -416,6 +419,7 @@ function build(instance, baseElem, optionsOrMethod) {
416
419
  var sizePicker = null; // The open column size picker, if there is one
417
420
  var sourceOpen = false; // Whether the canvas is being edited as html
418
421
  var dropMarker = null; // The line showing where a dragged toolbar button would land
422
+ var activeTarget = null; // The column or region the toolbar adds to, with active_target
419
423
  var warnedHere = {}; // Some warnings are worth saying once per instance, not once per call
420
424
  var sortables = []; // Every list made sortable, so deinit destroys exactly those
421
425
  var instanceId = ++editorCounter; // Scopes the sortable groups to this editor
@@ -979,6 +983,35 @@ function build(instance, baseElem, optionsOrMethod) {
979
983
  dom.delegate(canvas, 'click', '.ge-popup-trigger, [data-ge-popup-target]', function(e) {
980
984
  if (dom.hasClass(canvas, 'ge-editing')) { e.preventDefault(); }
981
985
  }, signal);
986
+
987
+ if (settings.active_target) {
988
+ // Whatever the click was for - a text, a tool - it also says
989
+ // where the toolbar adds next. A click on no column or region
990
+ // says nowhere, and the toolbar adds to the canvas again.
991
+ // The click a drag ends in - a resize, a move - is not one:
992
+ // it comes where the pointer let go, not where it pressed
993
+ var pressed = null;
994
+ canvas.addEventListener('pointerdown', function(e) {
995
+ pressed = { x: e.clientX, y: e.clientY };
996
+ }, signal);
997
+ canvas.addEventListener('click', function(e) {
998
+ var from = pressed;
999
+ pressed = null;
1000
+ if (!dom.hasClass(canvas, 'ge-editing')) { return; }
1001
+ if (from && Math.abs(e.clientX - from.x) + Math.abs(e.clientY - from.y) > settings.drag.threshold) { return; }
1002
+ var region = dom.closest(e.target, targetSelector());
1003
+ changeTarget(region && region !== canvas && canvas.contains(region) ? region : null);
1004
+ }, signal);
1005
+
1006
+ // Escape is the panel's, the dialog's, and a field's or a
1007
+ // text's while one has the focus; after those, it is this
1008
+ document.addEventListener('keydown', function(e) {
1009
+ if (e.key !== 'Escape' || !activeTarget || openSettingsState || dialogState) { return; }
1010
+ var focus = document.activeElement;
1011
+ if (focus && (dom.is(focus, 'input, textarea, select') || focus.isContentEditable)) { return; }
1012
+ changeTarget(null);
1013
+ }, signal);
1014
+ }
982
1015
  }
983
1016
 
984
1017
  /**
@@ -1019,6 +1052,8 @@ function build(instance, baseElem, optionsOrMethod) {
1019
1052
  });
1020
1053
 
1021
1054
  btn.addEventListener('click', function() {
1055
+ if (activeTarget) { addToTarget(btn, 'tool'); return; }
1056
+
1022
1057
  var row = rowFromLayoutValue(layout);
1023
1058
 
1024
1059
  var added = addNode('row', row, function() {
@@ -1055,6 +1090,8 @@ function build(instance, baseElem, optionsOrMethod) {
1055
1090
 
1056
1091
  dom.attr(button, { 'data-ge-toolbar': 'container', 'data-ge-container-type': type });
1057
1092
  button.addEventListener('click', function() {
1093
+ if (activeTarget) { addToTarget(button, 'tool'); return; }
1094
+
1058
1095
  var row = createRow();
1059
1096
  var column = row.appendChild(createColumn(MAX_COL_SIZE));
1060
1097
  var container = column.appendChild(definition.create({}));
@@ -1184,6 +1221,8 @@ function build(instance, baseElem, optionsOrMethod) {
1184
1221
  'data-ge-item': index,
1185
1222
  });
1186
1223
  button.addEventListener('click', function() {
1224
+ if (activeTarget) { addToTarget(button, item.source || 'tool'); return; }
1225
+
1187
1226
  var made = item.create();
1188
1227
 
1189
1228
  // One that belongs in a column brings a row and a column
@@ -1339,7 +1378,7 @@ function build(instance, baseElem, optionsOrMethod) {
1339
1378
  * not have what it makes - a section dropped on a column - gives way
1340
1379
  * to the canvas, just after the top level block the pointer was in.
1341
1380
  */
1342
- function insertFeatureFromToolbar(button, where) {
1381
+ function insertFeatureFromToolbar(button, where, source) {
1343
1382
  var item = FEATURES[button.getAttribute('data-ge-feature')].toolbar[parseInt(button.getAttribute('data-ge-item'), 10)];
1344
1383
  var made = item.create();
1345
1384
  var placed = made;
@@ -1360,7 +1399,7 @@ function build(instance, baseElem, optionsOrMethod) {
1360
1399
  } else {
1361
1400
  where.region.appendChild(placed);
1362
1401
  }
1363
- }, { parent: where.region, source: item.source || 'dragdrop' });
1402
+ }, { parent: where.region, source: item.source || source || 'dragdrop' });
1364
1403
  }
1365
1404
 
1366
1405
  /** A detached row with one full width column holding `node`. */
@@ -1396,9 +1435,9 @@ function build(instance, baseElem, optionsOrMethod) {
1396
1435
  * The row or container a toolbar button stands for, made and put where
1397
1436
  * the pointer left it.
1398
1437
  */
1399
- function insertFromToolbar(button, where) {
1438
+ function insertFromToolbar(button, where, source) {
1400
1439
  if (button.getAttribute('data-ge-toolbar') === 'feature') {
1401
- return insertFeatureFromToolbar(button, where);
1440
+ return insertFeatureFromToolbar(button, where, source);
1402
1441
  }
1403
1442
 
1404
1443
  var container = button.getAttribute('data-ge-toolbar') === 'container';
@@ -1418,7 +1457,7 @@ function build(instance, baseElem, optionsOrMethod) {
1418
1457
  } else {
1419
1458
  where.region.appendChild(placed);
1420
1459
  }
1421
- }, { parent: where.region, source: 'dragdrop' });
1460
+ }, { parent: where.region, source: source || 'dragdrop' });
1422
1461
  }
1423
1462
 
1424
1463
  /**
@@ -1710,6 +1749,8 @@ function build(instance, baseElem, optionsOrMethod) {
1710
1749
  */
1711
1750
  function openSource() {
1712
1751
  deinit();
1752
+ // The canvas is made again from the html when it comes back
1753
+ changeTarget(null);
1713
1754
  htmlTextArea.style.height = (0.8 * document.documentElement.clientHeight) + 'px';
1714
1755
  htmlTextArea.value = canvas.innerHTML;
1715
1756
  dom.show(htmlTextArea);
@@ -1732,6 +1773,39 @@ function build(instance, baseElem, optionsOrMethod) {
1732
1773
  dom.hide(htmlTextArea);
1733
1774
  }
1734
1775
 
1776
+ /**
1777
+ * What can be the active target: a column, or a plugin's region - a
1778
+ * section. Not the canvas, which is where the toolbar adds without one.
1779
+ */
1780
+ function targetSelector() {
1781
+ return ['.column'].concat(pluginHooks('regions')).join(', ');
1782
+ }
1783
+
1784
+ function isTarget(node) {
1785
+ return !!node && node !== canvas && canvas.contains(node) && dom.is(node, targetSelector());
1786
+ }
1787
+
1788
+ /** The one way the active target changes, told only when it does. */
1789
+ function changeTarget(node) {
1790
+ if (node === activeTarget) { return; }
1791
+
1792
+ var from = activeTarget;
1793
+ if (from) { dom.removeClass(from, 'ge-active-target'); }
1794
+ activeTarget = node;
1795
+ if (node && dom.hasClass(canvas, 'ge-editing')) { dom.addClass(node, 'ge-active-target'); }
1796
+
1797
+ emit('target-change', { canvas: canvas, target: node, from: from });
1798
+ }
1799
+
1800
+ /**
1801
+ * A toolbar button's click with an active target: what it makes goes
1802
+ * where a drop at the target's end would put it.
1803
+ */
1804
+ function addToTarget(button, source) {
1805
+ var made = insertFromToolbar(button, { region: activeTarget, before: null }, source);
1806
+ if (made && made.scrollIntoView) { made.scrollIntoView({ behavior: 'smooth' }); }
1807
+ }
1808
+
1735
1809
  function init() {
1736
1810
  // The node whose settings were open is gone - deleted, say - and
1737
1811
  // its panel with it
@@ -1759,12 +1833,21 @@ function build(instance, baseElem, optionsOrMethod) {
1759
1833
  makeResizable();
1760
1834
  switchLayout(curView);
1761
1835
  refreshPreviews(canvas);
1836
+
1837
+ // Deleted, or taken out some other way, it is a target no more
1838
+ if (activeTarget && !canvas.contains(activeTarget)) {
1839
+ changeTarget(null);
1840
+ } else if (activeTarget) {
1841
+ dom.addClass(activeTarget, 'ge-active-target');
1842
+ }
1762
1843
  }
1763
1844
 
1764
1845
  function deinit() {
1765
1846
  // Its panel goes home to its drawer first, and both go together
1766
1847
  closeSettings();
1767
1848
  dom.removeClass(canvas, 'ge-editing ge-drag-drawer ge-dropping');
1849
+ // The mark goes, the target stays: getHtml comes straight back
1850
+ if (activeTarget) { dom.removeClass(activeTarget, 'ge-active-target'); }
1768
1851
  // Before the drawers and the text blocks come off: an editor
1769
1852
  // plugin closes its editors here, and finds them where it left them
1770
1853
  plugins('onBeforeDeinit');
@@ -1826,6 +1909,7 @@ function build(instance, baseElem, optionsOrMethod) {
1826
1909
  // The html being edited is what the canvas is left with
1827
1910
  if (sourceOpen) { closeSource(); }
1828
1911
  deinit();
1912
+ activeTarget = null;
1829
1913
  removeConfirmModal();
1830
1914
  removeSettingsPanels();
1831
1915
  removeDialog();
@@ -5150,6 +5234,23 @@ function build(instance, baseElem, optionsOrMethod) {
5150
5234
  * finishes before the canvas is rebuilt. The ones that do something,
5151
5235
  * rather than answer something, hand back the instance, to chain.
5152
5236
  */
5237
+ function apiSetActiveTarget(value) {
5238
+ if (!settings.active_target) {
5239
+ warnOnceHere('active_target', 'setActiveTarget needs the active_target setting: without it, it does nothing');
5240
+ return;
5241
+ }
5242
+ if (value === null || value === undefined) { changeTarget(null); return; }
5243
+
5244
+ var node = nodeFrom(value);
5245
+ if (!isTarget(node)) {
5246
+ var named = typeof value === 'string' ? value : (node ? '<' + node.tagName.toLowerCase() + '>' : String(value));
5247
+ warn('setActiveTarget: ' + named + ' is not a column or a region of the canvas');
5248
+ return;
5249
+ }
5250
+
5251
+ changeTarget(node);
5252
+ }
5253
+
5153
5254
  var own = {
5154
5255
  getHtml: getHtml,
5155
5256
  getPlainHtml: getPlainHtml,
@@ -5180,6 +5281,8 @@ function build(instance, baseElem, optionsOrMethod) {
5180
5281
  setLocale: setLocale,
5181
5282
  getUtility: getUtility,
5182
5283
  setUtility: setUtility,
5284
+ getActiveTarget: function() { return settings.active_target ? activeTarget : null; },
5285
+ setActiveTarget: apiSetActiveTarget,
5183
5286
  };
5184
5287
 
5185
5288
  Object.keys(own).forEach(function(name) {
@@ -516,6 +516,16 @@
516
516
  outline-offset: 1px;
517
517
  }
518
518
 
519
+ /* Where the toolbar's buttons add, with active_target. A node that is both
520
+ shows both: this one a little further out */
521
+ .ge-canvas.ge-editing .ge-active-target {
522
+ outline: 2px dashed var(--bs-success, #198754);
523
+ outline-offset: -2px;
524
+ }
525
+ .ge-canvas.ge-editing .ge-active-target.ge-settings-target {
526
+ box-shadow: 0 0 0 4px var(--bs-success, #198754);
527
+ }
528
+
519
529
  /* While editing */
520
530
  .ge-canvas.ge-editing {
521
531
 
@@ -107,6 +107,8 @@ export interface GridEditorOptions {
107
107
  text_tools?: HostTool[];
108
108
  drag_handle?: 'tool' | 'drawer';
109
109
  toolbar_drag?: 'auto' | boolean;
110
+ /** A click in a column or a section makes it where the toolbar's buttons add. Default false. */
111
+ active_target?: boolean;
110
112
  /** The plugins to use, of any kind; null is every one loaded. */
111
113
  plugins?: string[] | null;
112
114
  /** The text editors offered, in order. */
@@ -218,6 +220,13 @@ export interface ViewChangePayload {
218
220
  to: View;
219
221
  }
220
222
 
223
+ /** The active target changed: the column or region the toolbar adds to, or null for the canvas. */
224
+ export interface TargetChangePayload {
225
+ canvas: HTMLElement;
226
+ target: HTMLElement | null;
227
+ from: HTMLElement | null;
228
+ }
229
+
221
230
  /** The events, by name, with their payloads. Adding fires a specific name and the generic one. */
222
231
  export interface GridEditorEventMap {
223
232
  'grideditor:before-add': Payload;
@@ -255,6 +264,7 @@ export interface GridEditorEventMap {
255
264
  'grideditor:after-copy': Payload;
256
265
  'grideditor:popup-orphan': PopupOrphanPayload;
257
266
  'grideditor:view-change': ViewChangePayload;
267
+ 'grideditor:target-change': TargetChangePayload;
258
268
  }
259
269
 
260
270
  /** An event the editor dispatches on its canvas: a CustomEvent whose detail is the payload. */
@@ -343,6 +353,10 @@ export declare class GridEditor {
343
353
  /** Take the editor off: the markup stays, everything else goes. */
344
354
  destroy(): this;
345
355
  changeView(view: View): this;
356
+ /** The column or region the toolbar adds to, or null. Null without active_target. */
357
+ getActiveTarget(): HTMLElement | null;
358
+ /** Make a column or region, or the first a selector matches, where the toolbar adds; null for none. */
359
+ setActiveTarget(target: HTMLElement | string | null): this;
346
360
  getView(): View;
347
361
  setLocale(code: string): this;
348
362
  createRow(layout?: RowLayout, options?: Placement): HTMLElement | null;