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

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

419 lines 15.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 "use strict";
2 (($) => {
3 const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
4
5 const parseSettings = (root) => {
6 if (!root || !root.dataset) {
7 return {};
8 }
9
10 try {
11 return JSON.parse(root.dataset.settings || "{}");
12 } catch (e) {
13 return {};
14 }
15 };
16
17 const initWidget = ($scope) => {
18 const root = $scope.find(".king-addons-interactive-steps").first()[0];
19 if (!root || root.dataset.kngIspInit === "yes") {
20 return;
21 }
22
23 root.dataset.kngIspInit = "yes";
24 const options = parseSettings(root);
25
26 const steps = Array.from(root.querySelectorAll(".king-addons-isp-step"));
27 if (!steps.length) {
28 return;
29 }
30
31 const progressCount = root.querySelector(".king-addons-isp-progress__count");
32 const progressPercent = root.querySelector(".king-addons-isp-progress__percent");
33 const progressFill = root.querySelector(".king-addons-isp-progress__fill");
34 const stickySteps = root.querySelector(".king-addons-isp-progress__steps");
35 const isStickyHeader = root.classList.contains("king-addons-isp-sticky-header");
36 let stickyStepItems = [];
37 const prefersReduced =
38 window.matchMedia &&
39 window.matchMedia("(prefers-reduced-motion: reduce)").matches;
40 const isEditor =
41 window.elementorFrontend &&
42 elementorFrontend.isEditMode &&
43 elementorFrontend.isEditMode();
44
45 const reducedMode = options.reducedMotion || "auto";
46 const reducedFull =
47 reducedMode === "full" ||
48 (prefersReduced && reducedMode !== "off" && reducedMode !== "minimal");
49 const reducedMinimal =
50 reducedMode === "minimal" ||
51 (prefersReduced && reducedMode === "auto");
52
53 if (reducedFull) {
54 root.classList.add("king-addons-isp-reduced-full");
55 } else if (reducedMinimal) {
56 root.classList.add("king-addons-isp-reduced-minimal");
57 }
58
59 const expandBehavior = options.expandBehavior || "click";
60 const defaultOpen = options.defaultOpen || "first";
61 const singleOpen = options.singleOpen !== "no";
62 const scrollToStep = options.scrollToStep === "yes";
63 const markersClickable = options.markersClickable !== "no";
64 const showStepCount = options.showStepCount === "yes";
65 const showPercent = options.showPercent === "yes";
66 const stepLabelTemplate = options.stepLabel || "Step %1$s of %2$s";
67 const updateHash = options.updateHash === "yes";
68 const scrollToHash = options.scrollToHash === "yes";
69 const anchorLinking = options.anchorLinking === "yes";
70 const keyboardNav = options.keyboardNav === "yes";
71 const activateOnScroll = !isEditor && options.activateOnScroll === "yes";
72 const reveal = options.reveal === "yes" && !isEditor;
73
74 const getPanel = (step) => step.querySelector(".king-addons-isp-step__panel");
75 const getButton = (step) => step.querySelector(".king-addons-isp-step__button");
76 const getAnchor = (step) => step.dataset.anchor || "";
77
78 const setExpanded = (step, isOpen) => {
79 const panel = getPanel(step);
80 const button = getButton(step);
81
82 if (!panel || !button) {
83 return;
84 }
85
86 step.classList.toggle("is-open", isOpen);
87 button.setAttribute("aria-expanded", isOpen ? "true" : "false");
88 panel.setAttribute("aria-hidden", isOpen ? "false" : "true");
89 if (isOpen) {
90 panel.removeAttribute("hidden");
91 } else {
92 panel.setAttribute("hidden", "hidden");
93 }
94 };
95
96 const closeAll = () => {
97 steps.forEach((step) => setExpanded(step, false));
98 };
99
100 const openAll = () => {
101 steps.forEach((step) => setExpanded(step, true));
102 };
103
104 const applyDefaults = () => {
105 if (expandBehavior === "none") {
106 openAll();
107 return;
108 }
109
110 closeAll();
111 if (defaultOpen === "first") {
112 const firstStep = steps[0];
113 if (firstStep && getPanel(firstStep)) {
114 setExpanded(firstStep, true);
115 }
116 }
117 };
118
119 const formatStepLabel = (current, total) =>
120 stepLabelTemplate
121 .replace("%1$s", current)
122 .replace("%2$s", total);
123
124 const updateProgress = (activeIndex) => {
125 const total = steps.length;
126 const ratio = total > 1 ? activeIndex / (total - 1) : 1;
127 const percentValue = Math.round(ratio * 100);
128
129 root.style.setProperty("--kng-isp-progress-ratio", ratio);
130 root.style.setProperty("--kng-isp-progress-percent", `${percentValue}%`);
131
132 if (progressFill) {
133 progressFill.style.width = `${percentValue}%`;
134 }
135
136 if (progressCount && showStepCount) {
137 progressCount.textContent = formatStepLabel(
138 activeIndex + 1,
139 total
140 );
141 }
142
143 if (progressPercent && showPercent) {
144 progressPercent.textContent = `${percentValue}%`;
145 }
146 };
147
148 const setActive = (activeIndex, shouldUpdateHash) => {
149 steps.forEach((step, index) => {
150 step.classList.toggle("is-active", index === activeIndex);
151 step.classList.toggle("is-completed", index < activeIndex);
152 step.classList.toggle("is-upcoming", index > activeIndex);
153
154 if (index === activeIndex) {
155 step.setAttribute("aria-current", "step");
156 } else {
157 step.removeAttribute("aria-current");
158 }
159 });
160
161 updateProgress(activeIndex);
162
163 if (stickyStepItems.length) {
164 stickyStepItems.forEach((item, index) => {
165 item.classList.toggle("is-active", index === activeIndex);
166 item.classList.toggle("is-completed", index < activeIndex);
167
168 if (index === activeIndex) {
169 item.setAttribute("aria-current", "step");
170 } else {
171 item.removeAttribute("aria-current");
172 }
173 });
174 }
175
176 if (shouldUpdateHash && updateHash && anchorLinking && !isEditor) {
177 const anchor = getAnchor(steps[activeIndex]);
178 if (anchor) {
179 try {
180 window.history.replaceState(null, "", `#${anchor}`);
181 } catch (e) {
182 window.location.hash = anchor;
183 }
184 }
185 }
186 };
187
188 const activateStep = (index, openPanel = true, shouldScroll = true) => {
189 const step = steps[index];
190 if (!step) {
191 return;
192 }
193
194 const hasPanel = !!getPanel(step);
195 if (expandBehavior === "click" && hasPanel && openPanel) {
196 if (singleOpen) {
197 closeAll();
198 setExpanded(step, true);
199 } else {
200 setExpanded(step, !step.classList.contains("is-open"));
201 }
202 }
203
204 setActive(index, true);
205
206 if (scrollToStep && shouldScroll && !isEditor) {
207 step.scrollIntoView({
208 behavior: reducedFull ? "auto" : "smooth",
209 block: "start",
210 });
211 }
212 };
213
214 applyDefaults();
215
216 if (stickySteps && isStickyHeader) {
217 stickySteps.innerHTML = "";
218 stickyStepItems = steps.map((_, index) => {
219 const item = document.createElement("button");
220 item.type = "button";
221 item.className = "king-addons-isp-progress-step";
222 item.dataset.stepIndex = String(index);
223 item.setAttribute(
224 "aria-label",
225 formatStepLabel(index + 1, steps.length)
226 );
227 stickySteps.appendChild(item);
228 return item;
229 });
230 }
231
232 const initialActive = Math.max(
233 0,
234 steps.findIndex((step) => step.classList.contains("is-active"))
235 );
236 setActive(initialActive, false);
237
238 root.addEventListener("click", (event) => {
239 const button = event.target.closest(".king-addons-isp-step__button");
240 if (button) {
241 const step = button.closest(".king-addons-isp-step");
242 const index = steps.indexOf(step);
243 if (index > -1) {
244 activateStep(index, true, true);
245 }
246 return;
247 }
248
249 if (markersClickable) {
250 const stickyDot = event.target.closest(".king-addons-isp-progress-step");
251 if (stickyDot && stickyDot.dataset && stickyDot.dataset.stepIndex) {
252 const index = Number(stickyDot.dataset.stepIndex);
253 if (!Number.isNaN(index)) {
254 activateStep(clamp(index, 0, steps.length - 1), true, true);
255 }
256 return;
257 }
258 }
259
260 if (markersClickable) {
261 const marker = event.target.closest(".king-addons-isp-step__marker");
262 if (marker) {
263 const step = marker.closest(".king-addons-isp-step");
264 const index = steps.indexOf(step);
265 if (index > -1) {
266 activateStep(index, true, true);
267 }
268 }
269 }
270 });
271
272 const isHorizontalLayout = () => {
273 if (!root.classList.contains("king-addons-isp-layout-horizontal")) {
274 return false;
275 }
276
277 if (
278 root.classList.contains("king-addons-isp-mobile-vertical") &&
279 window.innerWidth <= 767
280 ) {
281 return false;
282 }
283
284 return true;
285 };
286
287 if (keyboardNav) {
288 root.addEventListener("keydown", (event) => {
289 if (!event.target.closest(".king-addons-isp-step__button")) {
290 return;
291 }
292
293 if (!["ArrowDown", "ArrowUp", "ArrowLeft", "ArrowRight"].includes(event.key)) {
294 return;
295 }
296
297 const step = event.target.closest(".king-addons-isp-step");
298 const index = steps.indexOf(step);
299 if (index < 0) {
300 return;
301 }
302
303 const isHorizontal = isHorizontalLayout();
304 const stepDelta =
305 event.key === "ArrowDown" || event.key === "ArrowRight" ? 1 : -1;
306
307 if (
308 (isHorizontal && (event.key === "ArrowLeft" || event.key === "ArrowRight")) ||
309 (!isHorizontal && (event.key === "ArrowDown" || event.key === "ArrowUp"))
310 ) {
311 event.preventDefault();
312 const nextIndex = clamp(index + stepDelta, 0, steps.length - 1);
313 const nextButton = getButton(steps[nextIndex]);
314 if (nextButton) {
315 nextButton.focus();
316 }
317 activateStep(nextIndex, true, true);
318 }
319 });
320 }
321
322 if (anchorLinking && scrollToHash && window.location.hash && !isEditor) {
323 const hash = window.location.hash.replace("#", "");
324 const targetIndex = steps.findIndex(
325 (step) => getAnchor(step) === hash
326 );
327 if (targetIndex > -1) {
328 setTimeout(() => {
329 activateStep(targetIndex, true, false);
330 steps[targetIndex].scrollIntoView({
331 behavior: reducedFull ? "auto" : "smooth",
332 block: "start",
333 });
334 }, 150);
335 }
336 }
337
338 if (reveal) {
339 const revealType = options.revealType || "fade";
340 root.classList.add(`king-addons-isp-reveal-${revealType}`);
341
342 steps.forEach((step, idx) => {
343 step.classList.add("is-reveal");
344 if (options.revealStagger && !reducedFull) {
345 step.style.transitionDelay = `${idx * options.revealStagger}ms`;
346 }
347 });
348
349 if (reducedFull) {
350 steps.forEach((step) => step.classList.add("is-revealed"));
351 } else if ("IntersectionObserver" in window) {
352 const revealObserver = new IntersectionObserver(
353 (entries) => {
354 entries.forEach((entry) => {
355 if (entry.isIntersecting) {
356 entry.target.classList.add("is-revealed");
357 revealObserver.unobserve(entry.target);
358 }
359 });
360 },
361 { threshold: 0.2 }
362 );
363
364 steps.forEach((step) => revealObserver.observe(step));
365 } else {
366 steps.forEach((step) => step.classList.add("is-revealed"));
367 }
368 }
369
370 if (activateOnScroll && "IntersectionObserver" in window) {
371 const offset = clamp(Number(options.activationOffset) || 40, 10, 80);
372 const rootMargin = `-${offset}% 0px -${100 - offset}% 0px`;
373
374 const observer = new IntersectionObserver(
375 (entries) => {
376 const visible = entries
377 .filter((entry) => entry.isIntersecting)
378 .sort(
379 (a, b) =>
380 b.intersectionRatio - a.intersectionRatio
381 );
382
383 if (!visible.length) {
384 return;
385 }
386
387 const index = steps.indexOf(visible[0].target);
388 if (index > -1) {
389 activateStep(index, true, false);
390 }
391 },
392 {
393 root: null,
394 rootMargin,
395 threshold: [0, 0.25, 0.5, 0.75, 1],
396 }
397 );
398
399 steps.forEach((step) => observer.observe(step));
400 }
401 };
402
403 $(window).on("elementor/frontend/init", () => {
404 elementorFrontend.hooks.addAction(
405 "frontend/element_ready/king-addons-interactive-steps-progress.default",
406 ($scope) => {
407 elementorFrontend.elementsHandler.addHandler(
408 elementorModules.frontend.handlers.Base.extend({
409 onInit: function () {
410 initWidget(this.$element);
411 },
412 }),
413 { $element: $scope }
414 );
415 }
416 );
417 });
418 })(jQuery);
419