type checking

This commit is contained in:
Artur
2025-11-07 17:59:15 +03:00
parent c9fece3dab
commit 5b7730d547
4 changed files with 164 additions and 61 deletions

View File

@ -30,17 +30,31 @@
*
*/
// @ts-check
/** @typedef {import('../types.js').IconCategoryType} IconCategoryType */
class CategoriesPicker {
#container;
#onSelectCategoryCallback = () => {};
/**
* @param {string} category
*/
#onSelectCategoryCallback = (category) => {};
#selectedCategory = "";
/**
* Constructor
* @param {IconCategoryType[]} catalogOfIcons
*/
constructor(catalogOfIcons) {
/** @type {HTMLDivElement} */
this.#container = document.getElementById("categories");
this.#addEventListener();
this.#show(catalogOfIcons);
}
/**
* @param {IconCategoryType[]} catalogOfIcons
*/
#show(catalogOfIcons) {
this.#selectedCategory = "";
const fragment = document.createDocumentFragment();
@ -60,52 +74,61 @@ class CategoriesPicker {
categoryName.className = "category-name";
});
this.#container.appendChild(fragment);
this.#container?.appendChild(fragment);
}
reset() {
if (this.#selectedCategory !== "") {
this.#selectedCategory = "";
this.#container
.querySelectorAll(".category.selected")
.forEach((category) => {
category.classList.remove("selected");
});
if (this.#selectedCategory === "") {
return;
}
this.#selectedCategory = "";
this.#container
?.querySelectorAll(".category.selected")
.forEach((category) => {
category.classList.remove("selected");
});
}
/**
* Set the callback function to be called when a category is selected.
* The callback function will receive the name of the selected category as a parameter.
* @param {function} callback - The callback function to be called when a category is selected
* @param {() => void} callback
*/
setOnSelectCategoryCallback(callback) {
this.#onSelectCategoryCallback = callback;
}
#addEventListener() {
this.#container.addEventListener("click", (e) => {
const categoryName = e.target.closest(".category-name");
if (categoryName) {
let id = categoryName.getAttribute("data-id");
let category = categoryName.parentElement;
let wasSelected = category.classList.contains("selected");
this.#container?.addEventListener("click", (e) => {
let categoryName;
this.#container
.querySelectorAll(".category.selected")
.forEach((category) => {
category.classList.remove("selected");
});
if (wasSelected) {
category.classList.remove("selected");
this.#selectedCategory = "";
} else {
category.classList.add("selected");
this.#selectedCategory = id;
}
this.#onSelectCategoryCallback(this.#selectedCategory);
const target = e.target;
if (target && target instanceof HTMLElement) {
categoryName = target.closest(".category-name");
}
if (!categoryName) {
return;
}
let id = categoryName.getAttribute("data-id");
if (typeof id !== "string") {
id = "";
}
let category = categoryName.parentElement;
let wasSelected = category?.classList.contains("selected");
this.#container
?.querySelectorAll(".category.selected")
.forEach((category) => {
category.classList.remove("selected");
});
if (wasSelected) {
category?.classList.remove("selected");
this.#selectedCategory = "";
} else {
category?.classList.add("selected");
this.#selectedCategory = id;
}
this.#onSelectCategoryCallback(this.#selectedCategory);
});
}
}

View File

@ -30,24 +30,44 @@
*
*/
// @ts-check
/** @typedef {import('../types.js').IconCategoryType} IconCategoryType */
class IconPicker {
#container;
#onSelectIconCallback = () => {};
/**
* @param {Map<string, string>} map
* @param {boolean} [needToRun]
*/
#onSelectIconCallback = (map, needToRun) => {};
#listOfIconNames;
#selectedIcons;
#clearSelectionButton;
/**
* Constructor
* @param {IconCategoryType[]} catalogOfIcons
*/
constructor(catalogOfIcons) {
this.#listOfIconNames = new Set();
this.#selectedIcons = new Map();
this.#container = document.getElementById("icons");
this.#clearSelectionButton = document.getElementById("clear");
this.#addEventListener();
this.show(catalogOfIcons);
}
/**
* @param {IconCategoryType[]} catalogOfIcons
* @param {string} categoryId
*/
show(catalogOfIcons, categoryId = "") {
this.#listOfIconNames = new Set();
this.#selectedIcons = new Map();
this.#container.textContent = "";
if (this.#container) {
this.#container.textContent = "";
}
const fragment = document.createDocumentFragment();
catalogOfIcons.forEach((categoryInfo) => {
@ -72,21 +92,29 @@ class IconPicker {
this.#onChange();
});
this.#container.appendChild(fragment);
this.#container?.appendChild(fragment);
if (this.#listOfIconNames.size === 0) {
if (this.#listOfIconNames.size === 0 && this.#container) {
this.#container.textContent =
"Your search didn't match any content. Please try another term.";
}
}
/**
* @param {() => void} callback
*/
setOnSelectIconCallback(callback) {
this.#onSelectIconCallback = callback;
}
#addEventListener() {
this.#container.addEventListener("click", (e) => {
const icon = e.target.closest(".icon");
this.#container?.addEventListener("click", (e) => {
let icon;
const target = e.target;
if (target && target instanceof HTMLElement) {
icon = target.closest(".icon");
}
if (icon) {
const isModifierPressed = e.ctrlKey || e.metaKey;
@ -106,22 +134,28 @@ class IconPicker {
this.#onChange();
}
});
this.#container.addEventListener("dblclick", (e) => {
const icon = e.target.closest(".icon");
if (icon) {
let iconId = icon.getAttribute("data-name");
let section = icon.getAttribute("data-section");
icon.classList.add("selected");
this.#selectedIcons.set(iconId, section);
const needToRun = true;
this.#onSelectIconCallback(this.#selectedIcons, needToRun);
this.#container?.addEventListener("dblclick", (e) => {
let icon;
const target = e.target;
if (target && target instanceof HTMLElement) {
icon = target.closest(".icon");
}
if (!icon) {
return;
}
let iconId = icon.getAttribute("data-name");
let section = icon.getAttribute("data-section");
icon.classList.add("selected");
this.#selectedIcons.set(iconId, section);
const needToRun = true;
this.#onSelectIconCallback(this.#selectedIcons, needToRun);
});
this.#clearSelectionButton.addEventListener(
this.#clearSelectionButton?.addEventListener(
"click",
this.#unselectAll.bind(this, false)
);
this.#container.addEventListener("keydown", (e) => {
this.#container?.addEventListener("keydown", (e) => {
if ((e.ctrlKey || e.metaKey) && e.code === "KeyA") {
e.preventDefault();
this.#selectAll();
@ -131,7 +165,7 @@ class IconPicker {
#selectAll() {
this.#container
.querySelectorAll(".icon:not(.selected)")
?.querySelectorAll(".icon:not(.selected)")
.forEach((icon) => {
let iconId = icon.getAttribute("data-name");
let section = icon.getAttribute("data-section");
@ -143,7 +177,7 @@ class IconPicker {
#unselectAll(silent = false) {
this.#selectedIcons = new Map();
this.#container.querySelectorAll(".icon.selected").forEach((icon) => {
this.#container?.querySelectorAll(".icon.selected").forEach((icon) => {
icon.classList.remove("selected");
});
if (silent) return;
@ -153,14 +187,20 @@ class IconPicker {
#onChange() {
const total = this.#listOfIconNames.size;
const selected =
this.#container.querySelectorAll(".icon.selected").length;
document.getElementById(
"total"
).textContent = `${total} icons, ${selected} selected`;
this.#container?.querySelectorAll(".icon.selected").length;
const totalElement = document.getElementById("total");
if (totalElement) {
totalElement.textContent = `${total} icons, ${selected} selected`;
}
this.#onSelectIconCallback(this.#selectedIcons);
}
/**
* @param {string} iconId
* @param {string} section
* @returns
*/
#createIcon(iconId, section) {
const svgNS = "http://www.w3.org/2000/svg";
const xlinkNS = "http://www.w3.org/1999/xlink";
@ -172,7 +212,7 @@ class IconPicker {
svg.setAttribute("role", "img");
svg.setAttribute("data-name", iconId);
svg.setAttribute("data-section", section);
svg.setAttribute("tabindex", 0);
svg.setAttribute("tabindex", "0");
const title = document.createElementNS(svgNS, "title");
svg.appendChild(title);

View File

@ -30,37 +30,64 @@
*
*/
// @ts-check
/** @typedef {import('../types.js').IconCategoryType} IconCategoryType */
class SearchFilter {
#catalogOfIcons;
#filteredCatalog;
#onFilterCallback;
/**
* @param {IconCategoryType[]} catalogOfIcons
*/
constructor(catalogOfIcons) {
this.#onFilterCallback = (
/** @type {IconCategoryType[]} */ categories
) => {};
this.#filteredCatalog = catalogOfIcons;
this.#catalogOfIcons = catalogOfIcons;
this.input = document.getElementById("searchFilter");
this.input.addEventListener("input", this.#onInput.bind(this));
this.input?.addEventListener(
"input",
this.#onInput.bind(this, this.input)
);
}
reset() {
if (this.input instanceof HTMLInputElement === false) {
return;
}
if (this.input.value !== "") {
this.input.value = "";
this.#filteredCatalog = this.#catalogOfIcons;
}
}
/**
* @param {() => {}} callback
*/
setOnFilterCallback(callback) {
this.#onFilterCallback = callback;
}
#onInput(e) {
const value = e.target.value.slice().toLowerCase();
/**
* @param {HTMLElement} input
*/
#onInput(input) {
if (input instanceof HTMLInputElement === false) {
return;
}
let value = input.value.slice().toLowerCase();
if (value === "") {
this.#filteredCatalog = this.#catalogOfIcons;
} else {
this.#filteredCatalog = this.#catalogOfIcons
.slice()
.map((categoryInfo) => {
let filteredIcons = [];
/** @type {[string[]]} */
let filteredIcons = [[]];
categoryInfo.folders.forEach((folderName, index) => {
let icons = categoryInfo.icons[index];

View File

@ -0,0 +1,13 @@
/**
* @typedef {'brands' | 'regular' | 'solid'} FolderType
*/
/**
* @typedef {Object} IconCategoryType
* @property {string} id
* @property {string} label
* @property {FolderType[]} folders
* @property {[string[]]} icons
*/
export {};