Files
cms.uldp.edu.vn/public/js/admin-form-helpers.js
2026-04-10 15:55:15 +07:00

868 lines
30 KiB
JavaScript

;(function (window, document) {
"use strict";
const COUNTER_SELECTOR = ".admin-field-counter";
const GUIDANCE_SELECTOR = ".admin-upload-guidance";
const COUNTER_BOUND_KEY = "adminCounterBound";
const AUTO_GUIDANCE_ATTR = "data-admin-upload-guidance";
const OBSERVER_BOUND_KEY = "__adminFormHelpersObserver";
let generatedFieldToken = 0;
const FIELD_RULES = {
"/admin/about-us": [
{ selector: "#heroTitle", maxLength: 72 },
{ selector: "#heroBreadcrumb", maxLength: 120 },
{ selector: "#heroBackgroundImage", maxLength: 255 },
{ selector: "#introSubheading", maxLength: 40 },
{ selector: "#introHeading", maxLength: 72 },
{ selector: "#introDescription", maxLength: 260 },
{ selector: "#introImage", maxLength: 255 },
{ selector: "#missionSubheading", maxLength: 40 },
{ selector: "#missionHeading", maxLength: 72 },
{ selector: "#missionDescription", maxLength: 260 },
{ selector: "#missionCtaLabel", maxLength: 32 },
{ selector: "#missionCtaHref", maxLength: 255 },
{ selector: "[id^='missionImg_']", maxLength: 255 },
{ selector: "#featuresSubheading", maxLength: 40 },
{ selector: "#featuresHeading", maxLength: 72 },
{ selector: "#featuresDescription", maxLength: 260 },
{ selector: "#featuresBgImage", maxLength: 255 },
{ selector: "#featuresImage", maxLength: 255 },
{ selector: "#featuresCtaLabel", maxLength: 32 },
{ selector: "#featuresCtaHref", maxLength: 255 },
{ selector: "[id^='missionItemLabel_']", maxLength: 48 },
{ selector: "[id^='missionItemDescription_']", maxLength: 160 },
{ selector: "[id^='missionItemIcon_']", maxLength: 255 },
{ selector: "[id^='missionFeature_']", maxLength: 96 },
{ selector: "[id^='featureItemTitle_']", maxLength: 48 },
{ selector: "[id^='featureItemDescription_']", maxLength: 160 },
{ selector: "[id^='featureItemIcon_']", maxLength: 255 },
{ selector: "#newsSubheading", maxLength: 40 },
{ selector: "#newsHeading", maxLength: 72 },
{ selector: "#newsCtaLabel", maxLength: 32 },
{ selector: "#newsCtaHref", maxLength: 255 },
],
"/admin/booking": [
{ selector: "#heroBackgroundImage", maxLength: 255 },
{ selector: "#heroTitle", maxLength: 72 },
{ selector: "#searchBarLocationLabel", maxLength: 32 },
{ selector: "#searchBarHolidaySeasonLabel", maxLength: 32 },
{ selector: "#searchBarSearchButtonText", maxLength: 24 },
{ selector: "input[name^='locationValue_']", maxLength: 32 },
{ selector: "input[name^='locationLabel_']", maxLength: 48 },
{ selector: "input[name^='holidayValue_']", maxLength: 32 },
{ selector: "input[name^='holidayLabel_']", maxLength: 48 },
{ selector: "#filterPanelTitle", maxLength: 48 },
{ selector: "#filterPanelPriceTitle", maxLength: 40 },
{ selector: "#filterPanelPriceLabel", maxLength: 32 },
{ selector: "#filterPanelPricePlaceholder", maxLength: 32 },
{ selector: "#filterPanelActivitiesTitle", maxLength: 40 },
{ selector: "#filterPanelAgeTitle", maxLength: 40 },
{ selector: "#filterPanelAgeSelectPlaceholder", maxLength: 32 },
{ selector: "#filterPanelRatingTitle", maxLength: 40 },
{ selector: "#filterPanelResetButtonText", maxLength: 24 },
{ selector: "input[name^='ratingValue_']", maxLength: 8 },
{ selector: "input[name^='ratingLabel_']", maxLength: 32 },
{ selector: "input[name^='programValue_']", maxLength: 32 },
{ selector: "input[name^='programLabel_']", maxLength: 48 },
{ selector: "input[name^='campName_']", maxLength: 72 },
{ selector: "input[name^='campPriceText_']", maxLength: 32 },
{ selector: "input[name^='campProgram_']", maxLength: 32 },
{ selector: "input[name^='campImage_']", maxLength: 255 },
{ selector: "input[name^='campLink_']", maxLength: 255 },
{ selector: "input[name^='discountName_']", maxLength: 48 },
{ selector: "textarea[name^='discountDescription_']", maxLength: 180 },
{ selector: "input[name^='voucherCode_']", maxLength: 24 },
{ selector: "input[name^='voucherDescription_']", maxLength: 120 },
{ selector: "[name='formTitle']", maxLength: 64 },
{ selector: "[name='formSubtitle']", maxLength: 48 },
{ selector: "[name^='stepTitle_']", maxLength: 64 },
{ selector: "[name^='sectionTitle_']", maxLength: 64 },
{ selector: "[name^='fieldLabel_']", maxLength: 48 },
{ selector: "[name^='fieldName_']", maxLength: 32 },
{ selector: "[name^='fieldPlaceholder_']", maxLength: 72 },
{ selector: "[name^='validationMessage_']", maxLength: 120 },
],
"/admin/pricing": [
{ selector: "#heroBackgroundImage", maxLength: 255 },
{ selector: "#heroTitle", maxLength: 72 },
{ selector: "#pricingSectionSubtitle", maxLength: 40 },
{ selector: "#pricingSectionHeading", maxLength: 72 },
{ selector: "#pricingSectionDescription", maxLength: 220 },
{ selector: ".plan-name", maxLength: 40 },
{ selector: ".plan-price", maxLength: 16 },
{ selector: ".plan-currency", maxLength: 8 },
{ selector: ".plan-period", maxLength: 12 },
{ selector: ".plan-button-text", maxLength: 32 },
{ selector: ".plan-button-link", maxLength: 255 },
{ selector: ".plan-button-icon", maxLength: 64 },
{ selector: ".plan-features", maxLength: 320 },
{ selector: "#testimonialsSubtitle", maxLength: 40 },
{ selector: "#testimonialsHeading", maxLength: 72 },
{ selector: "#testimonialsButtonText", maxLength: 32 },
{ selector: "#testimonialsButtonLink", maxLength: 255 },
{ selector: "#testimonialsButtonIcon", maxLength: 64 },
{ selector: "#testimonialsImage", maxLength: 255 },
{ selector: ".testimonial-name", maxLength: 48 },
{ selector: ".testimonial-role", maxLength: 48 },
{ selector: ".testimonial-content", maxLength: 220 },
],
"/admin/visa": [
{ selector: "input[name='name']", maxLength: 40 },
{ selector: "input[name='icon']", maxLength: 255 },
{ selector: "input[name='services[]']", maxLength: 56 },
{ selector: "input[name='detail_title']", maxLength: 72 },
{ selector: "input[name='mainImage']", maxLength: 255 },
{ selector: "textarea[name='description']", maxLength: 360 },
{ selector: "textarea[name='additionalInfo']", maxLength: 360 },
{ selector: "input[name='tagline']", maxLength: 72 },
{ selector: "input[name^='visa_title_']", maxLength: 56 },
{ selector: "textarea[name^='visa_desc_']", maxLength: 220 },
{ selector: "input[name='process_title']", maxLength: 72 },
{ selector: "input[name='step_title[]']", maxLength: 56 },
{ selector: "textarea[name='step_desc[]']", maxLength: 180 },
{ selector: "input[name='bannerImageGallery']", maxLength: 255 },
{ selector: "input[name='category_title[]']", maxLength: 56 },
{ selector: "textarea[name='category_desc[]']", maxLength: 180 },
{ selector: "input[name='related_title[]']", maxLength: 56 },
{ selector: "textarea[name='related_desc[]']", maxLength: 180 },
{ selector: "input[name='related_file[]']", maxLength: 255 },
{ selector: "input[name='contact_title']", maxLength: 72 },
{ selector: "input[name='contact_phone']", maxLength: 32 },
{ selector: "input[name='contact_email']", maxLength: 120 },
{ selector: "input[name='contact_address']", maxLength: 160 },
{ selector: "input[name='contact_image']", maxLength: 255 },
],
"/admin/activity/*": [
{ selector: "input[name='heroTitle']", maxLength: 72 },
{ selector: "input[name='heroBannerImage']", maxLength: 255 },
{ selector: "input[name='name']", maxLength: 72 },
{ selector: "input[name='priceText']", maxLength: 32 },
{ selector: "input[name='link']", maxLength: 255 },
{ selector: "input[name='program']", maxLength: 32 },
{ selector: "#customLocations", maxLength: 120 },
{ selector: "input[name='image']", maxLength: 255 },
{ selector: "input[name='campDetailHeroTitle']", maxLength: 72 },
{ selector: "input[name='campDetailHeroBgImage']", maxLength: 255 },
{ selector: "input[name='campDetailBasicInfoLocation']", maxLength: 48 },
{ selector: "textarea[name='campDetailBasicInfoAgeRange']", maxLength: 120 },
{ selector: "input[name='campDetailBasicInfoAccommodationType']", maxLength: 72 },
{ selector: "input[name='campDetailBasicInfoCareLevel']", maxLength: 72 },
{ selector: "input[name='campDetailBasicInfoLanguages']", maxLength: 72 },
],
"/admin/activity": [],
};
const GUIDANCE_RULES = {
"/admin/about-us": [
{
selector: "#heroBackgroundImage",
title: "Upload guidance",
lines: [
"Displayed as a wide page hero.",
"Recommended upload: at least 1920x700px.",
],
},
{
selector: "#introImage",
title: "Upload guidance",
lines: [
"Displayed around 596x787px on desktop.",
"Recommended upload: at least 1200x1600px.",
],
},
{
selector: "#featuresImage",
title: "Upload guidance",
lines: [
"Displayed around 375x419px on desktop.",
"Recommended upload: at least 750x840px.",
],
},
],
"/admin/booking": [
{
selector: "#heroBackgroundImage",
title: "Upload guidance",
lines: [
"Booking page hero background.",
"Recommended upload: at least 1920x700px.",
],
},
{
selector: "input[name^='campImage_']",
title: "Upload guidance",
lines: [
"Used in booking camp cards.",
"Recommended upload: a landscape image at 704x432px or larger.",
],
},
],
"/admin/pricing": [
{
selector: "#heroBackgroundImage",
title: "Upload guidance",
lines: [
"Pricing page hero background.",
"Recommended upload: at least 1920x700px.",
],
},
],
"/admin/visa": [
{
selector: "input[name='icon']",
title: "Upload guidance",
lines: [
"Displayed as a small country flag or icon.",
"Prefer SVG; otherwise use a square image at 96x96px or larger.",
],
},
{
selector: "input[name='mainImage'], input[name='bannerImageGallery'], input[name='contact_image'], input[name='related_file[]']",
title: "Upload guidance",
lines: [
"Used in visa detail content blocks.",
"Recommended upload: at least 1000x750px for primary imagery and 800x600px for supporting images.",
],
},
],
"/admin/activity/*": [
{
selector: "input[name='heroBannerImage'], input[name='campDetailHeroBgImage']",
title: "Upload guidance",
lines: [
"Activity page hero-style image.",
"Recommended upload: at least 1920x700px.",
],
},
{
selector: "input[name='image']",
title: "Upload guidance",
lines: [
"Used in activity listing cards.",
"Recommended upload: a landscape image at 704x432px or larger.",
],
},
],
"/admin/home": [
{
selector: "#whyChooseUsMainImage",
title: "Upload guidance",
lines: [
"Displayed around 318x347px on desktop.",
"Recommended upload: at least 750x820px.",
],
},
{
selector: "#whyChooseUsSecondaryImage",
title: "Upload guidance",
lines: [
"Displayed around 363x380px on desktop.",
"Recommended upload: at least 760x800px.",
],
},
{
selector: "#testimonialsVideoThumbnail",
title: "Upload guidance",
lines: [
"Displayed around 416x370px on desktop.",
"Recommended upload: at least 832x740px.",
],
},
{
selector: "[id^='testimonialsAvatar_']",
title: "Upload guidance",
lines: [
"Displayed around 48x48px.",
"Recommended upload: 96x96px or 128x128px square.",
],
},
{
selector: "#visaCountriesFlag_0",
title: "Upload guidance",
lines: [
"Displayed around 840x830px on desktop.",
"Recommended upload: at least 1000x1000px.",
],
},
],
"*": [
{
selector: ".btn-upload-image",
title: "Upload guidance",
lines: [
"Use a clear, high-resolution image that matches the visible frame.",
"Prefer SVG for logos or icons and at least 2x the displayed size for raster uploads.",
],
},
],
};
function toScope(scope) {
return scope && scope.querySelectorAll ? scope : document;
}
function normalizeText(value) {
return String(value ?? "").replace(/\s+/g, " ").trim();
}
function buildDescriptor(input) {
return [
input?.id,
input?.name,
input?.placeholder,
input?.closest(".col-md-12, .col-md-9, .col-md-6, .col-md-5, .col-md-4, .col-md-3, .col-md-2, .col-lg-12, .col-lg-8, .col-lg-6, .col-lg-4, .col-12")?.querySelector("label")?.textContent,
]
.filter(Boolean)
.join(" ")
.replace(/([a-z])([A-Z])/g, "$1 $2")
.replace(/[_[\].-]+/g, " ")
.toLowerCase();
}
function isUploadDescriptor(descriptor) {
return /(?:^|[^a-z])(image|icon|logo|background|banner|thumbnail|avatar|flag|path|src|file)(?:[^a-z]|$)/.test(descriptor);
}
function resolveUploadTarget(targetId, anchor) {
if (targetId) {
const byId = document.getElementById(targetId);
if (byId) {
return byId;
}
if (window.CSS && typeof window.CSS.escape === "function") {
const byName = document.querySelector(`[name="${window.CSS.escape(targetId)}"]`);
if (byName) {
return byName;
}
}
}
return anchor || null;
}
function hasManualUploadHint(target) {
const anchor = resolveGuidanceAnchor(target);
if (!anchor) {
return false;
}
const host =
anchor.closest(".form-group, .mb-3, .col-md-12, .col-md-9, .col-md-6, .col-md-5, .col-md-4, .col-md-3, .col-md-2, .col-lg-12, .col-lg-8, .col-lg-6, .col-lg-4, .col-12") ||
anchor.parentElement;
if (!host) {
return false;
}
const candidates = [
...host.querySelectorAll("small.text-muted, small.form-text, .form-text, .text-muted"),
...Array.from(host.nextElementSibling ? host.nextElementSibling.querySelectorAll?.("small.text-muted, small.form-text, .form-text, .text-muted") || [] : []),
];
return candidates.some((node) => {
if (node.classList?.contains("admin-field-counter") || node.classList?.contains("admin-upload-guidance")) {
return false;
}
const text = normalizeText(node.textContent || "");
return /recommended|min(imum)? upload|upload|svg|png|webp|render|displayed|size|preview|icon/i.test(text);
});
}
function getFieldLimit(input) {
const dataMax = Number(input?.dataset?.maxlength);
if (Number.isFinite(dataMax) && dataMax > 0) {
return dataMax;
}
const attrMax = Number(input?.getAttribute("maxlength"));
if (Number.isFinite(attrMax) && attrMax > 0) {
return attrMax;
}
return null;
}
function getWordLimit(input) {
const dataMax = Number(input?.dataset?.maxwords);
if (Number.isFinite(dataMax) && dataMax > 0) {
return dataMax;
}
return null;
}
function getFieldToken(input) {
if (!input) {
return "";
}
if (input.id) {
return `id:${input.id}`;
}
if (input.name) {
const indexWithinNameGroup = Array.from(document.querySelectorAll(`[name="${CSS.escape(input.name)}"]`)).indexOf(input);
return `name:${input.name}:${Math.max(indexWithinNameGroup, 0)}`;
}
if (!input.dataset.adminFieldToken) {
generatedFieldToken += 1;
input.dataset.adminFieldToken = `generated:${generatedFieldToken}`;
}
return input.dataset.adminFieldToken;
}
function isDragDropField(element) {
if (!element || !element.closest) {
return false;
}
return Boolean(
element.closest(
".social-link-item, .floating-contact-action-item, .menu-links-sortable, .social-links-sortable, .sortable-container, .sortable-list, .sortable-item, [data-top-menu-index], [data-top-social-index], [data-bottom-menu-index], [data-bottom-social-index], [draggable='true']",
),
);
}
function refreshCountersWithin(scope) {
if (!scope || !scope.querySelectorAll) {
return;
}
scope
.querySelectorAll("input[data-maxlength], textarea[data-maxlength], input[data-maxwords], textarea[data-maxwords], input[maxlength], textarea[maxlength]")
.forEach((field) => {
if (field.dataset[COUNTER_BOUND_KEY] === "true") {
updateCounter(field);
}
});
}
function getCounterRefreshScope(input) {
if (!isDragDropField(input)) {
return null;
}
return (
input.closest(".floating-contact-action-item, .social-link-item, [data-top-menu-index], [data-top-social-index], [data-bottom-menu-index], [data-bottom-social-index]") ||
input.closest(".menu-links-sortable, .social-links-sortable, .sortable-container, .sortable-list")
);
}
function getCounterHost(input) {
return (
input.closest(".input-group") ||
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 ||
input
);
}
function ensureCounterElement(input) {
const counterToken = getFieldToken(input);
const existingAnywhere = counterToken ? document.querySelector(`[data-counter-for="${counterToken}"]`) : null;
if (existingAnywhere) {
return existingAnywhere;
}
const host = getCounterHost(input);
const nextSibling = host.nextElementSibling;
const matchingNextSibling =
nextSibling &&
nextSibling.matches(COUNTER_SELECTOR) &&
nextSibling.dataset.counterFor === counterToken
? nextSibling
: null;
const existing = matchingNextSibling || host.querySelector(`${COUNTER_SELECTOR}[data-counter-for="${counterToken}"]`);
if (existing) {
return existing;
}
const counter = document.createElement("small");
counter.className = "form-text admin-field-counter";
counter.dataset.counterFor = counterToken;
counter.setAttribute("aria-live", "polite");
host.insertAdjacentElement("afterend", counter);
return counter;
}
function matchesPathKey(key, pathname) {
if (key === "*") {
return true;
}
if (key.endsWith("*")) {
return pathname.startsWith(key.slice(0, -1));
}
return key === pathname;
}
function getPathRules(registry) {
const pathname = window.location.pathname;
return Object.keys(registry).reduce((rules, key) => {
if (!matchesPathKey(key, pathname)) {
return rules;
}
return rules.concat(registry[key] || []);
}, []);
}
function findTargets(root, selector) {
const targets = [];
if (root.matches && root.matches(selector)) {
targets.push(root);
}
if (root.querySelectorAll) {
targets.push(...root.querySelectorAll(selector));
}
return targets;
}
function applyFieldRules(scope) {
const root = toScope(scope);
getPathRules(FIELD_RULES).forEach((rule) => {
findTargets(root, rule.selector).forEach((input) => {
if (rule.maxLength && !input.dataset.maxlength) {
input.dataset.maxlength = String(rule.maxLength);
input.setAttribute("maxlength", String(rule.maxLength));
}
if (rule.maxWords && !input.dataset.maxwords) {
input.dataset.maxwords = String(rule.maxWords);
}
});
});
root.querySelectorAll("input, textarea").forEach((input) => {
if (
input.disabled ||
input.type === "hidden" ||
input.type === "file" ||
input.dataset.maxlength ||
input.dataset.maxwords ||
input.getAttribute("maxlength")
) {
return;
}
const type = (input.getAttribute("type") || "").toLowerCase();
if (type && !["text", "email", "tel", "search", "url"].includes(type) && input.tagName !== "TEXTAREA") {
return;
}
const descriptor = buildDescriptor(input);
if (/json|editor|html|content-block|blocks/.test(descriptor)) {
return;
}
let inferredMaxLength = 72;
if (input.tagName === "TEXTAREA") {
inferredMaxLength = /description|content|overview|additional info|quote|note|message|summary/.test(descriptor) ? 500 : 220;
} else if (type === "email" || /email/.test(descriptor)) {
inferredMaxLength = 120;
} else if (type === "tel" || /phone|tel|mobile|whatsapp|zalo/.test(descriptor)) {
inferredMaxLength = 32;
} else if (/url|href|link/.test(descriptor) || isUploadDescriptor(descriptor)) {
inferredMaxLength = 255;
} else if (/slug|code|id/.test(descriptor)) {
inferredMaxLength = 32;
} else if (/title|heading|name|label|subtitle|platform/.test(descriptor)) {
inferredMaxLength = 72;
}
input.dataset.maxlength = String(inferredMaxLength);
input.setAttribute("maxlength", String(inferredMaxLength));
});
}
function updateCounter(input) {
const counter = ensureCounterElement(input);
const maxLength = getFieldLimit(input);
const maxWords = getWordLimit(input);
const currentValue = normalizeText(input.value || "");
const currentLength = currentValue.length;
if (maxWords) {
const words = currentValue ? currentValue.split(" ") : [];
const currentWords = words.filter(Boolean).length;
if (maxLength && currentLength > maxLength) {
input.value = currentValue.slice(0, maxLength);
}
if (maxLength) {
counter.textContent = `${currentWords}/${maxWords} words, ${Math.min(currentLength, maxLength)}/${maxLength} characters`;
counter.classList.toggle("is-danger", currentWords >= maxWords || currentLength >= maxLength);
} else {
counter.textContent = `${currentWords}/${maxWords} words`;
counter.classList.toggle("is-danger", currentWords >= maxWords);
}
return;
}
if (!maxLength) {
counter.textContent = "";
return;
}
if (currentLength > maxLength) {
input.value = currentValue.slice(0, maxLength);
}
counter.textContent = `${Math.min(currentLength, maxLength)}/${maxLength} characters`;
counter.classList.toggle("is-danger", currentLength >= maxLength);
}
function bindCounter(input) {
if (!input || input.dataset[COUNTER_BOUND_KEY] === "true") {
return;
}
input.dataset[COUNTER_BOUND_KEY] = "true";
const syncCounter = () => {
updateCounter(input);
const refreshScope = getCounterRefreshScope(input);
if (refreshScope) {
refreshCountersWithin(refreshScope);
}
};
syncCounter();
input.addEventListener("input", syncCounter);
input.addEventListener("change", syncCounter);
input.addEventListener("blur", syncCounter);
input.addEventListener("focus", syncCounter);
}
function buildGuidanceLines(options = {}) {
const title = normalizeText(options.title) || "Upload guidance";
const lines = Array.isArray(options.lines) ? options.lines.map(normalizeText).filter(Boolean) : [];
if (!lines.length) {
lines.push("Use a clear, high-resolution image that matches the visible frame.");
lines.push("Prefer a file that is at least 2x the displayed size for crisp rendering.");
lines.push("Keep the original aspect ratio unless the page explicitly asks for a crop.");
}
return { title, lines };
}
function resolveGuidanceAnchor(target) {
if (!target) {
return null;
}
if (typeof target === "string") {
return document.querySelector(target);
}
if (target instanceof Element) {
return target;
}
return null;
}
function renderUploadGuidance(target, options = {}) {
const anchor = resolveGuidanceAnchor(target);
if (!anchor) {
return null;
}
const host = anchor.closest(".form-group, .mb-3, .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") || anchor.parentElement;
if (!host) {
return null;
}
const matchingAnchorSibling =
anchor.nextElementSibling &&
anchor.nextElementSibling.matches(GUIDANCE_SELECTOR) &&
anchor.nextElementSibling.dataset.guidanceFor === (options.for || "")
? anchor.nextElementSibling
: null;
const matchingHostSibling =
host.nextElementSibling &&
host.nextElementSibling.matches(GUIDANCE_SELECTOR) &&
host.nextElementSibling.dataset.guidanceFor === (options.for || "")
? host.nextElementSibling
: null;
const existing =
matchingAnchorSibling ||
matchingHostSibling ||
host.querySelector(`${GUIDANCE_SELECTOR}[data-guidance-for="${options.for || ""}"]`);
if (existing) {
return existing;
}
const payload = buildGuidanceLines(options);
const note = document.createElement("div");
note.className = "admin-upload-guidance";
note.dataset.guidanceFor = options.for || "";
note.setAttribute("role", "note");
note.innerHTML = `
<div class="admin-upload-guidance__title">${payload.title}</div>
<ul class="admin-upload-guidance__list">
${payload.lines.map((line) => `<li>${line}</li>`).join("")}
</ul>
`;
const insertionTarget = host !== anchor ? host : anchor;
insertionTarget.insertAdjacentElement("afterend", note);
return note;
}
function autoWireGuidance(scope) {
const root = toScope(scope);
root.querySelectorAll(`[${AUTO_GUIDANCE_ATTR}]`).forEach((anchor) => {
const guidanceValue = anchor.getAttribute(AUTO_GUIDANCE_ATTR);
if (guidanceValue === "false") {
return;
}
if (isDragDropField(anchor)) {
return;
}
if (hasManualUploadHint(anchor)) {
return;
}
renderUploadGuidance(anchor, {
for: anchor.id || anchor.dataset.targetInput || "",
title: anchor.dataset.adminUploadGuidanceTitle || "Upload guidance",
lines: anchor.dataset.adminUploadGuidance
? anchor.dataset.adminUploadGuidance.split("|").map((part) => part.trim())
: undefined,
});
});
}
function applyGuidanceRules(scope) {
const root = toScope(scope);
getPathRules(GUIDANCE_RULES).forEach((rule) => {
findTargets(root, rule.selector).forEach((anchor) => {
if (isDragDropField(anchor)) {
return;
}
if (hasManualUploadHint(anchor)) {
return;
}
if (anchor.matches(".btn-upload-image")) {
const targetId = anchor.dataset.targetInput;
const target = resolveUploadTarget(targetId, anchor);
renderUploadGuidance(target || anchor, {
for: targetId || anchor.id || "",
title: rule.title,
lines: rule.lines,
});
return;
}
renderUploadGuidance(anchor, {
for: anchor.id || anchor.name || "",
title: rule.title,
lines: rule.lines,
});
});
});
root.querySelectorAll("input[type='text'], textarea").forEach((input) => {
if (isDragDropField(input)) {
return;
}
const descriptor = buildDescriptor(input);
if (!isUploadDescriptor(descriptor)) {
return;
}
if (hasManualUploadHint(input)) {
return;
}
const host =
input.parentElement?.querySelector(".admin-upload-guidance") ||
input.closest(".col-md-12, .col-md-6, .col-md-4, .col-lg-12, .col-lg-6, .col-lg-4, .col-12")?.querySelector(".admin-upload-guidance");
if (host) {
return;
}
renderUploadGuidance(input, {
for: input.id || input.name || "",
title: "Upload guidance",
lines: [
"Use a clear, high-resolution image sized for the frontend frame.",
"Prefer SVG for logos or icons and at least 2x the displayed size for raster uploads.",
],
});
});
}
function observeMutations() {
if (document.body[OBSERVER_BOUND_KEY]) {
return;
}
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (!node || node.nodeType !== 1) {
return;
}
init(node);
});
});
});
observer.observe(document.body, { childList: true, subtree: true });
document.body[OBSERVER_BOUND_KEY] = true;
}
function init(scope = document) {
const root = toScope(scope);
applyFieldRules(root);
root.querySelectorAll("input[data-maxlength], textarea[data-maxlength], input[data-maxwords], textarea[data-maxwords], input[maxlength], textarea[maxlength]").forEach(bindCounter);
autoWireGuidance(root);
applyGuidanceRules(root);
}
function refresh(scope = document) {
init(scope);
}
function shouldAutoInit() {
return document.body && document.body.dataset.adminHelpers === "true";
}
const api = {
init,
refresh,
renderUploadGuidance,
updateCounter,
};
window.AdminFormHelpers = api;
if (shouldAutoInit()) {
if (document.readyState === "loading") {
window.addEventListener("load", () => {
init(document);
observeMutations();
}, { once: true });
} else {
init(document);
observeMutations();
}
}
})(window, document);