handy-collapse
Version:
A pure Javascript module for accordion/collapse UI without jQuery
263 lines (250 loc) • 8.59 kB
text/typescript
/**
* handyCollapse
* https://github.com/sk-rt/handy-collapse
* Copyright (c) 2019 Ryuta Sakai
* Licensed under the MIT license.
*/
export interface Options {
nameSpace: string;
toggleButtonAttr: string;
toggleContentAttr: string;
activeClass: string;
isAnimation: boolean;
closeOthers: boolean;
animationSpeed: number;
cssEasing: string;
onSlideStart: (isOpen: boolean, id: string) => void;
onSlideEnd: (isOpen: boolean, id: string) => void;
}
interface ItemState {
[key: string]: {
isOpen: boolean;
isAnimating: boolean;
};
}
export default class HandyCollapse {
toggleContentEls: HTMLElement[];
toggleButtonEls: HTMLElement[];
itemsState: ItemState = {};
options: Options;
constructor(_options: Partial<Options> = {}) {
const nameSpace = typeof _options === "object" && "nameSpace" in _options ? _options.nameSpace : "hc";
const defaultOptions = {
nameSpace: "hc",
toggleButtonAttr: `data-${nameSpace}-control`,
toggleContentAttr: `data-${nameSpace}-content`,
activeClass: "is-active",
isAnimation: true,
closeOthers: true,
animationSpeed: 400,
cssEasing: "ease-in-out",
onSlideStart: () => {},
onSlideEnd: () => {}
};
this.options = {
...defaultOptions,
..._options
};
this.toggleContentEls = [].slice.call(document.querySelectorAll(`[${this.options.toggleContentAttr}]`));
this.toggleButtonEls = [].slice.call(document.querySelectorAll(`[${this.options.toggleButtonAttr}]`));
if (this.toggleContentEls.length !== 0) {
this.initContentsState(this.toggleContentEls);
}
if (this.toggleButtonEls.length !== 0) {
this.handleButtonsEvent(this.toggleButtonEls);
}
}
/**
* init Param & show/hide items
*/
private initContentsState(contentEls: HTMLElement[]) {
this.itemsState = {};
contentEls.forEach((contentEl: HTMLElement) => {
contentEl.style.overflow = "hidden";
contentEl.style.maxHeight = "none";
const isOpen = contentEl.classList.contains(this.options.activeClass);
const id = contentEl.getAttribute(this.options.toggleContentAttr);
if (!id) return;
this.setItemState(id, isOpen);
if (!isOpen) {
this.close(id, false, false);
} else {
this.open(id, false, false);
}
});
}
/**
* Add toggleButton Listners
*/
handleButtonsEvent(buttonElement: HTMLElement[]) {
buttonElement.forEach((buttonEl: HTMLElement) => {
const id = buttonEl.getAttribute(this.options.toggleButtonAttr);
if (id) {
buttonEl.addEventListener(
"click",
(e) => {
e.preventDefault();
this.toggleSlide(id, true);
},
false
);
}
});
}
/**
* Set state
*/
private setItemState(id: string, isOpen: boolean) {
this.itemsState[id] = {
isOpen: isOpen,
isAnimating: false
};
}
/**
* button click listner
* @param id - accordion ID
*/
toggleSlide(id: string, isRunCallback = true) {
if (this.itemsState[id]?.isAnimating) return;
if (this.itemsState[id]?.isOpen === false) {
this.open(id, isRunCallback, this.options.isAnimation);
} else {
this.close(id, isRunCallback, this.options.isAnimation);
}
}
/**
* Open accordion
* @param id - accordion ID
*/
open(id: string, isRunCallback = true, isAnimation = true) {
if (!id) return;
if (!Object.prototype.hasOwnProperty.call(this.itemsState, id)) {
this.setItemState(id, false);
}
const toggleBody = document.querySelector<HTMLElement>(`[${this.options.toggleContentAttr}='${id}']`);
if (!toggleBody) {
return;
}
this.itemsState[id].isAnimating = true;
//Close Others
if (this.options.closeOthers) {
[].slice.call(this.toggleContentEls).forEach((contentEl: HTMLElement) => {
const closeId = contentEl.getAttribute(this.options.toggleContentAttr);
if (closeId && closeId !== id) this.close(closeId, false, isAnimation);
});
}
if (isRunCallback !== false) this.options.onSlideStart(true, id);
//Content : Set getHeight, add activeClass
const clientHeight = this.getTargetHeight(toggleBody);
toggleBody.style.visibility = "visible";
toggleBody.classList.add(this.options.activeClass);
//Button : Add activeClass
const toggleButton = document.querySelectorAll(`[${this.options.toggleButtonAttr}='${id}']`);
if (toggleButton.length > 0) {
[].slice.call(toggleButton).forEach((button: HTMLElement) => {
button.classList.add(this.options.activeClass);
if (button.hasAttribute("aria-expanded")) {
button.setAttribute("aria-expanded", "true");
}
});
}
if (isAnimation) {
//Slide Animation
toggleBody.style.overflow = "hidden";
toggleBody.style.transition = `${this.options.animationSpeed}ms ${this.options.cssEasing}`;
toggleBody.style.maxHeight = (clientHeight || "1000") + "px";
setTimeout(() => {
if (isRunCallback !== false) this.options.onSlideEnd(true, id);
toggleBody.style.maxHeight = "none";
toggleBody.style.transition = "";
toggleBody.style.overflow = "";
this.itemsState[id].isAnimating = false;
}, this.options.animationSpeed);
} else {
//No Animation
toggleBody.style.maxHeight = "none";
toggleBody.style.overflow = "";
this.itemsState[id].isAnimating = false;
}
this.itemsState[id].isOpen = true;
if (toggleBody.hasAttribute("aria-hidden")) {
toggleBody.setAttribute("aria-hidden", "false");
}
}
/**
* Close accordion
* @param id - accordion ID
*/
close(id: string, isRunCallback = true, isAnimation = true) {
if (!id) return;
if (!Object.prototype.hasOwnProperty.call(this.itemsState, id)) {
this.setItemState(id, false);
}
this.itemsState[id].isAnimating = true;
if (isRunCallback !== false) this.options.onSlideStart(false, id);
//Content : Set getHeight, remove activeClass
const toggleBody = document.querySelector(`[${this.options.toggleContentAttr}='${id}']`) as HTMLElement;
toggleBody.style.overflow = "hidden";
toggleBody.classList.remove(this.options.activeClass);
toggleBody.style.maxHeight = toggleBody.clientHeight + "px";
setTimeout(() => {
toggleBody.style.maxHeight = "0px";
}, 5);
//Buttons : Remove activeClass
const toggleButton = document.querySelectorAll(`[${this.options.toggleButtonAttr}='${id}']`);
if (toggleButton.length > 0) {
[].slice.call(toggleButton).forEach((button: HTMLElement) => {
button.classList.remove(this.options.activeClass);
if (button.hasAttribute("aria-expanded")) {
button.setAttribute("aria-expanded", "false");
}
});
}
if (isAnimation) {
//Slide Animation
toggleBody.style.transition = `${this.options.animationSpeed}ms ${this.options.cssEasing}`;
setTimeout(() => {
if (isRunCallback !== false) this.options.onSlideEnd(false, id);
toggleBody.style.transition = "";
this.itemsState[id].isAnimating = false;
toggleBody.style.visibility = "hidden";
}, this.options.animationSpeed);
} else {
//No Animation
this.options.onSlideEnd(false, id);
this.itemsState[id].isAnimating = false;
toggleBody.style.visibility = "hidden";
}
if (Object.prototype.hasOwnProperty.call(this.itemsState, id)) {
this.itemsState[id].isOpen = false;
}
if (toggleBody.hasAttribute("aria-hidden")) {
toggleBody.setAttribute("aria-hidden", "true");
}
}
/**
* Get Elemet Height
* @param targetEl - target Element
* @return Height(px)
*/
getTargetHeight(targetEl: HTMLElement): number | void {
if (!targetEl) return;
const cloneEl = targetEl.cloneNode(true) as HTMLElement;
const parentEl = targetEl.parentNode;
if (!parentEl) return;
// bugfix: Radio button being unchecked when collapsed
const inputElements: HTMLInputElement[] = [].slice.call(cloneEl.querySelectorAll("input[name]"));
if (inputElements.length !== 0) {
const suffix = "-" + new Date().getTime();
inputElements.forEach((input) => {
input.name += suffix;
});
}
cloneEl.style.maxHeight = "none";
cloneEl.style.opacity = "0";
parentEl.appendChild(cloneEl);
const clientHeight = cloneEl.clientHeight;
parentEl.removeChild(cloneEl);
return clientHeight;
}
}