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:
@@ -5,14 +5,10 @@
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-8">
|
||||
<div class="col-12">
|
||||
<label class="form-label fw-semibold">Section heading</label>
|
||||
<input class="form-control" id="directoryHeading" maxlength="70" value="<%= data.directory?.heading || '' %>">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-semibold">Load more button label</label>
|
||||
<input class="form-control" id="directoryLoadMoreLabel" maxlength="40" value="<%= data.directory?.loadMoreLabel || '' %>">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label fw-semibold">Section description</label>
|
||||
<textarea class="form-control" id="directoryDescription" rows="3" maxlength="180"><%= data.directory?.description || '' %></textarea>
|
||||
@@ -36,7 +32,7 @@
|
||||
<div class="mb-3">
|
||||
<div>
|
||||
<label class="form-label fw-semibold mb-1">Partners</label>
|
||||
<div class="form-text mt-0">Use a short unique partner key so card state stays stable.</div>
|
||||
<div class="form-text mt-0">Each partner card keeps its own open or closed state automatically.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="partnersList"></div>
|
||||
|
||||
@@ -14,6 +14,12 @@
|
||||
}
|
||||
|
||||
const state = JSON.parse(JSON.stringify(initialData));
|
||||
const slugifyValue = (value, fallback) =>
|
||||
String(value || "")
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "") || fallback;
|
||||
const templates = {
|
||||
tab: document.getElementById("directoryTabTemplate"),
|
||||
partner: document.getElementById("partnerTemplate"),
|
||||
@@ -21,8 +27,10 @@
|
||||
inquiryOption: document.getElementById("inquiryOptionTemplate"),
|
||||
};
|
||||
|
||||
ensurePartnershipIds();
|
||||
bindStaticEvents();
|
||||
renderAll();
|
||||
initStaticCounters();
|
||||
|
||||
function bindStaticEvents() {
|
||||
document.querySelectorAll("[data-tab-key]").forEach((tabTrigger) => {
|
||||
@@ -43,7 +51,6 @@
|
||||
|
||||
document.getElementById("addPartnerBtn")?.addEventListener("click", function () {
|
||||
state.directory.partners.push({
|
||||
id: "",
|
||||
name: "",
|
||||
category: "",
|
||||
summary: "",
|
||||
@@ -53,12 +60,12 @@
|
||||
collabType: "",
|
||||
benefits: "",
|
||||
});
|
||||
ensurePartnershipIds();
|
||||
renderPartners();
|
||||
});
|
||||
|
||||
document.getElementById("addInquiryFieldBtn")?.addEventListener("click", function () {
|
||||
state.inquiryForm.fields.push({
|
||||
id: "",
|
||||
label: "",
|
||||
placeholder: "",
|
||||
type: "text",
|
||||
@@ -66,6 +73,7 @@
|
||||
required: true,
|
||||
options: [],
|
||||
});
|
||||
ensurePartnershipIds();
|
||||
renderInquiryFields();
|
||||
});
|
||||
|
||||
@@ -75,6 +83,7 @@
|
||||
|
||||
form.addEventListener("submit", function () {
|
||||
syncStaticFields();
|
||||
ensurePartnershipIds();
|
||||
pageJsonInput.value = JSON.stringify(state);
|
||||
});
|
||||
|
||||
@@ -105,7 +114,6 @@
|
||||
|
||||
state.directory.heading = getValue("directoryHeading");
|
||||
state.directory.description = getValue("directoryDescription");
|
||||
state.directory.loadMoreLabel = getValue("directoryLoadMoreLabel");
|
||||
|
||||
state.cta = {
|
||||
heading: getValue("ctaHeading"),
|
||||
@@ -114,7 +122,34 @@
|
||||
};
|
||||
|
||||
state.inquiryForm.title = getValue("inquiryTitle");
|
||||
state.inquiryForm.submitLabel = getValue("inquirySubmitLabel");
|
||||
}
|
||||
|
||||
function ensurePartnershipIds() {
|
||||
const usedPartnerIds = new Set();
|
||||
state.directory.partners = (state.directory.partners || []).map((partner, index) => {
|
||||
let id = String(partner.id || "").trim() || slugifyValue(partner.name, `partner-${index + 1}`);
|
||||
let suffix = 2;
|
||||
while (usedPartnerIds.has(id)) {
|
||||
id = `${slugifyValue(partner.name, "partner")}-${suffix}`;
|
||||
suffix += 1;
|
||||
}
|
||||
usedPartnerIds.add(id);
|
||||
return { ...partner, id };
|
||||
});
|
||||
|
||||
const usedFieldIds = new Set();
|
||||
state.inquiryForm.fields = (state.inquiryForm.fields || []).map((field, index) => {
|
||||
let id =
|
||||
String(field.id || "").trim() ||
|
||||
slugifyValue(field.label || field.placeholder, `field-${index + 1}`);
|
||||
let suffix = 2;
|
||||
while (usedFieldIds.has(id)) {
|
||||
id = `${slugifyValue(field.label || field.placeholder, "field")}-${suffix}`;
|
||||
suffix += 1;
|
||||
}
|
||||
usedFieldIds.add(id);
|
||||
return { ...field, id };
|
||||
});
|
||||
}
|
||||
|
||||
function renderAll() {
|
||||
@@ -143,6 +178,7 @@
|
||||
node.querySelector("[data-toggle-item]")?.addEventListener("click", function () {
|
||||
node.classList.toggle("is-collapsed");
|
||||
});
|
||||
attachCounters(node);
|
||||
|
||||
directoryTabsList.appendChild(node);
|
||||
});
|
||||
@@ -210,6 +246,7 @@
|
||||
node.querySelector("[data-toggle-item]")?.addEventListener("click", function () {
|
||||
node.classList.toggle("is-collapsed");
|
||||
});
|
||||
attachCounters(node);
|
||||
|
||||
partnersList.appendChild(node);
|
||||
});
|
||||
@@ -304,6 +341,7 @@
|
||||
field.options.splice(optionIndex, 1);
|
||||
renderInquiryFields();
|
||||
});
|
||||
attachCounters(optionNode);
|
||||
optionsList.appendChild(optionNode);
|
||||
});
|
||||
initSortable(optionsList, field.options, renderInquiryFields, '[data-item="inquiry-option"]');
|
||||
@@ -311,6 +349,7 @@
|
||||
|
||||
toggleOptions();
|
||||
renderOptions();
|
||||
attachCounters(node);
|
||||
inquiryFieldsList.appendChild(node);
|
||||
});
|
||||
|
||||
@@ -339,6 +378,74 @@
|
||||
return template.content.firstElementChild.cloneNode(true);
|
||||
}
|
||||
|
||||
function initStaticCounters() {
|
||||
attachCounters(form);
|
||||
}
|
||||
|
||||
function attachCounters(root) {
|
||||
root.querySelectorAll("input[maxlength], textarea[maxlength]").forEach((input, index) => {
|
||||
if (input.dataset.counterReady === "true") {
|
||||
updateInputCounter(input);
|
||||
return;
|
||||
}
|
||||
|
||||
const counterId =
|
||||
input.id ||
|
||||
input.name ||
|
||||
input.dataset.field ||
|
||||
input.dataset.optionValue ||
|
||||
`counter-${index}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
let counter = root.querySelector(`[data-counter-for="${counterId}"]`);
|
||||
|
||||
if (!counter) {
|
||||
counter = document.createElement("div");
|
||||
counter.className = "field-char-count";
|
||||
counter.dataset.counterFor = counterId;
|
||||
|
||||
const next = input.nextElementSibling;
|
||||
if (next && next.classList.contains("form-text")) {
|
||||
let metaRow = next.nextElementSibling;
|
||||
if (!metaRow || !metaRow.classList.contains("field-meta-row")) {
|
||||
metaRow = document.createElement("div");
|
||||
metaRow.className = "field-meta-row";
|
||||
next.insertAdjacentElement("afterend", metaRow);
|
||||
}
|
||||
metaRow.appendChild(counter);
|
||||
} else {
|
||||
const metaRow = document.createElement("div");
|
||||
metaRow.className = "field-meta-row";
|
||||
metaRow.appendChild(counter);
|
||||
input.insertAdjacentElement("afterend", metaRow);
|
||||
}
|
||||
}
|
||||
|
||||
const sync = function () {
|
||||
updateInputCounter(input);
|
||||
};
|
||||
|
||||
input.addEventListener("input", sync);
|
||||
input.addEventListener("change", sync);
|
||||
input.dataset.counterReady = "true";
|
||||
updateInputCounter(input);
|
||||
});
|
||||
}
|
||||
|
||||
function updateInputCounter(input) {
|
||||
const maxLength = Number(input.getAttribute("maxlength"));
|
||||
if (!maxLength) return;
|
||||
|
||||
const counterId =
|
||||
input.id || input.name || input.dataset.field || input.dataset.optionValue;
|
||||
const scope = input.closest("[data-item]") || input.parentElement || form;
|
||||
let counter = scope.querySelector(`[data-counter-for="${counterId}"]`);
|
||||
if (!counter) {
|
||||
counter = form.querySelector(`[data-counter-for="${counterId}"]`);
|
||||
}
|
||||
if (!counter) return;
|
||||
|
||||
counter.textContent = `${String(input.value || "").length}/${maxLength}`;
|
||||
}
|
||||
|
||||
function getValue(id) {
|
||||
return document.getElementById(id)?.value || "";
|
||||
}
|
||||
|
||||
@@ -5,14 +5,10 @@
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-8">
|
||||
<div class="col-12">
|
||||
<label class="form-label fw-semibold">Modal title</label>
|
||||
<input class="form-control" id="inquiryTitle" maxlength="60" value="<%= data.inquiryForm?.title || '' %>">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-semibold">Submit button label</label>
|
||||
<input class="form-control" id="inquirySubmitLabel" maxlength="40" value="<%= data.inquiryForm?.submitLabel || '' %>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cms-editor-group">
|
||||
|
||||
@@ -46,11 +46,6 @@
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-semibold">Partner key</label>
|
||||
<input class="form-control" data-field="id" maxlength="50">
|
||||
<div class="form-text">Use a short unique key.</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-semibold">Partner name</label>
|
||||
<input class="form-control" data-field="name" maxlength="90">
|
||||
@@ -119,10 +114,6 @@
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-semibold">Field key</label>
|
||||
<input class="form-control" data-field="id" maxlength="40">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-semibold">Field label</label>
|
||||
<input class="form-control" data-field="label" maxlength="40">
|
||||
|
||||
Reference in New Issue
Block a user