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 col = createCol(schema.colClass || "col-12");
const card = document.createElement("div"); 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"); 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 = ` header.innerHTML = `
<div> <div>
<label class="form-label fw-semibold mb-1">${escapeHtml(schema.label)}</label> <label class="form-label fw-semibold mb-1">${escapeHtml(schema.label)}</label>
${schema.helpText ? `<div class="form-text mt-0">${escapeHtml(schema.helpText)}</div>` : ""} ${schema.helpText ? `<div class="form-text mt-0">${escapeHtml(schema.helpText)}</div>` : ""}
</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); card.appendChild(header);
if (parent[key].length === 0) { if (parent[key].length === 0) {
@@ -145,11 +136,11 @@
parent[key].forEach((item, index) => { parent[key].forEach((item, index) => {
const itemCard = document.createElement("div"); 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); itemCard.dataset.index = String(index);
const itemHeader = document.createElement("div"); 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 title = getArrayItemTitle(schema, item, index);
const subtitle = getArrayItemSubtitle(schema, item); const subtitle = getArrayItemSubtitle(schema, item);
@@ -157,7 +148,7 @@
<div class="d-flex align-items-center gap-2 flex-grow-1"> <div class="d-flex align-items-center gap-2 flex-grow-1">
${ ${
schema.sortable 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> <div>
@@ -166,13 +157,22 @@
</div> </div>
</div> </div>
<div class="d-flex align-items-center gap-2 flex-wrap"> <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)} ${renderItemActions(schema.itemActions, item)}
<button type="button" class="btn btn-outline-danger btn-sm" data-remove-item="true"> <button type="button" class="cms-remove-button" data-remove-item="true" title="Remove item">
<i class="fas fa-trash-alt me-1"></i>Remove <i class="fas fa-trash-alt"></i>
</button> </button>
</div> </div>
`; `;
itemHeader
.querySelector('[data-toggle-item="true"]')
.addEventListener("click", function () {
itemCard.classList.toggle("is-collapsed");
});
itemHeader itemHeader
.querySelector('[data-remove-item="true"]') .querySelector('[data-remove-item="true"]')
.addEventListener("click", function () { .addEventListener("click", function () {
@@ -224,6 +224,17 @@
list.appendChild(itemCard); 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) { if (schema.sortable && window.Sortable) {
window.Sortable.create(list, { window.Sortable.create(list, {
animation: 150, animation: 150,
@@ -598,7 +609,7 @@
function appendHelp(col, schema, value, extraHint) { function appendHelp(col, schema, value, extraHint) {
const wrapper = document.createElement("div"); const wrapper = document.createElement("div");
wrapper.className = "d-flex justify-content-between gap-3"; wrapper.className = "field-meta-row";
const help = document.createElement("div"); const help = document.createElement("div");
help.className = "form-text"; help.className = "form-text";
@@ -608,7 +619,7 @@
let counter = null; let counter = null;
if (schema.maxLength) { if (schema.maxLength) {
counter = document.createElement("div"); counter = document.createElement("div");
counter.className = "form-text text-end ms-auto"; counter.className = "field-char-count";
updateCounter(counter, String(value || "").length, schema.maxLength); updateCounter(counter, String(value || "").length, schema.maxLength);
wrapper.appendChild(counter); wrapper.appendChild(counter);
} }
@@ -111,26 +111,17 @@
const col = createCol(schema.colClass || "col-12"); const col = createCol(schema.colClass || "col-12");
const card = document.createElement("div"); 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"); 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 = ` header.innerHTML = `
<div> <div>
<label class="form-label fw-semibold mb-1">${escapeHtml(schema.label)}</label> <label class="form-label fw-semibold mb-1">${escapeHtml(schema.label)}</label>
${schema.helpText ? `<div class="form-text mt-0">${escapeHtml(schema.helpText)}</div>` : ""} ${schema.helpText ? `<div class="form-text mt-0">${escapeHtml(schema.helpText)}</div>` : ""}
</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); card.appendChild(header);
if (parent[key].length === 0) { if (parent[key].length === 0) {
@@ -145,11 +136,11 @@
parent[key].forEach((item, index) => { parent[key].forEach((item, index) => {
const itemCard = document.createElement("div"); 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); itemCard.dataset.index = String(index);
const itemHeader = document.createElement("div"); 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 title = getArrayItemTitle(schema, item, index);
const subtitle = getArrayItemSubtitle(schema, item); const subtitle = getArrayItemSubtitle(schema, item);
@@ -157,7 +148,7 @@
<div class="d-flex align-items-center gap-2 flex-grow-1"> <div class="d-flex align-items-center gap-2 flex-grow-1">
${ ${
schema.sortable 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> <div>
@@ -166,13 +157,22 @@
</div> </div>
</div> </div>
<div class="d-flex align-items-center gap-2 flex-wrap"> <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)} ${renderItemActions(schema.itemActions, item)}
<button type="button" class="btn btn-outline-danger btn-sm" data-remove-item="true"> <button type="button" class="cms-remove-button" data-remove-item="true" title="Remove item">
<i class="fas fa-trash-alt me-1"></i>Remove <i class="fas fa-trash-alt"></i>
</button> </button>
</div> </div>
`; `;
itemHeader
.querySelector('[data-toggle-item="true"]')
.addEventListener("click", function () {
itemCard.classList.toggle("is-collapsed");
});
itemHeader itemHeader
.querySelector('[data-remove-item="true"]') .querySelector('[data-remove-item="true"]')
.addEventListener("click", function () { .addEventListener("click", function () {
@@ -224,6 +224,17 @@
list.appendChild(itemCard); 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) { if (schema.sortable && window.Sortable) {
window.Sortable.create(list, { window.Sortable.create(list, {
animation: 150, animation: 150,
@@ -598,7 +609,7 @@
function appendHelp(col, schema, value, extraHint) { function appendHelp(col, schema, value, extraHint) {
const wrapper = document.createElement("div"); const wrapper = document.createElement("div");
wrapper.className = "d-flex justify-content-between gap-3"; wrapper.className = "field-meta-row";
const help = document.createElement("div"); const help = document.createElement("div");
help.className = "form-text"; help.className = "form-text";
@@ -608,7 +619,7 @@
let counter = null; let counter = null;
if (schema.maxLength) { if (schema.maxLength) {
counter = document.createElement("div"); counter = document.createElement("div");
counter.className = "form-text text-end ms-auto"; counter.className = "field-char-count";
updateCounter(counter, String(value || "").length, schema.maxLength); updateCounter(counter, String(value || "").length, schema.maxLength);
wrapper.appendChild(counter); wrapper.appendChild(counter);
} }
+29 -18
View File
@@ -111,26 +111,17 @@
const col = createCol(schema.colClass || "col-12"); const col = createCol(schema.colClass || "col-12");
const card = document.createElement("div"); 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"); 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 = ` header.innerHTML = `
<div> <div>
<label class="form-label fw-semibold mb-1">${escapeHtml(schema.label)}</label> <label class="form-label fw-semibold mb-1">${escapeHtml(schema.label)}</label>
${schema.helpText ? `<div class="form-text mt-0">${escapeHtml(schema.helpText)}</div>` : ""} ${schema.helpText ? `<div class="form-text mt-0">${escapeHtml(schema.helpText)}</div>` : ""}
</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); card.appendChild(header);
if (parent[key].length === 0) { if (parent[key].length === 0) {
@@ -145,11 +136,11 @@
parent[key].forEach((item, index) => { parent[key].forEach((item, index) => {
const itemCard = document.createElement("div"); 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); itemCard.dataset.index = String(index);
const itemHeader = document.createElement("div"); 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 title = getArrayItemTitle(schema, item, index);
const subtitle = getArrayItemSubtitle(schema, item); const subtitle = getArrayItemSubtitle(schema, item);
@@ -157,7 +148,7 @@
<div class="d-flex align-items-center gap-2 flex-grow-1"> <div class="d-flex align-items-center gap-2 flex-grow-1">
${ ${
schema.sortable 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> <div>
@@ -166,13 +157,22 @@
</div> </div>
</div> </div>
<div class="d-flex align-items-center gap-2 flex-wrap"> <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)} ${renderItemActions(schema.itemActions, item)}
<button type="button" class="btn btn-outline-danger btn-sm" data-remove-item="true"> <button type="button" class="cms-remove-button" data-remove-item="true" title="Remove item">
<i class="fas fa-trash-alt me-1"></i>Remove <i class="fas fa-trash-alt"></i>
</button> </button>
</div> </div>
`; `;
itemHeader
.querySelector('[data-toggle-item="true"]')
.addEventListener("click", function () {
itemCard.classList.toggle("is-collapsed");
});
itemHeader itemHeader
.querySelector('[data-remove-item="true"]') .querySelector('[data-remove-item="true"]')
.addEventListener("click", function () { .addEventListener("click", function () {
@@ -224,6 +224,17 @@
list.appendChild(itemCard); 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) { if (schema.sortable && window.Sortable) {
window.Sortable.create(list, { window.Sortable.create(list, {
animation: 150, animation: 150,
@@ -598,7 +609,7 @@
function appendHelp(col, schema, value, extraHint) { function appendHelp(col, schema, value, extraHint) {
const wrapper = document.createElement("div"); const wrapper = document.createElement("div");
wrapper.className = "d-flex justify-content-between gap-3"; wrapper.className = "field-meta-row";
const help = document.createElement("div"); const help = document.createElement("div");
help.className = "form-text"; help.className = "form-text";
@@ -608,7 +619,7 @@
let counter = null; let counter = null;
if (schema.maxLength) { if (schema.maxLength) {
counter = document.createElement("div"); counter = document.createElement("div");
counter.className = "form-text text-end ms-auto"; counter.className = "field-char-count";
updateCounter(counter, String(value || "").length, schema.maxLength); updateCounter(counter, String(value || "").length, schema.maxLength);
wrapper.appendChild(counter); wrapper.appendChild(counter);
} }
@@ -19,30 +19,30 @@
</div> </div>
</div> </div>
<div class="border rounded-3 bg-light-subtle p-3 mb-4"> <div class="cms-editor-group mb-4">
<div class="d-flex justify-content-between align-items-center mb-3"> <div class="mb-3">
<div> <div>
<label class="form-label fw-semibold mb-1">Category tabs</label> <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 class="form-text mt-0">The frontend shows up to 3 direct tabs. Additional tabs move into a More dropdown.</div>
</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>
<div id="directoryTabsList"></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>
<div class="border rounded-3 bg-light-subtle p-3"> <div class="cms-editor-group">
<div class="d-flex justify-content-between align-items-center mb-3"> <div class="mb-3">
<div> <div>
<label class="form-label fw-semibold mb-1">Partners</label> <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">Use a short unique partner key so card state stays stable.</div>
</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>
<div id="partnersList"></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> </div>
</div> </div>
@@ -140,6 +140,9 @@
renderDirectoryTabs(); renderDirectoryTabs();
renderPartners(); renderPartners();
}); });
node.querySelector("[data-toggle-item]")?.addEventListener("click", function () {
node.classList.toggle("is-collapsed");
});
directoryTabsList.appendChild(node); directoryTabsList.appendChild(node);
}); });
@@ -204,6 +207,9 @@
state.directory.partners.splice(index, 1); state.directory.partners.splice(index, 1);
renderPartners(); renderPartners();
}); });
node.querySelector("[data-toggle-item]")?.addEventListener("click", function () {
node.classList.toggle("is-collapsed");
});
partnersList.appendChild(node); partnersList.appendChild(node);
}); });
@@ -277,6 +283,9 @@
field.options.push(""); field.options.push("");
renderInquiryFields(); renderInquiryFields();
}); });
node.querySelector("[data-toggle-item]")?.addEventListener("click", function () {
node.classList.toggle("is-collapsed");
});
function toggleOptions() { function toggleOptions() {
optionsWrap.classList.toggle("d-none", typeSelect.value !== "select"); optionsWrap.classList.toggle("d-none", typeSelect.value !== "select");
@@ -15,17 +15,17 @@
</div> </div>
</div> </div>
<div class="border rounded-3 bg-light-subtle p-3"> <div class="cms-editor-group">
<div class="d-flex justify-content-between align-items-center mb-3"> <div class="mb-3">
<div> <div>
<label class="form-label fw-semibold mb-1">Form fields</label> <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 class="form-text mt-0">Manage labels, placeholders, type, width, and dropdown options.</div>
</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>
<div id="inquiryFieldsList"></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> </div>
</div> </div>
+40 -25
View File
@@ -1,15 +1,20 @@
<template id="directoryTabTemplate"> <template id="directoryTabTemplate">
<div class="card shadow-sm border-0 mb-3" data-item="directory-tab"> <div class="card cms-item-card 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-header d-flex justify-content-between align-items-center gap-2">
<div class="d-flex 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> <i class="fas fa-grip-vertical"></i>
</button> </button>
<div class="fw-semibold">Category Tab</div> <div class="fw-semibold">Category Tab</div>
</div> </div>
<button type="button" class="btn btn-outline-danger btn-sm" data-remove-item> <div class="d-flex align-items-center gap-2">
<i class="fas fa-trash-alt me-1"></i>Remove <button type="button" class="cms-collapse-toggle" data-toggle-item title="Collapse item">
</button> <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>
<div class="card-body"> <div class="card-body">
<label class="form-label fw-semibold">Tab label</label> <label class="form-label fw-semibold">Tab label</label>
@@ -19,10 +24,10 @@
</template> </template>
<template id="partnerTemplate"> <template id="partnerTemplate">
<div class="card shadow-sm border-0 mb-3" data-item="partner"> <div class="card cms-item-card 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-header d-flex justify-content-between align-items-center gap-2 flex-wrap">
<div class="d-flex 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> <i class="fas fa-grip-vertical"></i>
</button> </button>
<div> <div>
@@ -30,9 +35,14 @@
<div class="small text-muted" data-subtitle></div> <div class="small text-muted" data-subtitle></div>
</div> </div>
</div> </div>
<button type="button" class="btn btn-outline-danger btn-sm" data-remove-item> <div class="d-flex align-items-center gap-2">
<i class="fas fa-trash-alt me-1"></i>Remove <button type="button" class="cms-collapse-toggle" data-toggle-item title="Collapse item">
</button> <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>
<div class="card-body"> <div class="card-body">
<div class="row g-3"> <div class="row g-3">
@@ -87,10 +97,10 @@
</template> </template>
<template id="inquiryFieldTemplate"> <template id="inquiryFieldTemplate">
<div class="card shadow-sm border-0 mb-3" data-item="inquiry-field"> <div class="card cms-item-card 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-header d-flex justify-content-between align-items-center gap-2 flex-wrap">
<div class="d-flex 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> <i class="fas fa-grip-vertical"></i>
</button> </button>
<div> <div>
@@ -98,9 +108,14 @@
<div class="small text-muted" data-subtitle></div> <div class="small text-muted" data-subtitle></div>
</div> </div>
</div> </div>
<button type="button" class="btn btn-outline-danger btn-sm" data-remove-item> <div class="d-flex align-items-center gap-2">
<i class="fas fa-trash-alt me-1"></i>Remove <button type="button" class="cms-collapse-toggle" data-toggle-item title="Collapse item">
</button> <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>
<div class="card-body"> <div class="card-body">
<div class="row g-3"> <div class="row g-3">
@@ -138,15 +153,15 @@
</div> </div>
</div> </div>
<div class="col-12" data-options-wrap> <div class="col-12" data-options-wrap>
<div class="border rounded-3 p-3 bg-light"> <div class="cms-editor-group">
<div class="d-flex justify-content-between align-items-center mb-2"> <div class="mb-2">
<label class="form-label fw-semibold mb-0">Dropdown options</label> <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>
<div class="form-text mb-2">Only used when the field type is Dropdown.</div> <div class="form-text mb-2">Only used when the field type is Dropdown.</div>
<div data-options-list></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> </div>
</div> </div>
@@ -156,11 +171,11 @@
<template id="inquiryOptionTemplate"> <template id="inquiryOptionTemplate">
<div class="input-group mb-2" data-item="inquiry-option"> <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> <i class="fas fa-grip-vertical"></i>
</button> </button>
<input class="form-control" data-option-value maxlength="50" placeholder="Option label"> <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> <i class="fas fa-trash-alt"></i>
</button> </button>
</div> </div>
+29 -18
View File
@@ -111,26 +111,17 @@
const col = createCol(schema.colClass || "col-12"); const col = createCol(schema.colClass || "col-12");
const card = document.createElement("div"); 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"); 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 = ` header.innerHTML = `
<div> <div>
<label class="form-label fw-semibold mb-1">${escapeHtml(schema.label)}</label> <label class="form-label fw-semibold mb-1">${escapeHtml(schema.label)}</label>
${schema.helpText ? `<div class="form-text mt-0">${escapeHtml(schema.helpText)}</div>` : ""} ${schema.helpText ? `<div class="form-text mt-0">${escapeHtml(schema.helpText)}</div>` : ""}
</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); card.appendChild(header);
if (parent[key].length === 0) { if (parent[key].length === 0) {
@@ -145,11 +136,11 @@
parent[key].forEach((item, index) => { parent[key].forEach((item, index) => {
const itemCard = document.createElement("div"); 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); itemCard.dataset.index = String(index);
const itemHeader = document.createElement("div"); 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 title = getArrayItemTitle(schema, item, index);
const subtitle = getArrayItemSubtitle(schema, item); const subtitle = getArrayItemSubtitle(schema, item);
@@ -157,7 +148,7 @@
<div class="d-flex align-items-center gap-2 flex-grow-1"> <div class="d-flex align-items-center gap-2 flex-grow-1">
${ ${
schema.sortable 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> <div>
@@ -166,13 +157,22 @@
</div> </div>
</div> </div>
<div class="d-flex align-items-center gap-2 flex-wrap"> <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)} ${renderItemActions(schema.itemActions, item)}
<button type="button" class="btn btn-outline-danger btn-sm" data-remove-item="true"> <button type="button" class="cms-remove-button" data-remove-item="true" title="Remove item">
<i class="fas fa-trash-alt me-1"></i>Remove <i class="fas fa-trash-alt"></i>
</button> </button>
</div> </div>
`; `;
itemHeader
.querySelector('[data-toggle-item="true"]')
.addEventListener("click", function () {
itemCard.classList.toggle("is-collapsed");
});
itemHeader itemHeader
.querySelector('[data-remove-item="true"]') .querySelector('[data-remove-item="true"]')
.addEventListener("click", function () { .addEventListener("click", function () {
@@ -224,6 +224,17 @@
list.appendChild(itemCard); 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) { if (schema.sortable && window.Sortable) {
window.Sortable.create(list, { window.Sortable.create(list, {
animation: 150, animation: 150,
@@ -598,7 +609,7 @@
function appendHelp(col, schema, value, extraHint) { function appendHelp(col, schema, value, extraHint) {
const wrapper = document.createElement("div"); const wrapper = document.createElement("div");
wrapper.className = "d-flex justify-content-between gap-3"; wrapper.className = "field-meta-row";
const help = document.createElement("div"); const help = document.createElement("div");
help.className = "form-text"; help.className = "form-text";
@@ -608,7 +619,7 @@
let counter = null; let counter = null;
if (schema.maxLength) { if (schema.maxLength) {
counter = document.createElement("div"); counter = document.createElement("div");
counter.className = "form-text text-end ms-auto"; counter.className = "field-char-count";
updateCounter(counter, String(value || "").length, schema.maxLength); updateCounter(counter, String(value || "").length, schema.maxLength);
wrapper.appendChild(counter); wrapper.appendChild(counter);
} }
@@ -111,26 +111,17 @@
const col = createCol(schema.colClass || "col-12"); const col = createCol(schema.colClass || "col-12");
const card = document.createElement("div"); 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"); 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 = ` header.innerHTML = `
<div> <div class="mb-2">
<label class="form-label fw-semibold mb-1">${escapeHtml(schema.label)}</label> <label class="form-label fw-semibold mb-1">${escapeHtml(schema.label)}</label>
${schema.helpText ? `<div class="form-text mt-0">${escapeHtml(schema.helpText)}</div>` : ""} ${schema.helpText ? `<div class="form-text mt-0">${escapeHtml(schema.helpText)}</div>` : ""}
</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); card.appendChild(header);
if (parent[key].length === 0) { if (parent[key].length === 0) {
@@ -145,11 +136,11 @@
parent[key].forEach((item, index) => { parent[key].forEach((item, index) => {
const itemCard = document.createElement("div"); 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); itemCard.dataset.index = String(index);
const itemHeader = document.createElement("div"); 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 title = getArrayItemTitle(schema, item, index);
const subtitle = getArrayItemSubtitle(schema, item); const subtitle = getArrayItemSubtitle(schema, item);
@@ -157,7 +148,7 @@
<div class="d-flex align-items-center gap-2 flex-grow-1"> <div class="d-flex align-items-center gap-2 flex-grow-1">
${ ${
schema.sortable 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> <div>
@@ -166,13 +157,22 @@
</div> </div>
</div> </div>
<div class="d-flex align-items-center gap-2 flex-wrap"> <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)} ${renderItemActions(schema.itemActions, item)}
<button type="button" class="btn btn-outline-danger btn-sm" data-remove-item="true"> <button type="button" class="cms-remove-button" data-remove-item="true" title="Remove item">
<i class="fas fa-trash-alt me-1"></i>Remove <i class="fas fa-trash-alt"></i>
</button> </button>
</div> </div>
`; `;
itemHeader
.querySelector('[data-toggle-item="true"]')
.addEventListener("click", function () {
itemCard.classList.toggle("is-collapsed");
});
itemHeader itemHeader
.querySelector('[data-remove-item="true"]') .querySelector('[data-remove-item="true"]')
.addEventListener("click", function () { .addEventListener("click", function () {
@@ -224,6 +224,17 @@
list.appendChild(itemCard); 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) { if (schema.sortable && window.Sortable) {
window.Sortable.create(list, { window.Sortable.create(list, {
animation: 150, animation: 150,
@@ -598,7 +609,7 @@
function appendHelp(col, schema, value, extraHint) { function appendHelp(col, schema, value, extraHint) {
const wrapper = document.createElement("div"); const wrapper = document.createElement("div");
wrapper.className = "d-flex justify-content-between gap-3"; wrapper.className = "field-meta-row";
const help = document.createElement("div"); const help = document.createElement("div");
help.className = "form-text"; help.className = "form-text";
@@ -608,7 +619,7 @@
let counter = null; let counter = null;
if (schema.maxLength) { if (schema.maxLength) {
counter = document.createElement("div"); counter = document.createElement("div");
counter.className = "form-text text-end ms-auto"; counter.className = "field-char-count";
updateCounter(counter, String(value || "").length, schema.maxLength); updateCounter(counter, String(value || "").length, schema.maxLength);
wrapper.appendChild(counter); wrapper.appendChild(counter);
} }
+177 -6
View File
@@ -283,18 +283,22 @@
.fixed-bottom-buttons { .fixed-bottom-buttons {
position: fixed; position: fixed;
bottom: 0; left: 0;
right: 0; right: 0;
padding: 1rem; bottom: 0;
padding: 0.875rem 1.5rem;
z-index: 1000; z-index: 1000;
width: 25%;
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
gap: 0.5rem; gap: 0.75rem;
background: rgba(255, 255, 255, 0.96);
border-top: 1px solid rgba(15, 23, 42, 0.08);
box-shadow: 0 -4px 6px -1px rgba(0, 0, 0, 0.08);
backdrop-filter: blur(8px);
} }
.fixed-bottom-buttons .btn { .fixed-bottom-buttons .btn {
min-width: 120px; min-width: 136px;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -303,7 +307,174 @@
/* Add padding to prevent content from being hidden behind fixed buttons */ /* Add padding to prevent content from being hidden behind fixed buttons */
.content-with-fixed-buttons { .content-with-fixed-buttons {
padding-bottom: 80px; padding-bottom: 110px;
}
.content-with-fixed-buttons .card {
border: 1px solid rgba(148, 163, 184, 0.16) !important;
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04), 0 8px 24px rgba(15, 23, 42, 0.04) !important;
}
.content-with-fixed-buttons .card-header {
background: #fff;
border-bottom: 1px solid rgba(148, 163, 184, 0.14);
padding: 0.875rem 1rem;
}
.content-with-fixed-buttons .card-body {
padding: 1rem;
}
.content-with-fixed-buttons .form-control,
.content-with-fixed-buttons .form-select {
background: #fff;
border: 1px solid rgba(148, 163, 184, 0.18);
box-shadow: none;
}
.content-with-fixed-buttons .form-control:focus,
.content-with-fixed-buttons .form-select:focus {
border-color: rgba(184, 183, 106, 0.55);
box-shadow: 0 0 0 0.2rem rgba(184, 183, 106, 0.12);
}
.content-with-fixed-buttons .form-text {
color: #94a3b8;
font-size: 0.75rem;
margin-top: 0.4rem;
}
.content-with-fixed-buttons .card-header-tabs {
margin-bottom: -0.875rem;
}
.content-with-fixed-buttons .nav-tabs {
border-bottom: 0;
gap: 0.25rem;
}
.content-with-fixed-buttons .nav-tabs .nav-link {
border: 0;
border-radius: 8px 8px 0 0;
color: #64748b;
padding: 0.75rem 0.95rem;
}
.content-with-fixed-buttons .nav-tabs .nav-link.active {
color: #0f172a;
background: rgba(248, 250, 252, 0.96);
box-shadow: inset 0 -2px 0 var(--primary-color);
}
.cms-editor-group {
background: #f8fafc;
border: 1px solid rgba(148, 163, 184, 0.12);
border-radius: 12px;
padding: 1rem;
}
.cms-item-card {
background: #f8fafc;
border: 1px solid rgba(148, 163, 184, 0.12) !important;
border-radius: 12px;
box-shadow: none !important;
}
.cms-item-card .card-header {
background: transparent;
border-bottom: 1px solid rgba(148, 163, 184, 0.1);
padding: 0.75rem 0.875rem;
}
.cms-item-card .card-body {
background: transparent;
padding: 0.875rem;
}
.cms-item-card.is-collapsed .card-body {
display: none;
}
.cms-item-card .drag-handle {
background: transparent;
border: 0;
color: #94a3b8;
padding: 0.25rem 0.375rem;
box-shadow: none;
}
.cms-item-card .drag-handle:hover {
color: #475569;
background: rgba(255, 255, 255, 0.75);
}
.cms-collapse-toggle {
display: inline-flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
border: 0;
background: transparent;
color: #94a3b8;
border-radius: 999px;
}
.cms-collapse-toggle:hover {
background: rgba(148, 163, 184, 0.12);
color: #475569;
}
.cms-collapse-toggle i {
transition: transform 0.18s ease;
}
.cms-item-card.is-collapsed .cms-collapse-toggle i {
transform: rotate(-90deg);
}
.cms-remove-button {
border: 0;
background: transparent;
color: #94a3b8;
padding: 0.25rem;
box-shadow: none;
}
.cms-remove-button:hover {
color: #ef4444;
background: transparent;
}
.cms-add-button {
width: 100%;
border: 1px dashed rgba(148, 163, 184, 0.55);
border-radius: 10px;
background: rgba(255, 255, 255, 0.7);
color: #475569;
padding: 0.8rem 1rem;
font-weight: 600;
}
.cms-add-button:hover {
background: #fff;
border-color: rgba(184, 183, 106, 0.65);
color: var(--primary-dark);
}
.field-meta-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 0.75rem;
margin-top: 0.35rem;
}
.field-char-count {
color: #94a3b8;
font-size: 0.75rem;
white-space: nowrap;
margin-left: auto;
} }
main { main {