From 68e62f0aeccf07b753721e4afcbda5b28ffa925b Mon Sep 17 00:00:00 2001 From: ZeEarth Date: Fri, 10 Apr 2026 19:17:30 +0200 Subject: [PATCH 1/6] #699 - add type for public/commom/utils.js and public/components/expandable/expandable.js used in utils --- public/common/utils.js | 108 +++++++++++++++++---- public/components/expandable/expandable.js | 26 ++++- 2 files changed, 113 insertions(+), 21 deletions(-) diff --git a/public/common/utils.js b/public/common/utils.js index d196d511..43aee3de 100644 --- a/public/common/utils.js +++ b/public/common/utils.js @@ -3,12 +3,21 @@ import "../components/expandable/expandable.js"; window.activeLegendElement = null; +/** + * @param {{x: number, y: number}} location + * @param {{x: number, y: number}} pos + * @returns {number} + */ export function vec2Distance(location, pos) { return Math.sqrt( Math.pow(location.x - pos.x, 2) + Math.pow(location.y - pos.y, 2) ); } +/** + * @param {string} strWithEmojis + * @returns {string[]} + */ export function extractEmojis(strWithEmojis) { const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" @@ -62,6 +71,11 @@ export function createDOMElement(kind = "div", options = {}) { return el; } +/** + * @param {string} href + * @param {string|null} text + * @returns {HTMLElement} + */ export function createLink(href, text = null) { const attributes = { rel: "noopener", target: "_blank", href @@ -70,6 +84,10 @@ export function createLink(href, text = null) { return createDOMElement("a", { text, attributes }); } +/** + * @param {string} spec + * @returns {{ name: string, version: string }} + */ export function parseNpmSpec(spec) { const parts = spec.split("@"); const version = parts.at(-1); @@ -79,8 +97,13 @@ export function parseNpmSpec(spec) { { name: parts[0], version }; } +/** + * @param {{url?: string}} repository + * @param {string | null} defaultValue + * @returns {string | null} return repository url or defaultValue + */ export function parseRepositoryUrl(repository = {}, defaultValue = null) { - if (typeof repository !== "object" || !("url" in repository)) { + if (!repository || !repository.url || typeof repository !== "object" || !("url" in repository)) { return defaultValue; } @@ -92,7 +115,7 @@ export function parseRepositoryUrl(repository = {}, defaultValue = null) { } if (repository.url.startsWith("git@")) { const execResult = /git@(?[a-zA-Z.]+):(?.+)\.git/gm.exec(repository.url); - if (execResult === null) { + if (execResult === null || !execResult.groups) { return defaultValue; } @@ -107,6 +130,12 @@ export function parseRepositoryUrl(repository = {}, defaultValue = null) { } } +/** + * @param {string} title + * @param {string} value + * @param {Record} options + * @returns {HTMLElement} + */ export function createLiField(title, value, options = {}) { const { isLink = false } = options; @@ -126,12 +155,20 @@ export function createLiField(title, value, options = {}) { return liElement; } +/** + * @param {HTMLElement} node - The parent DOM element. + * @param {string[]} items - Array of strings to display. + * @param {Object} [options] - Optional configuration options. + * @param {Function} [options.onclick] - Callback function (event, item). + * @param {boolean} [options.hideItems] - Hide items if needed. + * @param {number} [options.hideItemsLength] - Number of visible elements before masking. + * @returns {void} + */ export function createItemsList(node, items = [], options = {}) { const { onclick = null, hideItems = false, hideItemsLength = 5 } = options; - if (items.length === 0) { const previousNode = node.previousElementSibling; - if (previousNode !== null) { + if (previousNode !== null && previousNode instanceof HTMLElement) { previousNode.style.display = "none"; } @@ -157,8 +194,10 @@ export function createItemsList(node, items = [], options = {}) { } if (hideItems && items.length > hideItemsLength) { + /** @type {import("../components/expandable/expandable.js").ExpandableType} */ + // @ts-expect-error createElement return HTMLElement and we can't cast directly from line (Unexpected comment inline with code.) const expandableSpan = document.createElement("expandable-span"); - expandableSpan.onToggle = (expandable) => toggle(expandable, node, hideItemsLength); + expandableSpan.onToggle = () => toggle(expandableSpan, node, hideItemsLength); fragment.appendChild(expandableSpan); } node.appendChild(fragment); @@ -168,10 +207,16 @@ export function createItemsList(node, items = [], options = {}) { TODO: this util function won't be necessary once the parents of the expandable component will be migrated to lit becuase the parents will handle the filtering of their children themselves */ +/** + * @param {import("../components/expandable/expandable.js").ExpandableType} expandable + * @param {HTMLElement} parentNode + * @param {number} hideItemsLength + * @returns {void} + */ export function toggle(expandable, parentNode, hideItemsLength) { expandable.isClosed = !expandable.isClosed; - for (let id = 0; id < parentNode.childNodes.length; id++) { - const node = parentNode.childNodes[id]; + for (let id = 0; id < parentNode.children.length; id++) { + const node = parentNode.children[id]; if (node.tagName === "EXPANDABLE-SPAN") { continue; } @@ -185,6 +230,10 @@ export function toggle(expandable, parentNode, hideItemsLength) { } } +/** + * @param {string} str + * @returns {void} + */ export function copyToClipboard(str) { const el = document.createElement("textarea"); el.value = str; @@ -192,19 +241,26 @@ export function copyToClipboard(str) { el.style.position = "absolute"; el.style.left = "-9999px"; document.body.appendChild(el); - const selected = - document.getSelection().rangeCount > 0 - ? document.getSelection().getRangeAt(0) - : false; + const selection = document.getSelection(); + const selected = selection && selection.rangeCount > 0 ? selection.getRangeAt(0) : false; el.select(); document.execCommand("copy"); document.body.removeChild(el); - if (selected) { - document.getSelection().removeAllRanges(); - document.getSelection().addRange(selected); + if (selected && selection) { + selection.removeAllRanges(); + selection.addRange(selected); } } +/** + * @typedef {{reverse?: boolean, blacklist?: Node[], hiddenTarget?: HTMLElement, callback?: () => void}} hideOnClickOutsideOptions + */ + +/** + * @param {HTMLElement} element + * @param {hideOnClickOutsideOptions} options + * @returns {(event: Event) => void} + */ export function hideOnClickOutside( element, options = {} @@ -216,8 +272,15 @@ export function hideOnClickOutside( callback = () => void 0 } = options; + /** @param {Event} event */ function outsideClickListener(event) { - if (!element.contains(event.target) && !blacklist.includes(event.target)) { + const target = event.target; + + if (!(target instanceof Node)) { + return; + } + + if (!element.contains(target) && !blacklist.includes(target)) { if (hiddenTarget) { if (reverse) { hiddenTarget.classList.remove("show"); @@ -240,13 +303,24 @@ export function hideOnClickOutside( return outsideClickListener; } +/** @returns {string} */ export function currentLang() { - const detectedLang = document.getElementById("lang").dataset.lang; + const detectedLang = document.getElementById("lang")?.dataset.lang; + const defaultLanguage = "english"; + if (!detectedLang) { + return defaultLanguage; + } - return detectedLang in window.i18n ? detectedLang : "english"; + return detectedLang in window.i18n ? detectedLang : defaultLanguage; } +/** + * @param {Function} callback + * @param {number} delay + * @returns {() => void} + */ export function debounce(callback, delay) { + /** @type {ReturnType | undefined} */ let timer; // eslint-disable-next-line func-names diff --git a/public/components/expandable/expandable.js b/public/components/expandable/expandable.js index 00a56ef3..289f012c 100644 --- a/public/components/expandable/expandable.js +++ b/public/components/expandable/expandable.js @@ -6,7 +6,17 @@ import { when } from "lit/directives/when.js"; import { currentLang } from "../../common/utils"; import "../icon/icon.js"; -class Expandable extends LitElement { +/** + * @typedef {Record} I18nLanguage + */ + +/** + * "Expandable" web component displaying a toggle button with an icon. + * @element expandable-span + * @prop {Function} onToggle - Function called during the interaction (default: () => void 0). + * @prop {boolean} isClosed - Specifies whether the associated content is hidden (true) or visible (false). + */ +export class Expandable extends LitElement { static styles = css` span.expandable { display: flex; @@ -43,19 +53,24 @@ span.expandable nsecure-icon { constructor() { super(); this.isClosed = true; - this.onToggle = () => void 0; + /** @type {(instance: Expandable) => void} */ + this.onToggle = () => void {}; } render() { const lang = currentLang(); + /** @type I18nLanguage */ + // @ts-expect-error window.i18n is Record and we can't cast in line (Unexpected comment inline with code.) + const i18n = window.i18n; + const translations = i18n[lang].home; return html` `; @@ -67,3 +82,6 @@ span.expandable nsecure-icon { } customElements.define("expandable-span", Expandable); +/** +* @typedef {import('./expandable.js').Expandable} ExpandableType +*/ From 6aca8366b8ea6857bb9df6d1bfebd3bd0a602bb9 Mon Sep 17 00:00:00 2001 From: zeearth Date: Fri, 10 Apr 2026 21:09:07 +0200 Subject: [PATCH 2/6] Update public/common/utils.js Co-authored-by: PierreDemailly <39910767+PierreDemailly@users.noreply.github.com> --- public/common/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/common/utils.js b/public/common/utils.js index 43aee3de..06e74e12 100644 --- a/public/common/utils.js +++ b/public/common/utils.js @@ -74,7 +74,7 @@ export function createDOMElement(kind = "div", options = {}) { /** * @param {string} href * @param {string|null} text - * @returns {HTMLElement} + * @returns {HTMLAnchorElement} */ export function createLink(href, text = null) { const attributes = { From d42d5e7ef3e68766f9ef9ecbb2b855af614b4102 Mon Sep 17 00:00:00 2001 From: zeearth Date: Fri, 10 Apr 2026 21:09:51 +0200 Subject: [PATCH 3/6] Update public/common/utils.js Co-authored-by: PierreDemailly <39910767+PierreDemailly@users.noreply.github.com> --- public/common/utils.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/common/utils.js b/public/common/utils.js index 06e74e12..58dced90 100644 --- a/public/common/utils.js +++ b/public/common/utils.js @@ -194,9 +194,9 @@ export function createItemsList(node, items = [], options = {}) { } if (hideItems && items.length > hideItemsLength) { - /** @type {import("../components/expandable/expandable.js").ExpandableType} */ - // @ts-expect-error createElement return HTMLElement and we can't cast directly from line (Unexpected comment inline with code.) - const expandableSpan = document.createElement("expandable-span"); + const expandableSpan = + /** @type {import("../components/expandable/expandable.js").ExpandableType} */ + (document.createElement("expandable-span")); expandableSpan.onToggle = () => toggle(expandableSpan, node, hideItemsLength); fragment.appendChild(expandableSpan); } From 5a54a0014b65b523eb6639cde8f812ea9abd398a Mon Sep 17 00:00:00 2001 From: zeearth Date: Fri, 10 Apr 2026 21:10:25 +0200 Subject: [PATCH 4/6] Update public/components/expandable/expandable.js Co-authored-by: PierreDemailly <39910767+PierreDemailly@users.noreply.github.com> --- public/components/expandable/expandable.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/components/expandable/expandable.js b/public/components/expandable/expandable.js index 289f012c..8be1b340 100644 --- a/public/components/expandable/expandable.js +++ b/public/components/expandable/expandable.js @@ -59,9 +59,9 @@ span.expandable nsecure-icon { render() { const lang = currentLang(); - /** @type I18nLanguage */ - // @ts-expect-error window.i18n is Record and we can't cast in line (Unexpected comment inline with code.) - const i18n = window.i18n; + const i18n = + /** @type I18nLanguage */ + (window.i18n); const translations = i18n[lang].home; return html` From 956f62a421de3033e7a851b1627458c630da53ed Mon Sep 17 00:00:00 2001 From: ZeEarth Date: Fri, 10 Apr 2026 21:22:40 +0200 Subject: [PATCH 5/6] integrate return from Pierre --- public/common/utils.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/public/common/utils.js b/public/common/utils.js index 58dced90..2a05c467 100644 --- a/public/common/utils.js +++ b/public/common/utils.js @@ -81,7 +81,11 @@ export function createLink(href, text = null) { rel: "noopener", target: "_blank", href }; - return createDOMElement("a", { text, attributes }); + const htmlAnchor = + /** @type {HTMLAnchorElement} */ + (createDOMElement("a", { text, attributes })); + + return htmlAnchor; } /** @@ -90,7 +94,7 @@ export function createLink(href, text = null) { */ export function parseNpmSpec(spec) { const parts = spec.split("@"); - const version = parts.at(-1); + const version = parts.at(-1) ?? ""; return spec.startsWith("@") ? { name: `@${parts[1]}`, version } : @@ -194,9 +198,9 @@ export function createItemsList(node, items = [], options = {}) { } if (hideItems && items.length > hideItemsLength) { - const expandableSpan = - /** @type {import("../components/expandable/expandable.js").ExpandableType} */ - (document.createElement("expandable-span")); + const expandableSpan = + /** @type {import("../components/expandable/expandable.js").ExpandableType} */ + (document.createElement("expandable-span")); expandableSpan.onToggle = () => toggle(expandableSpan, node, hideItemsLength); fragment.appendChild(expandableSpan); } From c1f1fdb1cae5ac797136a044d17f9e961562d642 Mon Sep 17 00:00:00 2001 From: ZeEarth Date: Fri, 10 Apr 2026 21:24:52 +0200 Subject: [PATCH 6/6] integrate return from Pierre - correction of number of indent spaces --- public/components/expandable/expandable.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/components/expandable/expandable.js b/public/components/expandable/expandable.js index 8be1b340..e170fe87 100644 --- a/public/components/expandable/expandable.js +++ b/public/components/expandable/expandable.js @@ -60,8 +60,8 @@ span.expandable nsecure-icon { render() { const lang = currentLang(); const i18n = - /** @type I18nLanguage */ - (window.i18n); + /** @type I18nLanguage */ + (window.i18n); const translations = i18n[lang].home; return html`