forked from UKSOURCE/cms.lams
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:
+176
-103
@@ -549,24 +549,24 @@
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label fw-semibold">Icon</label>
|
||||
<div class="icon-combobox" data-role="icon-combobox">
|
||||
<input type="hidden" data-field="icon" value="${escapeAttribute(block.icon || "")}" />
|
||||
<button type="button" class="icon-combobox-trigger">
|
||||
<span class="icon-combobox-value">
|
||||
<span class="icon-combobox-preview">
|
||||
${block.icon ? `<i class="fa-solid ${escapeAttribute(block.icon)}"></i>` : '<i class="fas fa-icons"></i>'}
|
||||
<div class="input-group" data-role="icon-picker-group">
|
||||
<span class="input-group-text icon-preview-cell">
|
||||
${block.icon ? `<i class="${escapeAttribute(resolveCalloutIconPreviewClass(block.icon))}"></i>` : ""}
|
||||
</span>
|
||||
<span class="icon-combobox-text">
|
||||
<strong>${escapeHtml(block.icon || "Select icon")}</strong>
|
||||
</span>
|
||||
</span>
|
||||
<i class="fas fa-chevron-down icon-combobox-caret"></i>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
data-field="icon"
|
||||
value="${escapeAttribute(block.icon || "")}"
|
||||
placeholder="Click to pick..."
|
||||
readonly
|
||||
style="cursor:pointer;background:#fff"
|
||||
data-icon-picker-value-mode="icon-name"
|
||||
data-icon-picker-preview-prefix="${escapeAttribute(resolveCalloutIconPreviewPrefix(block.icon))}"
|
||||
/>
|
||||
<button type="button" class="btn btn-outline-secondary" data-action="pick-icon">
|
||||
<i class="fas fa-icons me-1"></i>Pick Icon
|
||||
</button>
|
||||
<div class="icon-combobox-panel is-hidden">
|
||||
<input type="text" class="form-control icon-combobox-search" placeholder="Search icon name" value="${escapeAttribute(block.icon || "")}" />
|
||||
<div class="icon-combobox-options"></div>
|
||||
<div class="icon-combobox-empty is-hidden">No matching icons. Press Enter to use custom value.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
@@ -583,7 +583,7 @@
|
||||
block.html = value;
|
||||
});
|
||||
|
||||
initializeIconCombobox(body.querySelector('[data-role="icon-combobox"]'), block);
|
||||
initializeIconPicker(body.querySelector('[data-role="icon-picker-group"]'), block);
|
||||
|
||||
body.querySelectorAll("input[data-field], select[data-field], textarea[data-field]").forEach((input) => {
|
||||
const syncField = function () {
|
||||
@@ -661,111 +661,184 @@
|
||||
return editor;
|
||||
}
|
||||
|
||||
function initializeIconCombobox(wrapper, block) {
|
||||
function initializeIconPicker(wrapper, block) {
|
||||
if (!wrapper) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hiddenInput = wrapper.querySelector('input[data-field="icon"]');
|
||||
const trigger = wrapper.querySelector(".icon-combobox-trigger");
|
||||
const panel = wrapper.querySelector(".icon-combobox-panel");
|
||||
const searchInput = wrapper.querySelector(".icon-combobox-search");
|
||||
const optionsContainer = wrapper.querySelector(".icon-combobox-options");
|
||||
const emptyState = wrapper.querySelector(".icon-combobox-empty");
|
||||
const preview = wrapper.querySelector(".icon-combobox-preview");
|
||||
const title = wrapper.querySelector(".icon-combobox-text strong");
|
||||
const card = wrapper.closest(".block-card");
|
||||
const input = wrapper.querySelector('input[data-field="icon"]');
|
||||
const button = wrapper.querySelector('[data-action="pick-icon"]');
|
||||
|
||||
if (!hiddenInput || !trigger || !panel || !searchInput || !optionsContainer || !emptyState || !preview || !title) {
|
||||
if (!input || !button) {
|
||||
return;
|
||||
}
|
||||
|
||||
const setOpen = function (isOpen) {
|
||||
wrapper.classList.toggle("is-open", isOpen);
|
||||
panel.classList.toggle("is-hidden", !isOpen);
|
||||
if (card) {
|
||||
card.classList.toggle("has-floating-ui", isOpen);
|
||||
}
|
||||
if (isOpen) {
|
||||
searchInput.focus();
|
||||
searchInput.select();
|
||||
}
|
||||
};
|
||||
|
||||
const syncIconValue = function (value) {
|
||||
const nextValue = String(value || "").trim();
|
||||
hiddenInput.value = nextValue;
|
||||
searchInput.value = nextValue;
|
||||
input.value = nextValue;
|
||||
input.dataset.iconPickerPreviewPrefix = resolveCalloutIconPreviewPrefix(nextValue);
|
||||
block.icon = nextValue;
|
||||
preview.innerHTML = nextValue
|
||||
? `<i class="fa-solid ${escapeAttribute(nextValue)}"></i>`
|
||||
: '<i class="fas fa-icons"></i>';
|
||||
title.textContent = nextValue || "Select icon";
|
||||
syncCalloutIconPreview(input);
|
||||
renderPreview();
|
||||
renderValidation(validateState());
|
||||
};
|
||||
|
||||
const renderOptions = function (query) {
|
||||
const normalizedQuery = String(query || "").trim().toLowerCase();
|
||||
const filteredOptions = iconOptions.filter((option) =>
|
||||
option.toLowerCase().includes(normalizedQuery),
|
||||
);
|
||||
input.addEventListener("click", function () {
|
||||
openSharedIconPicker(input, syncIconValue);
|
||||
});
|
||||
|
||||
optionsContainer.innerHTML = "";
|
||||
emptyState.classList.toggle("is-hidden", filteredOptions.length > 0);
|
||||
|
||||
filteredOptions.forEach((option) => {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "icon-combobox-option";
|
||||
button.classList.toggle("is-active", option === hiddenInput.value);
|
||||
button.innerHTML = `
|
||||
<span class="icon-combobox-option-main">
|
||||
<i class="fa-solid ${escapeAttribute(option)}"></i>
|
||||
<span>${escapeHtml(option)}</span>
|
||||
</span>
|
||||
${option === hiddenInput.value ? '<i class="fas fa-check small"></i>' : ""}
|
||||
`;
|
||||
button.addEventListener("click", function () {
|
||||
syncIconValue(option);
|
||||
renderOptions(option);
|
||||
setOpen(false);
|
||||
});
|
||||
optionsContainer.appendChild(button);
|
||||
});
|
||||
};
|
||||
|
||||
trigger.addEventListener("click", function () {
|
||||
const nextIsOpen = panel.classList.contains("is-hidden");
|
||||
setOpen(nextIsOpen);
|
||||
if (nextIsOpen) {
|
||||
renderOptions(searchInput.value || hiddenInput.value);
|
||||
}
|
||||
});
|
||||
|
||||
searchInput.addEventListener("input", function () {
|
||||
renderOptions(searchInput.value);
|
||||
});
|
||||
|
||||
searchInput.addEventListener("keydown", function (event) {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
syncIconValue(searchInput.value);
|
||||
renderOptions(searchInput.value);
|
||||
setOpen(false);
|
||||
}
|
||||
});
|
||||
|
||||
wrapper.addEventListener("focusout", function () {
|
||||
window.setTimeout(function () {
|
||||
if (!wrapper.contains(document.activeElement)) {
|
||||
setOpen(false);
|
||||
}
|
||||
}, 0);
|
||||
openSharedIconPicker(input, syncIconValue);
|
||||
});
|
||||
|
||||
syncIconValue(block.icon || "");
|
||||
renderOptions(block.icon || "");
|
||||
}
|
||||
|
||||
function openSharedIconPicker(input, onPicked) {
|
||||
if (!window.IconPicker || typeof window.IconPicker.open !== "function") {
|
||||
return;
|
||||
}
|
||||
|
||||
patchSharedIconPicker();
|
||||
window.__policyBlockIconPickerInput = input;
|
||||
window.__policyBlockIconPickerOnPicked = onPicked;
|
||||
window.IconPicker.open(input);
|
||||
}
|
||||
|
||||
function patchSharedIconPicker() {
|
||||
if (!window.IconPicker || window.IconPicker.__policyBlockPatched) {
|
||||
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.__policyBlockIconPickerInput;
|
||||
const onPicked = window.__policyBlockIconPickerOnPicked;
|
||||
|
||||
if (!activeInput || typeof onPicked !== "function") {
|
||||
return;
|
||||
}
|
||||
|
||||
activeInput.dataset.iconPickerPreviewPrefix = extractIconStyle(value)
|
||||
|| resolveCalloutIconPreviewPrefix(activeInput.value || value);
|
||||
|
||||
onPicked(extractIconName(value));
|
||||
activeInput.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
activeInput.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
|
||||
window.__policyBlockIconPickerInput = null;
|
||||
window.__policyBlockIconPickerOnPicked = null;
|
||||
};
|
||||
|
||||
window.IconPicker.pick = patchedPick;
|
||||
window.IconPickerPick = patchedPick;
|
||||
window.IconPicker.__policyBlockPatched = true;
|
||||
}
|
||||
|
||||
function syncCalloutIconPreview(input) {
|
||||
if (!input) {
|
||||
return;
|
||||
}
|
||||
|
||||
const previewCell = input.closest(".input-group")?.querySelector(".icon-preview-cell");
|
||||
if (!previewCell) {
|
||||
return;
|
||||
}
|
||||
|
||||
const previewClass = resolveCalloutIconPreviewClass(input.value, input);
|
||||
previewCell.innerHTML = previewClass ? `<i class="${escapeAttribute(previewClass)}"></i>` : "";
|
||||
|
||||
loadIconStyleLookup().then(function () {
|
||||
const nextPrefix = resolveCalloutIconPreviewPrefix(input.value, input);
|
||||
if (nextPrefix !== input.dataset.iconPickerPreviewPrefix) {
|
||||
input.dataset.iconPickerPreviewPrefix = nextPrefix;
|
||||
const refreshedPreviewClass = resolveCalloutIconPreviewClass(input.value, input);
|
||||
previewCell.innerHTML = refreshedPreviewClass
|
||||
? `<i class="${escapeAttribute(refreshedPreviewClass)}"></i>`
|
||||
: "";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function resolveCalloutIconPreviewClass(iconName, input) {
|
||||
const normalizedValue = String(iconName || "").trim();
|
||||
if (!normalizedValue) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return `${resolveCalloutIconPreviewPrefix(normalizedValue, input)} ${normalizedValue}`.trim();
|
||||
}
|
||||
|
||||
function resolveCalloutIconPreviewPrefix(iconName, input) {
|
||||
const explicitPrefix = String(input?.dataset?.iconPickerPreviewPrefix || "").trim();
|
||||
if (explicitPrefix && explicitPrefix !== "fa-solid") {
|
||||
return explicitPrefix;
|
||||
}
|
||||
|
||||
const normalizedIconName = extractIconName(iconName);
|
||||
const knownStyles = window.__policyBlockIconStyleLookup?.[normalizedIconName] || [];
|
||||
|
||||
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 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 loadIconStyleLookup() {
|
||||
if (window.__policyBlockIconStyleLookupPromise) {
|
||||
return window.__policyBlockIconStyleLookupPromise;
|
||||
}
|
||||
|
||||
window.__policyBlockIconStyleLookupPromise = 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.__policyBlockIconStyleLookup = lookup;
|
||||
return lookup;
|
||||
})
|
||||
.catch(() => {
|
||||
window.__policyBlockIconStyleLookup = window.__policyBlockIconStyleLookup || {};
|
||||
return window.__policyBlockIconStyleLookup;
|
||||
});
|
||||
|
||||
return window.__policyBlockIconStyleLookupPromise;
|
||||
}
|
||||
|
||||
function bindEditable(element, onChange, options) {
|
||||
@@ -1150,7 +1223,7 @@
|
||||
callout.className = "callout-block";
|
||||
callout.dataset.tone = block.tone || "info";
|
||||
callout.innerHTML = `
|
||||
${block.title ? `<div class="fw-semibold mb-2"><i class="fa-solid ${escapeAttribute(block.icon || "fa-circle-info")} me-2"></i>${escapeHtml(block.title)}</div>` : ""}
|
||||
${block.title ? `<div class="fw-semibold mb-2"><i class="${escapeAttribute(resolveCalloutIconPreviewClass(block.icon || "fa-circle-info"))} me-2"></i>${escapeHtml(block.title)}</div>` : ""}
|
||||
<div>${block.html || ""}</div>
|
||||
`;
|
||||
node.appendChild(callout);
|
||||
|
||||
@@ -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,117 +593,189 @@
|
||||
}
|
||||
|
||||
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 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);
|
||||
});
|
||||
input.addEventListener("input", function () {
|
||||
parent[key] = input.value;
|
||||
});
|
||||
group.appendChild(input);
|
||||
|
||||
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.className = "btn btn-outline-secondary";
|
||||
button.innerHTML = '<i class="fas fa-icons me-1"></i>Pick Icon';
|
||||
button.addEventListener("click", function () {
|
||||
parent[key] = option;
|
||||
searchInput.value = option;
|
||||
updateTrigger(option);
|
||||
renderOptions(option);
|
||||
setOpen(false);
|
||||
});
|
||||
optionsWrap.appendChild(button);
|
||||
openIconPickerForInput(input);
|
||||
});
|
||||
group.appendChild(button);
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
trigger.addEventListener("click", function () {
|
||||
const nextOpen = panel.classList.contains("d-none");
|
||||
setOpen(nextOpen);
|
||||
if (nextOpen) {
|
||||
renderOptions(searchInput.value || parent[key] || "");
|
||||
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>`
|
||||
: "";
|
||||
}
|
||||
});
|
||||
|
||||
searchInput.addEventListener("input", function () {
|
||||
renderOptions(searchInput.value);
|
||||
});
|
||||
|
||||
wrapper.addEventListener("focusout", function () {
|
||||
window.setTimeout(function () {
|
||||
if (!wrapper.contains(document.activeElement)) {
|
||||
setOpen(false);
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
searchInput.value = selected;
|
||||
updateTrigger(selected);
|
||||
renderOptions(selected);
|
||||
col.appendChild(wrapper);
|
||||
appendHelp(col, schema, parent[key]);
|
||||
return window.__cmsIconStyleLookupPromise;
|
||||
}
|
||||
|
||||
function renderCheckbox(schema, container, 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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}, 0);
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
search.value = hiddenInput.value;
|
||||
updateTrigger(hiddenInput.value);
|
||||
renderOptions(hiddenInput.value);
|
||||
return window.__cmsIconStyleLookupPromise;
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value || "")
|
||||
|
||||
@@ -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,117 +1069,189 @@
|
||||
}
|
||||
|
||||
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 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);
|
||||
});
|
||||
input.addEventListener("input", function () {
|
||||
parent[key] = input.value;
|
||||
});
|
||||
group.appendChild(input);
|
||||
|
||||
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.className = "btn btn-outline-secondary";
|
||||
button.innerHTML = '<i class="fas fa-icons me-1"></i>Pick Icon';
|
||||
button.addEventListener("click", function () {
|
||||
parent[key] = option;
|
||||
searchInput.value = option;
|
||||
updateTrigger(option);
|
||||
renderOptions(option);
|
||||
setOpen(false);
|
||||
});
|
||||
optionsWrap.appendChild(button);
|
||||
openIconPickerForInput(input);
|
||||
});
|
||||
group.appendChild(button);
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
trigger.addEventListener("click", function () {
|
||||
const nextOpen = panel.classList.contains("d-none");
|
||||
setOpen(nextOpen);
|
||||
if (nextOpen) {
|
||||
renderOptions(searchInput.value || parent[key] || "");
|
||||
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>`
|
||||
: "";
|
||||
}
|
||||
});
|
||||
|
||||
searchInput.addEventListener("input", function () {
|
||||
renderOptions(searchInput.value);
|
||||
});
|
||||
|
||||
wrapper.addEventListener("focusout", function () {
|
||||
window.setTimeout(function () {
|
||||
if (!wrapper.contains(document.activeElement)) {
|
||||
setOpen(false);
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
searchInput.value = selected;
|
||||
updateTrigger(selected);
|
||||
renderOptions(selected);
|
||||
col.appendChild(wrapper);
|
||||
appendHelp(col, schema, parent[key]);
|
||||
return window.__cmsIconStyleLookupPromise;
|
||||
}
|
||||
|
||||
function renderCheckbox(schema, container, 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;
|
||||
|
||||
@@ -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,117 +522,189 @@
|
||||
}
|
||||
|
||||
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 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);
|
||||
});
|
||||
input.addEventListener("input", function () {
|
||||
parent[key] = input.value;
|
||||
});
|
||||
group.appendChild(input);
|
||||
|
||||
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.className = "btn btn-outline-secondary";
|
||||
button.innerHTML = '<i class="fas fa-icons me-1"></i>Pick Icon';
|
||||
button.addEventListener("click", function () {
|
||||
parent[key] = option;
|
||||
searchInput.value = option;
|
||||
updateTrigger(option);
|
||||
renderOptions(option);
|
||||
setOpen(false);
|
||||
});
|
||||
optionsWrap.appendChild(button);
|
||||
openIconPickerForInput(input);
|
||||
});
|
||||
group.appendChild(button);
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
trigger.addEventListener("click", function () {
|
||||
const nextOpen = panel.classList.contains("d-none");
|
||||
setOpen(nextOpen);
|
||||
if (nextOpen) {
|
||||
renderOptions(searchInput.value || parent[key] || "");
|
||||
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>`
|
||||
: "";
|
||||
}
|
||||
});
|
||||
|
||||
searchInput.addEventListener("input", function () {
|
||||
renderOptions(searchInput.value);
|
||||
});
|
||||
|
||||
wrapper.addEventListener("focusout", function () {
|
||||
window.setTimeout(function () {
|
||||
if (!wrapper.contains(document.activeElement)) {
|
||||
setOpen(false);
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
searchInput.value = selected;
|
||||
updateTrigger(selected);
|
||||
renderOptions(selected);
|
||||
col.appendChild(wrapper);
|
||||
appendHelp(col, schema, parent[key]);
|
||||
return window.__cmsIconStyleLookupPromise;
|
||||
}
|
||||
|
||||
function renderCheckbox(schema, container, 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;
|
||||
|
||||
@@ -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,117 +541,189 @@
|
||||
}
|
||||
|
||||
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 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);
|
||||
});
|
||||
input.addEventListener("input", function () {
|
||||
parent[key] = input.value;
|
||||
});
|
||||
group.appendChild(input);
|
||||
|
||||
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.className = "btn btn-outline-secondary";
|
||||
button.innerHTML = '<i class="fas fa-icons me-1"></i>Pick Icon';
|
||||
button.addEventListener("click", function () {
|
||||
parent[key] = option;
|
||||
searchInput.value = option;
|
||||
updateTrigger(option);
|
||||
renderOptions(option);
|
||||
setOpen(false);
|
||||
});
|
||||
optionsWrap.appendChild(button);
|
||||
openIconPickerForInput(input);
|
||||
});
|
||||
group.appendChild(button);
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
trigger.addEventListener("click", function () {
|
||||
const nextOpen = panel.classList.contains("d-none");
|
||||
setOpen(nextOpen);
|
||||
if (nextOpen) {
|
||||
renderOptions(searchInput.value || parent[key] || "");
|
||||
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>`
|
||||
: "";
|
||||
}
|
||||
});
|
||||
|
||||
searchInput.addEventListener("input", function () {
|
||||
renderOptions(searchInput.value);
|
||||
});
|
||||
|
||||
wrapper.addEventListener("focusout", function () {
|
||||
window.setTimeout(function () {
|
||||
if (!wrapper.contains(document.activeElement)) {
|
||||
setOpen(false);
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
searchInput.value = selected;
|
||||
updateTrigger(selected);
|
||||
renderOptions(selected);
|
||||
col.appendChild(wrapper);
|
||||
appendHelp(col, schema, parent[key]);
|
||||
return window.__cmsIconStyleLookupPromise;
|
||||
}
|
||||
|
||||
function renderCheckbox(schema, container, 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;
|
||||
|
||||
Reference in New Issue
Block a user