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