Merge branch 'main' into repo-sync
This commit is contained in:
@@ -2,7 +2,7 @@ import { useState, useEffect, useRef, ReactNode } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import debounce from 'lodash/debounce'
|
||||
import { useTranslation } from 'components/hooks/useTranslation'
|
||||
import { sendEvent } from '../javascripts/events'
|
||||
import { sendEvent, EventType } from '../javascripts/events'
|
||||
import { useMainContext } from './context/MainContext'
|
||||
import { useVersion } from 'components/hooks/useVersion'
|
||||
|
||||
@@ -126,7 +126,7 @@ export function Search({ isStandalone = false, updateSearchParams = true, childr
|
||||
// Analytics tracking
|
||||
if (xquery) {
|
||||
sendEvent({
|
||||
type: 'search',
|
||||
type: EventType.search,
|
||||
search_query: xquery,
|
||||
// search_context
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState, useRef } from 'react'
|
||||
import { ThumbsdownIcon, ThumbsupIcon } from '@primer/octicons-react'
|
||||
import { useTranslation } from 'components/hooks/useTranslation'
|
||||
import { Link } from 'components/Link'
|
||||
import { sendEvent } from '../javascripts/events'
|
||||
import { sendEvent, EventType } from '../javascripts/events'
|
||||
|
||||
enum ViewState {
|
||||
START = 'START',
|
||||
@@ -137,10 +137,10 @@ function trackEvent(formData: FormData | undefined) {
|
||||
if (!formData) return
|
||||
// Nota bene: convert empty strings to undefined
|
||||
return sendEvent({
|
||||
type: 'survey',
|
||||
survey_token: formData.get('survey-token') || undefined, // Honeypot
|
||||
type: EventType.survey,
|
||||
survey_token: (formData.get('survey-token') as string) || undefined, // Honeypot
|
||||
survey_vote: formData.get('survey-vote') === 'Y',
|
||||
survey_comment: formData.get('survey-comment') || undefined,
|
||||
survey_email: formData.get('survey-email') || undefined,
|
||||
survey_comment: (formData.get('survey-comment') as string) || undefined,
|
||||
survey_email: (formData.get('survey-email') as string) || undefined,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
export default function airgapLinks () {
|
||||
if (window.IS_NEXTJS_PAGE) return
|
||||
|
||||
// When in an airgapped environment,
|
||||
// show a tooltip on external links
|
||||
const { airgap } = JSON.parse(document.getElementById('expose').text)
|
||||
if (!airgap) return
|
||||
|
||||
const externaLinks = Array.from(
|
||||
document.querySelectorAll('a[href^="http"], a[href^="//"]')
|
||||
)
|
||||
externaLinks.forEach(link => {
|
||||
link.classList.add('tooltipped')
|
||||
link.setAttribute('aria-label', 'This link may not work in this environment.')
|
||||
link.setAttribute('rel', 'noopener')
|
||||
})
|
||||
}
|
||||
17
javascripts/airgap-links.ts
Normal file
17
javascripts/airgap-links.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export default function airgapLinks() {
|
||||
// @ts-ignore
|
||||
if (window.IS_NEXTJS_PAGE) return
|
||||
|
||||
// When in an airgapped environment,
|
||||
// show a tooltip on external links
|
||||
const exposeEl = document?.getElementById('expose') as HTMLScriptElement
|
||||
const { airgap } = JSON.parse(exposeEl.text)
|
||||
if (!airgap) return
|
||||
|
||||
const externaLinks = Array.from(document.querySelectorAll('a[href^="http"], a[href^="//"]'))
|
||||
externaLinks.forEach((link) => {
|
||||
link.classList.add('tooltipped')
|
||||
link.setAttribute('aria-label', 'This link may not work in this environment.')
|
||||
link.setAttribute('rel', 'noopener')
|
||||
})
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
/**
|
||||
* Handles the client-side events for `includes/all-articles.html`.
|
||||
*/
|
||||
export default function allArticles () {
|
||||
const buttons = document.querySelectorAll('button.js-all-articles-show-more')
|
||||
|
||||
for (const btn of buttons) {
|
||||
btn.addEventListener('click', evt => {
|
||||
// Show all hidden links
|
||||
const hiddenLinks = evt.currentTarget.parentElement.querySelectorAll('li.d-none')
|
||||
for (const link of hiddenLinks) {
|
||||
link.classList.remove('d-none')
|
||||
}
|
||||
// Remove the button, since we don't need it anymore
|
||||
evt.currentTarget.parentElement.removeChild(evt.currentTarget)
|
||||
})
|
||||
}
|
||||
}
|
||||
17
javascripts/all-articles.ts
Normal file
17
javascripts/all-articles.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Handles the client-side events for `includes/all-articles.html`.
|
||||
*/
|
||||
export default function allArticles() {
|
||||
const buttons = document.querySelectorAll('button.js-all-articles-show-more')
|
||||
|
||||
buttons.forEach((btn) =>
|
||||
btn.addEventListener('click', (evt) => {
|
||||
const target = evt.currentTarget as HTMLButtonElement
|
||||
// Show all hidden links
|
||||
const hiddenLinks = target?.parentElement?.querySelectorAll('li.d-none')
|
||||
hiddenLinks?.forEach((link) => link.classList.remove('d-none'))
|
||||
// Remove the button, since we don't need it anymore
|
||||
target?.parentElement?.removeChild(target)
|
||||
})
|
||||
)
|
||||
}
|
||||
3
javascripts/browser-date-formatter.d.ts
vendored
Normal file
3
javascripts/browser-date-formatter.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
declare module 'browser-date-formatter' {
|
||||
export default function browserDateFormatter(): void
|
||||
}
|
||||
@@ -3,12 +3,13 @@ export default () => {
|
||||
|
||||
if (!buttons) return
|
||||
|
||||
buttons.forEach(button =>
|
||||
button.addEventListener('click', async evt => {
|
||||
const text = button.dataset.clipboardText
|
||||
buttons.forEach((button) =>
|
||||
button.addEventListener('click', async () => {
|
||||
const text = (button as HTMLElement).dataset.clipboardText
|
||||
if (!text) return
|
||||
await navigator.clipboard.writeText(text)
|
||||
|
||||
const beforeTooltip = button.getAttribute('aria-label')
|
||||
const beforeTooltip = button.getAttribute('aria-label') || ''
|
||||
button.setAttribute('aria-label', 'Copied!')
|
||||
|
||||
setTimeout(() => {
|
||||
@@ -1,7 +1,7 @@
|
||||
const expandText = 'Expand All'
|
||||
const closeText = 'Close All'
|
||||
|
||||
export default function devToc () {
|
||||
export default function devToc() {
|
||||
const expandButton = document.querySelector('.js-expand')
|
||||
if (!expandButton) return
|
||||
|
||||
@@ -9,31 +9,29 @@ export default function devToc () {
|
||||
|
||||
expandButton.addEventListener('click', () => {
|
||||
// on click, toggle all the details elements open or closed
|
||||
const anyDetailsOpen = Array.from(detailsElements).find(details => details.open)
|
||||
const anyDetailsOpen = Array.from(detailsElements).find((details) => details.open)
|
||||
|
||||
for (const detailsElement of detailsElements) {
|
||||
anyDetailsOpen
|
||||
? detailsElement.removeAttribute('open')
|
||||
: detailsElement.open = true
|
||||
}
|
||||
detailsElements.forEach((detailsElement) => {
|
||||
anyDetailsOpen ? detailsElement.removeAttribute('open') : (detailsElement.open = true)
|
||||
})
|
||||
|
||||
// toggle the button text on click
|
||||
anyDetailsOpen
|
||||
? expandButton.textContent = expandText
|
||||
: expandButton.textContent = closeText
|
||||
? (expandButton.textContent = expandText)
|
||||
: (expandButton.textContent = closeText)
|
||||
})
|
||||
|
||||
// also toggle the button text on clicking any of the details elements
|
||||
for (const detailsElement of detailsElements) {
|
||||
detailsElements.forEach((detailsElement) => {
|
||||
detailsElement.addEventListener('click', () => {
|
||||
expandButton.textContent = closeText
|
||||
|
||||
// we can only get an accurate count of the open details elements if we wait a fraction after click
|
||||
setTimeout(() => {
|
||||
if (!Array.from(detailsElements).find(details => details.open)) {
|
||||
if (!Array.from(detailsElements).find((details) => details.open)) {
|
||||
expandButton.textContent = expandText
|
||||
}
|
||||
}, 50)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
import parseUserAgent from './user-agent'
|
||||
const supportedPlatforms = ['mac', 'windows', 'linux']
|
||||
const detectedPlatforms = new Set()
|
||||
|
||||
// Emphasize content for the visitor's OS (inferred from user agent string)
|
||||
|
||||
export default function displayPlatformSpecificContent () {
|
||||
let platform = getDefaultPlatform() || parseUserAgent().os
|
||||
|
||||
// adjust platform names to fit existing mac/windows/linux scheme
|
||||
if (!platform) platform = 'linux'
|
||||
if (platform === 'ios') platform = 'mac'
|
||||
|
||||
const platformsInContent = findPlatformSpecificContent(platform)
|
||||
|
||||
hideSwitcherLinks(platformsInContent)
|
||||
|
||||
showContentForPlatform(platform)
|
||||
|
||||
// configure links for switching platform content
|
||||
switcherLinks().forEach(link => {
|
||||
link.addEventListener('click', (event) => {
|
||||
event.preventDefault()
|
||||
showContentForPlatform(event.target.dataset.platform)
|
||||
findPlatformSpecificContent(event.target.dataset.platform)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function showContentForPlatform (platform) {
|
||||
// (de)activate switcher link appearances
|
||||
switcherLinks().forEach(link => {
|
||||
(link.dataset.platform === platform)
|
||||
? link.classList.add('selected')
|
||||
: link.classList.remove('selected')
|
||||
})
|
||||
}
|
||||
|
||||
function findPlatformSpecificContent (platform) {
|
||||
// find all platform-specific *block* elements and hide or show as appropriate
|
||||
// example: {{ #mac }} block content {{/mac}}
|
||||
Array.from(document.querySelectorAll('.extended-markdown'))
|
||||
.filter(el => supportedPlatforms.some(platform => el.classList.contains(platform)))
|
||||
.forEach(el => {
|
||||
detectPlatforms(el)
|
||||
el.style.display = el.classList.contains(platform)
|
||||
? ''
|
||||
: 'none'
|
||||
})
|
||||
|
||||
// find all platform-specific *inline* elements and hide or show as appropriate
|
||||
// example: <span class="platform-mac">inline content</span>
|
||||
Array.from(document.querySelectorAll('.platform-mac, .platform-windows, .platform-linux'))
|
||||
.forEach(el => {
|
||||
detectPlatforms(el)
|
||||
el.style.display = el.classList.contains('platform-' + platform)
|
||||
? ''
|
||||
: 'none'
|
||||
})
|
||||
|
||||
return Array.from(detectedPlatforms)
|
||||
}
|
||||
|
||||
// hide links for any platform-specific sections that are not present
|
||||
function hideSwitcherLinks (platformsInContent) {
|
||||
Array.from(document.querySelectorAll('a.platform-switcher'))
|
||||
.forEach(link => {
|
||||
if (platformsInContent.includes(link.dataset.platform)) return
|
||||
link.style.display = 'none'
|
||||
})
|
||||
}
|
||||
|
||||
function detectPlatforms (el) {
|
||||
el.classList.forEach(elClass => {
|
||||
const value = elClass.replace(/platform-/, '')
|
||||
if (supportedPlatforms.includes(value)) detectedPlatforms.add(value)
|
||||
})
|
||||
}
|
||||
|
||||
function getDefaultPlatform () {
|
||||
const el = document.querySelector('[data-default-platform]')
|
||||
if (el) return el.dataset.defaultPlatform
|
||||
}
|
||||
|
||||
function switcherLinks () {
|
||||
return Array.from(document.querySelectorAll('a.platform-switcher'))
|
||||
}
|
||||
91
javascripts/display-platform-specific-content.ts
Normal file
91
javascripts/display-platform-specific-content.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import parseUserAgent from './user-agent'
|
||||
const supportedPlatforms = ['mac', 'windows', 'linux']
|
||||
const detectedPlatforms = new Set()
|
||||
|
||||
// Emphasize content for the visitor's OS (inferred from user agent string)
|
||||
|
||||
export default function displayPlatformSpecificContent() {
|
||||
let platform = getDefaultPlatform() || parseUserAgent().os
|
||||
|
||||
// adjust platform names to fit existing mac/windows/linux scheme
|
||||
if (!platform) platform = 'linux'
|
||||
if (platform === 'ios') platform = 'mac'
|
||||
|
||||
const platformsInContent = findPlatformSpecificContent(platform)
|
||||
|
||||
hideSwitcherLinks(platformsInContent)
|
||||
|
||||
showContentForPlatform(platform)
|
||||
|
||||
// configure links for switching platform content
|
||||
switcherLinks().forEach((link) => {
|
||||
link.addEventListener('click', (event) => {
|
||||
event.preventDefault()
|
||||
const target = event.target as HTMLElement
|
||||
showContentForPlatform(target.dataset.platform || '')
|
||||
findPlatformSpecificContent(target.dataset.platform || '')
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function showContentForPlatform(platform: string) {
|
||||
// (de)activate switcher link appearances
|
||||
switcherLinks().forEach((link) => {
|
||||
link.dataset.platform === platform
|
||||
? link.classList.add('selected')
|
||||
: link.classList.remove('selected')
|
||||
})
|
||||
}
|
||||
|
||||
function findPlatformSpecificContent(platform: string) {
|
||||
// find all platform-specific *block* elements and hide or show as appropriate
|
||||
// example: {{ #mac }} block content {{/mac}}
|
||||
const markdowns = Array.from(
|
||||
document.querySelectorAll('.extended-markdown')
|
||||
) as Array<HTMLElement>
|
||||
markdowns
|
||||
.filter((el) => supportedPlatforms.some((platform) => el.classList.contains(platform)))
|
||||
.forEach((el) => {
|
||||
detectPlatforms(el)
|
||||
el.style.display = el.classList.contains(platform) ? '' : 'none'
|
||||
})
|
||||
|
||||
// find all platform-specific *inline* elements and hide or show as appropriate
|
||||
// example: <span class="platform-mac">inline content</span>
|
||||
const platforms = Array.from(
|
||||
document.querySelectorAll('.platform-mac, .platform-windows, .platform-linux')
|
||||
) as Array<HTMLElement>
|
||||
platforms.forEach((el) => {
|
||||
detectPlatforms(el)
|
||||
el.style.display = el.classList.contains('platform-' + platform) ? '' : 'none'
|
||||
})
|
||||
|
||||
return Array.from(detectedPlatforms) as Array<string>
|
||||
}
|
||||
|
||||
// hide links for any platform-specific sections that are not present
|
||||
function hideSwitcherLinks(platformsInContent: Array<string>) {
|
||||
const links = Array.from(
|
||||
document.querySelectorAll('a.platform-switcher')
|
||||
) as Array<HTMLAnchorElement>
|
||||
links.forEach((link) => {
|
||||
if (platformsInContent.includes(link.dataset.platform || '')) return
|
||||
link.style.display = 'none'
|
||||
})
|
||||
}
|
||||
|
||||
function detectPlatforms(el: HTMLElement) {
|
||||
el.classList.forEach((elClass) => {
|
||||
const value = elClass.replace(/platform-/, '')
|
||||
if (supportedPlatforms.includes(value)) detectedPlatforms.add(value)
|
||||
})
|
||||
}
|
||||
|
||||
function getDefaultPlatform() {
|
||||
const el = document.querySelector('[data-default-platform]') as HTMLElement
|
||||
if (el) return el.dataset.defaultPlatform
|
||||
}
|
||||
|
||||
function switcherLinks(): Array<HTMLAnchorElement> {
|
||||
return Array.from(document.querySelectorAll('a.platform-switcher'))
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
import Cookies from 'js-cookie'
|
||||
import { preserveAnchorNodePosition } from 'scroll-anchoring'
|
||||
|
||||
import { sendEvent } from './events'
|
||||
|
||||
const supportedTools = ['cli', 'desktop', 'webui', 'curl']
|
||||
|
||||
export default function displayToolSpecificContent () {
|
||||
const toolElements = Array.from(document.querySelectorAll('.extended-markdown'))
|
||||
.filter(el => supportedTools.some(tool => el.classList.contains(tool)))
|
||||
|
||||
const detectedTools = toolElements
|
||||
.flatMap(el => Array.from(el.classList).filter(className => supportedTools.includes(className)))
|
||||
|
||||
const tool = getDefaultTool(detectedTools)
|
||||
|
||||
showToolSpecificContent(tool, toolElements)
|
||||
|
||||
hideSwitcherLinks(detectedTools)
|
||||
|
||||
highlightTabForTool(tool)
|
||||
|
||||
// configure links for switching tool content
|
||||
switcherLinks().forEach(link => {
|
||||
link.addEventListener('click', (event) => {
|
||||
event.preventDefault()
|
||||
highlightTabForTool(event.target.dataset.tool)
|
||||
preserveAnchorNodePosition(document, () => {
|
||||
showToolSpecificContent(event.target.dataset.tool, toolElements)
|
||||
})
|
||||
|
||||
// Save this preference as a cookie.
|
||||
Cookies.set('toolPreferred', event.target.dataset.tool, { sameSite: 'strict', secure: true })
|
||||
|
||||
// Send event data
|
||||
sendEvent({
|
||||
type: 'preference',
|
||||
preference_name: 'application',
|
||||
preference_value: event.target.dataset.tool
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function highlightTabForTool (tool) {
|
||||
// (de)activate switcher link appearances
|
||||
switcherLinks().forEach(link => {
|
||||
(link.dataset.tool === tool)
|
||||
? link.classList.add('selected')
|
||||
: link.classList.remove('selected')
|
||||
})
|
||||
}
|
||||
|
||||
function showToolSpecificContent (tool, toolElements) {
|
||||
// show the content only for the highlighted tool
|
||||
toolElements
|
||||
.filter(el => supportedTools.some(tool => (el.classList.contains(tool))))
|
||||
.forEach(el => {
|
||||
el.style.display = (el.classList.contains(tool))
|
||||
? ''
|
||||
: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
// hide links for any tool-specific sections that are not present
|
||||
function hideSwitcherLinks (detectedTools) {
|
||||
Array.from(document.querySelectorAll('a.tool-switcher'))
|
||||
.forEach(link => {
|
||||
if (detectedTools.includes(link.dataset.tool)) return
|
||||
link.style.display = 'none'
|
||||
})
|
||||
}
|
||||
|
||||
function getDefaultTool (detectedTools) {
|
||||
// If the user selected a tool preference and the tool is present on this page
|
||||
const cookieValue = Cookies.get('toolPreferred')
|
||||
if (cookieValue && detectedTools.includes(cookieValue)) return cookieValue
|
||||
|
||||
// If there is a default tool and the tool is present on this page
|
||||
const defaultToolEl = document.querySelector('[data-default-tool]')
|
||||
if (defaultToolEl && detectedTools.includes(defaultToolEl.dataset.defaultTool)) {
|
||||
return defaultToolEl.dataset.defaultTool
|
||||
}
|
||||
|
||||
// Default to webui if present (this is generally the case where we show UI/CLI/Desktop info)
|
||||
if (detectedTools.includes('webui')) return 'webui'
|
||||
|
||||
// Default to cli if present (this is generally the case where we show curl/CLI info)
|
||||
if (detectedTools.includes('cli')) return 'cli'
|
||||
|
||||
// Otherwise, just choose the first detected tool
|
||||
return detectedTools[0]
|
||||
}
|
||||
|
||||
function switcherLinks () {
|
||||
return Array.from(document.querySelectorAll('a.tool-switcher'))
|
||||
}
|
||||
96
javascripts/display-tool-specific-content.ts
Normal file
96
javascripts/display-tool-specific-content.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import Cookies from 'js-cookie'
|
||||
|
||||
import { sendEvent, EventType } from './events'
|
||||
|
||||
const supportedTools = ['cli', 'desktop', 'webui', 'curl']
|
||||
|
||||
export default function displayToolSpecificContent() {
|
||||
const toolElements = Array.from(document.querySelectorAll('.extended-markdown')).filter((el) =>
|
||||
supportedTools.some((tool) => el.classList.contains(tool))
|
||||
) as Array<HTMLElement>
|
||||
|
||||
if (!toolElements.length) return
|
||||
|
||||
const detectedTools = toolElements.flatMap((el) =>
|
||||
Array.from(el.classList).filter((className) => supportedTools.includes(className))
|
||||
) as Array<string>
|
||||
|
||||
const tool = getDefaultTool(detectedTools)
|
||||
|
||||
showToolSpecificContent(tool, toolElements)
|
||||
|
||||
hideSwitcherLinks(detectedTools)
|
||||
|
||||
highlightTabForTool(tool)
|
||||
|
||||
// configure links for switching tool content
|
||||
switcherLinks().forEach((link) => {
|
||||
link.addEventListener('click', (event) => {
|
||||
event.preventDefault()
|
||||
const target = event.target as HTMLElement
|
||||
highlightTabForTool(target.dataset.tool || '')
|
||||
showToolSpecificContent(target.dataset.tool || '', toolElements)
|
||||
|
||||
// Save this preference as a cookie.
|
||||
Cookies.set('toolPreferred', target.dataset.tool || '', { sameSite: 'strict', secure: true })
|
||||
|
||||
// Send event data
|
||||
sendEvent({
|
||||
type: EventType.preference,
|
||||
preference_name: 'application',
|
||||
preference_value: target.dataset.tool,
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function highlightTabForTool(tool: string) {
|
||||
// (de)activate switcher link appearances
|
||||
switcherLinks().forEach((link) => {
|
||||
link.dataset.tool === tool ? link.classList.add('selected') : link.classList.remove('selected')
|
||||
})
|
||||
}
|
||||
|
||||
function showToolSpecificContent(tool: string, toolElements: Array<HTMLElement>) {
|
||||
// show the content only for the highlighted tool
|
||||
toolElements
|
||||
.filter((el) => supportedTools.some((tool) => el.classList.contains(tool)))
|
||||
.forEach((el) => {
|
||||
el.style.display = el.classList.contains(tool) ? '' : 'none'
|
||||
})
|
||||
}
|
||||
|
||||
// hide links for any tool-specific sections that are not present
|
||||
function hideSwitcherLinks(detectedTools: Array<string>) {
|
||||
const links = Array.from(document.querySelectorAll('a.tool-switcher')) as Array<HTMLAnchorElement>
|
||||
links.forEach((link) => {
|
||||
if (detectedTools.includes(link.dataset.tool || '')) return
|
||||
link.style.display = 'none'
|
||||
})
|
||||
}
|
||||
|
||||
function getDefaultTool(detectedTools: Array<string>): string {
|
||||
// If the user selected a tool preference and the tool is present on this page
|
||||
const cookieValue = Cookies.get('toolPreferred')
|
||||
if (cookieValue && detectedTools.includes(cookieValue)) return cookieValue
|
||||
|
||||
// If there is a default tool and the tool is present on this page
|
||||
const defaultToolEl = document.querySelector('[data-default-tool]') as HTMLElement
|
||||
const defaultToolValue = defaultToolEl.dataset.defaultTool
|
||||
if (defaultToolValue && detectedTools.includes(defaultToolValue)) {
|
||||
return defaultToolValue
|
||||
}
|
||||
|
||||
// Default to webui if present (this is generally the case where we show UI/CLI/Desktop info)
|
||||
if (detectedTools.includes('webui')) return 'webui'
|
||||
|
||||
// Default to cli if present (this is generally the case where we show curl/CLI info)
|
||||
if (detectedTools.includes('cli')) return 'cli'
|
||||
|
||||
// Otherwise, just choose the first detected tool
|
||||
return detectedTools[0]
|
||||
}
|
||||
|
||||
function switcherLinks() {
|
||||
return Array.from(document.querySelectorAll('a.tool-switcher')) as Array<HTMLAnchorElement>
|
||||
}
|
||||
@@ -1,246 +0,0 @@
|
||||
/* eslint-disable camelcase */
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import Cookies from 'js-cookie'
|
||||
import getCsrf from './get-csrf'
|
||||
import parseUserAgent from './user-agent'
|
||||
|
||||
const COOKIE_NAME = '_docs-events'
|
||||
|
||||
const startVisitTime = Date.now()
|
||||
|
||||
let cookieValue
|
||||
let pageEventId
|
||||
let maxScrollY = 0
|
||||
let pauseScrolling = false
|
||||
let sentExit = false
|
||||
|
||||
export function getUserEventsId () {
|
||||
if (cookieValue) return cookieValue
|
||||
cookieValue = Cookies.get(COOKIE_NAME)
|
||||
if (cookieValue) return cookieValue
|
||||
cookieValue = uuidv4()
|
||||
Cookies.set(COOKIE_NAME, cookieValue, {
|
||||
secure: true,
|
||||
sameSite: 'strict',
|
||||
expires: 365
|
||||
})
|
||||
return cookieValue
|
||||
}
|
||||
|
||||
export function sendEvent ({
|
||||
type,
|
||||
version = '1.0.0',
|
||||
// `= undefined` is a TypeScript hint.
|
||||
exit_render_duration = undefined,
|
||||
exit_first_paint = undefined,
|
||||
exit_dom_interactive = undefined,
|
||||
exit_dom_complete = undefined,
|
||||
exit_visit_duration = undefined,
|
||||
exit_scroll_length = undefined,
|
||||
link_url = undefined,
|
||||
search_query = undefined,
|
||||
search_context = undefined,
|
||||
navigate_label = undefined,
|
||||
survey_token = undefined, // Honeypot, doesn't exist in schema
|
||||
survey_vote = undefined,
|
||||
survey_comment = undefined,
|
||||
survey_email = undefined,
|
||||
experiment_name = undefined,
|
||||
experiment_variation = undefined,
|
||||
experiment_success = undefined,
|
||||
clipboard_operation = undefined,
|
||||
preference_name = undefined,
|
||||
preference_value = undefined
|
||||
}) {
|
||||
const body = {
|
||||
_csrf: getCsrf(),
|
||||
|
||||
type, // One of page, exit, link, search, navigate, survey, experiment, preference
|
||||
|
||||
context: {
|
||||
// Primitives
|
||||
event_id: uuidv4(),
|
||||
user: getUserEventsId(),
|
||||
version,
|
||||
created: new Date().toISOString(),
|
||||
page_event_id: pageEventId,
|
||||
|
||||
// Content information
|
||||
path: location.pathname,
|
||||
hostname: location.hostname,
|
||||
referrer: document.referrer,
|
||||
search: location.search,
|
||||
href: location.href,
|
||||
site_language: location.pathname.split('/')[1],
|
||||
|
||||
// Device information
|
||||
// os, os_version, browser, browser_version:
|
||||
...parseUserAgent(),
|
||||
viewport_width: document.documentElement.clientWidth,
|
||||
viewport_height: document.documentElement.clientHeight,
|
||||
|
||||
// Location information
|
||||
timezone: new Date().getTimezoneOffset() / -60,
|
||||
user_language: navigator.language,
|
||||
|
||||
// Preference information
|
||||
application_preference: Cookies.get('toolPreferred')
|
||||
},
|
||||
|
||||
// Page event
|
||||
// No extra fields
|
||||
|
||||
// Exit event
|
||||
exit_render_duration,
|
||||
exit_first_paint,
|
||||
exit_dom_interactive,
|
||||
exit_dom_complete,
|
||||
exit_visit_duration,
|
||||
exit_scroll_length,
|
||||
|
||||
// Link event
|
||||
link_url,
|
||||
|
||||
// Search event
|
||||
search_query,
|
||||
search_context,
|
||||
|
||||
// Navigate event
|
||||
navigate_label,
|
||||
|
||||
// Survey event
|
||||
survey_token, // Honeypot, doesn't exist in schema
|
||||
survey_vote,
|
||||
survey_comment,
|
||||
survey_email,
|
||||
|
||||
// Experiment event
|
||||
experiment_name,
|
||||
experiment_variation,
|
||||
experiment_success,
|
||||
|
||||
// Clipboard event
|
||||
clipboard_operation,
|
||||
|
||||
// Preference event
|
||||
preference_name,
|
||||
preference_value
|
||||
}
|
||||
const blob = new Blob([JSON.stringify(body)], { type: 'application/json' })
|
||||
navigator.sendBeacon('/events', blob)
|
||||
return body
|
||||
}
|
||||
|
||||
function getPerformance () {
|
||||
const paint = performance?.getEntriesByType('paint')?.find(
|
||||
({ name }) => name === 'first-contentful-paint'
|
||||
)
|
||||
const nav = performance?.getEntriesByType('navigation')?.[0]
|
||||
return {
|
||||
firstContentfulPaint: paint ? paint.startTime / 1000 : undefined,
|
||||
domInteractive: nav ? nav.domInteractive / 1000 : undefined,
|
||||
domComplete: nav ? nav.domComplete / 1000 : undefined,
|
||||
render: nav ? (nav.responseEnd - nav.requestStart) / 1000 : undefined
|
||||
}
|
||||
}
|
||||
|
||||
function trackScroll () {
|
||||
// Throttle the calculations to no more than five per second
|
||||
if (pauseScrolling) return
|
||||
pauseScrolling = true
|
||||
setTimeout(() => { pauseScrolling = false }, 200)
|
||||
|
||||
// Update maximum scroll position reached
|
||||
const scrollPosition = (
|
||||
(window.scrollY + window.innerHeight) /
|
||||
document.documentElement.scrollHeight
|
||||
)
|
||||
if (scrollPosition > maxScrollY) maxScrollY = scrollPosition
|
||||
}
|
||||
|
||||
function sendExit () {
|
||||
if (sentExit) return
|
||||
if (document.visibilityState !== 'hidden') return
|
||||
sentExit = true
|
||||
const {
|
||||
render,
|
||||
firstContentfulPaint,
|
||||
domInteractive,
|
||||
domComplete
|
||||
} = getPerformance()
|
||||
return sendEvent({
|
||||
type: 'exit',
|
||||
exit_render_duration: render,
|
||||
exit_first_paint: firstContentfulPaint,
|
||||
exit_dom_interactive: domInteractive,
|
||||
exit_dom_complete: domComplete,
|
||||
exit_visit_duration: (Date.now() - startVisitTime) / 1000,
|
||||
exit_scroll_length: maxScrollY
|
||||
})
|
||||
}
|
||||
|
||||
function initPageEvent () {
|
||||
const pageEvent = sendEvent({ type: 'page' })
|
||||
pageEventId = pageEvent?.context?.event_id
|
||||
}
|
||||
|
||||
function initClipboardEvent () {
|
||||
['copy', 'cut', 'paste'].forEach(verb => {
|
||||
document.documentElement.addEventListener(verb, () => {
|
||||
sendEvent({ type: 'clipboard', clipboard_operation: verb })
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function initLinkEvent () {
|
||||
document.documentElement.addEventListener('click', evt => {
|
||||
const link = evt.target.closest('a[href^="http"]')
|
||||
if (!link) return
|
||||
sendEvent({
|
||||
type: 'link',
|
||||
link_url: link.href
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function initExitEvent () {
|
||||
window.addEventListener('scroll', trackScroll)
|
||||
document.addEventListener('visibilitychange', sendExit)
|
||||
}
|
||||
|
||||
function initNavigateEvent () {
|
||||
if (!document.querySelector('.sidebar-products')) return
|
||||
|
||||
Array.from(
|
||||
document.querySelectorAll('.sidebar-products details')
|
||||
).forEach(details => details.addEventListener(
|
||||
'toggle',
|
||||
evt => sendEvent({
|
||||
type: 'navigate',
|
||||
navigate_label: `details ${evt.target.open ? 'open' : 'close'}: ${evt.target.querySelector('summary').innerText}`
|
||||
})
|
||||
))
|
||||
|
||||
document.querySelector('.sidebar-products').addEventListener('click', evt => {
|
||||
const link = evt.target.closest('a')
|
||||
if (!link) return
|
||||
sendEvent({
|
||||
type: 'navigate',
|
||||
navigate_label: `link: ${link.href}`
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export default function initializeEvents () {
|
||||
initPageEvent() // must come first
|
||||
initExitEvent()
|
||||
initLinkEvent()
|
||||
initClipboardEvent()
|
||||
initNavigateEvent()
|
||||
// print event in ./print.js
|
||||
// survey event in ./survey.js
|
||||
// experiment event in ./experiment.js
|
||||
// search event in ./search.js
|
||||
// redirect event in middleware/record-redirect.js
|
||||
// preference event in ./display-tool-specific-content.js
|
||||
}
|
||||
224
javascripts/events.ts
Normal file
224
javascripts/events.ts
Normal file
@@ -0,0 +1,224 @@
|
||||
/* eslint-disable camelcase */
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import Cookies from 'js-cookie'
|
||||
import getCsrf from './get-csrf'
|
||||
import parseUserAgent from './user-agent'
|
||||
|
||||
const COOKIE_NAME = '_docs-events'
|
||||
|
||||
const startVisitTime = Date.now()
|
||||
|
||||
let cookieValue: string | undefined
|
||||
let pageEventId: string | undefined
|
||||
let maxScrollY = 0
|
||||
let pauseScrolling = false
|
||||
let sentExit = false
|
||||
|
||||
export function getUserEventsId() {
|
||||
if (cookieValue) return cookieValue
|
||||
cookieValue = Cookies.get(COOKIE_NAME)
|
||||
if (cookieValue) return cookieValue
|
||||
cookieValue = uuidv4()
|
||||
Cookies.set(COOKIE_NAME, cookieValue, {
|
||||
secure: true,
|
||||
sameSite: 'strict',
|
||||
expires: 365,
|
||||
})
|
||||
return cookieValue
|
||||
}
|
||||
|
||||
export enum EventType {
|
||||
page = 'page',
|
||||
exit = 'exit',
|
||||
link = 'link',
|
||||
search = 'search',
|
||||
navigate = 'navigate',
|
||||
survey = 'survey',
|
||||
experiment = 'experiment',
|
||||
preference = 'preference',
|
||||
clipboard = 'clipboard',
|
||||
print = 'print',
|
||||
}
|
||||
|
||||
type SendEventProps = {
|
||||
type: EventType
|
||||
version?: string
|
||||
exit_render_duration?: number
|
||||
exit_first_paint?: number
|
||||
exit_dom_interactive?: number
|
||||
exit_dom_complete?: number
|
||||
exit_visit_duration?: number
|
||||
exit_scroll_length?: number
|
||||
link_url?: string
|
||||
search_query?: string
|
||||
search_context?: string
|
||||
navigate_label?: string
|
||||
survey_token?: string // Honeypot, doesn't exist in schema
|
||||
survey_vote?: boolean
|
||||
survey_comment?: string
|
||||
survey_email?: string
|
||||
experiment_name?: string
|
||||
experiment_variation?: string
|
||||
experiment_success?: boolean
|
||||
clipboard_operation?: string
|
||||
preference_name?: string
|
||||
preference_value?: string
|
||||
}
|
||||
|
||||
export function sendEvent({ type, version = '1.0.0', ...props }: SendEventProps) {
|
||||
const body = {
|
||||
_csrf: getCsrf(),
|
||||
|
||||
type,
|
||||
|
||||
context: {
|
||||
// Primitives
|
||||
event_id: uuidv4(),
|
||||
user: getUserEventsId(),
|
||||
version,
|
||||
created: new Date().toISOString(),
|
||||
page_event_id: pageEventId,
|
||||
|
||||
// Content information
|
||||
path: location.pathname,
|
||||
hostname: location.hostname,
|
||||
referrer: document.referrer,
|
||||
search: location.search,
|
||||
href: location.href,
|
||||
site_language: location.pathname.split('/')[1],
|
||||
|
||||
// Device information
|
||||
// os, os_version, browser, browser_version:
|
||||
...parseUserAgent(),
|
||||
viewport_width: document.documentElement.clientWidth,
|
||||
viewport_height: document.documentElement.clientHeight,
|
||||
|
||||
// Location information
|
||||
timezone: new Date().getTimezoneOffset() / -60,
|
||||
user_language: navigator.language,
|
||||
|
||||
// Preference information
|
||||
application_preference: Cookies.get('toolPreferred'),
|
||||
},
|
||||
|
||||
...props,
|
||||
}
|
||||
const blob = new Blob([JSON.stringify(body)], { type: 'application/json' })
|
||||
navigator.sendBeacon('/events', blob)
|
||||
return body
|
||||
}
|
||||
|
||||
function getPerformance() {
|
||||
const paint = performance
|
||||
?.getEntriesByType('paint')
|
||||
?.find(({ name }) => name === 'first-contentful-paint')
|
||||
const nav = performance?.getEntriesByType('navigation')?.[0] as
|
||||
| PerformanceNavigationTiming
|
||||
| undefined
|
||||
return {
|
||||
firstContentfulPaint: paint ? paint.startTime / 1000 : undefined,
|
||||
domInteractive: nav ? nav.domInteractive / 1000 : undefined,
|
||||
domComplete: nav ? nav.domComplete / 1000 : undefined,
|
||||
render: nav ? (nav.responseEnd - nav.requestStart) / 1000 : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
function trackScroll() {
|
||||
// Throttle the calculations to no more than five per second
|
||||
if (pauseScrolling) return
|
||||
pauseScrolling = true
|
||||
setTimeout(() => {
|
||||
pauseScrolling = false
|
||||
}, 200)
|
||||
|
||||
// Update maximum scroll position reached
|
||||
const scrollPixels = window.scrollY + window.innerHeight
|
||||
const scrollPosition = scrollPixels / document.documentElement.scrollHeight
|
||||
if (scrollPosition > maxScrollY) maxScrollY = scrollPosition
|
||||
}
|
||||
|
||||
function sendExit() {
|
||||
if (sentExit) return
|
||||
if (document.visibilityState !== 'hidden') return
|
||||
sentExit = true
|
||||
const { render, firstContentfulPaint, domInteractive, domComplete } = getPerformance()
|
||||
return sendEvent({
|
||||
type: EventType.exit,
|
||||
exit_render_duration: render,
|
||||
exit_first_paint: firstContentfulPaint,
|
||||
exit_dom_interactive: domInteractive,
|
||||
exit_dom_complete: domComplete,
|
||||
exit_visit_duration: (Date.now() - startVisitTime) / 1000,
|
||||
exit_scroll_length: maxScrollY,
|
||||
})
|
||||
}
|
||||
|
||||
function initPageEvent() {
|
||||
const pageEvent = sendEvent({ type: EventType.page })
|
||||
pageEventId = pageEvent?.context?.event_id
|
||||
}
|
||||
|
||||
function initClipboardEvent() {
|
||||
;['copy', 'cut', 'paste'].forEach((verb) => {
|
||||
document.documentElement.addEventListener(verb, () => {
|
||||
sendEvent({ type: EventType.clipboard, clipboard_operation: verb })
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function initLinkEvent() {
|
||||
document.documentElement.addEventListener('click', (evt) => {
|
||||
const target = evt.target as HTMLElement
|
||||
const link = target.closest('a[href^="http"]') as HTMLAnchorElement
|
||||
if (!link) return
|
||||
sendEvent({
|
||||
type: EventType.link,
|
||||
link_url: link.href,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function initExitEvent() {
|
||||
window.addEventListener('scroll', trackScroll)
|
||||
document.addEventListener('visibilitychange', sendExit)
|
||||
}
|
||||
|
||||
function initNavigateEvent() {
|
||||
if (!document.querySelector('.sidebar-products')) return
|
||||
|
||||
Array.from(document.querySelectorAll('.sidebar-products details')).forEach((details) =>
|
||||
details.addEventListener('toggle', (evt) => {
|
||||
const target = evt.target as HTMLDetailsElement
|
||||
sendEvent({
|
||||
type: EventType.navigate,
|
||||
navigate_label: `details ${target.open ? 'open' : 'close'}: ${
|
||||
target?.querySelector('summary')?.innerText
|
||||
}`,
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
document.querySelector('.sidebar-products')?.addEventListener('click', (evt) => {
|
||||
const target = evt.target as HTMLElement
|
||||
const link = target.closest('a') as HTMLAnchorElement
|
||||
if (!link) return
|
||||
sendEvent({
|
||||
type: EventType.navigate,
|
||||
navigate_label: `link: ${link.href}`,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export default function initializeEvents() {
|
||||
initPageEvent() // must come first
|
||||
initExitEvent()
|
||||
initLinkEvent()
|
||||
initClipboardEvent()
|
||||
initNavigateEvent()
|
||||
// print event in ./print.js
|
||||
// survey event in ./survey.js
|
||||
// experiment event in ./experiment.js
|
||||
// search event in ./search.js
|
||||
// redirect event in middleware/record-redirect.js
|
||||
// preference event in ./display-tool-specific-content.js
|
||||
}
|
||||
@@ -1,22 +1,22 @@
|
||||
import murmur from 'imurmurhash'
|
||||
import { getUserEventsId, sendEvent } from './events'
|
||||
import { getUserEventsId, sendEvent, EventType } from './events'
|
||||
// import h from './hyperscript'
|
||||
|
||||
const TREATMENT = 'TREATMENT'
|
||||
const CONTROL = 'CONTROL'
|
||||
|
||||
export function bucket (test) {
|
||||
export function bucket(test: string) {
|
||||
const id = getUserEventsId()
|
||||
const hash = murmur(test).hash(id).result()
|
||||
return hash % 2 ? TREATMENT : CONTROL
|
||||
}
|
||||
|
||||
export function sendSuccess (test) {
|
||||
export function sendSuccess(test: string) {
|
||||
return sendEvent({
|
||||
type: 'experiment',
|
||||
type: EventType.experiment,
|
||||
experiment_name: test,
|
||||
experiment_variation: bucket(test).toLowerCase(),
|
||||
experiment_success: true
|
||||
experiment_success: true,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
const explorerUrl = location.hostname === 'localhost'
|
||||
? 'http://localhost:3000'
|
||||
: 'https://graphql.github.com/explorer'
|
||||
|
||||
// Pass non-search query params to Explorer app via the iFrame
|
||||
export default function () {
|
||||
const graphiqlExplorer = document.getElementById('graphiql')
|
||||
const queryString = window.location.search
|
||||
|
||||
if (!(queryString && graphiqlExplorer)) return
|
||||
|
||||
window.onload = () => {
|
||||
graphiqlExplorer.contentWindow.postMessage(queryString, explorerUrl)
|
||||
}
|
||||
}
|
||||
16
javascripts/explorer.ts
Normal file
16
javascripts/explorer.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
const explorerUrl =
|
||||
location.hostname === 'localhost'
|
||||
? 'http://localhost:3000'
|
||||
: 'https://graphql.github.com/explorer'
|
||||
|
||||
// Pass non-search query params to Explorer app via the iFrame
|
||||
export default function () {
|
||||
const graphiqlExplorer = document.getElementById('graphiql') as HTMLIFrameElement
|
||||
const queryString = window.location.search
|
||||
|
||||
if (!(queryString && graphiqlExplorer)) return
|
||||
|
||||
window.onload = () => {
|
||||
graphiqlExplorer?.contentWindow?.postMessage(queryString, explorerUrl)
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,29 @@
|
||||
function matchCardBySearch (card, searchString) {
|
||||
function matchCardBySearch(card: HTMLElement, searchString: string) {
|
||||
const matchReg = new RegExp(searchString, 'i')
|
||||
// Check if this card matches - any `data-*` attribute contains the string
|
||||
return Object.keys(card.dataset).some(key => matchReg.test(card.dataset[key]))
|
||||
return Object.keys(card.dataset).some((key) => matchReg.test(card.dataset[key] || ''))
|
||||
}
|
||||
|
||||
function matchCardByAttribute (card, attribute, value) {
|
||||
function matchCardByAttribute(card: HTMLElement, attribute: string, value: string): boolean {
|
||||
if (attribute in card.dataset) {
|
||||
const allValues = card.dataset[attribute].split(',')
|
||||
return allValues.some(key => key === value)
|
||||
const allValues = (card.dataset[attribute] || '').split(',')
|
||||
return allValues.some((key) => key === value)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export default function cardsFilter () {
|
||||
const inputFilter = document.querySelector('.js-filter-card-filter')
|
||||
const dropdownFilters = document.querySelectorAll('.js-filter-card-filter-dropdown')
|
||||
const cards = Array.from(document.querySelectorAll('.js-filter-card'))
|
||||
const showMoreButton = document.querySelector('.js-filter-card-show-more')
|
||||
const noResults = document.querySelector('.js-filter-card-no-results')
|
||||
export default function cardsFilter() {
|
||||
const inputFilter = document.querySelector('.js-filter-card-filter') as HTMLInputElement
|
||||
const dropdownFilters = Array.from(
|
||||
document.querySelectorAll('.js-filter-card-filter-dropdown')
|
||||
) as Array<HTMLSelectElement>
|
||||
const cards = Array.from(document.querySelectorAll('.js-filter-card')) as Array<HTMLElement>
|
||||
const showMoreButton = document.querySelector('.js-filter-card-show-more') as HTMLButtonElement
|
||||
const noResults = document.querySelector('.js-filter-card-no-results') as HTMLElement
|
||||
// if jsFilterCardMax not set, assume no limit (well, at 99)
|
||||
// some landing pages don't include the button because the number of
|
||||
// guides is less than the max defined in includes/article-cards.html
|
||||
const maxCards = showMoreButton ? parseInt(showMoreButton.dataset.jsFilterCardMax || 99) : 99
|
||||
const maxCards = parseInt(showMoreButton?.dataset?.jsFilterCardMax || '') || 99
|
||||
|
||||
const noFilter = () => {
|
||||
if (showMoreButton) showMoreButton.classList.remove('d-none')
|
||||
@@ -36,8 +38,8 @@ export default function cardsFilter () {
|
||||
}
|
||||
}
|
||||
|
||||
const filterEventHandler = (evt) => {
|
||||
const { currentTarget } = evt
|
||||
const filterEventHandler = (evt: Event) => {
|
||||
const currentTarget = evt.currentTarget as HTMLSelectElement | HTMLInputElement
|
||||
const value = currentTarget.value
|
||||
|
||||
if (showMoreButton) showMoreButton.classList.add('d-none')
|
||||
@@ -46,7 +48,7 @@ export default function cardsFilter () {
|
||||
let hasMatches = false
|
||||
|
||||
for (let index = 0; index < cards.length; index++) {
|
||||
const card = cards[index]
|
||||
const card = cards[index] as HTMLElement
|
||||
|
||||
let cardMatches = false
|
||||
|
||||
@@ -62,7 +64,7 @@ export default function cardsFilter () {
|
||||
}
|
||||
|
||||
if (currentTarget.tagName === 'SELECT' && currentTarget.name) {
|
||||
const matches = []
|
||||
const matches: Array<boolean> = []
|
||||
// check all the other dropdowns
|
||||
dropdownFilters.forEach(({ name, value }) => {
|
||||
if (!name || !value) return
|
||||
@@ -75,7 +77,7 @@ export default function cardsFilter () {
|
||||
hasMatches = true
|
||||
continue
|
||||
}
|
||||
cardMatches = matches.every(value => value)
|
||||
cardMatches = matches.every((value) => value)
|
||||
}
|
||||
|
||||
if (cardMatches) {
|
||||
@@ -88,9 +90,9 @@ export default function cardsFilter () {
|
||||
|
||||
// If there wasn't at least one match, show the "no results" text
|
||||
if (!hasMatches) {
|
||||
noResults.classList.remove('d-none')
|
||||
noResults?.classList.remove('d-none')
|
||||
} else {
|
||||
noResults.classList.add('d-none')
|
||||
noResults?.classList.add('d-none')
|
||||
}
|
||||
|
||||
return hasMatches
|
||||
@@ -100,19 +102,20 @@ export default function cardsFilter () {
|
||||
inputFilter.addEventListener('keyup', (evt) => {
|
||||
const hasMatches = filterEventHandler(evt)
|
||||
if (!hasMatches) {
|
||||
document.querySelector('.js-filter-card-value').textContent = evt.currentTarget.value
|
||||
const cardValueEl = document.querySelector('.js-filter-card-value')
|
||||
if (cardValueEl) cardValueEl.textContent = (evt.currentTarget as HTMLInputElement)?.value
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (dropdownFilters) {
|
||||
dropdownFilters.forEach(filter => filter.addEventListener('change', filterEventHandler))
|
||||
dropdownFilters.forEach((filter) => filter.addEventListener('change', filterEventHandler))
|
||||
}
|
||||
|
||||
if (showMoreButton) {
|
||||
showMoreButton.addEventListener('click', evt => {
|
||||
showMoreButton.addEventListener('click', (evt: MouseEvent) => {
|
||||
// Number of cards that are currently visible
|
||||
const numShown = cards.filter(card => !card.classList.contains('d-none')).length
|
||||
const numShown = cards.filter((card) => !card.classList.contains('d-none')).length
|
||||
// We want to show n more cards
|
||||
const totalToShow = numShown + maxCards
|
||||
|
||||
@@ -131,7 +134,7 @@ export default function cardsFilter () {
|
||||
|
||||
// They're all shown now, we should hide the button
|
||||
if (totalToShow >= cards.length) {
|
||||
evt.currentTarget.classList.add('d-none')
|
||||
;(evt?.currentTarget as HTMLElement)?.classList.add('d-none')
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
export default function getCsrf () {
|
||||
const csrfEl = document
|
||||
.querySelector('meta[name="csrf-token"]')
|
||||
if (!csrfEl) return ''
|
||||
return csrfEl.getAttribute('content')
|
||||
}
|
||||
5
javascripts/get-csrf.ts
Normal file
5
javascripts/get-csrf.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export default function getCsrf() {
|
||||
const csrfEl = document.querySelector('meta[name="csrf-token"]')
|
||||
if (!csrfEl) return ''
|
||||
return csrfEl.getAttribute('content')
|
||||
}
|
||||
@@ -2,24 +2,24 @@ const xmlns = 'http://www.w3.org/2000/svg'
|
||||
|
||||
const plainObjectConstructor = {}.constructor
|
||||
|
||||
function exists (value) {
|
||||
function exists(value: any) {
|
||||
return value !== null && typeof value !== 'undefined'
|
||||
}
|
||||
|
||||
function isPlainObject (value) {
|
||||
function isPlainObject(value: any) {
|
||||
return value.constructor === plainObjectConstructor
|
||||
}
|
||||
|
||||
function isString (value) {
|
||||
function isString(value: any) {
|
||||
return typeof value === 'string'
|
||||
}
|
||||
|
||||
function renderChildren (el, children) {
|
||||
function renderChildren(el: HTMLElement | SVGElement, children: Array<any>) {
|
||||
for (const child of children) {
|
||||
if (isPlainObject(child)) {
|
||||
Object.entries(child)
|
||||
.filter(([key, value]) => exists(value))
|
||||
.forEach(([key, value]) => el.setAttribute(key, value))
|
||||
.filter(([, value]) => exists(value))
|
||||
.forEach(([key, value]) => el.setAttribute(key, value as string))
|
||||
} else if (Array.isArray(child)) {
|
||||
renderChildren(el, child)
|
||||
} else if (isString(child)) {
|
||||
@@ -30,7 +30,7 @@ function renderChildren (el, children) {
|
||||
}
|
||||
}
|
||||
|
||||
export default function h (tagName, ...children) {
|
||||
export default function h(tagName: string, ...children: Array<any>) {
|
||||
const el = ['svg', 'path'].includes(tagName)
|
||||
? document.createElementNS(xmlns, tagName)
|
||||
: document.createElement(tagName)
|
||||
@@ -39,6 +39,8 @@ export default function h (tagName, ...children) {
|
||||
}
|
||||
|
||||
export const tags = Object.fromEntries(
|
||||
['div', 'form', 'a', 'input', 'button', 'ol', 'li', 'mark']
|
||||
.map(tagName => [tagName, (...args) => h(tagName, ...args)])
|
||||
['div', 'form', 'a', 'input', 'button', 'ol', 'li', 'mark'].map((tagName) => [
|
||||
tagName,
|
||||
(...args: Array<any>) => h(tagName, ...args),
|
||||
])
|
||||
)
|
||||
@@ -1,5 +1,5 @@
|
||||
export default function () {
|
||||
const linkToEnglish = document.querySelector('#to-english-doc')
|
||||
const linkToEnglish = document.querySelector('#to-english-doc') as HTMLAnchorElement
|
||||
|
||||
if (linkToEnglish) {
|
||||
const pathname = window.location.pathname.split('/')
|
||||
@@ -1,9 +1,9 @@
|
||||
import { sendEvent } from './events'
|
||||
import { EventType, sendEvent } from './events'
|
||||
|
||||
export default function () {
|
||||
const printButtons = document.querySelectorAll('.js-print')
|
||||
|
||||
Array.from(printButtons).forEach(btn => {
|
||||
Array.from(printButtons).forEach((btn) => {
|
||||
// Open the print dialog when the button is clicked
|
||||
btn.addEventListener('click', () => {
|
||||
window.print()
|
||||
@@ -12,6 +12,6 @@ export default function () {
|
||||
|
||||
// Track print events
|
||||
window.onbeforeprint = function () {
|
||||
sendEvent({ type: 'print' })
|
||||
sendEvent({ type: EventType.print })
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
export default function releaseNotes () {
|
||||
if (window.next) return
|
||||
const patches = Array.from(document.querySelectorAll('.js-release-notes-patch'))
|
||||
if (patches.length === 0) return
|
||||
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
const { version } = entry.target.dataset
|
||||
const patchLink = document.querySelector(`.js-release-notes-patch-link[data-version="${version}"]`)
|
||||
patchLink.classList.toggle('selected', entry.isIntersecting)
|
||||
}
|
||||
}, {
|
||||
rootMargin: '-40% 0px -50%'
|
||||
})
|
||||
|
||||
patches.forEach(patch => {
|
||||
observer.observe(patch)
|
||||
})
|
||||
}
|
||||
26
javascripts/release-notes.ts
Normal file
26
javascripts/release-notes.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
export default function releaseNotes() {
|
||||
// @ts-ignore
|
||||
if (window.IS_NEXTJS_PAGE) return
|
||||
|
||||
const patches = Array.from(document.querySelectorAll('.js-release-notes-patch'))
|
||||
if (patches.length === 0) return
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
for (const entry of entries) {
|
||||
const { version } = (entry.target as HTMLElement).dataset
|
||||
const patchLink = document.querySelector(
|
||||
`.js-release-notes-patch-link[data-version="${version}"]`
|
||||
)
|
||||
patchLink?.classList.toggle('selected', entry.isIntersecting)
|
||||
}
|
||||
},
|
||||
{
|
||||
rootMargin: '-40% 0px -50%',
|
||||
}
|
||||
)
|
||||
|
||||
patches.forEach((patch) => {
|
||||
observer.observe(patch)
|
||||
})
|
||||
}
|
||||
@@ -3,10 +3,10 @@ export default function () {
|
||||
const PageTopBtn = document.getElementById('js-scroll-top')
|
||||
if (!PageTopBtn) return
|
||||
|
||||
PageTopBtn.addEventListener('click', (e) => {
|
||||
PageTopBtn.addEventListener('click', () => {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth'
|
||||
behavior: 'smooth',
|
||||
})
|
||||
})
|
||||
|
||||
3
javascripts/search-with-your-keyboard.d.ts
vendored
Normal file
3
javascripts/search-with-your-keyboard.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
declare module 'search-with-your-keyboard' {
|
||||
export default function searchWithYourKeyboard(inputSelector: string, hitsSelector: string): void
|
||||
}
|
||||
@@ -1,20 +1,21 @@
|
||||
import { tags } from './hyperscript'
|
||||
import { sendEvent } from './events'
|
||||
import { sendEvent, EventType } from './events'
|
||||
import searchWithYourKeyboard from 'search-with-your-keyboard'
|
||||
|
||||
let $searchInputContainer
|
||||
let $searchResultsContainer
|
||||
let $searchOverlay
|
||||
let $searchInput
|
||||
let $searchInputContainer: HTMLElement | null | undefined
|
||||
let $searchResultsContainer: HTMLElement | null | undefined
|
||||
let $searchOverlay: HTMLElement | null | undefined
|
||||
let $searchInput: HTMLInputElement | null | undefined
|
||||
|
||||
let isExplorerPage
|
||||
let isExplorerPage: boolean
|
||||
|
||||
// This is our default placeholder, but it can be localized with a <meta> tag
|
||||
let placeholder = 'Search topics, products...'
|
||||
let version
|
||||
let language
|
||||
let version: string
|
||||
let language: string
|
||||
|
||||
export default function search () {
|
||||
export default function search() {
|
||||
// @ts-ignore
|
||||
if (window.IS_NEXTJS_PAGE) return
|
||||
|
||||
// We don't want to mess with query params intended for the GraphQL Explorer
|
||||
@@ -26,30 +27,32 @@ export default function search () {
|
||||
if (!$searchInputContainer || !$searchResultsContainer) return
|
||||
|
||||
// This overlay exists so if you click off the search, it closes
|
||||
$searchOverlay = document.querySelector('.search-overlay-desktop')
|
||||
$searchOverlay = document.querySelector('.search-overlay-desktop') as HTMLElement
|
||||
|
||||
// There's an index for every version/language combination
|
||||
const {
|
||||
languages,
|
||||
versions,
|
||||
nonEnterpriseDefaultVersion
|
||||
} = JSON.parse(document.getElementById('expose').text).searchOptions
|
||||
const { languages, versions, nonEnterpriseDefaultVersion } = JSON.parse(
|
||||
(document.getElementById('expose') as HTMLScriptElement)?.text || ''
|
||||
).searchOptions
|
||||
version = deriveVersionFromPath(versions, nonEnterpriseDefaultVersion)
|
||||
language = deriveLanguageCodeFromPath(languages)
|
||||
|
||||
// Find search placeholder text in a <meta> tag, falling back to a default
|
||||
const $placeholderMeta = document.querySelector('meta[name="site.data.ui.search.placeholder"]')
|
||||
const $placeholderMeta = document.querySelector(
|
||||
'meta[name="site.data.ui.search.placeholder"]'
|
||||
) as HTMLMetaElement
|
||||
if ($placeholderMeta) {
|
||||
placeholder = $placeholderMeta.content
|
||||
}
|
||||
|
||||
// Write the search form into its container
|
||||
$searchInputContainer.append(tmplSearchInput())
|
||||
$searchInput = $searchInputContainer.querySelector('input')
|
||||
$searchInput = $searchInputContainer.querySelector('input') as HTMLInputElement
|
||||
|
||||
// Prevent 'enter' from refreshing the page
|
||||
$searchInputContainer.querySelector('form')
|
||||
.addEventListener('submit', evt => evt.preventDefault())
|
||||
;($searchInputContainer.querySelector('form') as HTMLFormElement).addEventListener(
|
||||
'submit',
|
||||
(evt) => evt.preventDefault()
|
||||
)
|
||||
|
||||
// Search when the user finished typing
|
||||
$searchInput.addEventListener('keyup', debounce(onSearch))
|
||||
@@ -67,12 +70,11 @@ export default function search () {
|
||||
}
|
||||
|
||||
// The home page and 404 pages have a standalone search
|
||||
function hasStandaloneSearch () {
|
||||
return document.getElementById('landing') ||
|
||||
document.querySelector('body.error-404') !== null
|
||||
function hasStandaloneSearch() {
|
||||
return document.getElementById('landing') || document.querySelector('body.error-404') !== null
|
||||
}
|
||||
|
||||
function toggleSearchDisplay () {
|
||||
function toggleSearchDisplay() {
|
||||
// Clear/close search, if ESC is clicked
|
||||
document.addEventListener('keyup', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
@@ -84,7 +86,7 @@ function toggleSearchDisplay () {
|
||||
if (hasStandaloneSearch()) return
|
||||
|
||||
// Open panel if input is clicked
|
||||
$searchInput.addEventListener('focus', openSearch)
|
||||
$searchInput?.addEventListener('focus', openSearch)
|
||||
|
||||
// Close panel if overlay is clicked
|
||||
if ($searchOverlay) {
|
||||
@@ -92,60 +94,63 @@ function toggleSearchDisplay () {
|
||||
}
|
||||
|
||||
// Open panel if page loads with query in the params/input
|
||||
if ($searchInput.value) {
|
||||
if ($searchInput?.value) {
|
||||
openSearch()
|
||||
}
|
||||
}
|
||||
|
||||
// On most pages, opens the search panel
|
||||
function openSearch () {
|
||||
$searchInput.classList.add('js-open')
|
||||
$searchResultsContainer.classList.add('js-open')
|
||||
$searchOverlay.classList.add('js-open')
|
||||
function openSearch() {
|
||||
$searchInput?.classList.add('js-open')
|
||||
$searchResultsContainer?.classList.add('js-open')
|
||||
$searchOverlay?.classList.add('js-open')
|
||||
}
|
||||
|
||||
// Close panel if not on homepage
|
||||
function closeSearch () {
|
||||
function closeSearch() {
|
||||
if (!hasStandaloneSearch()) {
|
||||
$searchInput.classList.remove('js-open')
|
||||
$searchResultsContainer.classList.remove('js-open')
|
||||
$searchOverlay.classList.remove('js-open')
|
||||
$searchInput?.classList.remove('js-open')
|
||||
$searchResultsContainer?.classList.remove('js-open')
|
||||
$searchOverlay?.classList.remove('js-open')
|
||||
}
|
||||
|
||||
$searchInput.value = ''
|
||||
if ($searchInput) $searchInput.value = ''
|
||||
onSearch()
|
||||
}
|
||||
|
||||
function deriveLanguageCodeFromPath (languageCodes) {
|
||||
function deriveLanguageCodeFromPath(languageCodes: Array<string>) {
|
||||
let languageCode = location.pathname.split('/')[1]
|
||||
if (!languageCodes.includes(languageCode)) languageCode = 'en'
|
||||
return languageCode
|
||||
}
|
||||
|
||||
function deriveVersionFromPath (allVersions, nonEnterpriseDefaultVersion) {
|
||||
function deriveVersionFromPath(
|
||||
allVersions: Record<string, string>,
|
||||
nonEnterpriseDefaultVersion: string
|
||||
) {
|
||||
// fall back to the non-enterprise default version (FPT currently) on the homepage, 404 page, etc.
|
||||
const versionStr = location.pathname.split('/')[2] || nonEnterpriseDefaultVersion
|
||||
return allVersions[versionStr] || allVersions[nonEnterpriseDefaultVersion]
|
||||
}
|
||||
|
||||
// Wait for the event to stop triggering for X milliseconds before responding
|
||||
function debounce (fn, delay = 300) {
|
||||
let timer
|
||||
return (...args) => {
|
||||
function debounce(fn: Function, delay = 300) {
|
||||
let timer: number
|
||||
return (...args: Array<any>) => {
|
||||
clearTimeout(timer)
|
||||
timer = setTimeout(() => fn.apply(null, args), delay)
|
||||
timer = window.setTimeout(() => fn.apply(null, args), delay)
|
||||
}
|
||||
}
|
||||
|
||||
// When the user finishes typing, update the results
|
||||
async function onSearch () {
|
||||
const query = $searchInput.value
|
||||
async function onSearch() {
|
||||
const query = $searchInput?.value || ''
|
||||
|
||||
// Update the URL with the search parameters in the query string
|
||||
// UNLESS this is the GraphQL Explorer page, where a query in the URL is a GraphQL query
|
||||
const pushUrl = new URL(location)
|
||||
pushUrl.search = query && !isExplorerPage ? new URLSearchParams({ query }) : ''
|
||||
history.pushState({}, '', pushUrl)
|
||||
const pushUrl = new URL(location.toString())
|
||||
pushUrl.search = query && !isExplorerPage ? new URLSearchParams({ query }).toString() : ''
|
||||
history.pushState({}, '', pushUrl.toString())
|
||||
|
||||
// If there's a query, call the endpoint
|
||||
// Otherwise, there's no results by default
|
||||
@@ -153,74 +158,78 @@ async function onSearch () {
|
||||
if (query.trim()) {
|
||||
const endpointUrl = new URL(location.origin)
|
||||
endpointUrl.pathname = '/search'
|
||||
endpointUrl.search = new URLSearchParams({ language, version, query })
|
||||
endpointUrl.search = new URLSearchParams({ language, version, query }).toString()
|
||||
|
||||
const response = await fetch(endpointUrl, {
|
||||
const response = await fetch(endpointUrl.toString(), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
results = response.ok ? await response.json() : []
|
||||
}
|
||||
|
||||
// Either way, update the display
|
||||
$searchResultsContainer.querySelectorAll('*').forEach(el => el.remove())
|
||||
$searchResultsContainer.append(
|
||||
tmplSearchResults(results)
|
||||
)
|
||||
$searchResultsContainer?.querySelectorAll('*').forEach((el) => el.remove())
|
||||
$searchResultsContainer?.append(tmplSearchResults(results))
|
||||
toggleStandaloneSearch()
|
||||
|
||||
// Analytics tracking
|
||||
if (query.trim()) {
|
||||
sendEvent({
|
||||
type: 'search',
|
||||
search_query: query
|
||||
type: EventType.search,
|
||||
search_query: query,
|
||||
// search_context
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// If on homepage, toggle results container if query is present
|
||||
function toggleStandaloneSearch () {
|
||||
function toggleStandaloneSearch() {
|
||||
if (!hasStandaloneSearch()) return
|
||||
|
||||
const query = $searchInput.value
|
||||
const queryPresent = query && query.length > 0
|
||||
const $results = document.querySelector('.ais-Hits')
|
||||
const query = $searchInput?.value
|
||||
const queryPresent = Boolean(query && query.length > 0)
|
||||
const $results = document.querySelector('.ais-Hits') as HTMLElement
|
||||
|
||||
// Primer classNames for showing and hiding the results container
|
||||
const activeClass = $searchResultsContainer.getAttribute('data-active-class')
|
||||
const inactiveClass = $searchResultsContainer.getAttribute('data-inactive-class')
|
||||
const activeClass = $searchResultsContainer?.getAttribute('data-active-class')
|
||||
const inactiveClass = $searchResultsContainer?.getAttribute('data-inactive-class')
|
||||
|
||||
if (!activeClass) {
|
||||
console.error('container is missing required `data-active-class` attribute', $searchResultsContainer)
|
||||
console.error(
|
||||
'container is missing required `data-active-class` attribute',
|
||||
$searchResultsContainer
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (!inactiveClass) {
|
||||
console.error('container is missing required `data-inactive-class` attribute', $searchResultsContainer)
|
||||
console.error(
|
||||
'container is missing required `data-inactive-class` attribute',
|
||||
$searchResultsContainer
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// hide the container when no query is present
|
||||
$searchResultsContainer.classList.toggle(activeClass, queryPresent)
|
||||
$searchResultsContainer.classList.toggle(inactiveClass, !queryPresent)
|
||||
$searchResultsContainer?.classList.toggle(activeClass, queryPresent)
|
||||
$searchResultsContainer?.classList.toggle(inactiveClass, !queryPresent)
|
||||
|
||||
if (queryPresent && $results) $results.style.display = 'block'
|
||||
}
|
||||
|
||||
// If the user shows up with a query in the URL, go ahead and search for it
|
||||
function parseExistingSearch () {
|
||||
function parseExistingSearch() {
|
||||
const params = new URLSearchParams(location.search)
|
||||
if (!params.has('query')) return
|
||||
$searchInput.value = params.get('query')
|
||||
if ($searchInput) $searchInput.value = params.get('query') || ''
|
||||
onSearch()
|
||||
}
|
||||
|
||||
/** * Template functions ***/
|
||||
|
||||
function tmplSearchInput () {
|
||||
function tmplSearchInput() {
|
||||
// only autofocus on the homepage, and only if no #hash is present in the URL
|
||||
const autofocus = (hasStandaloneSearch() && !location.hash.length) || null
|
||||
const { div, form, input, button } = tags
|
||||
@@ -237,40 +246,47 @@ function tmplSearchInput () {
|
||||
autocorrect: 'off',
|
||||
autocapitalize: 'off',
|
||||
spellcheck: 'false',
|
||||
maxlength: '512'
|
||||
maxlength: '512',
|
||||
}),
|
||||
button({
|
||||
class: 'ais-SearchBox-submit',
|
||||
type: 'submit',
|
||||
title: 'Submit the search query.',
|
||||
hidden: true
|
||||
hidden: true,
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
function tmplSearchResults (items) {
|
||||
type SearchResult = {
|
||||
url: string
|
||||
breadcrumbs: string
|
||||
heading: string
|
||||
title: string
|
||||
content: string
|
||||
}
|
||||
|
||||
function tmplSearchResults(items: Array<SearchResult>) {
|
||||
const { div, ol, li } = tags
|
||||
return div(
|
||||
{ class: 'ais-Hits', style: 'display:block' },
|
||||
ol(
|
||||
{ class: 'ais-Hits-list' },
|
||||
items.map(item => li(
|
||||
{ class: 'ais-Hits-item' },
|
||||
tmplSearchResult(item)
|
||||
))
|
||||
items.map((item) => li({ class: 'ais-Hits-item' }, tmplSearchResult(item)))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
function tmplSearchResult ({ url, breadcrumbs, heading, title, content }) {
|
||||
function tmplSearchResult({ url, breadcrumbs, heading, title, content }: SearchResult) {
|
||||
const { div, a } = tags
|
||||
return div(
|
||||
{ class: 'search-result border-top color-border-secondary py-3 px-2' },
|
||||
a(
|
||||
{ href: url, class: 'no-underline' },
|
||||
div(
|
||||
{ class: 'search-result-breadcrumbs d-block color-text-primary opacity-60 text-small pb-1' },
|
||||
{
|
||||
class: 'search-result-breadcrumbs d-block color-text-primary opacity-60 text-small pb-1',
|
||||
},
|
||||
// Breadcrumbs in search records don't include the page title
|
||||
markify(breadcrumbs || '')
|
||||
),
|
||||
@@ -279,18 +295,13 @@ function tmplSearchResult ({ url, breadcrumbs, heading, title, content }) {
|
||||
// Display page title and heading (if present exists)
|
||||
markify(heading ? `${title}: ${heading}` : title)
|
||||
),
|
||||
div(
|
||||
{ class: 'search-result-content d-block color-text-secondary' },
|
||||
markify(content)
|
||||
)
|
||||
div({ class: 'search-result-content d-block color-text-secondary' }, markify(content))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Convert mark tags in search responses
|
||||
function markify (text) {
|
||||
function markify(text: string) {
|
||||
const { mark } = tags
|
||||
return text
|
||||
.split(/<\/?mark>/g)
|
||||
.map((el, i) => i % 2 ? mark(el) : el)
|
||||
return text.split(/<\/?mark>/g).map((el, i) => (i % 2 ? mark(el) : el))
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
export default function setNextEnv () {
|
||||
export default function setNextEnv() {
|
||||
// @ts-ignore
|
||||
window.IS_NEXTJS_PAGE = !!document.querySelector('#__next')
|
||||
}
|
||||
@@ -10,20 +10,21 @@
|
||||
* <div class="js-show-more-item d-none">hidden item</div>
|
||||
* <button class="js-show-more-button" data-js-show-more-items="1">show one more item</button>
|
||||
* </div>
|
||||
*/
|
||||
*/
|
||||
|
||||
export default function showMore () {
|
||||
export default function showMore() {
|
||||
const buttons = document.querySelectorAll('.js-show-more-button')
|
||||
|
||||
for (const btn of buttons) {
|
||||
btn.addEventListener('click', evt => {
|
||||
const container = evt.currentTarget.closest('.js-show-more-container')
|
||||
buttons.forEach((btn) => {
|
||||
btn.addEventListener('click', (evt) => {
|
||||
const target = evt.currentTarget as HTMLButtonElement
|
||||
const container = target.closest('.js-show-more-container')
|
||||
if (!container) return
|
||||
const hiddenLinks = container.querySelectorAll('.js-show-more-item.d-none')
|
||||
// get number of items to show more of, if not set, show all remaining items
|
||||
const showMoreNum = evt.currentTarget.dataset.jsShowMoreItems || hiddenLinks.length
|
||||
const showMoreNum = target.dataset.jsShowMoreItems || hiddenLinks.length
|
||||
let count = 0
|
||||
for (const link of hiddenLinks) {
|
||||
for (const link of Array.from(hiddenLinks)) {
|
||||
if (count++ >= showMoreNum) {
|
||||
break
|
||||
}
|
||||
@@ -31,8 +32,8 @@ export default function showMore () {
|
||||
}
|
||||
// Remove the button if all items have been shown
|
||||
if (container.querySelectorAll('.js-show-more-item.d-none').length === 0) {
|
||||
evt.currentTarget.parentElement.removeChild(evt.currentTarget)
|
||||
target?.parentElement?.removeChild(target)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,28 +1,30 @@
|
||||
export default function () {
|
||||
// TODO override active classes set on server side if sidebar elements are clicked
|
||||
|
||||
const activeMenuItem = document.querySelector('.sidebar .active')
|
||||
const activeMenuItem = document.querySelector('.sidebar .active') as HTMLElement
|
||||
if (!activeMenuItem) return
|
||||
|
||||
const verticalBufferAboveActiveItem = 40
|
||||
const activeMenuItemPosition = activeMenuItem.offsetTop - verticalBufferAboveActiveItem
|
||||
const menu = document.querySelector('.sidebar')
|
||||
|
||||
if (activeMenuItemPosition > (window.innerHeight * 0.5)) {
|
||||
menu.scrollTo(0, activeMenuItemPosition)
|
||||
if (activeMenuItemPosition > window.innerHeight * 0.5) {
|
||||
menu?.scrollTo(0, activeMenuItemPosition)
|
||||
}
|
||||
|
||||
// if the active category is a standalone category, do not close the other open dropdowns
|
||||
const activeStandaloneCategory = document.querySelectorAll('.sidebar-category.active.standalone-category')
|
||||
const activeStandaloneCategory = document.querySelectorAll(
|
||||
'.sidebar-category.active.standalone-category'
|
||||
)
|
||||
if (activeStandaloneCategory.length) return
|
||||
|
||||
const allOpenDetails = document.querySelectorAll('.sidebar-category:not(.active) details[open]')
|
||||
|
||||
if (allOpenDetails) {
|
||||
for (const openDetail of allOpenDetails) {
|
||||
for (const openDetail of Array.from(allOpenDetails)) {
|
||||
openDetail.removeAttribute('open')
|
||||
const svgArrowElem = openDetail.querySelector('summary > div > svg')
|
||||
svgArrowElem.remove()
|
||||
svgArrowElem?.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
import { sendEvent } from './events'
|
||||
|
||||
function showElement (el) {
|
||||
el.removeAttribute('hidden')
|
||||
}
|
||||
|
||||
function hideElement (el) {
|
||||
el.setAttribute('hidden', true)
|
||||
}
|
||||
|
||||
function updateDisplay (form, state) {
|
||||
Array.from(
|
||||
form.querySelectorAll(
|
||||
['start', 'yes', 'no', 'end']
|
||||
.map(xstate => '[data-help-' + xstate + ']')
|
||||
.join(',')
|
||||
)
|
||||
)
|
||||
.forEach(hideElement)
|
||||
Array.from(form.querySelectorAll('[data-help-' + state + ']'))
|
||||
.forEach(showElement)
|
||||
}
|
||||
|
||||
function submitForm (form) {
|
||||
const formData = new FormData(form)
|
||||
const data = Object.fromEntries(
|
||||
Array.from(formData.entries())
|
||||
.map(
|
||||
([key, value]) => [
|
||||
key.replace('survey-', ''),
|
||||
value || undefined // Convert empty strings to undefined
|
||||
]
|
||||
)
|
||||
)
|
||||
return trackEvent(data)
|
||||
}
|
||||
|
||||
function trackEvent ({ token, vote, email, comment }) {
|
||||
return sendEvent({
|
||||
type: 'survey',
|
||||
token, // Honeypot
|
||||
survey_vote: vote === 'Yes',
|
||||
survey_comment: comment,
|
||||
survey_email: email
|
||||
})
|
||||
}
|
||||
|
||||
export default function survey () {
|
||||
if (window.IS_NEXTJS_PAGE) return
|
||||
|
||||
const form = document.querySelector('.js-survey')
|
||||
const texts = Array.from(document.querySelectorAll('.js-survey input, .js-survey textarea'))
|
||||
const votes = Array.from(document.querySelectorAll('.js-survey [type=radio]'))
|
||||
if (!form || !texts.length || !votes.length) return
|
||||
|
||||
form.addEventListener('submit', evt => {
|
||||
evt.preventDefault()
|
||||
submitForm(form)
|
||||
updateDisplay(form, 'end')
|
||||
})
|
||||
|
||||
votes.forEach(voteEl => {
|
||||
voteEl.addEventListener('change', evt => {
|
||||
const state = evt.target.value.toLowerCase()
|
||||
const form = voteEl.closest('form')
|
||||
submitForm(form)
|
||||
updateDisplay(form, state)
|
||||
})
|
||||
})
|
||||
|
||||
// Prevent the site search from overtaking your input
|
||||
texts.forEach(text => {
|
||||
text.addEventListener('keydown', evt => {
|
||||
if (evt.code === 'Slash') evt.stopPropagation()
|
||||
})
|
||||
})
|
||||
}
|
||||
69
javascripts/survey.ts
Normal file
69
javascripts/survey.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { sendEvent, EventType } from './events'
|
||||
|
||||
function showElement(el: HTMLElement) {
|
||||
el.removeAttribute('hidden')
|
||||
}
|
||||
|
||||
function hideElement(el: HTMLElement) {
|
||||
el.setAttribute('hidden', 'hidden')
|
||||
}
|
||||
|
||||
function updateDisplay(form: HTMLFormElement, state: string) {
|
||||
const allSelector = ['start', 'yes', 'no', 'end']
|
||||
.map((xstate) => '[data-help-' + xstate + ']')
|
||||
.join(',')
|
||||
const stateSelector = '[data-help-' + state + ']'
|
||||
const allEls = Array.from(form.querySelectorAll(allSelector)) as Array<HTMLElement>
|
||||
allEls.forEach(hideElement)
|
||||
const stateEls = Array.from(form.querySelectorAll(stateSelector)) as Array<HTMLElement>
|
||||
stateEls.forEach(showElement)
|
||||
}
|
||||
|
||||
function submitForm(form: HTMLFormElement) {
|
||||
const formData = new FormData(form)
|
||||
return trackEvent(formData)
|
||||
}
|
||||
|
||||
function trackEvent(formData: FormData) {
|
||||
return sendEvent({
|
||||
type: EventType.survey,
|
||||
survey_token: (formData.get('survey-token') as string) || undefined, // Honeypot
|
||||
survey_vote: formData.get('survey-vote') === 'Yes',
|
||||
survey_comment: (formData.get('survey-comment') as string) || undefined,
|
||||
survey_email: (formData.get('survey-email') as string) || undefined,
|
||||
})
|
||||
}
|
||||
|
||||
export default function survey() {
|
||||
// @ts-ignore
|
||||
if (window.IS_NEXTJS_PAGE) return
|
||||
|
||||
const form = document.querySelector('.js-survey') as HTMLFormElement | null
|
||||
const texts = Array.from(
|
||||
document.querySelectorAll('.js-survey input, .js-survey textarea')
|
||||
) as Array<HTMLElement>
|
||||
const votes = Array.from(document.querySelectorAll('.js-survey [type=radio]'))
|
||||
if (!form || !texts.length || !votes.length) return
|
||||
|
||||
form.addEventListener('submit', (evt) => {
|
||||
evt.preventDefault()
|
||||
submitForm(form)
|
||||
updateDisplay(form, 'end')
|
||||
})
|
||||
|
||||
votes.forEach((voteEl) => {
|
||||
voteEl.addEventListener('change', (evt) => {
|
||||
const radio = evt.target as HTMLInputElement
|
||||
const state = radio.value.toLowerCase()
|
||||
submitForm(form)
|
||||
updateDisplay(form, state)
|
||||
})
|
||||
})
|
||||
|
||||
// Prevent the site search from overtaking your input
|
||||
texts.forEach((text) => {
|
||||
text.addEventListener('keydown', (evt: KeyboardEvent) => {
|
||||
if (evt.code === 'Slash') evt.stopPropagation()
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -17,7 +17,7 @@ export default function () {
|
||||
|
||||
// If there are no images on the page, return!
|
||||
// Don't include images in tables, which are already small and shouldn't be hidden.
|
||||
const images = [...document.querySelectorAll('img')].filter(img => !img.closest('table'))
|
||||
const images = Array.from(document.querySelectorAll('img')).filter((img) => !img.closest('table'))
|
||||
if (!images.length) return
|
||||
|
||||
// The button is hidden by default so it doesn't appear on browsers with JS disabled.
|
||||
@@ -25,7 +25,7 @@ export default function () {
|
||||
toggleImagesBtn.removeAttribute('hidden')
|
||||
|
||||
// Look for a cookie with image visibility preference; otherwise, use the default.
|
||||
const hideImagesPreferred = (Cookies.get('hideImagesPreferred') === 'true') || hideImagesByDefault
|
||||
const hideImagesPreferred = Cookies.get('hideImagesPreferred') === 'true' || hideImagesByDefault
|
||||
|
||||
// Hide the images if that is the preference.
|
||||
if (hideImagesPreferred) {
|
||||
@@ -37,15 +37,15 @@ export default function () {
|
||||
const onIcon = document.getElementById('js-on-icon')
|
||||
|
||||
// Get the aria-labels from the span elements for the tooltips.
|
||||
const tooltipImagesOff = offIcon.getAttribute('aria-label')
|
||||
const tooltipImagesOn = onIcon.getAttribute('aria-label')
|
||||
const tooltipImagesOff = offIcon?.getAttribute('aria-label') || ''
|
||||
const tooltipImagesOn = onIcon?.getAttribute('aria-label') || ''
|
||||
|
||||
// Set the starting state depending on user preferences.
|
||||
if (hideImagesPreferred) {
|
||||
offIcon.removeAttribute('hidden')
|
||||
offIcon?.removeAttribute('hidden')
|
||||
toggleImagesBtn.setAttribute('aria-label', tooltipImagesOff)
|
||||
} else {
|
||||
onIcon.removeAttribute('hidden')
|
||||
onIcon?.removeAttribute('hidden')
|
||||
toggleImagesBtn.setAttribute('aria-label', tooltipImagesOn)
|
||||
}
|
||||
|
||||
@@ -53,27 +53,32 @@ export default function () {
|
||||
// If images are not hidden by default, showOnNextClick should be true.
|
||||
let showOnNextClick = !hideImagesPreferred
|
||||
|
||||
toggleImagesBtn.addEventListener('click', (e) => {
|
||||
toggleImagesBtn.addEventListener('click', () => {
|
||||
if (showOnNextClick) {
|
||||
// Button should say "Images are off" on first click (depending on prefs)
|
||||
offIcon.removeAttribute('hidden')
|
||||
onIcon.setAttribute('hidden', true)
|
||||
offIcon?.removeAttribute('hidden')
|
||||
onIcon?.setAttribute('hidden', 'hidden')
|
||||
toggleImagesBtn.setAttribute('aria-label', tooltipImagesOff)
|
||||
toggleImages(images, 'hide')
|
||||
} else {
|
||||
// Button should say "Images are on" on another click
|
||||
offIcon.setAttribute('hidden', true)
|
||||
onIcon.removeAttribute('hidden')
|
||||
offIcon?.setAttribute('hidden', 'hidden')
|
||||
onIcon?.removeAttribute('hidden')
|
||||
toggleImagesBtn.setAttribute('aria-label', tooltipImagesOn)
|
||||
toggleImages(images, 'show')
|
||||
}
|
||||
|
||||
// Remove focus from the button after click so the tooltip does not stay displayed.
|
||||
// Use settimeout to work around Firefox-specific issue.
|
||||
setTimeout(() => { toggleImagesBtn.blur() }, 100)
|
||||
setTimeout(() => {
|
||||
toggleImagesBtn.blur()
|
||||
}, 100)
|
||||
|
||||
// Save this preference as a cookie.
|
||||
Cookies.set('hideImagesPreferred', showOnNextClick, { sameSite: 'strict', secure: true })
|
||||
Cookies.set('hideImagesPreferred', showOnNextClick.toString(), {
|
||||
sameSite: 'strict',
|
||||
secure: true,
|
||||
})
|
||||
|
||||
// Toggle the action on every click.
|
||||
showOnNextClick = !showOnNextClick
|
||||
@@ -83,28 +88,28 @@ export default function () {
|
||||
})
|
||||
}
|
||||
|
||||
function toggleImages (images, action) {
|
||||
function toggleImages(images: Array<HTMLImageElement>, action: string) {
|
||||
for (const img of images) {
|
||||
toggleImage(img, action)
|
||||
}
|
||||
}
|
||||
|
||||
function toggleImage (img, action) {
|
||||
const parentEl = img.parentNode
|
||||
function toggleImage(img: HTMLImageElement, action: string) {
|
||||
const parentEl = img.parentNode as HTMLElement
|
||||
|
||||
// Style the parent element and image depending on the state.
|
||||
if (action === 'show') {
|
||||
img.src = img.getAttribute('originalSrc')
|
||||
img.src = img.getAttribute('originalSrc') || ''
|
||||
img.style.border = '2px solid var(--color-auto-gray-2)'
|
||||
parentEl.style.display = 'block'
|
||||
parentEl.style['margin-top'] = '20px'
|
||||
parentEl.style.marginTop = '20px'
|
||||
parentEl.style.padding = '10px 0'
|
||||
} else {
|
||||
if (!img.getAttribute('originalSrc')) img.setAttribute('originalSrc', img.src)
|
||||
img.src = placeholderImagePath
|
||||
img.style.border = 'none'
|
||||
parentEl.style.display = 'inline'
|
||||
parentEl.style['margin-top'] = '0'
|
||||
parentEl.style.marginTop = '0'
|
||||
parentEl.style.padding = '1px 6px'
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ const OS_REGEXPS = [
|
||||
/(windows) ([^);]+)/i,
|
||||
/(android) ([^);]+)/i,
|
||||
/(cros) ([^);]+)/i,
|
||||
/(linux) ([^);]+)/i
|
||||
/(linux) ([^);]+)/i,
|
||||
]
|
||||
|
||||
// The order matters with these
|
||||
@@ -16,17 +16,15 @@ const BROWSER_REGEXPS = [
|
||||
/(edge)\/([^\s)]+)/i,
|
||||
/(chrome)\/([^\s)]+)/i,
|
||||
/(safari)\/([^\s)]+)/i,
|
||||
/ms(ie)\/([^\s)]+)/i
|
||||
/ms(ie)\/([^\s)]+)/i,
|
||||
]
|
||||
|
||||
export default function parseUserAgent (ua = navigator.userAgent) {
|
||||
export default function parseUserAgent(ua = navigator.userAgent) {
|
||||
ua = ua.toLowerCase()
|
||||
let [, os = 'other', os_version = '0'] = ua.match(
|
||||
OS_REGEXPS.find(re => re.test(ua))
|
||||
)
|
||||
const osRe = OS_REGEXPS.find((re) => re.test(ua))
|
||||
let [, os = 'other', os_version = '0'] = (osRe && ua.match(osRe)) || []
|
||||
if (os === 'iphone os' || os === 'ipad os') os = 'ios'
|
||||
const [, browser = 'other', browser_version = '0'] = ua.match(
|
||||
BROWSER_REGEXPS.find(re => re.test(ua))
|
||||
)
|
||||
const browserRe = BROWSER_REGEXPS.find((re) => re.test(ua))
|
||||
const [, browser = 'other', browser_version = '0'] = (browserRe && ua.match(browserRe)) || []
|
||||
return { os, os_version, browser, browser_version }
|
||||
}
|
||||
@@ -11,20 +11,22 @@ export default function () {
|
||||
const codeTerms = document.querySelectorAll('#article-contents table code')
|
||||
if (!codeTerms) return
|
||||
|
||||
codeTerms.forEach(node => {
|
||||
codeTerms.forEach((node) => {
|
||||
// Do the wrapping on the inner text only, so we don't modify hrefs
|
||||
const oldText = escape(node.textContent)
|
||||
const oldText = escape(node.textContent || '')
|
||||
|
||||
const newText = oldText.replace(wordsLongerThan18Chars, (str) => {
|
||||
return str
|
||||
// GraphQL code terms use camelcase
|
||||
.replace(camelCaseChars, '$1<wbr>$2')
|
||||
// REST code terms use underscores
|
||||
// to keep word breaks looking nice, only break on underscores after the 12th char
|
||||
// so `has_organization_projects` will break after `has_organization` instead of after `has_`
|
||||
.replace(underscoresAfter12thChar, '$1_<wbr>')
|
||||
// Some Actions reference pages have tables with code terms separated by slashes
|
||||
.replace(slashChars, '$1<wbr>')
|
||||
return (
|
||||
str
|
||||
// GraphQL code terms use camelcase
|
||||
.replace(camelCaseChars, '$1<wbr>$2')
|
||||
// REST code terms use underscores
|
||||
// to keep word breaks looking nice, only break on underscores after the 12th char
|
||||
// so `has_organization_projects` will break after `has_organization` instead of after `has_`
|
||||
.replace(underscoresAfter12thChar, '$1_<wbr>')
|
||||
// Some Actions reference pages have tables with code terms separated by slashes
|
||||
.replace(slashChars, '$1<wbr>')
|
||||
)
|
||||
})
|
||||
|
||||
node.innerHTML = node.innerHTML.replace(oldText, newText)
|
||||
159
package-lock.json
generated
159
package-lock.json
generated
@@ -95,9 +95,12 @@
|
||||
"@graphql-tools/load": "^6.2.8",
|
||||
"@octokit/rest": "^18.5.3",
|
||||
"@types/github-slugger": "^1.3.0",
|
||||
"@types/imurmurhash": "^0.1.1",
|
||||
"@types/js-cookie": "^2.2.6",
|
||||
"@types/lodash": "^4.14.169",
|
||||
"@types/react": "^17.0.6",
|
||||
"@types/react-dom": "^17.0.5",
|
||||
"@types/uuid": "^8.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.26.1",
|
||||
"@typescript-eslint/parser": "^4.26.1",
|
||||
"async": "^3.2.0",
|
||||
@@ -158,6 +161,7 @@
|
||||
"strip-ansi": "^7.0.0",
|
||||
"style-loader": "^2.0.0",
|
||||
"supertest": "^6.1.3",
|
||||
"ts-loader": "^9.2.3",
|
||||
"typescript": "^4.3.2",
|
||||
"url-template": "^2.0.8",
|
||||
"webpack": "^5.37.0",
|
||||
@@ -4125,6 +4129,12 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.0.tgz",
|
||||
"integrity": "sha512-c3Xy026kOF7QOTn00hbIllV1dLR9hG9NkSrLQgCVs8NF6sBU+VGWjD3wLPhmh1TYAc7ugCFsvHYMN4VcBN1U1A=="
|
||||
},
|
||||
"node_modules/@types/imurmurhash": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/imurmurhash/-/imurmurhash-0.1.1.tgz",
|
||||
"integrity": "sha512-ThbETc7uxx6rIpNP0fE3bqrSSIeBWPrFY4TzY4WFsvdQYWinub+PLZV/9nT3zicRJJPWbmHqJIsHZHeh5Ad+Ug==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/istanbul-lib-coverage": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz",
|
||||
@@ -4149,6 +4159,12 @@
|
||||
"@types/istanbul-lib-report": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/js-cookie": {
|
||||
"version": "2.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.6.tgz",
|
||||
"integrity": "sha512-+oY0FDTO2GYKEV0YPvSshGq9t7YozVkgvXLty7zogQNuCxBhT9/3INX9Q7H1aRZ4SUDRXAKlJuA4EA5nTt7SNw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/json-schema": {
|
||||
"version": "7.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz",
|
||||
@@ -4290,6 +4306,12 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.3.tgz",
|
||||
"integrity": "sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ=="
|
||||
},
|
||||
"node_modules/@types/uuid": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.0.tgz",
|
||||
"integrity": "sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/yargs": {
|
||||
"version": "15.0.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.13.tgz",
|
||||
@@ -22754,6 +22776,74 @@
|
||||
"integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/ts-loader": {
|
||||
"version": "9.2.3",
|
||||
"resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.2.3.tgz",
|
||||
"integrity": "sha512-sEyWiU3JMHBL55CIeC4iqJQadI0U70A5af0kvgbNLHVNz2ACztQg0j/9x10bjjIht8WfFYLKfn4L6tkZ+pu+8Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"chalk": "^4.1.0",
|
||||
"enhanced-resolve": "^5.0.0",
|
||||
"micromatch": "^4.0.0",
|
||||
"semver": "^7.3.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "*",
|
||||
"webpack": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-loader/node_modules/braces": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"fill-range": "^7.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-loader/node_modules/fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-loader/node_modules/micromatch": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz",
|
||||
"integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"braces": "^3.0.1",
|
||||
"picomatch": "^2.2.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-loader/node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-pnp": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-pnp/-/ts-pnp-1.2.0.tgz",
|
||||
@@ -28247,6 +28337,12 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.0.tgz",
|
||||
"integrity": "sha512-c3Xy026kOF7QOTn00hbIllV1dLR9hG9NkSrLQgCVs8NF6sBU+VGWjD3wLPhmh1TYAc7ugCFsvHYMN4VcBN1U1A=="
|
||||
},
|
||||
"@types/imurmurhash": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/imurmurhash/-/imurmurhash-0.1.1.tgz",
|
||||
"integrity": "sha512-ThbETc7uxx6rIpNP0fE3bqrSSIeBWPrFY4TzY4WFsvdQYWinub+PLZV/9nT3zicRJJPWbmHqJIsHZHeh5Ad+Ug==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/istanbul-lib-coverage": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz",
|
||||
@@ -28271,6 +28367,12 @@
|
||||
"@types/istanbul-lib-report": "*"
|
||||
}
|
||||
},
|
||||
"@types/js-cookie": {
|
||||
"version": "2.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.6.tgz",
|
||||
"integrity": "sha512-+oY0FDTO2GYKEV0YPvSshGq9t7YozVkgvXLty7zogQNuCxBhT9/3INX9Q7H1aRZ4SUDRXAKlJuA4EA5nTt7SNw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/json-schema": {
|
||||
"version": "7.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz",
|
||||
@@ -28412,6 +28514,12 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.3.tgz",
|
||||
"integrity": "sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ=="
|
||||
},
|
||||
"@types/uuid": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.0.tgz",
|
||||
"integrity": "sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/yargs": {
|
||||
"version": "15.0.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.13.tgz",
|
||||
@@ -44031,6 +44139,57 @@
|
||||
"integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==",
|
||||
"optional": true
|
||||
},
|
||||
"ts-loader": {
|
||||
"version": "9.2.3",
|
||||
"resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.2.3.tgz",
|
||||
"integrity": "sha512-sEyWiU3JMHBL55CIeC4iqJQadI0U70A5af0kvgbNLHVNz2ACztQg0j/9x10bjjIht8WfFYLKfn4L6tkZ+pu+8Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chalk": "^4.1.0",
|
||||
"enhanced-resolve": "^5.0.0",
|
||||
"micromatch": "^4.0.0",
|
||||
"semver": "^7.3.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"braces": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fill-range": "^7.0.1"
|
||||
}
|
||||
},
|
||||
"fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"micromatch": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz",
|
||||
"integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"braces": "^3.0.1",
|
||||
"picomatch": "^2.2.3"
|
||||
}
|
||||
},
|
||||
"to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-number": "^7.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"ts-pnp": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-pnp/-/ts-pnp-1.2.0.tgz",
|
||||
|
||||
@@ -101,9 +101,12 @@
|
||||
"@graphql-tools/load": "^6.2.8",
|
||||
"@octokit/rest": "^18.5.3",
|
||||
"@types/github-slugger": "^1.3.0",
|
||||
"@types/imurmurhash": "^0.1.1",
|
||||
"@types/js-cookie": "^2.2.6",
|
||||
"@types/lodash": "^4.14.169",
|
||||
"@types/react": "^17.0.6",
|
||||
"@types/react-dom": "^17.0.5",
|
||||
"@types/uuid": "^8.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.26.1",
|
||||
"@typescript-eslint/parser": "^4.26.1",
|
||||
"async": "^3.2.0",
|
||||
@@ -164,6 +167,7 @@
|
||||
"strip-ansi": "^7.0.0",
|
||||
"style-loader": "^2.0.0",
|
||||
"supertest": "^6.1.3",
|
||||
"ts-loader": "^9.2.3",
|
||||
"typescript": "^4.3.2",
|
||||
"url-template": "^2.0.8",
|
||||
"webpack": "^5.37.0",
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noImplicitThis": false,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
@@ -15,8 +18,15 @@
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"baseUrl": ".",
|
||||
"noEmit": false,
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"exclude": ["node_modules"],
|
||||
"include": ["*.d.ts", "**/*.ts", "**/*.tsx"]
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
],
|
||||
"include": [
|
||||
"*.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -6,30 +6,22 @@ const { EnvironmentPlugin, ProvidePlugin } = require('webpack')
|
||||
module.exports = {
|
||||
mode: 'development',
|
||||
devtool: process.env.NODE_ENV === 'development' ? 'eval' : 'source-map', // no 'eval' outside of development
|
||||
entry: './javascripts/index.js',
|
||||
entry: './javascripts/index.ts',
|
||||
output: {
|
||||
filename: 'index.js',
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
publicPath: '/dist'
|
||||
},
|
||||
stats: 'errors-only',
|
||||
resolve: {
|
||||
extensions: ['.tsx', '.ts', '.js', '.css', '.scss']
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.m?js$/,
|
||||
exclude: /(node_modules)/,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
exclude: /node_modules\/lodash/,
|
||||
presets: [
|
||||
['@babel/preset-env', { targets: '> 0.25%, not dead' }]
|
||||
],
|
||||
plugins: [
|
||||
'@babel/transform-runtime'
|
||||
]
|
||||
}
|
||||
}
|
||||
test: /\.tsx?$/,
|
||||
use: 'ts-loader',
|
||||
exclude: /node_modules/
|
||||
},
|
||||
{
|
||||
test: /\.css$/i,
|
||||
|
||||
Reference in New Issue
Block a user