Skip to content

Commit 3c3bfa2

Browse files
hectorsectorheiskr
andauthored
Add toggle to header on annotations (github#37758)
Co-authored-by: Kevin Heis <[email protected]>
1 parent 06f24cf commit 3c3bfa2

File tree

9 files changed

+298
-59
lines changed

9 files changed

+298
-59
lines changed

components/lib/copy-code.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ export default function copyCode() {
55

66
buttons.forEach((button) =>
77
button.addEventListener('click', async () => {
8-
const text = (button as HTMLElement).dataset.clipboardText
8+
const codeId = (button as HTMLElement).dataset.clipboard
9+
if (!codeId) return
10+
const pre = document.querySelector(`pre[data-clipboard="${codeId}"]`) as HTMLElement | null
11+
if (!pre) return
12+
const text = pre.innerText
913
if (!text) return
1014
await navigator.clipboard.writeText(text)
1115

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import Cookies from 'components/lib/cookies'
2+
3+
enum annotationMode {
4+
Beside = '#annotation-beside',
5+
Inline = '#annotation-inline',
6+
}
7+
8+
/**
9+
* Validates if a given mode is one of expected annotation modes. If no acceptable mode is found, a default mode is returned.. Optionally, returns a default mode.
10+
* @param mode The mode to validate, ideally "#annotation-beside" or "#annotation-inline"
11+
* @param leaveNull Alters the return value of this function. If false, the function will return the mode that was passed in or, in the case of null, the default mode. If true, the function will return null instead of using the default mode.
12+
* @returns The validated mode, or null if leaveNull is true and no valid mode is found.
13+
*/
14+
function validateMode(mode?: string, leaveNull?: boolean) {
15+
if (mode === annotationMode.Beside || mode === annotationMode.Inline || (!mode && leaveNull))
16+
return mode
17+
else {
18+
if (leaveNull) {
19+
console.warn(`Leaving null.`)
20+
return
21+
}
22+
23+
// default to beside
24+
return annotationMode.Beside
25+
}
26+
}
27+
28+
export default function toggleAnnotation() {
29+
const subNavElements = Array.from(document.querySelectorAll('a.subnav-item'))
30+
if (!subNavElements.length) return
31+
32+
const cookie = validateMode(Cookies.get('annotate-mode')) // will default to beside
33+
displayAnnotationMode(setActive(subNavElements, cookie), subNavElements, cookie)
34+
35+
// this loop adds event listeners for both the annotation buttons
36+
for (const subnav of subNavElements) {
37+
subnav.addEventListener('click', (evt) => {
38+
evt.preventDefault()
39+
40+
// returns either:
41+
// 1. if href is correct, the href that was passed in
42+
// 2. if href is missing, null
43+
const validMode = validateMode(subnav.getAttribute('href')!)
44+
45+
Cookies.set('annotate-mode', validMode!)
46+
47+
getActive(subNavElements).removeAttribute('aria-current')
48+
setActive(subNavElements, validMode)
49+
displayAnnotationMode(subnav, subNavElements, validMode)
50+
})
51+
}
52+
}
53+
54+
// returns the active element via its aria-current attribute, errors if it can't find it
55+
function getActive(subnavItems: Array<Element>) {
56+
const currentlyActive = subnavItems.find((el) => el.ariaCurrent === 'true')
57+
58+
if (!currentlyActive) setActive(subnavItems)
59+
60+
return currentlyActive!
61+
}
62+
63+
// sets the active element's aria-current, if no targetMode is set we default to "Beside", errors if it can't set either Beside or the passed in targetMode
64+
function setActive(subNavElements: Array<Element>, targetMode?: string) {
65+
targetMode = validateMode(targetMode)
66+
const targetActiveElement = subNavElements.find((el) => el.getAttribute('href') === targetMode)
67+
68+
if (!targetActiveElement) {
69+
throw new Error('No subnav item is active for code annotation.')
70+
}
71+
72+
targetActiveElement.ariaCurrent = 'true'
73+
74+
return targetActiveElement
75+
}
76+
77+
// displays the chosen annotation mode
78+
function displayAnnotationMode(
79+
activeElement: Element,
80+
subNavItems: Array<Element>,
81+
targetMode?: string
82+
) {
83+
if (!targetMode || targetMode === annotationMode.Beside)
84+
activeElement.closest('.annotate')?.classList.replace('inline', 'beside')
85+
else if (targetMode === annotationMode.Inline)
86+
activeElement.closest('.annotate')?.classList.replace('beside', 'inline')
87+
else throw new Error('Invalid target mode set for annotation.')
88+
89+
setActive(subNavItems, targetMode)
90+
}

lib/render-content/plugins/annotate.js

Lines changed: 46 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -117,29 +117,55 @@ function matchComment(lang) {
117117
return (line) => regex.test(line)
118118
}
119119

120+
function getSubnav() {
121+
const besideBtn = h(
122+
'a',
123+
{
124+
className: 'subnav-item',
125+
href: '#annotation-beside',
126+
},
127+
['Beside']
128+
)
129+
const inlineBtn = h(
130+
'a',
131+
{
132+
className: 'subnav-item',
133+
href: '#annotation-inline',
134+
},
135+
['Inline']
136+
)
137+
138+
return h('nav', { className: 'subnav mb-0 pr-2' }, [besideBtn, inlineBtn])
139+
}
140+
120141
function template({ lang, code, rows }) {
121142
return h(
122143
'div',
123-
{ class: 'annotate' },
124-
h('div', { className: 'annotate-row header' }, [
125-
h('div', { className: 'annotate-code header color-bg-default' }, header(lang, code)),
126-
h('div', { className: 'annotate-note header' }),
127-
]),
128-
rows.map(([note, code]) =>
129-
h('div', { className: 'annotate-row' }, [
130-
h(
131-
'div',
132-
{ className: 'annotate-code' },
133-
// This tree matches the mdast -> hast tree of a regular fenced code block.
134-
h('pre', h('code', { className: `language-${lang}` }, code.join('\n')))
135-
),
136-
h(
137-
'div',
138-
{ className: 'annotate-note' },
139-
mdToHast(note.map(removeComment(lang)).join('\n'))
140-
),
141-
])
142-
)
144+
{ class: 'annotate beside' },
145+
h('div', { className: 'annotate-header' }, header(lang, code, getSubnav())),
146+
h(
147+
'div',
148+
{ className: 'annotate-beside' },
149+
rows.map(([note, code]) =>
150+
h('div', { className: 'annotate-row' }, [
151+
h(
152+
'div',
153+
{ className: 'annotate-code' },
154+
// pre > code matches the mdast -> hast tree of a regular fenced code block.
155+
h('pre', h('code', { className: `language-${lang}` }, code.join('\n')))
156+
),
157+
h(
158+
'div',
159+
{ className: 'annotate-note' },
160+
mdToHast(note.map(removeComment(lang)).join('\n'))
161+
),
162+
])
163+
)
164+
),
165+
h('div', { className: 'annotate-inline' }, [
166+
// pre > code matches the mdast -> hast tree of a regular fenced code block.
167+
h('pre', h('code', { className: `language-${lang}` }, code)),
168+
])
143169
)
144170
}
145171

lib/render-content/plugins/code-header.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { h } from 'hastscript'
99
import octicons from '@primer/octicons'
1010
import { parse } from 'parse5'
1111
import { fromParse5 } from 'hast-util-from-parse5'
12+
import murmur from 'imurmurhash'
1213

1314
const languages = yaml.load(fs.readFileSync('./data/variables/code-languages.yml', 'utf8'))
1415

@@ -35,7 +36,8 @@ function wrapCodeExample(node) {
3536
return h('div', { className: 'code-example' }, [header(lang, code), node])
3637
}
3738

38-
export function header(lang, code) {
39+
export function header(lang, code, subnav) {
40+
const codeId = murmur('js-btn-copy').hash(code).result()
3941
return h(
4042
'header',
4143
{
@@ -52,16 +54,18 @@ export function header(lang, code) {
5254
],
5355
},
5456
[
55-
h('span', languages[lang]?.name),
57+
h('span', { className: 'flex-1' }, languages[lang]?.name),
58+
subnav,
5659
h(
5760
'button',
5861
{
5962
class: ['js-btn-copy', 'btn', 'btn-sm', 'tooltipped', 'tooltipped-nw'],
60-
'data-clipboard-text': code,
6163
'aria-label': 'Copy code to clipboard',
64+
'data-clipboard': codeId,
6265
},
6366
btnIcon()
6467
),
68+
h('pre', { hidden: true, 'data-clipboard': codeId }, code),
6569
]
6670
)
6771
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import languages, { languageKeys } from '../lib/languages.js'
2+
import parser from 'accept-language-parser'
3+
4+
import { USER_LANGUAGE_COOKIE_NAME } from '../lib/constants.js'
5+
6+
const chineseRegions = [
7+
'CN', // Mainland
8+
'HK', // Hong Kong
9+
'SG', // Singapore
10+
'TW', // Taiwan
11+
]
12+
13+
function translationExists(language) {
14+
if (language.code === 'zh') {
15+
return chineseRegions.includes(language.region)
16+
}
17+
// 92BD1212-61B8-4E7A: Remove ` && !languages[language.code].wip` for the public ship of ko, fr, de, ru
18+
return languageKeys.includes(language.code) && !languages[language.code].wip
19+
}
20+
21+
function getLanguageCode(language) {
22+
return language.code === 'cn' && chineseRegions.includes(language.region) ? 'zh' : language.code
23+
}
24+
25+
function getUserLanguage(browserLanguages) {
26+
try {
27+
let numTopPreferences = 1
28+
for (let lang = 0; lang < browserLanguages.length; lang++) {
29+
// If language has multiple regions, Chrome adds the non-region language to list
30+
if (lang > 0 && browserLanguages[lang].code !== browserLanguages[lang - 1].code)
31+
numTopPreferences++
32+
if (translationExists(browserLanguages[lang]) && numTopPreferences < 3) {
33+
return getLanguageCode(browserLanguages[lang])
34+
}
35+
}
36+
} catch {
37+
return undefined
38+
}
39+
}
40+
41+
function getUserLanguageFromCookie(req) {
42+
const value = req.cookies[USER_LANGUAGE_COOKIE_NAME]
43+
// 92BD1212-61B8-4E7A: Remove ` && !languages[value].wip` for the public ship of ko, fr, de, ru
44+
if (value && languages[value] && !languages[value].wip) {
45+
return value
46+
}
47+
}
48+
49+
// determine language code from a path. Default to en if no valid match
50+
export function getLanguageCodeFromPath(path) {
51+
const maybeLanguage = (path.split('/')[path.startsWith('/_next/data/') ? 4 : 1] || '').slice(0, 2)
52+
return languageKeys.includes(maybeLanguage) ? maybeLanguage : 'en'
53+
}
54+
55+
export function getLanguageCodeFromHeader(req) {
56+
const browserLanguages = parser.parse(req.headers['accept-language'])
57+
return getUserLanguage(browserLanguages)
58+
}
59+
60+
export default function detectLanguage(req, res, next) {
61+
req.language = getLanguageCodeFromPath(req.path)
62+
// Detecting browser language by user preference
63+
req.userLanguage = getUserLanguageFromCookie(req)
64+
if (!req.userLanguage) {
65+
req.userLanguage = getLanguageCodeFromHeader(req)
66+
}
67+
return next()
68+
}

src/landings/pages/product.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useRouter } from 'next/router'
44
// "legacy" javascript needed to maintain existing functionality
55
// typically operating on elements **within** an article.
66
import copyCode from 'components/lib/copy-code'
7+
import toggleAnnotation from 'components/lib/toggle-annotations'
78
import localization from 'components/lib/localization'
89
import wrapCodeTerms from 'components/lib/wrap-code-terms'
910

@@ -41,6 +42,7 @@ function initiateArticleScripts() {
4142
copyCode()
4243
localization()
4344
wrapCodeTerms()
45+
toggleAnnotation()
4446
}
4547

4648
type Props = {

0 commit comments

Comments
 (0)