forked from UKSOURCE/cms.hailearning.edu.vn
484 lines
21 KiB
Plaintext
484 lines
21 KiB
Plaintext
<!-- Floating Contact Tab -->
|
|
<div class="tab-pane fade" id="floatingcontact" role="tabpanel">
|
|
<div class="row g-4">
|
|
<div class="col-md-12">
|
|
<div class="card border shadow-sm">
|
|
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
|
<h6 class="mb-0">
|
|
<i class="fas fa-sliders-h me-2"></i>Widget Settings
|
|
</h6>
|
|
<span class="badge bg-light text-dark border">Homepage floating contact widget</span>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row g-4 align-items-start">
|
|
<div class="col-12">
|
|
<div class="d-flex flex-column flex-lg-row justify-content-between align-items-lg-center gap-3 p-3 border rounded-3 bg-light-subtle">
|
|
<div>
|
|
<div class="fw-semibold">Widget visibility</div>
|
|
<small class="text-muted">Enable or disable the floating contact widget on the homepage.</small>
|
|
</div>
|
|
<div class="form-check form-switch m-0">
|
|
<input class="form-check-input" type="checkbox" id="floatingContactEnabled"
|
|
<%= data.floatingContact?.enabled !== false ? 'checked' : '' %> />
|
|
<label class="form-check-label fw-medium" for="floatingContactEnabled">
|
|
Enable floating widget
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-lg-8">
|
|
<label class="form-label fw-medium">Panel Title</label>
|
|
<input type="text" class="form-control" id="floatingContactPanelTitle"
|
|
value="<%= data.floatingContact?.panelTitle || '' %>"
|
|
placeholder="How can we help you today?"
|
|
maxlength="72"
|
|
data-maxlength="72" />
|
|
<small class="text-muted">Maximum 72 characters to keep the header from breaking the widget layout.</small>
|
|
</div>
|
|
<div class="col-lg-4">
|
|
<label class="form-label fw-medium">Brand Alt Text</label>
|
|
<input type="text" class="form-control" id="floatingContactBrandAlt"
|
|
value="<%= data.floatingContact?.brand?.imageAlt || 'HAI Learning' %>" placeholder="HAI Learning"
|
|
maxlength="60"
|
|
data-maxlength="60" />
|
|
<small class="text-muted d-block mt-2">Used for accessibility and fallback image descriptions.</small>
|
|
</div>
|
|
|
|
<div class="col-lg-6">
|
|
<label class="form-label fw-medium">Brand Image</label>
|
|
<div class="input-group mb-2">
|
|
<input type="text" class="form-control" id="floatingContactBrandImage"
|
|
value="<%= data.floatingContact?.brand?.imageSrc || '' %>" placeholder="/assets/img/logo/black-logo.svg"
|
|
maxlength="255" data-maxlength="255" />
|
|
<button type="button" class="btn btn-outline-primary btn-upload-image"
|
|
data-target-input="floatingContactBrandImage" data-image-type="home/floating-contact"
|
|
data-resize-preset="floatingContactBrandImage">
|
|
<i class="fas fa-upload me-1"></i>Upload
|
|
</button>
|
|
</div>
|
|
<div class="mt-2">
|
|
<img
|
|
src="<%= data.floatingContact?.brand?.imageSrc ? getFullImageUrl(data.floatingContact.brand.imageSrc, backendUrl) : '' %>"
|
|
class="img-thumbnail <%= data.floatingContact?.brand?.imageSrc ? '' : 'd-none' %>"
|
|
id="floatingContactBrandPreview"
|
|
style="height: 120px; width: 120px; object-fit: contain; background: #fff;"
|
|
alt="Brand preview" />
|
|
</div>
|
|
<small class="text-muted d-block mt-2">Displayed at roughly 35x35px. Raster uploads are normalized to 104x104 WebP to match the homepage widget.</small>
|
|
</div>
|
|
<div class="col-lg-6">
|
|
<label class="form-label fw-medium">Fallback Trigger Image</label>
|
|
<div class="input-group mb-2">
|
|
<input
|
|
type="text"
|
|
class="form-control"
|
|
id="floatingContactTriggerImage"
|
|
value="<%= data.floatingContact?.trigger?.imageSrc || '' %>"
|
|
placeholder="/uploads/home/floating-contact/floating-trigger-icon.webp"
|
|
maxlength="255" data-maxlength="255" />
|
|
<button
|
|
type="button"
|
|
class="btn btn-outline-primary btn-upload-image"
|
|
data-target-input="floatingContactTriggerImage"
|
|
data-image-type="home/floating-contact"
|
|
data-resize-preset="floatingContactTriggerIcon">
|
|
<i class="fas fa-upload me-1"></i>Upload
|
|
</button>
|
|
</div>
|
|
<div class="mt-2">
|
|
<img
|
|
src="<%= data.floatingContact?.trigger?.imageSrc ? getFullImageUrl(data.floatingContact.trigger.imageSrc, backendUrl) : '' %>"
|
|
class="img-thumbnail <%= data.floatingContact?.trigger?.imageSrc ? '' : 'd-none' %>"
|
|
id="floatingContactTriggerPreview"
|
|
style="height: 120px; width: 120px; object-fit: contain; background: #fff;"
|
|
alt="Trigger icon preview" />
|
|
</div>
|
|
<input type="hidden" id="floatingContactTriggerIconFallback"
|
|
value="<%= data.floatingContact?.trigger?.icon || 'fa-comments' %>" />
|
|
<small class="text-muted d-block mt-2">Displayed at roughly 26x26px. Shown only when the trigger slideshow cannot use the brand image and action icons. Raster uploads are normalized to 96x96 WebP.</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-12">
|
|
<div class="card border shadow-sm">
|
|
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<h6 class="mb-0">
|
|
<i class="fas fa-list me-2"></i>Contact Actions
|
|
</h6>
|
|
<small class="text-muted">Add, remove, and drag to reorder floating contact actions.</small>
|
|
</div>
|
|
<button type="button" class="btn btn-sm btn-primary" id="addFloatingContactActionBtn">
|
|
<i class="fas fa-plus me-1"></i>Add Action
|
|
</button>
|
|
</div>
|
|
<div class="card-body">
|
|
<div id="floatingContactActionsContainer" class="d-grid gap-3"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
id="floatingContactConfig"
|
|
data-cms-base-url="<%= backendUrl.replace(/\/$/, '') %>"
|
|
hidden
|
|
></div>
|
|
<script id="floatingContactInitialData" type="application/json"><%- JSON.stringify(data.floatingContact || {}) %></script>
|
|
<script>
|
|
window.homeScrapers = window.homeScrapers || {};
|
|
const floatingContactConfig = document.getElementById("floatingContactConfig");
|
|
const floatingContactCmsBaseUrl = floatingContactConfig?.dataset.cmsBaseUrl || "";
|
|
|
|
const normalizeFloatingContactPublicPath = (value) => {
|
|
const raw = (value || "").trim();
|
|
if (!raw) {
|
|
return "";
|
|
}
|
|
|
|
const knownPrefixes = ["/uploads/", "/assets/", "/img/"];
|
|
|
|
for (const prefix of knownPrefixes) {
|
|
const absolutePrefix = `${floatingContactCmsBaseUrl}${prefix}`;
|
|
if (raw.startsWith(absolutePrefix)) {
|
|
return raw.slice(floatingContactCmsBaseUrl.length);
|
|
}
|
|
}
|
|
|
|
for (const prefix of knownPrefixes) {
|
|
const index = raw.indexOf(prefix);
|
|
if (index >= 0) {
|
|
return raw.slice(index);
|
|
}
|
|
}
|
|
|
|
if (raw.startsWith("/")) {
|
|
return raw;
|
|
}
|
|
|
|
return `/${raw}`;
|
|
};
|
|
|
|
const resolveFloatingContactImageUrl = (value) => {
|
|
const normalized = normalizeFloatingContactPublicPath(value);
|
|
return normalized ? `${floatingContactCmsBaseUrl}${normalized}` : "";
|
|
};
|
|
|
|
const escapeFloatingContactHtml = (value) =>
|
|
String(value || "")
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'");
|
|
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
const initialDataElement = document.getElementById("floatingContactInitialData");
|
|
const initialData = initialDataElement?.textContent
|
|
? JSON.parse(initialDataElement.textContent)
|
|
: {};
|
|
const container = document.getElementById("floatingContactActionsContainer");
|
|
const addBtn = document.getElementById("addFloatingContactActionBtn");
|
|
const brandInput = document.getElementById("floatingContactBrandImage");
|
|
const brandPreview = document.getElementById("floatingContactBrandPreview");
|
|
const triggerInput = document.getElementById("floatingContactTriggerImage");
|
|
const triggerPreview = document.getElementById("floatingContactTriggerPreview");
|
|
|
|
if (!container || !addBtn) {
|
|
return;
|
|
}
|
|
|
|
const createActionId = () => `floating-action-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
|
|
const getActionDefaults = (platform) => {
|
|
if (platform === "facebook") {
|
|
return {
|
|
platform: "facebook",
|
|
enabled: true,
|
|
label: "Message us on Facebook",
|
|
subtitle: "facebook.com/hailearning.edu.vn",
|
|
href: "https://www.facebook.com/hailearning.edu.vn/",
|
|
iconImage: "",
|
|
iconType: "iconClass",
|
|
iconClass: "fa-brands fa-facebook-messenger",
|
|
iconText: "",
|
|
};
|
|
}
|
|
|
|
if (platform === "zalo") {
|
|
return {
|
|
platform: "zalo",
|
|
enabled: true,
|
|
label: "Message us on Zalo",
|
|
subtitle: "zalo.me/84961834040",
|
|
href: "https://zalo.me/84961834040",
|
|
iconImage: "",
|
|
iconType: "iconText",
|
|
iconClass: "",
|
|
iconText: "Zalo",
|
|
};
|
|
}
|
|
|
|
return {
|
|
platform: "custom",
|
|
enabled: true,
|
|
label: "",
|
|
subtitle: "",
|
|
href: "",
|
|
iconImage: "",
|
|
iconType: "iconClass",
|
|
iconClass: "fa-solid fa-comment-dots",
|
|
iconText: "",
|
|
};
|
|
};
|
|
|
|
const bindImageField = (input, preview) => {
|
|
if (!input || !preview) {
|
|
return;
|
|
}
|
|
|
|
input.value = normalizeFloatingContactPublicPath(input.value);
|
|
if (input.value) {
|
|
preview.src = resolveFloatingContactImageUrl(input.value);
|
|
preview.classList.remove("d-none");
|
|
}
|
|
|
|
if (input.dataset.previewBound === "true") {
|
|
return;
|
|
}
|
|
|
|
input.addEventListener("input", () => {
|
|
const value = normalizeFloatingContactPublicPath(input.value);
|
|
input.value = value;
|
|
|
|
if (!value) {
|
|
preview.classList.add("d-none");
|
|
preview.removeAttribute("src");
|
|
return;
|
|
}
|
|
|
|
preview.src = resolveFloatingContactImageUrl(value);
|
|
preview.classList.remove("d-none");
|
|
});
|
|
input.dataset.previewBound = "true";
|
|
};
|
|
|
|
const createActionCard = (action = {}) => {
|
|
const defaults = {
|
|
id: createActionId(),
|
|
...getActionDefaults(action.platform || "custom"),
|
|
...action,
|
|
};
|
|
|
|
const normalizedIconImage = normalizeFloatingContactPublicPath(defaults.iconImage || "");
|
|
const iconInputId = `floatingContactActionIconImage-${defaults.id}`;
|
|
const iconPreviewId = `${iconInputId}-preview`;
|
|
|
|
const wrapper = document.createElement("div");
|
|
wrapper.className = "card border floating-contact-action-item";
|
|
wrapper.innerHTML = `
|
|
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
|
<div class="d-flex align-items-center gap-2">
|
|
<button type="button" class="btn btn-sm btn-outline-secondary floating-contact-action-handle" title="Drag to reorder">
|
|
<i class="fas fa-grip-vertical"></i>
|
|
</button>
|
|
<strong class="floating-contact-action-title">Action</strong>
|
|
</div>
|
|
<button type="button" class="btn btn-sm btn-outline-danger floating-contact-remove-action">
|
|
<i class="fas fa-trash-alt me-1"></i>Remove
|
|
</button>
|
|
</div>
|
|
<div class="card-body">
|
|
<input type="hidden" class="floating-contact-action-id" value="${escapeFloatingContactHtml(defaults.id)}" />
|
|
<input type="hidden" class="floating-contact-action-legacy-icon-type" value="${escapeFloatingContactHtml(defaults.iconType)}" />
|
|
<input type="hidden" class="floating-contact-action-legacy-icon-class" value="${escapeFloatingContactHtml(defaults.iconClass)}" />
|
|
<input type="hidden" class="floating-contact-action-legacy-icon-text" value="${escapeFloatingContactHtml(defaults.iconText)}" />
|
|
<div class="row g-3 align-items-start">
|
|
<div class="col-lg-3 col-md-6">
|
|
<label class="form-label fw-medium">Platform</label>
|
|
<select class="form-select floating-contact-action-platform">
|
|
<option value="facebook" ${defaults.platform === "facebook" ? "selected" : ""}>Facebook</option>
|
|
<option value="zalo" ${defaults.platform === "zalo" ? "selected" : ""}>Zalo</option>
|
|
<option value="custom" ${defaults.platform === "custom" ? "selected" : ""}>Custom</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-lg-5 col-md-6">
|
|
<label class="form-label fw-medium">Label</label>
|
|
<input type="text" class="form-control floating-contact-action-label" value="${escapeFloatingContactHtml(defaults.label)}" placeholder="Message us on Facebook" maxlength="48" data-maxlength="48" />
|
|
</div>
|
|
<div class="col-lg-4 col-md-8">
|
|
<label class="form-label fw-medium">Subtitle</label>
|
|
<input type="text" class="form-control floating-contact-action-subtitle" value="${escapeFloatingContactHtml(defaults.subtitle)}" placeholder="facebook.com/hailearning.edu.vn" maxlength="48" data-maxlength="48" />
|
|
</div>
|
|
<div class="col-lg-3 col-md-4">
|
|
<label class="form-label fw-medium d-block">Status</label>
|
|
<div class="form-check form-switch mt-2">
|
|
<input class="form-check-input floating-contact-action-enabled" type="checkbox" ${defaults.enabled !== false ? "checked" : ""}>
|
|
<label class="form-check-label fw-medium">Enabled</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-lg-9 col-md-8">
|
|
<label class="form-label fw-medium">Link</label>
|
|
<input type="text" class="form-control floating-contact-action-href" value="${escapeFloatingContactHtml(defaults.href)}" placeholder="https://example.com" maxlength="255" data-maxlength="255" />
|
|
</div>
|
|
<div class="col-12">
|
|
<label class="form-label fw-medium">Icon Image</label>
|
|
<div class="input-group mb-2">
|
|
<input type="text" class="form-control floating-contact-action-icon-image" id="${escapeFloatingContactHtml(iconInputId)}" value="${escapeFloatingContactHtml(normalizedIconImage)}" placeholder="/uploads/home/floating-contact/floating-action-icon.webp" maxlength="255" data-maxlength="255" />
|
|
<button
|
|
type="button"
|
|
class="btn btn-outline-primary btn-upload-image"
|
|
data-target-input="${escapeFloatingContactHtml(iconInputId)}"
|
|
data-image-type="home/floating-contact"
|
|
data-resize-preset="floatingContactActionIcon">
|
|
<i class="fas fa-upload me-1"></i>Upload
|
|
</button>
|
|
</div>
|
|
<div class="mt-2">
|
|
<img
|
|
src="${normalizedIconImage ? escapeFloatingContactHtml(resolveFloatingContactImageUrl(normalizedIconImage)) : ""}"
|
|
class="img-thumbnail floating-contact-action-icon-preview ${normalizedIconImage ? "" : "d-none"}"
|
|
id="${escapeFloatingContactHtml(iconPreviewId)}"
|
|
style="height: 120px; width: 120px; object-fit: contain; background: #fff;"
|
|
alt="Action icon preview" />
|
|
</div>
|
|
<small class="text-muted d-block mt-2">Raster uploads are normalized to 84x84 WebP. SVG files remain unchanged.</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
return wrapper;
|
|
};
|
|
|
|
const updateActionCardTitles = () => {
|
|
container.querySelectorAll(".floating-contact-action-item").forEach((item, index) => {
|
|
const title = item.querySelector(".floating-contact-action-title");
|
|
if (title) {
|
|
title.textContent = `Action ${index + 1}`;
|
|
}
|
|
});
|
|
};
|
|
|
|
const bindActionCard = (card) => {
|
|
if (typeof initHomeCharacterCounters === "function") {
|
|
initHomeCharacterCounters(card);
|
|
}
|
|
bindImageField(
|
|
card.querySelector(".floating-contact-action-icon-image"),
|
|
card.querySelector(".floating-contact-action-icon-preview"),
|
|
);
|
|
};
|
|
|
|
const maybeApplyPlatformDefaults = (item) => {
|
|
const platform = item.querySelector(".floating-contact-action-platform")?.value || "custom";
|
|
const defaults = getActionDefaults(platform);
|
|
|
|
item.querySelector(".floating-contact-action-label").value =
|
|
item.querySelector(".floating-contact-action-label").value || defaults.label;
|
|
item.querySelector(".floating-contact-action-subtitle").value =
|
|
item.querySelector(".floating-contact-action-subtitle").value || defaults.subtitle;
|
|
item.querySelector(".floating-contact-action-href").value =
|
|
item.querySelector(".floating-contact-action-href").value || defaults.href;
|
|
item.querySelector(".floating-contact-action-legacy-icon-type").value = defaults.iconType;
|
|
item.querySelector(".floating-contact-action-legacy-icon-class").value = defaults.iconClass;
|
|
item.querySelector(".floating-contact-action-legacy-icon-text").value = defaults.iconText;
|
|
};
|
|
|
|
const renderInitialActions = () => {
|
|
const actions = Array.isArray(initialData.actions) && initialData.actions.length > 0
|
|
? initialData.actions
|
|
: [getActionDefaults("facebook"), getActionDefaults("zalo")];
|
|
|
|
actions.forEach((action) => {
|
|
const card = createActionCard(action);
|
|
container.appendChild(card);
|
|
bindActionCard(card);
|
|
});
|
|
|
|
updateActionCardTitles();
|
|
};
|
|
|
|
addBtn.addEventListener("click", () => {
|
|
const card = createActionCard(getActionDefaults("custom"));
|
|
container.appendChild(card);
|
|
bindActionCard(card);
|
|
updateActionCardTitles();
|
|
});
|
|
|
|
container.addEventListener("click", (event) => {
|
|
const removeBtn = event.target.closest(".floating-contact-remove-action");
|
|
if (!removeBtn) {
|
|
return;
|
|
}
|
|
|
|
const card = removeBtn.closest(".floating-contact-action-item");
|
|
if (!card) {
|
|
return;
|
|
}
|
|
|
|
card.remove();
|
|
updateActionCardTitles();
|
|
});
|
|
|
|
container.addEventListener("change", (event) => {
|
|
const card = event.target.closest(".floating-contact-action-item");
|
|
if (!card) {
|
|
return;
|
|
}
|
|
|
|
if (event.target.matches(".floating-contact-action-platform")) {
|
|
maybeApplyPlatformDefaults(card);
|
|
}
|
|
});
|
|
|
|
bindImageField(brandInput, brandPreview);
|
|
bindImageField(triggerInput, triggerPreview);
|
|
|
|
if (window.Sortable) {
|
|
window.Sortable.create(container, {
|
|
animation: 150,
|
|
handle: ".floating-contact-action-handle",
|
|
onEnd: updateActionCardTitles,
|
|
});
|
|
}
|
|
|
|
renderInitialActions();
|
|
});
|
|
|
|
window.homeScrapers.floatingContact = () => {
|
|
const actions = Array.from(document.querySelectorAll(".floating-contact-action-item")).map((item, index) => ({
|
|
id: item.querySelector(".floating-contact-action-id")?.value || "",
|
|
platform: item.querySelector(".floating-contact-action-platform")?.value || "custom",
|
|
enabled: !!item.querySelector(".floating-contact-action-enabled")?.checked,
|
|
label: item.querySelector(".floating-contact-action-label")?.value?.trim() || "",
|
|
subtitle: item.querySelector(".floating-contact-action-subtitle")?.value?.trim() || "",
|
|
href: item.querySelector(".floating-contact-action-href")?.value?.trim() || "",
|
|
iconImage: normalizeFloatingContactPublicPath(item.querySelector(".floating-contact-action-icon-image")?.value?.trim() || ""),
|
|
iconType: item.querySelector(".floating-contact-action-legacy-icon-type")?.value || "iconClass",
|
|
iconClass: item.querySelector(".floating-contact-action-legacy-icon-class")?.value?.trim() || "",
|
|
iconText: item.querySelector(".floating-contact-action-legacy-icon-text")?.value?.trim() || "",
|
|
order: index + 1,
|
|
}));
|
|
|
|
return {
|
|
enabled: !!document.getElementById("floatingContactEnabled")?.checked,
|
|
panelTitle: document.getElementById("floatingContactPanelTitle")?.value?.trim() || "",
|
|
brand: {
|
|
imageSrc: normalizeFloatingContactPublicPath(document.getElementById("floatingContactBrandImage")?.value?.trim() || ""),
|
|
imageAlt: document.getElementById("floatingContactBrandAlt")?.value?.trim() || "",
|
|
},
|
|
trigger: {
|
|
imageSrc: normalizeFloatingContactPublicPath(document.getElementById("floatingContactTriggerImage")?.value?.trim() || ""),
|
|
icon: document.getElementById("floatingContactTriggerIconFallback")?.value?.trim() || "fa-comments",
|
|
},
|
|
actions,
|
|
};
|
|
};
|
|
</script>
|