/** * ============================================================================== * SUPPORT WIDGET — Web Component (Shadow DOM) * ============================================================================== * Shadow DOM cô lập CSS hoàn toàn, tránh conflict/flicker với host page. * 3 tabs: SMS (AU Only) / Email / WhatsApp * ============================================================================== */ (function (w, d) { "use strict"; if (w.__MSW_LOADED__) return; w.__MSW_LOADED__ = true; var P = w.PROLOGY_CONFIG || {}; var CFG = { apiEndpoint: P.CHAT_API_URL || P.API_URL || "https://omni.prology.net/chat-plugin/api/support", whatsappNumber: P.CHAT_WHATSAPP_NUMBER || "84901234567", accentColor: P.CHAT_ACCENT_COLOR || "#4f46e5", position: P.CHAT_POSITION || "right", rateLimit: { max: P.CHAT_RATE_LIMIT_MAX != null ? P.CHAT_RATE_LIMIT_MAX : 3, windowMin: P.CHAT_RATE_LIMIT_WINDOW_MIN != null ? P.CHAT_RATE_LIMIT_WINDOW_MIN : 10, }, minFillMs: P.CHAT_MIN_FILL_MS != null ? P.CHAT_MIN_FILL_MS : 3000, fabImageUrl: P.CHAT_FAB_IMAGE_URL || "https://prology.nswteam.net/media/wysiwyg/image_2026-04-02_15-43-14.png", }; /* ── Utilities ── */ function sanitize(str, maxLen) { if (typeof str !== "string") return ""; return str .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'") .replace(/\//g, "/") .trim() .slice(0, maxLen || 2000); } function nonce() { var a = new Uint8Array(16); (w.crypto || w.msCrypto).getRandomValues(a); return Array.from(a, function (b) { return ("0" + b.toString(16)).slice(-2); }).join(""); } var RL_KEY = "_msw_rl"; function getRl() { try { return ( JSON.parse(sessionStorage.getItem(RL_KEY)) || { n: 0, t: Date.now() } ); } catch (e) { return { n: 0, t: Date.now() }; } } function setRl(o) { try { sessionStorage.setItem(RL_KEY, JSON.stringify(o)); } catch (e) {} } function isLimited() { var cfg = CFG.rateLimit, rl = getRl(), win = cfg.windowMin * 60 * 1000; if (Date.now() - rl.t > win) { setRl({ n: 0, t: Date.now() }); return false; } return rl.n >= cfg.max; } function limitCooldown() { var rl = getRl(), win = CFG.rateLimit.windowMin * 60 * 1000; return Math.max(0, Math.ceil((rl.t + win - Date.now()) / 1000)); } function bumpRl() { var rl = getRl(); rl.n++; setRl(rl); } function csrfToken() { var el = d.querySelector('input[name="form_key"]'); return el ? el.value : ""; } function validateSMS(f) { var e = []; var name = (f.name || "").trim(); var phone = (f.phone || "").trim(); var message = (f.message || "").trim(); if (!name || !/^[\p{L}\p{M}'\-\s]{2,80}$/u.test(name)) e.push("sms-name"); if (!phone || !/^04\s?\d{2}\s?\d{3}\s?\d{3}$/.test(phone)) e.push("sms-phone"); if (!message || message.length < 10) e.push("sms-msg"); return e; } function validateEmail(f) { var e = []; var name = (f.name || "").trim(); var email = (f.email || "").trim(); var message = (f.message || "").trim(); if (!name || !/^[\p{L}\p{M}'\-\s]{2,80}$/u.test(name)) e.push("email-name"); if (!email || !/^\w+([.\-]\w+)*@([\w\-]+\.)+[a-zA-Z]{2,12}$/.test(email)) e.push("email-email"); if (!message || message.length < 10) e.push("email-msg"); return e; } /* ── SVG assets ── */ var SVG_ARROW = ''; var WA_PATH = "M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347zM11.979 0C5.37 0 0 5.373 0 11.979c0 2.11.553 4.094 1.518 5.818L.057 24l6.349-1.663A11.938 11.938 0 0011.979 24C18.588 24 24 18.626 24 11.979 24 5.373 18.588 0 11.979 0zm0 21.818a9.839 9.839 0 01-5.012-1.369l-.36-.214-3.73.978.995-3.636-.235-.374a9.806 9.806 0 01-1.506-5.224c0-5.42 4.413-9.833 9.848-9.833 5.437 0 9.851 4.413 9.851 9.833 0 5.421-4.414 9.839-9.851 9.839z"; function waSVG(size) { return ( '' ); } var SVG_CHECK = ''; /* ── Shadow CSS ── */ var ac = CFG.accentColor; var pos = CFG.position === "left" ? "left:24px;" : "right:24px;"; var SHADOW_CSS = [ ':host{all:initial;display:block;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;}', /* FAB — no transition on background/box-shadow to kill flicker; only transform transitions */ "#fab{position:fixed;bottom:24px;" + pos + "z-index:99998;", "width:60px;height:60px;border-radius:50%;", "background-image:url(https://prology.nswteam.net/media/wysiwyg/image_2026-04-02_15-43-14.png);", "background-size:cover;background-position:center;background-repeat:no-repeat;", "border:none;cursor:pointer;outline:none;", "box-shadow:0 6px 20px rgba(79,70,229,.4);", "transform:translateZ(0);transition:transform .22s cubic-bezier(.34,1.56,.64,1);}", "#fab:hover{transform:scale(1.08) translateY(-3px) translateZ(0);}", "#badge{position:absolute;top:-4px;right:-4px;width:20px;height:20px;border-radius:50%;", "background:#ef4444;font-size:11px;font-weight:700;color:#fff;", "display:none;align-items:center;justify-content:center;border:2px solid #1a1f2e;}", /* Panel */ "#panel{position:fixed;bottom:92px;" + pos + "z-index:99999;width:360px;", "background:linear-gradient(145deg,rgb(42,59,84),rgb(30,41,59));border-radius:18px;overflow:hidden;", "box-shadow:0 20px 60px rgba(0,0,0,.55),0 4px 16px rgba(0,0,0,.3);", "transform:scale(.9) translateY(16px) translateZ(0);opacity:0;pointer-events:none;", "transition:transform .28s cubic-bezier(.34,1.56,.64,1),opacity .2s ease;", "will-change:transform,opacity;}", "#panel.open{transform:scale(1) translateY(0) translateZ(0);opacity:1;pointer-events:auto;}", /* Tabs */ "#tabs{display:flex;gap:3px;background:#242938;margin:14px 14px 0;border-radius:11px;padding:4px;}", ".tab{flex:1;padding:8px 4px;font-size:11.5px;font-weight:700;text-align:center;line-height:1.35;", "color:#8b92a8;background:transparent;border:none;cursor:pointer;border-radius:8px;", "transition:background .18s,color .18s;font-family:inherit;letter-spacing:.3px;}", ".tab:hover{color:#e0e2ea;}", ".tab.active{background:" + ac + ";color:#fff;}", /* Panes */ ".pane{display:none;padding:14px 14px 18px;}", ".pane.active{display:block;}", /* Fields */ ".field{margin-bottom:11px;}", ".field input,.field textarea{width:100%;padding:11px 13px;font-size:14px;font-family:inherit;", "background:#252b3b;color:#dde0eb;border:1px solid #333b52;border-radius:9px;outline:none;", "transition:border-color .18s,box-shadow .18s;box-sizing:border-box;-webkit-appearance:none;}", ".field input::placeholder,.field textarea::placeholder{color:#5d6478;}", ".field input:focus,.field textarea:focus{border-color:" + ac + ";box-shadow:0 0 0 3px rgba(79,70,229,.22);}", ".field input.err,.field textarea.err{border-color:#ef4444;}", ".field textarea{resize:vertical;min-height:88px;}", ".hint{font-size:11.5px;color:#f87171;margin-top:4px;display:none;}", ".hint.show{display:block;}", ".hp{position:absolute;left:-9999px;opacity:0;height:0;width:0;overflow:hidden;pointer-events:none;}", /* Submit */ ".btn-submit{display:flex;align-items:center;justify-content:center;gap:8px;width:100%;padding:12px;", "background:" + ac + ";color:#fff;border:none;border-radius:9px;", "font-size:14.5px;font-weight:600;font-family:inherit;cursor:pointer;margin-top:6px;", "transition:background .18s;}", ".btn-submit:hover{background:#4338ca;}", ".btn-submit:active{transform:scale(.98);}", ".btn-submit:disabled{background:#4a4f61;cursor:not-allowed;}", /* Status */ ".status{font-size:13px;font-weight:500;margin-top:9px;text-align:center;", "border-radius:7px;padding:8px 12px;display:none;}", ".status.ok{display:block;background:#0a2e1a;color:#34d399;}", ".status.fail{display:block;background:#2e0a0a;color:#f87171;}", ".status.warn{display:block;background:#2c1f00;color:#fbbf24;}", /* WhatsApp pane */ ".wa-wrap{text-align:center;padding:4px 0 2px;}", ".wa-icon-wrap{width:54px;height:54px;border-radius:50%;background:#1a2e20;", "display:flex;align-items:center;justify-content:center;margin:0 auto 14px;color:#25d366;}", ".wa-desc{font-size:13.5px;color:#8b92a8;line-height:1.6;margin:0 0 18px;}", ".btn-wa{display:flex;align-items:center;justify-content:center;gap:9px;width:100%;padding:13px;", "background:#25d366;color:#fff;border:none;border-radius:9px;", "font-size:14.5px;font-weight:600;font-family:inherit;cursor:pointer;transition:background .18s;}", ".btn-wa:hover{background:#1db954;}", /* Success */ "#success{display:none;flex-direction:column;align-items:center;text-align:center;padding:36px 20px;}", ".success-icon{width:58px;height:58px;border-radius:50%;background:#0a2e1a;", "display:flex;align-items:center;justify-content:center;color:#34d399;margin-bottom:15px;}", "#success h3{font-size:18px;font-weight:700;color:#dde0eb;margin:0 0 8px;}", "#success p{font-size:13.5px;color:#8b92a8;margin:0 0 22px;line-height:1.55;}", ".btn-new{padding:10px 24px;font-size:13.5px;font-weight:600;color:" + ac + ";", "background:rgba(79,70,229,.13);border:none;border-radius:8px;cursor:pointer;", "transition:background .18s;font-family:inherit;}", ".btn-new:hover{background:rgba(79,70,229,.22);}", /* Responsive */ "@media(max-width:420px){#panel{width:calc(100vw - 28px);" + (CFG.position === "left" ? "left:14px;" : "right:14px;") + "}", "#fab{bottom:20px;}#panel{bottom:88px;}}", ].join(""); /* ── Inner HTML ── */ var INNER_HTML = [ '', '", ].join(""); /* ── Web Component ── */ function SupportWidget() { return Reflect.construct(HTMLElement, [], SupportWidget); } Object.setPrototypeOf(SupportWidget.prototype, HTMLElement.prototype); Object.setPrototypeOf(SupportWidget, HTMLElement); SupportWidget.prototype.connectedCallback = function () { var shadow = this.attachShadow({ mode: "open" }); var style = d.createElement("style"); style.textContent = SHADOW_CSS; shadow.appendChild(style); var root = d.createElement("div"); root.innerHTML = INNER_HTML; shadow.appendChild(root); this._boot(shadow); }; SupportWidget.prototype._boot = function (s) { var isOpen = false; var formStartTs = Date.now(); var $fab = s.getElementById("fab"); var $panel = s.getElementById("panel"); var $badge = s.getElementById("badge"); var $success = s.getElementById("success"); var $tabs = s.getElementById("tabs"); function openPanel() { isOpen = true; $panel.classList.add("open"); $fab.setAttribute("aria-expanded", "true"); $badge.style.display = "none"; formStartTs = Date.now(); } function closePanel() { isOpen = false; $panel.classList.remove("open"); $fab.setAttribute("aria-expanded", "false"); } $fab.addEventListener("click", function (e) { e.stopPropagation(); isOpen ? closePanel() : openPanel(); }); d.addEventListener("click", function (e) { if (!isOpen) return; var path = e.composedPath ? e.composedPath() : []; if ( !path.some(function (n) { return n === $panel || n === $fab; }) ) closePanel(); }); d.addEventListener("keydown", function (e) { if (e.key === "Escape" && isOpen) closePanel(); }); /* Tabs */ s.querySelectorAll(".tab").forEach(function (tab) { tab.addEventListener("click", function () { s.querySelectorAll(".tab").forEach(function (el) { el.classList.toggle("active", el === tab); }); s.querySelectorAll(".pane").forEach(function (el) { el.classList.remove("active"); }); var pane = s.getElementById("pane-" + tab.dataset.tab); if (pane) pane.classList.add("active"); }); }); /* WhatsApp */ s.getElementById("btn-wa").addEventListener("click", function () { w.open( "https://wa.me/" + CFG.whatsappNumber, "_blank", "noopener,noreferrer", ); }); /* Helpers */ function clearErr(prefix, fields) { fields.forEach(function (f) { var inp = s.getElementById(prefix + "-" + f), hint = s.getElementById("h-" + prefix + "-" + f); if (inp) inp.classList.remove("err"); if (hint) hint.classList.remove("show"); }); var st = s.getElementById("st-" + prefix); if (st) { st.className = "status"; st.textContent = ""; } } function markErr(ids) { ids.forEach(function (id) { var inp = s.getElementById(id), hint = s.getElementById("h-" + id); if (inp) inp.classList.add("err"); if (hint) hint.classList.add("show"); }); } function setStatus(prefix, msg, type) { var st = s.getElementById("st-" + prefix); if (!st) return; st.textContent = msg; st.className = "status " + type; } function showSuccess() { s.querySelectorAll(".pane").forEach(function (el) { el.classList.remove("active"); }); $tabs.style.display = "none"; $success.style.display = "flex"; } function doSubmit(prefix, payload, $btn, label) { if (isLimited()) { setStatus( prefix, "Too many requests. Try again in " + limitCooldown() + "s.", "warn", ); return; } $btn.disabled = true; /* Preserve the SVG node — only update the leading text node */ var tn = $btn.firstChild; if (tn && tn.nodeType === 3) tn.textContent = "Sending... "; var xhr = new XMLHttpRequest(); xhr.open("POST", CFG.apiEndpoint, true); xhr.setRequestHeader("Content-Type", "application/json"); xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest"); xhr.setRequestHeader("X-Form-Key", csrfToken()); xhr.withCredentials = false; xhr.timeout = 12000; xhr.onreadystatechange = function () { if (xhr.readyState !== 4) return; $btn.disabled = false; if (tn && tn.nodeType === 3) tn.textContent = label + " "; if (xhr.status === 200 || xhr.status === 201) { bumpRl(); showSuccess(); } else if (xhr.status === 429) setStatus( prefix, "Too many requests. Please try again later.", "warn", ); else setStatus( prefix, "Too many requests. Please try again later.", "fail", ); }; xhr.ontimeout = function () { $btn.disabled = false; if (tn && tn.nodeType === 3) tn.textContent = label + " "; setStatus(prefix, "Connection timeout. Check your network.", "fail"); }; xhr.send(JSON.stringify(payload)); } /* SMS submit */ s.getElementById("btn-sms").addEventListener("click", function () { clearErr("sms", ["name", "phone", "msg"]); var hp = s.getElementById("hp-sms"); if (hp && hp.value) return; var remaining = Math.ceil( (CFG.minFillMs - (Date.now() - formStartTs)) / 1000, ); if (remaining > 0) { setStatus( "sms", "Please wait " + remaining + "s before submitting.", "warn", ); return; } var raw = { name: s.getElementById("sms-name").value, phone: s.getElementById("sms-phone").value, message: s.getElementById("sms-msg").value, }; var errs = validateSMS(raw); if (errs.length) { markErr(errs); return; } doSubmit( "sms", { channel: "sms", name: sanitize(raw.name, 80), phone: sanitize(raw.phone, 15), message: sanitize(raw.message, 1000), nonce: nonce(), source: w.location.href.slice(0, 200), ts: Date.now(), }, this, "Send SMS", ); }); /* Email submit */ s.getElementById("btn-email").addEventListener("click", function () { clearErr("email", ["name", "email", "msg"]); var hp = s.getElementById("hp-email"); if (hp && hp.value) return; var remaining = Math.ceil( (CFG.minFillMs - (Date.now() - formStartTs)) / 1000, ); if (remaining > 0) { setStatus( "email", "Please wait " + remaining + "s before submitting.", "warn", ); return; } var raw = { name: s.getElementById("email-name").value, email: s.getElementById("email-email").value, message: s.getElementById("email-msg").value, }; var errs = validateEmail(raw); if (errs.length) { markErr(errs); return; } doSubmit( "email", { channel: "email", name: sanitize(raw.name, 80), email: sanitize(raw.email, 120), message: sanitize(raw.message, 1000), nonce: nonce(), source: w.location.href.slice(0, 200), ts: Date.now(), }, this, "Send Email", ); }); /* New message */ s.getElementById("btn-new").addEventListener("click", function () { $success.style.display = "none"; $tabs.style.display = "flex"; s.querySelectorAll(".tab").forEach(function (el) { el.classList.toggle("active", el.dataset.tab === "sms"); }); s.querySelectorAll(".pane").forEach(function (el) { el.classList.remove("active"); }); s.getElementById("pane-sms").classList.add("active"); [ "sms-name", "sms-phone", "sms-msg", "email-name", "email-email", "email-msg", ].forEach(function (id) { var el = s.getElementById(id); if (el) el.value = ""; }); clearErr("sms", ["name", "phone", "msg"]); clearErr("email", ["name", "email", "msg"]); formStartTs = Date.now(); }); /* Badge */ setTimeout(function () { if (!isOpen) $badge.style.display = "flex"; }, 4000); }; /* Register & auto-mount */ if (!w.customElements.get("support-widget")) { w.customElements.define("support-widget", SupportWidget); } function mount() { if (!d.querySelector("support-widget")) d.body.appendChild(d.createElement("support-widget")); } if (d.readyState === "loading") d.addEventListener("DOMContentLoaded", mount); else mount(); })(window, document);