feat(cms): refactor page content management and implement detailed editors

Refactor the CMS page content system to use a more modular configuration-driven approach. This replaces the monolithic `pageContentConfig.js` with individual configuration files for each page and introduces a shared field utility to standardize editor components.
Key changes:
- Implement a generic `_renderSingletonPageView` helper to reduce duplication across controllers.
- Enhance `_createPageContentController` with `normalizeForEditor`, `normalizeForApi`, and `preparePayload` hooks for custom data transformation.
- Create dedicated configuration and view structures for Partnerships, History, Accreditation, Admissions, and Policies pages.
- Add a specialized section editor for Policies to manage complex nested content.
- Improve the frontend `page-content-editor.js` with support for visibility logic, auto-sequencing for arrays, and URL synchronization for active tabs.
- Update data JSON files to align with the new schema.
This commit is contained in:
Tống Thành Đạt
2026-04-20 19:43:28 +07:00
parent 8b6bf5fe6e
commit 7df3a6f6bf
58 changed files with 7501 additions and 844 deletions
@@ -0,0 +1,26 @@
<div class="tab-pane fade <%= activeTab === 'cta' ? 'show active' : '' %>" id="cta" role="tabpanel">
<div class="card border shadow-sm">
<div class="card-header bg-white">
<h6 class="mb-0"><i class="fas fa-bullhorn me-2"></i>Call To Action</h6>
</div>
<div class="card-body p-4">
<div class="row g-3">
<div class="col-12">
<label class="form-label fw-semibold">Headline</label>
<input class="form-control" id="ctaHeading" maxlength="80" value="<%= data.cta?.heading || '' %>">
</div>
<div class="col-12">
<label class="form-label fw-semibold">Supporting text</label>
<textarea class="form-control" id="ctaDescription" rows="4" maxlength="220"><%= data.cta?.description || '' %></textarea>
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">Button label</label>
<input class="form-control" id="ctaButtonLabel" maxlength="40" value="<%= data.cta?.buttonLabel || '' %>">
</div>
</div>
</div>
</div>
</div>
@@ -0,0 +1,52 @@
<div class="tab-pane fade <%= activeTab === 'directory' ? 'show active' : '' %>" id="directory" role="tabpanel">
<div class="card border shadow-sm">
<div class="card-header bg-white">
<h6 class="mb-0"><i class="fas fa-handshake me-2"></i>Partner Directory</h6>
</div>
<div class="card-body p-4">
<div class="row g-3 mb-4">
<div class="col-md-8">
<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>
</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>
<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>
</div>
<div class="border rounded-3 bg-light-subtle p-3">
<div class="d-flex justify-content-between align-items-center 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>
</div>
</div>
</div>
</div>
@@ -0,0 +1,413 @@
<script>
(function () {
const initialData = window.partnershipsPageData;
const backendUrl = (window.partnershipsBackendUrl || "").replace(/\/$/, "");
const form = document.getElementById("cmsEditorForm");
const pageJsonInput = document.getElementById("pageJson");
const activeTabInput = document.getElementById("activeTabInput");
const directoryTabsList = document.getElementById("directoryTabsList");
const partnersList = document.getElementById("partnersList");
const inquiryFieldsList = document.getElementById("inquiryFieldsList");
if (!initialData || !form || !pageJsonInput || !activeTabInput) {
return;
}
const state = JSON.parse(JSON.stringify(initialData));
const templates = {
tab: document.getElementById("directoryTabTemplate"),
partner: document.getElementById("partnerTemplate"),
inquiryField: document.getElementById("inquiryFieldTemplate"),
inquiryOption: document.getElementById("inquiryOptionTemplate"),
};
bindStaticEvents();
renderAll();
function bindStaticEvents() {
document.querySelectorAll("[data-tab-key]").forEach((tabTrigger) => {
tabTrigger.addEventListener("shown.bs.tab", function () {
const tabKey = this.dataset.tabKey;
if (!tabKey) return;
activeTabInput.value = tabKey;
const url = new URL(window.location.href);
url.searchParams.set("tab", tabKey);
window.history.replaceState({}, "", `${url.pathname}?${url.searchParams.toString()}`);
});
});
document.getElementById("addDirectoryTabBtn")?.addEventListener("click", function () {
state.directory.tabs.push("");
renderDirectoryTabs();
});
document.getElementById("addPartnerBtn")?.addEventListener("click", function () {
state.directory.partners.push({
id: "",
name: "",
category: "",
summary: "",
logo: "",
logoAlt: "",
about: "",
collabType: "",
benefits: "",
});
renderPartners();
});
document.getElementById("addInquiryFieldBtn")?.addEventListener("click", function () {
state.inquiryForm.fields.push({
id: "",
label: "",
placeholder: "",
type: "text",
width: "full",
required: true,
options: [],
});
renderInquiryFields();
});
document.getElementById("resetPartnershipsForm")?.addEventListener("click", function () {
window.location.reload();
});
form.addEventListener("submit", function () {
syncStaticFields();
pageJsonInput.value = JSON.stringify(state);
});
document.querySelectorAll("[data-upload-target]").forEach((button) => {
button.addEventListener("click", function () {
const targetId = button.getAttribute("data-upload-target");
const previewId = button.getAttribute("data-preview-target");
const input = document.getElementById(targetId);
const preview = document.getElementById(previewId);
openImagePicker(function (path) {
input.value = path;
preview.src = resolveImageUrl(path);
preview.classList.toggle("d-none", !path);
});
});
});
}
function syncStaticFields() {
state.hero = {
badge: getValue("heroBadge"),
title: getValue("heroTitle"),
description: getValue("heroDescription"),
linkLabel: getValue("heroLinkLabel"),
image: getValue("heroImage"),
imageAlt: getValue("heroImageAlt"),
};
state.directory.heading = getValue("directoryHeading");
state.directory.description = getValue("directoryDescription");
state.directory.loadMoreLabel = getValue("directoryLoadMoreLabel");
state.cta = {
heading: getValue("ctaHeading"),
description: getValue("ctaDescription"),
buttonLabel: getValue("ctaButtonLabel"),
};
state.inquiryForm.title = getValue("inquiryTitle");
state.inquiryForm.submitLabel = getValue("inquirySubmitLabel");
}
function renderAll() {
renderDirectoryTabs();
renderPartners();
renderInquiryFields();
}
function renderDirectoryTabs() {
directoryTabsList.innerHTML = "";
state.directory.tabs.forEach((tab, index) => {
const node = cloneTemplate(templates.tab);
const input = node.querySelector('[data-field="label"]');
input.value = tab || "";
input.addEventListener("input", function () {
state.directory.tabs[index] = input.value;
refreshPartnerCategoryLists();
});
node.querySelector("[data-remove-item]").addEventListener("click", function () {
state.directory.tabs.splice(index, 1);
renderDirectoryTabs();
renderPartners();
});
directoryTabsList.appendChild(node);
});
initSortable(directoryTabsList, state.directory.tabs, renderDirectoryTabs, '[data-item="directory-tab"]');
}
function renderPartners() {
partnersList.innerHTML = "";
state.directory.partners.forEach((partner, index) => {
const node = cloneTemplate(templates.partner);
const title = node.querySelector("[data-title]");
const subtitle = node.querySelector("[data-subtitle]");
const categoryInput = node.querySelector('[data-field="category"]');
const categoryListId = `partner-category-options-${index}`;
title.textContent = partner.name || `Partner ${index + 1}`;
subtitle.textContent = partner.category || "";
node.querySelectorAll("[data-field]").forEach((input) => {
const field = input.getAttribute("data-field");
input.value = partner[field] || "";
input.addEventListener("input", function () {
partner[field] = input.value;
if (field === "name") {
title.textContent = input.value || `Partner ${index + 1}`;
}
if (field === "category") {
subtitle.textContent = input.value || "";
}
});
});
categoryInput.setAttribute("list", categoryListId);
const dataList = document.createElement("datalist");
dataList.id = categoryListId;
state.directory.tabs.forEach((tab) => {
const option = document.createElement("option");
option.value = tab;
dataList.appendChild(option);
});
node.appendChild(dataList);
const preview = node.querySelector("[data-preview]");
const logoInput = node.querySelector('[data-field="logo"]');
if (partner.logo) {
preview.src = resolveImageUrl(partner.logo);
preview.classList.remove("d-none");
}
node.querySelector("[data-upload-button]").addEventListener("click", function () {
openImagePicker(function (path) {
partner.logo = path;
logoInput.value = path;
preview.src = resolveImageUrl(path);
preview.classList.remove("d-none");
});
});
node.querySelector("[data-remove-item]").addEventListener("click", function () {
state.directory.partners.splice(index, 1);
renderPartners();
});
partnersList.appendChild(node);
});
initSortable(partnersList, state.directory.partners, renderPartners, '[data-item="partner"]');
}
function refreshPartnerCategoryLists() {
partnersList.querySelectorAll("datalist").forEach((list) => list.remove());
renderPartners();
}
function renderInquiryFields() {
inquiryFieldsList.innerHTML = "";
state.inquiryForm.fields.forEach((field, index) => {
const node = cloneTemplate(templates.inquiryField);
const title = node.querySelector("[data-title]");
const subtitle = node.querySelector("[data-subtitle]");
const optionsWrap = node.querySelector("[data-options-wrap]");
const optionsList = node.querySelector("[data-options-list]");
const typeSelect = node.querySelector('[data-field="type"]');
title.textContent = field.label || `Field ${index + 1}`;
subtitle.textContent = field.type || "";
node.querySelectorAll("[data-field]").forEach((input) => {
const key = input.getAttribute("data-field");
if (input.type === "checkbox") {
input.checked = Boolean(field[key]);
input.addEventListener("change", function () {
field[key] = input.checked;
});
return;
}
input.value = field[key] || "";
input.addEventListener("input", function () {
field[key] = input.value;
if (key === "label") {
title.textContent = input.value || `Field ${index + 1}`;
}
if (key === "type") {
subtitle.textContent = input.value || "";
toggleOptions();
}
});
if (input.tagName === "SELECT") {
input.addEventListener("change", function () {
field[key] = input.value;
if (key === "type") {
subtitle.textContent = input.value || "";
if (input.value !== "select") {
field.options = [];
}
toggleOptions();
renderInquiryFields();
}
});
}
});
node.querySelector("[data-remove-item]").addEventListener("click", function () {
state.inquiryForm.fields.splice(index, 1);
renderInquiryFields();
});
node.querySelector("[data-add-option]").addEventListener("click", function () {
field.options = Array.isArray(field.options) ? field.options : [];
field.options.push("");
renderInquiryFields();
});
function toggleOptions() {
optionsWrap.classList.toggle("d-none", typeSelect.value !== "select");
}
function renderOptions() {
optionsList.innerHTML = "";
(field.options || []).forEach((optionValue, optionIndex) => {
const optionNode = cloneTemplate(templates.inquiryOption);
const optionInput = optionNode.querySelector("[data-option-value]");
optionInput.value = optionValue || "";
optionInput.addEventListener("input", function () {
field.options[optionIndex] = optionInput.value;
});
optionNode.querySelector("[data-remove-item]").addEventListener("click", function () {
field.options.splice(optionIndex, 1);
renderInquiryFields();
});
optionsList.appendChild(optionNode);
});
initSortable(optionsList, field.options, renderInquiryFields, '[data-item="inquiry-option"]');
}
toggleOptions();
renderOptions();
inquiryFieldsList.appendChild(node);
});
initSortable(inquiryFieldsList, state.inquiryForm.fields, renderInquiryFields, '[data-item="inquiry-field"]');
}
function initSortable(container, list, rerender, draggableSelector) {
if (!window.Sortable || !container) return;
if (container._sortableInstance) {
container._sortableInstance.destroy();
}
container._sortableInstance = window.Sortable.create(container, {
animation: 150,
handle: ".drag-handle",
draggable: draggableSelector,
onEnd: function (event) {
if (event.oldIndex === event.newIndex) return;
const moved = list.splice(event.oldIndex, 1)[0];
list.splice(event.newIndex, 0, moved);
rerender();
},
});
}
function cloneTemplate(template) {
return template.content.firstElementChild.cloneNode(true);
}
function getValue(id) {
return document.getElementById(id)?.value || "";
}
function openImagePicker(onSuccess) {
const fileInput = document.createElement("input");
fileInput.type = "file";
fileInput.accept = "image/*";
fileInput.style.display = "none";
document.body.appendChild(fileInput);
fileInput.addEventListener("change", async function () {
if (!fileInput.files || !fileInput.files[0]) {
fileInput.remove();
return;
}
try {
const formData = new FormData();
formData.append("image", fileInput.files[0]);
const response = await fetch("/admin/upload/image?imageType=partnerships", {
method: "POST",
body: formData,
});
const result = await response.json();
if (!result.success || !result.path) {
throw new Error(result.error || "Upload failed");
}
onSuccess(result.path);
showToast("Success", "Image uploaded successfully", "success");
} catch (error) {
showToast("Error", error.message || "Upload failed", "danger");
} finally {
fileInput.remove();
}
});
fileInput.click();
}
function resolveImageUrl(path) {
if (!path) return "";
if (/^https?:\/\//i.test(path)) return path;
if (path.startsWith("/")) return `${backendUrl}${path}`;
return `${backendUrl}/${path}`;
}
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;
}
function escapeHtml(value) {
return String(value || "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
})();
</script>
@@ -0,0 +1,45 @@
<div class="tab-pane fade <%= activeTab === 'hero' ? 'show active' : '' %>" id="hero" role="tabpanel">
<div class="card border shadow-sm">
<div class="card-header bg-white">
<h6 class="mb-0"><i class="fas fa-image me-2"></i>Hero</h6>
</div>
<div class="card-body p-4">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-semibold">Eyebrow label</label>
<input class="form-control" id="heroBadge" maxlength="40" value="<%= data.hero?.badge || '' %>">
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">Scroll link label</label>
<input class="form-control" id="heroLinkLabel" maxlength="40" value="<%= data.hero?.linkLabel || '' %>">
</div>
<div class="col-12">
<label class="form-label fw-semibold">Headline</label>
<input class="form-control" id="heroTitle" maxlength="90" value="<%= data.hero?.title || '' %>">
</div>
<div class="col-12">
<label class="form-label fw-semibold">Supporting text</label>
<textarea class="form-control" id="heroDescription" rows="4" maxlength="220"><%= data.hero?.description || '' %></textarea>
</div>
<div class="col-12">
<label class="form-label fw-semibold">Hero image</label>
<div class="input-group">
<input class="form-control" id="heroImage" value="<%= data.hero?.image || '' %>">
<button class="btn btn-outline-primary" type="button" data-upload-target="heroImage" data-preview-target="heroImagePreview">
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
<div class="form-text">Recommended 720x630 px</div>
<img id="heroImagePreview" src="<%= data.hero?.image ? `${backendUrl}${data.hero.image}` : '' %>" class="img-thumbnail mt-2 <%= data.hero?.image ? '' : 'd-none' %>" style="max-height: 200px;">
</div>
<div class="col-12">
<label class="form-label fw-semibold">Hero image alt text</label>
<input class="form-control" id="heroImageAlt" maxlength="120" value="<%= data.hero?.imageAlt || '' %>">
</div>
</div>
</div>
</div>
</div>
@@ -0,0 +1,35 @@
<div class="tab-pane fade <%= activeTab === 'inquiryForm' ? 'show active' : '' %>" id="inquiryForm" role="tabpanel">
<div class="card border shadow-sm">
<div class="card-header bg-white">
<h6 class="mb-0"><i class="fas fa-envelope me-2"></i>Inquiry Form</h6>
</div>
<div class="card-body p-4">
<div class="row g-3 mb-4">
<div class="col-md-8">
<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="border rounded-3 bg-light-subtle p-3">
<div class="d-flex justify-content-between align-items-center 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>
</div>
</div>
</div>
</div>
@@ -0,0 +1,167 @@
<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="d-flex align-items-center gap-2">
<button type="button" class="btn btn-light btn-sm border 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>
<div class="card-body">
<label class="form-label fw-semibold">Tab label</label>
<input class="form-control" data-field="label" maxlength="30" placeholder="Industry">
</div>
</div>
</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="d-flex align-items-center gap-2">
<button type="button" class="btn btn-light btn-sm border drag-handle" title="Drag to reorder">
<i class="fas fa-grip-vertical"></i>
</button>
<div>
<div class="fw-semibold" data-title>Partner</div>
<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>
<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">
</div>
<div class="col-md-4">
<label class="form-label fw-semibold">Category</label>
<input class="form-control" data-field="category" list="">
</div>
<div class="col-12">
<label class="form-label fw-semibold">Card summary</label>
<textarea class="form-control" data-field="summary" rows="3" maxlength="130"></textarea>
<div class="form-text">The card preview is capped at 130 characters.</div>
</div>
<div class="col-12">
<label class="form-label fw-semibold">Partner logo</label>
<div class="input-group">
<input class="form-control" data-field="logo">
<button class="btn btn-outline-primary" type="button" data-upload-button>
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
<div class="form-text">Recommended 105x80 px minimum visible ratio</div>
<img class="img-thumbnail mt-2 d-none" data-preview style="max-height: 180px;">
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">Logo alt text</label>
<input class="form-control" data-field="logoAlt" maxlength="120">
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">Collaboration type</label>
<input class="form-control" data-field="collabType" maxlength="40">
</div>
<div class="col-12">
<label class="form-label fw-semibold">About text</label>
<textarea class="form-control" data-field="about" rows="5" maxlength="600"></textarea>
</div>
<div class="col-12">
<label class="form-label fw-semibold">Benefits</label>
<textarea class="form-control" data-field="benefits" rows="4" maxlength="240"></textarea>
</div>
</div>
</div>
</div>
</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="d-flex align-items-center gap-2">
<button type="button" class="btn btn-light btn-sm border drag-handle" title="Drag to reorder">
<i class="fas fa-grip-vertical"></i>
</button>
<div>
<div class="fw-semibold" data-title>Field</div>
<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>
<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">
</div>
<div class="col-md-4">
<label class="form-label fw-semibold">Placeholder text</label>
<input class="form-control" data-field="placeholder" maxlength="80">
</div>
<div class="col-md-4">
<label class="form-label fw-semibold">Field type</label>
<select class="form-select" data-field="type">
<option value="text">Single line text</option>
<option value="textarea">Paragraph</option>
<option value="select">Dropdown</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label fw-semibold">Field width</label>
<select class="form-select" data-field="width">
<option value="half">Half width</option>
<option value="full">Full width</option>
</select>
</div>
<div class="col-md-4 d-flex align-items-end">
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" data-field="required">
<label class="form-check-label fw-semibold">Required field</label>
</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">
<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>
</div>
</div>
</div>
</div>
</div>
</template>
<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">
<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>
<i class="fas fa-trash-alt"></i>
</button>
</div>
</template>