@themarioga/grid-editor 9.0.0-beta.2 → 9.0.0-beta.4
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/CHANGELOG.md +34 -0
- package/README.md +76 -8
- package/dist/grideditor.bundle.min.js +2 -2
- package/dist/grideditor.css +187 -0
- package/dist/grideditor.d.ts +75 -0
- package/dist/grideditor.esm.js +38 -11
- package/dist/grideditor.js +38 -11
- package/dist/grideditor.min.css +1 -1
- package/dist/grideditor.min.css.map +1 -1
- package/dist/grideditor.min.js +1 -1
- package/dist/grideditor.min.js.map +3 -3
- package/dist/locales/grideditor.es.esm.js +22 -0
- package/dist/locales/grideditor.es.js +22 -0
- package/dist/locales/grideditor.es.min.js +2 -2
- package/dist/locales/grideditor.es.min.js.map +2 -2
- package/dist/plugins/grideditor.history.esm.js +360 -0
- package/dist/plugins/grideditor.history.js +365 -0
- package/dist/plugins/grideditor.history.min.js +1 -0
- package/dist/plugins/grideditor.history.min.js.map +7 -0
- package/dist/plugins/grideditor.offcanvas.esm.js +551 -0
- package/dist/plugins/grideditor.offcanvas.js +556 -0
- package/dist/plugins/grideditor.offcanvas.min.js +1 -0
- package/dist/plugins/grideditor.offcanvas.min.js.map +7 -0
- package/dist/plugins/grideditor.popup.esm.js +90 -37
- package/dist/plugins/grideditor.popup.js +90 -37
- package/dist/plugins/grideditor.popup.min.js +1 -1
- package/dist/plugins/grideditor.popup.min.js.map +4 -4
- package/docs/events.md +7 -3
- package/docs/locale-keys.md +32 -1
- package/docs/plugins.md +19 -5
- package/example/clipboard.html +22 -0
- package/example/containers.html +22 -4
- package/example/history.html +94 -0
- package/example/index.html +9 -1
- package/package.json +1 -1
- package/src/js/grideditor.js +37 -9
- package/src/js/locales/grideditor.es.js +22 -0
- package/src/js/plugins/grideditor.history.js +367 -0
- package/src/js/plugins/grideditor.offcanvas.js +420 -0
- package/src/js/plugins/grideditor.popup.js +8 -70
- package/src/js/triggers/triggers.js +121 -0
- package/src/less/grideditor.less +60 -0
- package/types/grideditor.d.ts +75 -0
package/src/js/grideditor.js
CHANGED
|
@@ -52,9 +52,10 @@ var PLACEMENTS = ['appendTo', 'prependTo', 'insertAfter', 'insertBefore'];
|
|
|
52
52
|
* The attributes that point at an id, as the id itself or as a #selector.
|
|
53
53
|
* A copy that has to take new ids (see freshNode) takes them here too, so
|
|
54
54
|
* its tabs, its accordion and its popups still point at their own panes.
|
|
55
|
+
* A container plugin adds its own, as `references` on its definition.
|
|
55
56
|
*/
|
|
56
57
|
var REFERENCES = ['data-bs-target', 'data-bs-parent', 'href', 'aria-controls', 'aria-labelledby',
|
|
57
|
-
'aria-describedby', 'for'
|
|
58
|
+
'aria-describedby', 'for'];
|
|
58
59
|
|
|
59
60
|
/** An id grid-editor generated: ge-{type}-{counter}-{random}. */
|
|
60
61
|
var GENERATED_ID = /^ge-([a-z][a-z-]*?)-\d+-[a-z0-9]+$/;
|
|
@@ -1096,12 +1097,6 @@ function build(instance, baseElem, optionsOrMethod) {
|
|
|
1096
1097
|
/* A click on the host's plain content offers to make it a text */
|
|
1097
1098
|
dom.delegate(canvas, 'click', '.ge-content', onContentClick, signal);
|
|
1098
1099
|
|
|
1099
|
-
/* A trigger is often a link, and a link still navigates even
|
|
1100
|
-
with its Bootstrap attributes suspended */
|
|
1101
|
-
dom.delegate(canvas, 'click', '.ge-popup-trigger, [data-ge-popup-target]', function(e) {
|
|
1102
|
-
if (dom.hasClass(canvas, 'ge-editing')) { e.preventDefault(); }
|
|
1103
|
-
}, signal);
|
|
1104
|
-
|
|
1105
1100
|
var sidebarMode = panelMode() === 'sidebar';
|
|
1106
1101
|
|
|
1107
1102
|
if (settings.active_target || sidebarMode) {
|
|
@@ -1614,6 +1609,8 @@ function build(instance, baseElem, optionsOrMethod) {
|
|
|
1614
1609
|
var colour = item.align === 'end' ? 'btn-primary' : 'btn-light';
|
|
1615
1610
|
var button = labelButton(dom.element('a', { 'class': 'btn btn-sm ' + colour + ' ge-add-container ge-add-feature' }), label, iconClass);
|
|
1616
1611
|
|
|
1612
|
+
var acting = typeof item.action === 'function';
|
|
1613
|
+
|
|
1617
1614
|
dom.addClass(button, item.className || '');
|
|
1618
1615
|
// Hidden as it is now: a plugin that shows and hides its buttons
|
|
1619
1616
|
// only hears of what changes, not of the toolbar being built again
|
|
@@ -1623,7 +1620,21 @@ function build(instance, baseElem, optionsOrMethod) {
|
|
|
1623
1620
|
'data-ge-feature': name,
|
|
1624
1621
|
'data-ge-item': index,
|
|
1625
1622
|
});
|
|
1623
|
+
if (acting) {
|
|
1624
|
+
// Not a block to add: it is not carried onto the canvas, and
|
|
1625
|
+
// the active target is none of its business
|
|
1626
|
+
button.setAttribute('data-ge-action', '');
|
|
1627
|
+
if (typeof item.disabled === 'function' ? item.disabled() : item.disabled) {
|
|
1628
|
+
dom.addClass(button, 'disabled');
|
|
1629
|
+
button.setAttribute('aria-disabled', 'true');
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1626
1632
|
button.addEventListener('click', function() {
|
|
1633
|
+
if (acting) {
|
|
1634
|
+
if (!dom.hasClass(button, 'disabled')) { item.action(button); }
|
|
1635
|
+
return;
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1627
1638
|
if (activeTarget) { addToTarget(button, item.source || 'tool'); return; }
|
|
1628
1639
|
|
|
1629
1640
|
var made = item.create();
|
|
@@ -1661,7 +1672,7 @@ function build(instance, baseElem, optionsOrMethod) {
|
|
|
1661
1672
|
}
|
|
1662
1673
|
|
|
1663
1674
|
function makeToolbarDraggable() {
|
|
1664
|
-
var buttons = dom.all(mainControls, '[data-ge-toolbar]');
|
|
1675
|
+
var buttons = dom.all(mainControls, '[data-ge-toolbar]:not([data-ge-action])');
|
|
1665
1676
|
|
|
1666
1677
|
buttons.forEach(function(button) {
|
|
1667
1678
|
dom.removeClass(button, 'ge-palette-button');
|
|
@@ -2624,6 +2635,19 @@ function build(instance, baseElem, optionsOrMethod) {
|
|
|
2624
2635
|
},
|
|
2625
2636
|
getSelected: function() { return panelMode() === 'sidebar' ? selected : null; },
|
|
2626
2637
|
getActiveTarget: function() { return settings.active_target ? activeTarget : null; },
|
|
2638
|
+
// True when it took: a node that cannot be, or no setting
|
|
2639
|
+
// for it, says nothing, since a plugin putting back what it
|
|
2640
|
+
// remembers cannot know the node is still one
|
|
2641
|
+
setSelected: function(node) {
|
|
2642
|
+
var chosen = panelMode() === 'sidebar' && !!node && node !== canvas && canvas.contains(node) && detailsFor.has(node);
|
|
2643
|
+
if (chosen) { changeSelection(node); }
|
|
2644
|
+
return chosen;
|
|
2645
|
+
},
|
|
2646
|
+
setActiveTarget: function(node) {
|
|
2647
|
+
var chosen = !!settings.active_target && isTarget(node);
|
|
2648
|
+
if (chosen) { changeTarget(node); }
|
|
2649
|
+
return chosen;
|
|
2650
|
+
},
|
|
2627
2651
|
// What keeps Escape from the selection and the target, and an
|
|
2628
2652
|
// inline panel unfolded in a drawer, which Escape leaves alone
|
|
2629
2653
|
panelOpen: function() {
|
|
@@ -3352,8 +3376,12 @@ function build(instance, baseElem, optionsOrMethod) {
|
|
|
3352
3376
|
|
|
3353
3377
|
if (!Object.keys(renamed).length) { return node; }
|
|
3354
3378
|
|
|
3379
|
+
var references = Object.keys(CONTAINERS).reduce(function(names, type) {
|
|
3380
|
+
return names.concat(CONTAINERS[type].references || []);
|
|
3381
|
+
}, REFERENCES);
|
|
3382
|
+
|
|
3355
3383
|
dom.selfAndAll(node, '*').forEach(function(element) {
|
|
3356
|
-
|
|
3384
|
+
references.forEach(function(name) {
|
|
3357
3385
|
var value = element.getAttribute(name);
|
|
3358
3386
|
if (value === null) { return; }
|
|
3359
3387
|
if (name === 'href' && value.charAt(0) !== '#') { return; }
|
|
@@ -44,6 +44,7 @@ GridEditor.locales.es = {
|
|
|
44
44
|
'tool.delete_pane': 'Borrar panel',
|
|
45
45
|
'tool.rename': 'Haz doble clic para renombrar',
|
|
46
46
|
'tool.toggle_popup': 'Plegar esta ventana mientras editas',
|
|
47
|
+
'tool.toggle_offcanvas': 'Plegar este panel mientras editas',
|
|
47
48
|
'tool.element_info': 'Elemento: {name}',
|
|
48
49
|
'tool.column_narrower': 'Estrechar columna\n(Mayús para el mínimo)',
|
|
49
50
|
'tool.column_wider': 'Ensanchar columna\n(Mayús para el máximo)',
|
|
@@ -134,6 +135,25 @@ GridEditor.locales.es = {
|
|
|
134
135
|
'container.add_popup': 'Ventana emergente',
|
|
135
136
|
'container.popup_title': 'Título',
|
|
136
137
|
'container.popup_trigger': 'Abrir',
|
|
138
|
+
'container.add_offcanvas': 'Panel deslizante',
|
|
139
|
+
'container.offcanvas_title': 'Título',
|
|
140
|
+
'container.offcanvas_trigger': 'Abrir',
|
|
141
|
+
'container.offcanvas_section': 'Panel deslizante',
|
|
142
|
+
'container.offcanvas_placement': 'Posición',
|
|
143
|
+
'container.offcanvas_start': 'Izquierda',
|
|
144
|
+
'container.offcanvas_end': 'Derecha',
|
|
145
|
+
'container.offcanvas_top': 'Arriba',
|
|
146
|
+
'container.offcanvas_bottom': 'Abajo',
|
|
147
|
+
'container.offcanvas_backdrop': 'Fondo',
|
|
148
|
+
'container.offcanvas_backdrop_yes': 'Sí',
|
|
149
|
+
'container.offcanvas_backdrop_no': 'No',
|
|
150
|
+
'container.offcanvas_backdrop_static': 'Estático',
|
|
151
|
+
'container.offcanvas_scroll': 'Permitir scroll de la página',
|
|
152
|
+
'container.offcanvas_keyboard': 'Cerrar con Esc',
|
|
153
|
+
'container.offcanvas_responsive': 'Responsive',
|
|
154
|
+
'container.offcanvas_responsive_never': 'Siempre panel',
|
|
155
|
+
'container.offcanvas_responsive_from': 'Contenido normal desde {breakpoint}',
|
|
156
|
+
'container.offcanvas_dark': 'Tema oscuro',
|
|
137
157
|
'container.add_card': 'Tarjeta',
|
|
138
158
|
'container.card_title': 'Título de la tarjeta',
|
|
139
159
|
'container.card_footer': 'Pie de la tarjeta',
|
|
@@ -175,6 +195,8 @@ GridEditor.locales.es = {
|
|
|
175
195
|
'autosave.restore_message': 'Hay un borrador de esta página guardado el {date}, con cambios que no se publicaron. ¿Quieres recuperarlo?',
|
|
176
196
|
'autosave.restore': 'Recuperar',
|
|
177
197
|
'autosave.discard': 'Descartar',
|
|
198
|
+
'history.undo': 'Deshacer',
|
|
199
|
+
'history.redo': 'Rehacer',
|
|
178
200
|
'utility.col_from_row': 'De la fila: {count}',
|
|
179
201
|
'utility.row_cols': 'Columnas por fila',
|
|
180
202
|
'badge.row_cols': '{count} por fila',
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Undo and redo for grid-editor.
|
|
3
|
+
*
|
|
4
|
+
* A feature plugin: load this file after the editor and what is edited is
|
|
5
|
+
* kept, step by step, in memory, so Ctrl+Z goes back to how it was and
|
|
6
|
+
* Ctrl+Y (or Ctrl+Shift+Z) comes forward again. Two buttons on the right of
|
|
7
|
+
* the toolbar do the same. What it can ask the editor for is the handle its
|
|
8
|
+
* factory is called with, described in docs/plugins.md.
|
|
9
|
+
*
|
|
10
|
+
* <script src="dist/grideditor.min.js"></script>
|
|
11
|
+
* <script src="dist/plugins/grideditor.history.min.js"></script>
|
|
12
|
+
*
|
|
13
|
+
* A step is the canvas's markup as ge.snapshotHtml() gives it, kept `delay`
|
|
14
|
+
* ms after the last change: a drag or a burst of typing is one step. Going
|
|
15
|
+
* to one puts its markup back with ge.setHtml(), and the node that was
|
|
16
|
+
* selected, and the target, are chosen again by where they were.
|
|
17
|
+
*
|
|
18
|
+
* Typing in a text is the text editor's own to undo while it has the focus:
|
|
19
|
+
* the keys are left alone then, and what was typed becomes a step when it
|
|
20
|
+
* is done.
|
|
21
|
+
*
|
|
22
|
+
* new GridEditor('#myGrid', { history: { max: 100, buttons: false } });
|
|
23
|
+
*/
|
|
24
|
+
import { GridEditor } from '../grideditor.js';
|
|
25
|
+
import * as dom from '../dom.js';
|
|
26
|
+
|
|
27
|
+
Object.assign(GridEditor.locales.en, {
|
|
28
|
+
'history.undo': 'Undo',
|
|
29
|
+
'history.redo': 'Redo',
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
var DEFAULT_MAX = 50;
|
|
33
|
+
var DEFAULT_DELAY = 500;
|
|
34
|
+
|
|
35
|
+
/** What the editor puts in a node that is not the node's own: a position counts without it. */
|
|
36
|
+
var FURNITURE = '.ge-tools-drawer, .ge-drop-marker';
|
|
37
|
+
|
|
38
|
+
/** What a drag or a resize leaves on the canvas while it lasts. */
|
|
39
|
+
var BUSY = '.ge-drag-chosen, .ge-drag-placeholder, .ge-drag-helper, .ge-resizing';
|
|
40
|
+
|
|
41
|
+
/** The editors listening for the keys, and the one used last, which a key press goes to. */
|
|
42
|
+
var listeningEditors = [];
|
|
43
|
+
var lastEditor = null;
|
|
44
|
+
|
|
45
|
+
function track(e) {
|
|
46
|
+
lastEditor = listeningEditors.filter(function(ge) { return ge.contains(e.target); })[0] || null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Captured: a control that stops the event still counts as using its editor
|
|
50
|
+
document.addEventListener('pointerdown', track, true);
|
|
51
|
+
document.addEventListener('focusin', track, true);
|
|
52
|
+
|
|
53
|
+
/** 'z', 'y' or null, from the letter typed or, on a layout without Latin letters, the key's place. */
|
|
54
|
+
function shortcutKey(e) {
|
|
55
|
+
var key = (e.key || '').toLowerCase();
|
|
56
|
+
if (!/^[a-z]$/.test(key)) { key = { KeyZ: 'z', KeyY: 'y' }[e.code] || ''; }
|
|
57
|
+
|
|
58
|
+
return key === 'z' || key === 'y' ? key : null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function typing() {
|
|
62
|
+
var focus = document.activeElement;
|
|
63
|
+
return !!focus && (dom.is(focus, 'input, textarea, select') || focus.isContentEditable);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The children of `parent` that are its own content, without the editor's furniture. */
|
|
67
|
+
function contentOf(parent) {
|
|
68
|
+
return dom.children(parent).filter(function(child) { return !dom.is(child, FURNITURE); });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
GridEditor.features.history = function(ge) {
|
|
72
|
+
|
|
73
|
+
var options = optionsFrom(ge.settings.history || {});
|
|
74
|
+
|
|
75
|
+
var stack = []; // The markup of each step, oldest first
|
|
76
|
+
var index = -1; // The step the canvas is at
|
|
77
|
+
var shown = { canUndo: false, canRedo: false }; // What the last history-change said
|
|
78
|
+
var timer = null;
|
|
79
|
+
var observer = null;
|
|
80
|
+
var observing = false;
|
|
81
|
+
var initiated = false; // Whether the first init has been looked at
|
|
82
|
+
var started = false; // Whether there is a first step to go back to
|
|
83
|
+
var sourceOpen = false;
|
|
84
|
+
var destroyed = false;
|
|
85
|
+
|
|
86
|
+
function optionsFrom(given) {
|
|
87
|
+
var result = {
|
|
88
|
+
shortcuts: given.shortcuts === undefined ? true : given.shortcuts,
|
|
89
|
+
buttons: given.buttons === undefined ? true : given.buttons,
|
|
90
|
+
max: given.max === undefined ? DEFAULT_MAX : given.max,
|
|
91
|
+
delay: given.delay === undefined ? DEFAULT_DELAY : given.delay,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
['shortcuts', 'buttons'].forEach(function(name) {
|
|
95
|
+
if (typeof result[name] !== 'boolean') {
|
|
96
|
+
ge.warn('history.' + name + ' is true or false: true is used');
|
|
97
|
+
result[name] = true;
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
if (!Number.isInteger(result.max) || result.max < 1) {
|
|
101
|
+
ge.warn('history.max must be a whole number of steps, 1 or more: ' + DEFAULT_MAX + ' is used');
|
|
102
|
+
result.max = DEFAULT_MAX;
|
|
103
|
+
}
|
|
104
|
+
if (!Number.isInteger(result.delay) || result.delay < 0) {
|
|
105
|
+
ge.warn('history.delay must be a whole number of milliseconds, 0 or more: ' + DEFAULT_DELAY + ' is used');
|
|
106
|
+
result.delay = DEFAULT_DELAY;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function observe() {
|
|
113
|
+
observer.observe(ge.canvas, { subtree: true, childList: true, attributes: true, characterData: true });
|
|
114
|
+
observing = true;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function stopObserving() {
|
|
118
|
+
if (observer) { observer.disconnect(); }
|
|
119
|
+
observing = false;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** The canvas's markup now. Reading may change the canvas, when a plugin cannot be read on a copy, and that is no change of the user's. */
|
|
123
|
+
function read() {
|
|
124
|
+
var watching = observing;
|
|
125
|
+
if (watching) { stopObserving(); }
|
|
126
|
+
var html = ge.snapshotHtml();
|
|
127
|
+
if (watching) { observe(); }
|
|
128
|
+
|
|
129
|
+
return html;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function editing() {
|
|
133
|
+
return dom.hasClass(ge.canvas, 'ge-editing');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** A drag or a resize is going on: the canvas is not at a step but between two. */
|
|
137
|
+
function busy() {
|
|
138
|
+
return dom.hasClass(ge.canvas, 'ge-dropping') || !!dom.one(ge.canvas, BUSY);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function schedule() {
|
|
142
|
+
window.clearTimeout(timer);
|
|
143
|
+
timer = window.setTimeout(commit, options.delay);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Whether something changed since the last step that is not a step yet. */
|
|
147
|
+
function pending() {
|
|
148
|
+
return timer !== null && read() !== stack[index];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** What is on the canvas, if it is not the step it is at, becomes the next one. */
|
|
152
|
+
function commit() {
|
|
153
|
+
window.clearTimeout(timer);
|
|
154
|
+
timer = null;
|
|
155
|
+
|
|
156
|
+
if (!started || destroyed || sourceOpen) { return; }
|
|
157
|
+
if (busy()) { schedule(); return; }
|
|
158
|
+
|
|
159
|
+
var html = read();
|
|
160
|
+
if (html === stack[index]) { return; }
|
|
161
|
+
|
|
162
|
+
stack.splice(index + 1);
|
|
163
|
+
stack.push(html);
|
|
164
|
+
if (stack.length > options.max + 1) { stack.shift(); }
|
|
165
|
+
index = stack.length - 1;
|
|
166
|
+
|
|
167
|
+
notify();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** The buttons, and a history-change when what can be done is not what was last said. */
|
|
171
|
+
function notify() {
|
|
172
|
+
var now = { canUndo: index > 0, canRedo: index < stack.length - 1 };
|
|
173
|
+
var changed = now.canUndo !== shown.canUndo || now.canRedo !== shown.canRedo;
|
|
174
|
+
|
|
175
|
+
shown = now;
|
|
176
|
+
paint();
|
|
177
|
+
|
|
178
|
+
if (changed) {
|
|
179
|
+
ge.emit('history-change', { canvas: ge.canvas, canUndo: now.canUndo, canRedo: now.canRedo });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function paint() {
|
|
184
|
+
ge.toolbarItems('history').forEach(function(button) {
|
|
185
|
+
var off = button.getAttribute('data-ge-item') === '0' ? !shown.canUndo : !shown.canRedo;
|
|
186
|
+
|
|
187
|
+
dom.toggleClass(button, 'disabled', off);
|
|
188
|
+
if (off) {
|
|
189
|
+
button.setAttribute('aria-disabled', 'true');
|
|
190
|
+
} else {
|
|
191
|
+
button.removeAttribute('aria-disabled');
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Where a node is: its index among its parent's content, all the way up
|
|
198
|
+
* from the canvas. The canvas is made again from markup, so a node is
|
|
199
|
+
* not the same one afterwards, but the place it was in is.
|
|
200
|
+
*/
|
|
201
|
+
function pathOf(node) {
|
|
202
|
+
if (!node || node === ge.canvas || !ge.canvas.contains(node)) { return null; }
|
|
203
|
+
|
|
204
|
+
var path = [];
|
|
205
|
+
for (var at = node; at !== ge.canvas; at = at.parentElement) {
|
|
206
|
+
path.unshift(contentOf(at.parentElement).indexOf(at));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return path;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function resolve(path) {
|
|
213
|
+
if (!path) { return null; }
|
|
214
|
+
|
|
215
|
+
var node = ge.canvas;
|
|
216
|
+
for (var i = 0; i < path.length && node; i++) { node = contentOf(node)[path[i]]; }
|
|
217
|
+
|
|
218
|
+
return node || null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function go(step, source) {
|
|
222
|
+
if (!started || destroyed || sourceOpen || !editing() || busy()) { return false; }
|
|
223
|
+
|
|
224
|
+
// A change not yet a step is a step of its own, and the one undone
|
|
225
|
+
commit();
|
|
226
|
+
|
|
227
|
+
var to = index + step;
|
|
228
|
+
if (to < 0 || to >= stack.length) { return false; }
|
|
229
|
+
|
|
230
|
+
var selected = pathOf(ge.getSelected());
|
|
231
|
+
var target = pathOf(ge.getActiveTarget());
|
|
232
|
+
|
|
233
|
+
stopObserving();
|
|
234
|
+
index = to;
|
|
235
|
+
ge.setHtml(stack[index]);
|
|
236
|
+
// The step as the canvas reads it back, so that is what is compared with
|
|
237
|
+
stack[index] = ge.snapshotHtml();
|
|
238
|
+
|
|
239
|
+
var node = resolve(selected);
|
|
240
|
+
if (node) { ge.setSelected(node); }
|
|
241
|
+
node = resolve(target);
|
|
242
|
+
if (node) { ge.setActiveTarget(node); }
|
|
243
|
+
|
|
244
|
+
observe();
|
|
245
|
+
notify();
|
|
246
|
+
ge.emit(step < 0 ? 'after-undo' : 'after-redo', { canvas: ge.canvas, html: stack[index], source: source });
|
|
247
|
+
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function onKeydown(e) {
|
|
252
|
+
if (lastEditor !== ge || e.altKey || !(e.ctrlKey || e.metaKey)) { return; }
|
|
253
|
+
|
|
254
|
+
var key = shortcutKey(e);
|
|
255
|
+
if (!key || (key === 'y' && e.shiftKey)) { return; }
|
|
256
|
+
if (!editing() || typing() || ge.panelOpen()) { return; }
|
|
257
|
+
|
|
258
|
+
// Left alone, it is the browser's
|
|
259
|
+
if (go(key === 'y' || e.shiftKey ? 1 : -1, 'shortcut')) { e.preventDefault(); }
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function listenForShortcuts(on) {
|
|
263
|
+
var at = listeningEditors.indexOf(ge);
|
|
264
|
+
|
|
265
|
+
if (on && at === -1) {
|
|
266
|
+
listeningEditors.push(ge);
|
|
267
|
+
document.addEventListener('keydown', onKeydown);
|
|
268
|
+
} else if (!on && at !== -1) {
|
|
269
|
+
listeningEditors.splice(at, 1);
|
|
270
|
+
document.removeEventListener('keydown', onKeydown);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** After the first init has finished: the canvas is ready to be read. */
|
|
275
|
+
function start() {
|
|
276
|
+
if (destroyed) { return; }
|
|
277
|
+
|
|
278
|
+
stack = [read()];
|
|
279
|
+
index = 0;
|
|
280
|
+
observer = new MutationObserver(schedule);
|
|
281
|
+
observe();
|
|
282
|
+
started = true;
|
|
283
|
+
notify();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
toolbar: function() {
|
|
288
|
+
if (!options.buttons) { return []; }
|
|
289
|
+
|
|
290
|
+
return [
|
|
291
|
+
{
|
|
292
|
+
labelKey: 'history.undo',
|
|
293
|
+
iconClass: 'bi bi-arrow-counterclockwise',
|
|
294
|
+
className: 'ge-history-undo',
|
|
295
|
+
align: 'end',
|
|
296
|
+
disabled: function() { return !shown.canUndo; },
|
|
297
|
+
action: function() { go(-1, 'tool'); },
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
labelKey: 'history.redo',
|
|
301
|
+
iconClass: 'bi bi-arrow-clockwise',
|
|
302
|
+
className: 'ge-history-redo',
|
|
303
|
+
align: 'end',
|
|
304
|
+
disabled: function() { return !shown.canRedo; },
|
|
305
|
+
action: function() { go(1, 'tool'); },
|
|
306
|
+
},
|
|
307
|
+
];
|
|
308
|
+
},
|
|
309
|
+
|
|
310
|
+
onInit: function() {
|
|
311
|
+
if (options.shortcuts) { listenForShortcuts(true); }
|
|
312
|
+
|
|
313
|
+
if (initiated) { return; }
|
|
314
|
+
initiated = true;
|
|
315
|
+
Promise.resolve().then(start);
|
|
316
|
+
},
|
|
317
|
+
|
|
318
|
+
// A canvas out of editing leaves the keys to the page: it is read again when it is back
|
|
319
|
+
onDeinit: function() { listenForShortcuts(false); },
|
|
320
|
+
|
|
321
|
+
onSourceOpen: function() { sourceOpen = true; },
|
|
322
|
+
onSourceClose: function() { sourceOpen = false; },
|
|
323
|
+
|
|
324
|
+
onDestroy: function() {
|
|
325
|
+
destroyed = true;
|
|
326
|
+
window.clearTimeout(timer);
|
|
327
|
+
timer = null;
|
|
328
|
+
stopObserving();
|
|
329
|
+
listenForShortcuts(false);
|
|
330
|
+
if (lastEditor === ge) { lastEditor = null; }
|
|
331
|
+
stack = [];
|
|
332
|
+
},
|
|
333
|
+
|
|
334
|
+
// Nothing of its own on the canvas
|
|
335
|
+
cleanMarkup: function() {},
|
|
336
|
+
|
|
337
|
+
methods: {
|
|
338
|
+
undo: function() { return go(-1, 'api'); },
|
|
339
|
+
|
|
340
|
+
redo: function() { return go(1, 'api'); },
|
|
341
|
+
|
|
342
|
+
// A change waiting for its step counts: it is what an undo would undo
|
|
343
|
+
canUndo: function() {
|
|
344
|
+
return started && !destroyed && (index > 0 || pending());
|
|
345
|
+
},
|
|
346
|
+
|
|
347
|
+
// ... and it ends what could be redone
|
|
348
|
+
canRedo: function() {
|
|
349
|
+
return started && !destroyed && index < stack.length - 1 && !pending();
|
|
350
|
+
},
|
|
351
|
+
|
|
352
|
+
// What is there is the first step, with nothing before it or after it
|
|
353
|
+
clearHistory: function() {
|
|
354
|
+
if (!started || destroyed) { return false; }
|
|
355
|
+
|
|
356
|
+
window.clearTimeout(timer);
|
|
357
|
+
timer = null;
|
|
358
|
+
// The textarea's markup is not the canvas's while the source is open
|
|
359
|
+
stack = [sourceOpen ? stack[index] : read()];
|
|
360
|
+
index = 0;
|
|
361
|
+
notify();
|
|
362
|
+
|
|
363
|
+
return true;
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
};
|
|
367
|
+
};
|