Skip to content

Instantly share code, notes, and snippets.

@tak-dcxi
Last active April 26, 2024 07:37
Show Gist options
  • Save tak-dcxi/0ba44dc2c869b559e9a19163e0dadbd6 to your computer and use it in GitHub Desktop.
Save tak-dcxi/0ba44dc2c869b559e9a19163e0dadbd6 to your computer and use it in GitHub Desktop.
initializeSplitText
type AccordionOptions = {
duration?: number
easing?: string
}
const defaultOptions: AccordionOptions = {
duration: 300,
easing: 'ease-in-out',
}
let isCurrentPanelTransitioning = false // 現在のパネルが開いているときの連打防止フラグ
let isOtherPanelTransitioning = false // 他のパネルが開いているときの連打防止フラグ
// アコーディオンを初期化する
const initializeDetailsAccordion = (details: HTMLDetailsElement, options: AccordionOptions = {}): void => {
if (!details) {
console.error('initializeDetailsAccordion: Details element is not found.')
return
}
const summary = details.querySelector('summary') as HTMLElement
const panel = details.querySelector('summary + *') as HTMLElement
if (!summary || !panel) {
console.error('initializeDetailsAccordion: Elements required for initializeDetailsAccordion are not found.')
return
}
const mergedOptions = { ...defaultOptions, ...options }
panel.style.transitionProperty = 'block-size'
panel.style.transitionDuration = `${Math.max(1, mergedOptions.duration || 1)}ms`
panel.style.transitionTimingFunction = mergedOptions.easing || ''
if (details.hasAttribute('name')) {
const detailsName = details.getAttribute('name') || ''
details.setAttribute('data-name', detailsName)
details.removeAttribute('name')
}
summary.addEventListener('click', (event: MouseEvent) => handleClick(event, details, panel), false)
}
// 縦書きモードかどうかを取得し、スクロールサイズを返す
const getScrollSize = (details: HTMLDetailsElement, panel: HTMLElement): string => {
const writingMode = window.getComputedStyle(details).writingMode
const scrollSize = `${writingMode.includes('vertical') ? panel.scrollWidth : panel.scrollHeight}px`
return scrollSize
}
// トランジション終了時の処理を扱う
const onTransitionEnd = (element: HTMLElement, callback: () => void): void => {
const onEnd = (event: TransitionEvent) => {
if (event.target !== element) return
element.removeEventListener('transitionend', onEnd)
callback()
}
element.addEventListener('transitionend', onEnd)
}
// アコーディオンを開く
const openAccordion = (details: HTMLDetailsElement, panel: HTMLElement): void => {
if (details.open) return
isCurrentPanelTransitioning = true
panel.style.blockSize = '0'
panel.style.overflow = 'clip'
details.open = true
requestAnimationFrame(() => {
requestAnimationFrame(() => {
panel.style.blockSize = getScrollSize(details, panel)
})
onTransitionEnd(panel, () => {
panel.style.blockSize = ''
panel.style.overflow = ''
isCurrentPanelTransitioning = false
})
})
}
// アコーディオンを閉じる
const closeAccordion = (details: HTMLDetailsElement, panel: HTMLElement): void => {
if (!details.open) return
isCurrentPanelTransitioning = true
panel.style.blockSize = getScrollSize(details, panel)
panel.style.overflow = 'clip'
requestAnimationFrame(() => {
requestAnimationFrame(() => {
panel.style.blockSize = '0'
})
onTransitionEnd(panel, () => {
details.open = false
panel.style.blockSize = ''
panel.style.overflow = ''
isCurrentPanelTransitioning = false
})
})
}
// 同一のdata-name属性を持つ他の開いているアコーディオンを閉じる
const closeOtherAccordions = (details: HTMLDetailsElement): void => {
const detailsName = details.getAttribute('data-name')
if (!detailsName) return
const otherDetails = document.querySelector(`details[data-name="${detailsName}"][open]`) as HTMLDetailsElement
if (!otherDetails || otherDetails === details) return
const otherPanel = otherDetails.querySelector('summary + *') as HTMLElement
if (!otherPanel) return
isOtherPanelTransitioning = true
otherPanel.style.blockSize = getScrollSize(otherDetails, otherPanel)
otherPanel.style.overflow = 'clip'
requestAnimationFrame(() => {
requestAnimationFrame(() => {
otherPanel.style.blockSize = '0'
})
onTransitionEnd(otherPanel, () => {
otherDetails.open = false
otherPanel.style.blockSize = ''
otherPanel.style.overflow = ''
isOtherPanelTransitioning = false
})
})
}
// クリックイベントハンドラーの設定
const handleClick = (event: MouseEvent, details: HTMLDetailsElement, panel: HTMLElement): void => {
event.preventDefault()
if (isCurrentPanelTransitioning || isOtherPanelTransitioning) return
if (details.open) {
closeAccordion(details, panel)
} else {
closeOtherAccordions(details)
openAccordion(details, panel)
}
}
export default initializeDetailsAccordion
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment