PluginProbe
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder / 51.1.83
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder v51.1.83
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 / Product_360_Viewer / script.js

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

374 lines 12.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 "use strict";
2
3 (() => {
4 const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
5
6 const parseJSON = (value, fallback) => {
7 try {
8 return JSON.parse(value);
9 } catch (e) {
10 return fallback;
11 }
12 };
13
14 class KingAddonsProduct360Viewer {
15 constructor(root) {
16 this.root = root;
17 this.canvas = root.querySelector(".king-addons-product-360-viewer__canvas");
18 this.image = root.querySelector(".king-addons-product-360-viewer__frame");
19 this.loader = root.querySelector(".king-addons-product-360-viewer__loader");
20 this.progress = root.querySelector(".king-addons-product-360-viewer__progress");
21 this.progressValue = root.querySelector(".king-addons-product-360-viewer__progress-value");
22 this.zoomButton = root.querySelector(".king-addons-product-360-viewer__zoom");
23 this.hotspotsLayer = root.querySelector(".king-addons-product-360-viewer__hotspots");
24
25 this.frames = parseJSON(root.dataset.frames || "[]", []);
26 this.hotspots = parseJSON(root.dataset.hotspots || "[]", []);
27
28 this.settings = {
29 isPro: root.dataset.isPro === "true",
30 autoplay: (root.dataset.autoplay || "no") === "yes",
31 autoplaySpeed: parseInt(root.dataset.autoplaySpeed || "90", 10),
32 autoplayMode: root.dataset.autoplayMode || "loop",
33 dragSensitivity: parseFloat(root.dataset.dragSensitivity || "8"),
34 invertDrag: (root.dataset.invertDrag || "no") === "yes",
35 dragAxis: root.dataset.dragAxis || "horizontal",
36 enableScroll: (root.dataset.enableScroll || "no") === "yes",
37 inertia: parseFloat(root.dataset.inertia || "0"),
38 showProgress: (root.dataset.showProgress || "yes") === "yes",
39 showZoom: (root.dataset.showZoom || "no") === "yes",
40 zoomScale: parseFloat(root.dataset.zoomScale || "1.35"),
41 startFrame: clamp(parseInt(root.dataset.startFrame || "1", 10) - 1, 0, this.frames.length - 1),
42 };
43
44 this.state = {
45 currentFrame: this.settings.startFrame,
46 autoplayTimer: null,
47 pointerDown: false,
48 lastPointer: { x: 0, y: 0 },
49 inertiaFrame: null,
50 inertiaAccumulator: 0,
51 velocity: 0,
52 autoplayWasRunning: false,
53 };
54 }
55
56 init() {
57 if (!this.frames.length || !this.canvas || !this.image) {
58 return;
59 }
60
61 this.setFrame(this.state.currentFrame, false);
62 this.buildHotspots();
63 this.bindEvents();
64 this.preloadFrames();
65
66 if (this.settings.autoplay) {
67 this.startAutoplay();
68 }
69 }
70
71 bindEvents() {
72 this.canvas.addEventListener("pointerdown", (event) => this.handlePointerDown(event));
73 if (this.settings.enableScroll) {
74 this.canvas.addEventListener(
75 "wheel",
76 (event) => this.handleWheel(event),
77 { passive: false }
78 );
79 }
80
81 if (this.zoomButton) {
82 this.zoomButton.addEventListener("click", () => this.toggleZoom());
83 }
84 }
85
86 handlePointerDown(event) {
87 event.preventDefault();
88 this.state.pointerDown = true;
89 this.state.lastPointer = { x: event.clientX, y: event.clientY };
90 this.state.velocity = 0;
91 this.state.inertiaAccumulator = 0;
92
93 if (this.settings.autoplay) {
94 this.state.autoplayWasRunning = true;
95 this.stopAutoplay();
96 } else {
97 this.state.autoplayWasRunning = false;
98 }
99
100 const moveHandler = (moveEvent) => this.handlePointerMove(moveEvent);
101 const upHandler = (upEvent) => this.handlePointerUp(upEvent, moveHandler, upHandler);
102
103 document.addEventListener("pointermove", moveHandler);
104 document.addEventListener("pointerup", upHandler);
105 document.addEventListener("pointercancel", upHandler);
106 }
107
108 handlePointerMove(event) {
109 if (!this.state.pointerDown) {
110 return;
111 }
112
113 const current = { x: event.clientX, y: event.clientY };
114 const deltaX = current.x - this.state.lastPointer.x;
115 const deltaY = current.y - this.state.lastPointer.y;
116
117 let delta = deltaX;
118 if (this.settings.dragAxis === "vertical") {
119 delta = deltaY * -1;
120 } else if (this.settings.dragAxis === "both") {
121 delta = Math.abs(deltaY) > Math.abs(deltaX) ? deltaY * -1 : deltaX;
122 }
123
124 const sensitivity = Math.max(1, this.settings.dragSensitivity);
125 const steps = Math.trunc(delta / sensitivity);
126
127 if (steps !== 0) {
128 const direction = this.settings.invertDrag ? -steps : steps;
129 this.rotateBy(direction);
130 this.state.velocity = direction;
131 this.state.lastPointer = current;
132 }
133 }
134
135 handlePointerUp(event, moveHandler, upHandler) {
136 event.preventDefault();
137 this.state.pointerDown = false;
138
139 document.removeEventListener("pointermove", moveHandler);
140 document.removeEventListener("pointerup", upHandler);
141 document.removeEventListener("pointercancel", upHandler);
142
143 if (this.settings.inertia > 0 && Math.abs(this.state.velocity) > 0) {
144 this.startInertia(this.state.velocity);
145 }
146
147 if (this.state.autoplayWasRunning) {
148 this.startAutoplay();
149 }
150 }
151
152 handleWheel(event) {
153 event.preventDefault();
154 const direction = event.deltaY > 0 ? 1 : -1;
155 this.rotateBy(direction);
156 }
157
158 rotateBy(step) {
159 if (!Number.isFinite(step) || this.frames.length === 0) {
160 return;
161 }
162
163 this.setFrame(this.state.currentFrame + step, true);
164 }
165
166 setFrame(targetIndex, updateProgress = true) {
167 const length = this.frames.length;
168 const nextIndex = ((targetIndex % length) + length) % length;
169 const nextFrame = this.frames[nextIndex];
170
171 if (!nextFrame) {
172 return;
173 }
174
175 this.state.currentFrame = nextIndex;
176 this.image.src = nextFrame.url;
177 this.image.alt = nextFrame.alt || nextFrame.label || "";
178
179 if (updateProgress) {
180 this.updateProgress();
181 }
182
183 this.updateHotspots();
184 }
185
186 updateProgress() {
187 if (!this.progressValue || !this.settings.showProgress) {
188 return;
189 }
190 const humanIndex = this.state.currentFrame + 1;
191 this.progressValue.textContent = `${humanIndex} / ${this.frames.length}`;
192 }
193
194 startAutoplay() {
195 this.stopAutoplay();
196 if (this.frames.length <= 1) {
197 return;
198 }
199
200 const speed = clamp(this.settings.autoplaySpeed, 10, 2000);
201 let direction = 1;
202
203 this.state.autoplayTimer = window.setInterval(() => {
204 if (this.settings.autoplayMode === "pingpong") {
205 const nextIndex = this.state.currentFrame + direction;
206 if (nextIndex >= this.frames.length || nextIndex < 0) {
207 direction *= -1;
208 }
209 this.setFrame(this.state.currentFrame + direction);
210 return;
211 }
212
213 this.setFrame(this.state.currentFrame + 1);
214 }, speed);
215 }
216
217 stopAutoplay() {
218 if (this.state.autoplayTimer) {
219 window.clearInterval(this.state.autoplayTimer);
220 this.state.autoplayTimer = null;
221 }
222 }
223
224 startInertia(initialVelocity) {
225 if (this.state.inertiaFrame) {
226 window.cancelAnimationFrame(this.state.inertiaFrame);
227 }
228
229 const friction = clamp(this.settings.inertia, 0, 1);
230 let velocity = initialVelocity;
231 this.state.inertiaAccumulator = 0;
232
233 const tick = () => {
234 velocity *= 1 - friction;
235 this.state.inertiaAccumulator += velocity;
236
237 const step = Math.trunc(this.state.inertiaAccumulator);
238 if (step !== 0) {
239 this.state.inertiaAccumulator -= step;
240 this.rotateBy(step);
241 }
242
243 if (Math.abs(velocity) > 0.05 || Math.abs(this.state.inertiaAccumulator) > 0.05) {
244 this.state.inertiaFrame = window.requestAnimationFrame(tick);
245 }
246 };
247
248 this.state.inertiaFrame = window.requestAnimationFrame(tick);
249 }
250
251 preloadFrames() {
252 if (!this.loader) {
253 return;
254 }
255
256 let loaded = 0;
257 const total = this.frames.length;
258
259 const handleLoaded = () => {
260 loaded += 1;
261 if (loaded >= Math.min(total, 2)) {
262 this.root.classList.remove("king-addons-product-360-viewer--loading");
263 }
264 };
265
266 this.frames.forEach((frame) => {
267 const img = new Image();
268 img.onload = handleLoaded;
269 img.onerror = handleLoaded;
270 img.src = frame.url;
271 });
272 }
273
274 toggleZoom() {
275 if (!this.settings.showZoom) {
276 return;
277 }
278
279 const isActive = this.root.classList.toggle("king-addons-product-360-viewer--zoomed");
280 if (this.zoomButton) {
281 this.zoomButton.setAttribute("aria-pressed", isActive ? "true" : "false");
282 }
283 this.image.style.setProperty("--ka-360-zoom-scale", this.settings.zoomScale || 1.35);
284 }
285
286 buildHotspots() {
287 if (!this.hotspotsLayer || !this.hotspots.length) {
288 return;
289 }
290
291 this.hotspotsLayer.querySelectorAll(".king-addons-product-360-viewer__hotspot").forEach((node) => {
292 node.addEventListener("click", (event) => this.handleHotspotClick(event.currentTarget));
293 });
294
295 this.updateHotspots();
296 }
297
298 handleHotspotClick(target) {
299 const url = target.dataset.link;
300 if (!url) {
301 return;
302 }
303
304 const anchor = document.createElement("a");
305 anchor.href = url;
306 anchor.target = target.dataset.newTab === "true" ? "_blank" : "_self";
307
308 const rel = [];
309 if (target.dataset.nofollow === "true") {
310 rel.push("nofollow");
311 }
312 if (anchor.target === "_blank") {
313 rel.push("noopener", "noreferrer");
314 }
315 anchor.rel = rel.join(" ");
316
317 anchor.style.display = "none";
318 document.body.appendChild(anchor);
319 anchor.click();
320 anchor.remove();
321 }
322
323 updateHotspots() {
324 if (!this.hotspotsLayer) {
325 return;
326 }
327
328 const activeFrame = this.state.currentFrame + 1;
329 this.hotspotsLayer.querySelectorAll(".king-addons-product-360-viewer__hotspot").forEach((hotspot) => {
330 const frameMatch = Number(hotspot.dataset.frame) === activeFrame;
331 hotspot.classList.toggle("is-active", frameMatch);
332 hotspot.setAttribute("aria-hidden", frameMatch ? "false" : "true");
333 });
334 }
335 }
336
337 const initScope = (scope) => {
338 const roots = scope.querySelectorAll(".king-addons-product-360-viewer");
339 roots.forEach((root) => {
340 if (root.dataset.ka360Init === "true") {
341 return;
342 }
343 root.dataset.ka360Init = "true";
344
345 const instance = new KingAddonsProduct360Viewer(root);
346 instance.init();
347 });
348 };
349
350 const onDocumentReady = () => initScope(document);
351
352 if (window.elementorFrontend && window.elementorFrontend.hooks) {
353 window.elementorFrontend.hooks.addAction(
354 "frontend/element_ready/king-addons-product-360-viewer.default",
355 (scope) => {
356 const target = Array.isArray(scope) ? scope[0] : scope;
357 if (target) {
358 initScope(target);
359 }
360 }
361 );
362 }
363
364 if (document.readyState === "complete" || document.readyState === "interactive") {
365 onDocumentReady();
366 } else {
367 document.addEventListener("DOMContentLoaded", onDocumentReady);
368 }
369 })();
370
371
372
373
374