Skip to content

Commit 1971ef4

Browse files
feat(interface): add type for public/commom/utils.js (#746)
Co-authored-by: PierreDemailly <39910767+PierreDemailly@users.noreply.github.com>
1 parent bacbf5b commit 1971ef4

2 files changed

Lines changed: 120 additions & 24 deletions

File tree

public/common/utils.js

Lines changed: 98 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,21 @@ import "../components/expandable/expandable.js";
33

44
window.activeLegendElement = null;
55

6+
/**
7+
* @param {{x: number, y: number}} location
8+
* @param {{x: number, y: number}} pos
9+
* @returns {number}
10+
*/
611
export function vec2Distance(location, pos) {
712
return Math.sqrt(
813
Math.pow(location.x - pos.x, 2) + Math.pow(location.y - pos.y, 2)
914
);
1015
}
1116

17+
/**
18+
* @param {string} strWithEmojis
19+
* @returns {string[]}
20+
*/
1221
export function extractEmojis(strWithEmojis) {
1322
const segmenter = new Intl.Segmenter("en", {
1423
granularity: "grapheme"
@@ -62,25 +71,43 @@ export function createDOMElement(kind = "div", options = {}) {
6271
return el;
6372
}
6473

74+
/**
75+
* @param {string} href
76+
* @param {string|null} text
77+
* @returns {HTMLAnchorElement}
78+
*/
6579
export function createLink(href, text = null) {
6680
const attributes = {
6781
rel: "noopener", target: "_blank", href
6882
};
6983

70-
return createDOMElement("a", { text, attributes });
84+
const htmlAnchor =
85+
/** @type {HTMLAnchorElement} */
86+
(createDOMElement("a", { text, attributes }));
87+
88+
return htmlAnchor;
7189
}
7290

91+
/**
92+
* @param {string} spec
93+
* @returns {{ name: string, version: string }}
94+
*/
7395
export function parseNpmSpec(spec) {
7496
const parts = spec.split("@");
75-
const version = parts.at(-1);
97+
const version = parts.at(-1) ?? "";
7698

7799
return spec.startsWith("@") ?
78100
{ name: `@${parts[1]}`, version } :
79101
{ name: parts[0], version };
80102
}
81103

104+
/**
105+
* @param {{url?: string}} repository
106+
* @param {string | null} defaultValue
107+
* @returns {string | null} return repository url or defaultValue
108+
*/
82109
export function parseRepositoryUrl(repository = {}, defaultValue = null) {
83-
if (typeof repository !== "object" || !("url" in repository)) {
110+
if (!repository || !repository.url || typeof repository !== "object" || !("url" in repository)) {
84111
return defaultValue;
85112
}
86113

@@ -92,7 +119,7 @@ export function parseRepositoryUrl(repository = {}, defaultValue = null) {
92119
}
93120
if (repository.url.startsWith("git@")) {
94121
const execResult = /git@(?<platform>[a-zA-Z.]+):(?<repo>.+)\.git/gm.exec(repository.url);
95-
if (execResult === null) {
122+
if (execResult === null || !execResult.groups) {
96123
return defaultValue;
97124
}
98125

@@ -107,6 +134,12 @@ export function parseRepositoryUrl(repository = {}, defaultValue = null) {
107134
}
108135
}
109136

137+
/**
138+
* @param {string} title
139+
* @param {string} value
140+
* @param {Record<string, any>} options
141+
* @returns {HTMLElement}
142+
*/
110143
export function createLiField(title, value, options = {}) {
111144
const { isLink = false } = options;
112145

@@ -126,12 +159,20 @@ export function createLiField(title, value, options = {}) {
126159
return liElement;
127160
}
128161

162+
/**
163+
* @param {HTMLElement} node - The parent DOM element.
164+
* @param {string[]} items - Array of strings to display.
165+
* @param {Object} [options] - Optional configuration options.
166+
* @param {Function} [options.onclick] - Callback function (event, item).
167+
* @param {boolean} [options.hideItems] - Hide items if needed.
168+
* @param {number} [options.hideItemsLength] - Number of visible elements before masking.
169+
* @returns {void}
170+
*/
129171
export function createItemsList(node, items = [], options = {}) {
130172
const { onclick = null, hideItems = false, hideItemsLength = 5 } = options;
131-
132173
if (items.length === 0) {
133174
const previousNode = node.previousElementSibling;
134-
if (previousNode !== null) {
175+
if (previousNode !== null && previousNode instanceof HTMLElement) {
135176
previousNode.style.display = "none";
136177
}
137178

@@ -157,8 +198,10 @@ export function createItemsList(node, items = [], options = {}) {
157198
}
158199

159200
if (hideItems && items.length > hideItemsLength) {
160-
const expandableSpan = document.createElement("expandable-span");
161-
expandableSpan.onToggle = (expandable) => toggle(expandable, node, hideItemsLength);
201+
const expandableSpan =
202+
/** @type {import("../components/expandable/expandable.js").ExpandableType} */
203+
(document.createElement("expandable-span"));
204+
expandableSpan.onToggle = () => toggle(expandableSpan, node, hideItemsLength);
162205
fragment.appendChild(expandableSpan);
163206
}
164207
node.appendChild(fragment);
@@ -168,10 +211,16 @@ export function createItemsList(node, items = [], options = {}) {
168211
TODO: this util function won't be necessary once the parents of the expandable component will be migrated to lit
169212
becuase the parents will handle the filtering of their children themselves
170213
*/
214+
/**
215+
* @param {import("../components/expandable/expandable.js").ExpandableType} expandable
216+
* @param {HTMLElement} parentNode
217+
* @param {number} hideItemsLength
218+
* @returns {void}
219+
*/
171220
export function toggle(expandable, parentNode, hideItemsLength) {
172221
expandable.isClosed = !expandable.isClosed;
173-
for (let id = 0; id < parentNode.childNodes.length; id++) {
174-
const node = parentNode.childNodes[id];
222+
for (let id = 0; id < parentNode.children.length; id++) {
223+
const node = parentNode.children[id];
175224
if (node.tagName === "EXPANDABLE-SPAN") {
176225
continue;
177226
}
@@ -185,26 +234,37 @@ export function toggle(expandable, parentNode, hideItemsLength) {
185234
}
186235
}
187236

237+
/**
238+
* @param {string} str
239+
* @returns {void}
240+
*/
188241
export function copyToClipboard(str) {
189242
const el = document.createElement("textarea");
190243
el.value = str;
191244
el.setAttribute("readonly", "");
192245
el.style.position = "absolute";
193246
el.style.left = "-9999px";
194247
document.body.appendChild(el);
195-
const selected =
196-
document.getSelection().rangeCount > 0
197-
? document.getSelection().getRangeAt(0)
198-
: false;
248+
const selection = document.getSelection();
249+
const selected = selection && selection.rangeCount > 0 ? selection.getRangeAt(0) : false;
199250
el.select();
200251
document.execCommand("copy");
201252
document.body.removeChild(el);
202-
if (selected) {
203-
document.getSelection().removeAllRanges();
204-
document.getSelection().addRange(selected);
253+
if (selected && selection) {
254+
selection.removeAllRanges();
255+
selection.addRange(selected);
205256
}
206257
}
207258

259+
/**
260+
* @typedef {{reverse?: boolean, blacklist?: Node[], hiddenTarget?: HTMLElement, callback?: () => void}} hideOnClickOutsideOptions
261+
*/
262+
263+
/**
264+
* @param {HTMLElement} element
265+
* @param {hideOnClickOutsideOptions} options
266+
* @returns {(event: Event) => void}
267+
*/
208268
export function hideOnClickOutside(
209269
element,
210270
options = {}
@@ -216,8 +276,15 @@ export function hideOnClickOutside(
216276
callback = () => void 0
217277
} = options;
218278

279+
/** @param {Event} event */
219280
function outsideClickListener(event) {
220-
if (!element.contains(event.target) && !blacklist.includes(event.target)) {
281+
const target = event.target;
282+
283+
if (!(target instanceof Node)) {
284+
return;
285+
}
286+
287+
if (!element.contains(target) && !blacklist.includes(target)) {
221288
if (hiddenTarget) {
222289
if (reverse) {
223290
hiddenTarget.classList.remove("show");
@@ -240,13 +307,24 @@ export function hideOnClickOutside(
240307
return outsideClickListener;
241308
}
242309

310+
/** @returns {string} */
243311
export function currentLang() {
244-
const detectedLang = document.getElementById("lang").dataset.lang;
312+
const detectedLang = document.getElementById("lang")?.dataset.lang;
313+
const defaultLanguage = "english";
314+
if (!detectedLang) {
315+
return defaultLanguage;
316+
}
245317

246-
return detectedLang in window.i18n ? detectedLang : "english";
318+
return detectedLang in window.i18n ? detectedLang : defaultLanguage;
247319
}
248320

321+
/**
322+
* @param {Function} callback
323+
* @param {number} delay
324+
* @returns {() => void}
325+
*/
249326
export function debounce(callback, delay) {
327+
/** @type {ReturnType<typeof setTimeout> | undefined} */
250328
let timer;
251329

252330
// eslint-disable-next-line func-names

public/components/expandable/expandable.js

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,17 @@ import { when } from "lit/directives/when.js";
66
import { currentLang } from "../../common/utils";
77
import "../icon/icon.js";
88

9-
class Expandable extends LitElement {
9+
/**
10+
* @typedef {Record<string, { home: { showMore: string, showLess: string } }>} I18nLanguage
11+
*/
12+
13+
/**
14+
* "Expandable" web component displaying a toggle button with an icon.
15+
* @element expandable-span
16+
* @prop {Function} onToggle - Function called during the interaction (default: () => void 0).
17+
* @prop {boolean} isClosed - Specifies whether the associated content is hidden (true) or visible (false).
18+
*/
19+
export class Expandable extends LitElement {
1020
static styles = css`
1121
span.expandable {
1222
display: flex;
@@ -43,19 +53,24 @@ span.expandable nsecure-icon {
4353
constructor() {
4454
super();
4555
this.isClosed = true;
46-
this.onToggle = () => void 0;
56+
/** @type {(instance: Expandable) => void} */
57+
this.onToggle = () => void {};
4758
}
4859

4960
render() {
5061
const lang = currentLang();
62+
const i18n =
63+
/** @type I18nLanguage */
64+
(window.i18n);
65+
const translations = i18n[lang].home;
5166

5267
return html`
5368
<span data-value=${this.isClosed ? "closed" : "opened"} @click=${this.#handleClick} class="expandable">
5469
${when(this.isClosed,
5570
() => html`<nsecure-icon name="plus"></nsecure-icon>
56-
<p>${window.i18n[lang].home.showMore}</p>`,
71+
<p>${translations.showMore}</p>`,
5772
() => html`<nsecure-icon name="minus"></nsecure-icon>
58-
<p>${window.i18n[lang].home.showLess}</p>`
73+
<p>${translations.showLess}</p>`
5974
)}
6075
</span>
6176
`;
@@ -67,3 +82,6 @@ span.expandable nsecure-icon {
6782
}
6883

6984
customElements.define("expandable-span", Expandable);
85+
/**
86+
* @typedef {import('./expandable.js').Expandable} ExpandableType
87+
*/

0 commit comments

Comments
 (0)