PluginProbe
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder / 51.1.83
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder v51.1.83
51.1.84 51.1.85 51.1.83 51.1.82 51.1.81 51.1.79 51.1.78 51.1.77 51.1.76 51.1.74 51.1.75 51.1.65 51.1.64 51.1.63 trunk 51.1.14 51.1.2 51.1.35 51.1.36 51.1.37 51.1.38 51.1.39 51.1.44 51.1.45 51.1.46 All 39 releases
king-addons / includes / widgets / Reveal_Swipe_Cards / script.js

script.js in King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder 51.1.83, at includes/widgets/Reveal_Swipe_Cards/script.js

432 lines 14.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 "use strict";
2 /**
3 * Reveal Swipe Cards Widget JavaScript.
4 *
5 * Handles reveal animations triggered by hover or scroll using IntersectionObserver.
6 * Pro features (touch swipe, sequence mode) are handled in this file when Pro is enabled.
7 *
8 * @package King_Addons
9 */
10 (($) => {
11 /**
12 * Clamp a value between min and max.
13 *
14 * @param {number} value - Value to clamp.
15 * @param {number} min - Minimum value.
16 * @param {number} max - Maximum value.
17 * @returns {number} Clamped value.
18 */
19 const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
20
21 /**
22 * Parse JSON settings from data attribute.
23 *
24 * @param {HTMLElement} root - Root element.
25 * @returns {Object} Parsed settings.
26 */
27 const parseSettings = (root) => {
28 if (!root || !root.dataset) {
29 return {};
30 }
31
32 try {
33 return JSON.parse(root.dataset.settings || "{}");
34 } catch (e) {
35 return {};
36 }
37 };
38
39 /**
40 * Check if user prefers reduced motion.
41 *
42 * @returns {boolean} True if reduced motion preferred.
43 */
44 const prefersReducedMotion = () =>
45 window.matchMedia &&
46 window.matchMedia("(prefers-reduced-motion: reduce)").matches;
47
48 /**
49 * Check if device supports touch.
50 *
51 * @returns {boolean} True if touch device.
52 */
53 const isTouchDevice = () =>
54 "ontouchstart" in window || navigator.maxTouchPoints > 0;
55
56 /**
57 * Initialize widget instance.
58 *
59 * @param {jQuery} $scope - Widget scope.
60 */
61 const initWidget = ($scope) => {
62 const root = $scope.find(".kng-rsc").first()[0];
63 if (!root || root.dataset.kngRscInit === "yes") {
64 return;
65 }
66
67 root.dataset.kngRscInit = "yes";
68 const options = parseSettings(root);
69
70 const cards = Array.from(root.querySelectorAll(".kng-rsc-card"));
71 if (!cards.length) {
72 return;
73 }
74
75 const isEditor =
76 window.elementorFrontend &&
77 elementorFrontend.isEditMode &&
78 elementorFrontend.isEditMode();
79
80 const reducedMotion = prefersReducedMotion();
81 const touchEnabled = isTouchDevice();
82
83 const trigger = options.trigger || "hover";
84 const direction = options.direction || "left";
85 const duration = options.duration || 500;
86 const resetOnLeave = options.resetOnLeave !== false;
87 const scrollThreshold = options.scrollThreshold || 0.3;
88 const scrollOnce = options.scrollOnce !== false;
89 const scrollReset = options.scrollReset === true;
90
91 // Pro settings
92 const isPro = root.classList.contains("kng-rsc--pro");
93 const maskShape = options.maskShape || "rect";
94 const blurEdge = options.blurEdge || 0;
95 const touchSwipe = isPro && options.touchSwipe === true && touchEnabled;
96 const sequenceMode = isPro ? options.sequenceMode || "off" : "off";
97 const staggerDelay = options.staggerDelay || 100;
98
99 // State tracking
100 const cardStates = new Map();
101 let activeCardIndex = -1;
102 let sequenceRevealed = 0;
103
104 /**
105 * Set CSS custom properties for mask shape (Pro).
106 *
107 * @param {HTMLElement} card - Card element.
108 */
109 const setMaskProperties = (card) => {
110 if (!isPro) return;
111
112 card.style.setProperty("--kng-rsc-mask-shape", maskShape);
113
114 if (blurEdge > 0) {
115 card.style.setProperty("--kng-rsc-blur-edge", `${blurEdge}px`);
116 card.classList.add("kng-rsc-card--blur-edge");
117 }
118 };
119
120 /**
121 * Reveal a card.
122 *
123 * @param {HTMLElement} card - Card element.
124 * @param {boolean} instant - Skip animation.
125 */
126 const revealCard = (card, instant = false) => {
127 if (cardStates.get(card) === "revealed") return;
128
129 cardStates.set(card, "revealed");
130 card.classList.add("is-revealed");
131
132 if (instant || reducedMotion) {
133 card.classList.add("is-instant");
134 } else {
135 card.classList.remove("is-instant");
136 }
137 };
138
139 /**
140 * Hide a card (reset reveal).
141 *
142 * @param {HTMLElement} card - Card element.
143 * @param {boolean} instant - Skip animation.
144 */
145 const hideCard = (card, instant = false) => {
146 if (cardStates.get(card) === "hidden") return;
147
148 cardStates.set(card, "hidden");
149 card.classList.remove("is-revealed");
150
151 if (instant || reducedMotion) {
152 card.classList.add("is-instant");
153 } else {
154 card.classList.remove("is-instant");
155 }
156 };
157
158 /**
159 * Toggle card reveal state.
160 *
161 * @param {HTMLElement} card - Card element.
162 */
163 const toggleCard = (card) => {
164 if (cardStates.get(card) === "revealed") {
165 hideCard(card);
166 } else {
167 revealCard(card);
168 }
169 };
170
171 /**
172 * Handle hover enter.
173 *
174 * @param {MouseEvent} event - Mouse event.
175 */
176 const handleMouseEnter = (event) => {
177 const card = event.currentTarget;
178
179 if (sequenceMode === "active-one") {
180 // Close previous active card
181 if (activeCardIndex >= 0 && cards[activeCardIndex] !== card) {
182 hideCard(cards[activeCardIndex]);
183 }
184 activeCardIndex = cards.indexOf(card);
185 }
186
187 revealCard(card);
188 };
189
190 /**
191 * Handle hover leave.
192 *
193 * @param {MouseEvent} event - Mouse event.
194 */
195 const handleMouseLeave = (event) => {
196 if (!resetOnLeave) return;
197 if (sequenceMode === "active-one") return;
198
199 const card = event.currentTarget;
200 hideCard(card);
201 };
202
203 /**
204 * Handle focus for keyboard accessibility.
205 *
206 * @param {FocusEvent} event - Focus event.
207 */
208 const handleFocus = (event) => {
209 const card = event.currentTarget;
210 revealCard(card);
211 };
212
213 /**
214 * Handle blur for keyboard accessibility.
215 *
216 * @param {FocusEvent} event - Blur event.
217 */
218 const handleBlur = (event) => {
219 if (!resetOnLeave) return;
220 if (sequenceMode === "active-one") return;
221
222 const card = event.currentTarget;
223 hideCard(card);
224 };
225
226 /**
227 * Setup hover triggers.
228 */
229 const setupHoverTriggers = () => {
230 cards.forEach((card) => {
231 card.addEventListener("mouseenter", handleMouseEnter);
232 card.addEventListener("mouseleave", handleMouseLeave);
233 card.addEventListener("focusin", handleFocus);
234 card.addEventListener("focusout", handleBlur);
235 });
236 };
237
238 /**
239 * Setup scroll triggers using IntersectionObserver.
240 */
241 const setupScrollTriggers = () => {
242 if (!("IntersectionObserver" in window)) {
243 // Fallback: reveal all cards immediately
244 cards.forEach((card) => revealCard(card, true));
245 return;
246 }
247
248 const observerOptions = {
249 root: null,
250 rootMargin: "0px",
251 threshold: scrollThreshold,
252 };
253
254 const handleIntersect = (entries, observer) => {
255 entries.forEach((entry) => {
256 const card = entry.target;
257 const cardIndex = cards.indexOf(card);
258
259 if (entry.isIntersecting) {
260 // Sequence mode: stagger reveal
261 if (sequenceMode === "stagger" && !isEditor) {
262 const delay = cardIndex * staggerDelay;
263 setTimeout(() => {
264 revealCard(card);
265 sequenceRevealed++;
266 }, delay);
267 } else {
268 revealCard(card);
269 }
270
271 if (scrollOnce) {
272 observer.unobserve(card);
273 }
274 } else if (!scrollOnce && scrollReset) {
275 hideCard(card);
276 }
277 });
278 };
279
280 const observer = new IntersectionObserver(handleIntersect, observerOptions);
281 cards.forEach((card) => observer.observe(card));
282 };
283
284 /**
285 * Setup touch swipe (Pro).
286 */
287 const setupTouchSwipe = () => {
288 if (!touchSwipe) return;
289
290 cards.forEach((card) => {
291 let startX = 0;
292 let startY = 0;
293 let currentX = 0;
294 let currentY = 0;
295 let isDragging = false;
296 const threshold = 0.4; // 40% swipe to trigger
297 const velocityThreshold = 0.5;
298 let startTime = 0;
299
300 const isHorizontal = direction === "left" || direction === "right";
301 const overlay = card.querySelector(".kng-rsc-card__overlay");
302
303 const handleTouchStart = (e) => {
304 const touch = e.touches[0];
305 startX = touch.clientX;
306 startY = touch.clientY;
307 startTime = Date.now();
308 isDragging = true;
309 card.classList.add("is-dragging");
310 };
311
312 const handleTouchMove = (e) => {
313 if (!isDragging) return;
314
315 const touch = e.touches[0];
316 currentX = touch.clientX - startX;
317 currentY = touch.clientY - startY;
318
319 const delta = isHorizontal ? currentX : currentY;
320 const size = isHorizontal ? card.offsetWidth : card.offsetHeight;
321 const progress = clamp(Math.abs(delta) / size, 0, 1);
322
323 // Determine if swipe direction matches reveal direction
324 let isCorrectDirection = false;
325 if (direction === "left" && currentX > 0) isCorrectDirection = true;
326 if (direction === "right" && currentX < 0) isCorrectDirection = true;
327 if (direction === "top" && currentY > 0) isCorrectDirection = true;
328 if (direction === "bottom" && currentY < 0) isCorrectDirection = true;
329
330 // Prevent page scroll if swiping in correct direction
331 if (isCorrectDirection && Math.abs(delta) > 10) {
332 e.preventDefault();
333 }
334
335 // Update overlay position for visual feedback
336 if (overlay && isCorrectDirection) {
337 const transformValue = isHorizontal
338 ? `translateX(${progress * 100}%)`
339 : `translateY(${progress * 100}%)`;
340 overlay.style.transform = transformValue;
341 }
342 };
343
344 const handleTouchEnd = () => {
345 if (!isDragging) return;
346
347 isDragging = false;
348 card.classList.remove("is-dragging");
349
350 const delta = isHorizontal ? currentX : currentY;
351 const size = isHorizontal ? card.offsetWidth : card.offsetHeight;
352 const progress = Math.abs(delta) / size;
353 const elapsed = Date.now() - startTime;
354 const velocity = Math.abs(delta) / elapsed;
355
356 // Snap decision
357 if (progress > threshold || velocity > velocityThreshold) {
358 revealCard(card);
359 } else {
360 hideCard(card);
361 }
362
363 // Reset overlay transform
364 if (overlay) {
365 overlay.style.transform = "";
366 }
367
368 startX = 0;
369 startY = 0;
370 currentX = 0;
371 currentY = 0;
372 };
373
374 card.addEventListener("touchstart", handleTouchStart, { passive: true });
375 card.addEventListener("touchmove", handleTouchMove, { passive: false });
376 card.addEventListener("touchend", handleTouchEnd, { passive: true });
377 card.addEventListener("touchcancel", handleTouchEnd, { passive: true });
378 });
379 };
380
381 // Initialize cards
382 cards.forEach((card, index) => {
383 cardStates.set(card, "hidden");
384 setMaskProperties(card);
385
386 // Set direction-specific class
387 card.classList.add(`kng-rsc-card--direction-${direction}`);
388
389 // Pro: mask shape class
390 if (isPro && maskShape !== "rect") {
391 card.classList.add(`kng-rsc-card--mask-${maskShape}`);
392 }
393 });
394
395 // Setup triggers based on configuration
396 if (trigger === "hover" || trigger === "both") {
397 setupHoverTriggers();
398 }
399
400 if ((trigger === "scroll" || trigger === "both") && !isEditor) {
401 setupScrollTriggers();
402 }
403
404 // Setup touch swipe (Pro)
405 if (touchSwipe && !isEditor) {
406 setupTouchSwipe();
407 }
408
409 // Reduced motion: instant reveal or simplified
410 if (reducedMotion) {
411 root.classList.add("kng-rsc--reduced-motion");
412 }
413 };
414
415 // Initialize on Elementor frontend ready
416 $(window).on("elementor/frontend/init", () => {
417 elementorFrontend.hooks.addAction(
418 "frontend/element_ready/king-addons-reveal-swipe-cards.default",
419 ($scope) => {
420 elementorFrontend.elementsHandler.addHandler(
421 elementorModules.frontend.handlers.Base.extend({
422 onInit: function () {
423 initWidget(this.$element);
424 },
425 }),
426 { $element: $scope }
427 );
428 }
429 );
430 });
431 })(jQuery);
432