Skip to content

Commit c49d4db

Browse files
update version
1 parent 1c56e46 commit c49d4db

File tree

7 files changed

+380
-840
lines changed

7 files changed

+380
-840
lines changed
Lines changed: 373 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,373 @@
1+
( function( $ ) {
2+
/**
3+
* @param $scope The Widget wrapper element as a jQuery element
4+
* @param $ The jQuery alias
5+
*/
6+
7+
$(window).on("elementor/frontend/init", function () {
8+
const Modules = elementorModules.frontend.handlers.Base;
9+
//table of content
10+
const table_of_content = Modules.extend({
11+
getDefaultSettings: function getDefaultSettings() {
12+
const elementSettings = this.getElementSettings(),
13+
listWrapperTag =
14+
"numbers" === elementSettings.marker_view ? "ol" : "ul";
15+
return {
16+
selectors: {
17+
widgetContainer: ".elementor-widget-container",
18+
postContentContainer:
19+
'.elementor:not([data-elementor-type="header"]):not([data-elementor-type="footer"]):not([data-elementor-type="popup"])',
20+
expandButton: ".toc__toggle-button--expand",
21+
collapseButton: ".toc__toggle-button--collapse",
22+
body: ".toc__body",
23+
headerTitle: ".toc__header-title",
24+
},
25+
classes: {
26+
anchor: "elementor-menu-anchor",
27+
listWrapper: "toc__list-wrapper",
28+
listItem: "toc__list-item",
29+
listTextWrapper: "toc__list-item-text-wrapper",
30+
firstLevelListItem: "toc__top-level",
31+
listItemText: "toc__list-item-text",
32+
activeItem: "elementor-item-active",
33+
headingAnchor: "toc__heading-anchor",
34+
collapsed: "toc--collapsed",
35+
},
36+
listWrapperTag,
37+
};
38+
},
39+
getDefaultElements: function getDefaultElements() {
40+
const settings = this.getSettings();
41+
return {
42+
$pageContainer: this.getContainer(),
43+
$widgetContainer: this.$element.find(
44+
settings.selectors.widgetContainer
45+
),
46+
$expandButton: this.$element.find(settings.selectors.expandButton),
47+
$collapseButton: this.$element.find(
48+
settings.selectors.collapseButton
49+
),
50+
$tocBody: this.$element.find(settings.selectors.body),
51+
$listItems: this.$element.find("." + settings.classes.listItem),
52+
};
53+
},
54+
getContainer: function getContainer() {
55+
const elementSettings = this.getElementSettings();
56+
57+
// If there is a custom container defined by the user, use it as the headings-scan container
58+
if (elementSettings.container) {
59+
return jQuery(elementSettings.container);
60+
}
61+
62+
// Get the document wrapper element in which the TOC is located
63+
const $documentWrapper = this.$element.parents(".elementor");
64+
65+
// If the TOC container is a popup, only scan the popup for headings
66+
if ("popup" === $documentWrapper.attr("data-elementor-type")) {
67+
return $documentWrapper;
68+
}
69+
70+
// If the TOC container is anything other than a popup, scan only the post/page content for headings
71+
const settings = this.getSettings();
72+
return jQuery(settings.selectors.postContentContainer);
73+
},
74+
getHeadings: function () {
75+
// Get all headings from document by user-selected tags
76+
const elementSettings = this.getElementSettings(),
77+
tags = elementSettings.headings_by_tags.join(","),
78+
selectors = this.getSettings("selectors"),
79+
excludedSelectors = elementSettings.exclude_headings_by_selector;
80+
return this.elements.$pageContainer
81+
.find(tags)
82+
.not(selectors.headerTitle)
83+
.filter((index, heading) => {
84+
if(typeof ScrollTrigger === 'object') {
85+
86+
ScrollTrigger.create({
87+
trigger: heading,
88+
start: "top center",
89+
end: "bottom center",
90+
onEnter: () => this.setActiveLink(heading.previousSibling.id),
91+
onLeaveBack: () => this.setActiveLink(heading.previousSibling.id),
92+
});
93+
}
94+
95+
return !jQuery(heading).closest(excludedSelectors).length; // Handle excluded selectors if there are any
96+
});
97+
},
98+
setActiveLink: function (id) {
99+
for (const element of this.headingsData) {
100+
let link = document.querySelector(`[href="#${element.anchorLink}"]`);
101+
link.classList.toggle(
102+
"elementor-item-active",
103+
link.getAttribute("href") === `#${id}`
104+
);
105+
}
106+
},
107+
handleNoHeadingsFound: function () {
108+
const noHeadingsText = "No headings were found on this page.";
109+
return this.elements.$tocBody.html(noHeadingsText);
110+
},
111+
getHeadingAnchorLink: function (index, classes) {
112+
const headingID = this.elements.$headings[index].id,
113+
wrapperID =
114+
this.elements.$headings[index].closest(".elementor-widget").id;
115+
let anchorLink = "";
116+
if (headingID) {
117+
anchorLink = headingID;
118+
} else if (wrapperID) {
119+
// If the heading itself has an ID, we don't want to overwrite it
120+
anchorLink = wrapperID;
121+
}
122+
123+
// If there is no existing ID, use the heading text to create a semantic ID
124+
if (headingID || wrapperID) {
125+
jQuery(this.elements.$headings[index]).data("hasOwnID", true);
126+
} else {
127+
anchorLink = `${classes.headingAnchor}-${index}`;
128+
}
129+
return anchorLink;
130+
},
131+
setHeadingsData: function () {
132+
this.headingsData = [];
133+
const classes = this.getSettings("classes");
134+
135+
// Create an array for simplifying TOC list creation
136+
this.elements.$headings.each((index, element) => {
137+
const anchorLink = this.getHeadingAnchorLink(index, classes);
138+
this.headingsData.push({
139+
tag: +element.nodeName.slice(1),
140+
text: element.textContent,
141+
anchorLink,
142+
});
143+
});
144+
},
145+
addAnchorsBeforeHeadings: function () {
146+
const classes = this.getSettings("classes");
147+
148+
// Add an anchor element right before each TOC heading to create anchors for TOC links
149+
this.elements.$headings.before((index) => {
150+
// Check if the heading element itself has an ID, or if it is a widget which includes a main heading element, whether the widget wrapper has an ID
151+
if (jQuery(this.elements.$headings[index]).data("hasOwnID")) {
152+
return;
153+
}
154+
return `<span id="${classes.headingAnchor}-${index}" class="${classes.anchor} "></span>`;
155+
});
156+
},
157+
158+
followAnchors: function () {
159+
this.$listItemTexts = this.$element.find(".toc__list-item-text");
160+
gsap.registerPlugin(ScrollToPlugin);
161+
this.$listItemTexts.toArray().forEach((link) => {
162+
link.addEventListener("click", (e) => {
163+
e.preventDefault();
164+
const targetId = link.getAttribute("href");
165+
gsap.to(window, {
166+
duration: 0.6,
167+
scrollTo: targetId,
168+
ease: "power2.inOut",
169+
});
170+
});
171+
});
172+
},
173+
populateTOC: function () {
174+
this.listItemPointer = 0;
175+
const elementSettings = this.getElementSettings();
176+
if (elementSettings.hierarchical_view) {
177+
this.createNestedList();
178+
} else {
179+
this.createFlatList();
180+
}
181+
182+
if (!elementorFrontend.isEditMode()) {
183+
this.followAnchors();
184+
}
185+
},
186+
createNestedList: function () {
187+
this.headingsData.forEach((heading, index) => {
188+
heading.level = 0;
189+
for (let i = index - 1; i >= 0; i--) {
190+
const currentOrderedItem = this.headingsData[i];
191+
if (currentOrderedItem.tag <= heading.tag) {
192+
heading.level = currentOrderedItem.level;
193+
if (currentOrderedItem.tag < heading.tag) {
194+
heading.level++;
195+
}
196+
break;
197+
}
198+
}
199+
});
200+
this.elements.$tocBody.html(this.getNestedLevel(0));
201+
},
202+
createFlatList: function () {
203+
this.elements.$tocBody.html(this.getNestedLevel());
204+
},
205+
getNestedLevel: function (level) {
206+
const settings = this.getSettings(),
207+
elementSettings = this.getElementSettings(),
208+
icon = this.getElementSettings("icon");
209+
let renderedIcon;
210+
if (icon) {
211+
// We generate the icon markup in PHP and make it available via get_frontend_settings(). As a result, the
212+
// rendered icon is not available in the editor, so in the editor we use the regular <i> tag.
213+
if (
214+
elementorFrontend.config.experimentalFeatures.e_font_icon_svg &&
215+
!elementorFrontend.isEditMode()
216+
) {
217+
renderedIcon = icon.rendered_tag;
218+
} else {
219+
renderedIcon = `<i class="${icon.value}"></i>`;
220+
}
221+
}
222+
223+
// Open new list/nested list
224+
let html = `<${settings.listWrapperTag} class="${settings.classes.listWrapper}">`;
225+
226+
// For each list item, build its markup.
227+
while (this.listItemPointer < this.headingsData.length) {
228+
const currentItem = this.headingsData[this.listItemPointer];
229+
let listItemTextClasses = settings.classes.listItemText;
230+
if (0 === currentItem.level) {
231+
// If the current list item is a top level item, give it the first level class
232+
listItemTextClasses += " " + settings.classes.firstLevelListItem;
233+
}
234+
if (level > currentItem.level) {
235+
break;
236+
}
237+
if (level === currentItem.level) {
238+
html += `<li class="${settings.classes.listItem}">`;
239+
html += `<div class="${settings.classes.listTextWrapper}">`;
240+
let liContent = `<a href="#${currentItem.anchorLink}" class="${listItemTextClasses}">${currentItem.text}</a>`;
241+
242+
// If list type is bullets, add the bullet icon as an <i> tag
243+
if ("bullets" === elementSettings.marker_view && icon) {
244+
liContent = `${renderedIcon}${liContent}`;
245+
}
246+
html += liContent;
247+
html += "</div>";
248+
this.listItemPointer++;
249+
const nextItem = this.headingsData[this.listItemPointer];
250+
if (nextItem && level < nextItem.level) {
251+
// If a new nested list has to be created under the current item,
252+
// this entire method is called recursively (outside the while loop, a list wrapper is created)
253+
html += this.getNestedLevel(nextItem.level);
254+
}
255+
html += "</li>";
256+
}
257+
}
258+
html += `</${settings.listWrapperTag}>`;
259+
return html;
260+
},
261+
run: function run() {
262+
this.elements.$headings = this.getHeadings();
263+
if (!this.elements.$headings.length) {
264+
return this.handleNoHeadingsFound();
265+
}
266+
this.setHeadingsData();
267+
if (!elementorFrontend.isEditMode()) {
268+
this.addAnchorsBeforeHeadings();
269+
}
270+
this.populateTOC();
271+
272+
if (this.getElementSettings("minimize_box")) {
273+
this.collapseBodyListener();
274+
}
275+
},
276+
bindEvents: function bindEvents() {
277+
this.viewportItems = [];
278+
this.run();
279+
280+
const elementSettings = this.getElementSettings();
281+
if (elementSettings.minimize_box) {
282+
this.elements.$expandButton
283+
.on("click", () => this.expandBox())
284+
.on("keyup", (event) => this.triggerClickOnEnterSpace(event));
285+
this.elements.$collapseButton
286+
.on("click", () => this.collapseBox())
287+
.on("keyup", (event) => this.triggerClickOnEnterSpace(event));
288+
}
289+
if (elementSettings.collapse_subitems) {
290+
this.elements.$listItems.on("hover", (event) =>
291+
jQuery(event.target).slideToggle()
292+
);
293+
}
294+
},
295+
296+
expandBox: function () {
297+
let changeFocus =
298+
arguments.length > 0 && arguments[0] !== undefined
299+
? arguments[0]
300+
: true;
301+
const boxHeight = this.getCurrentDeviceSetting("min_height");
302+
this.$element.removeClass(this.getSettings("classes.collapsed"));
303+
this.elements.$tocBody.attr("aria-expanded", "true").slideDown();
304+
305+
// Return container to the full height in case a min-height is defined by the user
306+
this.elements.$widgetContainer.css(
307+
"min-height",
308+
boxHeight.size + boxHeight.unit
309+
);
310+
if (changeFocus) {
311+
this.elements.$collapseButton.trigger("focus");
312+
}
313+
},
314+
collapseBox: function () {
315+
let changeFocus =
316+
arguments.length > 0 && arguments[0] !== undefined
317+
? arguments[0]
318+
: true;
319+
this.$element.addClass(this.getSettings("classes.collapsed"));
320+
this.elements.$tocBody.attr("aria-expanded", "false").slideUp();
321+
322+
// Close container in case a min-height is defined by the user
323+
this.elements.$widgetContainer.css("min-height", "0px");
324+
if (changeFocus) {
325+
this.elements.$expandButton.trigger("focus");
326+
}
327+
},
328+
triggerClickOnEnterSpace: function (event) {
329+
const ENTER_KEY = 13,
330+
SPACE_KEY = 32;
331+
if (ENTER_KEY === event.keyCode || SPACE_KEY === event.keyCode) {
332+
event.currentTarget.click();
333+
event.stopPropagation();
334+
}
335+
},
336+
collapseBodyListener: function () {
337+
const activeBreakpoints =
338+
elementorFrontend.breakpoints.getActiveBreakpointsList({
339+
withDesktop: true,
340+
});
341+
const minimizedOn = this.getElementSettings("minimized_on"),
342+
currentDeviceMode = elementorFrontend.getCurrentDeviceMode(),
343+
isCollapsed = this.$element.hasClass(
344+
this.getSettings("classes.collapsed")
345+
);
346+
347+
// If minimizedOn value is set to desktop, it applies for widescreen as well.
348+
if (
349+
"desktop" === minimizedOn ||
350+
activeBreakpoints.indexOf(minimizedOn) >=
351+
activeBreakpoints.indexOf(currentDeviceMode)
352+
) {
353+
if (!isCollapsed) {
354+
this.collapseBox(false);
355+
}
356+
} else if (isCollapsed) {
357+
this.expandBox(false);
358+
}
359+
},
360+
});
361+
362+
elementorFrontend.hooks.addAction(
363+
"frontend/element_ready/wcf--table-of-contents.default",
364+
function ($scope) {
365+
elementorFrontend.elementsHandler.addHandler(table_of_content, {
366+
$element: $scope,
367+
});
368+
}
369+
);
370+
371+
});
372+
373+
} )( jQuery );

0 commit comments

Comments
 (0)