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.