PluginProbe
Smart Post – Post Grid, Post Carousel, Post Slider Gutenberg Blocks for Blog & News / 4.0.7
Smart Post – Post Grid, Post Carousel, Post Slider Gutenberg Blocks for Blog & News v4.0.7
4.0.8 4.0.7 4.0.6 4.0.5 4.0.4 4.0.3 4.0.2 4.0.1 2.3.5 2.3.6 2.4.0 2.4.1 2.4.10 2.4.11 2.4.12 2.4.13 2.4.14 2.4.15 2.4.16 2.4.17 2.4.18 2.4.19 2.4.2 2.4.20 2.4.21 All 88 releases
post-carousel / blocks / assets / js / table-of-content.js

table-of-content.js in Smart Post – Post Grid, Post Carousel, Post Slider Gutenberg Blocks for Blog & News 4.0.7, at blocks/assets/js/table-of-content.js

498 lines 13.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 document.addEventListener("DOMContentLoaded", function () {
2 const tocContainer = document.querySelector(".sp-table-of-content-toc");
3 if (!tocContainer) {
4 return;
5 }
6
7 // Get all TOC links
8 const smoothScroll = tocContainer.dataset.smoothScroll === "true";
9 const offsetTop = parseFloat(tocContainer.dataset.offsetTop) || 0;
10 const tocLinks = document.querySelectorAll(".sps-toc-link");
11 const preset = tocContainer.dataset.preset || "presetOne";
12 const hashUrl = tocContainer.dataset.hashUrl || false;
13
14 //............ hash url
15 const headings = document.querySelectorAll("h1, h2, h3, h4, h5, h6");
16 headings.forEach((heading) => {
17 // Find the link inside this heading
18 const link = heading.querySelector(".sps-toc-heading-copy-link");
19
20 if (link) {
21 // Hide initially
22 link.style.opacity = "0";
23 link.style.transition = "opacity 0.3s";
24
25 // Control hover: show/hide link only when desired
26 heading.addEventListener("mouseenter", () => {
27 // Example: only show for h3 and h4
28 if (hashUrl) {
29 link.style.setProperty("opacity", "1", "important");
30 } else {
31 link.style.pointerEvents = "none";
32 }
33 });
34 heading.addEventListener("mouseleave", () => {
35 link.style.opacity = "0";
36 });
37 }
38 });
39
40 //............
41
42 // State management
43 let selectedItem = null;
44 const itemRefs = {};
45 const containerRef = tocContainer.querySelector(".sps-toc-container");
46 const indicatorRef = tocContainer.querySelector(".sps-toc-indicator");
47
48 // Initialize item refs
49 document.querySelectorAll(".sps-toc-item").forEach((item) => {
50 const itemId = item.getAttribute("data-item-id");
51 if (itemId) {
52 itemRefs[itemId] = item;
53 }
54 });
55
56 // Update indicator position
57 const updateIndicatorPosition = () => {
58 if (!containerRef || !indicatorRef || selectedItem === null) {
59 // Hide indicator without transition
60 if (indicatorRef) {
61 indicatorRef.classList.add("hidden");
62 }
63 return;
64 }
65
66 // Check if container is visible using the class
67 if (containerRef.classList.contains("sps-toc-hidden")) {
68 if (indicatorRef) {
69 indicatorRef.classList.add("hidden");
70 }
71 return;
72 }
73
74 const itemElement = itemRefs[selectedItem];
75 if (!itemElement) {
76 if (indicatorRef) {
77 indicatorRef.classList.add("hidden");
78 }
79 return;
80 }
81
82 const linkWrapper = itemElement.querySelector(".sps-toc-link-wrapper");
83 if (!linkWrapper) {
84 if (indicatorRef) {
85 indicatorRef.classList.add("hidden");
86 }
87 return;
88 }
89
90 // Remove hidden class to show indicator with transition
91 indicatorRef.classList.remove("hidden");
92
93 const containerRect = containerRef.getBoundingClientRect();
94 const linkWrapperRect = linkWrapper.getBoundingClientRect();
95 const relativeTop = linkWrapperRect.top - containerRect.top;
96
97 indicatorRef.style.top = `${relativeTop}px`;
98 indicatorRef.style.height = `${linkWrapperRect.height}px`;
99 };
100
101 // Update selected item background
102 const updateSelectedItemBackground = () => {
103 // Remove background from all wrappers
104 document
105 .querySelectorAll(".sps-toc-link-wrapper")
106 .forEach((wrapper) => {
107 wrapper.classList.remove("sps-toc-selected-background");
108 });
109
110 // Add background to selected item for presetTwo and presetFive
111 if (
112 ["presetTwo", "presetFive", "presetThree", "presetFour"].includes(
113 preset
114 ) &&
115 selectedItem
116 ) {
117 const itemElement = itemRefs[selectedItem];
118 if (itemElement) {
119 const linkWrapper = itemElement.querySelector(
120 ".sps-toc-link-wrapper"
121 );
122 if (linkWrapper) {
123 linkWrapper.classList.add("sps-toc-selected-background");
124 }
125 }
126 }
127 };
128
129 // Add click event listener to each link
130 tocLinks.forEach((link) => {
131 link.addEventListener("click", function (e) {
132 e.preventDefault();
133
134 // Get the target ID from the href attribute (URL encoded)
135 const encodedTargetId = this.getAttribute("href").substring(1);
136 // DECODE the URL-encoded ID to match the actual heading ID
137 const decodedTargetId = decodeURIComponent(encodedTargetId);
138
139 // Try to find the element with the decoded ID first
140 let targetElement = document.getElementById(decodedTargetId);
141
142 // If not found with decoded ID, try the encoded ID as fallback
143 if (!targetElement) {
144 targetElement = document.getElementById(encodedTargetId);
145 }
146
147 // Update selected item - use the actual ID that worked
148 if (targetElement) {
149 selectedItem = targetElement.id;
150 } else {
151 selectedItem = decodedTargetId; // Fallback to decoded ID
152 }
153
154 // Update selected item background
155 updateSelectedItemBackground();
156
157 // Update indicator position for presets 3, 4, 5
158 if (["presetThree", "presetFour", "presetFive"].includes(preset)) {
159 updateIndicatorPosition();
160 }
161
162 // Scroll to target
163 if (targetElement) {
164 const targetPosition =
165 targetElement.getBoundingClientRect().top +
166 window.pageYOffset;
167 window.scrollTo({
168 top: targetPosition - offsetTop,
169 behavior: smoothScroll ? "smooth" : "auto",
170 });
171 } else {
172 console.warn("TOC target not found:", decodedTargetId);
173 }
174 });
175 });
176
177 // Handle copy buttons
178 const copyButtons = document.querySelectorAll(".sps-toc-copy");
179 copyButtons.forEach((button) => {
180 button.addEventListener("click", function (e) {
181 e.preventDefault();
182 e.stopPropagation(); // Prevent triggering the parent link click
183
184 const textToCopy = this.getAttribute("data-clipboard-text");
185 // Create a temporary input element to copy the text
186 const tempInput = document.createElement("input");
187 tempInput.value = textToCopy;
188 document.body.appendChild(tempInput);
189 tempInput.select();
190 try {
191 // Execute the copy command
192 const successful = document.execCommand("copy");
193 if (successful) {
194 // Show success feedback
195 const originalTitle = this.getAttribute("title");
196 this.setAttribute("title", "Copied!");
197 this.classList.add("copied");
198 // Reset after 2 seconds
199 setTimeout(() => {
200 this.setAttribute("title", originalTitle);
201 this.classList.remove("copied");
202 }, 2000);
203 }
204 } catch (err) {
205 console.error("Copy error:", err);
206 }
207 // Remove the temporary input
208 document.body.removeChild(tempInput);
209 });
210 });
211
212 // Handle main toggle
213 const mainToggle = document.querySelector(".sps-toc-main-toggle");
214 if (mainToggle) {
215 mainToggle.addEventListener("click", function (e) {
216 e.preventDefault();
217 const tocInnerContainer =
218 document.querySelector(".sps-toc-container");
219 const isExpanding =
220 tocInnerContainer.classList.contains("sps-toc-hidden");
221
222 // Toggle the hidden class
223 tocInnerContainer.classList.toggle("sps-toc-hidden");
224
225 // If collapsing, hide indicator immediately without animation
226 if (!isExpanding && indicatorRef) {
227 indicatorRef.classList.add("hidden");
228 }
229
230 // Update button state
231 this.classList.toggle("expanded");
232
233 // Update aria-expanded and label
234 const isExpanded = this.classList.contains("expanded");
235 this.setAttribute("aria-expanded", isExpanded ? "true" : "false");
236
237 const buttonType = tocContainer.dataset.buttonType;
238 const collapseText = tocContainer.dataset.collapseText;
239 const expandText = tocContainer.dataset.expandText;
240 const iconSource = tocContainer.dataset.iconSource;
241
242 if (buttonType === "icon") {
243 const tocWrapper =
244 tocContainer.querySelector(".sps-toc-wrapper");
245 const iconSvgs = JSON.parse(tocWrapper.dataset.icons);
246
247 let newIcon;
248 switch (iconSource) {
249 case "one":
250 newIcon = iconSvgs.one;
251 break;
252 case "two":
253 newIcon = iconSvgs.two;
254 break;
255 case "three":
256 newIcon = iconSvgs.three;
257 break;
258 case "four":
259 newIcon = isExpanded
260 ? iconSvgs.four_expanded
261 : iconSvgs.four_collapsed;
262 break;
263 case "five":
264 newIcon = isExpanded
265 ? iconSvgs.five_expanded
266 : iconSvgs.five_collapsed;
267 break;
268 case "six":
269 newIcon = iconSvgs.six;
270 break;
271 case "seven":
272 newIcon = isExpanded
273 ? iconSvgs.seven_expanded
274 : iconSvgs.seven_collapsed;
275 break;
276 case "eight":
277 newIcon = iconSvgs.eight;
278 break;
279 default:
280 newIcon = isExpanded
281 ? iconSvgs.four_expanded
282 : iconSvgs.four_collapsed;
283 }
284
285 this.innerHTML = newIcon;
286 } else {
287 // Update text
288 this.textContent = isExpanded ? collapseText : expandText;
289 }
290
291 this.setAttribute(
292 "aria-label",
293 isExpanded ? collapseText : expandText
294 );
295
296 // Update indicator position after expanding (with delay for animation)
297 if (
298 isExpanding &&
299 ["presetThree", "presetFour", "presetFive"].includes(preset)
300 ) {
301 setTimeout(() => {
302 updateIndicatorPosition();
303 }, 350); // Slightly longer than the transition duration
304 }
305 });
306 }
307
308 // Handle item toggles
309 const itemToggles = document.querySelectorAll(".sps-toc-toggle");
310 itemToggles.forEach((toggle) => {
311 toggle.addEventListener("click", function (e) {
312 e.preventDefault();
313 e.stopPropagation();
314 const parentLi = this.closest("li.sps-toc-item");
315 const sublist = parentLi.querySelector(".sps-toc-sublist");
316 if (sublist) {
317 const isExpanding = sublist.classList.contains(
318 "sps-toc-sublist-collapsed"
319 );
320 sublist.classList.toggle("sps-toc-sublist-collapsed");
321 this.classList.toggle("expanded");
322
323 // Update icon
324 const isExpanded = this.classList.contains("expanded");
325
326 const childIconSource = tocContainer.dataset.childIconSource;
327 const tocWrapper =
328 tocContainer.querySelector(".sps-toc-wrapper");
329 const iconSvgs = JSON.parse(tocWrapper.dataset.icons);
330
331 let newIcon;
332 switch (childIconSource) {
333 case "one":
334 newIcon = iconSvgs.one;
335 break;
336 case "two":
337 newIcon = iconSvgs.two;
338 break;
339 case "three":
340 newIcon = iconSvgs.three;
341 break;
342 case "four":
343 newIcon = isExpanded
344 ? iconSvgs.four_expanded
345 : iconSvgs.four_collapsed;
346 break;
347 case "five":
348 newIcon = isExpanded
349 ? iconSvgs.five_expanded
350 : iconSvgs.five_collapsed;
351 break;
352 case "six":
353 newIcon = iconSvgs.six;
354 break;
355 case "seven":
356 newIcon = isExpanded
357 ? iconSvgs.seven_expanded
358 : iconSvgs.seven_collapsed;
359 break;
360 case "eight":
361 newIcon = iconSvgs.eight;
362 break;
363 default:
364 newIcon = isExpanded
365 ? iconSvgs.four_expanded
366 : iconSvgs.four_collapsed;
367 }
368
369 this.innerHTML = newIcon;
370
371 // Update aria-expanded and label
372 this.setAttribute(
373 "aria-expanded",
374 isExpanded ? "true" : "false"
375 );
376 this.setAttribute(
377 "aria-label",
378 isExpanded ? "Collapse section" : "Expand section"
379 );
380
381 // Update indicator position after expanding/collapsing (with delay for animation)
382 if (
383 ["presetThree", "presetFour", "presetFive"].includes(preset)
384 ) {
385 setTimeout(() => {
386 updateIndicatorPosition();
387 }, 350); // Slightly longer than the transition duration
388 }
389 }
390 });
391 });
392
393 // Initialize indicator position for presets 3, 4, 5
394 if (["presetThree", "presetFour", "presetFive"].includes(preset)) {
395 // Set initial selected item if none is set
396 if (!selectedItem && tocLinks.length > 0) {
397 const firstLink = tocLinks[0];
398 const firstItemId = firstLink.getAttribute("href").substring(1);
399 selectedItem = firstItemId;
400 updateIndicatorPosition();
401 }
402
403 // Update indicator on window resize
404 window.addEventListener("resize", () => {
405 requestAnimationFrame(updateIndicatorPosition);
406 });
407
408 // Update indicator when container size changes (using MutationObserver)
409 if (containerRef) {
410 const resizeObserver = new ResizeObserver(() => {
411 requestAnimationFrame(updateIndicatorPosition);
412 });
413
414 resizeObserver.observe(containerRef);
415 }
416 }
417
418 // Initialize selected item background for presetTwo and presetFive
419 if (["presetTwo", "presetFive"].includes(preset)) {
420 updateSelectedItemBackground();
421 }
422
423 // Copy to clipboard functionality for heading links.....................
424 function copyToClipboard(text) {
425 const textarea = document.createElement("textarea");
426 textarea.value = text;
427 textarea.style.position = "fixed";
428 textarea.style.top = 0;
429 textarea.style.left = 0;
430 textarea.style.opacity = "0";
431 document.body.appendChild(textarea);
432 textarea.focus();
433 textarea.select();
434 try {
435 document.execCommand("copy");
436 } catch (err) {
437 console.error("Failed to copy: ", err);
438 prompt("Copy this link:", text);
439 }
440 document.body.removeChild(textarea);
441 }
442
443 document.body.addEventListener("click", function (e) {
444 const target = e.target.closest(".sps-toc-heading-copy-link");
445 if (!target) {
446 return;
447 }
448 e.preventDefault();
449 e.stopPropagation();
450 const url = target.dataset.url;
451 copyToClipboard(url);
452 // Optional tooltip message
453 const msg = document.createElement("div");
454 msg.textContent = "Link copied!";
455 msg.style.position = "fixed";
456 msg.style.bottom = "20px";
457 msg.style.right = "20px";
458 msg.style.background = "#333";
459 msg.style.color = "#fff";
460 msg.style.padding = "8px 12px";
461 msg.style.borderRadius = "4px";
462 msg.style.zIndex = "99999";
463 msg.style.fontSize = "13px";
464 document.body.appendChild(msg);
465 setTimeout(() => {
466 msg.style.opacity = "0";
467 setTimeout(() => msg.remove(), 300);
468 }, 1500);
469 });
470
471 const tocContainers = document.querySelectorAll(".sp-table-of-content-toc");
472 tocContainers.forEach((container) => {
473 const floatingContainer = container.classList.contains("sp-toc-position-floating");
474 container.style.transition = "opacity 0.3s ease-in-out";
475 if ( floatingContainer ) {
476 const placeholder = document.createElement("div");
477 placeholder.style.height = container.offsetHeight + "px";
478
479 const rect = container.getBoundingClientRect();
480 const containerPosition = rect.top + window.scrollY + rect.height;
481 window.addEventListener("scroll", () => {
482 if ( window.scrollY > containerPosition) {
483 container.style.position = "fixed";
484 container.parentNode.insertBefore(placeholder, container);
485 container.style.opacity = "1";
486 } else {
487 container.style.position = "static";
488 container.style.opacity = "1";
489 placeholder.remove();
490 }
491 })
492 }
493 })
494 });
495
496
497
498