@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
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
// src/js/plugins/grideditor.history.js
|
|
2
|
+
import { GridEditor } from "../grideditor.esm.js";
|
|
3
|
+
|
|
4
|
+
// src/js/dom.js
|
|
5
|
+
function one(root, selector) {
|
|
6
|
+
return root ? root.querySelector(selector) : null;
|
|
7
|
+
}
|
|
8
|
+
function children(node, selector) {
|
|
9
|
+
if (!node) {
|
|
10
|
+
return [];
|
|
11
|
+
}
|
|
12
|
+
return Array.prototype.filter.call(node.children, function(each) {
|
|
13
|
+
return !selector || each.matches(selector);
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
function is(node, selector) {
|
|
17
|
+
return !!node && node.nodeType === 1 && node.matches(selector);
|
|
18
|
+
}
|
|
19
|
+
function toggleClass(node, names, state) {
|
|
20
|
+
split(names).forEach(function(name) {
|
|
21
|
+
if (state === void 0) {
|
|
22
|
+
node.classList.toggle(name);
|
|
23
|
+
} else {
|
|
24
|
+
node.classList.toggle(name, !!state);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
return node;
|
|
28
|
+
}
|
|
29
|
+
function hasClass(node, name) {
|
|
30
|
+
return !!node && node.nodeType === 1 && node.classList.contains(name);
|
|
31
|
+
}
|
|
32
|
+
function split(names) {
|
|
33
|
+
return String(names || "").split(/\s+/).filter(Boolean);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// src/js/plugins/grideditor.history.js
|
|
37
|
+
Object.assign(GridEditor.locales.en, {
|
|
38
|
+
"history.undo": "Undo",
|
|
39
|
+
"history.redo": "Redo"
|
|
40
|
+
});
|
|
41
|
+
var DEFAULT_MAX = 50;
|
|
42
|
+
var DEFAULT_DELAY = 500;
|
|
43
|
+
var FURNITURE = ".ge-tools-drawer, .ge-drop-marker";
|
|
44
|
+
var BUSY = ".ge-drag-chosen, .ge-drag-placeholder, .ge-drag-helper, .ge-resizing";
|
|
45
|
+
var listeningEditors = [];
|
|
46
|
+
var lastEditor = null;
|
|
47
|
+
function track(e) {
|
|
48
|
+
lastEditor = listeningEditors.filter(function(ge) {
|
|
49
|
+
return ge.contains(e.target);
|
|
50
|
+
})[0] || null;
|
|
51
|
+
}
|
|
52
|
+
document.addEventListener("pointerdown", track, true);
|
|
53
|
+
document.addEventListener("focusin", track, true);
|
|
54
|
+
function shortcutKey(e) {
|
|
55
|
+
var key = (e.key || "").toLowerCase();
|
|
56
|
+
if (!/^[a-z]$/.test(key)) {
|
|
57
|
+
key = { KeyZ: "z", KeyY: "y" }[e.code] || "";
|
|
58
|
+
}
|
|
59
|
+
return key === "z" || key === "y" ? key : null;
|
|
60
|
+
}
|
|
61
|
+
function typing() {
|
|
62
|
+
var focus = document.activeElement;
|
|
63
|
+
return !!focus && (is(focus, "input, textarea, select") || focus.isContentEditable);
|
|
64
|
+
}
|
|
65
|
+
function contentOf(parent) {
|
|
66
|
+
return children(parent).filter(function(child) {
|
|
67
|
+
return !is(child, FURNITURE);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
GridEditor.features.history = function(ge) {
|
|
71
|
+
var options = optionsFrom(ge.settings.history || {});
|
|
72
|
+
var stack = [];
|
|
73
|
+
var index = -1;
|
|
74
|
+
var shown = { canUndo: false, canRedo: false };
|
|
75
|
+
var timer = null;
|
|
76
|
+
var observer = null;
|
|
77
|
+
var observing = false;
|
|
78
|
+
var initiated = false;
|
|
79
|
+
var started = false;
|
|
80
|
+
var sourceOpen = false;
|
|
81
|
+
var destroyed = false;
|
|
82
|
+
function optionsFrom(given) {
|
|
83
|
+
var result = {
|
|
84
|
+
shortcuts: given.shortcuts === void 0 ? true : given.shortcuts,
|
|
85
|
+
buttons: given.buttons === void 0 ? true : given.buttons,
|
|
86
|
+
max: given.max === void 0 ? DEFAULT_MAX : given.max,
|
|
87
|
+
delay: given.delay === void 0 ? DEFAULT_DELAY : given.delay
|
|
88
|
+
};
|
|
89
|
+
["shortcuts", "buttons"].forEach(function(name) {
|
|
90
|
+
if (typeof result[name] !== "boolean") {
|
|
91
|
+
ge.warn("history." + name + " is true or false: true is used");
|
|
92
|
+
result[name] = true;
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
if (!Number.isInteger(result.max) || result.max < 1) {
|
|
96
|
+
ge.warn("history.max must be a whole number of steps, 1 or more: " + DEFAULT_MAX + " is used");
|
|
97
|
+
result.max = DEFAULT_MAX;
|
|
98
|
+
}
|
|
99
|
+
if (!Number.isInteger(result.delay) || result.delay < 0) {
|
|
100
|
+
ge.warn("history.delay must be a whole number of milliseconds, 0 or more: " + DEFAULT_DELAY + " is used");
|
|
101
|
+
result.delay = DEFAULT_DELAY;
|
|
102
|
+
}
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
function observe() {
|
|
106
|
+
observer.observe(ge.canvas, { subtree: true, childList: true, attributes: true, characterData: true });
|
|
107
|
+
observing = true;
|
|
108
|
+
}
|
|
109
|
+
function stopObserving() {
|
|
110
|
+
if (observer) {
|
|
111
|
+
observer.disconnect();
|
|
112
|
+
}
|
|
113
|
+
observing = false;
|
|
114
|
+
}
|
|
115
|
+
function read() {
|
|
116
|
+
var watching = observing;
|
|
117
|
+
if (watching) {
|
|
118
|
+
stopObserving();
|
|
119
|
+
}
|
|
120
|
+
var html = ge.snapshotHtml();
|
|
121
|
+
if (watching) {
|
|
122
|
+
observe();
|
|
123
|
+
}
|
|
124
|
+
return html;
|
|
125
|
+
}
|
|
126
|
+
function editing() {
|
|
127
|
+
return hasClass(ge.canvas, "ge-editing");
|
|
128
|
+
}
|
|
129
|
+
function busy() {
|
|
130
|
+
return hasClass(ge.canvas, "ge-dropping") || !!one(ge.canvas, BUSY);
|
|
131
|
+
}
|
|
132
|
+
function schedule() {
|
|
133
|
+
window.clearTimeout(timer);
|
|
134
|
+
timer = window.setTimeout(commit, options.delay);
|
|
135
|
+
}
|
|
136
|
+
function pending() {
|
|
137
|
+
return timer !== null && read() !== stack[index];
|
|
138
|
+
}
|
|
139
|
+
function commit() {
|
|
140
|
+
window.clearTimeout(timer);
|
|
141
|
+
timer = null;
|
|
142
|
+
if (!started || destroyed || sourceOpen) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (busy()) {
|
|
146
|
+
schedule();
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
var html = read();
|
|
150
|
+
if (html === stack[index]) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
stack.splice(index + 1);
|
|
154
|
+
stack.push(html);
|
|
155
|
+
if (stack.length > options.max + 1) {
|
|
156
|
+
stack.shift();
|
|
157
|
+
}
|
|
158
|
+
index = stack.length - 1;
|
|
159
|
+
notify();
|
|
160
|
+
}
|
|
161
|
+
function notify() {
|
|
162
|
+
var now = { canUndo: index > 0, canRedo: index < stack.length - 1 };
|
|
163
|
+
var changed = now.canUndo !== shown.canUndo || now.canRedo !== shown.canRedo;
|
|
164
|
+
shown = now;
|
|
165
|
+
paint();
|
|
166
|
+
if (changed) {
|
|
167
|
+
ge.emit("history-change", { canvas: ge.canvas, canUndo: now.canUndo, canRedo: now.canRedo });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function paint() {
|
|
171
|
+
ge.toolbarItems("history").forEach(function(button) {
|
|
172
|
+
var off = button.getAttribute("data-ge-item") === "0" ? !shown.canUndo : !shown.canRedo;
|
|
173
|
+
toggleClass(button, "disabled", off);
|
|
174
|
+
if (off) {
|
|
175
|
+
button.setAttribute("aria-disabled", "true");
|
|
176
|
+
} else {
|
|
177
|
+
button.removeAttribute("aria-disabled");
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
function pathOf(node) {
|
|
182
|
+
if (!node || node === ge.canvas || !ge.canvas.contains(node)) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
var path = [];
|
|
186
|
+
for (var at = node; at !== ge.canvas; at = at.parentElement) {
|
|
187
|
+
path.unshift(contentOf(at.parentElement).indexOf(at));
|
|
188
|
+
}
|
|
189
|
+
return path;
|
|
190
|
+
}
|
|
191
|
+
function resolve(path) {
|
|
192
|
+
if (!path) {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
var node = ge.canvas;
|
|
196
|
+
for (var i = 0; i < path.length && node; i++) {
|
|
197
|
+
node = contentOf(node)[path[i]];
|
|
198
|
+
}
|
|
199
|
+
return node || null;
|
|
200
|
+
}
|
|
201
|
+
function go(step, source) {
|
|
202
|
+
if (!started || destroyed || sourceOpen || !editing() || busy()) {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
commit();
|
|
206
|
+
var to = index + step;
|
|
207
|
+
if (to < 0 || to >= stack.length) {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
var selected = pathOf(ge.getSelected());
|
|
211
|
+
var target = pathOf(ge.getActiveTarget());
|
|
212
|
+
stopObserving();
|
|
213
|
+
index = to;
|
|
214
|
+
ge.setHtml(stack[index]);
|
|
215
|
+
stack[index] = ge.snapshotHtml();
|
|
216
|
+
var node = resolve(selected);
|
|
217
|
+
if (node) {
|
|
218
|
+
ge.setSelected(node);
|
|
219
|
+
}
|
|
220
|
+
node = resolve(target);
|
|
221
|
+
if (node) {
|
|
222
|
+
ge.setActiveTarget(node);
|
|
223
|
+
}
|
|
224
|
+
observe();
|
|
225
|
+
notify();
|
|
226
|
+
ge.emit(step < 0 ? "after-undo" : "after-redo", { canvas: ge.canvas, html: stack[index], source });
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
function onKeydown(e) {
|
|
230
|
+
if (lastEditor !== ge || e.altKey || !(e.ctrlKey || e.metaKey)) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
var key = shortcutKey(e);
|
|
234
|
+
if (!key || key === "y" && e.shiftKey) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (!editing() || typing() || ge.panelOpen()) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (go(key === "y" || e.shiftKey ? 1 : -1, "shortcut")) {
|
|
241
|
+
e.preventDefault();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
function listenForShortcuts(on) {
|
|
245
|
+
var at = listeningEditors.indexOf(ge);
|
|
246
|
+
if (on && at === -1) {
|
|
247
|
+
listeningEditors.push(ge);
|
|
248
|
+
document.addEventListener("keydown", onKeydown);
|
|
249
|
+
} else if (!on && at !== -1) {
|
|
250
|
+
listeningEditors.splice(at, 1);
|
|
251
|
+
document.removeEventListener("keydown", onKeydown);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function start() {
|
|
255
|
+
if (destroyed) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
stack = [read()];
|
|
259
|
+
index = 0;
|
|
260
|
+
observer = new MutationObserver(schedule);
|
|
261
|
+
observe();
|
|
262
|
+
started = true;
|
|
263
|
+
notify();
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
toolbar: function() {
|
|
267
|
+
if (!options.buttons) {
|
|
268
|
+
return [];
|
|
269
|
+
}
|
|
270
|
+
return [
|
|
271
|
+
{
|
|
272
|
+
labelKey: "history.undo",
|
|
273
|
+
iconClass: "bi bi-arrow-counterclockwise",
|
|
274
|
+
className: "ge-history-undo",
|
|
275
|
+
align: "end",
|
|
276
|
+
disabled: function() {
|
|
277
|
+
return !shown.canUndo;
|
|
278
|
+
},
|
|
279
|
+
action: function() {
|
|
280
|
+
go(-1, "tool");
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
labelKey: "history.redo",
|
|
285
|
+
iconClass: "bi bi-arrow-clockwise",
|
|
286
|
+
className: "ge-history-redo",
|
|
287
|
+
align: "end",
|
|
288
|
+
disabled: function() {
|
|
289
|
+
return !shown.canRedo;
|
|
290
|
+
},
|
|
291
|
+
action: function() {
|
|
292
|
+
go(1, "tool");
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
];
|
|
296
|
+
},
|
|
297
|
+
onInit: function() {
|
|
298
|
+
if (options.shortcuts) {
|
|
299
|
+
listenForShortcuts(true);
|
|
300
|
+
}
|
|
301
|
+
if (initiated) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
initiated = true;
|
|
305
|
+
Promise.resolve().then(start);
|
|
306
|
+
},
|
|
307
|
+
// A canvas out of editing leaves the keys to the page: it is read again when it is back
|
|
308
|
+
onDeinit: function() {
|
|
309
|
+
listenForShortcuts(false);
|
|
310
|
+
},
|
|
311
|
+
onSourceOpen: function() {
|
|
312
|
+
sourceOpen = true;
|
|
313
|
+
},
|
|
314
|
+
onSourceClose: function() {
|
|
315
|
+
sourceOpen = false;
|
|
316
|
+
},
|
|
317
|
+
onDestroy: function() {
|
|
318
|
+
destroyed = true;
|
|
319
|
+
window.clearTimeout(timer);
|
|
320
|
+
timer = null;
|
|
321
|
+
stopObserving();
|
|
322
|
+
listenForShortcuts(false);
|
|
323
|
+
if (lastEditor === ge) {
|
|
324
|
+
lastEditor = null;
|
|
325
|
+
}
|
|
326
|
+
stack = [];
|
|
327
|
+
},
|
|
328
|
+
// Nothing of its own on the canvas
|
|
329
|
+
cleanMarkup: function() {
|
|
330
|
+
},
|
|
331
|
+
methods: {
|
|
332
|
+
undo: function() {
|
|
333
|
+
return go(-1, "api");
|
|
334
|
+
},
|
|
335
|
+
redo: function() {
|
|
336
|
+
return go(1, "api");
|
|
337
|
+
},
|
|
338
|
+
// A change waiting for its step counts: it is what an undo would undo
|
|
339
|
+
canUndo: function() {
|
|
340
|
+
return started && !destroyed && (index > 0 || pending());
|
|
341
|
+
},
|
|
342
|
+
// ... and it ends what could be redone
|
|
343
|
+
canRedo: function() {
|
|
344
|
+
return started && !destroyed && index < stack.length - 1 && !pending();
|
|
345
|
+
},
|
|
346
|
+
// What is there is the first step, with nothing before it or after it
|
|
347
|
+
clearHistory: function() {
|
|
348
|
+
if (!started || destroyed) {
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
window.clearTimeout(timer);
|
|
352
|
+
timer = null;
|
|
353
|
+
stack = [sourceOpen ? stack[index] : read()];
|
|
354
|
+
index = 0;
|
|
355
|
+
notify();
|
|
356
|
+
return true;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
};
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
// src/js/register.js
|
|
3
|
+
var GridEditor = window.GridEditor;
|
|
4
|
+
if (!GridEditor || typeof GridEditor.get !== "function") {
|
|
5
|
+
throw new Error("grid-editor: load grideditor.js (or grideditor.bundle.min.js) before its plugins, its locales and grideditor.jquery.js");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// src/js/dom.js
|
|
9
|
+
function one(root, selector) {
|
|
10
|
+
return root ? root.querySelector(selector) : null;
|
|
11
|
+
}
|
|
12
|
+
function children(node, selector) {
|
|
13
|
+
if (!node) {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
return Array.prototype.filter.call(node.children, function(each) {
|
|
17
|
+
return !selector || each.matches(selector);
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function is(node, selector) {
|
|
21
|
+
return !!node && node.nodeType === 1 && node.matches(selector);
|
|
22
|
+
}
|
|
23
|
+
function toggleClass(node, names, state) {
|
|
24
|
+
split(names).forEach(function(name) {
|
|
25
|
+
if (state === void 0) {
|
|
26
|
+
node.classList.toggle(name);
|
|
27
|
+
} else {
|
|
28
|
+
node.classList.toggle(name, !!state);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
return node;
|
|
32
|
+
}
|
|
33
|
+
function hasClass(node, name) {
|
|
34
|
+
return !!node && node.nodeType === 1 && node.classList.contains(name);
|
|
35
|
+
}
|
|
36
|
+
function split(names) {
|
|
37
|
+
return String(names || "").split(/\s+/).filter(Boolean);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// src/js/plugins/grideditor.history.js
|
|
41
|
+
Object.assign(GridEditor.locales.en, {
|
|
42
|
+
"history.undo": "Undo",
|
|
43
|
+
"history.redo": "Redo"
|
|
44
|
+
});
|
|
45
|
+
var DEFAULT_MAX = 50;
|
|
46
|
+
var DEFAULT_DELAY = 500;
|
|
47
|
+
var FURNITURE = ".ge-tools-drawer, .ge-drop-marker";
|
|
48
|
+
var BUSY = ".ge-drag-chosen, .ge-drag-placeholder, .ge-drag-helper, .ge-resizing";
|
|
49
|
+
var listeningEditors = [];
|
|
50
|
+
var lastEditor = null;
|
|
51
|
+
function track(e) {
|
|
52
|
+
lastEditor = listeningEditors.filter(function(ge) {
|
|
53
|
+
return ge.contains(e.target);
|
|
54
|
+
})[0] || null;
|
|
55
|
+
}
|
|
56
|
+
document.addEventListener("pointerdown", track, true);
|
|
57
|
+
document.addEventListener("focusin", track, true);
|
|
58
|
+
function shortcutKey(e) {
|
|
59
|
+
var key = (e.key || "").toLowerCase();
|
|
60
|
+
if (!/^[a-z]$/.test(key)) {
|
|
61
|
+
key = { KeyZ: "z", KeyY: "y" }[e.code] || "";
|
|
62
|
+
}
|
|
63
|
+
return key === "z" || key === "y" ? key : null;
|
|
64
|
+
}
|
|
65
|
+
function typing() {
|
|
66
|
+
var focus = document.activeElement;
|
|
67
|
+
return !!focus && (is(focus, "input, textarea, select") || focus.isContentEditable);
|
|
68
|
+
}
|
|
69
|
+
function contentOf(parent) {
|
|
70
|
+
return children(parent).filter(function(child) {
|
|
71
|
+
return !is(child, FURNITURE);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
GridEditor.features.history = function(ge) {
|
|
75
|
+
var options = optionsFrom(ge.settings.history || {});
|
|
76
|
+
var stack = [];
|
|
77
|
+
var index = -1;
|
|
78
|
+
var shown = { canUndo: false, canRedo: false };
|
|
79
|
+
var timer = null;
|
|
80
|
+
var observer = null;
|
|
81
|
+
var observing = false;
|
|
82
|
+
var initiated = false;
|
|
83
|
+
var started = false;
|
|
84
|
+
var sourceOpen = false;
|
|
85
|
+
var destroyed = false;
|
|
86
|
+
function optionsFrom(given) {
|
|
87
|
+
var result = {
|
|
88
|
+
shortcuts: given.shortcuts === void 0 ? true : given.shortcuts,
|
|
89
|
+
buttons: given.buttons === void 0 ? true : given.buttons,
|
|
90
|
+
max: given.max === void 0 ? DEFAULT_MAX : given.max,
|
|
91
|
+
delay: given.delay === void 0 ? DEFAULT_DELAY : given.delay
|
|
92
|
+
};
|
|
93
|
+
["shortcuts", "buttons"].forEach(function(name) {
|
|
94
|
+
if (typeof result[name] !== "boolean") {
|
|
95
|
+
ge.warn("history." + name + " is true or false: true is used");
|
|
96
|
+
result[name] = true;
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
if (!Number.isInteger(result.max) || result.max < 1) {
|
|
100
|
+
ge.warn("history.max must be a whole number of steps, 1 or more: " + DEFAULT_MAX + " is used");
|
|
101
|
+
result.max = DEFAULT_MAX;
|
|
102
|
+
}
|
|
103
|
+
if (!Number.isInteger(result.delay) || result.delay < 0) {
|
|
104
|
+
ge.warn("history.delay must be a whole number of milliseconds, 0 or more: " + DEFAULT_DELAY + " is used");
|
|
105
|
+
result.delay = DEFAULT_DELAY;
|
|
106
|
+
}
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
function observe() {
|
|
110
|
+
observer.observe(ge.canvas, { subtree: true, childList: true, attributes: true, characterData: true });
|
|
111
|
+
observing = true;
|
|
112
|
+
}
|
|
113
|
+
function stopObserving() {
|
|
114
|
+
if (observer) {
|
|
115
|
+
observer.disconnect();
|
|
116
|
+
}
|
|
117
|
+
observing = false;
|
|
118
|
+
}
|
|
119
|
+
function read() {
|
|
120
|
+
var watching = observing;
|
|
121
|
+
if (watching) {
|
|
122
|
+
stopObserving();
|
|
123
|
+
}
|
|
124
|
+
var html = ge.snapshotHtml();
|
|
125
|
+
if (watching) {
|
|
126
|
+
observe();
|
|
127
|
+
}
|
|
128
|
+
return html;
|
|
129
|
+
}
|
|
130
|
+
function editing() {
|
|
131
|
+
return hasClass(ge.canvas, "ge-editing");
|
|
132
|
+
}
|
|
133
|
+
function busy() {
|
|
134
|
+
return hasClass(ge.canvas, "ge-dropping") || !!one(ge.canvas, BUSY);
|
|
135
|
+
}
|
|
136
|
+
function schedule() {
|
|
137
|
+
window.clearTimeout(timer);
|
|
138
|
+
timer = window.setTimeout(commit, options.delay);
|
|
139
|
+
}
|
|
140
|
+
function pending() {
|
|
141
|
+
return timer !== null && read() !== stack[index];
|
|
142
|
+
}
|
|
143
|
+
function commit() {
|
|
144
|
+
window.clearTimeout(timer);
|
|
145
|
+
timer = null;
|
|
146
|
+
if (!started || destroyed || sourceOpen) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (busy()) {
|
|
150
|
+
schedule();
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
var html = read();
|
|
154
|
+
if (html === stack[index]) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
stack.splice(index + 1);
|
|
158
|
+
stack.push(html);
|
|
159
|
+
if (stack.length > options.max + 1) {
|
|
160
|
+
stack.shift();
|
|
161
|
+
}
|
|
162
|
+
index = stack.length - 1;
|
|
163
|
+
notify();
|
|
164
|
+
}
|
|
165
|
+
function notify() {
|
|
166
|
+
var now = { canUndo: index > 0, canRedo: index < stack.length - 1 };
|
|
167
|
+
var changed = now.canUndo !== shown.canUndo || now.canRedo !== shown.canRedo;
|
|
168
|
+
shown = now;
|
|
169
|
+
paint();
|
|
170
|
+
if (changed) {
|
|
171
|
+
ge.emit("history-change", { canvas: ge.canvas, canUndo: now.canUndo, canRedo: now.canRedo });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function paint() {
|
|
175
|
+
ge.toolbarItems("history").forEach(function(button) {
|
|
176
|
+
var off = button.getAttribute("data-ge-item") === "0" ? !shown.canUndo : !shown.canRedo;
|
|
177
|
+
toggleClass(button, "disabled", off);
|
|
178
|
+
if (off) {
|
|
179
|
+
button.setAttribute("aria-disabled", "true");
|
|
180
|
+
} else {
|
|
181
|
+
button.removeAttribute("aria-disabled");
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
function pathOf(node) {
|
|
186
|
+
if (!node || node === ge.canvas || !ge.canvas.contains(node)) {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
var path = [];
|
|
190
|
+
for (var at = node; at !== ge.canvas; at = at.parentElement) {
|
|
191
|
+
path.unshift(contentOf(at.parentElement).indexOf(at));
|
|
192
|
+
}
|
|
193
|
+
return path;
|
|
194
|
+
}
|
|
195
|
+
function resolve(path) {
|
|
196
|
+
if (!path) {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
var node = ge.canvas;
|
|
200
|
+
for (var i = 0; i < path.length && node; i++) {
|
|
201
|
+
node = contentOf(node)[path[i]];
|
|
202
|
+
}
|
|
203
|
+
return node || null;
|
|
204
|
+
}
|
|
205
|
+
function go(step, source) {
|
|
206
|
+
if (!started || destroyed || sourceOpen || !editing() || busy()) {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
commit();
|
|
210
|
+
var to = index + step;
|
|
211
|
+
if (to < 0 || to >= stack.length) {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
var selected = pathOf(ge.getSelected());
|
|
215
|
+
var target = pathOf(ge.getActiveTarget());
|
|
216
|
+
stopObserving();
|
|
217
|
+
index = to;
|
|
218
|
+
ge.setHtml(stack[index]);
|
|
219
|
+
stack[index] = ge.snapshotHtml();
|
|
220
|
+
var node = resolve(selected);
|
|
221
|
+
if (node) {
|
|
222
|
+
ge.setSelected(node);
|
|
223
|
+
}
|
|
224
|
+
node = resolve(target);
|
|
225
|
+
if (node) {
|
|
226
|
+
ge.setActiveTarget(node);
|
|
227
|
+
}
|
|
228
|
+
observe();
|
|
229
|
+
notify();
|
|
230
|
+
ge.emit(step < 0 ? "after-undo" : "after-redo", { canvas: ge.canvas, html: stack[index], source });
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
function onKeydown(e) {
|
|
234
|
+
if (lastEditor !== ge || e.altKey || !(e.ctrlKey || e.metaKey)) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
var key = shortcutKey(e);
|
|
238
|
+
if (!key || key === "y" && e.shiftKey) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (!editing() || typing() || ge.panelOpen()) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (go(key === "y" || e.shiftKey ? 1 : -1, "shortcut")) {
|
|
245
|
+
e.preventDefault();
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
function listenForShortcuts(on) {
|
|
249
|
+
var at = listeningEditors.indexOf(ge);
|
|
250
|
+
if (on && at === -1) {
|
|
251
|
+
listeningEditors.push(ge);
|
|
252
|
+
document.addEventListener("keydown", onKeydown);
|
|
253
|
+
} else if (!on && at !== -1) {
|
|
254
|
+
listeningEditors.splice(at, 1);
|
|
255
|
+
document.removeEventListener("keydown", onKeydown);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
function start() {
|
|
259
|
+
if (destroyed) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
stack = [read()];
|
|
263
|
+
index = 0;
|
|
264
|
+
observer = new MutationObserver(schedule);
|
|
265
|
+
observe();
|
|
266
|
+
started = true;
|
|
267
|
+
notify();
|
|
268
|
+
}
|
|
269
|
+
return {
|
|
270
|
+
toolbar: function() {
|
|
271
|
+
if (!options.buttons) {
|
|
272
|
+
return [];
|
|
273
|
+
}
|
|
274
|
+
return [
|
|
275
|
+
{
|
|
276
|
+
labelKey: "history.undo",
|
|
277
|
+
iconClass: "bi bi-arrow-counterclockwise",
|
|
278
|
+
className: "ge-history-undo",
|
|
279
|
+
align: "end",
|
|
280
|
+
disabled: function() {
|
|
281
|
+
return !shown.canUndo;
|
|
282
|
+
},
|
|
283
|
+
action: function() {
|
|
284
|
+
go(-1, "tool");
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
labelKey: "history.redo",
|
|
289
|
+
iconClass: "bi bi-arrow-clockwise",
|
|
290
|
+
className: "ge-history-redo",
|
|
291
|
+
align: "end",
|
|
292
|
+
disabled: function() {
|
|
293
|
+
return !shown.canRedo;
|
|
294
|
+
},
|
|
295
|
+
action: function() {
|
|
296
|
+
go(1, "tool");
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
];
|
|
300
|
+
},
|
|
301
|
+
onInit: function() {
|
|
302
|
+
if (options.shortcuts) {
|
|
303
|
+
listenForShortcuts(true);
|
|
304
|
+
}
|
|
305
|
+
if (initiated) {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
initiated = true;
|
|
309
|
+
Promise.resolve().then(start);
|
|
310
|
+
},
|
|
311
|
+
// A canvas out of editing leaves the keys to the page: it is read again when it is back
|
|
312
|
+
onDeinit: function() {
|
|
313
|
+
listenForShortcuts(false);
|
|
314
|
+
},
|
|
315
|
+
onSourceOpen: function() {
|
|
316
|
+
sourceOpen = true;
|
|
317
|
+
},
|
|
318
|
+
onSourceClose: function() {
|
|
319
|
+
sourceOpen = false;
|
|
320
|
+
},
|
|
321
|
+
onDestroy: function() {
|
|
322
|
+
destroyed = true;
|
|
323
|
+
window.clearTimeout(timer);
|
|
324
|
+
timer = null;
|
|
325
|
+
stopObserving();
|
|
326
|
+
listenForShortcuts(false);
|
|
327
|
+
if (lastEditor === ge) {
|
|
328
|
+
lastEditor = null;
|
|
329
|
+
}
|
|
330
|
+
stack = [];
|
|
331
|
+
},
|
|
332
|
+
// Nothing of its own on the canvas
|
|
333
|
+
cleanMarkup: function() {
|
|
334
|
+
},
|
|
335
|
+
methods: {
|
|
336
|
+
undo: function() {
|
|
337
|
+
return go(-1, "api");
|
|
338
|
+
},
|
|
339
|
+
redo: function() {
|
|
340
|
+
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
|
+
// ... and it ends what could be redone
|
|
347
|
+
canRedo: function() {
|
|
348
|
+
return started && !destroyed && index < stack.length - 1 && !pending();
|
|
349
|
+
},
|
|
350
|
+
// What is there is the first step, with nothing before it or after it
|
|
351
|
+
clearHistory: function() {
|
|
352
|
+
if (!started || destroyed) {
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
window.clearTimeout(timer);
|
|
356
|
+
timer = null;
|
|
357
|
+
stack = [sourceOpen ? stack[index] : read()];
|
|
358
|
+
index = 0;
|
|
359
|
+
notify();
|
|
360
|
+
return true;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
};
|
|
365
|
+
})();
|