feat: Improve home and contact CMS field guidance

This commit is contained in:
Tống Thành Đạt
2026-04-10 01:38:30 +07:00
parent ed09c7fa89
commit 7ce5921fe0
15 changed files with 529 additions and 230 deletions

View File

@@ -189,6 +189,8 @@
// Khởi tạo các nút upload ảnh (dùng chung cho toàn bộ các section)
initImageUploads();
initHomeCharacterCounters(document);
initImagePreviewFallbacks(document);
});
// --- UTILITIES (Dùng chung) ---
@@ -223,6 +225,43 @@
);
}
function clearImagePreviewError(previewImg) {
if (!previewImg?.parentElement) {
return;
}
previewImg.parentElement.querySelectorAll(".image-preview-missing").forEach((node) => node.remove());
}
function bindImagePreviewFallback(previewImg) {
if (!previewImg || previewImg.dataset.previewFallbackBound === "true") {
return;
}
previewImg.dataset.previewFallbackBound = "true";
previewImg.addEventListener("error", () => {
previewImg.classList.add("d-none");
previewImg.removeAttribute("src");
if (!previewImg.parentElement?.querySelector(".image-preview-missing")) {
const note = document.createElement("small");
note.className = "text-warning d-block mt-2 image-preview-missing";
note.textContent = "Preview unavailable: current image path could not be loaded.";
previewImg.parentElement?.appendChild(note);
}
});
}
function initImagePreviewFallbacks(scope = document) {
scope.querySelectorAll("img.img-thumbnail").forEach((previewImg) => {
bindImagePreviewFallback(previewImg);
if (previewImg.complete && previewImg.getAttribute("src") && previewImg.naturalWidth === 0) {
previewImg.dispatchEvent(new Event("error"));
}
});
}
function revokePendingPreview(targetInput) {
const previewUrl = pendingPreviewUrls.get(targetInput);
if (previewUrl) {
@@ -307,6 +346,8 @@
const previewImg = findImagePreview(input);
if (previewImg) {
clearImagePreviewError(previewImg);
bindImagePreviewFallback(previewImg);
previewImg.src = new URL(result.path, window.location.origin).toString();
previewImg.classList.remove("d-none");
}
@@ -364,6 +405,8 @@
const previewImg = findImagePreview(input);
if (previewImg) {
clearImagePreviewError(previewImg);
bindImagePreviewFallback(previewImg);
previewImg.src = previewUrl;
previewImg.classList.remove("d-none");
}
@@ -402,4 +445,93 @@
new bootstrap.Toast(toast, { autohide: true, delay: 3000 }).show();
toast.addEventListener("hidden.bs.toast", () => toast.remove());
}
function ensureCharacterHint(input) {
if (!input || (!input.dataset.maxlength && !input.dataset.maxwords)) {
return null;
}
if (!input.id) {
input.id = `homeField_${Math.random().toString(36).slice(2, 10)}`;
}
const field = input.closest(".col-md-12, .col-md-9, .col-md-6, .col-md-4, .col-md-3, .col-lg-12, .col-lg-8, .col-lg-6, .col-lg-4, .col-12") || input.parentElement;
const anchor = input.closest(".input-group") || input;
const parent = anchor?.parentElement || field;
if (!field || !anchor || !parent) {
return null;
}
let hint = field.querySelector(`[data-counter-for="${input.id}"]`);
if (!hint) {
hint = document.createElement("small");
hint.className = "form-text home-limit-counter text-secondary";
hint.dataset.counterFor = input.id;
}
if (hint.previousElementSibling !== anchor) {
parent.insertBefore(hint, anchor.nextSibling);
}
return hint;
}
function updateCharacterHint(input) {
const hint = ensureCharacterHint(input);
if (!hint) {
return;
}
const hasWordLimit = Boolean(input.dataset.maxwords);
const hasCharLimit = Boolean(input.dataset.maxlength);
if (hasWordLimit) {
const maxWords = Number(input.dataset.maxwords);
const normalized = (input.value || "").replace(/\s+/g, " ").trim();
const words = normalized ? normalized.split(" ") : [];
if (Number.isFinite(maxWords) && maxWords > 0 && words.length > maxWords) {
input.value = words.slice(0, maxWords).join(" ");
} else if (normalized !== input.value) {
input.value = normalized;
}
}
if (hasCharLimit) {
const max = Number(input.dataset.maxlength);
if (Number.isFinite(max) && max > 0 && (input.value || "").length > max) {
input.value = (input.value || "").slice(0, max);
}
}
if (hasWordLimit) {
const maxWords = Number(input.dataset.maxwords);
const currentWords = input.value ? input.value.split(" ").filter(Boolean).length : 0;
const currentLength = (input.value || "").length;
const maxLength = Number(input.dataset.maxlength);
hint.textContent = hasCharLimit
? `${currentWords}/${maxWords} words, ${currentLength}/${maxLength} characters`
: `${currentWords}/${maxWords} words`;
hint.classList.toggle("text-danger", currentWords >= maxWords || (hasCharLimit && currentLength >= maxLength));
return;
}
const max = Number(input.dataset.maxlength);
const length = (input.value || "").length;
hint.textContent = `${length}/${max} characters`;
hint.classList.toggle("text-danger", length >= max);
}
function initHomeCharacterCounters(scope = document) {
scope.querySelectorAll("input[data-maxlength], textarea[data-maxlength], input[data-maxwords], textarea[data-maxwords]").forEach((input) => {
updateCharacterHint(input);
if (input.dataset.counterBound === "true") {
return;
}
input.dataset.counterBound = "true";
input.addEventListener("input", () => updateCharacterHint(input));
});
}
</script>