Skip to content

Commit 9a3dad3

Browse files
committed
Adding icon picker asset
1 parent 4c366c0 commit 9a3dad3

File tree

3 files changed

+160
-1
lines changed

3 files changed

+160
-1
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
obj/
55
bin/
66
artifacts/
7-
wwwroot/
7+
# Ignore generated vendor assets, keep custom wwwroot source files trackable.
8+
Lombiq.HelpfulExtensions/wwwroot/vendors/*
89
node_modules/
910
*.user
1011
.pnpm-debug.log
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
.lucide-icon-picker__menu {
2+
min-width: 20rem;
3+
}
4+
5+
.lucide-icon-picker__grid {
6+
display: grid;
7+
grid-template-columns: repeat(3, minmax(0, 1fr));
8+
gap: 0.5rem;
9+
max-height: 18rem;
10+
overflow-y: auto;
11+
}
12+
13+
.lucide-icon-picker__option {
14+
display: flex;
15+
align-items: center;
16+
gap: 0.5rem;
17+
width: 100%;
18+
padding: 0.5rem;
19+
border: 1px solid var(--bs-border-color, #dee2e6);
20+
border-radius: 0.375rem;
21+
background: var(--bs-body-bg, #fff);
22+
color: inherit;
23+
text-align: left;
24+
}
25+
26+
.lucide-icon-picker__option:hover,
27+
.lucide-icon-picker__option:focus-visible,
28+
.lucide-icon-picker__option.active {
29+
border-color: var(--bs-primary, #0d6efd);
30+
background: color-mix(in srgb, var(--bs-primary, #0d6efd) 8%, var(--bs-body-bg, #fff));
31+
outline: 0;
32+
}
33+
34+
.lucide-icon-picker__icon,
35+
.lucide-icon-picker__preview {
36+
display: inline-flex;
37+
align-items: center;
38+
justify-content: center;
39+
min-width: 1.25rem;
40+
min-height: 1.25rem;
41+
}
42+
43+
.lucide-icon-picker__label {
44+
overflow: hidden;
45+
text-overflow: ellipsis;
46+
white-space: nowrap;
47+
font-size: 0.875rem;
48+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
(function () {
2+
const lucide = window.lucide;
3+
4+
if (!lucide || !lucide.icons) return;
5+
6+
const iconEntries = Object.keys(lucide.icons)
7+
.map(key => ({
8+
key,
9+
value: key
10+
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
11+
.replace(/([A-Z])([A-Z][a-z])/g, "$1-$2")
12+
.toLowerCase(),
13+
}))
14+
.sort((left, right) => left.value.localeCompare(right.value));
15+
16+
const renderIcons = () => lucide.createIcons({
17+
attrs: {
18+
width: 18,
19+
height: 18,
20+
"stroke-width": 1.75,
21+
},
22+
});
23+
24+
const getHiddenInput = root =>
25+
root.closest("[id$='_FieldWrapper']")?.querySelector("[data-lucide-value]") ??
26+
root.parentElement?.querySelector("[data-lucide-value]") ??
27+
root.querySelector("[data-lucide-value]");
28+
29+
const updateSelection = (root, value) => {
30+
const hiddenInput = getHiddenInput(root);
31+
const preview = root.querySelector("[data-lucide-preview]");
32+
33+
if (hiddenInput) hiddenInput.value = value || "";
34+
preview.innerHTML = value
35+
? `<i data-lucide="${value}"></i>`
36+
: "";
37+
38+
root.querySelectorAll("[data-lucide-icon]").forEach(option => {
39+
const isActive = option.dataset.lucideIcon === value;
40+
option.classList.toggle("active", isActive);
41+
option.setAttribute("aria-selected", isActive ? "true" : "false");
42+
});
43+
44+
renderIcons();
45+
};
46+
47+
const filterOptions = root => {
48+
const search = root.querySelector("[data-lucide-search]");
49+
const empty = root.querySelector("[data-lucide-empty]");
50+
const query = search.value.trim().toLowerCase();
51+
52+
let visibleOptionCount = 0;
53+
54+
root.querySelectorAll("[data-lucide-icon]").forEach(option => {
55+
const matches = !query || option.dataset.lucideIcon.includes(query);
56+
option.hidden = !matches;
57+
58+
if (matches) visibleOptionCount++;
59+
});
60+
61+
empty.classList.toggle("d-none", visibleOptionCount > 0);
62+
};
63+
64+
const initializePicker = root => {
65+
if (root.dataset.lucideIconPickerInitialized === "true") return;
66+
root.dataset.lucideIconPickerInitialized = "true";
67+
68+
const grid = root.querySelector("[data-lucide-grid]");
69+
const search = root.querySelector("[data-lucide-search]");
70+
const clear = root.querySelector("[data-lucide-clear]");
71+
const hiddenInput = getHiddenInput(root);
72+
73+
const optionsMarkup = iconEntries.map(({ value }) =>
74+
`<button type="button" class="lucide-icon-picker__option" role="option" ` +
75+
`data-lucide-icon="${value}" aria-label="${value}" aria-selected="false" title="${value}">` +
76+
`<span class="lucide-icon-picker__icon" aria-hidden="true"><i data-lucide="${value}"></i></span>` +
77+
`<span class="lucide-icon-picker__label">${value}</span>` +
78+
`</button>`).join("");
79+
80+
grid.innerHTML = optionsMarkup;
81+
82+
grid.addEventListener("click", event => {
83+
const option = event.target.closest("[data-lucide-icon]");
84+
if (!option) return;
85+
86+
updateSelection(root, option.dataset.lucideIcon);
87+
});
88+
89+
search.addEventListener("input", () => filterOptions(root));
90+
clear.addEventListener("click", () => {
91+
search.value = "";
92+
updateSelection(root, "");
93+
filterOptions(root);
94+
});
95+
96+
updateSelection(root, hiddenInput?.value);
97+
filterOptions(root);
98+
};
99+
100+
const initialize = () => {
101+
document.querySelectorAll("[data-lucide-icon-picker]").forEach(initializePicker);
102+
};
103+
104+
if (document.readyState === "loading") {
105+
document.addEventListener("DOMContentLoaded", initialize, { once: true });
106+
}
107+
else {
108+
initialize();
109+
}
110+
})();

0 commit comments

Comments
 (0)