@themarioga/grid-editor 8.4.0 → 8.6.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/CHANGELOG.md +51 -0
- package/README.md +105 -5
- package/dist/grideditor.bundle.min.js +3 -3
- package/dist/grideditor.css +15 -4
- package/dist/grideditor.d.ts +76 -4
- package/dist/grideditor.esm.js +212 -38
- package/dist/grideditor.js +212 -38
- package/dist/grideditor.min.css +1 -1
- package/dist/grideditor.min.css.map +1 -1
- package/dist/grideditor.min.js +2 -2
- package/dist/grideditor.min.js.map +3 -3
- package/dist/locales/grideditor.es.esm.js +5 -0
- package/dist/locales/grideditor.es.js +5 -0
- package/dist/locales/grideditor.es.min.js +1 -1
- package/dist/locales/grideditor.es.min.js.map +2 -2
- package/dist/plugins/grideditor.accordion.esm.js +7 -0
- package/dist/plugins/grideditor.accordion.js +7 -0
- package/dist/plugins/grideditor.accordion.min.js +1 -1
- package/dist/plugins/grideditor.accordion.min.js.map +3 -3
- package/dist/plugins/grideditor.autosave.esm.js +309 -0
- package/dist/plugins/grideditor.autosave.js +316 -0
- package/dist/plugins/grideditor.autosave.min.js +1 -0
- package/dist/plugins/grideditor.autosave.min.js.map +7 -0
- package/dist/plugins/grideditor.card.esm.js +10 -0
- package/dist/plugins/grideditor.card.js +10 -0
- package/dist/plugins/grideditor.card.min.js +1 -1
- package/dist/plugins/grideditor.card.min.js.map +3 -3
- package/dist/plugins/grideditor.ckeditor.esm.js +30 -1
- package/dist/plugins/grideditor.ckeditor.js +30 -1
- package/dist/plugins/grideditor.ckeditor.min.js +1 -1
- package/dist/plugins/grideditor.ckeditor.min.js.map +3 -3
- package/dist/plugins/grideditor.clipboard.esm.js +3 -0
- package/dist/plugins/grideditor.clipboard.js +3 -0
- package/dist/plugins/grideditor.clipboard.min.js +1 -1
- package/dist/plugins/grideditor.clipboard.min.js.map +2 -2
- package/dist/plugins/grideditor.codemirror-inline.esm.js +11 -0
- package/dist/plugins/grideditor.codemirror-inline.js +11 -0
- package/dist/plugins/grideditor.codemirror-inline.min.js +1 -1
- package/dist/plugins/grideditor.codemirror-inline.min.js.map +3 -3
- package/dist/plugins/grideditor.elements.esm.js +78 -4
- package/dist/plugins/grideditor.elements.js +78 -4
- package/dist/plugins/grideditor.elements.min.js +1 -1
- package/dist/plugins/grideditor.elements.min.js.map +3 -3
- package/dist/plugins/grideditor.gutters.esm.js +5 -0
- package/dist/plugins/grideditor.gutters.js +5 -0
- package/dist/plugins/grideditor.gutters.min.js +1 -1
- package/dist/plugins/grideditor.gutters.min.js.map +3 -3
- package/dist/plugins/grideditor.inline-style.esm.js +18 -10
- package/dist/plugins/grideditor.inline-style.js +18 -10
- package/dist/plugins/grideditor.inline-style.min.js +3 -3
- package/dist/plugins/grideditor.inline-style.min.js.map +3 -3
- package/dist/plugins/grideditor.order.esm.js +5 -0
- package/dist/plugins/grideditor.order.js +5 -0
- package/dist/plugins/grideditor.order.min.js +1 -1
- package/dist/plugins/grideditor.order.min.js.map +3 -3
- package/dist/plugins/grideditor.popup.esm.js +17 -6
- package/dist/plugins/grideditor.popup.js +17 -6
- package/dist/plugins/grideditor.popup.min.js +1 -1
- package/dist/plugins/grideditor.popup.min.js.map +3 -3
- package/dist/plugins/grideditor.sections.esm.js +6 -3
- package/dist/plugins/grideditor.sections.js +6 -3
- package/dist/plugins/grideditor.sections.min.js +1 -1
- package/dist/plugins/grideditor.sections.min.js.map +3 -3
- package/dist/plugins/grideditor.summernote.esm.js +40 -1
- package/dist/plugins/grideditor.summernote.js +40 -1
- package/dist/plugins/grideditor.summernote.min.js +1 -1
- package/dist/plugins/grideditor.summernote.min.js.map +3 -3
- package/dist/plugins/grideditor.tabs.esm.js +7 -0
- package/dist/plugins/grideditor.tabs.js +7 -0
- package/dist/plugins/grideditor.tabs.min.js +1 -1
- package/dist/plugins/grideditor.tabs.min.js.map +3 -3
- package/dist/plugins/grideditor.tinymce.esm.js +32 -1
- package/dist/plugins/grideditor.tinymce.js +32 -1
- package/dist/plugins/grideditor.tinymce.min.js +1 -1
- package/dist/plugins/grideditor.tinymce.min.js.map +3 -3
- package/docs/events.md +8 -0
- package/docs/locale-keys.md +16 -1
- package/docs/plugins.md +60 -7
- package/example/autosave.html +39 -22
- package/example/index.html +1 -1
- package/example/plugins.html +10 -0
- package/package.json +1 -2
- package/src/js/grideditor.js +248 -28
- package/src/js/inline-style/display.js +4 -3
- package/src/js/inline-style/drawerflow.js +10 -5
- package/src/js/locales/grideditor.es.js +5 -0
- package/src/js/plugins/grideditor.accordion.js +6 -0
- package/src/js/plugins/grideditor.autosave.js +349 -0
- package/src/js/plugins/grideditor.card.js +6 -0
- package/src/js/plugins/grideditor.ckeditor.js +6 -0
- package/src/js/plugins/grideditor.clipboard.js +3 -0
- package/src/js/plugins/grideditor.codemirror-inline.js +7 -0
- package/src/js/plugins/grideditor.elements.js +98 -4
- package/src/js/plugins/grideditor.gutters.js +4 -0
- package/src/js/plugins/grideditor.inline-style.js +2 -0
- package/src/js/plugins/grideditor.order.js +4 -0
- package/src/js/plugins/grideditor.popup.js +15 -6
- package/src/js/plugins/grideditor.sections.js +4 -3
- package/src/js/plugins/grideditor.summernote.js +14 -0
- package/src/js/plugins/grideditor.tabs.js +6 -0
- package/src/js/plugins/grideditor.tinymce.js +8 -0
- package/src/js/text/grideditor.text.js +27 -1
- package/src/less/grideditor.less +16 -8
- package/types/grideditor.d.ts +76 -4
- package/AUTO_SAVE.md +0 -30
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Autosave for grid-editor.
|
|
3
|
+
*
|
|
4
|
+
* A feature plugin: load this file after the editor and what is being edited
|
|
5
|
+
* is kept in the browser's storage as it changes, and offered back the next
|
|
6
|
+
* time an editor starts from the same html. What it can ask the editor for is
|
|
7
|
+
* the handle its factory is called with, described in docs/plugins.md.
|
|
8
|
+
*
|
|
9
|
+
* <script src="dist/grideditor.min.js"></script>
|
|
10
|
+
* <script src="dist/plugins/grideditor.autosave.min.js"></script>
|
|
11
|
+
*
|
|
12
|
+
* It reads the canvas with ge.snapshotHtml(), which leaves the canvas
|
|
13
|
+
* editing: the text being typed into stays open, with its cursor. A save
|
|
14
|
+
* comes `delay` ms after the last change, and when the page is left.
|
|
15
|
+
*
|
|
16
|
+
* A draft remembers the html its editor started from, and is only offered
|
|
17
|
+
* to an editor that starts from the same html: a page that loads one
|
|
18
|
+
* document after another is not offered one document's draft over another.
|
|
19
|
+
* Such a page gives each document a key of its own, and each keeps its draft.
|
|
20
|
+
*
|
|
21
|
+
* new GridEditor('#myGrid', { autosave: { key: 'page-42', storage: 'session' } });
|
|
22
|
+
*/
|
|
23
|
+
import { GridEditor } from '../grideditor.js';
|
|
24
|
+
|
|
25
|
+
Object.assign(GridEditor.locales.en, {
|
|
26
|
+
'autosave.restore_title': 'Restore the draft?',
|
|
27
|
+
'autosave.restore_message': 'There is a draft of this page saved on {date}, with changes that were not published. Restore it?',
|
|
28
|
+
'autosave.restore': 'Restore',
|
|
29
|
+
'autosave.discard': 'Discard',
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
var VERSION = 1;
|
|
33
|
+
var STORAGES = { local: 'localStorage', session: 'sessionStorage' };
|
|
34
|
+
var DEFAULT_DELAY = 1000;
|
|
35
|
+
|
|
36
|
+
/** The keys the editors on this page save to: one editor to a key. */
|
|
37
|
+
var liveKeys = new Set();
|
|
38
|
+
|
|
39
|
+
/** FNV-1a, 32 bits, as hex: enough to tell one document's html from another's. */
|
|
40
|
+
function fingerprint(text) {
|
|
41
|
+
var hash = 0x811c9dc5;
|
|
42
|
+
|
|
43
|
+
for (var i = 0; i < text.length; i++) {
|
|
44
|
+
hash ^= text.charCodeAt(i);
|
|
45
|
+
hash = Math.imul(hash, 0x01000193);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return (hash >>> 0).toString(16).padStart(8, '0');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A stored value that is a draft this file wrote, or null. */
|
|
52
|
+
function validDraft(value) {
|
|
53
|
+
return !!value && typeof value === 'object' && value.version === VERSION &&
|
|
54
|
+
typeof value.html === 'string' && typeof value.base === 'string' &&
|
|
55
|
+
typeof value.savedAt === 'string' && !isNaN(Date.parse(value.savedAt));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
GridEditor.features.autosave = function(ge) {
|
|
59
|
+
|
|
60
|
+
var options = optionsFrom(ge.settings.autosave || {});
|
|
61
|
+
var key = options.key || 'grideditor.autosave:' + window.location.pathname + '#' + ge.canvas.id;
|
|
62
|
+
var storage = storageFor(options.storage);
|
|
63
|
+
|
|
64
|
+
// unavailable | disabled | asking | enabled | destroyed
|
|
65
|
+
var state = 'disabled';
|
|
66
|
+
var enabledAfterAsking = true; // What asking ends in: a method may have changed it meanwhile
|
|
67
|
+
var started = false; // Whether the first init has been looked at
|
|
68
|
+
var draft = null; // The draft found when the editor was made, while it may be offered
|
|
69
|
+
var base = null; // The fingerprint of the html the editor started from
|
|
70
|
+
var last = null; // The html saved last, or the one there was to start from
|
|
71
|
+
var timer = null;
|
|
72
|
+
var observer = null;
|
|
73
|
+
var sourceOpen = false;
|
|
74
|
+
var warnedWrite = false;
|
|
75
|
+
|
|
76
|
+
if (!storage) {
|
|
77
|
+
ge.warn('autosave: the browser will not give this page its ' + STORAGES[options.storage] +
|
|
78
|
+
', so nothing is saved');
|
|
79
|
+
state = 'unavailable';
|
|
80
|
+
} else if (liveKeys.has(key)) {
|
|
81
|
+
ge.warn('autosave: another editor on this page already saves to "' + key + '", so this one does not; ' +
|
|
82
|
+
'give each its own autosave.key');
|
|
83
|
+
state = 'unavailable';
|
|
84
|
+
} else {
|
|
85
|
+
liveKeys.add(key);
|
|
86
|
+
if (options.enabled) {
|
|
87
|
+
state = 'enabled';
|
|
88
|
+
draft = readDraft(true);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function optionsFrom(given) {
|
|
93
|
+
var result = {
|
|
94
|
+
enabled: given.enabled === undefined ? true : !!given.enabled,
|
|
95
|
+
storage: given.storage === undefined ? 'local' : given.storage,
|
|
96
|
+
key: given.key === undefined ? null : given.key,
|
|
97
|
+
delay: given.delay === undefined ? DEFAULT_DELAY : given.delay,
|
|
98
|
+
maxAge: given.maxAge === undefined ? null : given.maxAge,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
if (!STORAGES[result.storage]) {
|
|
102
|
+
ge.warn('autosave.storage "' + result.storage + '" is not local or session: local is used');
|
|
103
|
+
result.storage = 'local';
|
|
104
|
+
}
|
|
105
|
+
if (result.key !== null && (typeof result.key !== 'string' || result.key === '')) {
|
|
106
|
+
ge.warn('autosave.key must be a string: the page\'s own key is used');
|
|
107
|
+
result.key = null;
|
|
108
|
+
}
|
|
109
|
+
if (typeof result.delay !== 'number' || !Number.isInteger(result.delay) || result.delay < 0) {
|
|
110
|
+
ge.warn('autosave.delay must be a whole number of milliseconds, 0 or more: ' + DEFAULT_DELAY + ' is used');
|
|
111
|
+
result.delay = DEFAULT_DELAY;
|
|
112
|
+
}
|
|
113
|
+
if (result.maxAge !== null && !(typeof result.maxAge === 'number' && isFinite(result.maxAge) && result.maxAge > 0)) {
|
|
114
|
+
ge.warn('autosave.maxAge must be a number of milliseconds over 0, or null: drafts do not expire');
|
|
115
|
+
result.maxAge = null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** The storage, if the browser lets the page have it. */
|
|
122
|
+
function storageFor(name) {
|
|
123
|
+
try {
|
|
124
|
+
var found = window[STORAGES[name]];
|
|
125
|
+
found.getItem(key);
|
|
126
|
+
return found;
|
|
127
|
+
} catch (error) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The draft saved under the key, or null. Starting, one that cannot be
|
|
134
|
+
* read or has expired is taken away: nothing will ever offer it.
|
|
135
|
+
*/
|
|
136
|
+
function readDraft(starting) {
|
|
137
|
+
var raw;
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
raw = storage.getItem(key);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
if (raw === null) { return null; }
|
|
145
|
+
|
|
146
|
+
var value = null;
|
|
147
|
+
try { value = JSON.parse(raw); } catch (error) { /* not ours, or mangled */ }
|
|
148
|
+
|
|
149
|
+
if (!validDraft(value)) {
|
|
150
|
+
if (starting) {
|
|
151
|
+
ge.warn('autosave: what is saved under "' + key + '" is not a draft this version can read: it is discarded');
|
|
152
|
+
remove();
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (starting && options.maxAge !== null && Date.now() - Date.parse(value.savedAt) > options.maxAge) {
|
|
158
|
+
remove();
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return value;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function remove() {
|
|
166
|
+
try { storage.removeItem(key); } catch (error) { /* nothing to take away */ }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Write the canvas as it is, if it changed. True when it was written. */
|
|
170
|
+
function save(source) {
|
|
171
|
+
window.clearTimeout(timer);
|
|
172
|
+
timer = null;
|
|
173
|
+
|
|
174
|
+
if (sourceOpen || base === null) { return false; }
|
|
175
|
+
|
|
176
|
+
// The read itself may change the canvas - getHtml, when a plugin
|
|
177
|
+
// cannot be read on a copy - and that is not a change to save
|
|
178
|
+
var watching = !!observer;
|
|
179
|
+
if (watching) { observer.disconnect(); }
|
|
180
|
+
var html = ge.snapshotHtml();
|
|
181
|
+
if (watching) { watch(); }
|
|
182
|
+
|
|
183
|
+
if (html === last) { return false; }
|
|
184
|
+
|
|
185
|
+
var value = { version: VERSION, html: html, savedAt: new Date().toISOString(), base: base };
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
storage.setItem(key, JSON.stringify(value));
|
|
189
|
+
} catch (error) {
|
|
190
|
+
if (!warnedWrite) {
|
|
191
|
+
warnedWrite = true;
|
|
192
|
+
ge.warn('autosave: the draft could not be saved (' + (error && error.message || error) + ')');
|
|
193
|
+
}
|
|
194
|
+
ge.emit('autosave-error', { canvas: ge.canvas, error: error });
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
last = html;
|
|
199
|
+
ge.emit('after-autosave', { canvas: ge.canvas, html: html, savedAt: value.savedAt, source: source });
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function schedule() {
|
|
204
|
+
window.clearTimeout(timer);
|
|
205
|
+
timer = window.setTimeout(function() { save('change'); }, options.delay);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function watch() {
|
|
209
|
+
if (!observer) { observer = new MutationObserver(schedule); }
|
|
210
|
+
observer.observe(ge.canvas, { subtree: true, childList: true, attributes: true, characterData: true });
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function unwatch() {
|
|
214
|
+
window.clearTimeout(timer);
|
|
215
|
+
timer = null;
|
|
216
|
+
if (observer) { observer.disconnect(); }
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function onPageHide() {
|
|
220
|
+
if (state === 'enabled') { save('pagehide'); }
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** After the first init has finished: what there is to start from, and the draft offered. */
|
|
224
|
+
function start() {
|
|
225
|
+
if (state === 'destroyed' || state === 'unavailable') { return; }
|
|
226
|
+
|
|
227
|
+
last = ge.snapshotHtml();
|
|
228
|
+
base = fingerprint(last);
|
|
229
|
+
window.addEventListener('pagehide', onPageHide);
|
|
230
|
+
|
|
231
|
+
// One of another document, or the same as what is there, is not offered
|
|
232
|
+
if (state === 'enabled' && draft && draft.base === base && draft.html !== last) {
|
|
233
|
+
ask(draft);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
draft = null;
|
|
238
|
+
if (state === 'enabled') { watch(); }
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function dateOf(savedAt) {
|
|
242
|
+
try {
|
|
243
|
+
return new Date(savedAt).toLocaleString(ge.settings.locale || undefined);
|
|
244
|
+
} catch (error) {
|
|
245
|
+
return new Date(savedAt).toLocaleString();
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function ask(found) {
|
|
250
|
+
state = 'asking';
|
|
251
|
+
|
|
252
|
+
ge.confirm(ge.t('autosave.restore_message', { date: dateOf(found.savedAt) }), {
|
|
253
|
+
title: ge.t('autosave.restore_title'),
|
|
254
|
+
ok: ge.t('autosave.restore'),
|
|
255
|
+
cancel: ge.t('autosave.discard'),
|
|
256
|
+
}, function(restore) {
|
|
257
|
+
if (state === 'destroyed') { return; }
|
|
258
|
+
|
|
259
|
+
// Taken away unanswered - setLocale - so asked again, in the new
|
|
260
|
+
// language, once the old modal is out of the way
|
|
261
|
+
if (restore === null) {
|
|
262
|
+
window.setTimeout(function() {
|
|
263
|
+
if (state === 'asking') { ask(found); }
|
|
264
|
+
}, 0);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
draft = null;
|
|
269
|
+
|
|
270
|
+
if (restore) {
|
|
271
|
+
last = found.html;
|
|
272
|
+
ge.setHtml(found.html);
|
|
273
|
+
ge.emit('after-restore-draft', { canvas: ge.canvas, html: found.html, savedAt: found.savedAt });
|
|
274
|
+
} else {
|
|
275
|
+
remove();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
state = enabledAfterAsking ? 'enabled' : 'disabled';
|
|
279
|
+
if (state === 'enabled') { watch(); }
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
// The first init, once it has finished: the canvas is ready to be read
|
|
285
|
+
onInit: function() {
|
|
286
|
+
if (started) { return; }
|
|
287
|
+
started = true;
|
|
288
|
+
Promise.resolve().then(start);
|
|
289
|
+
},
|
|
290
|
+
|
|
291
|
+
onSourceOpen: function() { sourceOpen = true; },
|
|
292
|
+
onSourceClose: function() { sourceOpen = false; },
|
|
293
|
+
|
|
294
|
+
onDestroy: function() {
|
|
295
|
+
if (state === 'enabled' && timer) { save('destroy'); }
|
|
296
|
+
|
|
297
|
+
unwatch();
|
|
298
|
+
observer = null;
|
|
299
|
+
window.removeEventListener('pagehide', onPageHide);
|
|
300
|
+
if (state !== 'unavailable') { liveKeys.delete(key); }
|
|
301
|
+
state = 'destroyed';
|
|
302
|
+
},
|
|
303
|
+
|
|
304
|
+
methods: {
|
|
305
|
+
enableAutosave: function() {
|
|
306
|
+
if (state === 'unavailable') { return false; }
|
|
307
|
+
if (state === 'asking') {
|
|
308
|
+
enabledAfterAsking = true;
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
if (state !== 'enabled') {
|
|
312
|
+
state = 'enabled';
|
|
313
|
+
if (base !== null) { watch(); }
|
|
314
|
+
}
|
|
315
|
+
return true;
|
|
316
|
+
},
|
|
317
|
+
|
|
318
|
+
disableAutosave: function() {
|
|
319
|
+
if (state === 'unavailable') { return false; }
|
|
320
|
+
if (state === 'asking') {
|
|
321
|
+
enabledAfterAsking = false;
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
unwatch();
|
|
325
|
+
state = 'disabled';
|
|
326
|
+
return true;
|
|
327
|
+
},
|
|
328
|
+
|
|
329
|
+
saveDraft: function() {
|
|
330
|
+
if (state === 'unavailable' || state === 'asking') { return false; }
|
|
331
|
+
return save('api');
|
|
332
|
+
},
|
|
333
|
+
|
|
334
|
+
getDraft: function() {
|
|
335
|
+
if (state === 'unavailable') { return null; }
|
|
336
|
+
|
|
337
|
+
var found = readDraft(false);
|
|
338
|
+
return found ? { html: found.html, savedAt: found.savedAt } : null;
|
|
339
|
+
},
|
|
340
|
+
|
|
341
|
+
clearDraft: function() {
|
|
342
|
+
if (state === 'unavailable') { return false; }
|
|
343
|
+
|
|
344
|
+
remove();
|
|
345
|
+
return true;
|
|
346
|
+
},
|
|
347
|
+
},
|
|
348
|
+
};
|
|
349
|
+
};
|
|
@@ -64,5 +64,11 @@ GridEditor.containers.card = function(ge) {
|
|
|
64
64
|
unmark: function(container) {
|
|
65
65
|
ge.unwrapLabels(container);
|
|
66
66
|
},
|
|
67
|
+
|
|
68
|
+
// The same, on a copy of the canvas: unmark only touches markup
|
|
69
|
+
cleanMarkup: function(root) {
|
|
70
|
+
var definition = this;
|
|
71
|
+
dom.all(root, '[data-ge-container="card"]').forEach(function(container) { definition.unmark(container); });
|
|
72
|
+
},
|
|
67
73
|
};
|
|
68
74
|
};
|
|
@@ -157,6 +157,12 @@ GridEditor.texts.ckeditor = function(ge) {
|
|
|
157
157
|
});
|
|
158
158
|
},
|
|
159
159
|
|
|
160
|
+
// What destroy() puts back. Null while it is still starting
|
|
161
|
+
read: function(contentArea) {
|
|
162
|
+
var editor = editors.get(contentArea);
|
|
163
|
+
return editor ? editor.getData() : null;
|
|
164
|
+
},
|
|
165
|
+
|
|
160
166
|
stop: function(contentAreas) {
|
|
161
167
|
contentAreas.filter(function(contentArea) {
|
|
162
168
|
return dom.hasClass(contentArea, 'active');
|
|
@@ -187,6 +187,13 @@ GridEditor.features['codemirror-inline'] = function(ge) {
|
|
|
187
187
|
open.forEach(hide);
|
|
188
188
|
},
|
|
189
189
|
|
|
190
|
+
// On a copy of the canvas: the editors are drawers, which the core
|
|
191
|
+
// takes off, and what they hid shows again. What is typed in one and
|
|
192
|
+
// not applied is not the block's yet
|
|
193
|
+
cleanMarkup: function(root) {
|
|
194
|
+
dom.all(root, '.ge-code-hidden').forEach(function(node) { dom.removeClass(node, 'ge-code-hidden'); });
|
|
195
|
+
},
|
|
196
|
+
|
|
190
197
|
onInit: function() {
|
|
191
198
|
open.slice().forEach(function(entry) {
|
|
192
199
|
if (!dom.attached(entry.node)) {
|
|
@@ -28,11 +28,95 @@ var NOT_ELEMENTS = '.row, .ge-content, .ge-text-block, [data-ge-container], .ge-
|
|
|
28
28
|
GridEditor.features.elements = function(ge) {
|
|
29
29
|
|
|
30
30
|
var warnedIntoText = false;
|
|
31
|
+
var types = validTypes();
|
|
31
32
|
|
|
32
33
|
function elementsEnabled() {
|
|
33
34
|
if (ge.settings.elements.enabled !== 'auto') { return !!ge.settings.elements.enabled; }
|
|
34
35
|
|
|
35
|
-
|
|
36
|
+
// A host that offers elements on the toolbar uses them, even on a
|
|
37
|
+
// page that has none yet
|
|
38
|
+
return ge.settings.elements.auto || types.length > 0 ||
|
|
39
|
+
!!ge.canvas.querySelector(ge.settings.elements.selector);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The elements.types the toolbar can offer. One that can't be made is left out, with a word to the host. */
|
|
43
|
+
function validTypes() {
|
|
44
|
+
return (ge.settings.elements.types || []).filter(function(type, index) {
|
|
45
|
+
var named = type && typeof type.type === 'string' && type.type.trim() !== '';
|
|
46
|
+
var html = type && (typeof type.html === 'function' || (typeof type.html === 'string' && type.html.trim() !== ''));
|
|
47
|
+
|
|
48
|
+
if (!named || !html) {
|
|
49
|
+
ge.warn('elements.types[' + index + ']: a type needs a type name and its html, as a string or a ' +
|
|
50
|
+
'function; it is left off the toolbar.');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return named && html;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** A type's toolbar button. */
|
|
58
|
+
function typeItem(type) {
|
|
59
|
+
var mismatched = false;
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
// Asked again whenever the toolbar is built, so setLocale
|
|
63
|
+
// translates a labelKey
|
|
64
|
+
label: function() { return labelOf(type); },
|
|
65
|
+
iconClass: type.iconClass,
|
|
66
|
+
group: type.group || 'elements',
|
|
67
|
+
kind: 'element',
|
|
68
|
+
inColumn: true,
|
|
69
|
+
create: function() {
|
|
70
|
+
var element = makeElement(type);
|
|
71
|
+
|
|
72
|
+
if (element && !mismatched && !ge.settings.elements.auto && !element.matches(ge.settings.elements.selector)) {
|
|
73
|
+
mismatched = true;
|
|
74
|
+
ge.warn('elements.types "' + type.type + '": what it makes does not match elements.selector "' +
|
|
75
|
+
ge.settings.elements.selector + '", so it will not be an element once the editor looks again.');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return element;
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function labelOf(type) {
|
|
84
|
+
if (type.labelKey) { return ge.t(type.labelKey); }
|
|
85
|
+
|
|
86
|
+
return type.label || type.type;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* What a type's button makes: the root of its html, marked as the host
|
|
91
|
+
* marks its own, or a div around it when it has no single root. Null,
|
|
92
|
+
* with a word to the host, when there is nothing to make.
|
|
93
|
+
*/
|
|
94
|
+
function makeElement(type) {
|
|
95
|
+
var made;
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
made = typeof type.html === 'function' ? type.html() : type.html;
|
|
99
|
+
} catch (error) {
|
|
100
|
+
ge.warn('elements.types "' + type.type + '": its html function threw (' + error.message + '); nothing was added.');
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
var nodes = typeof made === 'string' ? dom.parse(made) : (made && made.nodeType ? [made] : []);
|
|
105
|
+
// Whitespace between tags is not content
|
|
106
|
+
nodes = nodes.filter(function(node) { return node.nodeType !== 3 || node.textContent.trim() !== ''; });
|
|
107
|
+
|
|
108
|
+
if (!nodes.length) {
|
|
109
|
+
ge.warn('elements.types "' + type.type + '": its html came out empty; nothing was added.');
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
var element = nodes.length === 1 && nodes[0].nodeType === 1 ? nodes[0] : dom.element('div');
|
|
114
|
+
if (element !== nodes[0] || nodes.length > 1) { fill(element, nodes); }
|
|
115
|
+
|
|
116
|
+
element.setAttribute('data-ge-element', type.type);
|
|
117
|
+
if (!element.hasAttribute('data-ge-label')) { element.setAttribute('data-ge-label', labelOf(type)); }
|
|
118
|
+
|
|
119
|
+
return element;
|
|
36
120
|
}
|
|
37
121
|
|
|
38
122
|
/** Whether a child of a column is an element. */
|
|
@@ -55,8 +139,8 @@ GridEditor.features.elements = function(ge) {
|
|
|
55
139
|
});
|
|
56
140
|
}
|
|
57
141
|
|
|
58
|
-
function unmarkElements() {
|
|
59
|
-
dom.all(ge.canvas, '.ge-element').forEach(function(element) {
|
|
142
|
+
function unmarkElements(root) {
|
|
143
|
+
dom.all(root || ge.canvas, '.ge-element').forEach(function(element) {
|
|
60
144
|
dom.removeClass(element, 'ge-element');
|
|
61
145
|
|
|
62
146
|
// A host element that had no class of its own should not come
|
|
@@ -121,6 +205,11 @@ GridEditor.features.elements = function(ge) {
|
|
|
121
205
|
return placed;
|
|
122
206
|
}
|
|
123
207
|
|
|
208
|
+
/** enabled: false takes the toolbar's element buttons away too. */
|
|
209
|
+
function elementsOff() {
|
|
210
|
+
return ge.settings.elements.enabled === false;
|
|
211
|
+
}
|
|
212
|
+
|
|
124
213
|
/** A place a caller named: an element, or the first one a selector matches. */
|
|
125
214
|
function nodeFrom(node) {
|
|
126
215
|
if (typeof node === 'string') { return document.querySelector(node); }
|
|
@@ -163,6 +252,10 @@ GridEditor.features.elements = function(ge) {
|
|
|
163
252
|
createElement: apiCreateElement,
|
|
164
253
|
},
|
|
165
254
|
|
|
255
|
+
// A button for each type the host offers, in the elements category
|
|
256
|
+
// unless the type names another
|
|
257
|
+
toolbar: elementsOff() ? [] : types.map(typeItem),
|
|
258
|
+
|
|
166
259
|
/** A node this plugin marked is an element, whatever else it is. */
|
|
167
260
|
kindOf: function(node) {
|
|
168
261
|
return dom.hasClass(node, 'ge-element') ? 'element' : null;
|
|
@@ -188,6 +281,7 @@ GridEditor.features.elements = function(ge) {
|
|
|
188
281
|
},
|
|
189
282
|
|
|
190
283
|
onInit: markElements,
|
|
191
|
-
onDeinit: unmarkElements,
|
|
284
|
+
onDeinit: function() { unmarkElements(); },
|
|
285
|
+
cleanMarkup: unmarkElements,
|
|
192
286
|
};
|
|
193
287
|
};
|
|
@@ -146,5 +146,9 @@ GridEditor.utilities.gutters = function(ge) {
|
|
|
146
146
|
listening = null;
|
|
147
147
|
dom.all(ge.canvas, '.ge-gutters').forEach(function(row) { dom.removeClass(row, 'ge-gutters'); });
|
|
148
148
|
},
|
|
149
|
+
|
|
150
|
+
cleanMarkup: function(root) {
|
|
151
|
+
dom.all(root, '.ge-gutters').forEach(function(row) { dom.removeClass(row, 'ge-gutters'); });
|
|
152
|
+
},
|
|
149
153
|
};
|
|
150
154
|
};
|
|
@@ -205,5 +205,9 @@ GridEditor.utilities.order = function(ge) {
|
|
|
205
205
|
listening = null;
|
|
206
206
|
dom.all(ge.canvas, '[data-ge-order]').forEach(function(col) { col.removeAttribute('data-ge-order'); });
|
|
207
207
|
},
|
|
208
|
+
|
|
209
|
+
cleanMarkup: function(root) {
|
|
210
|
+
dom.all(root, '[data-ge-order]').forEach(function(col) { col.removeAttribute('data-ge-order'); });
|
|
211
|
+
},
|
|
208
212
|
};
|
|
209
213
|
};
|
|
@@ -25,8 +25,8 @@ GridEditor.containers.popup = function(ge) {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/** Whether a popup with this id is on the canvas. */
|
|
28
|
-
function popupExists(id) {
|
|
29
|
-
return dom.all(ge.canvas, '[data-ge-popup-id]').some(function(popup) {
|
|
28
|
+
function popupExists(id, root) {
|
|
29
|
+
return dom.all(root || ge.canvas, '[data-ge-popup-id]').some(function(popup) {
|
|
30
30
|
return popupIdOf(popup) === id;
|
|
31
31
|
});
|
|
32
32
|
}
|
|
@@ -67,8 +67,10 @@ GridEditor.containers.popup = function(ge) {
|
|
|
67
67
|
});
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
function writePopupTriggerAttributes() {
|
|
71
|
-
|
|
70
|
+
function writePopupTriggerAttributes(root) {
|
|
71
|
+
root = root || ge.canvas;
|
|
72
|
+
|
|
73
|
+
dom.all(root, '[data-ge-popup-target]').forEach(function(trigger) {
|
|
72
74
|
var wanted = trigger.getAttribute('data-ge-popup-target');
|
|
73
75
|
|
|
74
76
|
// The warning marking is editing furniture, whether or not
|
|
@@ -76,7 +78,7 @@ GridEditor.containers.popup = function(ge) {
|
|
|
76
78
|
dom.removeClass(trigger, 'ge-popup-orphan');
|
|
77
79
|
dom.dropEmptyClass(trigger);
|
|
78
80
|
|
|
79
|
-
if (!popupExists(wanted)) { return; }
|
|
81
|
+
if (!popupExists(wanted, root)) { return; }
|
|
80
82
|
|
|
81
83
|
trigger.setAttribute('data-bs-toggle', 'modal');
|
|
82
84
|
trigger.setAttribute('data-bs-target', '#' + wanted);
|
|
@@ -91,7 +93,14 @@ GridEditor.containers.popup = function(ge) {
|
|
|
91
93
|
// are looked at whenever the canvas is initialized rather than only
|
|
92
94
|
// when a popup is touched
|
|
93
95
|
onInit: wirePopupTriggers,
|
|
94
|
-
onDeinit: writePopupTriggerAttributes,
|
|
96
|
+
onDeinit: function() { writePopupTriggerAttributes(); },
|
|
97
|
+
|
|
98
|
+
// What onDeinit and unmark do, on a copy of the canvas
|
|
99
|
+
cleanMarkup: function(root) {
|
|
100
|
+
var definition = this;
|
|
101
|
+
writePopupTriggerAttributes(root);
|
|
102
|
+
dom.all(root, '[data-ge-container="popup"]').forEach(function(container) { definition.unmark(container); });
|
|
103
|
+
},
|
|
95
104
|
|
|
96
105
|
create: function(options) {
|
|
97
106
|
var id = ge.containerId('popup');
|
|
@@ -149,8 +149,8 @@ GridEditor.features.sections = function(ge) {
|
|
|
149
149
|
});
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
function unmark() {
|
|
153
|
-
dom.children(ge.canvas, '.ge-section').forEach(function(section) {
|
|
152
|
+
function unmark(root) {
|
|
153
|
+
dom.children(root || ge.canvas, '.ge-section').forEach(function(section) {
|
|
154
154
|
dom.removeClass(section, 'ge-section');
|
|
155
155
|
});
|
|
156
156
|
}
|
|
@@ -197,7 +197,8 @@ GridEditor.features.sections = function(ge) {
|
|
|
197
197
|
}],
|
|
198
198
|
|
|
199
199
|
onInit: mark,
|
|
200
|
-
onDeinit: unmark,
|
|
200
|
+
onDeinit: function() { unmark(); },
|
|
201
|
+
cleanMarkup: unmark,
|
|
201
202
|
};
|
|
202
203
|
|
|
203
204
|
/** A section the toolbar just made, before init has marked it. */
|
|
@@ -120,6 +120,20 @@ GridEditor.texts.summernote = function(ge) {
|
|
|
120
120
|
});
|
|
121
121
|
},
|
|
122
122
|
|
|
123
|
+
// What destroy leaves in the content area: the editor's code
|
|
124
|
+
read: function(contentArea) {
|
|
125
|
+
var $ = window.jQuery;
|
|
126
|
+
if (!$ || !$.fn.summernote || !$(contentArea).data('summernote')) { return null; }
|
|
127
|
+
|
|
128
|
+
return $(contentArea).summernote('code');
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
// Its frame, which air mode puts right after the content area
|
|
132
|
+
cleanCopy: function(copy) {
|
|
133
|
+
var frame = copy.nextElementSibling;
|
|
134
|
+
if (frame && dom.hasClass(frame, 'note-editor')) { frame.remove(); }
|
|
135
|
+
},
|
|
136
|
+
|
|
123
137
|
stop: function(contentAreas) {
|
|
124
138
|
var $ = window.jQuery;
|
|
125
139
|
|
|
@@ -428,6 +428,12 @@ GridEditor.containers.tabs = function(ge) {
|
|
|
428
428
|
ge.unwrapLabels(container);
|
|
429
429
|
},
|
|
430
430
|
|
|
431
|
+
// The same, on a copy of the canvas: unmark only touches markup
|
|
432
|
+
cleanMarkup: function(root) {
|
|
433
|
+
var definition = this;
|
|
434
|
+
dom.all(root, '[data-ge-container="tabs"]').forEach(function(container) { definition.unmark(container); });
|
|
435
|
+
},
|
|
436
|
+
|
|
431
437
|
panelSection: function(node, kind) {
|
|
432
438
|
if (kind !== 'tabs' || !stripOf(node)) { return null; }
|
|
433
439
|
|