PluginProbe
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder / 51.1.86
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder v51.1.86
51.1.86 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 All 40 releases
← All changes | includes/widgets/Styled_Text_Builder/script.js +612 -22 51.1.44 → 51.1.86 View file →
@@ -1,54 +1,644 @@
1 1 "use strict";
2 2
3 3 (function ($) {
4 - const isMobile = () => window.matchMedia("(max-width: 1023px)").matches;
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;
5 7
6 - const revealSpoiler = ($inner) => {
7 - $inner.addClass("is-revealed");
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;
8 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 + };
9 20
10 - const hideSpoiler = ($inner) => {
11 - $inner.removeClass("is-revealed");
12 - };
21 + const setupSpoilerCanvas = ($item, $inner) => {
22 + if ($item.data("kngSpoilerInit")) return;
23 + $item.data("kngSpoilerInit", true);
13 24
14 - const bindSpoiler = ($item) => {
15 - const $inner = $item.find(".king-addons-styled-text-inner");
16 - if (!$inner.length) return;
25 + const innerEl = $inner.get(0);
26 + const textEl = $inner.find("span").first().get(0);
27 + if (!innerEl || !textEl) return;
17 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.
18 550 const triggerDesktop = $item.data("spoiler-desktop") || "hover";
19 551 const triggerMobile = $item.data("spoiler-mobile") || "tap";
20 552
21 - const attachHover = () => {
22 - $item.on("mouseenter", () => revealSpoiler($inner));
23 - $item.on("mouseleave", () => hideSpoiler($inner));
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 + });
24 563 };
25 564
26 - const attachClick = () => {
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 = () => {
27 585 $item.on("click", (e) => {
28 586 e.preventDefault();
29 - const isOpen = $inner.hasClass("is-revealed");
30 - if (isOpen) {
31 - hideSpoiler($inner);
587 + if ($inner.hasClass("is-revealed")) {
588 + hideAll();
32 589 } else {
33 - revealSpoiler($inner);
590 + stopAndRevealAll();
34 591 }
35 592 });
36 593 };
37 594
38 - if (isMobile()) {
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()) {
39 607 if (triggerMobile === "hover") {
40 - attachHover();
608 + attachTouchInk();
609 + } else if (triggerMobile === "hover-click") {
610 + attachTouchInk();
611 + attachClickRemove();
41 612 } else {
42 - attachClick();
613 + attachClickToggle();
43 614 }
44 615 } else {
45 616 if (triggerDesktop === "click") {
46 - attachClick();
617 + attachClickToggle();
618 + } else if (triggerDesktop === "hover-click") {
619 + attachHoverInk();
620 + attachClickRemove();
47 621 } else {
48 - attachHover();
622 + attachHoverInk();
49 623 }
50 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);
51 641 };
52 642
53 643 const startTyping = ($item) => {
54 644 const $inner = $item.find(".king-addons-styled-text-inner span").first();