style(cms): improve editor ui and implement collapsible item cards

Enhance the CMS administration interface by introducing a more refined design system for content editors. This update replaces generic Bootstrap utility classes with dedicated CMS styling to improve visual hierarchy and usability.
Key changes:
- Introduce `cms-editor-group` and `cms-item-card` classes for consistent grouping and item styling.
- Implement collapsible functionality for item cards to manage long lists of content more effectively.
- Standardize editor buttons, including new styles for drag handles, remove buttons, and add buttons.
- Update the fixed bottom action bar with improved backdrop filters, shadows, and spacing.
- Refactor editor scripts across Accreditation, Admissions, History, Partnerships, and Policies to utilize the new UI components and collapse logic.
- Add comprehensive CSS to `main.ejs` to support the new CMS design tokens and layout behaviors.
This commit is contained in:
Tống Thành Đạt
2026-04-20 20:10:27 +07:00
parent 7df3a6f6bf
commit 5615c494cf
10 changed files with 387 additions and 137 deletions
@@ -111,26 +111,17 @@
const col = createCol(schema.colClass || "col-12");
const card = document.createElement("div");
card.className = "border rounded-3 bg-light-subtle p-3";
card.className = "cms-editor-group";
const header = document.createElement("div");
header.className = "d-flex justify-content-between align-items-center mb-3 gap-3";
header.className = "mb-3";
header.innerHTML = `
<div>
<label class="form-label fw-semibold mb-1">${escapeHtml(schema.label)}</label>
${schema.helpText ? `<div class="form-text mt-0">${escapeHtml(schema.helpText)}</div>` : ""}
</div>
<button type="button" class="btn btn-outline-primary btn-sm">
<i class="fas fa-plus me-1"></i>${escapeHtml(schema.addLabel || `Add ${schema.itemLabel || "Item"}`)}
</button>
`;
header.querySelector("button").addEventListener("click", function () {
parent[key].push(createDefaultValue(schema.itemSchema));
applyAutoSequenceToArray(schema, parent[key]);
renderSection(tabKey);
});
card.appendChild(header);
if (parent[key].length === 0) {
@@ -145,11 +136,11 @@
parent[key].forEach((item, index) => {
const itemCard = document.createElement("div");
itemCard.className = "card shadow-sm border-0 mb-3";
itemCard.className = "card cms-item-card mb-3";
itemCard.dataset.index = String(index);
const itemHeader = document.createElement("div");
itemHeader.className = "bg-white border-bottom px-3 py-3 d-flex justify-content-between align-items-center gap-2 flex-wrap";
itemHeader.className = "card-header d-flex justify-content-between align-items-center gap-2 flex-wrap";
const title = getArrayItemTitle(schema, item, index);
const subtitle = getArrayItemSubtitle(schema, item);
@@ -157,7 +148,7 @@
<div class="d-flex align-items-center gap-2 flex-grow-1">
${
schema.sortable
? '<button type="button" class="btn btn-light btn-sm border drag-handle" title="Drag to reorder"><i class="fas fa-grip-vertical"></i></button>'
? '<button type="button" class="drag-handle" title="Drag to reorder"><i class="fas fa-grip-vertical"></i></button>'
: ""
}
<div>
@@ -166,13 +157,22 @@
</div>
</div>
<div class="d-flex align-items-center gap-2 flex-wrap">
<button type="button" class="cms-collapse-toggle" data-toggle-item="true" title="Collapse section">
<i class="fas fa-chevron-down"></i>
</button>
${renderItemActions(schema.itemActions, item)}
<button type="button" class="btn btn-outline-danger btn-sm" data-remove-item="true">
<i class="fas fa-trash-alt me-1"></i>Remove
<button type="button" class="cms-remove-button" data-remove-item="true" title="Remove item">
<i class="fas fa-trash-alt"></i>
</button>
</div>
`;
itemHeader
.querySelector('[data-toggle-item="true"]')
.addEventListener("click", function () {
itemCard.classList.toggle("is-collapsed");
});
itemHeader
.querySelector('[data-remove-item="true"]')
.addEventListener("click", function () {
@@ -224,6 +224,17 @@
list.appendChild(itemCard);
});
const addButton = document.createElement("button");
addButton.type = "button";
addButton.className = "cms-add-button mt-2";
addButton.innerHTML = `<i class="fas fa-plus me-2"></i>${escapeHtml(schema.addLabel || `Add ${schema.itemLabel || "Item"}`)}`;
addButton.addEventListener("click", function () {
parent[key].push(createDefaultValue(schema.itemSchema));
applyAutoSequenceToArray(schema, parent[key]);
renderSection(tabKey);
});
card.appendChild(addButton);
if (schema.sortable && window.Sortable) {
window.Sortable.create(list, {
animation: 150,
@@ -598,7 +609,7 @@
function appendHelp(col, schema, value, extraHint) {
const wrapper = document.createElement("div");
wrapper.className = "d-flex justify-content-between gap-3";
wrapper.className = "field-meta-row";
const help = document.createElement("div");
help.className = "form-text";
@@ -608,7 +619,7 @@
let counter = null;
if (schema.maxLength) {
counter = document.createElement("div");
counter.className = "form-text text-end ms-auto";
counter.className = "field-char-count";
updateCounter(counter, String(value || "").length, schema.maxLength);
wrapper.appendChild(counter);
}
@@ -111,26 +111,17 @@
const col = createCol(schema.colClass || "col-12");
const card = document.createElement("div");
card.className = "border rounded-3 bg-light-subtle p-3";
card.className = "cms-editor-group";
const header = document.createElement("div");
header.className = "d-flex justify-content-between align-items-center mb-3 gap-3";
header.className = "mb-3";
header.innerHTML = `
<div>
<label class="form-label fw-semibold mb-1">${escapeHtml(schema.label)}</label>
${schema.helpText ? `<div class="form-text mt-0">${escapeHtml(schema.helpText)}</div>` : ""}
</div>
<button type="button" class="btn btn-outline-primary btn-sm">
<i class="fas fa-plus me-1"></i>${escapeHtml(schema.addLabel || `Add ${schema.itemLabel || "Item"}`)}
</button>
`;
header.querySelector("button").addEventListener("click", function () {
parent[key].push(createDefaultValue(schema.itemSchema));
applyAutoSequenceToArray(schema, parent[key]);
renderSection(tabKey);
});
card.appendChild(header);
if (parent[key].length === 0) {
@@ -145,11 +136,11 @@
parent[key].forEach((item, index) => {
const itemCard = document.createElement("div");
itemCard.className = "card shadow-sm border-0 mb-3";
itemCard.className = "card cms-item-card mb-3";
itemCard.dataset.index = String(index);
const itemHeader = document.createElement("div");
itemHeader.className = "bg-white border-bottom px-3 py-3 d-flex justify-content-between align-items-center gap-2 flex-wrap";
itemHeader.className = "card-header d-flex justify-content-between align-items-center gap-2 flex-wrap";
const title = getArrayItemTitle(schema, item, index);
const subtitle = getArrayItemSubtitle(schema, item);
@@ -157,7 +148,7 @@
<div class="d-flex align-items-center gap-2 flex-grow-1">
${
schema.sortable
? '<button type="button" class="btn btn-light btn-sm border drag-handle" title="Drag to reorder"><i class="fas fa-grip-vertical"></i></button>'
? '<button type="button" class="drag-handle" title="Drag to reorder"><i class="fas fa-grip-vertical"></i></button>'
: ""
}
<div>
@@ -166,13 +157,22 @@
</div>
</div>
<div class="d-flex align-items-center gap-2 flex-wrap">
<button type="button" class="cms-collapse-toggle" data-toggle-item="true" title="Collapse section">
<i class="fas fa-chevron-down"></i>
</button>
${renderItemActions(schema.itemActions, item)}
<button type="button" class="btn btn-outline-danger btn-sm" data-remove-item="true">
<i class="fas fa-trash-alt me-1"></i>Remove
<button type="button" class="cms-remove-button" data-remove-item="true" title="Remove item">
<i class="fas fa-trash-alt"></i>
</button>
</div>
`;
itemHeader
.querySelector('[data-toggle-item="true"]')
.addEventListener("click", function () {
itemCard.classList.toggle("is-collapsed");
});
itemHeader
.querySelector('[data-remove-item="true"]')
.addEventListener("click", function () {
@@ -224,6 +224,17 @@
list.appendChild(itemCard);
});
const addButton = document.createElement("button");
addButton.type = "button";
addButton.className = "cms-add-button mt-2";
addButton.innerHTML = `<i class="fas fa-plus me-2"></i>${escapeHtml(schema.addLabel || `Add ${schema.itemLabel || "Item"}`)}`;
addButton.addEventListener("click", function () {
parent[key].push(createDefaultValue(schema.itemSchema));
applyAutoSequenceToArray(schema, parent[key]);
renderSection(tabKey);
});
card.appendChild(addButton);
if (schema.sortable && window.Sortable) {
window.Sortable.create(list, {
animation: 150,
@@ -598,7 +609,7 @@
function appendHelp(col, schema, value, extraHint) {
const wrapper = document.createElement("div");
wrapper.className = "d-flex justify-content-between gap-3";
wrapper.className = "field-meta-row";
const help = document.createElement("div");
help.className = "form-text";
@@ -608,7 +619,7 @@
let counter = null;
if (schema.maxLength) {
counter = document.createElement("div");
counter.className = "form-text text-end ms-auto";
counter.className = "field-char-count";
updateCounter(counter, String(value || "").length, schema.maxLength);
wrapper.appendChild(counter);
}
+29 -18
View File
@@ -111,26 +111,17 @@
const col = createCol(schema.colClass || "col-12");
const card = document.createElement("div");
card.className = "border rounded-3 bg-light-subtle p-3";
card.className = "cms-editor-group";
const header = document.createElement("div");
header.className = "d-flex justify-content-between align-items-center mb-3 gap-3";
header.className = "mb-3";
header.innerHTML = `
<div>
<label class="form-label fw-semibold mb-1">${escapeHtml(schema.label)}</label>
${schema.helpText ? `<div class="form-text mt-0">${escapeHtml(schema.helpText)}</div>` : ""}
</div>
<button type="button" class="btn btn-outline-primary btn-sm">
<i class="fas fa-plus me-1"></i>${escapeHtml(schema.addLabel || `Add ${schema.itemLabel || "Item"}`)}
</button>
`;
header.querySelector("button").addEventListener("click", function () {
parent[key].push(createDefaultValue(schema.itemSchema));
applyAutoSequenceToArray(schema, parent[key]);
renderSection(tabKey);
});
card.appendChild(header);
if (parent[key].length === 0) {
@@ -145,11 +136,11 @@
parent[key].forEach((item, index) => {
const itemCard = document.createElement("div");
itemCard.className = "card shadow-sm border-0 mb-3";
itemCard.className = "card cms-item-card mb-3";
itemCard.dataset.index = String(index);
const itemHeader = document.createElement("div");
itemHeader.className = "bg-white border-bottom px-3 py-3 d-flex justify-content-between align-items-center gap-2 flex-wrap";
itemHeader.className = "card-header d-flex justify-content-between align-items-center gap-2 flex-wrap";
const title = getArrayItemTitle(schema, item, index);
const subtitle = getArrayItemSubtitle(schema, item);
@@ -157,7 +148,7 @@
<div class="d-flex align-items-center gap-2 flex-grow-1">
${
schema.sortable
? '<button type="button" class="btn btn-light btn-sm border drag-handle" title="Drag to reorder"><i class="fas fa-grip-vertical"></i></button>'
? '<button type="button" class="drag-handle" title="Drag to reorder"><i class="fas fa-grip-vertical"></i></button>'
: ""
}
<div>
@@ -166,13 +157,22 @@
</div>
</div>
<div class="d-flex align-items-center gap-2 flex-wrap">
<button type="button" class="cms-collapse-toggle" data-toggle-item="true" title="Collapse section">
<i class="fas fa-chevron-down"></i>
</button>
${renderItemActions(schema.itemActions, item)}
<button type="button" class="btn btn-outline-danger btn-sm" data-remove-item="true">
<i class="fas fa-trash-alt me-1"></i>Remove
<button type="button" class="cms-remove-button" data-remove-item="true" title="Remove item">
<i class="fas fa-trash-alt"></i>
</button>
</div>
`;
itemHeader
.querySelector('[data-toggle-item="true"]')
.addEventListener("click", function () {
itemCard.classList.toggle("is-collapsed");
});
itemHeader
.querySelector('[data-remove-item="true"]')
.addEventListener("click", function () {
@@ -224,6 +224,17 @@
list.appendChild(itemCard);
});
const addButton = document.createElement("button");
addButton.type = "button";
addButton.className = "cms-add-button mt-2";
addButton.innerHTML = `<i class="fas fa-plus me-2"></i>${escapeHtml(schema.addLabel || `Add ${schema.itemLabel || "Item"}`)}`;
addButton.addEventListener("click", function () {
parent[key].push(createDefaultValue(schema.itemSchema));
applyAutoSequenceToArray(schema, parent[key]);
renderSection(tabKey);
});
card.appendChild(addButton);
if (schema.sortable && window.Sortable) {
window.Sortable.create(list, {
animation: 150,
@@ -598,7 +609,7 @@
function appendHelp(col, schema, value, extraHint) {
const wrapper = document.createElement("div");
wrapper.className = "d-flex justify-content-between gap-3";
wrapper.className = "field-meta-row";
const help = document.createElement("div");
help.className = "form-text";
@@ -608,7 +619,7 @@
let counter = null;
if (schema.maxLength) {
counter = document.createElement("div");
counter.className = "form-text text-end ms-auto";
counter.className = "field-char-count";
updateCounter(counter, String(value || "").length, schema.maxLength);
wrapper.appendChild(counter);
}
@@ -19,30 +19,30 @@
</div>
</div>
<div class="border rounded-3 bg-light-subtle p-3 mb-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<div class="cms-editor-group mb-4">
<div class="mb-3">
<div>
<label class="form-label fw-semibold mb-1">Category tabs</label>
<div class="form-text mt-0">The frontend shows up to 3 direct tabs. Additional tabs move into a More dropdown.</div>
</div>
<button type="button" class="btn btn-outline-primary btn-sm" id="addDirectoryTabBtn">
<i class="fas fa-plus me-1"></i>Add Tab
</button>
</div>
<div id="directoryTabsList"></div>
<button type="button" class="cms-add-button mt-3" id="addDirectoryTabBtn">
<i class="fas fa-plus me-2"></i>Add Tab
</button>
</div>
<div class="border rounded-3 bg-light-subtle p-3">
<div class="d-flex justify-content-between align-items-center mb-3">
<div class="cms-editor-group">
<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>
<button type="button" class="btn btn-outline-primary btn-sm" id="addPartnerBtn">
<i class="fas fa-plus me-1"></i>Add Partner
</button>
</div>
<div id="partnersList"></div>
<button type="button" class="cms-add-button mt-3" id="addPartnerBtn">
<i class="fas fa-plus me-2"></i>Add Partner
</button>
</div>
</div>
</div>
@@ -140,6 +140,9 @@
renderDirectoryTabs();
renderPartners();
});
node.querySelector("[data-toggle-item]")?.addEventListener("click", function () {
node.classList.toggle("is-collapsed");
});
directoryTabsList.appendChild(node);
});
@@ -204,6 +207,9 @@
state.directory.partners.splice(index, 1);
renderPartners();
});
node.querySelector("[data-toggle-item]")?.addEventListener("click", function () {
node.classList.toggle("is-collapsed");
});
partnersList.appendChild(node);
});
@@ -277,6 +283,9 @@
field.options.push("");
renderInquiryFields();
});
node.querySelector("[data-toggle-item]")?.addEventListener("click", function () {
node.classList.toggle("is-collapsed");
});
function toggleOptions() {
optionsWrap.classList.toggle("d-none", typeSelect.value !== "select");
@@ -15,17 +15,17 @@
</div>
</div>
<div class="border rounded-3 bg-light-subtle p-3">
<div class="d-flex justify-content-between align-items-center mb-3">
<div class="cms-editor-group">
<div class="mb-3">
<div>
<label class="form-label fw-semibold mb-1">Form fields</label>
<div class="form-text mt-0">Manage labels, placeholders, type, width, and dropdown options.</div>
</div>
<button type="button" class="btn btn-outline-primary btn-sm" id="addInquiryFieldBtn">
<i class="fas fa-plus me-1"></i>Add Field
</button>
</div>
<div id="inquiryFieldsList"></div>
<button type="button" class="cms-add-button mt-3" id="addInquiryFieldBtn">
<i class="fas fa-plus me-2"></i>Add Field
</button>
</div>
</div>
</div>
+40 -25
View File
@@ -1,15 +1,20 @@
<template id="directoryTabTemplate">
<div class="card shadow-sm border-0 mb-3" data-item="directory-tab">
<div class="bg-white border-bottom px-3 py-3 d-flex justify-content-between align-items-center gap-2">
<div class="card cms-item-card mb-3" data-item="directory-tab">
<div class="card-header d-flex justify-content-between align-items-center gap-2">
<div class="d-flex align-items-center gap-2">
<button type="button" class="btn btn-light btn-sm border drag-handle" title="Drag to reorder">
<button type="button" class="drag-handle" title="Drag to reorder">
<i class="fas fa-grip-vertical"></i>
</button>
<div class="fw-semibold">Category Tab</div>
</div>
<button type="button" class="btn btn-outline-danger btn-sm" data-remove-item>
<i class="fas fa-trash-alt me-1"></i>Remove
</button>
<div class="d-flex align-items-center gap-2">
<button type="button" class="cms-collapse-toggle" data-toggle-item title="Collapse item">
<i class="fas fa-chevron-down"></i>
</button>
<button type="button" class="cms-remove-button" data-remove-item title="Remove item">
<i class="fas fa-trash-alt"></i>
</button>
</div>
</div>
<div class="card-body">
<label class="form-label fw-semibold">Tab label</label>
@@ -19,10 +24,10 @@
</template>
<template id="partnerTemplate">
<div class="card shadow-sm border-0 mb-3" data-item="partner">
<div class="bg-white border-bottom px-3 py-3 d-flex justify-content-between align-items-center gap-2 flex-wrap">
<div class="card cms-item-card mb-3" data-item="partner">
<div class="card-header d-flex justify-content-between align-items-center gap-2 flex-wrap">
<div class="d-flex align-items-center gap-2">
<button type="button" class="btn btn-light btn-sm border drag-handle" title="Drag to reorder">
<button type="button" class="drag-handle" title="Drag to reorder">
<i class="fas fa-grip-vertical"></i>
</button>
<div>
@@ -30,9 +35,14 @@
<div class="small text-muted" data-subtitle></div>
</div>
</div>
<button type="button" class="btn btn-outline-danger btn-sm" data-remove-item>
<i class="fas fa-trash-alt me-1"></i>Remove
</button>
<div class="d-flex align-items-center gap-2">
<button type="button" class="cms-collapse-toggle" data-toggle-item title="Collapse item">
<i class="fas fa-chevron-down"></i>
</button>
<button type="button" class="cms-remove-button" data-remove-item title="Remove item">
<i class="fas fa-trash-alt"></i>
</button>
</div>
</div>
<div class="card-body">
<div class="row g-3">
@@ -87,10 +97,10 @@
</template>
<template id="inquiryFieldTemplate">
<div class="card shadow-sm border-0 mb-3" data-item="inquiry-field">
<div class="bg-white border-bottom px-3 py-3 d-flex justify-content-between align-items-center gap-2 flex-wrap">
<div class="card cms-item-card mb-3" data-item="inquiry-field">
<div class="card-header d-flex justify-content-between align-items-center gap-2 flex-wrap">
<div class="d-flex align-items-center gap-2">
<button type="button" class="btn btn-light btn-sm border drag-handle" title="Drag to reorder">
<button type="button" class="drag-handle" title="Drag to reorder">
<i class="fas fa-grip-vertical"></i>
</button>
<div>
@@ -98,9 +108,14 @@
<div class="small text-muted" data-subtitle></div>
</div>
</div>
<button type="button" class="btn btn-outline-danger btn-sm" data-remove-item>
<i class="fas fa-trash-alt me-1"></i>Remove
</button>
<div class="d-flex align-items-center gap-2">
<button type="button" class="cms-collapse-toggle" data-toggle-item title="Collapse item">
<i class="fas fa-chevron-down"></i>
</button>
<button type="button" class="cms-remove-button" data-remove-item title="Remove item">
<i class="fas fa-trash-alt"></i>
</button>
</div>
</div>
<div class="card-body">
<div class="row g-3">
@@ -138,15 +153,15 @@
</div>
</div>
<div class="col-12" data-options-wrap>
<div class="border rounded-3 p-3 bg-light">
<div class="d-flex justify-content-between align-items-center mb-2">
<div class="cms-editor-group">
<div class="mb-2">
<label class="form-label fw-semibold mb-0">Dropdown options</label>
<button type="button" class="btn btn-outline-primary btn-sm" data-add-option>
<i class="fas fa-plus me-1"></i>Add Option
</button>
</div>
<div class="form-text mb-2">Only used when the field type is Dropdown.</div>
<div data-options-list></div>
<button type="button" class="cms-add-button mt-3" data-add-option>
<i class="fas fa-plus me-2"></i>Add Option
</button>
</div>
</div>
</div>
@@ -156,11 +171,11 @@
<template id="inquiryOptionTemplate">
<div class="input-group mb-2" data-item="inquiry-option">
<button type="button" class="btn btn-light border drag-handle" title="Drag to reorder">
<button type="button" class="drag-handle" title="Drag to reorder">
<i class="fas fa-grip-vertical"></i>
</button>
<input class="form-control" data-option-value maxlength="50" placeholder="Option label">
<button type="button" class="btn btn-outline-danger" data-remove-item>
<button type="button" class="cms-remove-button" data-remove-item title="Remove option">
<i class="fas fa-trash-alt"></i>
</button>
</div>
+29 -18
View File
@@ -111,26 +111,17 @@
const col = createCol(schema.colClass || "col-12");
const card = document.createElement("div");
card.className = "border rounded-3 bg-light-subtle p-3";
card.className = "cms-editor-group";
const header = document.createElement("div");
header.className = "d-flex justify-content-between align-items-center mb-3 gap-3";
header.className = "mb-3";
header.innerHTML = `
<div>
<label class="form-label fw-semibold mb-1">${escapeHtml(schema.label)}</label>
${schema.helpText ? `<div class="form-text mt-0">${escapeHtml(schema.helpText)}</div>` : ""}
</div>
<button type="button" class="btn btn-outline-primary btn-sm">
<i class="fas fa-plus me-1"></i>${escapeHtml(schema.addLabel || `Add ${schema.itemLabel || "Item"}`)}
</button>
`;
header.querySelector("button").addEventListener("click", function () {
parent[key].push(createDefaultValue(schema.itemSchema));
applyAutoSequenceToArray(schema, parent[key]);
renderSection(tabKey);
});
card.appendChild(header);
if (parent[key].length === 0) {
@@ -145,11 +136,11 @@
parent[key].forEach((item, index) => {
const itemCard = document.createElement("div");
itemCard.className = "card shadow-sm border-0 mb-3";
itemCard.className = "card cms-item-card mb-3";
itemCard.dataset.index = String(index);
const itemHeader = document.createElement("div");
itemHeader.className = "bg-white border-bottom px-3 py-3 d-flex justify-content-between align-items-center gap-2 flex-wrap";
itemHeader.className = "card-header d-flex justify-content-between align-items-center gap-2 flex-wrap";
const title = getArrayItemTitle(schema, item, index);
const subtitle = getArrayItemSubtitle(schema, item);
@@ -157,7 +148,7 @@
<div class="d-flex align-items-center gap-2 flex-grow-1">
${
schema.sortable
? '<button type="button" class="btn btn-light btn-sm border drag-handle" title="Drag to reorder"><i class="fas fa-grip-vertical"></i></button>'
? '<button type="button" class="drag-handle" title="Drag to reorder"><i class="fas fa-grip-vertical"></i></button>'
: ""
}
<div>
@@ -166,13 +157,22 @@
</div>
</div>
<div class="d-flex align-items-center gap-2 flex-wrap">
<button type="button" class="cms-collapse-toggle" data-toggle-item="true" title="Collapse section">
<i class="fas fa-chevron-down"></i>
</button>
${renderItemActions(schema.itemActions, item)}
<button type="button" class="btn btn-outline-danger btn-sm" data-remove-item="true">
<i class="fas fa-trash-alt me-1"></i>Remove
<button type="button" class="cms-remove-button" data-remove-item="true" title="Remove item">
<i class="fas fa-trash-alt"></i>
</button>
</div>
`;
itemHeader
.querySelector('[data-toggle-item="true"]')
.addEventListener("click", function () {
itemCard.classList.toggle("is-collapsed");
});
itemHeader
.querySelector('[data-remove-item="true"]')
.addEventListener("click", function () {
@@ -224,6 +224,17 @@
list.appendChild(itemCard);
});
const addButton = document.createElement("button");
addButton.type = "button";
addButton.className = "cms-add-button mt-2";
addButton.innerHTML = `<i class="fas fa-plus me-2"></i>${escapeHtml(schema.addLabel || `Add ${schema.itemLabel || "Item"}`)}`;
addButton.addEventListener("click", function () {
parent[key].push(createDefaultValue(schema.itemSchema));
applyAutoSequenceToArray(schema, parent[key]);
renderSection(tabKey);
});
card.appendChild(addButton);
if (schema.sortable && window.Sortable) {
window.Sortable.create(list, {
animation: 150,
@@ -598,7 +609,7 @@
function appendHelp(col, schema, value, extraHint) {
const wrapper = document.createElement("div");
wrapper.className = "d-flex justify-content-between gap-3";
wrapper.className = "field-meta-row";
const help = document.createElement("div");
help.className = "form-text";
@@ -608,7 +619,7 @@
let counter = null;
if (schema.maxLength) {
counter = document.createElement("div");
counter.className = "form-text text-end ms-auto";
counter.className = "field-char-count";
updateCounter(counter, String(value || "").length, schema.maxLength);
wrapper.appendChild(counter);
}
@@ -111,26 +111,17 @@
const col = createCol(schema.colClass || "col-12");
const card = document.createElement("div");
card.className = "border rounded-3 bg-light-subtle p-3";
card.className = "cms-editor-group";
const header = document.createElement("div");
header.className = "d-flex justify-content-between align-items-center mb-3 gap-3";
header.className = "mb-3";
header.innerHTML = `
<div>
<div class="mb-2">
<label class="form-label fw-semibold mb-1">${escapeHtml(schema.label)}</label>
${schema.helpText ? `<div class="form-text mt-0">${escapeHtml(schema.helpText)}</div>` : ""}
</div>
<button type="button" class="btn btn-outline-primary btn-sm">
<i class="fas fa-plus me-1"></i>${escapeHtml(schema.addLabel || `Add ${schema.itemLabel || "Item"}`)}
</button>
`;
header.querySelector("button").addEventListener("click", function () {
parent[key].push(createDefaultValue(schema.itemSchema));
applyAutoSequenceToArray(schema, parent[key]);
renderSection(tabKey);
});
card.appendChild(header);
if (parent[key].length === 0) {
@@ -145,11 +136,11 @@
parent[key].forEach((item, index) => {
const itemCard = document.createElement("div");
itemCard.className = "card shadow-sm border-0 mb-3";
itemCard.className = "card cms-item-card mb-3";
itemCard.dataset.index = String(index);
const itemHeader = document.createElement("div");
itemHeader.className = "bg-white border-bottom px-3 py-3 d-flex justify-content-between align-items-center gap-2 flex-wrap";
itemHeader.className = "card-header d-flex justify-content-between align-items-center gap-2 flex-wrap";
const title = getArrayItemTitle(schema, item, index);
const subtitle = getArrayItemSubtitle(schema, item);
@@ -157,7 +148,7 @@
<div class="d-flex align-items-center gap-2 flex-grow-1">
${
schema.sortable
? '<button type="button" class="btn btn-light btn-sm border drag-handle" title="Drag to reorder"><i class="fas fa-grip-vertical"></i></button>'
? '<button type="button" class="drag-handle" title="Drag to reorder"><i class="fas fa-grip-vertical"></i></button>'
: ""
}
<div>
@@ -166,13 +157,22 @@
</div>
</div>
<div class="d-flex align-items-center gap-2 flex-wrap">
<button type="button" class="cms-collapse-toggle" data-toggle-item="true" title="Collapse section">
<i class="fas fa-chevron-down"></i>
</button>
${renderItemActions(schema.itemActions, item)}
<button type="button" class="btn btn-outline-danger btn-sm" data-remove-item="true">
<i class="fas fa-trash-alt me-1"></i>Remove
<button type="button" class="cms-remove-button" data-remove-item="true" title="Remove item">
<i class="fas fa-trash-alt"></i>
</button>
</div>
`;
itemHeader
.querySelector('[data-toggle-item="true"]')
.addEventListener("click", function () {
itemCard.classList.toggle("is-collapsed");
});
itemHeader
.querySelector('[data-remove-item="true"]')
.addEventListener("click", function () {
@@ -224,6 +224,17 @@
list.appendChild(itemCard);
});
const addButton = document.createElement("button");
addButton.type = "button";
addButton.className = "cms-add-button mt-2";
addButton.innerHTML = `<i class="fas fa-plus me-2"></i>${escapeHtml(schema.addLabel || `Add ${schema.itemLabel || "Item"}`)}`;
addButton.addEventListener("click", function () {
parent[key].push(createDefaultValue(schema.itemSchema));
applyAutoSequenceToArray(schema, parent[key]);
renderSection(tabKey);
});
card.appendChild(addButton);
if (schema.sortable && window.Sortable) {
window.Sortable.create(list, {
animation: 150,
@@ -598,7 +609,7 @@
function appendHelp(col, schema, value, extraHint) {
const wrapper = document.createElement("div");
wrapper.className = "d-flex justify-content-between gap-3";
wrapper.className = "field-meta-row";
const help = document.createElement("div");
help.className = "form-text";
@@ -608,7 +619,7 @@
let counter = null;
if (schema.maxLength) {
counter = document.createElement("div");
counter.className = "form-text text-end ms-auto";
counter.className = "field-char-count";
updateCounter(counter, String(value || "").length, schema.maxLength);
wrapper.appendChild(counter);
}