Skip to content

Commit

Permalink
feat: Full conversion to TypeScript #39 (#53)
Browse files Browse the repository at this point in the history
Full conversion to TypeScript #39
  • Loading branch information
kimyvgy authored Dec 14, 2023
2 parents b463837 + 72ec319 commit e6d7bc6
Show file tree
Hide file tree
Showing 9 changed files with 216 additions and 172 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.

6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
"name": "simple-scrollspy",
"version": "2.4.2",
"description": "Simple scrollspy javascript without jQuery, no dependencies.",
"main": "src/index.js",
"main": "dist/index.js",
"scripts": {
"dev": "webpack --mode production --watch --progress",
"build": "webpack --mode production --progress",
"build": "tsc && webpack --mode production --progress",
"prerelease": "npm run build && git add . && git commit -m 'release: Update /dist folder' || true",
"release": "standard-version"
},
Expand All @@ -26,6 +26,8 @@
"devDependencies": {
"standard-version": "^9.1.1",
"terser-webpack-plugin": "^5.3.6",
"ts-loader": "^9.5.1",
"typescript": "^5.3.3",
"webpack": "^5.76.0",
"webpack-cli": "^5.0.0"
},
Expand Down
35 changes: 0 additions & 35 deletions src/index.d.ts

This file was deleted.

9 changes: 0 additions & 9 deletions src/index.js

This file was deleted.

19 changes: 19 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { ScrollSpy, type MenuElement, type Options } from './scrollspy'

const scrollSpy = (el: MenuElement, options: Partial<Options> = {}) => {
const instance = new ScrollSpy(el, options)

window.onload = instance.onScroll;
window.addEventListener('scroll', () => instance.onScroll())

return instance
}

export type {
MenuElement,
Options,
ScrollSpy
};

export default scrollSpy;

122 changes: 0 additions & 122 deletions src/scrollspy.js

This file was deleted.

151 changes: 151 additions & 0 deletions src/scrollspy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
export type MenuElement = string | HTMLElement;
export type SmoothScroll = boolean | ScrollIntoViewOptions;

export type Options = {
sectionClass: string;
menuActiveTarget: string;
offset: number;
hrefAttribute: string;
activeClass: string;
scrollContainer: string | HTMLElement;
smoothScroll: SmoothScroll,
smoothScrollBehavior?: (targetElement: HTMLElement, smoothScrollOptions: SmoothScroll) => void;
onActive?: (activeItem: HTMLElement) => void;
};

export class ScrollSpy {
public menuList: HTMLElement | null;
public options: Options;
public scroller: HTMLElement | Window | null;
public sections: NodeListOf<HTMLElement>;

constructor(menu: MenuElement, options: Partial<Options> = {}) {
if (!menu) {
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.')
}

let defaultOptions = {
sectionClass: '.scrollspy',
menuActiveTarget: 'li > a',
offset: 0,
hrefAttribute: 'href',
activeClass: 'active',
scrollContainer: '',
smoothScroll: {},
}

options.smoothScroll = options.smoothScroll === true && {} || options.smoothScroll

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)
} else {
this.scroller = window
}

this.sections = document.querySelectorAll<HTMLElement>(this.options.sectionClass)

this.attachEventListeners()
}

attachEventListeners() {
if (this.scroller) {
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)))
}
}
}

onClick(event: MouseEvent) {
if (event.target) {
const targetSelector = (event.target as HTMLElement).getAttribute(this.options.hrefAttribute)
if (targetSelector) {
const targetElement = document.querySelector<HTMLElement>(targetSelector)

if (targetElement && this.options.smoothScroll) {
// prevent click event
event.preventDefault()
// handle smooth scrolling to 'targetElement'
this.scrollTo(targetElement)
}
}
}
}

onScroll() {
const section = this.getSectionInView()
if (section) {
const menuItem = this.getMenuItemBySection(section)

if (menuItem) {
this.removeCurrentActive({ ignore: menuItem })
this.setActive(menuItem)
}
}
}

scrollTo(targetElement: HTMLElement) {
const smoothScrollBehavior = typeof this.options.smoothScrollBehavior === 'function' && this.options.smoothScrollBehavior

if (smoothScrollBehavior) {
smoothScrollBehavior(targetElement, this.options.smoothScroll)
} else {
targetElement.scrollIntoView({
...(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}"]`)
}

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

if(this.options.scrollContainer && this.scroller) {
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]
}
}

setActive(activeItem: HTMLElement) {
const isActive = activeItem.classList.contains(this.options.activeClass)
if (!isActive) {
activeItem.classList.add(this.options.activeClass)
if (typeof this.options.onActive === 'function') {
this.options.onActive(activeItem)
}
}
}

removeCurrentActive({ ignore }: { ignore: HTMLElement }) {
if (this.menuList) {
const { hrefAttribute, menuActiveTarget, activeClass } = this.options
const items = `${menuActiveTarget}.${activeClass}:not([${hrefAttribute}="${ignore.getAttribute(hrefAttribute)}"])`
const menuItems = this.menuList.querySelectorAll(items)

menuItems.forEach((item) => item.classList.remove(this.options.activeClass))
}
}
}
22 changes: 22 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"compilerOptions": {
"esModuleInterop": true,
"skipLibCheck": true,
"target": "es2022",
"allowJs": true,
"resolveJsonModule": true,
"moduleDetection": "force",
"isolatedModules": true,
"strict": true,
"strictNullChecks": true,
"noUncheckedIndexedAccess": true,
"moduleResolution": "NodeNext",
"module": "NodeNext",
"outDir": "dist",
"sourceMap": true,
"declaration": true,
"lib": ["es2022", "dom", "dom.iterable"],
},
"include": ["src/*.ts"],
"exclude": ["node_modules", "dist", "demo"]
}
Loading

0 comments on commit e6d7bc6

Please sign in to comment.