PluginProbe
GutSlider – All in One Slider and Carousel Blocks for Gutenberg / trunk
GutSlider – All in One Slider and Carousel Blocks for Gutenberg vtrunk
3.1.0 3.0.0 2.13.2 2.13.1 2.13.0 trunk 1.0.0 2.1.0 2.10.0 2.10.1 2.11.0 2.11.1 2.11.2 2.11.3 2.11.4 2.12.0 2.2.1 2.2.2 2.3.0 2.4.0 2.5.1 2.5.3 2.5.4 2.5.5 2.6.1 All 56 releases
slider-blocks / admin / js / admin.js

admin.js in GutSlider – All in One Slider and Carousel Blocks for Gutenberg trunk, at admin/js/admin.js

710 lines 22.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /* eslint-disable no-undef */
2 /**
3 * GutSlider admin dashboard.
4 *
5 * Staged-change editing: toggles update the UI immediately, a save bar
6 * collects the diff, and one request persists everything.
7 */
8 (function () {
9 'use strict';
10
11 var settings = window.gutslider || {};
12 var i18n = settings.i18n || {};
13 var app = document.querySelector('.gs-app');
14
15 if (!app) {
16 return;
17 }
18
19 /* ----------------------------------------------------------------
20 * Utilities
21 * -------------------------------------------------------------- */
22
23 function $(selector, scope) {
24 return (scope || document).querySelector(selector);
25 }
26
27 function $$(selector, scope) {
28 return Array.prototype.slice.call((scope || document).querySelectorAll(selector));
29 }
30
31 function sprintf(template, values) {
32 var i = 0;
33 return String(template)
34 .replace(/%(\d)\$d/g, function (match, index) {
35 return values[parseInt(index, 10) - 1];
36 })
37 .replace(/%d|%s/g, function () {
38 return values[i++];
39 });
40 }
41
42 function post(endpoint, body) {
43 return window
44 .fetch(settings.restUrl + endpoint, {
45 method: 'POST',
46 credentials: 'same-origin',
47 headers: {
48 'Content-Type': 'application/json',
49 'X-WP-Nonce': settings.nonce
50 },
51 body: JSON.stringify(body || {})
52 })
53 .then(function (response) {
54 if (!response.ok) {
55 throw new Error('HTTP ' + response.status);
56 }
57 return response.json();
58 });
59 }
60
61 /* ----------------------------------------------------------------
62 * Toasts
63 * -------------------------------------------------------------- */
64
65 var ICON_OK = '<path d="m5 13 4 4L19 7"/>';
66 var ICON_ERR = '<path d="M18 6 6 18M6 6l12 12"/>';
67
68 function toast(message, type) {
69 var host = $('[data-gs-toasts]');
70
71 if (!host) {
72 return;
73 }
74
75 var el = document.createElement('div');
76 el.className = 'gs-toast' + (type === 'error' ? ' gs-toast--error' : '');
77 el.setAttribute('role', 'status');
78 el.innerHTML =
79 '<span class="gs-toast__icon"><svg class="gs-icon" viewBox="0 0 24 24" fill="none" ' +
80 'stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">' +
81 (type === 'error' ? ICON_ERR : ICON_OK) +
82 '</svg></span><span></span>';
83 el.lastChild.textContent = message;
84
85 host.appendChild(el);
86
87 window.setTimeout(function () {
88 el.classList.add('is-out');
89 window.setTimeout(function () {
90 if (el.parentNode) {
91 el.parentNode.removeChild(el);
92 }
93 }, 240);
94 }, 2600);
95 }
96
97 /* ----------------------------------------------------------------
98 * Colour scheme
99 * -------------------------------------------------------------- */
100
101 (function theme() {
102 var KEY = 'gutslider-theme';
103 var stored = null;
104
105 try {
106 stored = window.localStorage.getItem(KEY);
107 } catch (e) {
108 stored = null;
109 }
110
111 var prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
112 var mode = stored || (prefersDark ? 'dark' : 'light');
113
114 app.setAttribute('data-gs-theme', mode);
115
116 var button = $('[data-gs-theme-toggle]');
117
118 if (!button) {
119 return;
120 }
121
122 button.addEventListener('click', function () {
123 mode = app.getAttribute('data-gs-theme') === 'dark' ? 'light' : 'dark';
124 app.setAttribute('data-gs-theme', mode);
125
126 try {
127 window.localStorage.setItem(KEY, mode);
128 } catch (e) {
129 /* Storage unavailable — the choice just will not persist. */
130 }
131 });
132 })();
133
134 /* ----------------------------------------------------------------
135 * Walkthrough — a facade, so YouTube only loads if it is asked for
136 * -------------------------------------------------------------- */
137
138 (function walkthrough() {
139 var trigger = $('[data-gs-video]');
140
141 if (!trigger) {
142 return;
143 }
144
145 trigger.addEventListener('click', function () {
146 var frame = document.createElement('iframe');
147
148 frame.src = trigger.getAttribute('data-gs-video');
149 frame.title = trigger.getAttribute('data-gs-video-title') || '';
150 frame.allow =
151 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share';
152 frame.referrerPolicy = 'strict-origin-when-cross-origin';
153 frame.allowFullscreen = true;
154
155 trigger.innerHTML = '';
156 trigger.appendChild(frame);
157 trigger.disabled = true;
158 trigger.style.cursor = 'default';
159 });
160 })();
161
162 /* ----------------------------------------------------------------
163 * Save bar — shared by the blocks and settings screens
164 * -------------------------------------------------------------- */
165
166 function createSaveBar(handler) {
167 var bar = $('[data-gs-savebar]');
168 var text = $('[data-gs-savebar-text]');
169 var saveBtn = $('[data-gs-save]');
170 var discardBtn = $('[data-gs-discard]');
171 var kbd = $('[data-gs-save-kbd]');
172 var count = 0;
173 var onDiscard = function () {};
174
175 if (!bar) {
176 return null;
177 }
178
179 if (kbd && navigator.platform.indexOf('Mac') === -1) {
180 kbd.textContent = 'Ctrl+S';
181 }
182
183 function render(next) {
184 count = next;
185
186 if (count > 0) {
187 text.textContent = sprintf(count === 1 ? i18n.changeSingle : i18n.changePlural, [count]);
188 bar.classList.add('is-visible');
189 bar.setAttribute('aria-hidden', 'false');
190 } else {
191 bar.classList.remove('is-visible');
192 bar.setAttribute('aria-hidden', 'true');
193 }
194 }
195
196 function save() {
197 if (count === 0 || saveBtn.classList.contains('is-busy')) {
198 return;
199 }
200
201 saveBtn.classList.add('is-busy');
202
203 handler()
204 .then(function () {
205 render(0);
206 toast(i18n.saved);
207 })
208 .catch(function () {
209 toast(i18n.saveFailed, 'error');
210 })
211 .then(function () {
212 saveBtn.classList.remove('is-busy');
213 });
214 }
215
216 saveBtn.addEventListener('click', save);
217
218 discardBtn.addEventListener('click', function () {
219 onDiscard();
220 render(0);
221 toast(i18n.discarded);
222 });
223
224 document.addEventListener('keydown', function (event) {
225 if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 's') {
226 if (count > 0) {
227 event.preventDefault();
228 save();
229 }
230 }
231 });
232
233 window.addEventListener('beforeunload', function (event) {
234 if (count > 0) {
235 event.preventDefault();
236 event.returnValue = i18n.leaveWarning;
237 return i18n.leaveWarning;
238 }
239
240 return undefined;
241 });
242
243 return {
244 update: render,
245 onDiscard: function (fn) {
246 onDiscard = fn;
247 }
248 };
249 }
250
251 /* ----------------------------------------------------------------
252 * Blocks screen
253 * -------------------------------------------------------------- */
254
255 (function blocksScreen() {
256 var screen = $('[data-gs-blocks]');
257
258 if (!screen) {
259 return;
260 }
261
262 var cards = $$('[data-gs-block]', screen);
263 var searchInput = $('[data-gs-search]', screen);
264 var counter = $('[data-gs-counter]', screen);
265 var summary = $('[data-gs-summary]', screen);
266 var metricValue = $('[data-gs-metric-enabled]', screen);
267 var metricNote = $('[data-gs-metric-note]', screen);
268 var empty = $('[data-gs-empty]', screen);
269 var grid = $('[data-gs-grid]', screen);
270 var filter = 'all';
271 var query = '';
272
273 var freeTotal = cards.filter(function (card) {
274 return card.getAttribute('data-gs-tier') === 'free';
275 }).length;
276 var proTotal = cards.length - freeTotal;
277
278 var state = {};
279 var initial = {};
280
281 cards.forEach(function (card) {
282 var name = card.getAttribute('data-gs-block');
283 var on = card.getAttribute('data-gs-initial') === '1';
284 initial[name] = on;
285 state[name] = on;
286 });
287
288 var bar = createSaveBar(function () {
289 var changed = diff();
290
291 return post('dashboard/blocks', { blocks: changed }).then(function () {
292 Object.keys(changed).forEach(function (name) {
293 initial[name] = changed[name];
294 });
295 markDirty();
296 });
297 });
298
299 function diff() {
300 var changed = {};
301
302 Object.keys(state).forEach(function (name) {
303 if (state[name] !== initial[name]) {
304 changed[name] = state[name];
305 }
306 });
307
308 return changed;
309 }
310
311 function markDirty() {
312 var changed = diff();
313
314 cards.forEach(function (card) {
315 var name = card.getAttribute('data-gs-block');
316 card.classList.toggle('is-dirty', Object.prototype.hasOwnProperty.call(changed, name));
317 });
318
319 if (bar) {
320 bar.update(Object.keys(changed).length);
321 }
322 }
323
324 function updateCount() {
325 var enabled = 0;
326
327 cards.forEach(function (card) {
328 if (state[card.getAttribute('data-gs-block')]) {
329 enabled += 1;
330 }
331 });
332
333 if (counter) {
334 counter.textContent = sprintf(i18n.enabledCount, [enabled, cards.length]);
335 }
336
337 if (summary) {
338 summary.textContent = sprintf(i18n.blockSummary, [
339 freeTotal,
340 proTotal,
341 enabled,
342 cards.length
343 ]);
344 }
345
346 if (metricValue) {
347 metricValue.textContent = String(enabled);
348 }
349
350 if (metricNote) {
351 var off = cards.length - enabled;
352 metricNote.textContent = off > 0 ? sprintf(i18n.someDisabled, [off]) : i18n.allEnabled;
353 }
354 }
355
356 function applyFilters() {
357 var visible = 0;
358
359 cards.forEach(function (card) {
360 var name = card.getAttribute('data-gs-block');
361 var tier = card.getAttribute('data-gs-tier');
362 var haystack = card.getAttribute('data-gs-name') || '';
363 var on = !!state[name];
364
365 var matchesQuery = query === '' || haystack.indexOf(query) !== -1;
366 var matchesFilter =
367 filter === 'all' ||
368 (filter === 'free' && tier === 'free') ||
369 (filter === 'pro' && tier === 'pro') ||
370 (filter === 'enabled' && on) ||
371 (filter === 'disabled' && !on);
372
373 var show = matchesQuery && matchesFilter;
374 card.classList.toggle('is-hidden', !show);
375
376 if (show) {
377 visible += 1;
378 }
379 });
380
381 if (empty) {
382 empty.hidden = visible !== 0;
383 }
384
385 if (grid) {
386 grid.hidden = visible === 0;
387 }
388 }
389
390 function setToggle(card, on) {
391 var name = card.getAttribute('data-gs-block');
392 var input = $('[data-gs-toggle]', card);
393
394 state[name] = on;
395
396 if (input) {
397 input.checked = on;
398 }
399
400 card.classList.toggle('is-on', on);
401 card.classList.toggle('is-off', !on);
402 }
403
404 // Toggles.
405 $$('[data-gs-toggle]', screen).forEach(function (input) {
406 input.addEventListener('change', function () {
407 var card = input.closest('[data-gs-block]');
408 setToggle(card, input.checked);
409 markDirty();
410 updateCount();
411 applyFilters();
412 });
413 });
414
415 // Search.
416 if (searchInput) {
417 searchInput.addEventListener('input', function () {
418 query = searchInput.value.trim().toLowerCase();
419 applyFilters();
420 });
421
422 document.addEventListener('keydown', function (event) {
423 var tag = document.activeElement ? document.activeElement.tagName : '';
424
425 if (event.key === '/' && tag !== 'INPUT' && tag !== 'TEXTAREA') {
426 event.preventDefault();
427 searchInput.focus();
428 }
429
430 if (event.key === 'Escape' && document.activeElement === searchInput) {
431 searchInput.value = '';
432 query = '';
433 applyFilters();
434 }
435 });
436 }
437
438 // Filter segments.
439 $$('[data-gs-filter]', screen).forEach(function (button) {
440 button.addEventListener('click', function () {
441 filter = button.getAttribute('data-gs-filter');
442
443 $$('[data-gs-filter]', screen).forEach(function (other) {
444 var active = other === button;
445 other.classList.toggle('is-active', active);
446 other.setAttribute('aria-selected', active ? 'true' : 'false');
447 });
448
449 applyFilters();
450 });
451 });
452
453 // Clear filters from the empty state.
454 var clearBtn = $('[data-gs-clear-filters]', screen);
455
456 if (clearBtn) {
457 clearBtn.addEventListener('click', function () {
458 query = '';
459 filter = 'all';
460
461 if (searchInput) {
462 searchInput.value = '';
463 }
464
465 $$('[data-gs-filter]', screen).forEach(function (other) {
466 var active = other.getAttribute('data-gs-filter') === 'all';
467 other.classList.toggle('is-active', active);
468 other.setAttribute('aria-selected', active ? 'true' : 'false');
469 });
470
471 applyFilters();
472 });
473 }
474
475 // Bulk actions apply to whatever is currently visible.
476 $$('[data-gs-bulk]', screen).forEach(function (button) {
477 button.addEventListener('click', function () {
478 var on = button.getAttribute('data-gs-bulk') === 'on';
479
480 cards.forEach(function (card) {
481 if (card.classList.contains('is-hidden') || card.getAttribute('data-gs-locked') === '1') {
482 return;
483 }
484
485 setToggle(card, on);
486 });
487
488 markDirty();
489 updateCount();
490 applyFilters();
491 });
492 });
493
494 if (bar) {
495 bar.onDiscard(function () {
496 cards.forEach(function (card) {
497 setToggle(card, initial[card.getAttribute('data-gs-block')]);
498 card.classList.remove('is-dirty');
499 });
500
501 updateCount();
502 applyFilters();
503 });
504 }
505
506 updateCount();
507 applyFilters();
508 })();
509
510 /* ----------------------------------------------------------------
511 * Settings screen
512 * -------------------------------------------------------------- */
513
514 (function settingsScreen() {
515 var screen = $('[data-gs-settings]');
516
517 if (!screen) {
518 return;
519 }
520
521 var inputs = $$('[data-gs-setting]', screen);
522 var initial = {};
523 var state = {};
524
525 inputs.forEach(function (input) {
526 var key = input.getAttribute('data-gs-setting');
527
528 if (input.type === 'radio') {
529 initial[key] = input.getAttribute('data-gs-initial');
530
531 if (input.checked) {
532 state[key] = input.value;
533 }
534 } else {
535 initial[key] = input.getAttribute('data-gs-initial') === '1';
536 state[key] = input.checked;
537 }
538 });
539
540 var bar = createSaveBar(function () {
541 var changed = diff();
542
543 return post('dashboard/settings', { settings: changed }).then(function () {
544 Object.keys(changed).forEach(function (key) {
545 initial[key] = changed[key];
546 });
547 });
548 });
549
550 function diff() {
551 var changed = {};
552
553 Object.keys(state).forEach(function (key) {
554 if (state[key] !== initial[key]) {
555 changed[key] = state[key];
556 }
557 });
558
559 return changed;
560 }
561
562 function markDirty() {
563 if (bar) {
564 bar.update(Object.keys(diff()).length);
565 }
566 }
567
568 function syncRadioCards() {
569 $$('.gs-radio-card', screen).forEach(function (card) {
570 var input = $('input', card);
571 card.classList.toggle('is-active', !!input && input.checked);
572 });
573 }
574
575 inputs.forEach(function (input) {
576 input.addEventListener('change', function () {
577 var key = input.getAttribute('data-gs-setting');
578
579 if (input.type === 'radio') {
580 if (!input.checked) {
581 return;
582 }
583 state[key] = input.value;
584 syncRadioCards();
585 } else {
586 state[key] = input.checked;
587 }
588
589 markDirty();
590 });
591 });
592
593 if (bar) {
594 bar.onDiscard(function () {
595 inputs.forEach(function (input) {
596 var key = input.getAttribute('data-gs-setting');
597
598 if (input.type === 'radio') {
599 input.checked = input.value === initial[key];
600 state[key] = initial[key];
601 } else {
602 input.checked = initial[key];
603 state[key] = initial[key];
604 }
605 });
606
607 syncRadioCards();
608 });
609 }
610
611 // Clear generated stylesheets.
612 var clearCache = $('[data-gs-clear-cache]', screen);
613
614 if (clearCache) {
615 clearCache.addEventListener('click', function () {
616 if (clearCache.classList.contains('is-busy')) {
617 return;
618 }
619
620 clearCache.classList.add('is-busy');
621
622 post('dashboard/clear-cache')
623 .then(function () {
624 toast(i18n.cacheCleared);
625 })
626 .catch(function () {
627 toast(i18n.saveFailed, 'error');
628 })
629 .then(function () {
630 clearCache.classList.remove('is-busy');
631 });
632 });
633 }
634
635 /*
636 * Section navigation.
637 *
638 * Deliberately not an IntersectionObserver: the trailing sections are
639 * often too short to ever cross a fixed detection band, so they could
640 * never become active. Instead, the current section is the last one
641 * whose top has passed the offset line, and the bottom of the page
642 * always owns the final section.
643 */
644 var sections = $$('[data-gs-sidenav]', screen)
645 .map(function (link) {
646 var panel = $(link.getAttribute('href'), screen);
647 return panel ? { link: link, panel: panel } : null;
648 })
649 .filter(Boolean);
650
651 if (sections.length) {
652 // Distance from the viewport top, clearing the sticky bar.
653 var SPY_OFFSET = 140;
654 var spyFrame = null;
655 var spyLockedUntil = 0;
656
657 var setActiveSection = function (link) {
658 sections.forEach(function (section) {
659 section.link.classList.toggle('is-active', section.link === link);
660 });
661 };
662
663 var syncSpy = function () {
664 spyFrame = null;
665
666 // A click owns the highlight until its smooth scroll settles.
667 if (Date.now() < spyLockedUntil) {
668 return;
669 }
670
671 var scrollable = document.documentElement.scrollHeight - window.innerHeight;
672 var current = sections[0];
673
674 if (scrollable > 4 && window.pageYOffset >= scrollable - 2) {
675 current = sections[sections.length - 1];
676 } else {
677 sections.forEach(function (section) {
678 if (section.panel.getBoundingClientRect().top <= SPY_OFFSET) {
679 current = section;
680 }
681 });
682 }
683
684 setActiveSection(current.link);
685 };
686
687 var queueSpy = function () {
688 if (spyFrame === null) {
689 spyFrame = window.requestAnimationFrame(syncSpy);
690 }
691 };
692
693 sections.forEach(function (section) {
694 section.link.addEventListener('click', function (event) {
695 event.preventDefault();
696 setActiveSection(section.link);
697 spyLockedUntil = Date.now() + 700;
698 section.panel.scrollIntoView({ behavior: 'smooth', block: 'start' });
699 });
700 });
701
702 window.addEventListener('scroll', queueSpy, { passive: true });
703 window.addEventListener('resize', queueSpy);
704 syncSpy();
705 }
706
707 syncRadioCards();
708 })();
709 })();
710