PluginProbe
Slider Block by Sliderberg – WordPress Slider & Carousel Plugin for Gutenberg / trunk
Slider Block by Sliderberg – WordPress Slider & Carousel Plugin for Gutenberg vtrunk
1.2.3 1.2.2 1.2.0 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.7 1.0.8 1.0.9
sliderberg / src / view.ts

view.ts in Slider Block by Sliderberg – WordPress Slider & Carousel Plugin for Gutenberg trunk, at src/view.ts

344 lines 9.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * SliderBerg Frontend Entry Point
3 *
4 * Initializes Swiper instances from data-swiper-data attributes
5 * rendered by the PHP slider-renderer.
6 */
7
8 import Swiper from "swiper";
9 import {
10 Navigation,
11 Pagination,
12 Autoplay,
13 EffectFade,
14 EffectCreative,
15 Keyboard,
16 A11y,
17 } from "swiper/modules";
18 import type { SwiperModule, SwiperOptions } from "swiper/types";
19
20 import "swiper/css";
21 import "swiper/css/navigation";
22 import "swiper/css/pagination";
23 import "swiper/css/effect-fade";
24 import "swiper/css/effect-creative";
25
26 // Extend Window interface for public API and WP hooks
27 declare const jQuery: any;
28 declare global {
29 interface Window {
30 SliderBerg: {
31 init: () => void;
32 destroyAll: () => void;
33 getInstance: (element: Element) => Swiper | undefined;
34 getInstances: () => Map<Element, Swiper>;
35 next: (element: Element) => void;
36 prev: (element: Element) => void;
37 goTo: (element: Element, index: number) => void;
38 pause: (element: Element) => void;
39 play: (element: Element) => void;
40 };
41 wp?: {
42 hooks?: {
43 applyFilters: (filter: string, value: any, ...args: any[]) => any;
44 };
45 };
46 }
47 }
48
49 /** All active Swiper instances keyed by their container element */
50 const instances = new Map<Element, Swiper>();
51
52 /**
53 * AbortControllers for slide-link DOM listeners, keyed by container.
54 * Aborting removes all associated click/keydown handlers in one call,
55 * preventing duplicate listeners when the slider is reinitialized after
56 * a bfcache restore (back/forward navigation).
57 */
58 const slideControllers = new Map<Element, AbortController>();
59
60 const defaultModules: SwiperModule[] = [
61 Navigation,
62 Pagination,
63 Autoplay,
64 EffectFade,
65 EffectCreative,
66 Keyboard,
67 A11y,
68 ];
69
70 function applySliderbergFilter<T>(filter: string, value: T, ...args: any[]): T {
71 if (!window.wp?.hooks?.applyFilters) {
72 return value;
73 }
74
75 return window.wp.hooks.applyFilters(filter, value, ...args) as T;
76 }
77
78 function getSwiperModules(
79 container: HTMLElement,
80 config: SwiperOptions,
81 ): SwiperModule[] {
82 const modules = applySliderbergFilter(
83 "sliderberg.swiperModules",
84 defaultModules.slice(),
85 container,
86 config,
87 );
88
89 return Array.isArray(modules) ? modules : defaultModules;
90 }
91
92 /**
93 * Initialize all SliderBerg sliders on the page.
94 */
95 function initializeSliders(): void {
96 const containers =
97 document.querySelectorAll<HTMLElement>(".sliderberg-swiper");
98
99 if (!containers.length) {
100 return;
101 }
102
103 // Allow pro features to override initialization entirely
104 if (window.wp?.hooks?.applyFilters) {
105 const customInit = window.wp.hooks.applyFilters(
106 "sliderberg.frontendInit",
107 null,
108 containers,
109 );
110 if (customInit && typeof customInit === "function") {
111 customInit(containers);
112 return;
113 }
114 }
115
116 containers.forEach((container) => {
117 // Skip already-initialized containers
118 if (instances.has(container)) {
119 return;
120 }
121
122 // Allow pro features to veto individual slider init
123 if (window.wp?.hooks?.applyFilters) {
124 const shouldInit = window.wp.hooks.applyFilters(
125 "sliderberg.beforeSliderInit",
126 true,
127 container,
128 );
129 if (!shouldInit) {
130 return;
131 }
132 }
133
134 // Parse Swiper config from data attribute
135 const dataAttr = container.getAttribute("data-swiper-data");
136 if (!dataAttr) {
137 return;
138 }
139
140 let config: SwiperOptions;
141 try {
142 config = JSON.parse(dataAttr) as SwiperOptions;
143 } catch {
144 return;
145 }
146
147 // Scope navigation/pagination selectors to this container instance.
148 // Both nav buttons and pagination live OUTSIDE the swiper container as
149 // siblings inside .sliderberg-swiper-wrap, so query from the parent.
150 if (config.navigation) {
151 const prevEl =
152 container.parentElement?.querySelector(".swiper-button-prev") ??
153 container.querySelector(".swiper-button-prev");
154 const nextEl =
155 container.parentElement?.querySelector(".swiper-button-next") ??
156 container.querySelector(".swiper-button-next");
157 if (prevEl && nextEl) {
158 config.navigation = { prevEl, nextEl };
159 }
160 }
161 if (config.pagination) {
162 // Pagination is a sibling of the swiper container inside
163 // .sliderberg-swiper-wrap, so query from the parent element.
164 const paginationEl =
165 container.parentElement?.querySelector(".swiper-pagination") ??
166 container.querySelector(".swiper-pagination");
167 if (paginationEl) {
168 config.pagination = {
169 ...config.pagination,
170 el: paginationEl,
171 };
172 }
173 }
174
175 const swiperConfig = applySliderbergFilter(
176 "sliderberg.swiperConfig",
177 config,
178 container,
179 );
180 const modules = getSwiperModules(container, swiperConfig);
181
182 // Create Swiper instance
183 const swiper = new Swiper(container, {
184 modules,
185 ...swiperConfig,
186 });
187
188 instances.set(container, swiper);
189 initSlideLinkHandlers(container, swiper);
190 });
191 }
192
193 /**
194 * Wire up click-to-navigate behaviour for slides that have data-slide-link.
195 * Uses a JS handler rather than wrapping in <a> so nested interactive
196 * elements (buttons, links) continue to work normally.
197 *
198 * Listeners are tied to an AbortController stored in slideControllers so they
199 * can be cancelled cleanly on reinit without leaving orphaned handlers on the
200 * DOM nodes.
201 */
202 function initSlideLinkHandlers(container: HTMLElement, swiper: Swiper): void {
203 const linkedSlides = Array.from(
204 container.querySelectorAll<HTMLElement>(".sliderberg-slide[data-slide-link]"),
205 );
206 if (!linkedSlides.length) return;
207
208 const ac = new AbortController();
209 const { signal } = ac;
210 slideControllers.set(container, ac);
211
212 // Prevent navigation when the user drags to the next slide
213 let isDragging = false;
214 swiper.on("touchStart", () => {
215 isDragging = false;
216 });
217 swiper.on("touchMove", () => {
218 isDragging = true;
219 });
220
221 // Only the active slide should be reachable via Tab
222 const updateTabindex = () => {
223 linkedSlides.forEach((slide) => {
224 slide.tabIndex = slide.classList.contains("swiper-slide-active") ? 0 : -1;
225 });
226 };
227 updateTabindex();
228 swiper.on("slideChange", updateTabindex);
229
230 linkedSlides.forEach((slide) => {
231 const url = slide.dataset.slideLink!;
232 const linkTarget = slide.dataset.slideLinkTarget ?? "_self";
233
234 const navigate = () => {
235 if (linkTarget === "_blank") {
236 window.open(url, "_blank", "noopener,noreferrer");
237 } else {
238 window.location.href = url;
239 }
240 };
241
242 slide.addEventListener(
243 "click",
244 (e) => {
245 if (isDragging) {
246 isDragging = false;
247 return;
248 }
249 // Let clicks on interactive elements through unchanged
250 const clickedEl = e.target as Element;
251 if (
252 clickedEl.closest(
253 "a, button, input, select, textarea, [role='button'], [role='link']",
254 )
255 ) {
256 return;
257 }
258 navigate();
259 },
260 { signal },
261 );
262
263 slide.addEventListener(
264 "keydown",
265 (e) => {
266 if (e.key === "Enter" || e.key === " ") {
267 e.preventDefault();
268 navigate();
269 }
270 },
271 { signal },
272 );
273 });
274 }
275
276 /**
277 * Destroy all Swiper instances and clear the maps.
278 */
279 function destroyAll(): void {
280 slideControllers.forEach((ac) => ac.abort());
281 slideControllers.clear();
282 instances.forEach((swiper) => {
283 swiper.destroy(true, true);
284 });
285 instances.clear();
286 }
287
288 // --- Initialization ---
289
290 if (document.readyState === "loading") {
291 document.addEventListener("DOMContentLoaded", initializeSliders);
292 } else {
293 initializeSliders();
294 }
295
296 // On bfcache restore (back/forward navigation on desktop), the entire JS heap
297 // is preserved — instances Map, Swiper objects, and DOM event listeners all
298 // survive the freeze intact. DOMContentLoaded does NOT re-fire, but we do not
299 // need to destroy/reinitialize. We only need to restart autoplay (setInterval
300 // timers are paused during the freeze) and recalculate layout in case the
301 // viewport changed. Destroying and reinitializing would strip inline styles
302 // (background-image etc.) and cause a visible flash.
303 window.addEventListener("pageshow", (event) => {
304 if (event.persisted) {
305 instances.forEach((swiper) => {
306 swiper.update();
307 if (swiper.params.autoplay) {
308 swiper.autoplay.start();
309 }
310 });
311 }
312 });
313
314 // Re-initialize for AJAX / dynamic content
315 document.addEventListener("DOMContentLoaded", () => {
316 // jQuery AJAX support for legacy themes
317 if (typeof jQuery !== "undefined") {
318 jQuery(document).on("ajaxComplete", (_event: any, xhr: any) => {
319 if (xhr?.responseText?.includes("sliderberg-swiper")) {
320 setTimeout(initializeSliders, 150);
321 }
322 });
323 }
324 // Custom theme event
325 document.addEventListener("content-updated", () =>
326 setTimeout(initializeSliders, 150),
327 );
328 });
329
330 // --- Public API ---
331
332 window.SliderBerg = {
333 init: initializeSliders,
334 destroyAll,
335 getInstance: (element: Element) => instances.get(element),
336 getInstances: () => instances,
337 next: (element: Element) => instances.get(element)?.slideNext(),
338 prev: (element: Element) => instances.get(element)?.slidePrev(),
339 goTo: (element: Element, index: number) =>
340 instances.get(element)?.slideTo(index),
341 pause: (element: Element) => instances.get(element)?.autoplay?.stop(),
342 play: (element: Element) => instances.get(element)?.autoplay?.start(),
343 };
344