diff --git a/public/common/utils.js b/public/common/utils.js index d196d511..2a05c467 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,25 +71,43 @@ export function createDOMElement(kind = "div", options = {}) { return el; } +/** + * @param {string} href + * @param {string|null} text + * @returns {HTMLAnchorElement} + */ export function createLink(href, text = null) { const attributes = { rel: "noopener", target: "_blank", href }; - return createDOMElement("a", { text, attributes }); + const htmlAnchor = + /** @type {HTMLAnchorElement} */ + (createDOMElement("a", { text, attributes })); + + return htmlAnchor; } +/** + * @param {string} spec + * @returns {{ name: string, version: string }} + */ 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 } : { 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 +119,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 +134,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 +159,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 +198,10 @@ export function createItemsList(node, items = [], options = {}) { } if (hideItems && items.length > hideItemsLength) { - const expandableSpan = document.createElement("expandable-span"); - expandableSpan.onToggle = (expandable) => toggle(expandable, node, hideItemsLength); + const expandableSpan = + /** @type {import("../components/expandable/expandable.js").ExpandableType} */ + (document.createElement("expandable-span")); + expandableSpan.onToggle = () => toggle(expandableSpan, node, hideItemsLength); fragment.appendChild(expandableSpan); } node.appendChild(fragment); @@ -168,10 +211,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 +234,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 +245,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 +276,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 +307,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..e170fe87 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(); + const i18n = + /** @type I18nLanguage */ + (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 +*/