@@ -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
- ? `
`
- : '
';
- 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),
- );
-
- 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 = `
-
-
- ${escapeHtml(option)}
-
- ${option === hiddenInput.value ? '
' : ""}
- `;
- 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);
- }
+ input.addEventListener("click", function () {
+ openSharedIconPicker(input, syncIconValue);
});
- 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);
+ button.addEventListener("click", function () {
+ 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 ? `
` : "";
+
+ 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
+ ? `
`
+ : "";
+ }
+ });
+ }
+
+ 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 ? `
${escapeHtml(block.title)}
` : ""}
+ ${block.title ? `
${escapeHtml(block.title)}
` : ""}
${block.html || ""}
`;
node.appendChild(callout);
diff --git a/views/admin/accreditation/partials/editor-script.ejs b/views/admin/accreditation/partials/editor-script.ejs
index f7b66fa..f088024 100644
--- a/views/admin/accreditation/partials/editor-script.ejs
+++ b/views/admin/accreditation/partials/editor-script.ejs
@@ -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
- ? `
`
- : '
--';
- triggerText.innerHTML = value
- ? `
${escapeHtml(value)}Selected icon`
- : '
Select iconNo icon selected';
- };
-
- 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 = `
${escapeHtml(option)}${parent[key] === option ? '
' : ""}`;
- 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 = '
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 ? `
` : "";
+
+ 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
+ ? `
`
+ : "";
+ }
+ });
+ }
+ }
+
+ 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;
diff --git a/views/admin/admissions/calculator-option.ejs b/views/admin/admissions/calculator-option.ejs
index 51f7c0d..5839e97 100644
--- a/views/admin/admissions/calculator-option.ejs
+++ b/views/admin/admissions/calculator-option.ejs
@@ -87,20 +87,23 @@
-