@themarioga/grid-editor 8.7.0 → 8.8.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 +29 -0
- package/README.md +24 -7
- package/dist/grideditor.bundle.min.js +2 -2
- package/dist/grideditor.css +32 -0
- package/dist/grideditor.d.ts +44 -1
- package/dist/grideditor.esm.js +43 -5
- package/dist/grideditor.js +43 -5
- 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 +27 -0
- package/dist/locales/grideditor.es.js +27 -0
- package/dist/locales/grideditor.es.min.js +1 -1
- package/dist/locales/grideditor.es.min.js.map +2 -2
- package/dist/plugins/grideditor.autosave.esm.js +15 -0
- package/dist/plugins/grideditor.autosave.js +15 -0
- package/dist/plugins/grideditor.autosave.min.js +1 -1
- package/dist/plugins/grideditor.autosave.min.js.map +2 -2
- package/dist/plugins/grideditor.carousel.esm.js +775 -0
- package/dist/plugins/grideditor.carousel.js +780 -0
- package/dist/plugins/grideditor.carousel.min.js +1 -0
- package/dist/plugins/grideditor.carousel.min.js.map +7 -0
- package/docs/events.md +6 -3
- package/docs/locale-keys.md +29 -2
- package/docs/plugins.md +17 -14
- package/example/containers.html +44 -4
- package/package.json +1 -1
- package/src/js/grideditor.js +58 -17
- package/src/js/locales/grideditor.es.js +27 -0
- package/src/js/plugins/grideditor.autosave.js +13 -0
- package/src/js/plugins/grideditor.carousel.js +780 -0
- package/src/less/grideditor.less +35 -0
- package/types/grideditor.d.ts +44 -1
|
@@ -0,0 +1,780 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Carousels for grid-editor.
|
|
3
|
+
*
|
|
4
|
+
* A container plugin: load this file after the editor and the toolbar offers
|
|
5
|
+
* a carousel, a bootstrap carousel whose slides are editable regions. What it
|
|
6
|
+
* can ask the editor for is the handle its factory is called with, described
|
|
7
|
+
* in docs/plugins.md.
|
|
8
|
+
*
|
|
9
|
+
* <script src="dist/grideditor.min.js"></script>
|
|
10
|
+
* <script src="dist/plugins/grideditor.carousel.min.js"></script>
|
|
11
|
+
*
|
|
12
|
+
* Only one slide shows on the canvas, as on the page. The container's drawer
|
|
13
|
+
* says which, and moves to the next and the previous; Bootstrap's own arrows
|
|
14
|
+
* and indicators are drawn but do not answer, and neither does its javascript:
|
|
15
|
+
* the attributes it acts on are put aside while editing. A carousel's options
|
|
16
|
+
* - arrows, indicators, fade, dark theme, autoplay and its interval, pause,
|
|
17
|
+
* wrap, keyboard and touch - are Bootstrap's classes and attributes and
|
|
18
|
+
* nothing else: they are read from the markup, chosen in the container's
|
|
19
|
+
* settings panel, or given to createContainer and the carousel setting for
|
|
20
|
+
* the ones made new.
|
|
21
|
+
*/
|
|
22
|
+
import { GridEditor } from '../grideditor.js';
|
|
23
|
+
import * as dom from '../dom.js';
|
|
24
|
+
|
|
25
|
+
Object.assign(GridEditor.locales.en, {
|
|
26
|
+
'container.add_carousel': 'Carousel',
|
|
27
|
+
'container.add_carousel_item': 'Add slide',
|
|
28
|
+
'confirm.delete_carousel_item': 'Delete this slide and everything in it?',
|
|
29
|
+
'panel.kind_carousel_item': 'Slide',
|
|
30
|
+
'container.carousel_previous': 'Previous',
|
|
31
|
+
'container.carousel_next': 'Next',
|
|
32
|
+
'container.carousel_slide_label': 'Slide {number}',
|
|
33
|
+
'container.carousel_counter': '{current} / {total}',
|
|
34
|
+
'tool.carousel_show_previous': 'Previous slide',
|
|
35
|
+
'tool.carousel_show_next': 'Next slide',
|
|
36
|
+
'tool.carousel_move_back': 'Move back',
|
|
37
|
+
'tool.carousel_move_forward': 'Move forward',
|
|
38
|
+
'container.carousel_section': 'Carousel',
|
|
39
|
+
'container.carousel_controls': 'Arrows',
|
|
40
|
+
'container.carousel_indicators': 'Indicators',
|
|
41
|
+
'container.carousel_fade': 'Fade',
|
|
42
|
+
'container.carousel_dark': 'Dark theme',
|
|
43
|
+
'container.carousel_ride': 'Autoplay',
|
|
44
|
+
'container.carousel_ride_no': 'No',
|
|
45
|
+
'container.carousel_ride_load': 'On load',
|
|
46
|
+
'container.carousel_ride_interaction': 'After first interaction',
|
|
47
|
+
'container.carousel_interval': 'Interval (s)',
|
|
48
|
+
'container.carousel_pause': 'Pause on hover',
|
|
49
|
+
'container.carousel_wrap': 'Wrap around',
|
|
50
|
+
'container.carousel_keyboard': 'Keyboard',
|
|
51
|
+
'container.carousel_touch': 'Touch swipe',
|
|
52
|
+
'container.carousel_item_section': 'Slide',
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
/** What a carousel made new has when neither the call nor the setting says. */
|
|
56
|
+
var DEFAULTS = {
|
|
57
|
+
controls: true,
|
|
58
|
+
indicators: true,
|
|
59
|
+
fade: false,
|
|
60
|
+
dark: false,
|
|
61
|
+
ride: false,
|
|
62
|
+
interval: null,
|
|
63
|
+
pause: true,
|
|
64
|
+
wrap: true,
|
|
65
|
+
keyboard: true,
|
|
66
|
+
touch: true,
|
|
67
|
+
slides: 2,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
var CHECKBOXES = ['controls', 'indicators', 'fade', 'dark'];
|
|
71
|
+
var BEHAVIOURS = ['pause', 'wrap', 'keyboard', 'touch'];
|
|
72
|
+
|
|
73
|
+
/** Each option's checkbox in the panel, and its label. */
|
|
74
|
+
var LABELS = {
|
|
75
|
+
controls: { labelKey: 'container.carousel_controls' },
|
|
76
|
+
indicators: { labelKey: 'container.carousel_indicators' },
|
|
77
|
+
fade: { labelKey: 'container.carousel_fade' },
|
|
78
|
+
dark: { labelKey: 'container.carousel_dark' },
|
|
79
|
+
pause: { labelKey: 'container.carousel_pause' },
|
|
80
|
+
wrap: { labelKey: 'container.carousel_wrap' },
|
|
81
|
+
keyboard: { labelKey: 'container.carousel_keyboard' },
|
|
82
|
+
touch: { labelKey: 'container.carousel_touch' },
|
|
83
|
+
};
|
|
84
|
+
var RIDE_LABEL = { labelKey: 'container.carousel_ride' };
|
|
85
|
+
var INTERVAL_LABEL = { labelKey: 'container.carousel_interval' };
|
|
86
|
+
var RIDES = [false, 'carousel', 'true'];
|
|
87
|
+
var MIN_INTERVAL = 1000; // ms
|
|
88
|
+
|
|
89
|
+
/** What Bootstrap puts on a slide while it moves one in: never part of the page. */
|
|
90
|
+
var TRANSITION = 'carousel-item-next carousel-item-prev carousel-item-start carousel-item-end';
|
|
91
|
+
|
|
92
|
+
function validInterval(value) {
|
|
93
|
+
return typeof value === 'number' && isFinite(value) && Math.floor(value) === value && value >= MIN_INTERVAL;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
var CHECKS = {
|
|
97
|
+
ride: function(value) { return RIDES.indexOf(value) !== -1; },
|
|
98
|
+
interval: function(value) { return value === null || validInterval(value); },
|
|
99
|
+
slides: function(value) { return typeof value === 'number' && Math.floor(value) === value && value >= 1; },
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/** The values an option takes, as the warning lists them. */
|
|
103
|
+
function takes(key) {
|
|
104
|
+
if (key === 'ride') { return JSON.stringify(RIDES); }
|
|
105
|
+
if (key === 'interval') { return 'null or an integer of ' + MIN_INTERVAL + ' or more'; }
|
|
106
|
+
if (key === 'slides') { return 'an integer of 1 or more'; }
|
|
107
|
+
|
|
108
|
+
return '[true,false]';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function isValid(key, value) {
|
|
112
|
+
return CHECKS[key] ? CHECKS[key](value) : typeof value === 'boolean';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
var fieldCounter = 0;
|
|
116
|
+
|
|
117
|
+
GridEditor.containers.carousel = function(ge) {
|
|
118
|
+
|
|
119
|
+
var shown = new WeakMap(); // The slide each carousel shows, whatever its markup says
|
|
120
|
+
var sections = []; // { node, element, render } for every panel section built
|
|
121
|
+
|
|
122
|
+
function carouselOf(container) {
|
|
123
|
+
return dom.child(container, '.carousel');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function innerOf(container) {
|
|
127
|
+
return dom.child(carouselOf(container), '.carousel-inner');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** A container with no carousel or no slides' holder is left as it is. */
|
|
131
|
+
function usable(container) {
|
|
132
|
+
return !!innerOf(container);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function itemsOf(container) {
|
|
136
|
+
return dom.children(innerOf(container), '.carousel-item');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** The carousel's id, which its arrows and indicators point at. */
|
|
140
|
+
function idOf(carousel) {
|
|
141
|
+
if (!carousel.getAttribute('id')) { carousel.setAttribute('id', ge.containerId('carousel')); }
|
|
142
|
+
|
|
143
|
+
return carousel.getAttribute('id');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** A Bootstrap attribute, whether it is where Bootstrap reads it or put aside for editing. */
|
|
147
|
+
function bsAttribute(node, name) {
|
|
148
|
+
var value = node.getAttribute('data-bs-' + name);
|
|
149
|
+
|
|
150
|
+
return value === null ? node.getAttribute('data-ge-bs-' + name) : value;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function intervalOf(node) {
|
|
154
|
+
var value = parseInt(node.getAttribute('data-bs-interval'), 10);
|
|
155
|
+
|
|
156
|
+
return isFinite(value) && value > 0 ? value : null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function writeInterval(node, value) {
|
|
160
|
+
if (value === null) {
|
|
161
|
+
node.removeAttribute('data-bs-interval');
|
|
162
|
+
} else {
|
|
163
|
+
node.setAttribute('data-bs-interval', String(value));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** What a carousel's classes and attributes say it is. */
|
|
168
|
+
function read(container) {
|
|
169
|
+
var carousel = carouselOf(container);
|
|
170
|
+
var ride = bsAttribute(carousel, 'ride');
|
|
171
|
+
var off = function(name) { return carousel.getAttribute('data-bs-' + name) !== 'false'; };
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
controls: !!dom.child(carousel, '.carousel-control-prev, .carousel-control-next'),
|
|
175
|
+
indicators: !!dom.child(carousel, '.carousel-indicators'),
|
|
176
|
+
fade: dom.hasClass(carousel, 'carousel-fade'),
|
|
177
|
+
dark: carousel.getAttribute('data-bs-theme') === 'dark',
|
|
178
|
+
ride: ride === 'carousel' || ride === 'true' ? ride : false,
|
|
179
|
+
interval: intervalOf(carousel),
|
|
180
|
+
pause: off('pause'),
|
|
181
|
+
wrap: off('wrap'),
|
|
182
|
+
keyboard: off('keyboard'),
|
|
183
|
+
touch: off('touch'),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function controlButton(carousel, direction) {
|
|
188
|
+
var button = dom.element('button', {
|
|
189
|
+
'class': 'carousel-control-' + direction,
|
|
190
|
+
type: 'button',
|
|
191
|
+
'data-bs-target': '#' + idOf(carousel),
|
|
192
|
+
'data-bs-slide': direction,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
button.appendChild(dom.element('span', { 'class': 'carousel-control-' + direction + '-icon', 'aria-hidden': 'true' }));
|
|
196
|
+
button.appendChild(dom.element('span', { 'class': 'visually-hidden' },
|
|
197
|
+
direction === 'prev' ? ge.t('container.carousel_previous') : ge.t('container.carousel_next')));
|
|
198
|
+
|
|
199
|
+
return button;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function setControls(container, on) {
|
|
203
|
+
var carousel = carouselOf(container);
|
|
204
|
+
|
|
205
|
+
dom.children(carousel, '.carousel-control-prev, .carousel-control-next').forEach(function(button) { button.remove(); });
|
|
206
|
+
|
|
207
|
+
if (on) {
|
|
208
|
+
carousel.appendChild(controlButton(carousel, 'prev'));
|
|
209
|
+
carousel.appendChild(controlButton(carousel, 'next'));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function setIndicators(container, on) {
|
|
214
|
+
var carousel = carouselOf(container);
|
|
215
|
+
var bar = dom.child(carousel, '.carousel-indicators');
|
|
216
|
+
|
|
217
|
+
if (!on) {
|
|
218
|
+
if (bar) { bar.remove(); }
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (!bar) {
|
|
223
|
+
bar = carousel.insertBefore(dom.element('div', { 'class': 'carousel-indicators' }), carousel.firstChild);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
rebuildIndicators(container);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** One indicator a slide, in the slides' order, the shown one marked. */
|
|
230
|
+
function rebuildIndicators(container, list) {
|
|
231
|
+
var carousel = carouselOf(container);
|
|
232
|
+
var bar = dom.child(carousel, '.carousel-indicators');
|
|
233
|
+
if (!bar) { return; }
|
|
234
|
+
|
|
235
|
+
list = list || itemsOf(container);
|
|
236
|
+
var id = idOf(carousel);
|
|
237
|
+
var active = 0;
|
|
238
|
+
|
|
239
|
+
list.forEach(function(item, index) {
|
|
240
|
+
if (dom.hasClass(item, 'active')) { active = index; }
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
bar.textContent = '';
|
|
244
|
+
list.forEach(function(item, index) {
|
|
245
|
+
bar.appendChild(dom.element('button', {
|
|
246
|
+
type: 'button',
|
|
247
|
+
'data-bs-target': '#' + id,
|
|
248
|
+
'data-bs-slide-to': String(index),
|
|
249
|
+
'aria-label': ge.t('container.carousel_slide_label', { number: index + 1 }),
|
|
250
|
+
}));
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
syncIndicators(container, list.length ? active : -1);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function syncIndicators(container, index) {
|
|
257
|
+
var bar = dom.child(carouselOf(container), '.carousel-indicators');
|
|
258
|
+
|
|
259
|
+
dom.children(bar).forEach(function(indicator, position) {
|
|
260
|
+
dom.toggleClass(indicator, 'active', position === index);
|
|
261
|
+
|
|
262
|
+
if (position === index) {
|
|
263
|
+
indicator.setAttribute('aria-current', 'true');
|
|
264
|
+
} else {
|
|
265
|
+
indicator.removeAttribute('aria-current');
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function write(container, key, value) {
|
|
271
|
+
var carousel = carouselOf(container);
|
|
272
|
+
|
|
273
|
+
if (key === 'controls') { setControls(container, value); }
|
|
274
|
+
if (key === 'indicators') { setIndicators(container, value); }
|
|
275
|
+
if (key === 'fade') { dom.toggleClass(carousel, 'carousel-fade', value); }
|
|
276
|
+
|
|
277
|
+
if (key === 'dark') {
|
|
278
|
+
if (value) {
|
|
279
|
+
carousel.setAttribute('data-bs-theme', 'dark');
|
|
280
|
+
} else if (carousel.getAttribute('data-bs-theme') === 'dark') {
|
|
281
|
+
carousel.removeAttribute('data-bs-theme');
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (key === 'ride') {
|
|
286
|
+
carousel.removeAttribute('data-bs-ride');
|
|
287
|
+
carousel.removeAttribute('data-ge-bs-ride');
|
|
288
|
+
if (value) { carousel.setAttribute('data-bs-ride', value); }
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (key === 'interval') { writeInterval(carousel, value); }
|
|
292
|
+
|
|
293
|
+
if (BEHAVIOURS.indexOf(key) !== -1) {
|
|
294
|
+
if (value) {
|
|
295
|
+
carousel.removeAttribute('data-bs-' + key);
|
|
296
|
+
} else {
|
|
297
|
+
carousel.setAttribute('data-bs-' + key, 'false');
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
dom.dropEmptyClass(carousel);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* A value for a new carousel: the option, or the carousel setting, or the
|
|
306
|
+
* default. One the option does not take is warned about and the default used.
|
|
307
|
+
*/
|
|
308
|
+
function chosen(options, key) {
|
|
309
|
+
var setting = ge.settings.carousel || {};
|
|
310
|
+
var value = options[key] !== undefined ? options[key] : setting[key];
|
|
311
|
+
|
|
312
|
+
if (value === undefined) { return DEFAULTS[key]; }
|
|
313
|
+
if (isValid(key, value)) { return value; }
|
|
314
|
+
|
|
315
|
+
ge.warn('carousel: ' + JSON.stringify(value) + ' is not a ' + key + ', which takes ' +
|
|
316
|
+
takes(key) + ': ' + JSON.stringify(DEFAULTS[key]) + ' is used');
|
|
317
|
+
return DEFAULTS[key];
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** What stops Bootstrap's javascript acting on the carousel while it is edited. */
|
|
321
|
+
function suspend(container) {
|
|
322
|
+
var carousel = carouselOf(container);
|
|
323
|
+
|
|
324
|
+
dom.children(carousel, '.carousel-control-prev, .carousel-control-next').forEach(ge.suspendToggles);
|
|
325
|
+
dom.children(dom.child(carousel, '.carousel-indicators')).forEach(ge.suspendToggles);
|
|
326
|
+
|
|
327
|
+
// By hand: suspendToggles(carousel) would take a tabs or an accordion
|
|
328
|
+
// inside a slide off its toggles as well
|
|
329
|
+
var ride = carousel.getAttribute('data-bs-ride');
|
|
330
|
+
if (ride !== null) {
|
|
331
|
+
carousel.setAttribute('data-ge-bs-ride', ride);
|
|
332
|
+
carousel.removeAttribute('data-bs-ride');
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** A carousel Bootstrap started before the editor did stops, and is forgotten. */
|
|
337
|
+
function stopBootstrap(carousel) {
|
|
338
|
+
var library = GridEditor.bootstrap || window.bootstrap;
|
|
339
|
+
var instance = library && library.Carousel ? library.Carousel.getInstance(carousel) : null;
|
|
340
|
+
|
|
341
|
+
if (instance) {
|
|
342
|
+
instance.pause();
|
|
343
|
+
instance.dispose();
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function buildItem(container, active, interval) {
|
|
348
|
+
var item = innerOf(container).appendChild(dom.element('div', { 'class': active ? 'carousel-item active' : 'carousel-item' }));
|
|
349
|
+
|
|
350
|
+
item.appendChild(ge.defaultRegion());
|
|
351
|
+
if (interval !== null && interval !== undefined) { writeInterval(item, interval); }
|
|
352
|
+
|
|
353
|
+
return item;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** The slide the carousel shows: the one it was left on, or the markup's, or the first. */
|
|
357
|
+
function currentOf(container) {
|
|
358
|
+
var list = itemsOf(container);
|
|
359
|
+
var kept = shown.get(container);
|
|
360
|
+
|
|
361
|
+
if (kept && list.indexOf(kept) !== -1) { return kept; }
|
|
362
|
+
|
|
363
|
+
return list.filter(function(item) { return dom.hasClass(item, 'active'); })[0] || list[0] || null;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** Show one slide and only that one. */
|
|
367
|
+
function show(container, item, list) {
|
|
368
|
+
list = list || itemsOf(container);
|
|
369
|
+
|
|
370
|
+
list.forEach(function(each) { dom.toggleClass(each, 'active', each === item); });
|
|
371
|
+
|
|
372
|
+
if (item) {
|
|
373
|
+
shown.set(container, item);
|
|
374
|
+
} else {
|
|
375
|
+
shown.delete(container);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
syncIndicators(container, list.indexOf(item));
|
|
379
|
+
refreshNavigation(container);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function step(container, offset) {
|
|
383
|
+
var list = itemsOf(container);
|
|
384
|
+
if (list.length < 2) { return; }
|
|
385
|
+
|
|
386
|
+
var index = list.indexOf(currentOf(container));
|
|
387
|
+
show(container, list[(index + offset + list.length) % list.length]);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function disable(tool, state) {
|
|
391
|
+
if (!tool) { return; }
|
|
392
|
+
|
|
393
|
+
dom.toggleClass(tool, 'disabled', state);
|
|
394
|
+
|
|
395
|
+
if (state) {
|
|
396
|
+
tool.setAttribute('aria-disabled', 'true');
|
|
397
|
+
} else {
|
|
398
|
+
tool.removeAttribute('aria-disabled');
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function disabled(tool) {
|
|
403
|
+
return tool.getAttribute('aria-disabled') === 'true';
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** The container's counter and arrows, and every slide's move tools, as the slides stand. */
|
|
407
|
+
function refreshNavigation(container) {
|
|
408
|
+
var list = itemsOf(container);
|
|
409
|
+
var drawer = dom.child(container, '.ge-container-drawer');
|
|
410
|
+
var index = list.indexOf(currentOf(container));
|
|
411
|
+
|
|
412
|
+
if (drawer) {
|
|
413
|
+
var counter = dom.child(drawer, '.ge-carousel-counter');
|
|
414
|
+
|
|
415
|
+
if (counter) {
|
|
416
|
+
counter.textContent = ge.t('container.carousel_counter', {
|
|
417
|
+
current: list.length ? index + 1 : 0,
|
|
418
|
+
total: list.length,
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
disable(dom.child(drawer, '.ge-carousel-previous'), list.length < 2);
|
|
423
|
+
disable(dom.child(drawer, '.ge-carousel-next'), list.length < 2);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
list.forEach(function(item, position) {
|
|
427
|
+
var tools = dom.child(item, '.ge-pane-drawer');
|
|
428
|
+
|
|
429
|
+
disable(dom.child(tools, '.ge-carousel-move-back'), position === 0);
|
|
430
|
+
disable(dom.child(tools, '.ge-carousel-move-forward'), position === list.length - 1);
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function moveItem(container, item, offset) {
|
|
435
|
+
var inner = innerOf(container);
|
|
436
|
+
var list = itemsOf(container);
|
|
437
|
+
var index = list.indexOf(item);
|
|
438
|
+
var target = index + offset;
|
|
439
|
+
|
|
440
|
+
if (target < 0 || target >= list.length) { return; }
|
|
441
|
+
|
|
442
|
+
var from = { parent: inner, index: index };
|
|
443
|
+
|
|
444
|
+
ge.operate(function() {
|
|
445
|
+
if (!ge.emit('before-move', ge.payloadFor('carousel-item', item, {
|
|
446
|
+
parent: inner,
|
|
447
|
+
source: 'tool',
|
|
448
|
+
from: from,
|
|
449
|
+
}))) { return; }
|
|
450
|
+
|
|
451
|
+
inner.insertBefore(item, offset < 0 ? list[target] : list[target].nextSibling);
|
|
452
|
+
show(container, currentOf(container));
|
|
453
|
+
rebuildIndicators(container);
|
|
454
|
+
suspend(container);
|
|
455
|
+
|
|
456
|
+
ge.emit('after-move', ge.payloadFor('carousel-item', item, {
|
|
457
|
+
parent: inner,
|
|
458
|
+
source: 'tool',
|
|
459
|
+
from: from,
|
|
460
|
+
to: { parent: inner, index: target },
|
|
461
|
+
container: container,
|
|
462
|
+
}));
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/** What the slide to show is once `item` goes: the next one, or the previous if it was the last. */
|
|
467
|
+
function afterRemoving(container, item, removed) {
|
|
468
|
+
dom.fadeOut(item, 200, function() {
|
|
469
|
+
var list = itemsOf(container);
|
|
470
|
+
var rest = list.filter(function(each) { return each !== item; });
|
|
471
|
+
|
|
472
|
+
if (currentOf(container) === item) {
|
|
473
|
+
show(container, rest[Math.min(list.indexOf(item), rest.length - 1)] || null, rest);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
rebuildIndicators(container, rest);
|
|
477
|
+
removed();
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function createItemControls(container, item) {
|
|
482
|
+
var drawer = ge.createPaneControls(item, 'carousel-item', ge.settings.carousel_tools,
|
|
483
|
+
ge.t('confirm.delete_carousel_item'), function(removed) {
|
|
484
|
+
afterRemoving(container, item, removed);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
// A slide moves a place at a time, not by dragging
|
|
488
|
+
var handle = dom.child(drawer, '.ge-move');
|
|
489
|
+
if (handle) { handle.remove(); }
|
|
490
|
+
|
|
491
|
+
var back = ge.createTool(drawer, ge.t('tool.carousel_move_back'), 'ge-carousel-move-back', 'bi bi-arrow-left', function() {
|
|
492
|
+
if (!disabled(this)) { moveItem(container, item, -1); }
|
|
493
|
+
});
|
|
494
|
+
var forward = ge.createTool(drawer, ge.t('tool.carousel_move_forward'), 'ge-carousel-move-forward', 'bi bi-arrow-right', function() {
|
|
495
|
+
if (!disabled(this)) { moveItem(container, item, 1); }
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
drawer.insertBefore(forward, drawer.firstChild);
|
|
499
|
+
drawer.insertBefore(back, forward);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function addSlideTo(container, options) {
|
|
503
|
+
options = options || {};
|
|
504
|
+
|
|
505
|
+
if (!usable(container)) {
|
|
506
|
+
ge.warn('carousel: there is no .carousel-inner to add a slide to');
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
var interval = null;
|
|
511
|
+
|
|
512
|
+
if (options.interval !== undefined && options.interval !== null) {
|
|
513
|
+
if (validInterval(options.interval)) {
|
|
514
|
+
interval = options.interval;
|
|
515
|
+
} else {
|
|
516
|
+
ge.warn('carousel: ' + JSON.stringify(options.interval) + ' is not a interval, which takes ' +
|
|
517
|
+
takes('interval') + ': null is used');
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
var item = buildItem(container, false, interval);
|
|
522
|
+
|
|
523
|
+
show(container, item);
|
|
524
|
+
rebuildIndicators(container);
|
|
525
|
+
|
|
526
|
+
return item;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function field(labelKey, control, id) {
|
|
530
|
+
var box = dom.element('div', { 'class': 'ge-carousel-option mb-2' });
|
|
531
|
+
|
|
532
|
+
box.appendChild(dom.element('label', { 'class': 'form-label', 'for': id }, ge.t(labelKey)));
|
|
533
|
+
box.appendChild(control);
|
|
534
|
+
|
|
535
|
+
return box;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function nextId() {
|
|
539
|
+
return 'ge-carousel-option-' + (++fieldCounter);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* The interval in seconds, for a node: empty takes it off, under a
|
|
544
|
+
* second is refused and the field says so, and anything else is written
|
|
545
|
+
* in milliseconds.
|
|
546
|
+
*/
|
|
547
|
+
function intervalField(node, onWrite) {
|
|
548
|
+
var id = nextId();
|
|
549
|
+
var input = dom.element('input', {
|
|
550
|
+
'class': 'form-control form-control-sm',
|
|
551
|
+
id: id,
|
|
552
|
+
type: 'number',
|
|
553
|
+
step: '0.1',
|
|
554
|
+
min: String(MIN_INTERVAL / 1000),
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
input.addEventListener('change', function() {
|
|
558
|
+
var text = input.value.trim();
|
|
559
|
+
var seconds = parseFloat(text);
|
|
560
|
+
|
|
561
|
+
if (text === '') {
|
|
562
|
+
dom.removeClass(input, 'is-invalid');
|
|
563
|
+
writeInterval(node, null);
|
|
564
|
+
} else if (!isFinite(seconds) || Math.round(seconds * 1000) < MIN_INTERVAL) {
|
|
565
|
+
dom.addClass(input, 'is-invalid');
|
|
566
|
+
return;
|
|
567
|
+
} else {
|
|
568
|
+
dom.removeClass(input, 'is-invalid');
|
|
569
|
+
writeInterval(node, Math.round(seconds * 1000));
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (onWrite) { onWrite(); }
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
return {
|
|
576
|
+
id: id,
|
|
577
|
+
input: input,
|
|
578
|
+
render: function() {
|
|
579
|
+
var interval = intervalOf(node);
|
|
580
|
+
|
|
581
|
+
dom.removeClass(input, 'is-invalid');
|
|
582
|
+
input.value = interval === null ? '' : String(interval / 1000);
|
|
583
|
+
},
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function checkbox(labelKey, onChange) {
|
|
588
|
+
var id = nextId();
|
|
589
|
+
var box = dom.element('div', { 'class': 'form-check' });
|
|
590
|
+
var input = box.appendChild(dom.element('input', { 'class': 'form-check-input', type: 'checkbox', id: id }));
|
|
591
|
+
|
|
592
|
+
box.appendChild(dom.element('label', { 'class': 'form-check-label', 'for': id }, ge.t(labelKey)));
|
|
593
|
+
input.addEventListener('change', function() { onChange(input.checked); });
|
|
594
|
+
|
|
595
|
+
return { box: box, input: input };
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/** The Carousel section of a carousel's settings panel. */
|
|
599
|
+
function createSection(container) {
|
|
600
|
+
var body = dom.element('div', { 'class': 'ge-carousel-options' });
|
|
601
|
+
var inputs = {};
|
|
602
|
+
|
|
603
|
+
function change(key, value) {
|
|
604
|
+
write(container, key, value);
|
|
605
|
+
suspend(container);
|
|
606
|
+
render();
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
var rideId = nextId();
|
|
610
|
+
var ride = dom.element('select', { 'class': 'form-select form-select-sm', id: rideId });
|
|
611
|
+
|
|
612
|
+
[
|
|
613
|
+
{ value: 'false', labelKey: 'container.carousel_ride_no' },
|
|
614
|
+
{ value: 'carousel', labelKey: 'container.carousel_ride_load' },
|
|
615
|
+
{ value: 'true', labelKey: 'container.carousel_ride_interaction' },
|
|
616
|
+
].forEach(function(choice) {
|
|
617
|
+
ride.appendChild(dom.element('option', { value: choice.value }, ge.t(choice.labelKey)));
|
|
618
|
+
});
|
|
619
|
+
ride.addEventListener('change', function() { change('ride', ride.value === 'false' ? false : ride.value); });
|
|
620
|
+
body.appendChild(field(RIDE_LABEL.labelKey, ride, rideId));
|
|
621
|
+
|
|
622
|
+
var interval = intervalField(carouselOf(container), function() { render(); });
|
|
623
|
+
body.appendChild(field(INTERVAL_LABEL.labelKey, interval.input, interval.id));
|
|
624
|
+
|
|
625
|
+
CHECKBOXES.concat(BEHAVIOURS).forEach(function(key) {
|
|
626
|
+
var each = checkbox(LABELS[key].labelKey, function(on) { change(key, on); });
|
|
627
|
+
|
|
628
|
+
inputs[key] = each.input;
|
|
629
|
+
each.box.setAttribute('data-ge-carousel-option', key);
|
|
630
|
+
body.appendChild(each.box);
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
/** Fill the fields from the markup, and turn off what autoplay being off leaves without use. */
|
|
634
|
+
function render() {
|
|
635
|
+
var values = read(container);
|
|
636
|
+
|
|
637
|
+
ride.value = String(values.ride);
|
|
638
|
+
interval.render();
|
|
639
|
+
Object.keys(inputs).forEach(function(key) { inputs[key].checked = values[key]; });
|
|
640
|
+
|
|
641
|
+
interval.input.disabled = values.ride === false;
|
|
642
|
+
inputs.pause.disabled = values.ride === false;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
render();
|
|
646
|
+
sections.push({ node: container, element: body, render: render });
|
|
647
|
+
|
|
648
|
+
return body;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/** The Slide section of a slide's settings panel. */
|
|
652
|
+
function createItemSection(item) {
|
|
653
|
+
var body = dom.element('div', { 'class': 'ge-carousel-options' });
|
|
654
|
+
var interval = intervalField(item);
|
|
655
|
+
|
|
656
|
+
body.appendChild(field(INTERVAL_LABEL.labelKey, interval.input, interval.id));
|
|
657
|
+
interval.render();
|
|
658
|
+
sections.push({ node: item, element: body, render: interval.render });
|
|
659
|
+
|
|
660
|
+
return body;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
return {
|
|
664
|
+
labelKey: 'container.add_carousel',
|
|
665
|
+
addPaneKey: 'container.add_carousel_item',
|
|
666
|
+
paneKind: 'carousel-item',
|
|
667
|
+
paneClass: 'ge-carousel-item',
|
|
668
|
+
paneLabelKey: 'panel.kind_carousel_item',
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* `slides` slides, each a region, and the options of DEFAULTS as the
|
|
672
|
+
* call, the carousel setting or the defaults give them.
|
|
673
|
+
*/
|
|
674
|
+
create: function(options) {
|
|
675
|
+
options = options || {};
|
|
676
|
+
|
|
677
|
+
var container = dom.element('div', { 'data-ge-container': 'carousel' });
|
|
678
|
+
var carousel = container.appendChild(dom.element('div', { 'class': 'carousel slide', id: ge.containerId('carousel') }));
|
|
679
|
+
var slides = chosen(options, 'slides');
|
|
680
|
+
|
|
681
|
+
carousel.appendChild(dom.element('div', { 'class': 'carousel-inner' }));
|
|
682
|
+
|
|
683
|
+
for (var i = 0; i < slides; i++) {
|
|
684
|
+
buildItem(container, i === 0);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
Object.keys(DEFAULTS).filter(function(key) { return key !== 'slides'; }).forEach(function(key) {
|
|
688
|
+
write(container, key, chosen(options, key));
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
return container;
|
|
692
|
+
},
|
|
693
|
+
|
|
694
|
+
addPane: addSlideTo,
|
|
695
|
+
|
|
696
|
+
mark: function(container) {
|
|
697
|
+
var carousel = carouselOf(container);
|
|
698
|
+
if (!carousel || !usable(container)) { return; }
|
|
699
|
+
|
|
700
|
+
stopBootstrap(carousel);
|
|
701
|
+
suspend(container);
|
|
702
|
+
|
|
703
|
+
var current = currentOf(container);
|
|
704
|
+
|
|
705
|
+
itemsOf(container).forEach(function(item) {
|
|
706
|
+
dom.addClass(item, 'ge-carousel-item');
|
|
707
|
+
dom.removeClass(item, TRANSITION);
|
|
708
|
+
|
|
709
|
+
if (!dom.child(item, '.ge-tools-drawer')) { createItemControls(container, item); }
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
show(container, current);
|
|
713
|
+
},
|
|
714
|
+
|
|
715
|
+
unmark: function(container) {
|
|
716
|
+
ge.resumeToggles(container);
|
|
717
|
+
if (!usable(container)) { return; }
|
|
718
|
+
|
|
719
|
+
// What the page ships starts at the first slide, whichever one
|
|
720
|
+
// the canvas was showing
|
|
721
|
+
itemsOf(container).forEach(function(item, index) {
|
|
722
|
+
dom.removeClass(item, 'ge-carousel-item ' + TRANSITION);
|
|
723
|
+
dom.toggleClass(item, 'active', index === 0);
|
|
724
|
+
dom.dropEmptyClass(item);
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
syncIndicators(container, itemsOf(container).length ? 0 : -1);
|
|
728
|
+
},
|
|
729
|
+
|
|
730
|
+
// The same, on a copy of the canvas: unmark only touches markup
|
|
731
|
+
cleanMarkup: function(root) {
|
|
732
|
+
var definition = this;
|
|
733
|
+
dom.all(root, '[data-ge-container="carousel"]').forEach(function(container) { definition.unmark(container); });
|
|
734
|
+
},
|
|
735
|
+
|
|
736
|
+
// The arrows, the counter and the arrow's other side: what moves the
|
|
737
|
+
// carousel from slide to slide while it is edited
|
|
738
|
+
tools: function(drawer, container) {
|
|
739
|
+
if (!usable(container)) { return; }
|
|
740
|
+
|
|
741
|
+
ge.createTool(drawer, ge.t('tool.carousel_show_previous'), 'ge-carousel-previous', 'bi bi-chevron-left', function() {
|
|
742
|
+
if (!disabled(this)) { step(container, -1); }
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
var counter = ge.createTool(drawer, '', 'ge-carousel-counter', '', null);
|
|
746
|
+
counter.textContent = '';
|
|
747
|
+
counter.removeAttribute('title');
|
|
748
|
+
|
|
749
|
+
ge.createTool(drawer, ge.t('tool.carousel_show_next'), 'ge-carousel-next', 'bi bi-chevron-right', function() {
|
|
750
|
+
if (!disabled(this)) { step(container, 1); }
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
refreshNavigation(container);
|
|
754
|
+
},
|
|
755
|
+
|
|
756
|
+
panelSection: function(node, kind) {
|
|
757
|
+
if (kind === 'carousel' && usable(node)) {
|
|
758
|
+
return { labelKey: 'container.carousel_section', body: createSection(node) };
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
if (kind === 'carousel-item') {
|
|
762
|
+
return { labelKey: 'container.carousel_item_section', body: createItemSection(node) };
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
return null;
|
|
766
|
+
},
|
|
767
|
+
|
|
768
|
+
// After a view change or a change of classes: the sections follow
|
|
769
|
+
onRefresh: function(scope) {
|
|
770
|
+
sections = sections.filter(function(entry) { return entry.element.isConnected; });
|
|
771
|
+
sections.forEach(function(entry) {
|
|
772
|
+
if (entry.node === scope || scope.contains(entry.node)) { entry.render(); }
|
|
773
|
+
});
|
|
774
|
+
},
|
|
775
|
+
|
|
776
|
+
onDeinit: function() {
|
|
777
|
+
sections = [];
|
|
778
|
+
},
|
|
779
|
+
};
|
|
780
|
+
};
|