Skip to content

Commit 5d97b33

Browse files
committed
update: blog.js — support for copy and expand pre elements
1 parent 2d7e047 commit 5d97b33

File tree

1 file changed

+247
-0
lines changed

1 file changed

+247
-0
lines changed

src/assets/js/blog.js

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,249 @@
1+
const keyboardSymbols = {
2+
CTRL: '⌃',
3+
CONTROL: '⌃',
4+
SHIFT: '⇧',
5+
ALT: '⎇',
6+
META: '⌘',
7+
ENTER: '⏎',
8+
ESC: '⎋',
9+
TAB: '⇥',
10+
BACKSPACE: '⌫',
11+
DELETE: '⌦',
12+
ARROWUP: '↑',
13+
ARROWDOWN: '↓',
14+
ARROWLEFT: '←',
15+
ARROWRIGHT: '→',
16+
PAGEUP: '⇞',
17+
PAGEDOWN: '⇟',
18+
HOME: '↖',
19+
END: '↘',
20+
SPACE: '␣'
21+
}
22+
23+
document.querySelectorAll('kbd').forEach(kbd => {
24+
let gotSymbol = false
25+
const originalText = kbd.innerHTML
26+
const text = originalText.split(" ")
27+
// console.log(text)
28+
const newText = text.map(text => {
29+
text = text.trim()
30+
if (keyboardSymbols[text.toUpperCase()]) {
31+
text = keyboardSymbols[text.toUpperCase()]
32+
gotSymbol = true
33+
}
34+
return text
35+
})
36+
if (!gotSymbol) return kbd.innerHTML = originalText
37+
kbd.innerHTML = newText.join(" ")
38+
kbd.setAttribute('aria-label', `Keyboard shortcut: ${originalText}`)
39+
})
40+
41+
const getCodeType = pre => {
42+
if (!pre.firstChild && !pre.firstChild.classList) return null
43+
return Array.from(pre.firstChild.classList).find(cls => cls.startsWith('language-'))
44+
}
45+
46+
const createToolsContainer = (language, codeContent) => {
47+
const toolsContainer = document.createElement('div')
48+
const pre = codeContent?.parentElement
49+
const parentOfPre = pre?.parentElement
50+
let expandButton = null
51+
let ariaLabel = pre?.getAttribute('aria-label') || 'Code block'
52+
53+
const codeTypeDiv = createTypeDiv(language)
54+
const copyButton = createCopyButton(codeContent)
55+
if (parentOfPre.nodeName.toLowerCase() !== "blockquote") {
56+
expandButton = createExpandButton(toolsContainer)
57+
}
58+
59+
toolsContainer.className = 'tools-container'
60+
ariaLabel += `, ${copyButton.getAttribute('aria-label')}`
61+
62+
toolsContainer.appendChild(codeTypeDiv)
63+
toolsContainer.appendChild(copyButton)
64+
if (expandButton) {
65+
toolsContainer.appendChild(expandButton)
66+
ariaLabel += `, ${expandButton.getAttribute('aria-label')}`
67+
}
68+
69+
pre.setAttribute('aria-label', ariaLabel)
70+
return toolsContainer
71+
}
72+
73+
const createExpandablePre = (pre) => {
74+
pre.className = 'expandable'
75+
pre.setAttribute('tabindex', '0')
76+
pre.setAttribute('role', 'region')
77+
pre.addEventListener('keydown', (event) => {
78+
const expandOption = pre.querySelector('#expand-option')
79+
if (event.key === 'Enter') {
80+
if (!expandOption) return
81+
if (event.target.id === 'copy-option') return
82+
event.currentTarget.classList.toggle('expanded')
83+
pre.querySelector('#expand-option').firstChild.innerText =
84+
pre.classList.contains('expanded') ? 'Collapse' : 'Expand'
85+
}
86+
87+
if (event.key === 'c' && event.ctrlKey) {
88+
const copyButton = pre.querySelector('#copy-option')
89+
if (copyButton) copyButton.click()
90+
}
91+
})
92+
return pre
93+
}
94+
95+
const createExpandButton = (container) => {
96+
const expandButton = createInteractiveButton({
97+
text: 'Expand',
98+
id: 'expand-option',
99+
label: 'Press Enter to expand code block or collapse it',
100+
defaultState: false,
101+
})
102+
const shortcut = createKeyboardShortcut({
103+
keyCode: 'Enter',
104+
})
105+
106+
expandButton.addEventListener('click', () => {
107+
if (!container.parentElement.classList.contains('expanded')) {
108+
container.parentElement.classList.add('expanded')
109+
expandButton.firstChild.innerText = 'Collapse'
110+
expandButton.setAttribute('aria-pressed', 'true')
111+
} else {
112+
container.parentElement.classList.remove('expanded')
113+
expandButton.firstChild.innerText = 'Expand'
114+
expandButton.setAttribute('aria-pressed', 'false')
115+
}
116+
})
117+
118+
expandButton.addEventListener('keydown', (event) => {
119+
if (event.key === 'Enter') {
120+
event.currentTarget.click()
121+
}
122+
})
123+
124+
expandButton.appendChild(shortcut)
125+
126+
return expandButton
127+
}
128+
129+
const createTypeDiv = language => {
130+
const typeDiv = document.createElement('div')
131+
typeDiv.className = 'code-type'
132+
typeDiv.innerText = language || 'Code'
133+
return typeDiv
134+
}
135+
136+
const createCopyButton = codeContent => {
137+
const shortcut = createKeyboardShortcut({
138+
keyCode: 'c',
139+
keyModifier: 'Ctrl'
140+
})
141+
const copyButton = createInteractiveButton({
142+
text: 'Copy',
143+
id: 'copy-option',
144+
label: 'Press Control + C to copy code to clipboard',
145+
defaultState: false,
146+
})
147+
148+
copyButton.addEventListener('click', () => {
149+
navigator.clipboard.writeText(codeContent.textContent)
150+
.then(() => {
151+
copyButton.firstChild.innerText = 'Copied!'
152+
setTimeout(() => {
153+
copyButton.firstChild.innerText = 'Copy'
154+
}, 2000)
155+
})
156+
.catch(err => {
157+
console.error('Failed to copy text: ', err)
158+
copyButton.firstChild.innerText = 'Error'
159+
})
160+
})
161+
copyButton.addEventListener('keydown', (event) => {
162+
if (event.key === 'Enter') {
163+
copyButton.click()
164+
}
165+
})
166+
167+
copyButton.appendChild(shortcut)
168+
169+
return copyButton
170+
}
171+
172+
const createKeyboardShortcut = ({ keyCode, keyModifier }) => {
173+
const shortcut = document.createElement('kbd')
174+
const keyCodeElement = document.createElement('span')
175+
let keyModifierElement
176+
177+
switch (keyModifier) {
178+
case 'Ctrl':
179+
keyModifierElement = createKeyboardModifier({
180+
keyboardSymbol: getKeyboardCode("Ctrl"),
181+
keyboardModifier: 'Control'
182+
})
183+
break;
184+
case 'Shift':
185+
keyModifierElement = createKeyboardModifier({
186+
keyboardSymbol: getKeyboardCode("Shift"),
187+
keyboardModifier: 'Shift'
188+
})
189+
break;
190+
}
191+
192+
keyCodeElement.className = 'key-code'
193+
keyCodeElement.innerText = getKeyboardCode(keyCode)
194+
195+
shortcut.className = 'keyboard-shortcut'
196+
if (keyModifier) shortcut.appendChild(keyModifierElement)
197+
shortcut.appendChild(keyCodeElement)
198+
return shortcut
199+
}
200+
201+
const getKeyboardCode = (text) => {
202+
return keyboardSymbols[text.toUpperCase()] || text
203+
}
204+
205+
const createKeyboardModifier = ({ keyboardSymbol, keyboardModifier }) => {
206+
const keyModifierElement = document.createElement('span')
207+
const keyModifierText = document.createElement('span')
208+
const keyModifierSymbol = document.createElement('span')
209+
210+
keyModifierElement.className = 'key-modifier'
211+
212+
keyModifierText.innerText = keyboardModifier ?? 'Control'
213+
keyModifierText.className = 'sr-only'
214+
215+
keyModifierSymbol.innerText = keyboardSymbol ?? '⌃'
216+
keyModifierSymbol.ariaHidden = true
217+
218+
keyModifierElement.appendChild(keyModifierText)
219+
keyModifierElement.appendChild(keyModifierSymbol)
220+
221+
return keyModifierElement
222+
}
223+
224+
const createInteractiveButton = ({ text, id, label, defaultState } = {}) => {
225+
const button = document.createElement('button')
226+
const buttonText = document.createElement('span')
227+
buttonText.innerText = text || 'Button'
228+
button.id = id || 'interactive-button'
229+
button.setAttribute('aria-label', label || 'Interactive button')
230+
button.setAttribute('aria-pressed', defaultState ? 'true' : 'false')
231+
button.appendChild(buttonText)
232+
return button
233+
}
234+
235+
const enhancePreElement = pre => {
236+
const codeType = getCodeType(pre)
237+
if (codeType) pre.setAttribute('data-language', codeType.replace('language-', ''))
238+
239+
const codeContent = pre.querySelector('code')
240+
const language = pre.getAttribute('data-language')
241+
const toolsContainer = createToolsContainer(language, codeContent)
242+
243+
createExpandablePre(pre)
244+
pre.insertBefore(toolsContainer, pre.firstChild)
245+
}
246+
1247
const checkTableOfContents = () => {
2248
const toc = document.getElementById("main").querySelector(".table-of-contents")
3249
const skipToMain = document.querySelector('a[href="#main"]')
@@ -19,6 +265,7 @@ const createSkipLink = (id, text = "Skip pass the Table of Contents") => {
19265
return skipLink
20266
}
21267

268+
document.querySelectorAll('pre').forEach(enhancePreElement)
22269
checkTableOfContents()
23270
Array.from(document.links).filter(link => link.hostname != window.location.hostname)
24271
.forEach(link => link.target = '_blank')

0 commit comments

Comments
 (0)