@themarioga/grid-editor 9.0.0-beta.1 → 9.0.0-beta.3

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.
@@ -0,0 +1,94 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
5
+
6
+ <!-- CSS dependencies -->
7
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
8
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/font/bootstrap-icons.css">
9
+
10
+ <!-- Javascript dependencies -->
11
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>
12
+
13
+ <!-- Grid editor CSS -->
14
+ <link rel="stylesheet" type="text/css" href="../dist/grideditor.css" />
15
+ <!-- Grid editor javascript -->
16
+ <script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.6/Sortable.min.js"></script>
17
+ <script src="../dist/grideditor.min.js"></script>
18
+ <!-- Undo and redo are a plugin of its own, loaded after the editor. The
19
+ others are here so there is a bit of everything to undo. -->
20
+ <script src="../dist/plugins/grideditor.history.min.js"></script>
21
+ <script src="../dist/plugins/grideditor.clipboard.min.js"></script>
22
+ <script src="../dist/plugins/grideditor.tabs.min.js"></script>
23
+ <script src="../dist/plugins/grideditor.elements.min.js"></script>
24
+
25
+ <script>
26
+ document.addEventListener('DOMContentLoaded', function() {
27
+ var grid = document.getElementById('myGrid');
28
+ var log = document.getElementById('log');
29
+
30
+ new GridEditor(grid, {
31
+ new_row_layouts: [[12], [6, 6], [4, 4, 4]],
32
+ content_types: [],
33
+ // What was selected is selected again after an undo
34
+ settings_panel: 'sidebar',
35
+ history: {
36
+ max: 50, // the steps that can be undone
37
+ delay: 500, // a drag or a burst of changes is one step
38
+ },
39
+ });
40
+
41
+ // A step done says where it came from: 'shortcut', 'tool' or 'api'
42
+ ['undo', 'redo'].forEach(function(name) {
43
+ grid.addEventListener('grideditor:after-' + name, function(e) {
44
+ log.textContent = name + ' (' + e.detail.source + ')';
45
+ });
46
+ });
47
+ });
48
+ </script>
49
+
50
+ <title>Grid Editor example - undo and redo</title>
51
+ </head>
52
+ <body>
53
+ <!-- Back to the demo index -->
54
+ <div class="container pt-3">
55
+ <a class="link-secondary" href="index.html"><i class="bi bi-arrow-left"></i> All demos</a>
56
+ </div>
57
+
58
+ <div class="container py-3">
59
+ <h2>Undo and redo</h2>
60
+ <p class="text-body-secondary">
61
+ Delete a row, drag a column to another size, paste something, change
62
+ an id in the panel: each is a step. <kbd>Ctrl</kbd>+<kbd>Z</kbd>
63
+ (<kbd>⌘</kbd>+<kbd>Z</kbd> on a Mac) goes back one, and
64
+ <kbd>Ctrl</kbd>+<kbd>Y</kbd> or <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>Z</kbd>
65
+ comes forward again, as do the <i class="bi bi-arrow-counterclockwise"></i>
66
+ and <i class="bi bi-arrow-clockwise"></i> buttons on the right of
67
+ the toolbar. A change made after going back ends what could be redone.
68
+ </p>
69
+ <p class="text-body-secondary">
70
+ While you are writing in a text or a field, the keys undo what you
71
+ typed there, as usual; it becomes a step when you are done. The
72
+ column or row selected in the sidebar is selected again after a step.
73
+ <span id="log" class="fw-semibold"></span>
74
+ </p>
75
+
76
+ <div id="myGrid">
77
+ <div class="row">
78
+ <div class="col-lg-6">
79
+ <h3>Change this page</h3>
80
+ <p>Then undo it.</p>
81
+ </div>
82
+ <div class="col-lg-6">
83
+ <p>Delete this column, or the whole row.</p>
84
+ </div>
85
+ </div>
86
+ <div class="row">
87
+ <div class="col-lg-4"><p>One.</p></div>
88
+ <div class="col-lg-4"><p>Two.</p></div>
89
+ <div class="col-lg-4"><p>Three.</p></div>
90
+ </div>
91
+ </div>
92
+ </div>
93
+ </body>
94
+ </html>
@@ -178,6 +178,14 @@
178
178
  <span class="text-body-secondary">Copying a row, a column or a container and pasting it somewhere else, in this editor or another</span>
179
179
  </a>
180
180
 
181
+ <a class="list-group-item list-group-item-action" href="history.html">
182
+ <div class="d-flex justify-content-between">
183
+ <strong>Undo and redo</strong>
184
+ <code class="text-body-secondary">history.html</code>
185
+ </div>
186
+ <span class="text-body-secondary">The history plugin: Ctrl+Z and Ctrl+Y, and buttons on the toolbar, over what the editor changes</span>
187
+ </a>
188
+
181
189
  <a class="list-group-item list-group-item-action" href="wrap_content.html">
182
190
  <div class="d-flex justify-content-between">
183
191
  <strong>Wrapped content</strong>
package/package.json CHANGED
@@ -32,7 +32,7 @@
32
32
  "publishConfig": {
33
33
  "access": "public"
34
34
  },
35
- "version": "9.0.0-beta.1",
35
+ "version": "9.0.0-beta.3",
36
36
  "main": "dist/grideditor.js",
37
37
  "module": "dist/grideditor.esm.js",
38
38
  "types": "dist/grideditor.d.ts",
@@ -1614,6 +1614,8 @@ function build(instance, baseElem, optionsOrMethod) {
1614
1614
  var colour = item.align === 'end' ? 'btn-primary' : 'btn-light';
1615
1615
  var button = labelButton(dom.element('a', { 'class': 'btn btn-sm ' + colour + ' ge-add-container ge-add-feature' }), label, iconClass);
1616
1616
 
1617
+ var acting = typeof item.action === 'function';
1618
+
1617
1619
  dom.addClass(button, item.className || '');
1618
1620
  // Hidden as it is now: a plugin that shows and hides its buttons
1619
1621
  // only hears of what changes, not of the toolbar being built again
@@ -1623,7 +1625,21 @@ function build(instance, baseElem, optionsOrMethod) {
1623
1625
  'data-ge-feature': name,
1624
1626
  'data-ge-item': index,
1625
1627
  });
1628
+ if (acting) {
1629
+ // Not a block to add: it is not carried onto the canvas, and
1630
+ // the active target is none of its business
1631
+ button.setAttribute('data-ge-action', '');
1632
+ if (typeof item.disabled === 'function' ? item.disabled() : item.disabled) {
1633
+ dom.addClass(button, 'disabled');
1634
+ button.setAttribute('aria-disabled', 'true');
1635
+ }
1636
+ }
1626
1637
  button.addEventListener('click', function() {
1638
+ if (acting) {
1639
+ if (!dom.hasClass(button, 'disabled')) { item.action(button); }
1640
+ return;
1641
+ }
1642
+
1627
1643
  if (activeTarget) { addToTarget(button, item.source || 'tool'); return; }
1628
1644
 
1629
1645
  var made = item.create();
@@ -1661,7 +1677,7 @@ function build(instance, baseElem, optionsOrMethod) {
1661
1677
  }
1662
1678
 
1663
1679
  function makeToolbarDraggable() {
1664
- var buttons = dom.all(mainControls, '[data-ge-toolbar]');
1680
+ var buttons = dom.all(mainControls, '[data-ge-toolbar]:not([data-ge-action])');
1665
1681
 
1666
1682
  buttons.forEach(function(button) {
1667
1683
  dom.removeClass(button, 'ge-palette-button');
@@ -2622,6 +2638,40 @@ function build(instance, baseElem, optionsOrMethod) {
2622
2638
  ? dom.all(mainControls, '[data-ge-toolbar="feature"][data-ge-feature="' + name + '"]')
2623
2639
  : [];
2624
2640
  },
2641
+ getSelected: function() { return panelMode() === 'sidebar' ? selected : null; },
2642
+ getActiveTarget: function() { return settings.active_target ? activeTarget : null; },
2643
+ // True when it took: a node that cannot be, or no setting
2644
+ // for it, says nothing, since a plugin putting back what it
2645
+ // remembers cannot know the node is still one
2646
+ setSelected: function(node) {
2647
+ var chosen = panelMode() === 'sidebar' && !!node && node !== canvas && canvas.contains(node) && detailsFor.has(node);
2648
+ if (chosen) { changeSelection(node); }
2649
+ return chosen;
2650
+ },
2651
+ setActiveTarget: function(node) {
2652
+ var chosen = !!settings.active_target && isTarget(node);
2653
+ if (chosen) { changeTarget(node); }
2654
+ return chosen;
2655
+ },
2656
+ // What keeps Escape from the selection and the target, and an
2657
+ // inline panel unfolded in a drawer, which Escape leaves alone
2658
+ panelOpen: function() {
2659
+ if (openSettingsState || dialogState) { return true; }
2660
+ if (confirmDialog && dom.hasClass(confirmDialog, 'show')) { return true; }
2661
+
2662
+ return panelMode() === 'inline' && dom.all(canvas, '.ge-tools-drawer > .ge-details').some(dom.visible);
2663
+ },
2664
+ // The toolbar and the settings panels are outside the canvas
2665
+ contains: function(node) {
2666
+ if (!node || node.nodeType !== 1) { return false; }
2667
+ if (canvas.contains(node) || (mainControls && mainControls.contains(node))) { return true; }
2668
+ if (dialogPanel && dialogPanel.contains(node)) { return true; }
2669
+ if (confirmDialog && confirmDialog.contains(node)) { return true; }
2670
+
2671
+ return Object.keys(settingsPanels).some(function(mode) {
2672
+ return settingsPanels[mode].contains(node);
2673
+ });
2674
+ },
2625
2675
  };
2626
2676
  }
2627
2677
 
@@ -175,6 +175,8 @@ GridEditor.locales.es = {
175
175
  'autosave.restore_message': 'Hay un borrador de esta página guardado el {date}, con cambios que no se publicaron. ¿Quieres recuperarlo?',
176
176
  'autosave.restore': 'Recuperar',
177
177
  'autosave.discard': 'Descartar',
178
+ 'history.undo': 'Deshacer',
179
+ 'history.redo': 'Rehacer',
178
180
  'utility.col_from_row': 'De la fila: {count}',
179
181
  'utility.row_cols': 'Columnas por fila',
180
182
  'badge.row_cols': '{count} por fila',
@@ -17,6 +17,11 @@
17
17
  *
18
18
  * A paste is an add like any other - before-add-* and after-add-*, with
19
19
  * `source: 'paste'` - so a host can turn one away.
20
+ *
21
+ * With active_target (which the sidebar turns on), Ctrl+C copies the node
22
+ * selected, or the target, and Ctrl+V pastes into the target, or the node
23
+ * selected. `clipboard: { shortcuts: false }` turns them off. They go through
24
+ * the same store as the tools, never the system clipboard.
20
25
  */
21
26
  import { GridEditor } from '../grideditor.js';
22
27
  import * as dom from '../dom.js';
@@ -85,10 +90,41 @@ window.addEventListener('storage', function(e) {
85
90
  if (e.key === STORAGE_ITEM) { announce(); }
86
91
  });
87
92
 
93
+ /** The editors listening for the shortcuts, and the one used last, which a key press goes to. */
94
+ var listeningEditors = [];
95
+ var lastEditor = null;
96
+
97
+ function track(e) {
98
+ lastEditor = listeningEditors.filter(function(ge) { return ge.contains(e.target); })[0] || null;
99
+ }
100
+
101
+ // Captured: a control that stops the event still counts as using its editor
102
+ document.addEventListener('pointerdown', track, true);
103
+ document.addEventListener('focusin', track, true);
104
+
105
+ /** 'c', 'v' or null, from the letter typed or, on a layout without Latin letters, the key's place. */
106
+ function shortcutKey(e) {
107
+ var key = (e.key || '').toLowerCase();
108
+ if (!/^[a-z]$/.test(key)) { key = { KeyC: 'c', KeyV: 'v' }[e.code] || ''; }
109
+
110
+ return key === 'c' || key === 'v' ? key : null;
111
+ }
112
+
113
+ function typing() {
114
+ var focus = document.activeElement;
115
+ return !!focus && (dom.is(focus, 'input, textarea, select') || focus.isContentEditable);
116
+ }
117
+
118
+ function textSelected() {
119
+ var selection = window.getSelection ? window.getSelection() : null;
120
+ return !!selection && !selection.isCollapsed && selection.toString() !== '';
121
+ }
122
+
88
123
  GridEditor.features.clipboard = function(ge) {
89
124
 
90
125
  var shown = null; // What the toolbar's paste buttons stand for
91
126
  var listening = false; // Whether refresh is on the document's change event
127
+ var shortcuts = shortcutsSetting();
92
128
 
93
129
  function used(name) {
94
130
  return !ge.settings.plugins || ge.settings.plugins.indexOf(name) !== -1;
@@ -115,11 +151,29 @@ GridEditor.features.clipboard = function(ge) {
115
151
  return ['row', 'column', 'section', 'text', 'element'].indexOf(kind) !== -1 ? kind : null;
116
152
  }
117
153
 
118
- function copy(node, kind) {
154
+ /** On, unless the host said otherwise: with no target there is nothing for a key to act on. */
155
+ function shortcutsSetting() {
156
+ var wanted = (ge.settings.clipboard || {}).shortcuts;
157
+ var target = !!ge.settings.active_target;
158
+
159
+ if (wanted !== undefined && typeof wanted !== 'boolean') {
160
+ ge.warn('clipboard.shortcuts is true or false: ' + (target ? 'on' : 'off') + ', as with active_target, is used');
161
+ wanted = undefined;
162
+ }
163
+ if (wanted === undefined) { return target; }
164
+ if (wanted && !target) {
165
+ ge.warn('clipboard.shortcuts needs active_target, or settings_panel "sidebar": without it, Ctrl+C and Ctrl+V do nothing');
166
+ return false;
167
+ }
168
+
169
+ return wanted;
170
+ }
171
+
172
+ function copy(node, kind, source) {
119
173
  var html = ge.nodeHtml(node);
120
174
 
121
175
  write({ version: VERSION, category: categoryOf(kind), kind: kind, html: html });
122
- ge.emit('after-copy', ge.payloadFor(kind, node, { source: 'tool' }));
176
+ ge.emit('after-copy', ge.payloadFor(kind, node, { source: source || 'tool' }));
123
177
 
124
178
  // The canvas came back with new drawers: the one to flash is the
125
179
  // node's new copy tool
@@ -142,6 +196,67 @@ GridEditor.features.clipboard = function(ge) {
142
196
  ge.place(ge.freshNode(clip.html), clip.kind, { appendTo: target, source: 'paste' });
143
197
  }
144
198
 
199
+ /**
200
+ * Ctrl+V: into the target as the toolbar's paste button would - a
201
+ * section, which only the canvas takes, after the block the target is in
202
+ * - and what that button has none for as the target's paste tool would.
203
+ * With no target, as the selected node's paste tool. What does not fit
204
+ * is not pasted anywhere else.
205
+ */
206
+ function pasteByShortcut() {
207
+ var clip = read();
208
+ var target = ge.getActiveTarget();
209
+ var node = target || ge.getSelected();
210
+ if (!clip || !node) { return false; }
211
+
212
+ var placement = { appendTo: node, source: 'paste' };
213
+ if (target && clip.category === 'section') {
214
+ if (!available(clip)) { return false; }
215
+ var top = target;
216
+ while (top.parentElement && top.parentElement !== ge.canvas) { top = top.parentElement; }
217
+ placement = { insertAfter: top, source: 'paste' };
218
+ } else if (!fits(clip, TARGETS[ge.kindOf(node)] || [])) {
219
+ return false;
220
+ }
221
+
222
+ var made = ge.place(ge.freshNode(clip.html), clip.kind, placement);
223
+ if (made && made.scrollIntoView) { made.scrollIntoView({ behavior: 'smooth' }); }
224
+
225
+ return true;
226
+ }
227
+
228
+ function copyByShortcut() {
229
+ if (textSelected()) { return false; }
230
+
231
+ var node = ge.getSelected() || ge.getActiveTarget();
232
+ var kind = node && ge.kindOf(node);
233
+ if (!kind || !categoryOf(kind)) { return false; }
234
+
235
+ copy(node, kind, 'shortcut');
236
+ return true;
237
+ }
238
+
239
+ function onKeydown(e) {
240
+ if (lastEditor !== ge || e.repeat || e.shiftKey || e.altKey || !(e.ctrlKey || e.metaKey)) { return; }
241
+ var key = shortcutKey(e);
242
+ if (!key || !dom.hasClass(ge.canvas, 'ge-editing') || typing() || ge.panelOpen()) { return; }
243
+
244
+ // Left alone, it is the browser's: a copy of the text, a paste in a field
245
+ if (key === 'c' ? copyByShortcut() : pasteByShortcut()) { e.preventDefault(); }
246
+ }
247
+
248
+ function listenForShortcuts(on) {
249
+ var at = listeningEditors.indexOf(ge);
250
+
251
+ if (on && at === -1) {
252
+ listeningEditors.push(ge);
253
+ document.addEventListener('keydown', onKeydown);
254
+ } else if (!on && at !== -1) {
255
+ listeningEditors.splice(at, 1);
256
+ document.removeEventListener('keydown', onKeydown);
257
+ }
258
+ }
259
+
145
260
  /** Show the paste tools, and the toolbar's paste buttons, that fit what is copied. */
146
261
  function refresh() {
147
262
  var clip = read();
@@ -203,11 +318,19 @@ GridEditor.features.clipboard = function(ge) {
203
318
  document.addEventListener(CHANGE, refresh);
204
319
  listening = true;
205
320
  }
321
+ listenForShortcuts(shortcuts);
206
322
  },
207
323
 
208
324
  onDeinit: function() {
209
325
  document.removeEventListener(CHANGE, refresh);
210
326
  listening = false;
327
+ // A copy takes the canvas out of editing and back: the editor
328
+ // stays the one used last, as it is still
329
+ listenForShortcuts(false);
330
+ },
331
+
332
+ onDestroy: function() {
333
+ if (lastEditor === ge) { lastEditor = null; }
211
334
  },
212
335
 
213
336
  // Nothing of its own on the canvas: the tools are in the drawers