PageSourceSearch

https://www.24kitchen.nl/themes/kitchen/src/scripts/limethemed-js-plugins-2/ltInPageNavigation.js?tlcdnw

js 24kitchen.nl collected 2026-10-02 04:56:36 UTC 10,060 bytes, 318 lines download raw bytes

1/**
2 * @file
3 * ltInPageNavigation.
4 *
5 * Documentation, see https://kennis.limoengroen.nl/js-plugins/v2/
6 */
7
8"use strict";
9
10// Populate global object container "lt" with plugin specific object.
11lt.data.InPageNavigation = {};
12lt.data.InPageNavigation.usedId = {};
13
14// Support jQuery.
15(function ($) {
16  $.fn.ltInPageNavigation = function(settings) {
17    const ltInPageNavigationArray = [];
18    this.each(function(i) {
19      ltInPageNavigationArray[i] = new ltInPageNavigation(this, settings);
20    });
21    return ltInPageNavigationArray;
22  };
23}(jQuery));
24
25// In: 1 DOM object.
26class ltInPageNavigation {
27
28  // Special method.
29  constructor(domElement, options) {
30    // If there is no element do nothing.
31    if (!domElement) {
32      return;
33    }
34    domElement.ltInPageNavigationInstance = this;
35    this.element = domElement;
36    this.applied = false;
37
38    // Settings.
39    this.settings = lt.helpers.mergeObjects({
40      sections: 'ul > li',
41      sectionTitle: 'h2',
42      useCSS: false,
43      sectionsListType: 'ul',
44      offsetAddSectionsListHeight: false,
45      offsetExtraHeight: 0,
46      responsive: false,
47      breakpointMin: '0',
48      breakpointMax: '999999'
49    }, options);
50
51    // When there are no section-titles do nothing.
52    if (!this.element.querySelectorAll(`${this.settings.sections} ${this.settings.sectionTitle}`).length) {
53      return;
54    }
55    this.getSections();
56    this.init();
57
58    if (this.settings.responsive) {
59      const initialWidth = window.innerWidth;
60      const initialHeight = window.innerHeight;
61      const that = this;
62
63      window.addEventListener('resize', function (e) {
64        if (window.innerWidth != initialWidth || window.innerHeight != initialHeight) {
65          that.resizeTimer = setTimeout(function () {
66            that.toggleApplyDestroy()
67          }, 250);
68        }
69      });
70      window.addEventListener('orientationchange', function () {
71        that.toggleApplyDestroy();
72      });
73    }
74  }
75
76  getSections() {
77    const that = this;
78    const sections = this.element.querySelectorAll(this.settings.sections);
79    this.sections = [];
80    this.sectionTitles = [];
81    sections.forEach(function(e, i) {
82      if (e.querySelectorAll(that.settings.sectionTitle).length) {
83        that.sections[i] = e;
84        that.sectionTitles[i] = Array.prototype.slice.call(e.querySelectorAll(that.settings.sectionTitle))[0];
85      }
86    });
87  }
88
89  // Toggle method.
90  toggleApplyDestroy() {
91    const minRes = this.settings.breakpointMin;
92    const maxRes = this.settings.breakpointMax;
93
94    const browserWidth = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
95    if (browserWidth > minRes && browserWidth < maxRes) {
96      if (!this.applied) {
97        this.apply();
98      }
99    }
100    else {
101      if (this.applied) {
102        this.destroy();
103      }
104    }
105  }
106
107  // Init method.
108  init() {
109    this.toggleApplyDestroy();
110  }
111
112  // Set id.
113  setId() {
114    const that = this;
115    this.id = [];
116    this.sectionTitles.forEach(function(e, i) {
117      that.id[i] = that.uniqueHtmlId(e.textContent);
118    });
119  }
120
121  // Store navOffset.
122  initNavOffset() {
123    let navOffset = 0;
124    // Add extraHeight when set.
125    navOffset += this.settings.offsetExtraHeight;
126    // Add navOffset when set.
127    if (this.settings.offsetAddSectionsListHeight) {
128      navOffset += this.navOuter.offsetHeight;
129    }
130    this.navOffset = navOffset;
131  }
132
133  uniqueHtmlId(text) {
134    const id = lt.helpers.cleanAttrName(text);
135    if (lt.data.InPageNavigation.usedId[id]) {
136      lt.data.InPageNavigation.usedId[id].count++;
137      return `${id}-${lt.data.InPageNavigation.usedId[id].count}`;
138    }
139    else {
140      lt.data.InPageNavigation.usedId[id] = {};
141      lt.data.InPageNavigation.usedId[id].count = 1;
142      return id;
143    }
144  }
145
146  // Section.
147  modifySection() {
148    const that = this;
149    this.sections.forEach(function(e, i) {
150      e.setAttribute('id', `${that.id[i]}`);
151      e.setAttribute('tabindex', `-1`);
152    });
153  }
154  cleanUpSection() {
155    this.sections.forEach(function(e) {
156      e.removeAttribute('id');
157      e.removeAttribute('tabindex');
158    });
159    lt.data.InPageNavigation.usedId = {};
160  }
161
162  // Anchorlinks.
163  initNav() {
164    const that = this;
165    const navOuter = document.createElement('nav');
166    navOuter.classList.add('sections-list-outer');
167    navOuter.setAttribute('aria-label', Drupal.t('Jump to section'))
168    const navInner = document.createElement('div');
169    navInner.classList.add('sections-list-inner');
170    const sectionsList = document.createElement(this.settings.sectionsListType);
171    sectionsList.classList.add('sections-list');
172    this.anchorLinks = [];
173    this.sectionTitles.forEach(function(e, i) {
174      const link = document.createElement('a');
175      const listItem = document.createElement('li');
176      link.setAttribute('href', `#${that.id[i]}`);
177      link.textContent = e.textContent;
178      listItem.setAttribute('data-origin-element', e.tagName.toLowerCase());
179      if (e.classList) {
180        listItem.setAttribute('data-origin-class', e.classList);
181      }
182      listItem.insertBefore(link, listItem.firstChild);
183      sectionsList.appendChild(listItem);
184      that.anchorLinks[i] = link;
185    });
186    navInner.insertBefore(sectionsList, navInner.firstChild);
187    navOuter.insertBefore(navInner, navOuter.firstChild);
188    this.element.insertBefore(navOuter, this.element.firstChild);
189    this.navOuter = navOuter;
190  }
191  modifyNav() {
192    const that = this;
193    // Add click behavior to Anchorlinks.
194    this.anchorLinks.forEach(function(e, i) {
195      e.addEventListener('click', function(e) {
196        // Move focus to section.
197        const section = document.getElementById(that.id[i]);
198        section.focus();
199        // Check if scrolling should be done by css or js.
200        if (that.settings.useCSS) {
201          location.hash = that.id[i];
202        } else {
203          e.preventDefault();
204          let posTop = lt.helpers.getOffsetTop(section) - lt.helpers.getBodyPaddingTop() - that.navOffset;
205          // Get scrollMarginTop.
206          const supportsScrollMarginTop = 'scrollMarginTop' in document.documentElement.style;
207          if (supportsScrollMarginTop) {
208            let compStyles = window.getComputedStyle(section);
209            posTop -= parseInt(compStyles.getPropertyValue('scroll-margin-top'), 10);
210          }
211          // Scroll.
212          const supportsSmoothScroll = 'scrollBehavior' in document.documentElement.style;
213          if (supportsSmoothScroll) {
214            window.scroll({behavior: 'smooth', top: posTop});
215          } else {
216            window.scrollTo(0, posTop);
217          }
218        }
219      });
220    });
221  }
222  cleanupNav() {
223    // All markup for togglers will be removed,
224    // so no need to cleanup specific elements inside navOuter.
225    this.navOuter.parentNode.removeChild(this.navOuter);
226  }
227
228  // Determine activeSection.
229  initActiveSectionBehavior() {
230    const that = this;
231    this.activeSectionBehavior = function() {
232      that.sections.forEach(function (e, i) {
233        let top = window.scrollY;
234        // Get navOffset.
235        if (that.navOffset) {
236          top += that.navOffset;
237        }
238        // Get section values.
239        const sectionHeight = e.offsetHeight;
240        const start = lt.helpers.getOffsetTop(e) - lt.helpers.getBodyPaddingTop();
241        let end = start + sectionHeight;
242        // Add some margin between end and start.
243        end -= 2;
244        const parent = that.anchorLinks[i].parentNode;
245        // Get scrollMarginTop.
246        const supportsScrollMarginTop = 'scrollMarginTop' in document.documentElement.style;
247        if (supportsScrollMarginTop) {
248          let compStyles = window.getComputedStyle(e);
249          top += parseInt(compStyles.getPropertyValue('scroll-margin-top'), 10);
250        }
251        // When section top reaches top of window (with given extra space).
252        if (top >= start && top <= end) {
253          e.classList.add('ipn-active-section');
254          parent.classList.add('ipn-active-item');
255          // Event.
256          const setActiveEvent = new Event('ltInPageNavigationSetActive');
257          that.element.dispatchEvent(setActiveEvent);
258        }
259        else {
260          e.classList.remove('ipn-active-section');
261          parent.classList.remove('ipn-active-item');
262          // Event.
263          const unSetActiveEvent = new Event('ltInPageNavigationUnSetActive');
264          that.element.dispatchEvent(unSetActiveEvent);
265        }
266      });
267    }
268  }
269  addActiveSectionBehavior() {
270    window.addEventListener('scroll', this.activeSectionBehavior, false);
271    window.addEventListener('resize', this.activeSectionBehavior, false);
272    if (this.settings.useCSS) {
273      window.addEventListener('hashchange', this.activeSectionBehavior, false);
274    }
275  }
276  cleanupActiveSectionBehavior() {
277    window.removeEventListener('scroll', this.activeSectionBehavior, false);
278    window.removeEventListener('resize', this.activeSectionBehavior, false);
279    if (this.settings.useCSS) {
280      window.removeEventListener('hashchange', this.activeSectionBehavior, false);
281    }
282    this.sections.forEach(function (e) {
283      e.classList.remove('ipn-active-section');
284    });
285  }
286
287  // Apply method.
288  apply() {
289    this.element.setAttribute('data-lt-inpagenavigation-init', 'true');
290      // Methods.
291    this.setId();
292    this.initActiveSectionBehavior();
293    this.initNav();
294    this.initNavOffset();
295    this.addActiveSectionBehavior();
296    this.modifyNav();
297    this.modifySection();
298    // Event.
299    const applyEvent = new Event('ltInPageNavigationApply');
300    this.element.dispatchEvent(applyEvent);
301    // Status.
302    this.applied = true;
303  }
304
305  // Destroy method.
306  destroy() {
307    this.element.removeAttribute('data-lt-inpagenavigation-init');
308    // Methods.
309    this.cleanupActiveSectionBehavior();
310    this.cleanupNav();
311    this.cleanUpSection();
312    // Event.
313    const destroyEvent = new Event('ltInPageNavigationDestroy');
314    this.element.dispatchEvent(destroyEvent);
315    // Status.
316    this.applied = false;
317  }
318}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.