refactor(cms): replace custom icon comboboxes with standardized icon picker

Replace the legacy `cms-icon-combobox` implementation with a new, standardized `icon-picker` across multiple admin modules. This change simplifies the UI by using a Bootstrap input group with a dedicated "Pick Icon" button and a preview cell.

Affected areas:
- Policies block editor
- Accreditation, Admissions, History, and Policies admin editor scripts
- Admissions calculator options view
This removes redundant icon list generation and manual dropdown management in favor of a centralized icon picker utility.
This commit is contained in:
Tống Thành Đạt
2026-04-22 20:27:42 +07:00
parent c0ea0dcb4d
commit 2cee0172f2
6 changed files with 1043 additions and 740 deletions
@@ -12,15 +12,6 @@
}
const state = JSON.parse(JSON.stringify(initialData));
const iconOptions = Array.from(
new Set(
(config.tabs || [])
.flatMap((tab) => collectIcons(tab.schema))
.filter(Boolean),
),
);
ensureIconDatalist(iconOptions);
renderAllSections();
document.querySelectorAll("[data-tab-key]").forEach((tabTrigger) => {
@@ -602,119 +593,191 @@
}
function renderIconField(schema, col, parent, key) {
const selected = parent[key] || "";
const wrapper = document.createElement("div");
wrapper.className = "cms-icon-combobox";
const trigger = document.createElement("button");
trigger.type = "button";
trigger.className = "cms-icon-dropdown-trigger";
const group = document.createElement("div");
group.className = "input-group";
const triggerValue = document.createElement("div");
triggerValue.className = "cms-icon-dropdown-value";
const preview = document.createElement("span");
preview.className = "input-group-text icon-preview-cell";
preview.style.minWidth = "38px";
group.appendChild(preview);
const preview = document.createElement("div");
preview.className = "cms-icon-preview";
triggerValue.appendChild(preview);
const triggerText = document.createElement("div");
triggerText.className = "cms-icon-dropdown-text";
triggerValue.appendChild(triggerText);
trigger.appendChild(triggerValue);
const caret = document.createElement("i");
caret.className = "fas fa-chevron-down cms-icon-dropdown-caret";
trigger.appendChild(caret);
wrapper.appendChild(trigger);
const panel = document.createElement("div");
panel.className = "cms-icon-dropdown-panel d-none";
const searchInput = document.createElement("input");
searchInput.type = "text";
searchInput.className = "form-control";
searchInput.placeholder = "Search icon name";
panel.appendChild(searchInput);
const optionsWrap = document.createElement("div");
optionsWrap.className = "cms-icon-options";
panel.appendChild(optionsWrap);
const empty = document.createElement("div");
empty.className = "cms-icon-option-empty d-none";
empty.textContent = "No matching icons";
panel.appendChild(empty);
wrapper.appendChild(panel);
const setOpen = function (isOpen) {
wrapper.classList.toggle("is-open", isOpen);
panel.classList.toggle("d-none", !isOpen);
if (isOpen) {
searchInput.focus();
searchInput.select();
}
};
const updateTrigger = function (value) {
preview.innerHTML = value
? `<i class="fa-solid ${escapeHtml(value)}"></i>`
: '<span class="text-muted small">--</span>';
triggerText.innerHTML = value
? `<strong>${escapeHtml(value)}</strong><span class="text-muted small">Selected icon</span>`
: '<strong>Select icon</strong><span class="text-muted small">No icon selected</span>';
};
const renderOptions = function (searchTerm = "") {
const normalized = String(searchTerm || "").trim().toLowerCase();
const filteredOptions = (schema.options || []).filter((option) =>
option.toLowerCase().includes(normalized),
);
optionsWrap.innerHTML = "";
empty.classList.toggle("d-none", filteredOptions.length > 0);
filteredOptions.forEach((option) => {
const button = document.createElement("button");
button.type = "button";
button.className = `cms-icon-option ${parent[key] === option ? "is-active" : ""}`;
button.innerHTML = `<span class="cms-icon-option-main"><i class="fa-solid ${escapeHtml(option)}"></i><span>${escapeHtml(option)}</span></span>${parent[key] === option ? '<i class="fas fa-check small"></i>' : ""}`;
button.addEventListener("click", function () {
parent[key] = option;
searchInput.value = option;
updateTrigger(option);
renderOptions(option);
setOpen(false);
});
optionsWrap.appendChild(button);
});
};
trigger.addEventListener("click", function () {
const nextOpen = panel.classList.contains("d-none");
setOpen(nextOpen);
if (nextOpen) {
renderOptions(searchInput.value || parent[key] || "");
}
const input = document.createElement("input");
input.type = "text";
input.className = "form-control";
input.value = parent[key] || "";
input.readOnly = true;
input.placeholder = schema.placeholder || "Click to pick...";
input.style.cursor = "pointer";
input.style.backgroundColor = "#fff";
input.dataset.iconPickerValueMode = "icon-name";
input.dataset.iconPickerPreviewPrefix = "fa-solid";
input.addEventListener("click", function () {
openIconPickerForInput(input);
});
searchInput.addEventListener("input", function () {
renderOptions(searchInput.value);
input.addEventListener("input", function () {
parent[key] = input.value;
});
group.appendChild(input);
wrapper.addEventListener("focusout", function () {
window.setTimeout(function () {
if (!wrapper.contains(document.activeElement)) {
setOpen(false);
}
}, 0);
const button = document.createElement("button");
button.type = "button";
button.className = "btn btn-outline-secondary";
button.innerHTML = '<i class="fas fa-icons me-1"></i>Pick Icon';
button.addEventListener("click", function () {
openIconPickerForInput(input);
});
group.appendChild(button);
searchInput.value = selected;
updateTrigger(selected);
renderOptions(selected);
col.appendChild(wrapper);
col.appendChild(group);
syncIconPickerPreview(input);
appendHelp(col, schema, parent[key]);
}
function openIconPickerForInput(input) {
window.__cmsIconPickerActiveInput = input;
patchIconPickerForIconNames();
window.IconPicker?.open(input);
}
function patchIconPickerForIconNames() {
if (!window.IconPicker || window.IconPicker.__cmsIconNamePatched) {
return;
}
const originalPick = typeof window.IconPicker.pick === "function"
? window.IconPicker.pick.bind(window.IconPicker)
: null;
if (!originalPick) {
return;
}
const patchedPick = function (value) {
originalPick(value);
const activeInput = window.__cmsIconPickerActiveInput;
if (!activeInput) {
return;
}
if (activeInput.dataset.iconPickerValueMode === "icon-name") {
activeInput.dataset.iconPickerPreviewPrefix = extractIconStyle(
activeInput.value || value,
) || determineIconPreviewPrefix(activeInput);
activeInput.value = extractIconName(activeInput.value || value);
syncIconPickerPreview(activeInput);
activeInput.dispatchEvent(new Event("input", { bubbles: true }));
activeInput.dispatchEvent(new Event("change", { bubbles: true }));
}
window.__cmsIconPickerActiveInput = null;
};
window.IconPicker.pick = patchedPick;
window.IconPickerPick = patchedPick;
window.IconPicker.__cmsIconNamePatched = true;
}
function syncIconPickerPreview(input) {
if (!input) return;
const previewCell = input.closest(".input-group")?.querySelector(".icon-preview-cell");
if (!previewCell) return;
const previewClass = resolveIconPreviewClass(input);
previewCell.innerHTML = previewClass ? `<i class="${escapeHtml(previewClass)}"></i>` : "";
if (input.dataset.iconPickerValueMode === "icon-name") {
loadIconStyleLookup().then(function () {
const nextPrefix = determineIconPreviewPrefix(input);
if (nextPrefix !== input.dataset.iconPickerPreviewPrefix) {
input.dataset.iconPickerPreviewPrefix = nextPrefix;
const nextPreviewClass = resolveIconPreviewClass(input);
previewCell.innerHTML = nextPreviewClass
? `<i class="${escapeHtml(nextPreviewClass)}"></i>`
: "";
}
});
}
}
function resolveIconPreviewClass(input) {
const normalizedValue = String(input?.value || "").trim();
if (!normalizedValue) {
return "";
}
if (input?.dataset.iconPickerValueMode === "icon-name") {
const prefix = determineIconPreviewPrefix(input);
return `${prefix} ${normalizedValue}`.trim();
}
return normalizedValue;
}
function extractIconName(value) {
return String(value || "")
.trim()
.split(/\s+/)
.find(
(token) =>
/^fa-[a-z0-9-]+$/i.test(token) && !/^fa-(solid|regular|brands)$/i.test(token),
) || "";
}
function extractIconStyle(value) {
return String(value || "")
.trim()
.split(/\s+/)
.find((token) => /^fa-(solid|regular|brands)$/i.test(token)) || "";
}
function determineIconPreviewPrefix(input) {
const explicitPrefix = String(input?.dataset.iconPickerPreviewPrefix || "").trim();
if (explicitPrefix && explicitPrefix !== "fa-solid") {
return explicitPrefix;
}
const iconName = extractIconName(input?.value || "");
const knownStyles = window.__cmsIconStyleLookup?.[iconName] || [];
if (knownStyles.includes("fa-brands")) return "fa-brands";
if (knownStyles.includes("fa-regular")) return "fa-regular";
if (knownStyles.includes("fa-solid")) return "fa-solid";
return explicitPrefix || "fa-solid";
}
function loadIconStyleLookup() {
if (window.__cmsIconStyleLookupPromise) {
return window.__cmsIconStyleLookupPromise;
}
window.__cmsIconStyleLookupPromise = fetch("/js/fa-icons.json")
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
})
.then((json) => {
const lookup = {};
Object.entries(json || {}).forEach(([name, meta]) => {
lookup[`fa-${name}`] = (meta.styles || [])
.filter((style) => ["solid", "regular", "brands"].includes(style))
.map((style) => `fa-${style}`);
});
window.__cmsIconStyleLookup = lookup;
return lookup;
})
.catch(() => {
window.__cmsIconStyleLookup = window.__cmsIconStyleLookup || {};
return window.__cmsIconStyleLookup;
});
return window.__cmsIconStyleLookupPromise;
}
function renderCheckbox(schema, container, parent, key) {
if (typeof parent[key] !== "boolean") {
parent[key] = Boolean(parent[key]);
@@ -892,33 +955,6 @@
return schema.options || [];
}
function collectIcons(schema) {
if (!schema) return [];
if (schema.type === "icon") return schema.options || [];
if (schema.type === "object") return (schema.fields || []).flatMap(collectIcons);
if (schema.type === "array") return collectIcons(schema.itemSchema);
if (schema.type === "variant") {
return Object.values(schema.variants || {}).flatMap((variant) =>
collectIcons(variant.schema),
);
}
return [];
}
function ensureIconDatalist(options) {
const existing = document.getElementById("cms-icon-options");
if (existing) existing.remove();
const dataList = document.createElement("datalist");
dataList.id = "cms-icon-options";
options.forEach((option) => {
const item = document.createElement("option");
item.value = option;
dataList.appendChild(item);
});
document.body.appendChild(dataList);
}
function applyAutoSequenceToArray(schema, targetArray) {
if (!schema || !Array.isArray(targetArray) || schema.itemSchema.type !== "object") {
return;
+166 -80
View File
@@ -87,20 +87,23 @@
<div class="col-12">
<label class="form-label fw-semibold"><%= fieldConfig.noteIcon?.label || "Note icon" %></label>
<div class="cms-icon-combobox" id="noteIconCombobox">
<input type="hidden" name="noteIcon" id="noteIcon" value="<%= option.noteIcon %>" />
<button type="button" class="cms-icon-dropdown-trigger" id="noteIconTrigger">
<div class="cms-icon-dropdown-value">
<div class="cms-icon-preview" id="noteIconPreview"></div>
<div class="cms-icon-dropdown-text" id="noteIconText"></div>
</div>
<i class="fas fa-chevron-down cms-icon-dropdown-caret"></i>
<div class="input-group">
<span class="input-group-text icon-preview-cell" style="min-width:38px"></span>
<input
type="text"
name="noteIcon"
id="noteIcon"
class="form-control"
value="<%= option.noteIcon %>"
placeholder="Click to pick..."
readonly
style="cursor:pointer;background:#fff"
data-icon-picker-value-mode="icon-name"
data-icon-picker-preview-prefix="fa-solid"
/>
<button type="button" class="btn btn-outline-secondary" id="noteIconButton">
<i class="fas fa-icons me-1"></i>Pick Icon
</button>
<div class="cms-icon-dropdown-panel d-none" id="noteIconPanel">
<input type="text" class="form-control" id="noteIconSearch" placeholder="Search icon name" />
<div class="cms-icon-options" id="noteIconOptions"></div>
<div class="cms-icon-option-empty d-none" id="noteIconEmpty">No matching icons</div>
</div>
</div>
<div class="field-meta-row">
<div class="form-text"><%= fieldConfig.noteIcon?.helpText || "" %></div>
@@ -136,19 +139,11 @@
<script>
(function () {
const iconOptions = <%- JSON.stringify(iconOptions) %>;
const existingOptionLabels = <%- JSON.stringify(existingOptionLabels || []) %>;
const form = document.getElementById("calculatorOptionForm");
const labelInput = document.getElementById("label");
const hiddenInput = document.getElementById("noteIcon");
const combobox = document.getElementById("noteIconCombobox");
const trigger = document.getElementById("noteIconTrigger");
const panel = document.getElementById("noteIconPanel");
const preview = document.getElementById("noteIconPreview");
const text = document.getElementById("noteIconText");
const search = document.getElementById("noteIconSearch");
const optionsWrap = document.getElementById("noteIconOptions");
const empty = document.getElementById("noteIconEmpty");
const noteIconInput = document.getElementById("noteIcon");
const noteIconButton = document.getElementById("noteIconButton");
form.querySelectorAll("[maxlength]").forEach((input) => {
const counter = form.querySelector(`[data-counter-for="${input.id}"]`);
@@ -207,69 +202,160 @@
this.value = numericValue > 0 ? String(Math.floor(numericValue)) : "1";
});
function updateTrigger(value) {
preview.innerHTML = value
? `<i class="fa-solid ${escapeHtml(value)}"></i>`
: '<span class="text-muted small">--</span>';
text.innerHTML = value
? `<strong>${escapeHtml(value)}</strong><span class="text-muted small">Selected icon</span>`
: '<strong>Select icon</strong><span class="text-muted small">No icon selected</span>';
}
function setOpen(isOpen) {
combobox.classList.toggle("is-open", isOpen);
panel.classList.toggle("d-none", !isOpen);
if (isOpen) {
search.focus();
search.select();
}
}
function renderOptions(term) {
const query = String(term || "").trim().toLowerCase();
const filtered = iconOptions.filter((option) => option.toLowerCase().includes(query));
optionsWrap.innerHTML = "";
empty.classList.toggle("d-none", filtered.length > 0);
filtered.forEach((option) => {
const button = document.createElement("button");
button.type = "button";
button.className = `cms-icon-option ${hiddenInput.value === option ? "is-active" : ""}`;
button.innerHTML = `<span class="cms-icon-option-main"><i class="fa-solid ${escapeHtml(option)}"></i><span>${escapeHtml(option)}</span></span>${hiddenInput.value === option ? '<i class="fas fa-check small"></i>' : ""}`;
button.addEventListener("click", function () {
hiddenInput.value = option;
search.value = option;
updateTrigger(option);
renderOptions(option);
setOpen(false);
});
optionsWrap.appendChild(button);
});
}
trigger.addEventListener("click", function () {
const shouldOpen = panel.classList.contains("d-none");
setOpen(shouldOpen);
if (shouldOpen) {
renderOptions(search.value || hiddenInput.value);
}
noteIconInput?.addEventListener("click", function () {
openIconPickerForInput(noteIconInput);
});
search.addEventListener("input", function () {
renderOptions(search.value);
noteIconButton?.addEventListener("click", function () {
openIconPickerForInput(noteIconInput);
});
combobox.addEventListener("focusout", function () {
window.setTimeout(function () {
if (!combobox.contains(document.activeElement)) {
setOpen(false);
syncIconPickerPreview(noteIconInput);
function openIconPickerForInput(input) {
window.__cmsIconPickerActiveInput = input;
patchIconPickerForIconNames();
window.IconPicker?.open(input);
}
function patchIconPickerForIconNames() {
if (!window.IconPicker || window.IconPicker.__cmsIconNamePatched) {
return;
}
const originalPick = typeof window.IconPicker.pick === "function"
? window.IconPicker.pick.bind(window.IconPicker)
: null;
if (!originalPick) {
return;
}
const patchedPick = function (value) {
originalPick(value);
const activeInput = window.__cmsIconPickerActiveInput;
if (!activeInput) {
return;
}
}, 0);
});
search.value = hiddenInput.value;
updateTrigger(hiddenInput.value);
renderOptions(hiddenInput.value);
if (activeInput.dataset.iconPickerValueMode === "icon-name") {
activeInput.dataset.iconPickerPreviewPrefix = extractIconStyle(
activeInput.value || value,
) || determineIconPreviewPrefix(activeInput);
activeInput.value = extractIconName(activeInput.value || value);
syncIconPickerPreview(activeInput);
activeInput.dispatchEvent(new Event("input", { bubbles: true }));
activeInput.dispatchEvent(new Event("change", { bubbles: true }));
}
window.__cmsIconPickerActiveInput = null;
};
window.IconPicker.pick = patchedPick;
window.IconPickerPick = patchedPick;
window.IconPicker.__cmsIconNamePatched = true;
}
function syncIconPickerPreview(input) {
if (!input) return;
const previewCell = input.closest(".input-group")?.querySelector(".icon-preview-cell");
if (!previewCell) return;
const previewClass = resolveIconPreviewClass(input);
previewCell.innerHTML = previewClass ? `<i class="${escapeHtml(previewClass)}"></i>` : "";
if (input.dataset.iconPickerValueMode === "icon-name") {
loadIconStyleLookup().then(function () {
const nextPrefix = determineIconPreviewPrefix(input);
if (nextPrefix !== input.dataset.iconPickerPreviewPrefix) {
input.dataset.iconPickerPreviewPrefix = nextPrefix;
const nextPreviewClass = resolveIconPreviewClass(input);
previewCell.innerHTML = nextPreviewClass
? `<i class="${escapeHtml(nextPreviewClass)}"></i>`
: "";
}
});
}
}
function resolveIconPreviewClass(input) {
const normalizedValue = String(input?.value || "").trim();
if (!normalizedValue) {
return "";
}
if (input?.dataset.iconPickerValueMode === "icon-name") {
const prefix = determineIconPreviewPrefix(input);
return `${prefix} ${normalizedValue}`.trim();
}
return normalizedValue;
}
function extractIconName(value) {
return String(value || "")
.trim()
.split(/\s+/)
.find(
(token) =>
/^fa-[a-z0-9-]+$/i.test(token) && !/^fa-(solid|regular|brands)$/i.test(token),
) || "";
}
function extractIconStyle(value) {
return String(value || "")
.trim()
.split(/\s+/)
.find((token) => /^fa-(solid|regular|brands)$/i.test(token)) || "";
}
function determineIconPreviewPrefix(input) {
const explicitPrefix = String(input?.dataset.iconPickerPreviewPrefix || "").trim();
if (explicitPrefix && explicitPrefix !== "fa-solid") {
return explicitPrefix;
}
const iconName = extractIconName(input?.value || "");
const knownStyles = window.__cmsIconStyleLookup?.[iconName] || [];
if (knownStyles.includes("fa-brands")) return "fa-brands";
if (knownStyles.includes("fa-regular")) return "fa-regular";
if (knownStyles.includes("fa-solid")) return "fa-solid";
return explicitPrefix || "fa-solid";
}
function loadIconStyleLookup() {
if (window.__cmsIconStyleLookupPromise) {
return window.__cmsIconStyleLookupPromise;
}
window.__cmsIconStyleLookupPromise = fetch("/js/fa-icons.json")
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
})
.then((json) => {
const lookup = {};
Object.entries(json || {}).forEach(([name, meta]) => {
lookup[`fa-${name}`] = (meta.styles || [])
.filter((style) => ["solid", "regular", "brands"].includes(style))
.map((style) => `fa-${style}`);
});
window.__cmsIconStyleLookup = lookup;
return lookup;
})
.catch(() => {
window.__cmsIconStyleLookup = window.__cmsIconStyleLookup || {};
return window.__cmsIconStyleLookup;
});
return window.__cmsIconStyleLookupPromise;
}
function escapeHtml(value) {
return String(value || "")
+175 -139
View File
@@ -18,15 +18,6 @@
? initialData.calculator.options.map((item) => item.id).filter(Boolean)
: [],
);
const iconOptions = Array.from(
new Set(
(config.tabs || [])
.flatMap((tab) => collectIcons(tab.schema))
.filter(Boolean),
),
);
ensureIconDatalist(iconOptions);
renderAllSections();
document.querySelectorAll("[data-tab-key]").forEach((tabTrigger) => {
@@ -1078,119 +1069,191 @@
}
function renderIconField(schema, col, parent, key) {
const selected = parent[key] || "";
const wrapper = document.createElement("div");
wrapper.className = "cms-icon-combobox";
const trigger = document.createElement("button");
trigger.type = "button";
trigger.className = "cms-icon-dropdown-trigger";
const group = document.createElement("div");
group.className = "input-group";
const triggerValue = document.createElement("div");
triggerValue.className = "cms-icon-dropdown-value";
const preview = document.createElement("span");
preview.className = "input-group-text icon-preview-cell";
preview.style.minWidth = "38px";
group.appendChild(preview);
const preview = document.createElement("div");
preview.className = "cms-icon-preview";
triggerValue.appendChild(preview);
const triggerText = document.createElement("div");
triggerText.className = "cms-icon-dropdown-text";
triggerValue.appendChild(triggerText);
trigger.appendChild(triggerValue);
const caret = document.createElement("i");
caret.className = "fas fa-chevron-down cms-icon-dropdown-caret";
trigger.appendChild(caret);
wrapper.appendChild(trigger);
const panel = document.createElement("div");
panel.className = "cms-icon-dropdown-panel d-none";
const searchInput = document.createElement("input");
searchInput.type = "text";
searchInput.className = "form-control";
searchInput.placeholder = "Search icon name";
panel.appendChild(searchInput);
const optionsWrap = document.createElement("div");
optionsWrap.className = "cms-icon-options";
panel.appendChild(optionsWrap);
const empty = document.createElement("div");
empty.className = "cms-icon-option-empty d-none";
empty.textContent = "No matching icons";
panel.appendChild(empty);
wrapper.appendChild(panel);
const setOpen = function (isOpen) {
wrapper.classList.toggle("is-open", isOpen);
panel.classList.toggle("d-none", !isOpen);
if (isOpen) {
searchInput.focus();
searchInput.select();
}
};
const updateTrigger = function (value) {
preview.innerHTML = value
? `<i class="fa-solid ${escapeHtml(value)}"></i>`
: '<span class="text-muted small">--</span>';
triggerText.innerHTML = value
? `<strong>${escapeHtml(value)}</strong><span class="text-muted small">Selected icon</span>`
: '<strong>Select icon</strong><span class="text-muted small">No icon selected</span>';
};
const renderOptions = function (searchTerm = "") {
const normalized = String(searchTerm || "").trim().toLowerCase();
const filteredOptions = (schema.options || []).filter((option) =>
option.toLowerCase().includes(normalized),
);
optionsWrap.innerHTML = "";
empty.classList.toggle("d-none", filteredOptions.length > 0);
filteredOptions.forEach((option) => {
const button = document.createElement("button");
button.type = "button";
button.className = `cms-icon-option ${parent[key] === option ? "is-active" : ""}`;
button.innerHTML = `<span class="cms-icon-option-main"><i class="fa-solid ${escapeHtml(option)}"></i><span>${escapeHtml(option)}</span></span>${parent[key] === option ? '<i class="fas fa-check small"></i>' : ""}`;
button.addEventListener("click", function () {
parent[key] = option;
searchInput.value = option;
updateTrigger(option);
renderOptions(option);
setOpen(false);
});
optionsWrap.appendChild(button);
});
};
trigger.addEventListener("click", function () {
const nextOpen = panel.classList.contains("d-none");
setOpen(nextOpen);
if (nextOpen) {
renderOptions(searchInput.value || parent[key] || "");
}
const input = document.createElement("input");
input.type = "text";
input.className = "form-control";
input.value = parent[key] || "";
input.readOnly = true;
input.placeholder = schema.placeholder || "Click to pick...";
input.style.cursor = "pointer";
input.style.backgroundColor = "#fff";
input.dataset.iconPickerValueMode = "icon-name";
input.dataset.iconPickerPreviewPrefix = "fa-solid";
input.addEventListener("click", function () {
openIconPickerForInput(input);
});
searchInput.addEventListener("input", function () {
renderOptions(searchInput.value);
input.addEventListener("input", function () {
parent[key] = input.value;
});
group.appendChild(input);
wrapper.addEventListener("focusout", function () {
window.setTimeout(function () {
if (!wrapper.contains(document.activeElement)) {
setOpen(false);
}
}, 0);
const button = document.createElement("button");
button.type = "button";
button.className = "btn btn-outline-secondary";
button.innerHTML = '<i class="fas fa-icons me-1"></i>Pick Icon';
button.addEventListener("click", function () {
openIconPickerForInput(input);
});
group.appendChild(button);
searchInput.value = selected;
updateTrigger(selected);
renderOptions(selected);
col.appendChild(wrapper);
col.appendChild(group);
syncIconPickerPreview(input);
appendHelp(col, schema, parent[key]);
}
function openIconPickerForInput(input) {
window.__cmsIconPickerActiveInput = input;
patchIconPickerForIconNames();
window.IconPicker?.open(input);
}
function patchIconPickerForIconNames() {
if (!window.IconPicker || window.IconPicker.__cmsIconNamePatched) {
return;
}
const originalPick = typeof window.IconPicker.pick === "function"
? window.IconPicker.pick.bind(window.IconPicker)
: null;
if (!originalPick) {
return;
}
const patchedPick = function (value) {
originalPick(value);
const activeInput = window.__cmsIconPickerActiveInput;
if (!activeInput) {
return;
}
if (activeInput.dataset.iconPickerValueMode === "icon-name") {
activeInput.dataset.iconPickerPreviewPrefix = extractIconStyle(
activeInput.value || value,
) || determineIconPreviewPrefix(activeInput);
activeInput.value = extractIconName(activeInput.value || value);
syncIconPickerPreview(activeInput);
activeInput.dispatchEvent(new Event("input", { bubbles: true }));
activeInput.dispatchEvent(new Event("change", { bubbles: true }));
}
window.__cmsIconPickerActiveInput = null;
};
window.IconPicker.pick = patchedPick;
window.IconPickerPick = patchedPick;
window.IconPicker.__cmsIconNamePatched = true;
}
function syncIconPickerPreview(input) {
if (!input) return;
const previewCell = input.closest(".input-group")?.querySelector(".icon-preview-cell");
if (!previewCell) return;
const previewClass = resolveIconPreviewClass(input);
previewCell.innerHTML = previewClass ? `<i class="${escapeHtml(previewClass)}"></i>` : "";
if (input.dataset.iconPickerValueMode === "icon-name") {
loadIconStyleLookup().then(function () {
const nextPrefix = determineIconPreviewPrefix(input);
if (nextPrefix !== input.dataset.iconPickerPreviewPrefix) {
input.dataset.iconPickerPreviewPrefix = nextPrefix;
const nextPreviewClass = resolveIconPreviewClass(input);
previewCell.innerHTML = nextPreviewClass
? `<i class="${escapeHtml(nextPreviewClass)}"></i>`
: "";
}
});
}
}
function resolveIconPreviewClass(input) {
const normalizedValue = String(input?.value || "").trim();
if (!normalizedValue) {
return "";
}
if (input?.dataset.iconPickerValueMode === "icon-name") {
const prefix = determineIconPreviewPrefix(input);
return `${prefix} ${normalizedValue}`.trim();
}
return normalizedValue;
}
function extractIconName(value) {
return String(value || "")
.trim()
.split(/\s+/)
.find(
(token) =>
/^fa-[a-z0-9-]+$/i.test(token) && !/^fa-(solid|regular|brands)$/i.test(token),
) || "";
}
function extractIconStyle(value) {
return String(value || "")
.trim()
.split(/\s+/)
.find((token) => /^fa-(solid|regular|brands)$/i.test(token)) || "";
}
function determineIconPreviewPrefix(input) {
const explicitPrefix = String(input?.dataset.iconPickerPreviewPrefix || "").trim();
if (explicitPrefix && explicitPrefix !== "fa-solid") {
return explicitPrefix;
}
const iconName = extractIconName(input?.value || "");
const knownStyles = window.__cmsIconStyleLookup?.[iconName] || [];
if (knownStyles.includes("fa-brands")) return "fa-brands";
if (knownStyles.includes("fa-regular")) return "fa-regular";
if (knownStyles.includes("fa-solid")) return "fa-solid";
return explicitPrefix || "fa-solid";
}
function loadIconStyleLookup() {
if (window.__cmsIconStyleLookupPromise) {
return window.__cmsIconStyleLookupPromise;
}
window.__cmsIconStyleLookupPromise = fetch("/js/fa-icons.json")
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
})
.then((json) => {
const lookup = {};
Object.entries(json || {}).forEach(([name, meta]) => {
lookup[`fa-${name}`] = (meta.styles || [])
.filter((style) => ["solid", "regular", "brands"].includes(style))
.map((style) => `fa-${style}`);
});
window.__cmsIconStyleLookup = lookup;
return lookup;
})
.catch(() => {
window.__cmsIconStyleLookup = window.__cmsIconStyleLookup || {};
return window.__cmsIconStyleLookup;
});
return window.__cmsIconStyleLookupPromise;
}
function renderCheckbox(schema, container, parent, key) {
if (typeof parent[key] !== "boolean") {
parent[key] = Boolean(parent[key]);
@@ -1394,33 +1457,6 @@
return schema.options || [];
}
function collectIcons(schema) {
if (!schema) return [];
if (schema.type === "icon") return schema.options || [];
if (schema.type === "object") return (schema.fields || []).flatMap(collectIcons);
if (schema.type === "array") return collectIcons(schema.itemSchema);
if (schema.type === "variant") {
return Object.values(schema.variants || {}).flatMap((variant) =>
collectIcons(variant.schema),
);
}
return [];
}
function ensureIconDatalist(options) {
const existing = document.getElementById("cms-icon-options");
if (existing) existing.remove();
const dataList = document.createElement("datalist");
dataList.id = "cms-icon-options";
options.forEach((option) => {
const item = document.createElement("option");
item.value = option;
dataList.appendChild(item);
});
document.body.appendChild(dataList);
}
function applyAutoSequenceToArray(schema, targetArray) {
if (!schema || !Array.isArray(targetArray) || schema.itemSchema.type !== "object") {
return;
+175 -139
View File
@@ -12,15 +12,6 @@
}
const state = JSON.parse(JSON.stringify(initialData));
const iconOptions = Array.from(
new Set(
(config.tabs || [])
.flatMap((tab) => collectIcons(tab.schema))
.filter(Boolean),
),
);
ensureIconDatalist(iconOptions);
renderAllSections();
document.querySelectorAll("[data-tab-key]").forEach((tabTrigger) => {
@@ -531,119 +522,191 @@
}
function renderIconField(schema, col, parent, key) {
const selected = parent[key] || "";
const wrapper = document.createElement("div");
wrapper.className = "cms-icon-combobox";
const trigger = document.createElement("button");
trigger.type = "button";
trigger.className = "cms-icon-dropdown-trigger";
const group = document.createElement("div");
group.className = "input-group";
const triggerValue = document.createElement("div");
triggerValue.className = "cms-icon-dropdown-value";
const preview = document.createElement("span");
preview.className = "input-group-text icon-preview-cell";
preview.style.minWidth = "38px";
group.appendChild(preview);
const preview = document.createElement("div");
preview.className = "cms-icon-preview";
triggerValue.appendChild(preview);
const triggerText = document.createElement("div");
triggerText.className = "cms-icon-dropdown-text";
triggerValue.appendChild(triggerText);
trigger.appendChild(triggerValue);
const caret = document.createElement("i");
caret.className = "fas fa-chevron-down cms-icon-dropdown-caret";
trigger.appendChild(caret);
wrapper.appendChild(trigger);
const panel = document.createElement("div");
panel.className = "cms-icon-dropdown-panel d-none";
const searchInput = document.createElement("input");
searchInput.type = "text";
searchInput.className = "form-control";
searchInput.placeholder = "Search icon name";
panel.appendChild(searchInput);
const optionsWrap = document.createElement("div");
optionsWrap.className = "cms-icon-options";
panel.appendChild(optionsWrap);
const empty = document.createElement("div");
empty.className = "cms-icon-option-empty d-none";
empty.textContent = "No matching icons";
panel.appendChild(empty);
wrapper.appendChild(panel);
const setOpen = function (isOpen) {
wrapper.classList.toggle("is-open", isOpen);
panel.classList.toggle("d-none", !isOpen);
if (isOpen) {
searchInput.focus();
searchInput.select();
}
};
const updateTrigger = function (value) {
preview.innerHTML = value
? `<i class="fa-solid ${escapeHtml(value)}"></i>`
: '<span class="text-muted small">--</span>';
triggerText.innerHTML = value
? `<strong>${escapeHtml(value)}</strong><span class="text-muted small">Selected icon</span>`
: '<strong>Select icon</strong><span class="text-muted small">No icon selected</span>';
};
const renderOptions = function (searchTerm = "") {
const normalized = String(searchTerm || "").trim().toLowerCase();
const filteredOptions = (schema.options || []).filter((option) =>
option.toLowerCase().includes(normalized),
);
optionsWrap.innerHTML = "";
empty.classList.toggle("d-none", filteredOptions.length > 0);
filteredOptions.forEach((option) => {
const button = document.createElement("button");
button.type = "button";
button.className = `cms-icon-option ${parent[key] === option ? "is-active" : ""}`;
button.innerHTML = `<span class="cms-icon-option-main"><i class="fa-solid ${escapeHtml(option)}"></i><span>${escapeHtml(option)}</span></span>${parent[key] === option ? '<i class="fas fa-check small"></i>' : ""}`;
button.addEventListener("click", function () {
parent[key] = option;
searchInput.value = option;
updateTrigger(option);
renderOptions(option);
setOpen(false);
});
optionsWrap.appendChild(button);
});
};
trigger.addEventListener("click", function () {
const nextOpen = panel.classList.contains("d-none");
setOpen(nextOpen);
if (nextOpen) {
renderOptions(searchInput.value || parent[key] || "");
}
const input = document.createElement("input");
input.type = "text";
input.className = "form-control";
input.value = parent[key] || "";
input.readOnly = true;
input.placeholder = schema.placeholder || "Click to pick...";
input.style.cursor = "pointer";
input.style.backgroundColor = "#fff";
input.dataset.iconPickerValueMode = "icon-name";
input.dataset.iconPickerPreviewPrefix = "fa-solid";
input.addEventListener("click", function () {
openIconPickerForInput(input);
});
searchInput.addEventListener("input", function () {
renderOptions(searchInput.value);
input.addEventListener("input", function () {
parent[key] = input.value;
});
group.appendChild(input);
wrapper.addEventListener("focusout", function () {
window.setTimeout(function () {
if (!wrapper.contains(document.activeElement)) {
setOpen(false);
}
}, 0);
const button = document.createElement("button");
button.type = "button";
button.className = "btn btn-outline-secondary";
button.innerHTML = '<i class="fas fa-icons me-1"></i>Pick Icon';
button.addEventListener("click", function () {
openIconPickerForInput(input);
});
group.appendChild(button);
searchInput.value = selected;
updateTrigger(selected);
renderOptions(selected);
col.appendChild(wrapper);
col.appendChild(group);
syncIconPickerPreview(input);
appendHelp(col, schema, parent[key]);
}
function openIconPickerForInput(input) {
window.__cmsIconPickerActiveInput = input;
patchIconPickerForIconNames();
window.IconPicker?.open(input);
}
function patchIconPickerForIconNames() {
if (!window.IconPicker || window.IconPicker.__cmsIconNamePatched) {
return;
}
const originalPick = typeof window.IconPicker.pick === "function"
? window.IconPicker.pick.bind(window.IconPicker)
: null;
if (!originalPick) {
return;
}
const patchedPick = function (value) {
originalPick(value);
const activeInput = window.__cmsIconPickerActiveInput;
if (!activeInput) {
return;
}
if (activeInput.dataset.iconPickerValueMode === "icon-name") {
activeInput.dataset.iconPickerPreviewPrefix = extractIconStyle(
activeInput.value || value,
) || determineIconPreviewPrefix(activeInput);
activeInput.value = extractIconName(activeInput.value || value);
syncIconPickerPreview(activeInput);
activeInput.dispatchEvent(new Event("input", { bubbles: true }));
activeInput.dispatchEvent(new Event("change", { bubbles: true }));
}
window.__cmsIconPickerActiveInput = null;
};
window.IconPicker.pick = patchedPick;
window.IconPickerPick = patchedPick;
window.IconPicker.__cmsIconNamePatched = true;
}
function syncIconPickerPreview(input) {
if (!input) return;
const previewCell = input.closest(".input-group")?.querySelector(".icon-preview-cell");
if (!previewCell) return;
const previewClass = resolveIconPreviewClass(input);
previewCell.innerHTML = previewClass ? `<i class="${escapeHtml(previewClass)}"></i>` : "";
if (input.dataset.iconPickerValueMode === "icon-name") {
loadIconStyleLookup().then(function () {
const nextPrefix = determineIconPreviewPrefix(input);
if (nextPrefix !== input.dataset.iconPickerPreviewPrefix) {
input.dataset.iconPickerPreviewPrefix = nextPrefix;
const nextPreviewClass = resolveIconPreviewClass(input);
previewCell.innerHTML = nextPreviewClass
? `<i class="${escapeHtml(nextPreviewClass)}"></i>`
: "";
}
});
}
}
function resolveIconPreviewClass(input) {
const normalizedValue = String(input?.value || "").trim();
if (!normalizedValue) {
return "";
}
if (input?.dataset.iconPickerValueMode === "icon-name") {
const prefix = determineIconPreviewPrefix(input);
return `${prefix} ${normalizedValue}`.trim();
}
return normalizedValue;
}
function extractIconName(value) {
return String(value || "")
.trim()
.split(/\s+/)
.find(
(token) =>
/^fa-[a-z0-9-]+$/i.test(token) && !/^fa-(solid|regular|brands)$/i.test(token),
) || "";
}
function extractIconStyle(value) {
return String(value || "")
.trim()
.split(/\s+/)
.find((token) => /^fa-(solid|regular|brands)$/i.test(token)) || "";
}
function determineIconPreviewPrefix(input) {
const explicitPrefix = String(input?.dataset.iconPickerPreviewPrefix || "").trim();
if (explicitPrefix && explicitPrefix !== "fa-solid") {
return explicitPrefix;
}
const iconName = extractIconName(input?.value || "");
const knownStyles = window.__cmsIconStyleLookup?.[iconName] || [];
if (knownStyles.includes("fa-brands")) return "fa-brands";
if (knownStyles.includes("fa-regular")) return "fa-regular";
if (knownStyles.includes("fa-solid")) return "fa-solid";
return explicitPrefix || "fa-solid";
}
function loadIconStyleLookup() {
if (window.__cmsIconStyleLookupPromise) {
return window.__cmsIconStyleLookupPromise;
}
window.__cmsIconStyleLookupPromise = fetch("/js/fa-icons.json")
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
})
.then((json) => {
const lookup = {};
Object.entries(json || {}).forEach(([name, meta]) => {
lookup[`fa-${name}`] = (meta.styles || [])
.filter((style) => ["solid", "regular", "brands"].includes(style))
.map((style) => `fa-${style}`);
});
window.__cmsIconStyleLookup = lookup;
return lookup;
})
.catch(() => {
window.__cmsIconStyleLookup = window.__cmsIconStyleLookup || {};
return window.__cmsIconStyleLookup;
});
return window.__cmsIconStyleLookupPromise;
}
function renderCheckbox(schema, container, parent, key) {
if (typeof parent[key] !== "boolean") {
parent[key] = Boolean(parent[key]);
@@ -821,33 +884,6 @@
return schema.options || [];
}
function collectIcons(schema) {
if (!schema) return [];
if (schema.type === "icon") return schema.options || [];
if (schema.type === "object") return (schema.fields || []).flatMap(collectIcons);
if (schema.type === "array") return collectIcons(schema.itemSchema);
if (schema.type === "variant") {
return Object.values(schema.variants || {}).flatMap((variant) =>
collectIcons(variant.schema),
);
}
return [];
}
function ensureIconDatalist(options) {
const existing = document.getElementById("cms-icon-options");
if (existing) existing.remove();
const dataList = document.createElement("datalist");
dataList.id = "cms-icon-options";
options.forEach((option) => {
const item = document.createElement("option");
item.value = option;
dataList.appendChild(item);
});
document.body.appendChild(dataList);
}
function applyAutoSequenceToArray(schema, targetArray) {
if (!schema || !Array.isArray(targetArray) || schema.itemSchema.type !== "object") {
return;
+175 -139
View File
@@ -12,15 +12,6 @@
}
const state = JSON.parse(JSON.stringify(initialData));
const iconOptions = Array.from(
new Set(
(config.tabs || [])
.flatMap((tab) => collectIcons(tab.schema))
.filter(Boolean),
),
);
ensureIconDatalist(iconOptions);
renderAllSections();
document.querySelectorAll("[data-tab-key]").forEach((tabTrigger) => {
@@ -550,119 +541,191 @@
}
function renderIconField(schema, col, parent, key) {
const selected = parent[key] || "";
const wrapper = document.createElement("div");
wrapper.className = "cms-icon-combobox";
const trigger = document.createElement("button");
trigger.type = "button";
trigger.className = "cms-icon-dropdown-trigger";
const group = document.createElement("div");
group.className = "input-group";
const triggerValue = document.createElement("div");
triggerValue.className = "cms-icon-dropdown-value";
const preview = document.createElement("span");
preview.className = "input-group-text icon-preview-cell";
preview.style.minWidth = "38px";
group.appendChild(preview);
const preview = document.createElement("div");
preview.className = "cms-icon-preview";
triggerValue.appendChild(preview);
const triggerText = document.createElement("div");
triggerText.className = "cms-icon-dropdown-text";
triggerValue.appendChild(triggerText);
trigger.appendChild(triggerValue);
const caret = document.createElement("i");
caret.className = "fas fa-chevron-down cms-icon-dropdown-caret";
trigger.appendChild(caret);
wrapper.appendChild(trigger);
const panel = document.createElement("div");
panel.className = "cms-icon-dropdown-panel d-none";
const searchInput = document.createElement("input");
searchInput.type = "text";
searchInput.className = "form-control";
searchInput.placeholder = "Search icon name";
panel.appendChild(searchInput);
const optionsWrap = document.createElement("div");
optionsWrap.className = "cms-icon-options";
panel.appendChild(optionsWrap);
const empty = document.createElement("div");
empty.className = "cms-icon-option-empty d-none";
empty.textContent = "No matching icons";
panel.appendChild(empty);
wrapper.appendChild(panel);
const setOpen = function (isOpen) {
wrapper.classList.toggle("is-open", isOpen);
panel.classList.toggle("d-none", !isOpen);
if (isOpen) {
searchInput.focus();
searchInput.select();
}
};
const updateTrigger = function (value) {
preview.innerHTML = value
? `<i class="fa-solid ${escapeHtml(value)}"></i>`
: '<span class="text-muted small">--</span>';
triggerText.innerHTML = value
? `<strong>${escapeHtml(value)}</strong><span class="text-muted small">Selected icon</span>`
: '<strong>Select icon</strong><span class="text-muted small">No icon selected</span>';
};
const renderOptions = function (searchTerm = "") {
const normalized = String(searchTerm || "").trim().toLowerCase();
const filteredOptions = (schema.options || []).filter((option) =>
option.toLowerCase().includes(normalized),
);
optionsWrap.innerHTML = "";
empty.classList.toggle("d-none", filteredOptions.length > 0);
filteredOptions.forEach((option) => {
const button = document.createElement("button");
button.type = "button";
button.className = `cms-icon-option ${parent[key] === option ? "is-active" : ""}`;
button.innerHTML = `<span class="cms-icon-option-main"><i class="fa-solid ${escapeHtml(option)}"></i><span>${escapeHtml(option)}</span></span>${parent[key] === option ? '<i class="fas fa-check small"></i>' : ""}`;
button.addEventListener("click", function () {
parent[key] = option;
searchInput.value = option;
updateTrigger(option);
renderOptions(option);
setOpen(false);
});
optionsWrap.appendChild(button);
});
};
trigger.addEventListener("click", function () {
const nextOpen = panel.classList.contains("d-none");
setOpen(nextOpen);
if (nextOpen) {
renderOptions(searchInput.value || parent[key] || "");
}
const input = document.createElement("input");
input.type = "text";
input.className = "form-control";
input.value = parent[key] || "";
input.readOnly = true;
input.placeholder = schema.placeholder || "Click to pick...";
input.style.cursor = "pointer";
input.style.backgroundColor = "#fff";
input.dataset.iconPickerValueMode = "icon-name";
input.dataset.iconPickerPreviewPrefix = "fa-solid";
input.addEventListener("click", function () {
openIconPickerForInput(input);
});
searchInput.addEventListener("input", function () {
renderOptions(searchInput.value);
input.addEventListener("input", function () {
parent[key] = input.value;
});
group.appendChild(input);
wrapper.addEventListener("focusout", function () {
window.setTimeout(function () {
if (!wrapper.contains(document.activeElement)) {
setOpen(false);
}
}, 0);
const button = document.createElement("button");
button.type = "button";
button.className = "btn btn-outline-secondary";
button.innerHTML = '<i class="fas fa-icons me-1"></i>Pick Icon';
button.addEventListener("click", function () {
openIconPickerForInput(input);
});
group.appendChild(button);
searchInput.value = selected;
updateTrigger(selected);
renderOptions(selected);
col.appendChild(wrapper);
col.appendChild(group);
syncIconPickerPreview(input);
appendHelp(col, schema, parent[key]);
}
function openIconPickerForInput(input) {
window.__cmsIconPickerActiveInput = input;
patchIconPickerForIconNames();
window.IconPicker?.open(input);
}
function patchIconPickerForIconNames() {
if (!window.IconPicker || window.IconPicker.__cmsIconNamePatched) {
return;
}
const originalPick = typeof window.IconPicker.pick === "function"
? window.IconPicker.pick.bind(window.IconPicker)
: null;
if (!originalPick) {
return;
}
const patchedPick = function (value) {
originalPick(value);
const activeInput = window.__cmsIconPickerActiveInput;
if (!activeInput) {
return;
}
if (activeInput.dataset.iconPickerValueMode === "icon-name") {
activeInput.dataset.iconPickerPreviewPrefix = extractIconStyle(
activeInput.value || value,
) || determineIconPreviewPrefix(activeInput);
activeInput.value = extractIconName(activeInput.value || value);
syncIconPickerPreview(activeInput);
activeInput.dispatchEvent(new Event("input", { bubbles: true }));
activeInput.dispatchEvent(new Event("change", { bubbles: true }));
}
window.__cmsIconPickerActiveInput = null;
};
window.IconPicker.pick = patchedPick;
window.IconPickerPick = patchedPick;
window.IconPicker.__cmsIconNamePatched = true;
}
function syncIconPickerPreview(input) {
if (!input) return;
const previewCell = input.closest(".input-group")?.querySelector(".icon-preview-cell");
if (!previewCell) return;
const previewClass = resolveIconPreviewClass(input);
previewCell.innerHTML = previewClass ? `<i class="${escapeHtml(previewClass)}"></i>` : "";
if (input.dataset.iconPickerValueMode === "icon-name") {
loadIconStyleLookup().then(function () {
const nextPrefix = determineIconPreviewPrefix(input);
if (nextPrefix !== input.dataset.iconPickerPreviewPrefix) {
input.dataset.iconPickerPreviewPrefix = nextPrefix;
const nextPreviewClass = resolveIconPreviewClass(input);
previewCell.innerHTML = nextPreviewClass
? `<i class="${escapeHtml(nextPreviewClass)}"></i>`
: "";
}
});
}
}
function resolveIconPreviewClass(input) {
const normalizedValue = String(input?.value || "").trim();
if (!normalizedValue) {
return "";
}
if (input?.dataset.iconPickerValueMode === "icon-name") {
const prefix = determineIconPreviewPrefix(input);
return `${prefix} ${normalizedValue}`.trim();
}
return normalizedValue;
}
function extractIconName(value) {
return String(value || "")
.trim()
.split(/\s+/)
.find(
(token) =>
/^fa-[a-z0-9-]+$/i.test(token) && !/^fa-(solid|regular|brands)$/i.test(token),
) || "";
}
function extractIconStyle(value) {
return String(value || "")
.trim()
.split(/\s+/)
.find((token) => /^fa-(solid|regular|brands)$/i.test(token)) || "";
}
function determineIconPreviewPrefix(input) {
const explicitPrefix = String(input?.dataset.iconPickerPreviewPrefix || "").trim();
if (explicitPrefix && explicitPrefix !== "fa-solid") {
return explicitPrefix;
}
const iconName = extractIconName(input?.value || "");
const knownStyles = window.__cmsIconStyleLookup?.[iconName] || [];
if (knownStyles.includes("fa-brands")) return "fa-brands";
if (knownStyles.includes("fa-regular")) return "fa-regular";
if (knownStyles.includes("fa-solid")) return "fa-solid";
return explicitPrefix || "fa-solid";
}
function loadIconStyleLookup() {
if (window.__cmsIconStyleLookupPromise) {
return window.__cmsIconStyleLookupPromise;
}
window.__cmsIconStyleLookupPromise = fetch("/js/fa-icons.json")
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
})
.then((json) => {
const lookup = {};
Object.entries(json || {}).forEach(([name, meta]) => {
lookup[`fa-${name}`] = (meta.styles || [])
.filter((style) => ["solid", "regular", "brands"].includes(style))
.map((style) => `fa-${style}`);
});
window.__cmsIconStyleLookup = lookup;
return lookup;
})
.catch(() => {
window.__cmsIconStyleLookup = window.__cmsIconStyleLookup || {};
return window.__cmsIconStyleLookup;
});
return window.__cmsIconStyleLookupPromise;
}
function renderCheckbox(schema, container, parent, key) {
if (typeof parent[key] !== "boolean") {
parent[key] = Boolean(parent[key]);
@@ -910,33 +973,6 @@
return schema.options || [];
}
function collectIcons(schema) {
if (!schema) return [];
if (schema.type === "icon") return schema.options || [];
if (schema.type === "object") return (schema.fields || []).flatMap(collectIcons);
if (schema.type === "array") return collectIcons(schema.itemSchema);
if (schema.type === "variant") {
return Object.values(schema.variants || {}).flatMap((variant) =>
collectIcons(variant.schema),
);
}
return [];
}
function ensureIconDatalist(options) {
const existing = document.getElementById("cms-icon-options");
if (existing) existing.remove();
const dataList = document.createElement("datalist");
dataList.id = "cms-icon-options";
options.forEach((option) => {
const item = document.createElement("option");
item.value = option;
dataList.appendChild(item);
});
document.body.appendChild(dataList);
}
function applyAutoSequenceToArray(schema, targetArray) {
if (!schema || !Array.isArray(targetArray) || schema.itemSchema.type !== "object") {
return;