PluginProbe
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder / 51.1.49
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder v51.1.49
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 51.1.47 51.1.49 All 37 releases
king-addons / includes / widgets / Styled_Text_Builder / script.js

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

716 lines 27.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 "use strict";
2
3 (function ($) {
4 // In Elementor the preview iframe can be narrow even on desktop.
5 // Detect "mobile" by pointer/hover capabilities, not by width.
6 const isTouchLike = () => window.matchMedia("(hover: none), (pointer: coarse)").matches;
7
8 const clamp = (val, min, max) => Math.max(min, Math.min(max, val));
9
10 const getCssVar = (el, name, fallback) => {
11 const value = window.getComputedStyle(el).getPropertyValue(name).trim();
12 return value || fallback;
13 };
14
15 const getNumberCssVar = (el, name, fallback) => {
16 const raw = getCssVar(el, name, "");
17 const num = parseFloat(raw);
18 return Number.isFinite(num) ? num : fallback;
19 };
20
21 const setupSpoilerCanvas = ($item, $inner) => {
22 if ($item.data("kngSpoilerInit")) return;
23 $item.data("kngSpoilerInit", true);
24
25 const innerEl = $inner.get(0);
26 const textEl = $inner.find("span").first().get(0);
27 if (!innerEl || !textEl) return;
28
29 const canvas = document.createElement("canvas");
30 canvas.className = "king-addons-spoiler-canvas";
31 innerEl.appendChild(canvas);
32 const ctx = canvas.getContext("2d");
33 if (!ctx) return;
34
35 const state = {
36 dpr: Math.max(1, Math.min(2, window.devicePixelRatio || 1)),
37 w: 0,
38 h: 0,
39 particles: [],
40 revealPoints: [],
41 rafId: 0,
42 start: performance.now(),
43 last: performance.now(),
44 running: false,
45 bounds: null,
46 cloud: null,
47 region: null,
48 sprites: null,
49 spriteKey: "",
50 edgeFadeStart: 0.65,
51 edgeFadeEnd: 2.05,
52 };
53
54 const getText = () => ($(textEl).text() || "").trim();
55
56 const parseColor = (color) => {
57 // Supports: #rgb, #rrggbb, rgb(), rgba(). Falls back to medium gray.
58 const fallback = { r: 156, g: 163, b: 175 };
59 if (!color) return fallback;
60 const c = color.toString().trim();
61 if (c.startsWith("#")) {
62 const hex = c.slice(1);
63 if (hex.length === 3) {
64 const r = parseInt(hex[0] + hex[0], 16);
65 const g = parseInt(hex[1] + hex[1], 16);
66 const b = parseInt(hex[2] + hex[2], 16);
67 return { r, g, b };
68 }
69 if (hex.length === 6) {
70 const r = parseInt(hex.slice(0, 2), 16);
71 const g = parseInt(hex.slice(2, 4), 16);
72 const b = parseInt(hex.slice(4, 6), 16);
73 return { r, g, b };
74 }
75 return fallback;
76 }
77 const m = c.match(/rgba?\(([^)]+)\)/i);
78 if (m) {
79 const parts = m[1].split(",").map((p) => p.trim());
80 const r = parseInt(parts[0], 10);
81 const g = parseInt(parts[1], 10);
82 const b = parseInt(parts[2], 10);
83 if ([r, g, b].every((n) => Number.isFinite(n))) {
84 return { r, g, b };
85 }
86 }
87 return fallback;
88 };
89
90 const rebuildParticles = () => {
91 const text = getText();
92 state.particles = [];
93 state.cloud = null;
94 state.region = null;
95 if (!text || state.w <= 1 || state.h <= 1) return;
96
97 // iMessage Invisible Ink is not per-letter; it's a tight rounded-rect over the text box.
98 const computed = window.getComputedStyle($item.get(0));
99 const font = computed.font || `${computed.fontWeight} ${computed.fontSize} ${computed.fontFamily}`;
100
101 // Measure using an offscreen context.
102 const mcanvas = document.createElement("canvas");
103 const mctx = mcanvas.getContext("2d");
104 if (!mctx) return;
105 mctx.font = font;
106
107 const fontSize = parseFloat(computed.fontSize) || 16;
108 const lineHeightRaw = computed.lineHeight;
109 const lineHeight = Number.isFinite(parseFloat(lineHeightRaw)) ? parseFloat(lineHeightRaw) : fontSize * 1.25;
110
111 // Region from actual laid-out text box.
112 const innerRect = innerEl.getBoundingClientRect();
113 const spanRect = textEl.getBoundingClientRect();
114 const relX = spanRect.left - innerRect.left;
115 const relY = spanRect.top - innerRect.top;
116 const relW = spanRect.width;
117 const relH = spanRect.height;
118
119 const padX = Math.max(10, relW * 0.04, fontSize * 0.7);
120 const padY = Math.max(6, relH * 0.18, fontSize * 0.35);
121 const rx = relW / 2;
122 const corner = clamp(lineHeight * 0.55, 10, Math.max(10, (relH + padY * 2) / 2));
123
124 const region = {
125 x: clamp(relX - padX, 0, Math.max(0, state.w - 1)),
126 y: clamp(relY - padY, 0, Math.max(0, state.h - 1)),
127 w: Math.max(1, relW + padX * 2),
128 h: Math.max(1, relH + padY * 2),
129 r: corner,
130 feather: Math.max(8, lineHeight * 0.28),
131 };
132 // Clamp region size inside canvas.
133 if (region.x + region.w > state.w) region.w = Math.max(1, state.w - region.x);
134 if (region.y + region.h > state.h) region.h = Math.max(1, state.h - region.y);
135 state.region = region;
136
137 // Target count based on region area.
138 const maxParticles = 2200;
139 const minParticles = 520;
140 const area = region.w * region.h;
141 const targetTotal = clamp(Math.round(area / 22), minParticles, maxParticles);
142
143 const sdfRoundRect = (px, py, rr) => {
144 const cx = rr.x + rr.w / 2;
145 const cy = rr.y + rr.h / 2;
146 const hx = rr.w / 2 - rr.r;
147 const hy = rr.h / 2 - rr.r;
148 const qx = Math.abs(px - cx) - hx;
149 const qy = Math.abs(py - cy) - hy;
150 const ax = Math.max(qx, 0);
151 const ay = Math.max(qy, 0);
152 return Math.hypot(ax, ay) + Math.min(Math.max(qx, qy), 0) - rr.r;
153 };
154
155 const samplePoint = () => {
156 const margin = region.feather * 1.15;
157 for (let k = 0; k < 14; k += 1) {
158 const x = region.x + (Math.random() * (region.w + margin * 2) - margin);
159 const y = region.y + (Math.random() * (region.h + margin * 2) - margin);
160 if (sdfRoundRect(x, y, region) <= margin) return { x, y };
161 }
162 return { x: region.x + region.w / 2, y: region.y + region.h / 2 };
163 };
164
165 for (let i = 0; i < targetTotal; i += 1) {
166 const pt = samplePoint();
167 const speed = 7 + Math.random() * 10;
168 const dir = Math.random() * Math.PI * 2;
169 const sparkle = Math.random() < 0.06;
170 const size = sparkle ? (0.90 + Math.random() * 0.35) : (0.55 + Math.random() * 0.28);
171 const tint = sparkle ? (0.65 + Math.random() * 0.25) : (0.22 + Math.random() * 0.22);
172 const kind = sparkle ? "spark" : (Math.random() < 0.55 ? "dust" : "dust2");
173 state.particles.push({
174 x: pt.x,
175 y: pt.y,
176 vx: Math.cos(dir) * speed,
177 vy: Math.sin(dir) * speed,
178 baseSpeed: speed,
179 alpha: 1,
180 size,
181 sparkle,
182 tint,
183 kind,
184 phase: Math.random() * Math.PI * 2,
185 twinkleSpeed: 0.6 + Math.random() * 1.0,
186 // store region ref values for cheaper access
187 rx: region.x,
188 ry: region.y,
189 rw: region.w,
190 rh: region.h,
191 rr: region.r,
192 rf: region.feather,
193 });
194 }
195 };
196
197 const resize = () => {
198 const rect = innerEl.getBoundingClientRect();
199 const w = Math.max(1, rect.width);
200 const h = Math.max(1, rect.height);
201
202 // Avoid rebuilding on tiny sub-pixel changes.
203 if (Math.abs(w - state.w) < 0.5 && Math.abs(h - state.h) < 0.5) return;
204
205 state.w = w;
206 state.h = h;
207 canvas.width = Math.ceil(w * state.dpr);
208 canvas.height = Math.ceil(h * state.dpr);
209 canvas.style.width = `${w}px`;
210 canvas.style.height = `${h}px`;
211 ctx.setTransform(state.dpr, 0, 0, state.dpr, 0, 0);
212
213 rebuildParticles();
214 };
215
216 const addRevealPoint = (x, y) => {
217 state.revealPoints.push({ x, y, t: performance.now() });
218 if (state.revealPoints.length > 30) {
219 state.revealPoints.shift();
220 }
221 };
222
223 const setSpotlight = (clientX, clientY, enabled) => {
224 if (!enabled) {
225 innerEl.style.setProperty("--kng-spoiler-spot-r", "0px");
226 return;
227 }
228
229 const rect = textEl.getBoundingClientRect();
230 const x = clientX - rect.left;
231 const y = clientY - rect.top;
232 const r = 34;
233
234 innerEl.style.setProperty("--kng-spoiler-spot-x", `${x}px`);
235 innerEl.style.setProperty("--kng-spoiler-spot-y", `${y}px`);
236 innerEl.style.setProperty("--kng-spoiler-spot-r", `${r}px`);
237 };
238
239 const clearRevealPoints = () => {
240 state.revealPoints = [];
241 // Restore particles quickly.
242 state.particles.forEach((p) => {
243 p.alpha = 1;
244 // Keep their motion; just restore opacity.
245 });
246 };
247
248 const draw = () => {
249 if ($inner.hasClass("is-revealed")) {
250 state.running = false;
251 state.rafId = 0;
252 return;
253 }
254
255 const now = performance.now();
256 const time = (now - state.start) / 1000;
257 const dt = clamp((now - state.last) / 1000, 0, 0.05);
258 state.last = now;
259
260 // Decay reveal points.
261 const ttl = 1200;
262 state.revealPoints = state.revealPoints.filter((p) => now - p.t < ttl);
263
264 resize();
265 if (!state.w || !state.h) {
266 state.rafId = requestAnimationFrame(draw);
267 return;
268 }
269
270 const text = getText();
271 const computed = window.getComputedStyle($item.get(0));
272 const font = computed.font || `${computed.fontWeight} ${computed.fontSize} ${computed.fontFamily}`;
273 const textColor = computed.color || "#000";
274
275 const spoilerColorRaw = getCssVar($item.get(0), "--kng-spoiler-color", "#9ca3af");
276 const spoilerRgb = parseColor(spoilerColorRaw);
277 const spoilerOpacity = clamp(getNumberCssVar($item.get(0), "--kng-spoiler-opacity", 1), 0, 1);
278
279 ctx.clearRect(0, 0, state.w, state.h);
280
281 const radius = 26;
282 const fadeSpeed = 0.12;
283
284 const smoothStep = (a, b, x) => {
285 const t = clamp((x - a) / (b - a), 0, 1);
286 return t * t * (3 - 2 * t);
287 };
288
289 const mixToWhite = (rgb, t) => {
290 const tt = clamp(t, 0, 1);
291 const r = Math.round(rgb.r + (255 - rgb.r) * tt);
292 const g = Math.round(rgb.g + (255 - rgb.g) * tt);
293 const b = Math.round(rgb.b + (255 - rgb.b) * tt);
294 return { r, g, b };
295 };
296
297 const snap = (v) => {
298 // Snap to device pixels for crisp rendering.
299 const dpr = state.dpr || 1;
300 return Math.round(v * dpr) / dpr;
301 };
302
303 const ensureSprites = (rgb) => {
304 const key = `${rgb.r},${rgb.g},${rgb.b}`;
305 if (state.sprites && state.spriteKey === key) return;
306
307 const makeSprite = (radiusPx, tintToWhite) => {
308 const size = Math.ceil(radiusPx * 2 + 4);
309 const c = document.createElement("canvas");
310 c.width = size;
311 c.height = size;
312 const cctx = c.getContext("2d");
313 if (!cctx) return c;
314
315 // Keep color visible: only tiny whitening.
316 const col = mixToWhite(rgb, tintToWhite);
317 const cx = Math.floor(size / 2);
318 const cy = Math.floor(size / 2);
319
320 cctx.clearRect(0, 0, size, size);
321 cctx.imageSmoothingEnabled = false;
322
323 // Hard core (no AA): 1–2 device-independent pixels.
324 const core = radiusPx >= 1.05 ? 2 : 1;
325 cctx.globalAlpha = 1;
326 cctx.fillStyle = `rgba(${col.r}, ${col.g}, ${col.b}, 1)`;
327 cctx.fillRect(cx - Math.floor(core / 2), cy - Math.floor(core / 2), core, core);
328
329 // Minimal halo (very subtle) so it doesn't look blurry.
330 const grad = cctx.createRadialGradient(cx, cy, 0, cx, cy, radiusPx + 0.8);
331 grad.addColorStop(0, `rgba(${col.r}, ${col.g}, ${col.b}, 0.18)`);
332 grad.addColorStop(1, `rgba(${col.r}, ${col.g}, ${col.b}, 0)`);
333 cctx.fillStyle = grad;
334 cctx.beginPath();
335 cctx.arc(cx + 0.5, cy + 0.5, radiusPx + 0.8, 0, Math.PI * 2);
336 cctx.fill();
337 return c;
338 };
339
340 // Mostly small dust, rare brighter sparkles.
341 state.sprites = {
342 dust: makeSprite(0.72, 0.06),
343 dust2: makeSprite(0.92, 0.08),
344 spark: makeSprite(1.18, 0.14),
345 };
346 state.spriteKey = key;
347 };
348
349 const sdfRoundRectFast = (px, py, x, y, w, h, r) => {
350 const cx = x + w / 2;
351 const cy = y + h / 2;
352 const hx = w / 2 - r;
353 const hy = h / 2 - r;
354 const qx = Math.abs(px - cx) - hx;
355 const qy = Math.abs(py - cy) - hy;
356 const ax = Math.max(qx, 0);
357 const ay = Math.max(qy, 0);
358 return Math.hypot(ax, ay) + Math.min(Math.max(qx, qy), 0) - r;
359 };
360
361 const edgeFactorAt = (p) => {
362 if (typeof p.cx !== "number" || typeof p.cy !== "number" || typeof p.rx !== "number" || typeof p.ry !== "number") {
363 return 1;
364 }
365 const nx = (p.x - p.cx) / p.rx;
366 const ny = (p.y - p.cy) / p.ry;
367 const r = Math.sqrt(nx * nx + ny * ny) + (typeof p.edgeJitter === "number" ? p.edgeJitter : 0);
368
369 // Feathered edge so the cloud doesn't end with a hard oval.
370 // r <= 1.0 -> fully visible
371 // 1.0 < r < 1.55 -> fade out
372 // r >= 1.55 -> fully invisible (and we respawn)
373 const fadeStart = state.edgeFadeStart || 1.0;
374 const fadeEnd = state.edgeFadeEnd || 1.55;
375 if (r <= fadeStart) return 1;
376 if (r >= fadeEnd) return 0;
377 const t = clamp((r - fadeStart) / (fadeEnd - fadeStart), 0, 1);
378 const s = t * t * (3 - 2 * t);
379 return 1 - s;
380 };
381
382 const shouldRespawn = (p) => {
383 if (typeof p.cx !== "number" || typeof p.cy !== "number" || typeof p.rx !== "number" || typeof p.ry !== "number") {
384 return false;
385 }
386 const nx = (p.x - p.cx) / p.rx;
387 const ny = (p.y - p.cy) / p.ry;
388 const r = Math.sqrt(nx * nx + ny * ny);
389 const fadeEnd = state.edgeFadeEnd || 1.55;
390 return r > fadeEnd + 0.55;
391 };
392
393 const respawn = (p) => {
394 if (!state.region) {
395 return;
396 }
397 const margin = state.region.feather * 1.15;
398 for (let k = 0; k < 12; k += 1) {
399 const x = state.region.x + (Math.random() * (state.region.w + margin * 2) - margin);
400 const y = state.region.y + (Math.random() * (state.region.h + margin * 2) - margin);
401 if (sdfRoundRectFast(x, y, state.region.x, state.region.y, state.region.w, state.region.h, state.region.r) <= margin) {
402 p.x = x;
403 p.y = y;
404 break;
405 }
406 }
407
408 const speed = typeof p.baseSpeed === "number" ? p.baseSpeed : (7 + Math.random() * 10);
409 const dir = Math.random() * Math.PI * 2;
410 p.vx = Math.cos(dir) * speed;
411 p.vy = Math.sin(dir) * speed;
412 };
413
414 ensureSprites(spoilerRgb);
415
416 // Preserve color (lighter tends to wash into white).
417 ctx.globalCompositeOperation = "source-over";
418 ctx.imageSmoothingEnabled = false;
419
420 for (let i = 0; i < state.particles.length; i += 1) {
421 const p = state.particles[i];
422
423 // Natural iMessage-like shimmer: gentle drift + tiny curl.
424 if (typeof p.vx === "number" && typeof p.vy === "number") {
425 const phase = (p.phase || 0);
426 const ax = Math.sin(p.y * 0.045 + time * 0.85 + phase) * 3.2;
427 const ay = Math.cos(p.x * 0.045 - time * 0.80 + phase) * 3.2;
428 p.vx += ax * dt;
429 p.vy += ay * dt;
430
431 const base = typeof p.baseSpeed === "number" ? p.baseSpeed : 14;
432 const maxV = base * 1.18;
433 const v = Math.sqrt(p.vx * p.vx + p.vy * p.vy);
434 if (v > maxV) {
435 const k = maxV / (v || 1);
436 p.vx *= k;
437 p.vy *= k;
438 }
439
440 p.x += p.vx * dt;
441 p.y += p.vy * dt;
442 }
443
444 if (!state.region) continue;
445 // No hard boundary: fade via SDF with a bit of noise.
446 const sdf = sdfRoundRectFast(p.x, p.y, state.region.x, state.region.y, state.region.w, state.region.h, state.region.r);
447 // Irregular edge (not a visible geometric boundary)
448 const edgeNoise = (Math.sin(p.x * 0.11 + time * 0.8 + (p.phase || 0)) + Math.cos(p.y * 0.13 - time * 0.7 + (p.phase || 0))) * 0.5;
449 const feather = state.region.feather;
450 const edge = 1 - smoothStep(-feather, 0, sdf + edgeNoise * feather * 0.18);
451 if (edge <= 0.01) {
452 // if far outside, respawn; if near outside, just drift back.
453 if (sdf > feather * 1.8) respawn(p);
454 continue;
455 }
456
457 // If slightly outside, gently pull back toward center (no bounce).
458 if (sdf > 0.6) {
459 const cx = state.region.x + state.region.w / 2;
460 const cy = state.region.y + state.region.h / 2;
461 p.x += (cx - p.x) * 0.02;
462 p.y += (cy - p.y) * 0.02;
463 p.vx *= 0.98;
464 p.vy *= 0.98;
465 }
466
467 // Soft wrap/respawn so the cloud keeps moving without visible "bounce".
468 // Only respawn when far outside; near-edge particles fade out instead.
469 if (shouldRespawn(p)) {
470 respawn(p);
471 }
472
473 // Reveal around cursor/touch movement.
474 let shouldFade = false;
475 for (let k = 0; k < state.revealPoints.length; k += 1) {
476 const rp = state.revealPoints[k];
477 const dx = p.x - rp.x;
478 const dy = p.y - rp.y;
479 if (dx * dx + dy * dy < radius * radius) {
480 shouldFade = true;
481 break;
482 }
483 }
484
485 if (shouldFade) {
486 p.alpha = Math.max(0, p.alpha - fadeSpeed);
487 } else {
488 p.alpha = Math.min(1, p.alpha + fadeSpeed * 0.35);
489 }
490
491 const tw = 0.94 + 0.06 * Math.sin(time * (p.twinkleSpeed || 1.0) + (p.phase || 0));
492 // Boost a bit so color reads.
493 const baseAlpha = clamp(spoilerOpacity * p.alpha * edge * tw * 1.48, 0, 1);
494 if (baseAlpha <= 0.01) continue;
495
496 const size = typeof p.size === "number" ? p.size : 0.8;
497 const kind = (p.kind || (p.sparkle ? "spark" : "dust"));
498 const sprite = kind === "spark" ? state.sprites.spark : (kind === "dust2" ? state.sprites.dust2 : state.sprites.dust);
499 const sw = sprite.width;
500 const sh = sprite.height;
501 // Limit scaling to avoid blur and keep pointy look.
502 const scale = clamp(size / 0.8, 0.9, 1.15);
503 const dw = Math.max(1, Math.round(sw * scale));
504 const dh = Math.max(1, Math.round(sh * scale));
505
506 const tintBoost = typeof p.tint === "number" ? (1 + p.tint * 0.82) : 1;
507 ctx.globalAlpha = clamp(baseAlpha * tintBoost * (kind === "spark" ? 1 : 1), 0, 1);
508 // Snap to device pixel grid for crispness.
509 const dx = snap(p.x - dw / 2);
510 const dy = snap(p.y - dh / 2);
511 ctx.drawImage(sprite, dx, dy, dw, dh);
512 }
513
514 ctx.globalCompositeOperation = "source-over";
515
516 ctx.globalAlpha = 1;
517 state.rafId = requestAnimationFrame(draw);
518 };
519
520 const start = () => {
521 if (state.running) return;
522 state.running = true;
523 state.start = performance.now();
524 state.rafId = requestAnimationFrame(draw);
525 };
526
527 const stopAndRevealAll = () => {
528 $inner.addClass("is-revealed");
529 setSpotlight(0, 0, false);
530 clearRevealPoints();
531 if (state.rafId) {
532 cancelAnimationFrame(state.rafId);
533 state.rafId = 0;
534 }
535 state.running = false;
536 };
537
538 const hideAll = () => {
539 $inner.removeClass("is-revealed");
540 setSpotlight(0, 0, false);
541 clearRevealPoints();
542 start();
543 };
544
545 // Initial sizing & animation.
546 resize();
547 start();
548
549 // Use triggers from widget settings.
550 const triggerDesktop = $item.data("spoiler-desktop") || "hover";
551 const triggerMobile = $item.data("spoiler-mobile") || "tap";
552
553 const attachHoverInk = () => {
554 $item.on("mousemove", (e) => {
555 const rect = innerEl.getBoundingClientRect();
556 addRevealPoint(e.clientX - rect.left, e.clientY - rect.top);
557 setSpotlight(e.clientX, e.clientY, true);
558 });
559 $item.on("mouseleave", () => {
560 setSpotlight(0, 0, false);
561 clearRevealPoints();
562 });
563 };
564
565 const attachTouchInk = () => {
566 $item.on("touchstart", (e) => {
567 const touch = e.originalEvent && e.originalEvent.touches && e.originalEvent.touches[0];
568 if (touch) setSpotlight(touch.clientX, touch.clientY, true);
569 });
570 $item.on("touchmove", (e) => {
571 const touch = e.originalEvent && e.originalEvent.touches && e.originalEvent.touches[0];
572 if (!touch) return;
573 const rect = innerEl.getBoundingClientRect();
574 addRevealPoint(touch.clientX - rect.left, touch.clientY - rect.top);
575 setSpotlight(touch.clientX, touch.clientY, true);
576 });
577 $item.on("touchend", () => {
578 setSpotlight(0, 0, false);
579 // On mobile iOS-like behavior: reset when touch ends.
580 clearRevealPoints();
581 });
582 };
583
584 const attachClickRemove = () => {
585 $item.on("click", (e) => {
586 e.preventDefault();
587 if ($inner.hasClass("is-revealed")) {
588 hideAll();
589 } else {
590 stopAndRevealAll();
591 }
592 });
593 };
594
595 const attachClickToggle = () => {
596 $item.on("click", (e) => {
597 e.preventDefault();
598 if ($inner.hasClass("is-revealed")) {
599 hideAll();
600 } else {
601 stopAndRevealAll();
602 }
603 });
604 };
605
606 if (isTouchLike()) {
607 if (triggerMobile === "hover") {
608 attachTouchInk();
609 } else if (triggerMobile === "hover-click") {
610 attachTouchInk();
611 attachClickRemove();
612 } else {
613 attachClickToggle();
614 }
615 } else {
616 if (triggerDesktop === "click") {
617 attachClickToggle();
618 } else if (triggerDesktop === "hover-click") {
619 attachHoverInk();
620 attachClickRemove();
621 } else {
622 attachHoverInk();
623 }
624 }
625
626 // Keep canvas in sync with layout changes.
627 if (window.ResizeObserver) {
628 const ro = new ResizeObserver(() => {
629 resize();
630 });
631 ro.observe(innerEl);
632 } else {
633 $(window).on("resize", () => resize());
634 }
635 };
636
637 const bindSpoiler = ($item) => {
638 const $inner = $item.find(".king-addons-styled-text-inner").first();
639 if (!$inner.length) return;
640 setupSpoilerCanvas($item, $inner);
641 };
642
643 const startTyping = ($item) => {
644 const $inner = $item.find(".king-addons-styled-text-inner span").first();
645 if (!$inner.length) return;
646
647 const fullText = $inner.text();
648 const speed = parseInt($item.data("typing-speed"), 10) || 80;
649 const delay = parseInt($item.data("typing-delay"), 10) || 1200;
650 const loop = $item.data("typing-loop") === "yes";
651 const cursorChar = ($item.data("typing-cursor") || "|").toString();
652
653 const $cursor = $('<span class="king-addons-styled-text__cursor"></span>').text(cursorChar);
654 $inner.after($cursor);
655
656 let pos = 0;
657 let isTyping = false;
658
659 const typeOnce = () => {
660 if (isTyping) return;
661 isTyping = true;
662 $inner.text("");
663 pos = 0;
664
665 const step = () => {
666 if (pos <= fullText.length) {
667 $inner.text(fullText.substring(0, pos));
668 pos += 1;
669 setTimeout(step, speed);
670 } else {
671 isTyping = false;
672 if (loop) {
673 setTimeout(typeOnce, delay);
674 }
675 }
676 };
677
678 step();
679 };
680
681 typeOnce();
682 };
683
684 const initStyledText = ($scope) => {
685 const $items = $scope.find('.king-addons-styled-text[data-effect="spoiler"]');
686 if ($items.length) {
687 $items.each(function () {
688 bindSpoiler($(this));
689 });
690 }
691
692 const $typingItems = $scope.find('.king-addons-styled-text[data-effect="typing"]');
693 if ($typingItems.length) {
694 $typingItems.each(function () {
695 startTyping($(this));
696 });
697 }
698 };
699
700 $(window).on("elementor/frontend/init", function () {
701 elementorFrontend.hooks.addAction(
702 "frontend/element_ready/king-addons-styled-text-builder.default",
703 function ($scope) {
704 initStyledText($scope);
705 }
706 );
707 });
708 })(jQuery);
709
710
711
712
713
714
715
716