PluginProbe
Smart Post – Post Grid, Post Carousel, Post Slider Gutenberg Blocks for Blog & News / 4.0.8
Smart Post – Post Grid, Post Carousel, Post Slider Gutenberg Blocks for Blog & News v4.0.8
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 / src / blocks / table-of-content / RenderToc.js

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

387 lines 10.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { useBlockProps } from "@wordpress/block-editor";
2 import { useSelect, useDispatch } from "@wordpress/data";
3 import { useEffect, useMemo, useRef, useState, useCallback } from "@wordpress/element";
4 import { usePanelBodyContext } from "../../context";
5 import { buildNestedStructure, flattenTOC, removePluginContent } from "./helper";
6 import {
7 Icon1,
8 Icon10,
9 Icon11,
10 Icon13,
11 Icon14,
12 Icon15,
13 Icon3,
14 Icon5,
15 Icon7,
16 Icon8,
17 Icon9,
18 } from "../../icons/collapsibleIcons";
19
20 export default function TocRender({ attributes }) {
21 const {
22 tocHeading,
23 supportedHeadingTag,
24 listStyle,
25 tocAlignment,
26 listHierarchy,
27 copyLink,
28 tocCollapsed,
29 collapsedInitially,
30 childItemCollapsible,
31 CollapsibleIconSource,
32 collapseText,
33 expandText,
34 CollapsibleButtonType,
35 collapsibleColor,
36 collapsibleIconPosition,
37 childCollapsibleIconSource,
38 preset,
39 } = attributes;
40
41 const { togglePanelBody } = usePanelBodyContext();
42
43 const blockProps = useBlockProps();
44 const [headings, setHeadings] = useState([]);
45 const [expandedItems, setExpandedItems] = useState([]);
46 const [isTocExpanded, setIsTocExpanded] = useState(!collapsedInitially);
47 const [selectedItem, setSelectedItem] = useState(null);
48 const containerRef = useRef(null);
49 const indicatorRef = useRef(null);
50 const [indicatorStyle, setIndicatorStyle] = useState({ top: 0, height: 0 });
51 const itemRefs = useRef({}); // Store refs for each item
52
53 // Log when selectedItem changes
54 useEffect(() => {}, [selectedItem]);
55
56 const postContent = useSelect((select) => {
57 const content = select("core/editor").getEditedPostContent();
58 return removePluginContent(content);
59 }, []);
60
61 const dummyHeadingElements = [
62 {
63 tagName: "H2",
64 className: "wp-block-heading",
65 id: "",
66 textContent: "Dummy Introduction",
67 },
68 {
69 tagName: "H3",
70 className: "wp-block-heading",
71 id: "overview-section",
72 textContent: "Dummy Overview Section",
73 },
74 {
75 tagName: "H4",
76 className: "wp-block-heading",
77 id: "",
78 textContent: "Dummy Details",
79 },
80 {
81 tagName: "H2",
82 className: "wp-block-heading",
83 id: "",
84 textContent: "Dummy Conclusion",
85 },
86 {
87 tagName: "H3",
88 className: "wp-block-heading",
89 id: "final-thoughts",
90 textContent: "Dummy Final Thoughts",
91 },
92 ];
93
94 // Parse headings and add IDs if missing
95 useEffect(() => {
96 if (!postContent) {
97 return;
98 }
99
100 const parser = new DOMParser();
101 const doc = parser.parseFromString(postContent, "text/html");
102 const headingSelector = supportedHeadingTag?.map((tag) => tag.value).join(", ") || "h1,h2,h3,h4,h5,h6";
103
104 const headingElements = Array.from(doc.querySelectorAll(headingSelector)).filter((el) => {
105 let parent = el.parentElement;
106 while (parent) {
107 const classList = parent.classList || [];
108 // Skip headings inside unwanted blocks
109 if (classList.contains("wp-block-comments") || classList.contains("wp-block-query")) {
110 return false;
111 }
112 parent = parent.parentElement;
113 }
114 // Keep any heading that's not empty
115 return el.textContent.trim() !== "";
116 });
117
118 const nestedHeadings = buildNestedStructure(headingElements.length ? headingElements : dummyHeadingElements);
119 setHeadings(nestedHeadings);
120 }, [postContent, buildNestedStructure, setHeadings]);
121
122 // Initialize headingItems
123 const headingItems = useMemo(() => {
124 return listHierarchy ? headings : flattenTOC(headings);
125 }, [listHierarchy, headings]);
126
127 // Expand all parent items with children initially
128 useEffect(() => {
129 const parentIds = [];
130 const traverse = (items) => {
131 items.forEach((item) => {
132 if (item.children?.length > 0) {
133 parentIds.push(item.id);
134 traverse(item.children);
135 }
136 });
137 };
138 traverse(headingItems);
139 setExpandedItems(parentIds);
140 }, [headingItems]);
141
142 // ---- Indicator Logic (refactored) ----
143 const updateIndicatorPosition = useCallback(() => {
144 if (containerRef.current && indicatorRef.current && selectedItem !== null) {
145 const itemElement = itemRefs.current[selectedItem];
146
147 if (itemElement) {
148 const linkWrapper = itemElement.querySelector(".sps-toc-link-wrapper");
149
150 if (linkWrapper) {
151 const containerRect = containerRef.current.getBoundingClientRect();
152 const linkWrapperRect = linkWrapper.getBoundingClientRect();
153 const relativeTop = linkWrapperRect.top - containerRect.top;
154
155 setIndicatorStyle({
156 top: relativeTop,
157 height: linkWrapperRect.height,
158 });
159 }
160 }
161 }
162 }, [selectedItem, preset]);
163
164 // Run when selection or TOC changes
165 useEffect(() => {
166 if (["presetThree", "presetFour", "presetFive"].includes(preset)) {
167 // Add a small delay to ensure DOM has updated
168 const timer = setTimeout(() => {
169 updateIndicatorPosition();
170 }, 50);
171 return () => clearTimeout(timer);
172 }
173 }, [updateIndicatorPosition, isTocExpanded, headingItems, preset, selectedItem, expandedItems]);
174
175 // Run on window resize
176 useEffect(() => {
177 if (!["presetThree", "presetFour", "presetFive"].includes(preset)) return;
178
179 const handleResize = () => {
180 requestAnimationFrame(updateIndicatorPosition);
181 };
182
183 window.addEventListener("resize", handleResize);
184 return () => window.removeEventListener("resize", handleResize);
185 }, [updateIndicatorPosition, preset]);
186
187 // ---- TOC Behavior ----
188 const toggleItem = (itemId) => {
189 setExpandedItems((prev) => (prev.includes(itemId) ? prev.filter((id) => id !== itemId) : [...prev, itemId]));
190 };
191
192 const toggleToc = () => {
193 setIsTocExpanded(!isTocExpanded);
194 togglePanelBody("collapsible");
195 };
196
197 // Icon maps
198 const iconMap = {
199 one: Icon1,
200 two: Icon3,
201 three: Icon5,
202 four: isTocExpanded ? Icon8 : Icon7,
203 five: isTocExpanded ? Icon10 : Icon9,
204 six: Icon11,
205 seven: isTocExpanded ? Icon14 : Icon13,
206 eight: Icon15,
207 };
208
209 const TocItem = ({ item, depth = 0 }) => {
210 const isExpanded = expandedItems.includes(item.id);
211 const hasChildren = item.children?.length > 0;
212 const isSelected = selectedItem === item.id;
213
214 const childIconMap = {
215 one: Icon1,
216 two: Icon3,
217 three: Icon5,
218 four: isExpanded ? Icon8 : Icon7,
219 five: isExpanded ? Icon10 : Icon9,
220 six: Icon11,
221 seven: isExpanded ? Icon14 : Icon13,
222 eight: Icon15,
223 };
224 const ChildIconComponent = childIconMap[childCollapsibleIconSource] || null;
225
226 // Store ref for this item
227 const setItemRef = (element) => {
228 if (element) {
229 itemRefs.current[item.id] = element;
230 }
231 };
232
233 return (
234 <li
235 ref={setItemRef}
236 className={`sps-toc-item ${hasChildren ? "has-children" : ""}`}
237 data-level={item.level}
238 data-depth={depth}
239 data-item-id={item.id}
240 >
241 <div
242 className={`sps-toc-link-wrapper sps-toc-${tocAlignment} ${isSelected ? "sps-toc-selected-background" : ""}`}
243 >
244 <a
245 href={`#${item.id}`}
246 className={`sps-toc-link ${isSelected ? "sps-toc-link-selected" : ""}`}
247 onClick={(e) => {
248 e.preventDefault();
249 // Log what we're setting as selected
250
251 setSelectedItem(item.id);
252 const headingElement = document.getElementById(item.id);
253 if (headingElement) {
254 headingElement.scrollIntoView({
255 behavior: "smooth",
256 block: "start",
257 });
258 }
259 }}
260 >
261 {listStyle !== "none" && listStyle !== "bullet" ? (
262 <span className="sps-toc-number"></span>
263 ) : null}
264 <span className="sps-toc-text">{item.text}</span>
265 </a>
266
267 <div className="sps-toc-actions">
268 {copyLink && (
269 <button className="sps-toc-copy" title="Copy link" aria-label="Copy link to this section">
270 #
271 </button>
272 )}
273
274 {hasChildren && childItemCollapsible && (
275 <button
276 className="sps-toc-toggle"
277 onClick={(e) => {
278 e.preventDefault();
279 e.stopPropagation();
280 toggleItem(item.id);
281 }}
282 aria-label={isExpanded ? "Collapse section" : "Expand section"}
283 aria-expanded={isExpanded}
284 >
285 <ChildIconComponent color={collapsibleColor.color} isTocExpanded={isExpanded} />
286 </button>
287 )}
288 </div>
289 </div>
290
291 {hasChildren &&
292 (childItemCollapsible ? (
293 isExpanded && (
294 <ul className="sps-toc-sublist">
295 {item.children.map((child) => (
296 <TocItem key={child.id} item={child} depth={depth + 1} />
297 ))}
298 </ul>
299 )
300 ) : (
301 <ul className="sps-toc-sublist">
302 {item.children.map((child) => (
303 <TocItem key={child.id} item={child} depth={depth + 1} />
304 ))}
305 </ul>
306 ))}
307 </li>
308 );
309 };
310
311 if (!headingItems.length) {
312 return <p {...blockProps}>No headings found in this post for table of content.</p>;
313 }
314
315 const IconComponent = iconMap[CollapsibleIconSource] || null;
316
317 return (
318 <div {...blockProps} className="sps-toc-wrapper">
319 <div className="sps-toc-header-row">
320 <div onClick={() => togglePanelBody("heading")} className="sps-toc-header">
321 <strong className="sps-toc-title">{tocHeading}</strong>
322 {IconComponent &&
323 collapsibleIconPosition === "besideTitle" &&
324 CollapsibleButtonType === "icon" &&
325 tocCollapsed && (
326 <button
327 className="sps-toc-main-toggle"
328 onClick={toggleToc}
329 aria-label={`${isTocExpanded ? "Collapse" : "Expand"} table of contents`}
330 aria-expanded={isTocExpanded}
331 >
332 <IconComponent color={collapsibleColor.color} isTocExpanded={isTocExpanded} />
333 </button>
334 )}
335 </div>
336 {tocCollapsed && (
337 <div className="sp-main-collapse-toggle">
338 {(collapsibleIconPosition === "right" || CollapsibleButtonType !== "icon") && (
339 <button
340 className="sps-toc-main-toggle"
341 onClick={toggleToc}
342 aria-label={`${isTocExpanded ? "Collapse" : "Expand"} table of contents`}
343 aria-expanded={isTocExpanded}
344 >
345 {CollapsibleButtonType === "icon"
346 ? IconComponent && (
347 <IconComponent
348 color={collapsibleColor.color}
349 isTocExpanded={isTocExpanded}
350 />
351 )
352 : isTocExpanded
353 ? collapseText
354 : expandText}
355 </button>
356 )}
357 </div>
358 )}
359 </div>
360
361 {isTocExpanded && (
362 <div
363 onClick={() => togglePanelBody("tocBody")}
364 className={`sps-toc-container ${preset}`}
365 ref={containerRef}
366 >
367 {["presetThree", "presetFour", "presetFive"].includes(preset) && (
368 <div
369 className="sps-toc-indicator"
370 ref={indicatorRef}
371 style={{
372 top: `${indicatorStyle.top}px`,
373 height: `${indicatorStyle.height}px`,
374 }}
375 />
376 )}
377 <ul className={`sps-toc-list sps-toc-style-${listStyle}`}>
378 {headingItems.map((heading) => (
379 <TocItem key={heading.id} item={heading} depth={0} />
380 ))}
381 </ul>
382 </div>
383 )}
384 </div>
385 );
386 }
387