forked from UKSOURCE/cms.lams
feat(cms): enhance content editors with dynamic UI configs and validation
Implement a more flexible, configuration-driven approach for CMS editors across accreditation, admissions, and partnerships modules. - Move UI labels, help texts, and field limits from hardcoded views to `editorUi` configurations in config files. - Add server-side and client-side validation to prevent duplicate category tabs in accreditation and partnerships editors. - Refactor partnership and admission views to dynamically render tabs and fields based on the provided configuration. - Update field length constraints and default values across multiple content editors to better align with frontend requirements. - Improve the admissions calculator editor with dynamic field configurations and default value fallbacks.
This commit is contained in:
@@ -24,69 +24,69 @@
|
||||
<div class="card-body p-4">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label for="label" class="form-label fw-semibold">Option label</label>
|
||||
<input id="label" name="label" type="text" class="form-control" maxlength="12" value="<%= option.label %>" required />
|
||||
<label for="label" class="form-label fw-semibold"><%= fieldConfig.label?.label || "Option label" %></label>
|
||||
<input id="label" name="label" type="text" class="form-control" maxlength="<%= fieldLimits.label %>" value="<%= option.label %>" required />
|
||||
<div class="field-meta-row">
|
||||
<div class="form-text">This label appears in the calculator option switcher.</div>
|
||||
<div class="field-char-count" data-counter-for="label">0/12</div>
|
||||
<div class="form-text"><%= fieldConfig.label?.helpText || "" %></div>
|
||||
<div class="field-char-count" data-counter-for="label">0/<%= fieldLimits.label %></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label for="paceLabel" class="form-label fw-semibold">Pace label</label>
|
||||
<input id="paceLabel" name="paceLabel" type="text" class="form-control" maxlength="30" value="<%= option.paceLabel %>" required />
|
||||
<label for="paceLabel" class="form-label fw-semibold"><%= fieldConfig.paceLabel?.label || "Pace label" %></label>
|
||||
<input id="paceLabel" name="paceLabel" type="text" class="form-control" maxlength="<%= fieldLimits.paceLabel %>" value="<%= option.paceLabel %>" required />
|
||||
<div class="field-meta-row">
|
||||
<div class="form-text">This appears above the pace slider.</div>
|
||||
<div class="field-char-count" data-counter-for="paceLabel">0/30</div>
|
||||
<div class="form-text"><%= fieldConfig.paceLabel?.helpText || "" %></div>
|
||||
<div class="field-char-count" data-counter-for="paceLabel">0/<%= fieldLimits.paceLabel %></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label for="minPaceLabel" class="form-label fw-semibold">Minimum pace label</label>
|
||||
<input id="minPaceLabel" name="minPaceLabel" type="text" class="form-control" maxlength="20" value="<%= option.minPaceLabel %>" required />
|
||||
<label for="minPaceLabel" class="form-label fw-semibold"><%= fieldConfig.minPaceLabel?.label || "Minimum pace label" %></label>
|
||||
<input id="minPaceLabel" name="minPaceLabel" type="text" class="form-control" maxlength="<%= fieldLimits.minPaceLabel %>" value="<%= option.minPaceLabel %>" required />
|
||||
<div class="field-meta-row">
|
||||
<div class="form-text">This appears on the left side of the pace slider.</div>
|
||||
<div class="field-char-count" data-counter-for="minPaceLabel">0/20</div>
|
||||
<div class="form-text"><%= fieldConfig.minPaceLabel?.helpText || "" %></div>
|
||||
<div class="field-char-count" data-counter-for="minPaceLabel">0/<%= fieldLimits.minPaceLabel %></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label for="maxPaceLabel" class="form-label fw-semibold">Maximum pace label</label>
|
||||
<input id="maxPaceLabel" name="maxPaceLabel" type="text" class="form-control" maxlength="20" value="<%= option.maxPaceLabel %>" required />
|
||||
<label for="maxPaceLabel" class="form-label fw-semibold"><%= fieldConfig.maxPaceLabel?.label || "Maximum pace label" %></label>
|
||||
<input id="maxPaceLabel" name="maxPaceLabel" type="text" class="form-control" maxlength="<%= fieldLimits.maxPaceLabel %>" value="<%= option.maxPaceLabel %>" required />
|
||||
<div class="field-meta-row">
|
||||
<div class="form-text">This appears on the right side of the pace slider.</div>
|
||||
<div class="field-char-count" data-counter-for="maxPaceLabel">0/20</div>
|
||||
<div class="form-text"><%= fieldConfig.maxPaceLabel?.helpText || "" %></div>
|
||||
<div class="field-char-count" data-counter-for="maxPaceLabel">0/<%= fieldLimits.maxPaceLabel %></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label for="resultLabel" class="form-label fw-semibold">Result label</label>
|
||||
<input id="resultLabel" name="resultLabel" type="text" class="form-control" maxlength="40" value="<%= option.resultLabel %>" required />
|
||||
<label for="resultLabel" class="form-label fw-semibold"><%= fieldConfig.resultLabel?.label || "Result label" %></label>
|
||||
<input id="resultLabel" name="resultLabel" type="text" class="form-control" maxlength="<%= fieldLimits.resultLabel %>" value="<%= option.resultLabel %>" required />
|
||||
<div class="field-meta-row">
|
||||
<div class="form-text">This label appears above the calculated amount.</div>
|
||||
<div class="field-char-count" data-counter-for="resultLabel">0/40</div>
|
||||
<div class="form-text"><%= fieldConfig.resultLabel?.helpText || "" %></div>
|
||||
<div class="field-char-count" data-counter-for="resultLabel">0/<%= fieldLimits.resultLabel %></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label for="monthlyAmount" class="form-label fw-semibold">Monthly amount</label>
|
||||
<label for="monthlyAmount" class="form-label fw-semibold"><%= fieldConfig.monthlyAmount?.label || "Monthly amount" %></label>
|
||||
<input id="monthlyAmount" name="monthlyAmount" type="number" class="form-control" min="1" step="1" inputmode="numeric" value="<%= option.monthlyAmount %>" required />
|
||||
<div class="field-meta-row">
|
||||
<div class="form-text">Enter digits only. The currency symbol is added on the website automatically.</div>
|
||||
<div class="form-text"><%= fieldConfig.monthlyAmount?.helpText || "" %></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label for="monthlySuffix" class="form-label fw-semibold">Monthly suffix</label>
|
||||
<input id="monthlySuffix" name="monthlySuffix" type="text" class="form-control" maxlength="10" value="<%= option.monthlySuffix %>" required />
|
||||
<label for="monthlySuffix" class="form-label fw-semibold"><%= fieldConfig.monthlySuffix?.label || "Monthly suffix" %></label>
|
||||
<input id="monthlySuffix" name="monthlySuffix" type="text" class="form-control" maxlength="<%= fieldLimits.monthlySuffix %>" value="<%= option.monthlySuffix %>" required />
|
||||
<div class="field-meta-row">
|
||||
<div class="form-text">Example: /mo</div>
|
||||
<div class="field-char-count" data-counter-for="monthlySuffix">0/10</div>
|
||||
<div class="form-text"><%= fieldConfig.monthlySuffix?.helpText || "" %></div>
|
||||
<div class="field-char-count" data-counter-for="monthlySuffix">0/<%= fieldLimits.monthlySuffix %></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label fw-semibold">Note icon</label>
|
||||
<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">
|
||||
@@ -103,16 +103,16 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="field-meta-row">
|
||||
<div class="form-text">Choose the icon shown beside the note.</div>
|
||||
<div class="form-text"><%= fieldConfig.noteIcon?.helpText || "" %></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label for="note" class="form-label fw-semibold">Note text</label>
|
||||
<input id="note" name="note" type="text" class="form-control" maxlength="60" value="<%= option.note %>" required />
|
||||
<label for="note" class="form-label fw-semibold"><%= fieldConfig.note?.label || "Note text" %></label>
|
||||
<input id="note" name="note" type="text" class="form-control" maxlength="<%= fieldLimits.note %>" value="<%= option.note %>" required />
|
||||
<div class="field-meta-row">
|
||||
<div class="form-text">This short note appears under the amount.</div>
|
||||
<div class="field-char-count" data-counter-for="note">0/60</div>
|
||||
<div class="form-text"><%= fieldConfig.note?.helpText || "" %></div>
|
||||
<div class="field-char-count" data-counter-for="note">0/<%= fieldLimits.note %></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -137,7 +137,9 @@
|
||||
<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");
|
||||
@@ -159,6 +161,43 @@
|
||||
updateCounter();
|
||||
});
|
||||
|
||||
function normalizeLabel(value) {
|
||||
return String(value || "").trim().toLowerCase();
|
||||
}
|
||||
|
||||
function clearLabelValidation() {
|
||||
labelInput.classList.remove("is-invalid");
|
||||
}
|
||||
|
||||
function validateUniqueLabel() {
|
||||
const normalizedValue = normalizeLabel(labelInput.value);
|
||||
const isDuplicate = normalizedValue
|
||||
? existingOptionLabels.some((label) => normalizeLabel(label) === normalizedValue)
|
||||
: false;
|
||||
|
||||
labelInput.classList.toggle("is-invalid", isDuplicate);
|
||||
return !isDuplicate;
|
||||
}
|
||||
|
||||
labelInput.addEventListener("input", function () {
|
||||
clearLabelValidation();
|
||||
validateUniqueLabel();
|
||||
});
|
||||
|
||||
form.addEventListener("submit", function (event) {
|
||||
if (validateUniqueLabel()) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
showToast(
|
||||
"Duplicate option label",
|
||||
`Option label "${labelInput.value.trim()}" already exists. Please use a unique label before saving.`,
|
||||
"danger",
|
||||
);
|
||||
labelInput.focus();
|
||||
});
|
||||
|
||||
const monthlyAmountInput = document.getElementById("monthlyAmount");
|
||||
monthlyAmountInput.addEventListener("input", function () {
|
||||
this.value = this.value.replace(/[^\d]/g, "");
|
||||
@@ -240,5 +279,28 @@
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
function showToast(title, message, type) {
|
||||
const container =
|
||||
document.querySelector(".toast-container") || createToastContainer();
|
||||
const toast = document.createElement("div");
|
||||
toast.className = `toast align-items-center text-white bg-${type || "info"} border-0`;
|
||||
toast.setAttribute("role", "alert");
|
||||
toast.innerHTML = `<div class="d-flex"><div class="toast-body"><strong>${escapeHtml(
|
||||
title,
|
||||
)}:</strong> ${escapeHtml(message)}</div><button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button></div>`;
|
||||
container.appendChild(toast);
|
||||
new bootstrap.Toast(toast, { autohide: true, delay: 3000 }).show();
|
||||
toast.addEventListener("hidden.bs.toast", function () {
|
||||
toast.remove();
|
||||
});
|
||||
}
|
||||
|
||||
function createToastContainer() {
|
||||
const container = document.createElement("div");
|
||||
container.className = "toast-container position-fixed top-0 end-0 p-3";
|
||||
document.body.appendChild(container);
|
||||
return container;
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user