forked from UKSOURCE/cms.lams
feat(cms): enhance content editors and implement automatic ID generation
Improve the CMS administration interface across multiple pages (Accreditation, Admissions, History, Partnerships, and Policies) with a focus on usability and data integrity. Key changes include: - Implement `ensureUniqueIds` utility to automatically generate and maintain unique slugs for content items, removing the need for manual ID entry in the UI. - Refactor the Admissions calculator to support detailed per-option editing via a new dedicated view and routes. - Replace basic datalists with a custom, searchable icon combobox component for better visual selection. - Update `_renderSingletonPageView` to handle active tab persistence via query parameters. - Streamline editor configurations by removing redundant fields and improving help text. - Enhance the Admissions "Key Dates" editor with a dynamic table interface for managing columns and rows. - Normalize data payloads in controllers to ensure consistent API responses and internal linking.
This commit is contained in:
@@ -524,52 +524,113 @@
|
||||
function renderIconField(schema, col, parent, key) {
|
||||
const selected = parent[key] || "";
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.className = "border rounded-3 bg-white p-3";
|
||||
wrapper.className = "cms-icon-combobox";
|
||||
const trigger = document.createElement("button");
|
||||
trigger.type = "button";
|
||||
trigger.className = "cms-icon-dropdown-trigger";
|
||||
|
||||
const top = document.createElement("div");
|
||||
top.className = "d-flex gap-2 align-items-center mb-3";
|
||||
|
||||
const input = document.createElement("input");
|
||||
input.type = "text";
|
||||
input.className = "form-control";
|
||||
input.value = selected;
|
||||
input.placeholder = "fa-shield-check";
|
||||
input.setAttribute("list", "cms-icon-options");
|
||||
top.appendChild(input);
|
||||
const triggerValue = document.createElement("div");
|
||||
triggerValue.className = "cms-icon-dropdown-value";
|
||||
|
||||
const preview = document.createElement("div");
|
||||
preview.className = "border rounded-2 px-3 d-flex align-items-center justify-content-center";
|
||||
preview.style.width = "52px";
|
||||
preview.innerHTML = selected
|
||||
? `<i class="fa-solid ${escapeHtml(selected)}"></i>`
|
||||
: '<span class="text-muted small">--</span>';
|
||||
top.appendChild(preview);
|
||||
wrapper.appendChild(top);
|
||||
preview.className = "cms-icon-preview";
|
||||
triggerValue.appendChild(preview);
|
||||
|
||||
const grid = document.createElement("div");
|
||||
grid.className = "d-flex flex-wrap gap-2";
|
||||
(schema.options || []).forEach((option) => {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = `btn btn-sm ${selected === option ? "btn-primary" : "btn-outline-secondary"}`;
|
||||
button.innerHTML = `<i class="fa-solid ${escapeHtml(option)} me-1"></i>${escapeHtml(option)}`;
|
||||
button.addEventListener("click", function () {
|
||||
parent[key] = option;
|
||||
input.value = option;
|
||||
preview.innerHTML = `<i class="fa-solid ${escapeHtml(option)}"></i>`;
|
||||
renderAllSections();
|
||||
});
|
||||
grid.appendChild(button);
|
||||
});
|
||||
wrapper.appendChild(grid);
|
||||
const triggerText = document.createElement("div");
|
||||
triggerText.className = "cms-icon-dropdown-text";
|
||||
triggerValue.appendChild(triggerText);
|
||||
trigger.appendChild(triggerValue);
|
||||
|
||||
input.addEventListener("input", function () {
|
||||
parent[key] = input.value;
|
||||
preview.innerHTML = input.value
|
||||
? `<i class="fa-solid ${escapeHtml(input.value)}"></i>`
|
||||
const caret = document.createElement("i");
|
||||
caret.className = "fas fa-chevron-down cms-icon-dropdown-caret";
|
||||
trigger.appendChild(caret);
|
||||
wrapper.appendChild(trigger);
|
||||
|
||||
const panel = document.createElement("div");
|
||||
panel.className = "cms-icon-dropdown-panel d-none";
|
||||
|
||||
const searchInput = document.createElement("input");
|
||||
searchInput.type = "text";
|
||||
searchInput.className = "form-control";
|
||||
searchInput.placeholder = "Search icon name";
|
||||
panel.appendChild(searchInput);
|
||||
|
||||
const optionsWrap = document.createElement("div");
|
||||
optionsWrap.className = "cms-icon-options";
|
||||
panel.appendChild(optionsWrap);
|
||||
|
||||
const empty = document.createElement("div");
|
||||
empty.className = "cms-icon-option-empty d-none";
|
||||
empty.textContent = "No matching icons";
|
||||
panel.appendChild(empty);
|
||||
wrapper.appendChild(panel);
|
||||
|
||||
const setOpen = function (isOpen) {
|
||||
wrapper.classList.toggle("is-open", isOpen);
|
||||
panel.classList.toggle("d-none", !isOpen);
|
||||
if (isOpen) {
|
||||
searchInput.focus();
|
||||
searchInput.select();
|
||||
}
|
||||
};
|
||||
|
||||
const updateTrigger = function (value) {
|
||||
preview.innerHTML = value
|
||||
? `<i class="fa-solid ${escapeHtml(value)}"></i>`
|
||||
: '<span class="text-muted small">--</span>';
|
||||
triggerText.innerHTML = value
|
||||
? `<strong>${escapeHtml(value)}</strong><span class="text-muted small">Selected icon</span>`
|
||||
: '<strong>Select icon</strong><span class="text-muted small">No icon selected</span>';
|
||||
};
|
||||
|
||||
const renderOptions = function (searchTerm = "") {
|
||||
const normalized = String(searchTerm || "").trim().toLowerCase();
|
||||
const filteredOptions = (schema.options || []).filter((option) =>
|
||||
option.toLowerCase().includes(normalized),
|
||||
);
|
||||
|
||||
optionsWrap.innerHTML = "";
|
||||
empty.classList.toggle("d-none", filteredOptions.length > 0);
|
||||
|
||||
filteredOptions.forEach((option) => {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = `cms-icon-option ${parent[key] === option ? "is-active" : ""}`;
|
||||
button.innerHTML = `<span class="cms-icon-option-main"><i class="fa-solid ${escapeHtml(option)}"></i><span>${escapeHtml(option)}</span></span>${parent[key] === option ? '<i class="fas fa-check small"></i>' : ""}`;
|
||||
button.addEventListener("click", function () {
|
||||
parent[key] = option;
|
||||
searchInput.value = option;
|
||||
updateTrigger(option);
|
||||
renderOptions(option);
|
||||
setOpen(false);
|
||||
});
|
||||
optionsWrap.appendChild(button);
|
||||
});
|
||||
};
|
||||
|
||||
trigger.addEventListener("click", function () {
|
||||
const nextOpen = panel.classList.contains("d-none");
|
||||
setOpen(nextOpen);
|
||||
if (nextOpen) {
|
||||
renderOptions(searchInput.value || parent[key] || "");
|
||||
}
|
||||
});
|
||||
|
||||
searchInput.addEventListener("input", function () {
|
||||
renderOptions(searchInput.value);
|
||||
});
|
||||
|
||||
wrapper.addEventListener("focusout", function () {
|
||||
window.setTimeout(function () {
|
||||
if (!wrapper.contains(document.activeElement)) {
|
||||
setOpen(false);
|
||||
}
|
||||
}, 0);
|
||||
});
|
||||
|
||||
searchInput.value = selected;
|
||||
updateTrigger(selected);
|
||||
renderOptions(selected);
|
||||
col.appendChild(wrapper);
|
||||
appendHelp(col, schema, parent[key]);
|
||||
}
|
||||
@@ -883,3 +944,4 @@
|
||||
})();
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user