Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 98 additions & 20 deletions public/common/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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;
}

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

Expand All @@ -107,6 +134,12 @@ export function parseRepositoryUrl(repository = {}, defaultValue = null) {
}
}

/**
* @param {string} title
* @param {string} value
* @param {Record<string, any>} options
* @returns {HTMLElement}
*/
export function createLiField(title, value, options = {}) {
const { isLink = false } = options;

Expand All @@ -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";
}

Expand All @@ -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);
Expand All @@ -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;
}
Expand All @@ -185,26 +234,37 @@ export function toggle(expandable, parentNode, hideItemsLength) {
}
}

/**
* @param {string} str
* @returns {void}
*/
export function copyToClipboard(str) {
const el = document.createElement("textarea");
el.value = str;
el.setAttribute("readonly", "");
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 = {}
Expand All @@ -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");
Expand All @@ -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<typeof setTimeout> | undefined} */
let timer;

// eslint-disable-next-line func-names
Expand Down
26 changes: 22 additions & 4 deletions public/components/expandable/expandable.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, { home: { showMore: string, showLess: string } }>} 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;
Expand Down Expand Up @@ -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`
<span data-value=${this.isClosed ? "closed" : "opened"} @click=${this.#handleClick} class="expandable">
${when(this.isClosed,
() => html`<nsecure-icon name="plus"></nsecure-icon>
<p>${window.i18n[lang].home.showMore}</p>`,
<p>${translations.showMore}</p>`,
() => html`<nsecure-icon name="minus"></nsecure-icon>
<p>${window.i18n[lang].home.showLess}</p>`
<p>${translations.showLess}</p>`
)}
</span>
`;
Expand All @@ -67,3 +82,6 @@ span.expandable nsecure-icon {
}

customElements.define("expandable-span", Expandable);
/**
* @typedef {import('./expandable.js').Expandable} ExpandableType
*/
Loading