@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,420 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Offcanvases for grid-editor.
|
|
3
|
+
*
|
|
4
|
+
* A container plugin: load this file after the editor and the toolbar offers
|
|
5
|
+
* an offcanvas, a bootstrap panel that slides in from an edge of the window,
|
|
6
|
+
* and its trigger. What it can ask the editor for is the handle its factory
|
|
7
|
+
* is called with, described in docs/plugins.md.
|
|
8
|
+
*
|
|
9
|
+
* <script src="dist/grideditor.min.js"></script>
|
|
10
|
+
* <script src="dist/plugins/grideditor.offcanvas.min.js"></script>
|
|
11
|
+
*
|
|
12
|
+
* While editing the panel is rendered unfolded and in place, with its body as
|
|
13
|
+
* an ordinary region, and nothing in it reaches Bootstrap's javascript. Its
|
|
14
|
+
* options - placement, backdrop, body scroll, keyboard, responsive and dark
|
|
15
|
+
* theme - are Bootstrap's classes and attributes and nothing else: they are
|
|
16
|
+
* read from the markup, chosen in the container's settings panel, or given to
|
|
17
|
+
* createContainer and the offcanvas setting for the ones made new.
|
|
18
|
+
*/
|
|
19
|
+
import { GridEditor } from '../grideditor.js';
|
|
20
|
+
import * as dom from '../dom.js';
|
|
21
|
+
import { triggersFor } from '../triggers/triggers.js';
|
|
22
|
+
|
|
23
|
+
Object.assign(GridEditor.locales.en, {
|
|
24
|
+
'container.add_offcanvas': 'Offcanvas',
|
|
25
|
+
'container.offcanvas_title': 'Title',
|
|
26
|
+
'container.offcanvas_trigger': 'Open',
|
|
27
|
+
'tool.toggle_offcanvas': 'Fold this offcanvas away while editing',
|
|
28
|
+
'container.offcanvas_section': 'Offcanvas',
|
|
29
|
+
'container.offcanvas_placement': 'Placement',
|
|
30
|
+
'container.offcanvas_start': 'Start',
|
|
31
|
+
'container.offcanvas_end': 'End',
|
|
32
|
+
'container.offcanvas_top': 'Top',
|
|
33
|
+
'container.offcanvas_bottom': 'Bottom',
|
|
34
|
+
'container.offcanvas_backdrop': 'Backdrop',
|
|
35
|
+
'container.offcanvas_backdrop_yes': 'Yes',
|
|
36
|
+
'container.offcanvas_backdrop_no': 'No',
|
|
37
|
+
'container.offcanvas_backdrop_static': 'Static',
|
|
38
|
+
'container.offcanvas_scroll': 'Allow body scroll',
|
|
39
|
+
'container.offcanvas_keyboard': 'Close with Esc',
|
|
40
|
+
'container.offcanvas_responsive': 'Responsive',
|
|
41
|
+
'container.offcanvas_responsive_never': 'Always offcanvas',
|
|
42
|
+
'container.offcanvas_responsive_from': 'Inline from {breakpoint}',
|
|
43
|
+
'container.offcanvas_dark': 'Dark theme',
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
/** What an offcanvas made new has when neither the call nor the setting says. */
|
|
47
|
+
var DEFAULTS = {
|
|
48
|
+
placement: 'start',
|
|
49
|
+
backdrop: true,
|
|
50
|
+
scroll: false,
|
|
51
|
+
keyboard: true,
|
|
52
|
+
responsive: null,
|
|
53
|
+
dark: false,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
var KEYS = ['placement', 'backdrop', 'scroll', 'keyboard', 'responsive', 'dark'];
|
|
57
|
+
var PLACEMENTS = ['start', 'end', 'top', 'bottom'];
|
|
58
|
+
var BACKDROPS = [true, false, 'static'];
|
|
59
|
+
|
|
60
|
+
/** Each field's label, and each choice's, as keys the locale test can find. */
|
|
61
|
+
var LABELS = {
|
|
62
|
+
placement: { labelKey: 'container.offcanvas_placement' },
|
|
63
|
+
backdrop: { labelKey: 'container.offcanvas_backdrop' },
|
|
64
|
+
responsive: { labelKey: 'container.offcanvas_responsive' },
|
|
65
|
+
scroll: { labelKey: 'container.offcanvas_scroll' },
|
|
66
|
+
keyboard: { labelKey: 'container.offcanvas_keyboard' },
|
|
67
|
+
dark: { labelKey: 'container.offcanvas_dark' },
|
|
68
|
+
};
|
|
69
|
+
var PLACEMENT_CHOICES = [
|
|
70
|
+
{ value: 'start', labelKey: 'container.offcanvas_start' },
|
|
71
|
+
{ value: 'end', labelKey: 'container.offcanvas_end' },
|
|
72
|
+
{ value: 'top', labelKey: 'container.offcanvas_top' },
|
|
73
|
+
{ value: 'bottom', labelKey: 'container.offcanvas_bottom' },
|
|
74
|
+
];
|
|
75
|
+
var BACKDROP_CHOICES = [
|
|
76
|
+
{ value: 'true', labelKey: 'container.offcanvas_backdrop_yes' },
|
|
77
|
+
{ value: 'false', labelKey: 'container.offcanvas_backdrop_no' },
|
|
78
|
+
{ value: 'static', labelKey: 'container.offcanvas_backdrop_static' },
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
var fieldCounter = 0;
|
|
82
|
+
|
|
83
|
+
GridEditor.containers.offcanvas = function(ge) {
|
|
84
|
+
|
|
85
|
+
// Bootstrap has no offcanvas-xs: below the smallest breakpoint is always a panel
|
|
86
|
+
var RESPONSIVE = ge.breakpoints.filter(function(key) { return key !== 'xs'; });
|
|
87
|
+
var sections = []; // { node, element, render } for every panel section built
|
|
88
|
+
|
|
89
|
+
var triggers = triggersFor(ge, 'offcanvas', 'data-ge-offcanvas-id', 'data-ge-offcanvas-target', 'offcanvas');
|
|
90
|
+
|
|
91
|
+
var CHECKS = {
|
|
92
|
+
placement: function(value) { return PLACEMENTS.indexOf(value) !== -1; },
|
|
93
|
+
backdrop: function(value) { return BACKDROPS.indexOf(value) !== -1; },
|
|
94
|
+
responsive: function(value) { return value === null || RESPONSIVE.indexOf(value) !== -1; },
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
function isValid(key, value) {
|
|
98
|
+
return CHECKS[key] ? CHECKS[key](value) : typeof value === 'boolean';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** The values an option takes, as the warning lists them. */
|
|
102
|
+
function takes(key) {
|
|
103
|
+
if (key === 'placement') { return JSON.stringify(PLACEMENTS); }
|
|
104
|
+
if (key === 'backdrop') { return JSON.stringify(BACKDROPS); }
|
|
105
|
+
if (key === 'responsive') { return JSON.stringify([null].concat(RESPONSIVE)); }
|
|
106
|
+
|
|
107
|
+
return '[true,false]';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* A value for a new offcanvas: the option, or the offcanvas setting, or
|
|
112
|
+
* the default. One the option does not take is warned about and the
|
|
113
|
+
* default used.
|
|
114
|
+
*/
|
|
115
|
+
function chosen(options, key) {
|
|
116
|
+
var setting = ge.settings.offcanvas || {};
|
|
117
|
+
var value = options[key] !== undefined ? options[key] : setting[key];
|
|
118
|
+
|
|
119
|
+
if (value === undefined) { return DEFAULTS[key]; }
|
|
120
|
+
if (isValid(key, value)) { return value; }
|
|
121
|
+
|
|
122
|
+
ge.warn('offcanvas: ' + JSON.stringify(value) + ' is not a ' + key + ', which takes ' +
|
|
123
|
+
takes(key) + ': ' + JSON.stringify(DEFAULTS[key]) + ' is used');
|
|
124
|
+
return DEFAULTS[key];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** The class that makes a node a panel: `offcanvas`, or `offcanvas-{breakpoint}` for a responsive one. */
|
|
128
|
+
function isPanelClass(name) {
|
|
129
|
+
return name === 'offcanvas' || (/^offcanvas-/.test(name) && RESPONSIVE.indexOf(name.slice(10)) !== -1);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function isPanel(node) {
|
|
133
|
+
return Array.prototype.some.call(node.classList, isPanelClass);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** The panel, found by the id the container carries: with responsive it has no .offcanvas class. */
|
|
137
|
+
function panelOf(container) {
|
|
138
|
+
var id = container.getAttribute('data-ge-offcanvas-id');
|
|
139
|
+
var own = dom.children(container);
|
|
140
|
+
|
|
141
|
+
return own.filter(function(node) { return id && node.id === id; })[0] || own.filter(isPanel)[0] || null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function triggerOf(container) {
|
|
145
|
+
return dom.child(container, '[data-ge-offcanvas-target]');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** The class that marks a trigger while editing is furniture: the page has no use for it. */
|
|
149
|
+
function dropTriggerMarks(root) {
|
|
150
|
+
dom.all(root, '.ge-offcanvas-trigger').forEach(function(trigger) {
|
|
151
|
+
dom.removeClass(trigger, 'ge-offcanvas-trigger');
|
|
152
|
+
dom.dropEmptyClass(trigger);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function hideClass(responsive) {
|
|
157
|
+
return 'd-' + responsive + '-none';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** What a panel's classes and attributes say it is. */
|
|
161
|
+
function read(container) {
|
|
162
|
+
var panel = panelOf(container);
|
|
163
|
+
var backdrop = panel.getAttribute('data-bs-backdrop');
|
|
164
|
+
var responsive = null;
|
|
165
|
+
|
|
166
|
+
Array.prototype.forEach.call(panel.classList, function(name) {
|
|
167
|
+
if (name !== 'offcanvas' && isPanelClass(name)) { responsive = name.slice(10); }
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
placement: PLACEMENTS.filter(function(place) { return dom.hasClass(panel, 'offcanvas-' + place); })[0] || 'start',
|
|
172
|
+
backdrop: backdrop === 'false' ? false : backdrop === 'static' ? 'static' : true,
|
|
173
|
+
scroll: panel.getAttribute('data-bs-scroll') === 'true',
|
|
174
|
+
keyboard: panel.getAttribute('data-bs-keyboard') !== 'false',
|
|
175
|
+
responsive: responsive,
|
|
176
|
+
dark: panel.getAttribute('data-bs-theme') === 'dark',
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function setAttribute(panel, name, value) {
|
|
181
|
+
if (value === null) {
|
|
182
|
+
panel.removeAttribute(name);
|
|
183
|
+
} else {
|
|
184
|
+
panel.setAttribute(name, value);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function write(container, key, value) {
|
|
189
|
+
var panel = panelOf(container);
|
|
190
|
+
|
|
191
|
+
if (key === 'placement') {
|
|
192
|
+
PLACEMENTS.forEach(function(place) { dom.removeClass(panel, 'offcanvas-' + place); });
|
|
193
|
+
dom.addClass(panel, 'offcanvas-' + value);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (key === 'backdrop') { setAttribute(panel, 'data-bs-backdrop', value === true ? null : String(value)); }
|
|
197
|
+
if (key === 'scroll') { setAttribute(panel, 'data-bs-scroll', value ? 'true' : null); }
|
|
198
|
+
if (key === 'keyboard') { setAttribute(panel, 'data-bs-keyboard', value ? null : 'false'); }
|
|
199
|
+
|
|
200
|
+
if (key === 'responsive') {
|
|
201
|
+
var before = read(container).responsive;
|
|
202
|
+
var trigger = triggerOf(container);
|
|
203
|
+
var rest = Array.prototype.filter.call(panel.classList, function(name) { return !isPanelClass(name); });
|
|
204
|
+
|
|
205
|
+
// The one class that makes it a panel comes first, as Bootstrap writes it
|
|
206
|
+
panel.className = [value === null ? 'offcanvas' : 'offcanvas-' + value].concat(rest).join(' ');
|
|
207
|
+
|
|
208
|
+
if (trigger) {
|
|
209
|
+
if (before !== null) { dom.removeClass(trigger, hideClass(before)); }
|
|
210
|
+
if (value !== null) { dom.addClass(trigger, hideClass(value)); }
|
|
211
|
+
dom.dropEmptyClass(trigger);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (key === 'dark') {
|
|
216
|
+
if (value) {
|
|
217
|
+
panel.setAttribute('data-bs-theme', 'dark');
|
|
218
|
+
} else if (panel.getAttribute('data-bs-theme') === 'dark') {
|
|
219
|
+
panel.removeAttribute('data-bs-theme');
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function nextId() {
|
|
225
|
+
return 'ge-offcanvas-option-' + (++fieldCounter);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function field(labelKey, control, id) {
|
|
229
|
+
var box = dom.element('div', { 'class': 'ge-offcanvas-option mb-2' });
|
|
230
|
+
|
|
231
|
+
box.appendChild(dom.element('label', { 'class': 'form-label', 'for': id }, ge.t(labelKey)));
|
|
232
|
+
box.appendChild(control);
|
|
233
|
+
|
|
234
|
+
return box;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function checkbox(labelKey, onChange) {
|
|
238
|
+
var id = nextId();
|
|
239
|
+
var box = dom.element('div', { 'class': 'form-check' });
|
|
240
|
+
var input = box.appendChild(dom.element('input', { 'class': 'form-check-input', type: 'checkbox', id: id }));
|
|
241
|
+
|
|
242
|
+
box.appendChild(dom.element('label', { 'class': 'form-check-label', 'for': id }, ge.t(labelKey)));
|
|
243
|
+
input.addEventListener('change', function() { onChange(input.checked); });
|
|
244
|
+
|
|
245
|
+
return { box: box, input: input };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** A select over `choices`, each { value, label }, that calls `onChange` with the value chosen. */
|
|
249
|
+
function select(choices, onChange) {
|
|
250
|
+
var id = nextId();
|
|
251
|
+
var control = dom.element('select', { 'class': 'form-select form-select-sm', id: id });
|
|
252
|
+
|
|
253
|
+
choices.forEach(function(choice) {
|
|
254
|
+
control.appendChild(dom.element('option', { value: choice.value }, choice.label));
|
|
255
|
+
});
|
|
256
|
+
control.addEventListener('change', function() { onChange(control.value); });
|
|
257
|
+
|
|
258
|
+
return { id: id, control: control };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** The Offcanvas section of an offcanvas's settings panel. */
|
|
262
|
+
function createSection(container) {
|
|
263
|
+
var body = dom.element('div', { 'class': 'ge-offcanvas-options' });
|
|
264
|
+
|
|
265
|
+
function change(key, value) {
|
|
266
|
+
write(container, key, value);
|
|
267
|
+
render();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
var translated = function(choice) { return { value: choice.value, label: ge.t(choice.labelKey) }; };
|
|
271
|
+
|
|
272
|
+
var selects = {
|
|
273
|
+
placement: select(PLACEMENT_CHOICES.map(translated), function(value) { change('placement', value); }),
|
|
274
|
+
backdrop: select(BACKDROP_CHOICES.map(translated), function(value) {
|
|
275
|
+
change('backdrop', value === 'static' ? 'static' : value === 'true');
|
|
276
|
+
}),
|
|
277
|
+
responsive: select([{ value: '', label: ge.t('container.offcanvas_responsive_never') }].concat(
|
|
278
|
+
RESPONSIVE.map(function(key) {
|
|
279
|
+
return { value: key, label: ge.t('container.offcanvas_responsive_from', { breakpoint: key }) };
|
|
280
|
+
})
|
|
281
|
+
), function(value) { change('responsive', value === '' ? null : value); }),
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
Object.keys(selects).forEach(function(key) {
|
|
285
|
+
var box = field(LABELS[key].labelKey, selects[key].control, selects[key].id);
|
|
286
|
+
|
|
287
|
+
box.setAttribute('data-ge-offcanvas-option', key);
|
|
288
|
+
body.appendChild(box);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
var inputs = {};
|
|
292
|
+
['scroll', 'keyboard', 'dark'].forEach(function(key) {
|
|
293
|
+
var each = checkbox(LABELS[key].labelKey, function(on) { change(key, on); });
|
|
294
|
+
|
|
295
|
+
inputs[key] = each.input;
|
|
296
|
+
each.box.setAttribute('data-ge-offcanvas-option', key);
|
|
297
|
+
body.appendChild(each.box);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
/** Fill the fields from the markup. */
|
|
301
|
+
function render() {
|
|
302
|
+
var values = read(container);
|
|
303
|
+
|
|
304
|
+
selects.placement.control.value = values.placement;
|
|
305
|
+
selects.backdrop.control.value = String(values.backdrop);
|
|
306
|
+
selects.responsive.control.value = values.responsive === null ? '' : values.responsive;
|
|
307
|
+
Object.keys(inputs).forEach(function(key) { inputs[key].checked = values[key]; });
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
render();
|
|
311
|
+
sections.push({ node: container, element: body, render: render });
|
|
312
|
+
|
|
313
|
+
return body;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
labelKey: 'container.add_offcanvas',
|
|
318
|
+
paneKind: 'offcanvas',
|
|
319
|
+
|
|
320
|
+
// The ids a copy of an offcanvas has to rename together
|
|
321
|
+
references: ['data-ge-offcanvas-id', 'data-ge-offcanvas-target'],
|
|
322
|
+
|
|
323
|
+
onInit: triggers.onInit,
|
|
324
|
+
|
|
325
|
+
onDeinit: function() {
|
|
326
|
+
sections = [];
|
|
327
|
+
triggers.onDeinit();
|
|
328
|
+
dropTriggerMarks(ge.canvas);
|
|
329
|
+
},
|
|
330
|
+
|
|
331
|
+
// What onDeinit and unmark do, on a copy of the canvas
|
|
332
|
+
cleanMarkup: function(root) {
|
|
333
|
+
var definition = this;
|
|
334
|
+
triggers.write(root);
|
|
335
|
+
dropTriggerMarks(root);
|
|
336
|
+
dom.all(root, '[data-ge-container="offcanvas"]').forEach(function(container) { definition.unmark(container); });
|
|
337
|
+
},
|
|
338
|
+
|
|
339
|
+
create: function(options) {
|
|
340
|
+
options = options || {};
|
|
341
|
+
|
|
342
|
+
var id = ge.containerId('offcanvas');
|
|
343
|
+
var container = dom.element('div', { 'data-ge-container': 'offcanvas', 'data-ge-offcanvas-id': id });
|
|
344
|
+
|
|
345
|
+
if (options.trigger !== false) {
|
|
346
|
+
container.appendChild(dom.element('button', {
|
|
347
|
+
type: 'button',
|
|
348
|
+
'class': 'btn btn-primary ge-offcanvas-trigger',
|
|
349
|
+
'data-ge-offcanvas-target': id,
|
|
350
|
+
}, options.trigger_label || ge.t('container.offcanvas_trigger')));
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
var panel = container.appendChild(dom.element('div', {
|
|
354
|
+
'class': 'offcanvas',
|
|
355
|
+
tabindex: '-1',
|
|
356
|
+
id: id,
|
|
357
|
+
'aria-labelledby': id + '-label',
|
|
358
|
+
}));
|
|
359
|
+
var header = panel.appendChild(dom.element('div', { 'class': 'offcanvas-header' }));
|
|
360
|
+
|
|
361
|
+
header.appendChild(dom.element('h5', { 'class': 'offcanvas-title', id: id + '-label' }))
|
|
362
|
+
.appendChild(dom.element('span', { 'class': 'ge-pane-label' },
|
|
363
|
+
options.title || ge.t('container.offcanvas_title')));
|
|
364
|
+
header.appendChild(dom.create('<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>'));
|
|
365
|
+
panel.appendChild(dom.element('div', { 'class': 'offcanvas-body' })).appendChild(ge.defaultRegion());
|
|
366
|
+
|
|
367
|
+
KEYS.forEach(function(key) { write(container, key, chosen(options, key)); });
|
|
368
|
+
|
|
369
|
+
return container;
|
|
370
|
+
},
|
|
371
|
+
|
|
372
|
+
mark: function(container) {
|
|
373
|
+
var header = dom.child(panelOf(container), '.offcanvas-header');
|
|
374
|
+
|
|
375
|
+
// Nothing in an offcanvas may reach Bootstrap while editing:
|
|
376
|
+
// the panel is rendered unfolded and static, and a real
|
|
377
|
+
// one sliding in over it would be the editor fighting itself
|
|
378
|
+
dom.all(header, '[data-bs-dismiss]').forEach(function(dismiss) {
|
|
379
|
+
ge.suspendToggles(dismiss);
|
|
380
|
+
});
|
|
381
|
+
dom.all(header, '.offcanvas-title').forEach(function(title) {
|
|
382
|
+
ge.makeLabelEditable(ge.labelIn(title));
|
|
383
|
+
});
|
|
384
|
+
},
|
|
385
|
+
|
|
386
|
+
unmark: function(container) {
|
|
387
|
+
var header = dom.child(panelOf(container), '.offcanvas-header');
|
|
388
|
+
|
|
389
|
+
if (header) {
|
|
390
|
+
ge.resumeToggles(header);
|
|
391
|
+
ge.unwrapLabels(header);
|
|
392
|
+
}
|
|
393
|
+
dom.removeClass(container, 'ge-offcanvas-collapsed');
|
|
394
|
+
},
|
|
395
|
+
|
|
396
|
+
/** A page of unfolded panels stays workable if they can be folded away. */
|
|
397
|
+
tools: function(drawer, container) {
|
|
398
|
+
ge.createTool(drawer, ge.t('tool.toggle_offcanvas'), 'ge-toggle-offcanvas', 'bi bi-chevron-bar-contract',
|
|
399
|
+
function() {
|
|
400
|
+
dom.toggleClass(container, 'ge-offcanvas-collapsed');
|
|
401
|
+
});
|
|
402
|
+
},
|
|
403
|
+
|
|
404
|
+
panelSection: function(node, kind) {
|
|
405
|
+
if (kind === 'offcanvas' && panelOf(node)) {
|
|
406
|
+
return { labelKey: 'container.offcanvas_section', body: createSection(node) };
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return null;
|
|
410
|
+
},
|
|
411
|
+
|
|
412
|
+
// After a view change or a change of classes: the sections follow
|
|
413
|
+
onRefresh: function(scope) {
|
|
414
|
+
sections = sections.filter(function(entry) { return entry.element.isConnected; });
|
|
415
|
+
sections.forEach(function(entry) {
|
|
416
|
+
if (entry.node === scope || scope.contains(entry.node)) { entry.render(); }
|
|
417
|
+
});
|
|
418
|
+
},
|
|
419
|
+
};
|
|
420
|
+
};
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { GridEditor } from '../grideditor.js';
|
|
12
12
|
import * as dom from '../dom.js';
|
|
13
|
+
import { triggersFor } from '../triggers/triggers.js';
|
|
13
14
|
|
|
14
15
|
Object.assign(GridEditor.locales.en, {
|
|
15
16
|
'container.add_popup': 'Popup',
|
|
@@ -20,85 +21,22 @@ Object.assign(GridEditor.locales.en, {
|
|
|
20
21
|
|
|
21
22
|
GridEditor.containers.popup = function(ge) {
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
return container.getAttribute('data-ge-popup-id');
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** Whether a popup with this id is on the canvas. */
|
|
28
|
-
function popupExists(id, root) {
|
|
29
|
-
return dom.all(root || ge.canvas, '[data-ge-popup-id]').some(function(popup) {
|
|
30
|
-
return popupIdOf(popup) === id;
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function wirePopupTriggers() {
|
|
35
|
-
dom.all(ge.canvas, '[data-ge-popup-target]').forEach(function(trigger) {
|
|
36
|
-
dom.addClass(trigger, 'ge-popup-trigger');
|
|
37
|
-
var wanted = trigger.getAttribute('data-ge-popup-target');
|
|
38
|
-
|
|
39
|
-
// Bootstrap's own attributes are written at getHtml time, not
|
|
40
|
-
// while editing, so a click here cannot open a real modal
|
|
41
|
-
trigger.removeAttribute('data-bs-toggle');
|
|
42
|
-
trigger.removeAttribute('data-bs-target');
|
|
43
|
-
|
|
44
|
-
if (popupExists(wanted)) {
|
|
45
|
-
dom.removeClass(trigger, 'ge-popup-orphan');
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
var column = trigger.closest('.column');
|
|
50
|
-
var nearby = column ? dom.all(column, '[data-ge-popup-id]') : [];
|
|
51
|
-
|
|
52
|
-
if (nearby.length === 1) {
|
|
53
|
-
trigger.setAttribute('data-ge-popup-target', popupIdOf(nearby[0]));
|
|
54
|
-
dom.removeClass(trigger, 'ge-popup-orphan');
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
dom.addClass(trigger, 'ge-popup-orphan');
|
|
59
|
-
|
|
60
|
-
ge.operate(function() {
|
|
61
|
-
ge.emit('popup-orphan', ge.payloadFor('popup', trigger, {
|
|
62
|
-
parent: trigger.parentElement,
|
|
63
|
-
source: 'api',
|
|
64
|
-
missing: wanted,
|
|
65
|
-
}));
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function writePopupTriggerAttributes(root) {
|
|
71
|
-
root = root || ge.canvas;
|
|
72
|
-
|
|
73
|
-
dom.all(root, '[data-ge-popup-target]').forEach(function(trigger) {
|
|
74
|
-
var wanted = trigger.getAttribute('data-ge-popup-target');
|
|
75
|
-
|
|
76
|
-
// The warning marking is editing furniture, whether or not
|
|
77
|
-
// the trigger can be wired up
|
|
78
|
-
dom.removeClass(trigger, 'ge-popup-orphan');
|
|
79
|
-
dom.dropEmptyClass(trigger);
|
|
80
|
-
|
|
81
|
-
if (!popupExists(wanted, root)) { return; }
|
|
82
|
-
|
|
83
|
-
trigger.setAttribute('data-bs-toggle', 'modal');
|
|
84
|
-
trigger.setAttribute('data-bs-target', '#' + wanted);
|
|
85
|
-
});
|
|
86
|
-
}
|
|
24
|
+
var triggers = triggersFor(ge, 'popup', 'data-ge-popup-id', 'data-ge-popup-target', 'modal');
|
|
87
25
|
|
|
88
26
|
return {
|
|
89
27
|
labelKey: 'container.add_popup',
|
|
90
28
|
paneKind: 'popup',
|
|
91
29
|
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
onInit:
|
|
96
|
-
onDeinit:
|
|
30
|
+
// The ids a copy of a popup has to rename together
|
|
31
|
+
references: ['data-ge-popup-id', 'data-ge-popup-target'],
|
|
32
|
+
|
|
33
|
+
onInit: triggers.onInit,
|
|
34
|
+
onDeinit: triggers.onDeinit,
|
|
97
35
|
|
|
98
36
|
// What onDeinit and unmark do, on a copy of the canvas
|
|
99
37
|
cleanMarkup: function(root) {
|
|
100
38
|
var definition = this;
|
|
101
|
-
|
|
39
|
+
triggers.write(root);
|
|
102
40
|
dom.all(root, '[data-ge-container="popup"]').forEach(function(container) { definition.unmark(container); });
|
|
103
41
|
},
|
|
104
42
|
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the popup and the offcanvas plugins share: the host's triggers.
|
|
3
|
+
*
|
|
4
|
+
* A trigger is markup the host owns - a button, a link - anywhere in the
|
|
5
|
+
* canvas, that points at a container by id. While editing it must not reach
|
|
6
|
+
* Bootstrap, and one that points at nothing is warned about. Not a public
|
|
7
|
+
* API: the build puts a copy of this file in each plugin that imports it,
|
|
8
|
+
* so it keeps nothing of its own between plugins.
|
|
9
|
+
*/
|
|
10
|
+
import * as dom from '../dom.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The trigger handling of one kind of container.
|
|
14
|
+
*
|
|
15
|
+
* `kind` names the container (`popup`), its classes (`ge-popup-trigger`,
|
|
16
|
+
* `ge-popup-orphan`) and the event for a trigger left pointing at nothing
|
|
17
|
+
* (`popup-orphan`). `idAttribute` is the attribute a container has its id in,
|
|
18
|
+
* `targetAttribute` the one a trigger names it with, and `toggle` is
|
|
19
|
+
* Bootstrap's `data-bs-toggle` for it, written on the way out.
|
|
20
|
+
*
|
|
21
|
+
* Gives back `onInit` and `onDeinit`, for the plugin to hand the editor as
|
|
22
|
+
* its own, and `write(root)`, what onDeinit does to the canvas, for
|
|
23
|
+
* cleanMarkup to do to a copy.
|
|
24
|
+
*/
|
|
25
|
+
export function triggersFor(ge, kind, idAttribute, targetAttribute, toggle) {
|
|
26
|
+
var triggerClass = 'ge-' + kind + '-trigger';
|
|
27
|
+
var orphanClass = 'ge-' + kind + '-orphan';
|
|
28
|
+
var listening = null;
|
|
29
|
+
|
|
30
|
+
function idOf(container) {
|
|
31
|
+
return container.getAttribute(idAttribute);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Whether a container with this id is on the canvas. */
|
|
35
|
+
function exists(id, root) {
|
|
36
|
+
return dom.all(root || ge.canvas, '[' + idAttribute + ']').some(function(container) {
|
|
37
|
+
return idOf(container) === id;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function wire() {
|
|
42
|
+
dom.all(ge.canvas, '[' + targetAttribute + ']').forEach(function(trigger) {
|
|
43
|
+
dom.addClass(trigger, triggerClass);
|
|
44
|
+
var wanted = trigger.getAttribute(targetAttribute);
|
|
45
|
+
|
|
46
|
+
// Bootstrap's own attributes are written at getHtml time, not
|
|
47
|
+
// while editing, so a click here cannot open a real container
|
|
48
|
+
trigger.removeAttribute('data-bs-toggle');
|
|
49
|
+
trigger.removeAttribute('data-bs-target');
|
|
50
|
+
|
|
51
|
+
if (exists(wanted)) {
|
|
52
|
+
dom.removeClass(trigger, orphanClass);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
var column = trigger.closest('.column');
|
|
57
|
+
var nearby = column ? dom.all(column, '[' + idAttribute + ']') : [];
|
|
58
|
+
|
|
59
|
+
if (nearby.length === 1) {
|
|
60
|
+
trigger.setAttribute(targetAttribute, idOf(nearby[0]));
|
|
61
|
+
dom.removeClass(trigger, orphanClass);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
dom.addClass(trigger, orphanClass);
|
|
66
|
+
|
|
67
|
+
ge.operate(function() {
|
|
68
|
+
ge.emit(kind + '-orphan', ge.payloadFor(kind, trigger, {
|
|
69
|
+
parent: trigger.parentElement,
|
|
70
|
+
source: 'api',
|
|
71
|
+
missing: wanted,
|
|
72
|
+
}));
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function write(root) {
|
|
78
|
+
root = root || ge.canvas;
|
|
79
|
+
|
|
80
|
+
dom.all(root, '[' + targetAttribute + ']').forEach(function(trigger) {
|
|
81
|
+
var wanted = trigger.getAttribute(targetAttribute);
|
|
82
|
+
|
|
83
|
+
// The warning marking is editing furniture, whether or not
|
|
84
|
+
// the trigger can be wired up
|
|
85
|
+
dom.removeClass(trigger, orphanClass);
|
|
86
|
+
dom.dropEmptyClass(trigger);
|
|
87
|
+
|
|
88
|
+
if (!exists(wanted, root)) { return; }
|
|
89
|
+
|
|
90
|
+
trigger.setAttribute('data-bs-toggle', toggle);
|
|
91
|
+
trigger.setAttribute('data-bs-target', '#' + wanted);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
write: write,
|
|
97
|
+
|
|
98
|
+
// Triggers are markup the host owns, anywhere in the canvas, so they
|
|
99
|
+
// are looked at whenever the canvas is initialized rather than only
|
|
100
|
+
// when a container is touched
|
|
101
|
+
onInit: function() {
|
|
102
|
+
wire();
|
|
103
|
+
|
|
104
|
+
if (listening) { listening.abort(); }
|
|
105
|
+
listening = new AbortController();
|
|
106
|
+
|
|
107
|
+
// A trigger is often a link, and a link still navigates even
|
|
108
|
+
// with its Bootstrap attributes suspended
|
|
109
|
+
dom.delegate(ge.canvas, 'click', '.' + triggerClass + ', [' + targetAttribute + ']', function(e) {
|
|
110
|
+
if (dom.hasClass(ge.canvas, 'ge-editing')) { e.preventDefault(); }
|
|
111
|
+
}, { signal: listening.signal });
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
onDeinit: function() {
|
|
115
|
+
if (listening) { listening.abort(); }
|
|
116
|
+
listening = null;
|
|
117
|
+
|
|
118
|
+
write();
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|