diff --git a/demo/dist/simple-scrollspy.min.js b/demo/dist/simple-scrollspy.min.js index 019f1fd..8effd89 100644 --- a/demo/dist/simple-scrollspy.min.js +++ b/demo/dist/simple-scrollspy.min.js @@ -1 +1 @@ -!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.scrollSpy=e():t.scrollSpy=e()}(self,(()=>(()=>{"use strict";var t={37:(t,e)=>{Object.defineProperty(e,"__esModule",{value:!0}),e.ScrollSpy=void 0;e.ScrollSpy=class{menuList;options;scroller;sections;activeItem=null;constructor(t,e={}){if(!t)throw new Error("Your navigation query selector is the first argument.");if("object"!=typeof e)throw new Error("The second argument must be an instance of an Object.");e.smoothScroll=!0===e.smoothScroll&&{}||e.smoothScroll,this.menuList=t instanceof HTMLElement?t:document.querySelector(t),this.options=Object.assign({},{sectionClass:".scrollspy",menuActiveTarget:"li > a",offset:0,hrefAttribute:"href",activeClass:"active",scrollContainer:"",smoothScroll:{}},e),this.options.scrollContainer?this.scroller=this.options.scrollContainer instanceof HTMLElement?this.options.scrollContainer:document.querySelector(this.options.scrollContainer):this.scroller=window,this.sections=document.querySelectorAll(this.options.sectionClass),this.attachEventListeners()}attachEventListeners(){if(this.scroller&&(this.scroller.addEventListener("scroll",(()=>this.onScroll())),this.options.smoothScroll&&this.menuList)){this.menuList.querySelectorAll(this.options.menuActiveTarget).forEach((t=>t.addEventListener("click",this.onClick.bind(this))))}}onClick(t){if(t.target){const e=t.target.getAttribute(this.options.hrefAttribute);if(e){const o=document.querySelector(e);o&&this.options.smoothScroll&&(t.preventDefault(),this.scrollTo(o))}}}onScroll(){const t=this.getSectionInView(),e=this.getMenuItemBySection(t);if(!t||!e)return this.removeCurrentActive();const o=e?.getAttribute(this.options.hrefAttribute),s=this.activeItem?.getAttribute(this.options.hrefAttribute);e&&o&&o!==s&&(this.removeCurrentActive({ignore:e}),this.setActive(e))}scrollTo(t){const e="function"==typeof this.options.smoothScrollBehavior&&this.options.smoothScrollBehavior;e?e(t,this.options.smoothScroll):t.scrollIntoView({...!0===this.options.smoothScroll?{}:this.options.smoothScroll,behavior:"smooth"})}getMenuItemBySection(t){if(!t||!this.menuList)return;const e=t.getAttribute("id");return this.menuList.querySelector(`[${this.options.hrefAttribute}="#${e}"]`)}getSectionInView(){for(let t=0;te&&s<=o)return this.sections[t]}}setActive(t){this.activeItem=t;this.activeItem.classList.contains(this.options.activeClass)||(this.activeItem.classList.add(this.options.activeClass),"function"==typeof this.options.onActive&&this.options.onActive(this.activeItem))}removeCurrentActive({ignore:t}={}){if(this.activeItem)this.activeItem.classList.remove(this.options.activeClass),this.activeItem=null;else if(this.menuList){const{hrefAttribute:e,menuActiveTarget:o,activeClass:s}=this.options,i=t?`${o}.${s}:not([${e}="${t.getAttribute(e)}"])`:`${o}.${s}`;this.menuList.querySelectorAll(i).forEach((t=>t.classList.remove(this.options.activeClass)))}}}}},e={};function o(s){var i=e[s];if(void 0!==i)return i.exports;var r=e[s]={exports:{}};return t[s](r,r.exports,o),r.exports}var s={};return(()=>{var t=s;const e=o(37);t.default=(t,o={})=>{const s=new e.ScrollSpy(t,o);return s.onScroll(),window.addEventListener("scroll",(()=>s.onScroll())),s}})(),s=s.default})())); \ No newline at end of file +!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.scrollSpy=e():t.scrollSpy=e()}(self,(()=>(()=>{"use strict";var t={37:(t,e)=>{Object.defineProperty(e,"__esModule",{value:!0}),e.ScrollSpy=void 0;e.ScrollSpy=class{menuList;options;scroller;sections;activeItem=null;constructor(t,e={}){if(!t)throw new Error("Your navigation query selector is the first argument.");if("object"!=typeof e)throw new Error("The second argument must be an instance of an Object.");e.smoothScroll=!0===e.smoothScroll&&{}||e.smoothScroll,this.menuList=t instanceof HTMLElement?t:document.querySelector(t),this.options=Object.assign({},{sectionClass:".scrollspy",menuActiveTarget:"li > a",offset:0,hrefAttribute:"href",activeClass:"active",scrollContainer:"",smoothScroll:{}},e),this.options.scrollContainer?this.scroller=this.options.scrollContainer instanceof HTMLElement?this.options.scrollContainer:document.querySelector(this.options.scrollContainer):this.scroller=window,this.sections=document.querySelectorAll(this.options.sectionClass),this.attachEventListeners()}attachEventListeners(){if(this.scroller&&(this.scroller.addEventListener("scroll",(()=>this.onScroll())),this.options.smoothScroll&&this.menuList)){this.menuList.querySelectorAll(this.options.menuActiveTarget).forEach((t=>t.addEventListener("click",this.onClick.bind(this))))}}onClick(t){if(t.target){const e=t.target.getAttribute(this.options.hrefAttribute);if(e){const o=document.querySelector(e);o&&this.options.smoothScroll&&(t.preventDefault(),this.scrollTo(o))}}}onScroll(){const t=this.getSectionInView(),e=this.getMenuItemBySection(t);if(!t||!e)return;const o=e?.getAttribute(this.options.hrefAttribute),s=this.activeItem?.getAttribute(this.options.hrefAttribute);e&&o&&o!==s&&(this.removeCurrentActive({ignore:e}),this.setActive(e))}scrollTo(t){const e="function"==typeof this.options.smoothScrollBehavior&&this.options.smoothScrollBehavior;e?e(t,this.options.smoothScroll):t.scrollIntoView({...!0===this.options.smoothScroll?{}:this.options.smoothScroll,behavior:"smooth"})}getMenuItemBySection(t){if(!t||!this.menuList)return;const e=t.getAttribute("id");return this.menuList.querySelector(`[${this.options.hrefAttribute}="#${e}"]`)}getSectionInView(){for(let t=0;te&&s<=o)return this.sections[t]}}setActive(t){this.activeItem=t;this.activeItem.classList.contains(this.options.activeClass)||(this.activeItem.classList.add(this.options.activeClass),"function"==typeof this.options.onActive&&this.options.onActive(this.activeItem))}removeCurrentActive({ignore:t}={}){if(this.activeItem)this.activeItem.classList.remove(this.options.activeClass),this.activeItem=null;else if(this.menuList){const{hrefAttribute:e,menuActiveTarget:o,activeClass:s}=this.options,i=t?`${o}.${s}:not([${e}="${t.getAttribute(e)}"])`:`${o}.${s}`;this.menuList.querySelectorAll(i).forEach((t=>t.classList.remove(this.options.activeClass)))}}}}},e={};function o(s){var i=e[s];if(void 0!==i)return i.exports;var r=e[s]={exports:{}};return t[s](r,r.exports,o),r.exports}var s={};return(()=>{var t=s;const e=o(37);t.default=(t,o={})=>{const s=new e.ScrollSpy(t,o);return s.onScroll(),window.addEventListener("scroll",(()=>s.onScroll())),s}})(),s=s.default})())); \ No newline at end of file diff --git a/demo/toc.html b/demo/toc.html index eda9a68..5efa6ff 100644 --- a/demo/toc.html +++ b/demo/toc.html @@ -190,8 +190,11 @@
-
-

+
+

Section 1: Lorem ipsum dolor sit amet consectetur.

@@ -218,8 +221,11 @@

-
-

+
+

Section 2: Lorem, ipsum dolor.

@@ -244,8 +250,11 @@

-
-

+
+

Section 3: Lorem ipsum dolor sit amet consectetur.

@@ -258,8 +267,11 @@

-
-

+
+

Section 4: Lorem ipsum dolor sit amet consectetur.

@@ -286,8 +298,11 @@

-
-

+
+

Section 5: Lorem, ipsum dolor.

@@ -312,8 +327,11 @@

-
-

+
+

Section 6: Lorem ipsum dolor sit amet consectetur.

@@ -443,7 +461,7 @@

scrollSpy("#toc-menu", { sectionClass: ".scrollspy", menuActiveTarget: "li > a.block", - offset: 100, + offset: 35, // scrollContainer: null, // smooth scroll smoothScroll: true, diff --git a/src/scrollspy.ts b/src/scrollspy.ts index 7e9aa0a..ca85938 100644 --- a/src/scrollspy.ts +++ b/src/scrollspy.ts @@ -8,8 +8,11 @@ export type Options = { hrefAttribute: string; activeClass: string; scrollContainer: string | HTMLElement; - smoothScroll: SmoothScroll, - smoothScrollBehavior?: (targetElement: HTMLElement, smoothScrollOptions: SmoothScroll) => void; + smoothScroll: SmoothScroll; + smoothScrollBehavior?: ( + targetElement: HTMLElement, + smoothScrollOptions: SmoothScroll + ) => void; onActive?: (activeItem: HTMLElement) => void; }; @@ -23,142 +26,174 @@ export class ScrollSpy { constructor(menu: MenuElement, options: Partial = {}) { if (!menu) { - throw new Error('Your navigation query selector is the first argument.') + throw new Error("Your navigation query selector is the first argument."); } - if (typeof options !== 'object') { - throw new Error('The second argument must be an instance of an Object.') + if (typeof options !== "object") { + throw new Error("The second argument must be an instance of an Object."); } let defaultOptions = { - sectionClass: '.scrollspy', - menuActiveTarget: 'li > a', + sectionClass: ".scrollspy", + menuActiveTarget: "li > a", offset: 0, - hrefAttribute: 'href', - activeClass: 'active', - scrollContainer: '', + hrefAttribute: "href", + activeClass: "active", + scrollContainer: "", smoothScroll: {}, - } + }; - options.smoothScroll = options.smoothScroll === true && {} || options.smoothScroll + options.smoothScroll = + (options.smoothScroll === true && {}) || options.smoothScroll; - this.menuList = menu instanceof HTMLElement ? menu : document.querySelector(menu) - this.options = Object.assign({}, defaultOptions, options) + this.menuList = + menu instanceof HTMLElement ? menu : document.querySelector(menu); + this.options = Object.assign({}, defaultOptions, options); if (this.options.scrollContainer) { - this.scroller = this.options.scrollContainer instanceof HTMLElement ? this.options.scrollContainer : document.querySelector(this.options.scrollContainer) + this.scroller = + this.options.scrollContainer instanceof HTMLElement + ? this.options.scrollContainer + : document.querySelector(this.options.scrollContainer); } else { - this.scroller = window + this.scroller = window; } - this.sections = document.querySelectorAll(this.options.sectionClass) + this.sections = document.querySelectorAll( + this.options.sectionClass + ); - this.attachEventListeners() + this.attachEventListeners(); } attachEventListeners() { if (this.scroller) { - this.scroller.addEventListener('scroll', () => this.onScroll()) + this.scroller.addEventListener("scroll", () => this.onScroll()); // smooth scroll if (this.options.smoothScroll && this.menuList) { - const menuItems = this.menuList.querySelectorAll(this.options.menuActiveTarget) - menuItems.forEach((item) => item.addEventListener('click', this.onClick.bind(this))) + const menuItems = this.menuList.querySelectorAll( + this.options.menuActiveTarget + ); + menuItems.forEach((item) => + item.addEventListener("click", this.onClick.bind(this)) + ); } } } onClick(event: MouseEvent) { if (event.target) { - const targetSelector = (event.target as HTMLElement).getAttribute(this.options.hrefAttribute) + const targetSelector = (event.target as HTMLElement).getAttribute( + this.options.hrefAttribute + ); if (targetSelector) { - const targetElement = document.querySelector(targetSelector) + const targetElement = + document.querySelector(targetSelector); if (targetElement && this.options.smoothScroll) { // prevent click event - event.preventDefault() + event.preventDefault(); // handle smooth scrolling to 'targetElement' - this.scrollTo(targetElement) + this.scrollTo(targetElement); } } } } onScroll() { - const section = this.getSectionInView() - const menuItem = this.getMenuItemBySection(section) + const section = this.getSectionInView(); + const menuItem = this.getMenuItemBySection(section); if (!section || !menuItem) { - return this.removeCurrentActive() + return; } - const nextItemId = menuItem?.getAttribute(this.options.hrefAttribute) - const currentItemId = this.activeItem?.getAttribute(this.options.hrefAttribute) + const nextItemId = menuItem?.getAttribute(this.options.hrefAttribute); + const currentItemId = this.activeItem?.getAttribute( + this.options.hrefAttribute + ); if (menuItem && nextItemId && nextItemId !== currentItemId) { - this.removeCurrentActive({ ignore: menuItem }) - this.setActive(menuItem) + this.removeCurrentActive({ ignore: menuItem }); + this.setActive(menuItem); } } scrollTo(targetElement: HTMLElement) { - const smoothScrollBehavior = typeof this.options.smoothScrollBehavior === 'function' && this.options.smoothScrollBehavior + const smoothScrollBehavior = + typeof this.options.smoothScrollBehavior === "function" && + this.options.smoothScrollBehavior; if (smoothScrollBehavior) { - smoothScrollBehavior(targetElement, this.options.smoothScroll) + smoothScrollBehavior(targetElement, this.options.smoothScroll); } else { targetElement.scrollIntoView({ - ...(this.options.smoothScroll === true ? {} : this.options.smoothScroll), - behavior: 'smooth', - }) + ...(this.options.smoothScroll === true + ? {} + : this.options.smoothScroll), + behavior: "smooth", + }); } } getMenuItemBySection(section?: HTMLElement) { - if (!section || !this.menuList) return - const sectionId = section.getAttribute('id') - return this.menuList.querySelector(`[${this.options.hrefAttribute}="#${sectionId}"]`) + if (!section || !this.menuList) return; + const sectionId = section.getAttribute("id"); + return this.menuList.querySelector( + `[${this.options.hrefAttribute}="#${sectionId}"]` + ); } getSectionInView() { for (let i = 0; i < this.sections.length; i++) { - const startAt = this.sections[i]!.offsetTop - const endAt = startAt + this.sections[i]!.offsetHeight - let currentPosition = (document.documentElement.scrollTop || document.body.scrollTop) + this.options.offset + const startAt = this.sections[i]!.offsetTop; + const endAt = startAt + this.sections[i]!.offsetHeight; + let currentPosition = + (document.documentElement.scrollTop || document.body.scrollTop) + + this.options.offset; if (this.options.scrollContainer && this.scroller) { - const scrollTop = this.scroller instanceof Window ? this.scroller.scrollY : this.scroller.scrollTop; - currentPosition = (scrollTop) + this.options.offset + const scrollTop = + this.scroller instanceof Window + ? this.scroller.scrollY + : this.scroller.scrollTop; + currentPosition = scrollTop + this.options.offset; } - const isInView = currentPosition > startAt && currentPosition <= endAt - if (isInView) return this.sections[i] + const isInView = currentPosition > startAt && currentPosition <= endAt; + if (isInView) return this.sections[i]; } } setActive(menuItem: HTMLElement) { - this.activeItem = menuItem - const isActive = this.activeItem.classList.contains(this.options.activeClass) + this.activeItem = menuItem; + const isActive = this.activeItem.classList.contains( + this.options.activeClass + ); if (!isActive) { - this.activeItem.classList.add(this.options.activeClass) - if (typeof this.options.onActive === 'function') { - this.options.onActive(this.activeItem) + this.activeItem.classList.add(this.options.activeClass); + if (typeof this.options.onActive === "function") { + this.options.onActive(this.activeItem); } } } removeCurrentActive({ ignore }: { ignore?: HTMLElement } = {}) { if (this.activeItem) { - this.activeItem.classList.remove(this.options.activeClass) - this.activeItem = null + this.activeItem.classList.remove(this.options.activeClass); + this.activeItem = null; } else if (this.menuList) { - const { hrefAttribute, menuActiveTarget, activeClass } = this.options + const { hrefAttribute, menuActiveTarget, activeClass } = this.options; const itemSelector = ignore - ? `${menuActiveTarget}.${activeClass}:not([${hrefAttribute}="${ignore.getAttribute(hrefAttribute)}"])` - : `${menuActiveTarget}.${activeClass}` - - this.menuList.querySelectorAll(itemSelector) - .forEach((item) => item.classList.remove(this.options.activeClass)) + ? `${menuActiveTarget}.${activeClass}:not([${hrefAttribute}="${ignore.getAttribute( + hrefAttribute + )}"])` + : `${menuActiveTarget}.${activeClass}`; + + this.menuList + .querySelectorAll(itemSelector) + .forEach((item) => item.classList.remove(this.options.activeClass)); } } }