mirror of https://github.com/vuejs/vitepress
parent
300dea4aad
commit
de76a6203d
@ -1,46 +1,164 @@
|
|||||||
import { inBrowser, onContentUpdated } from 'vitepress'
|
import { inBrowser, onContentUpdated } from 'vitepress'
|
||||||
|
|
||||||
|
const SCROLL_OPTIONS: ScrollIntoViewOptions = { block: 'nearest' }
|
||||||
|
const codeGroupCache = new Map<string, HTMLElement[]>()
|
||||||
|
|
||||||
export function useCodeGroups() {
|
export function useCodeGroups() {
|
||||||
if (import.meta.env.DEV) {
|
if (import.meta.env.DEV) {
|
||||||
onContentUpdated(() => {
|
onContentUpdated(() => {
|
||||||
|
clearCache()
|
||||||
|
|
||||||
document.querySelectorAll('.vp-code-group > .blocks').forEach((el) => {
|
document.querySelectorAll('.vp-code-group > .blocks').forEach((el) => {
|
||||||
Array.from(el.children).forEach((child) => {
|
Array.from(el.children).forEach((child) => {
|
||||||
child.classList.remove('active')
|
child.classList.remove('active')
|
||||||
})
|
})
|
||||||
el.children[0].classList.add('active')
|
el.children[0].classList.add('active')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
handleQueryParamNavigation()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (inBrowser) {
|
if (inBrowser) {
|
||||||
|
const handleUrlChange = () => {
|
||||||
|
handleQueryParamNavigation()
|
||||||
|
}
|
||||||
|
|
||||||
|
handleQueryParamNavigation()
|
||||||
|
|
||||||
|
window.addEventListener('popstate', handleUrlChange)
|
||||||
|
|
||||||
window.addEventListener('click', (e) => {
|
window.addEventListener('click', (e) => {
|
||||||
const el = e.target as HTMLInputElement
|
const el = e.target as HTMLInputElement
|
||||||
|
|
||||||
if (el.matches('.vp-code-group input')) {
|
if (el.matches('.vp-code-group input')) {
|
||||||
// input <- .tabs <- .vp-code-group
|
|
||||||
const group = el.parentElement?.parentElement
|
const group = el.parentElement?.parentElement
|
||||||
if (!group) return
|
if (!group) return
|
||||||
|
|
||||||
const i = Array.from(group.querySelectorAll('input')).indexOf(el)
|
const label = group?.querySelector(`label[for="${el.id}"]`)
|
||||||
if (i < 0) return
|
if (!label) return
|
||||||
|
|
||||||
const blocks = group.querySelector('.blocks')
|
// Activate the clicked tab
|
||||||
if (!blocks) return
|
if (!activateTab(group, el)) return
|
||||||
|
|
||||||
const current = Array.from(blocks.children).find((child) =>
|
label.scrollIntoView({ block: 'nearest' })
|
||||||
child.classList.contains('active')
|
|
||||||
)
|
|
||||||
if (!current) return
|
|
||||||
|
|
||||||
const next = blocks.children[i]
|
// Get the group key and tab title for URL update and sync
|
||||||
if (!next || current === next) return
|
const groupKey = group.getAttribute('data-group-key')
|
||||||
|
const tabTitle = label.getAttribute('data-title')?.toLowerCase()
|
||||||
|
|
||||||
current.classList.remove('active')
|
if (groupKey && tabTitle) {
|
||||||
next.classList.add('active')
|
// Synchronize all other code groups with the same key
|
||||||
|
syncCodeGroupsByKeyAndValue(groupKey, tabTitle, group)
|
||||||
|
|
||||||
const label = group?.querySelector(`label[for="${el.id}"]`)
|
// Update URL query parameter with key=value format
|
||||||
label?.scrollIntoView({ block: 'nearest' })
|
updateUrl(groupKey, tabTitle)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCodeGroupsByKey(groupKey: string): HTMLElement[] {
|
||||||
|
if (!codeGroupCache.has(groupKey)) {
|
||||||
|
codeGroupCache.set(
|
||||||
|
groupKey,
|
||||||
|
Array.from(
|
||||||
|
document.querySelectorAll(
|
||||||
|
`.vp-code-group[data-group-key="${groupKey}"]`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return codeGroupCache.get(groupKey) || []
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearCache() {
|
||||||
|
codeGroupCache.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
function activateTab(group: HTMLElement, input: HTMLInputElement): boolean {
|
||||||
|
const inputs = Array.from(
|
||||||
|
group.querySelectorAll('input')
|
||||||
|
) as HTMLInputElement[]
|
||||||
|
const index = inputs.indexOf(input)
|
||||||
|
if (index < 0) return false
|
||||||
|
|
||||||
|
const blocks = group.querySelector('.blocks')
|
||||||
|
if (!blocks) return false
|
||||||
|
|
||||||
|
// Remove active class from all blocks and add to the target block
|
||||||
|
Array.from(blocks.children).forEach((child, i) => {
|
||||||
|
child.classList.toggle('active', i === index)
|
||||||
|
})
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function findTabByTitle(
|
||||||
|
group: HTMLElement,
|
||||||
|
tabTitle: string
|
||||||
|
): HTMLInputElement | null {
|
||||||
|
if (!tabTitle) return null
|
||||||
|
const labels = Array.from(group.querySelectorAll('label[data-title]'))
|
||||||
|
const targetLabel = labels.find(
|
||||||
|
(label) =>
|
||||||
|
label.getAttribute('data-title')?.toLowerCase() === tabTitle.toLowerCase()
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!targetLabel) return null
|
||||||
|
|
||||||
|
const inputId = targetLabel.getAttribute('for')
|
||||||
|
if (!inputId) return null
|
||||||
|
|
||||||
|
return group.querySelector(`#${inputId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncCodeGroupsByKeyAndValue(
|
||||||
|
groupKey: string,
|
||||||
|
tabValue: string,
|
||||||
|
excludeGroup?: HTMLElement
|
||||||
|
) {
|
||||||
|
const groups = getCodeGroupsByKey(groupKey)
|
||||||
|
|
||||||
|
groups.forEach((group) => {
|
||||||
|
// Skip the group that was just clicked
|
||||||
|
if (excludeGroup && group === excludeGroup) return
|
||||||
|
|
||||||
|
const input = findTabByTitle(group, tabValue)
|
||||||
|
if (input) {
|
||||||
|
activateTab(group, input)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateUrl(groupKey: string, tabValue: string) {
|
||||||
|
const url = new URL(window.location.href)
|
||||||
|
url.searchParams.set(groupKey, tabValue)
|
||||||
|
window.history.replaceState(null, '', url.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleQueryParamNavigation() {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search)
|
||||||
|
if (urlParams.size === 0) return
|
||||||
|
|
||||||
|
const matches: HTMLElement[] = []
|
||||||
|
|
||||||
|
for (const [groupKey, tabValue] of urlParams.entries()) {
|
||||||
|
const groups = getCodeGroupsByKey(groupKey)
|
||||||
|
|
||||||
|
for (const group of groups) {
|
||||||
|
const input = findTabByTitle(group, tabValue)
|
||||||
|
if (input && activateTab(group, input)) {
|
||||||
|
matches.push(group)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scroll to the first matching group
|
||||||
|
if (matches.length > 0) {
|
||||||
|
const firstMatchGroup = matches[0]
|
||||||
|
const firstLabel = firstMatchGroup.querySelector('label[data-title]')
|
||||||
|
firstLabel?.scrollIntoView(SCROLL_OPTIONS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in new issue