Repository navigation
Expand file tree
/
Copy pathsite.js
More file actions
134 lines (120 loc) · 5.09 KB
/
Copy pathsite.js
File metadata and controls
134 lines (120 loc) · 5.09 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
// Screen reader announcements through a polite status region.
export function announce(message, region = document.querySelector('[data-status="page"]')) {
region.textContent = ''
// Cleared first so that repeating the same message is announced again.
setTimeout(() => (region.textContent = message), 50)
}
// Color scheme toggle: two states, the system default or its opposite.
const schemeMeta = document.querySelector('meta[name="color-scheme"]')
const systemDark = matchMedia('(prefers-color-scheme: dark)')
const storeScheme = (value) => {
try {
value ? localStorage.setItem('color-scheme', value) : localStorage.removeItem('color-scheme')
} catch {}
}
const systemScheme = () => (systemDark.matches ? 'dark' : 'light')
const renderedScheme = () => (schemeMeta.content === 'light dark' ? systemScheme() : schemeMeta.content)
const themeSwitches = [...document.querySelectorAll('[data-theme-toggle] input')]
const syncSwitches = () => themeSwitches.forEach((input) => (input.checked = renderedScheme() === 'dark'))
const applyScheme = (stored) => {
schemeMeta.content = stored ?? 'light dark'
syncSwitches()
document.dispatchEvent(new CustomEvent('colorschemechange'))
}
for (const input of themeSwitches) {
input.closest('[data-theme-toggle]').hidden = false
input.addEventListener('change', () => {
const target = input.checked ? 'dark' : 'light'
// Switching back to what the system shows means following the system again.
const stored = target === systemScheme() ? null : target
storeScheme(stored)
applyScheme(stored)
})
}
syncSwitches()
systemDark.addEventListener('change', syncSwitches)
addEventListener('storage', (event) => {
if (event.key === 'color-scheme') applyScheme(event.newValue)
})
// Code blocks: a bar with the language, a Copy button and, inside `.example`, Try and Reset buttons.
const languageNames = { html: 'HTML', markup: 'HTML', js: 'JavaScript', jsx: 'JSX', ts: 'TypeScript', bash: 'Shell', css: 'CSS', json: 'JSON' }
for (const pre of document.querySelectorAll('pre[class*="language-"]')) {
const language = [...pre.classList].find((name) => name.startsWith('language-'))?.slice(9)
const wrapper = document.createElement('div')
wrapper.className = 'code'
const bar = document.createElement('div')
bar.className = 'code-bar'
const label = document.createElement('span')
label.className = 'code-lang'
label.textContent = pre.dataset.label ?? languageNames[language] ?? language
bar.append(label)
const example = pre.closest('.example')
if (example) {
let heading = example
while ((heading = heading.previousElementSibling) && !/^H[23]$/.test(heading.tagName));
const tryButton = document.createElement('button')
tryButton.type = 'button'
tryButton.className = 'button button-primary'
tryButton.dataset.tryButton = ''
tryButton.innerHTML = '<span aria-hidden="true">▶</span> Try'
if (heading) tryButton.setAttribute('aria-label', `Try the ${heading.textContent.trim()} example`)
const resetButton = document.createElement('button')
resetButton.type = 'button'
resetButton.className = 'button'
resetButton.dataset.resetButton = ''
resetButton.textContent = 'Reset'
resetButton.setAttribute('aria-label', 'Reset preview')
bar.append(tryButton, resetButton)
}
if (navigator.clipboard) {
const copy = document.createElement('button')
copy.type = 'button'
copy.className = 'button'
copy.textContent = 'Copy'
copy.addEventListener('click', async () => {
try {
await navigator.clipboard.writeText(pre.textContent)
copy.textContent = 'Copied'
announce('Copied to clipboard')
} catch {
copy.textContent = 'Copy failed'
announce('Copy failed')
}
setTimeout(() => (copy.textContent = 'Copy'), 1500)
})
bar.append(copy)
}
pre.before(wrapper)
wrapper.append(bar, pre)
}
// Scrollspy for the sidebar table of contents.
const tocLinks = [...document.querySelectorAll('.toc a[href^="#"]')]
if (tocLinks.length) {
const markCurrent = (current) => {
for (const link of tocLinks) {
link === current ? link.setAttribute('aria-current', 'true') : link.removeAttribute('aria-current')
}
}
if (CSS.supports('scroll-target-group: auto')) {
const sync = () => markCurrent(document.querySelector('.toc a:target-current'))
let timer
sync()
// :target-current settles after the scroll, so read it once scrolling has paused.
addEventListener('scroll', () => {
clearTimeout(timer)
timer = setTimeout(sync, 100)
}, { passive: true })
} else {
const targets = tocLinks.map((link) => document.getElementById(link.hash.slice(1)))
const update = () => {
let index = 0
targets.forEach((target, i) => {
if (target && target.getBoundingClientRect().top < innerHeight * 0.3) index = i
})
tocLinks.forEach((link, i) => link.classList.toggle(':target-current', i === index))
markCurrent(tocLinks[index])
}
const observer = new IntersectionObserver(update, { rootMargin: '0px 0px -70% 0px' })
targets.forEach((target) => target && observer.observe(target))
}
}