Skip to content

Commit

Permalink
Merge pull request #61 from kimyvgy/fix
Browse files Browse the repository at this point in the history
fix: v2.5.0 breaks implementation with headings
  • Loading branch information
kimyvgy authored Sep 18, 2024
2 parents 8f58287 + 48cd9a2 commit ed0704e
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 74 deletions.
2 changes: 1 addition & 1 deletion demo/dist/simple-scrollspy.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

44 changes: 31 additions & 13 deletions demo/toc.html
Original file line number Diff line number Diff line change
Expand Up @@ -190,8 +190,11 @@
<article
class="col-span-9 prose prose-stone max-w-screen-md mx-auto mb-40"
>
<section class="scrollspy" id="section-1">
<h2 class="font-bold text-primary-700 my-4 text-xl">
<section>
<h2
id="section-1"
class="scrollspy font-bold text-primary-700 my-4 text-xl"
>
Section 1: Lorem ipsum dolor sit amet consectetur.
</h2>

Expand All @@ -218,8 +221,11 @@ <h2 class="font-bold text-primary-700 my-4 text-xl">
</p>
</section>

<section class="scrollspy" id="section-2">
<h2 class="font-bold text-primary-700 my-4 text-xl">
<section>
<h2
id="section-2"
class="scrollspy font-bold text-primary-700 my-4 text-xl"
>
Section 2: Lorem, ipsum dolor.
</h2>

Expand All @@ -244,8 +250,11 @@ <h2 class="font-bold text-primary-700 my-4 text-xl">
</p>
</section>

<section class="scrollspy" id="section-3">
<h2 class="font-bold text-primary-700 my-4 text-xl">
<section>
<h2
id="section-3"
class="scrollspy font-bold text-primary-700 my-4 text-xl"
>
Section 3: Lorem ipsum dolor sit amet consectetur.
</h2>

Expand All @@ -258,8 +267,11 @@ <h2 class="font-bold text-primary-700 my-4 text-xl">
</p>
</section>

<section class="scrollspy" id="section-4">
<h2 class="font-bold text-primary-700 my-4 text-xl">
<section>
<h2
id="section-4"
class="scrollspy font-bold text-primary-700 my-4 text-xl"
>
Section 4: Lorem ipsum dolor sit amet consectetur.
</h2>

Expand All @@ -286,8 +298,11 @@ <h2 class="font-bold text-primary-700 my-4 text-xl">
</p>
</section>

<section class="scrollspy" id="section-5">
<h2 class="font-bold text-primary-700 my-4 text-xl">
<section>
<h2
id="section-5"
class="scrollspy font-bold text-primary-700 my-4 text-xl"
>
Section 5: Lorem, ipsum dolor.
</h2>

Expand All @@ -312,8 +327,11 @@ <h2 class="font-bold text-primary-700 my-4 text-xl">
</p>
</section>

<section class="scrollspy" id="section-6">
<h2 class="font-bold text-primary-700 my-4 text-xl">
<section>
<h2
id="section-6"
class="scrollspy font-bold text-primary-700 my-4 text-xl"
>
Section 6: Lorem ipsum dolor sit amet consectetur.
</h2>

Expand Down Expand Up @@ -443,7 +461,7 @@ <h2 class="font-bold text-primary-700 my-4 text-xl">
scrollSpy("#toc-menu", {
sectionClass: ".scrollspy",
menuActiveTarget: "li > a.block",
offset: 100,
offset: 35,
// scrollContainer: null,
// smooth scroll
smoothScroll: true,
Expand Down
155 changes: 95 additions & 60 deletions src/scrollspy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

Expand All @@ -23,142 +26,174 @@ export class ScrollSpy {

constructor(menu: MenuElement, options: Partial<Options> = {}) {
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<HTMLElement>(this.options.scrollContainer)
this.scroller =
this.options.scrollContainer instanceof HTMLElement
? this.options.scrollContainer
: document.querySelector<HTMLElement>(this.options.scrollContainer);
} else {
this.scroller = window
this.scroller = window;
}

this.sections = document.querySelectorAll<HTMLElement>(this.options.sectionClass)
this.sections = document.querySelectorAll<HTMLElement>(
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<HTMLElement>(this.options.menuActiveTarget)
menuItems.forEach((item) => item.addEventListener('click', this.onClick.bind(this)))
const menuItems = this.menuList.querySelectorAll<HTMLElement>(
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<HTMLElement>(targetSelector)
const targetElement =
document.querySelector<HTMLElement>(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<HTMLElement>(`[${this.options.hrefAttribute}="#${sectionId}"]`)
if (!section || !this.menuList) return;
const sectionId = section.getAttribute("id");
return this.menuList.querySelector<HTMLElement>(
`[${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));
}
}
}

0 comments on commit ed0704e

Please sign in to comment.