diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 72bbd2ef5..fd94badd3 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -99,6 +99,9 @@ jobs:
- name: Format
run: npm run fmt:check
+
+ - name: Compile
+ run: npm run compile:check
- name: Build
run: npm run build
diff --git a/src/Elastic.Documentation.Site/Assets/applies-switch.ts b/src/Elastic.Documentation.Site/Assets/applies-switch.ts
index 5f7fe933b..193c22887 100644
--- a/src/Elastic.Documentation.Site/Assets/applies-switch.ts
+++ b/src/Elastic.Documentation.Site/Assets/applies-switch.ts
@@ -1,12 +1,11 @@
// TODO: refactor to typescript. this was copied from the tabs implementation
-
-// @ts-check
+import { $$ } from 'select-dom'
// Extra JS capability for selected applies switches to be synced
// The selection is stored in local storage so that it persists across page loads.
-const as_id_to_elements = {}
-const storageKeyPrefix = 'sphinx-design-applies-switch-id-'
+const as_id_to_elements: { [key: string]: HTMLElement[] } = {}
+const storageKeyPrefix = 'applies-switch-id-'
function create_key(el: HTMLElement) {
const syncId = el.getAttribute('data-sync-id')
@@ -22,16 +21,16 @@ function create_key(el: HTMLElement) {
function ready() {
// Find all applies switches with sync data
- const groups = []
+ const groups: string[] = []
- document.querySelectorAll('.applies-switch-label').forEach((label) => {
+ $$('.applies-switch-label').forEach((label) => {
if (label instanceof HTMLElement) {
const data = create_key(label)
if (data) {
const [group, id, key] = data
// add click event listener
- label.onclick = onAppliesSwitchLabelClick
+ label.addEventListener('click', onAppliesSwitchLabelClick)
// store map of key to elements
if (!as_id_to_elements[key]) {
@@ -72,13 +71,17 @@ function ready() {
*
* @this {HTMLElement} - The element that was clicked.
*/
-function onAppliesSwitchLabelClick() {
+function onAppliesSwitchLabelClick(this: HTMLLabelElement) {
const data = create_key(this)
if (!data) return
const [group, id, key] = data
for (const label of as_id_to_elements[key]) {
- if (label === this) continue
- label.previousElementSibling.checked = true
+ if (label === this) {
+ continue
+ }
+ if (label.previousElementSibling instanceof HTMLInputElement) {
+ label.previousElementSibling.checked = true
+ }
}
window.sessionStorage.setItem(storageKeyPrefix + group, id)
}
diff --git a/src/Elastic.Documentation.Site/Assets/copybutton.ts b/src/Elastic.Documentation.Site/Assets/copybutton.ts
index 5613b23f7..e3b1e9da8 100644
--- a/src/Elastic.Documentation.Site/Assets/copybutton.ts
+++ b/src/Elastic.Documentation.Site/Assets/copybutton.ts
@@ -1,3 +1,6 @@
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-nocheck
+// This is copied from legacy. It works, but we should rework this if we ever need to change it
import { $$ } from 'select-dom'
const DOCUMENTATION_OPTIONS = {
diff --git a/src/Elastic.Documentation.Site/Assets/eui-icons-cache.ts b/src/Elastic.Documentation.Site/Assets/eui-icons-cache.ts
index 1e67657cf..64ad3d6b4 100644
--- a/src/Elastic.Documentation.Site/Assets/eui-icons-cache.ts
+++ b/src/Elastic.Documentation.Site/Assets/eui-icons-cache.ts
@@ -1,3 +1,5 @@
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-nocheck: EUI icons do not have types
import { icon as EuiIconVisualizeApp } from '@elastic/eui/es/components/icon/assets/app_visualize'
import { icon as EuiIconArrowDown } from '@elastic/eui/es/components/icon/assets/arrow_down'
import { icon as EuiIconArrowLeft } from '@elastic/eui/es/components/icon/assets/arrow_left'
diff --git a/src/Elastic.Documentation.Site/Assets/global.d.ts b/src/Elastic.Documentation.Site/Assets/global.d.ts
new file mode 100644
index 000000000..5ec0fc014
--- /dev/null
+++ b/src/Elastic.Documentation.Site/Assets/global.d.ts
@@ -0,0 +1,5 @@
+declare module '@elastic/highlightjs-esql' {
+ import { LanguageFn } from 'highlight.js'
+ const esql: LanguageFn
+ export default esql
+}
diff --git a/src/Elastic.Documentation.Site/Assets/image-carousel.ts b/src/Elastic.Documentation.Site/Assets/image-carousel.ts
index 340152398..fe4e33ab0 100644
--- a/src/Elastic.Documentation.Site/Assets/image-carousel.ts
+++ b/src/Elastic.Documentation.Site/Assets/image-carousel.ts
@@ -1,9 +1,11 @@
+import { $$ } from 'select-dom'
+
class ImageCarousel {
private container: HTMLElement
- private slides: HTMLElement[]
- private indicators: HTMLElement[]
- private prevButton: HTMLElement | null
- private nextButton: HTMLElement | null
+ private slides: HTMLElement[] = []
+ private indicators: HTMLElement[] = []
+ private prevButton: HTMLElement | null = null
+ private nextButton: HTMLElement | null = null
private currentIndex: number = 0
private touchStartX: number = 0
private touchEndX: number = 0
@@ -245,9 +247,7 @@ export function initImageCarousel(): void {
if (!section) return
// First, collect all images we want in the carousel
- const allImageLinks = Array.from(
- section.querySelectorAll('a[href*="epr.elastic.co"]')
- )
+ const allImageLinks = $$('a[href*="epr.elastic.co"]', section)
// Track URLs to prevent duplicates
const processedUrls = new Set()
@@ -329,7 +329,11 @@ export function initImageCarousel(): void {
parent.style.display = 'none'
break
}
- parent = parent.parentElement
+
+ if (parent.parentElement) {
+ parent = parent.parentElement
+ }
+
maxAttempts--
}
@@ -397,9 +401,9 @@ export function initImageCarousel(): void {
// Helper to find a suitable container for an image
function findClosestContainer(
- element: Element,
- carousel: Element
-): Element | null {
+ element: HTMLElement,
+ carousel: HTMLElement
+): HTMLElement | null {
let current = element
while (
current &&
diff --git a/src/Elastic.Documentation.Site/Assets/main.ts b/src/Elastic.Documentation.Site/Assets/main.ts
index a28a14dd7..f5079d744 100644
--- a/src/Elastic.Documentation.Site/Assets/main.ts
+++ b/src/Elastic.Documentation.Site/Assets/main.ts
@@ -10,7 +10,7 @@ import { initTabs } from './tabs'
import { initTocNav } from './toc-nav'
import 'htmx-ext-head-support'
import 'htmx-ext-preload'
-import katex from 'katex'
+import * as katex from 'katex'
import { $, $$ } from 'select-dom'
import { UAParser } from 'ua-parser-js'
@@ -18,6 +18,9 @@ const { getOS } = new UAParser()
const isLazyLoadNavigationEnabled =
$('meta[property="docs:feature:lazy-load-navigation"]')?.content === 'true'
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+type HtmxEvent = any
+
/**
* Initialize KaTeX math rendering for elements with class 'math'
*/
@@ -71,7 +74,7 @@ document.addEventListener('DOMContentLoaded', function () {
initMath()
})
-document.addEventListener('htmx:load', function (event) {
+document.addEventListener('htmx:load', function (event: HtmxEvent) {
initTocNav()
initHighlight()
initCopyButton()
@@ -99,14 +102,17 @@ document.addEventListener('htmx:load', function (event) {
})
// Don't remove style tags because they are used by the elastic global nav.
-document.addEventListener('htmx:removingHeadElement', function (event) {
- const tagName = event.detail.headElement.tagName
- if (tagName === 'STYLE') {
- event.preventDefault()
+document.addEventListener(
+ 'htmx:removingHeadElement',
+ function (event: HtmxEvent) {
+ const tagName = event.detail.headElement.tagName
+ if (tagName === 'STYLE') {
+ event.preventDefault()
+ }
}
-})
+)
-document.addEventListener('htmx:beforeRequest', function (event) {
+document.addEventListener('htmx:beforeRequest', function (event: HtmxEvent) {
if (
event.detail.requestConfig.verb === 'get' &&
event.detail.requestConfig.triggeringEvent
@@ -126,60 +132,75 @@ document.addEventListener('htmx:beforeRequest', function (event) {
}
})
-document.body.addEventListener('htmx:oobBeforeSwap', function (event) {
- // This is needed to scroll to the top of the page when the content is swapped
- if (
- event.target.id === 'main-container' ||
- event.target.id === 'markdown-content' ||
- event.target.id === 'content-container'
- ) {
- window.scrollTo(0, 0)
+document.body.addEventListener(
+ 'htmx:oobBeforeSwap',
+ function (event: HtmxEvent) {
+ // This is needed to scroll to the top of the page when the content is swapped
+ if (
+ event.target?.id === 'main-container' ||
+ event.target?.id === 'markdown-content' ||
+ event.target?.id === 'content-container'
+ ) {
+ window.scrollTo(0, 0)
+ }
}
-})
-
-document.body.addEventListener('htmx:pushedIntoHistory', function (event) {
- const pagesNav = $('#pages-nav')
- const currentNavItem = $$('.current', pagesNav)
- currentNavItem.forEach((el) => {
- el.classList.remove('current')
- })
- const navItems = $$('a[href="' + event.detail.path + '"]', pagesNav)
- navItems.forEach((navItem) => {
- navItem.classList.add('current')
- })
-})
-
-document.body.addEventListener('htmx:responseError', function (event) {
- // If you get a 404 error while clicking on a hx-get link, actually open the link
- // This is needed because the browser doesn't update the URL when the response is a 404
- // In production, cloudfront handles serving the 404 page.
- // Locally, the DocumentationWebHost handles it.
- // On previews, a generic 404 page is shown.
- if (event.detail.xhr.status === 404) {
- window.location.assign(event.detail.pathInfo.requestPath)
+)
+
+document.body.addEventListener(
+ 'htmx:pushedIntoHistory',
+ function (event: HtmxEvent) {
+ const pagesNav = $('#pages-nav')
+ const currentNavItem = $$('.current', pagesNav)
+ currentNavItem.forEach((el) => {
+ el.classList.remove('current')
+ })
+ const navItems = $$('a[href="' + event.detail.path + '"]', pagesNav)
+ navItems.forEach((navItem) => {
+ navItem.classList.add('current')
+ })
}
-})
+)
+
+document.body.addEventListener(
+ 'htmx:responseError',
+ function (event: HtmxEvent) {
+ // If you get a 404 error while clicking on a hx-get link, actually open the link
+ // This is needed because the browser doesn't update the URL when the response is a 404
+ // In production, cloudfront handles serving the 404 page.
+ // Locally, the DocumentationWebHost handles it.
+ // On previews, a generic 404 page is shown.
+ if (event.detail.xhr.status === 404) {
+ window.location.assign(event.detail.pathInfo.requestPath)
+ }
+ }
+)
// We add a query string to the get request to make sure the requested page is up to date
-const docsBuilderVersion = $('body').dataset.docsBuilderVersion
-document.body.addEventListener('htmx:configRequest', function (event) {
- if (event.detail.verb === 'get') {
- event.detail.parameters['v'] = docsBuilderVersion
+const docsBuilderVersion = $('body')?.dataset.docsBuilderVersion
+document.body.addEventListener(
+ 'htmx:configRequest',
+ function (event: HtmxEvent) {
+ if (event.detail.verb === 'get' && docsBuilderVersion) {
+ event.detail.parameters['v'] = docsBuilderVersion
+ }
}
-})
+)
// Here we need to strip the v parameter from the URL so
// that the browser doesn't show the v parameter in the address bar
-document.body.addEventListener('htmx:beforeHistoryUpdate', function (event) {
- const params = new URLSearchParams(
- event.detail.history.path.split('?')[1] ?? ''
- )
- params.delete('v')
- const pathWithoutQueryString = event.detail.history.path.split('?')[0]
- if (params.size === 0) {
- event.detail.history.path = pathWithoutQueryString
- } else {
- event.detail.history.path =
- pathWithoutQueryString + '?' + params.toString()
+document.body.addEventListener(
+ 'htmx:beforeHistoryUpdate',
+ function (event: HtmxEvent) {
+ const params = new URLSearchParams(
+ event.detail.history.path.split('?')[1] ?? ''
+ )
+ params.delete('v')
+ const pathWithoutQueryString = event.detail.history.path.split('?')[0]
+ if (params.size === 0) {
+ event.detail.history.path = pathWithoutQueryString
+ } else {
+ event.detail.history.path =
+ pathWithoutQueryString + '?' + params.toString()
+ }
}
-})
+)
diff --git a/src/Elastic.Documentation.Site/Assets/pages-nav.ts b/src/Elastic.Documentation.Site/Assets/pages-nav.ts
index 65cc56549..fa3be4362 100644
--- a/src/Elastic.Documentation.Site/Assets/pages-nav.ts
+++ b/src/Elastic.Documentation.Site/Assets/pages-nav.ts
@@ -1,11 +1,11 @@
import { $, $$ } from 'select-dom'
function expandAllParents(navItem: HTMLElement) {
- let parent = navItem?.closest('li')
+ let parent: HTMLLIElement | null | undefined = navItem?.closest('li')
while (parent) {
const input = parent.querySelector('input')
- if (input) {
- ;(input as HTMLInputElement).checked = true
+ if (input instanceof HTMLInputElement) {
+ input.checked = true
}
parent = parent.parentElement?.closest('li')
}
@@ -13,7 +13,11 @@ function expandAllParents(navItem: HTMLElement) {
function scrollCurrentNaviItemIntoView(nav: HTMLElement) {
const currentNavItem = $('.current', nav)
- expandAllParents(currentNavItem)
+
+ if (currentNavItem) {
+ expandAllParents(currentNavItem)
+ }
+
if (currentNavItem && !isElementInViewport(nav, currentNavItem)) {
const navRect = nav.getBoundingClientRect()
const currentNavItemRect = currentNavItem.getBoundingClientRect()
@@ -65,10 +69,13 @@ export function initNav() {
}
const pagesDropdown = $('#pages-dropdown')
+ if (pagesDropdown) {
+ setDropdown(pagesDropdown)
+ }
const pageVersionDropdown = $('#page-version-dropdown')
- setDropdown(pagesDropdown)
- setDropdown(pageVersionDropdown)
-
+ if (pageVersionDropdown) {
+ setDropdown(pageVersionDropdown)
+ }
const navItems = $$(
'a[href="' +
window.location.pathname +
diff --git a/src/Elastic.Documentation.Site/Assets/smooth-scroll.ts b/src/Elastic.Documentation.Site/Assets/smooth-scroll.ts
index 929501f8e..07f9a1674 100644
--- a/src/Elastic.Documentation.Site/Assets/smooth-scroll.ts
+++ b/src/Elastic.Documentation.Site/Assets/smooth-scroll.ts
@@ -3,9 +3,9 @@ import { $$ } from 'select-dom'
export function initSmoothScroll() {
$$('#markdown-content a[href^="#"]').forEach((el) => {
el.addEventListener('click', (e) => {
- const target = document.getElementById(
- el.getAttribute('href').slice(1)
- )
+ // Using ! because href is guaranteed to be present due to the selector
+ const id = el.getAttribute('href')!.slice(1) // remove the '#' character
+ const target = document.getElementById(id)
if (target) {
e.preventDefault()
target.scrollIntoView({ behavior: 'smooth' })
diff --git a/src/Elastic.Documentation.Site/Assets/tabs.ts b/src/Elastic.Documentation.Site/Assets/tabs.ts
index 05396332a..72e18d251 100644
--- a/src/Elastic.Documentation.Site/Assets/tabs.ts
+++ b/src/Elastic.Documentation.Site/Assets/tabs.ts
@@ -5,8 +5,8 @@
// Extra JS capability for selected tabs to be synced
// The selection is stored in local storage so that it persists across page loads.
-const sd_id_to_elements = {}
-const storageKeyPrefix = 'sphinx-design-tab-id-'
+const sd_id_to_elements: { [key: string]: HTMLElement[] } = {}
+const storageKeyPrefix = 'tab-id-'
function create_key(el: HTMLElement) {
const syncId = el.getAttribute('data-sync-id')
@@ -22,7 +22,7 @@ function create_key(el: HTMLElement) {
function ready() {
// Find all tabs with sync data
- const groups = []
+ const groups: string[] = []
document.querySelectorAll('.tabs-label').forEach((label) => {
if (label instanceof HTMLElement) {
@@ -31,7 +31,7 @@ function ready() {
const [group, id, key] = data
// add click event listener
- label.onclick = onSDLabelClick
+ label.addEventListener('click', onSDLabelClick)
// store map of key to elements
if (!sd_id_to_elements[key]) {
@@ -72,13 +72,17 @@ function ready() {
*
* @this {HTMLElement} - The element that was clicked.
*/
-function onSDLabelClick() {
+function onSDLabelClick(this: HTMLLabelElement) {
const data = create_key(this)
if (!data) return
const [group, id, key] = data
for (const label of sd_id_to_elements[key]) {
- if (label === this) continue
- label.previousElementSibling.checked = true
+ if (label === this) {
+ continue
+ }
+ if (label.previousElementSibling instanceof HTMLInputElement) {
+ label.previousElementSibling.checked = true
+ }
}
window.sessionStorage.setItem(storageKeyPrefix + group, id)
}
diff --git a/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/ChatMessage.test.tsx b/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/ChatMessage.test.tsx
index 5aa1ba5c3..90a79238b 100644
--- a/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/ChatMessage.test.tsx
+++ b/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/ChatMessage.test.tsx
@@ -109,7 +109,7 @@ describe('ChatMessage Component', () => {
it('should show loading icon when streaming', () => {
// Act
- render()
+ render()
// Assert
// Loading elastic icon should be present
diff --git a/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/useMessageThrottling.ts b/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/useMessageThrottling.ts
index 8a91b5c7c..978ff48da 100644
--- a/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/useMessageThrottling.ts
+++ b/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/useMessageThrottling.ts
@@ -18,7 +18,7 @@ export function useMessageThrottling({
onMessage,
}: UseMessageThrottlingOptions): UseMessageThrottlingReturn {
const messageQueueRef = useRef([])
- const timerRef = useRef(null)
+ const timerRef = useRef(null)
const isProcessingRef = useRef(false)
const processNextMessage = useCallback(() => {
diff --git a/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/useStatusMinDisplay.ts b/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/useStatusMinDisplay.ts
index e9469c9cd..ae4f78fb8 100644
--- a/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/useStatusMinDisplay.ts
+++ b/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/useStatusMinDisplay.ts
@@ -36,7 +36,7 @@ export const useStatusMinDisplay = (
)
const lastChangeTimeRef = useRef(Date.now())
const pendingStatusRef = useRef(null)
- const timeoutRef = useRef(null)
+ const timeoutRef = useRef(null)
useEffect(() => {
// Clear any pending timeout
diff --git a/src/Elastic.Documentation.Site/Assets/web-components/VersionDropdown.tsx b/src/Elastic.Documentation.Site/Assets/web-components/VersionDropdown.tsx
index 5bcb88502..0a2514e74 100644
--- a/src/Elastic.Documentation.Site/Assets/web-components/VersionDropdown.tsx
+++ b/src/Elastic.Documentation.Site/Assets/web-components/VersionDropdown.tsx
@@ -96,17 +96,18 @@ const VersionDropdown = ({
const WIDTH = 175
- const topLevelItems = () =>
- items.map((item, index) => {
+ const topLevelItems = (): EuiContextMenuPanelItemDescriptor[] =>
+ items?.map((item, index) => {
return {
name: item.name,
panel: item.children?.length ? index + 1 : undefined,
href: item.href,
disabled: item.disabled,
}
- })
+ }) ?? []
- const subpanels = () => convertToPanels(items)
+ const subpanels = (): EuiContextMenuPanelDescriptor[] =>
+ convertToPanels(items || [])
const panels = (): EuiContextMenuPanelDescriptor[] => [
{
diff --git a/src/Elastic.Documentation.Site/package.json b/src/Elastic.Documentation.Site/package.json
index 4c57a0050..8110e55d2 100644
--- a/src/Elastic.Documentation.Site/package.json
+++ b/src/Elastic.Documentation.Site/package.json
@@ -11,6 +11,7 @@
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"lint": "eslint . --ignore-pattern './_static/*' --ignore-pattern 'jest.config.js'",
+ "compile:check": "tsc --noEmit",
"fmt:check": "prettier --check .",
"fmt:write": "prettier --write .",
"synthetics:test": "npx @elastic/synthetics ./synthetics/journeys --config=./synthetics/synthetics.config.ts",
diff --git a/src/Elastic.Documentation.Site/tsconfig.json b/src/Elastic.Documentation.Site/tsconfig.json
index b5139ff8b..5b630d707 100644
--- a/src/Elastic.Documentation.Site/tsconfig.json
+++ b/src/Elastic.Documentation.Site/tsconfig.json
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"target": "ES2020",
- "lib": ["DOM", "DOM.Iterable", "ES6"],
+ "lib": ["DOM", "DOM.Iterable", "ES6", "ES2020"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,