var _NAVIPLUS_VERSION = /*replace*/'DEV'/*endreplace*/; //==> with be replaced to $_GET['version'] //******** Khai báo các biến cần thiết ****************************/ var cdnCloudflare = 'https://live-r2.naviplus.app/live'; // Fallback CDN if (typeof _isUseNaviCDNFallback !== 'undefined' && _isUseNaviCDNFallback === true) cdnCloudflare = 'https://flov.b-cdn.net/live'; var cdnUigenCSS = 'https://dev-shopify.naviplus.app/naviplus/frontend'; if (_NAVIPLUS_VERSION != 'DEV') cdnUigenCSS = cdnCloudflare + "/" + _NAVIPLUS_VERSION; var cdnUigenJS = 'https://dev-shopify.naviplus.app/naviplus/frontend'; if (_NAVIPLUS_VERSION != 'DEV') cdnUigenJS = cdnCloudflare + "/" + _NAVIPLUS_VERSION; var cdnJson = 'https://cdn.naviplus.app/naviplus/data/json'; var startJs = 'start.js.php'; if (_NAVIPLUS_VERSION != 'DEV') startJs = "start.js"; var doc = document; var uigenCss = "/uigen/uigen.css.php"; if (_NAVIPLUS_VERSION != 'DEV') uigenCss = "/uigen.min.css"; var uigenJs = "/uigen.js.php"; if (_NAVIPLUS_VERSION != 'DEV') uigenJs = "/uigen.min.js"; //******** Khai báo các biến cần thiết ****************************/ var _NAVIPLUS = { VER: _NAVIPLUS_VERSION, SHOP: '', VARS: [ cdnUigenCSS + uigenCss, //==> with be replaced to uigen.min.css 'https://cdn.jsdelivr.net/npm/animate.css@4.1.1/animate.min.css', 'https://cdn.jsdelivr.net/npm/remixicon@4.8.0/fonts/remixicon.css', // Cần phải đánh giá vì hiệu qủa có khi ko lớn mà ko update ngay. // cdnJson + '/{shop}.all.json', // cdnJson + '/{shop}.info.json', cdnUigenJS + uigenJs //==> with be replaced to uigen.min.js ], GO: (function () { const _loaded = { css: [], js: [], json: [] }; //============================== MAIN FUNCTIONS ============================== function loadUIGenJS(url, ver, naviSetting) { if (!navistart.validateUIgenUrl(url)) return; // ---- BƯỚC 1: Tạo script ---- const script = doc.createElement("script"); script.src = navistart.addVersionToUrl(url, ver); _loaded.js.push(script.src); // ---- Bước 2: Copy các biến trong _navi_setting thành attributes cho uigen.js ---- navistart.copyAttribute(script, naviSetting); navistart.setAttributeDefer(script); // ---- Bước 3: Fallback nếu CDN chậm ---- script.onerror = () => { navistart.failOver(script.src, script, _NAVIPLUS.VER); }; // ---- Bước 4: Append vào head ---- // console.log("Loading... uigen.js", script); doc.head.appendChild(script); } //============================== MAIN FUNCTIONS ============================== function start(vars, naviSetting) { navistart.initDOM(); navistart.initResources(vars, naviSetting, loadUIGenJS); } return { start, loadUIGenJS }; })() }; /** * ============================================ * NAVISTART - Naviplus Start Utilities Module * ============================================ * * Module tiện ích chính cho việc khởi tạo và quản lý tài nguyên của Naviplus. * Cung cấp các hàm utility cho: * * - Resource loading (CSS, JSON, JS) * - DOM manipulation và initialization * - Shop/domain normalization * - Performance monitoring và timing * - Configuration management * * Tất cả functions đều static và có thể truy cập qua navistart.* */ if (typeof navistart === 'undefined') navistart = {}; if (!window._NAVIPLUS_START_MODULE) { window._NAVIPLUS_START_MODULE = true; /** * In ra biến lên console chỉ một lần trong suốt phiên làm việc của trang. * Sử dụng cơ chế lưu trữ trên window để đảm bảo mỗi key chỉ được log một lần. * * @param {*} variable - Biến cần in ra console (có thể là bất kỳ kiểu dữ liệu nào) * @param {string} [key='_CONSOLE_NAVIPLUS_START'] - Khóa duy nhất để kiểm tra đã log chưa. * Mặc định là '_CONSOLE_NAVIPLUS_START' * * @example * navistart.consoleOnce({user: 'john', version: '1.0'}); // Log object * navistart.consoleOnce('Debug message', 'custom_key'); // Log với key tùy chỉnh */ navistart.consoleOnce = function(variable, key = '_CONSOLE_NAVIPLUS_START') { if (!window[key]) { window[key] = true; console.log(variable); } }; /** * Chuẩn hóa tên shop dựa trên môi trường sử dụng. * * Quy tắc chuẩn hóa: * - Nếu env là "shopify": loại bỏ phần '.myshopify.com' khỏi tên shop * (VD: 'mystore.myshopify.com' -> 'mystore') * - Nếu env không phải "shopify": trả về domain đã chuẩn hóa bằng normalizeDomainSync * - Nếu không có env hoặc shop rỗng: trả lại shop gốc * * @param {string} shop - Tên shop hoặc domain cần chuẩn hóa * @param {string} env - Môi trường sử dụng ('shopify', 'woocommerce', etc.) * @returns {string} Tên shop đã được chuẩn hóa theo quy tắc của môi trường * * @example * navistart.normalizeShop('mystore.myshopify.com', 'shopify'); // 'mystore' * navistart.normalizeShop('example.com', 'woocommerce'); // 'example.com' (đã chuẩn hóa) * navistart.normalizeShop('', 'shopify'); // '' (rỗng thì trả về rỗng) */ navistart.normalizeShop = function(shop, env) { if (!shop) return shop; if (env && env !== "shopify") { return navistart.normalizeDomainSync(shop); } if (env === "shopify") { return shop.replace('.myshopify.com', ''); } return shop; } /** * Chuẩn hóa domain một cách đồng bộ, trả về root domain. * * Ưu tiên sử dụng thư viện tldts (nếu có sẵn) để parse domain chính xác. * Nếu không có tldts, fallback về logic tự implement với các quy tắc: * * - Shopify domains (*.myshopify.com) được trả nguyên vẹn * - Loại bỏ protocol (http://, https://) * - Loại bỏ path, query string, hash, port * - Loại bỏ www prefix * - Xử lý TLD phức tạp (co.uk, com.vn, etc.) * * @param {string} host - Domain hoặc URL cần chuẩn hóa * @returns {string} Root domain đã được chuẩn hóa * * @example * navistart.normalizeDomainSync('https://sub.example.co.uk/path'); // 'example.co.uk' * navistart.normalizeDomainSync('mystore.myshopify.com'); // 'mystore.myshopify.com' * navistart.normalizeDomainSync('www.example.com:8080'); // 'example.com' */ navistart.normalizeDomainSync = function (host) { host = String(host || "").trim().toLowerCase(); host = host.replace(/^[a-z]+:\/\//, "") .split(/[/?#]/)[0] .replace(/:\d+$/, "") .replace(/^www\d*\./, ""); if (host.endsWith(".myshopify.com")) { return host; } if (window.tldts) { const rootDomain = window.tldts.getDomain(host); // Kiểm tra kết quả từ tldts có hợp lý không if (rootDomain && rootDomain !== host) { // Kiểm tra thêm: nếu rootDomain quá ngắn (chỉ là TLD) thì có thể sai // Ví dụ: khoipn.com.jp -> com.jp (sai, phải là khoipn.com.jp) const hostParts = host.split('.'); const rootParts = rootDomain.split('.'); // Nếu rootDomain không chứa phần domain chính (phần đầu tiên), có thể sai if (rootParts.length >= 2 && hostParts.length > rootParts.length) { // Kiểm tra xem rootDomain có phải chỉ là TLD không const firstHostPart = hostParts[0]; if (!rootDomain.includes(firstHostPart)) { // Có thể tldts trả về sai, dùng fallback } else { return rootDomain; } } else { return rootDomain; } } // Nếu tldts trả về null hoặc chính host gốc (không nhận diện được TLD phức tạp) // thì dùng fallback logic bên dưới } const parts = host.split("."); const len = parts.length; if (len <= 2) return host; const cc = parts[len - 1]; // country code dự đoán const common2 = parts[len - 2]; // phần đứng trước cc const remaining = parts.slice(0, -1); const countryCodes = new Set([ 'vn', 'uk', 'au', 'jp', 'kr', 'cn', 'id', 'my', 'sg', 'th', 'ph', 'nz', 'br', 'mx', 'za', 'in', 'ru', 'tr', 'pl', 'pt', 'es', 'it', 'fr', 'de' ]); const commonLevel2 = new Set(['com', 'net', 'org', 'gov', 'edu', 'co', 'ac', 'ne', 'or']); // rule: multi-level TLD dạng com.vn, net.vn, co.uk, com.au ... if (cc.length === 2 && countryCodes.has(cc) && commonLevel2.has(common2)) { // Lấy 1 phần trước TLD phức tạp (nếu có) // khoipn.com.jp -> khoipn.com.jp (đúng vì chỉ có 3 phần) // sub.khoipn.com.jp -> khoipn.com.jp if (remaining.length > 2) { return remaining.slice(-2).join('.') + '.' + cc; } else { // Nếu remaining chỉ có 2 phần, lấy cả 2 phần return remaining.join('.') + '.' + cc; } } // fallback mặc định: TLD 1 lớp return parts.slice(-2).join('.'); }; /** * Lấy thuộc tính từ object naviSetting một cách an toàn. * * Hàm này kiểm tra: * - naviSetting có tồn tại và là object không * - Thuộc tính có tồn tại trong naviSetting không (không phải undefined) * - Nếu không thỏa mãn, trả về giá trị mặc định * * @param {Object} naviSetting - Object chứa các setting của Naviplus * @param {string} attr - Tên thuộc tính cần lấy * @param {*} [defaultValue=null] - Giá trị mặc định nếu thuộc tính không tồn tại * @returns {*} Giá trị của thuộc tính hoặc giá trị mặc định * * @example * const setting = { shop: 'example.com', embed_id: '123' }; * navistart.getNaviSetting(setting, 'shop'); // 'example.com' * navistart.getNaviSetting(setting, 'token'); // '123' * navistart.getNaviSetting(setting, 'version', '1.0'); // '1.0' (mặc định) * navistart.getNaviSetting(null, 'shop'); // null (mặc định) */ navistart.getNaviSetting = function(naviSetting, attr, defaultValue = null) { if (!naviSetting || typeof naviSetting !== 'object') return defaultValue; return naviSetting[attr] !== undefined ? naviSetting[attr] : defaultValue; } /** * Module tối ưu hóa hiệu năng Naviplus. * * Module này tự động đo thời gian tải các tài nguyên quan trọng: * - uigen.css: File CSS chính của UI generator * - uigen.js: File JavaScript chính của UI generator * * Sử dụng IIFE pattern để encapsulate các biến private và chỉ expose * những gì cần thiết qua return object. * * @returns {Object} Object chứa các hàm public của module */ navistart.Optimize = (function () { const timing = { start: performance.now(), uigenCss: null, uigenJs: null }; let hasLogged = false; /** * Ghi log thời gian tải các tài nguyên chính và lưu vào window._NAVIPLUS_TIMING. * * Chỉ thực hiện một lần khi cả uigenCss và uigenJs đều đã được đo. * Kết quả được lưu vào window._NAVIPLUS_TIMING để debug và monitoring. * * @private */ function logTiming() { if (hasLogged) return; if (timing.uigenCss !== null && timing.uigenJs !== null) { hasLogged = true; window._NAVIPLUS_TIMING = { start: timing.start, uigenCss: timing.uigenCss, uigenJs: timing.uigenJs }; console.log(window._NAVIPLUS_TIMING); } } /** * Đợi đến khi file CSS được tải hoàn toàn và gọi callback với thời gian đã chờ. * * Cách hoạt động: * 1. Kiểm tra mỗi 10ms để tìm link element chứa urlContains trong href * 2. Nếu link.sheet đã tồn tại → callback ngay lập tức * 3. Nếu link chưa có sheet → gán onload event để callback khi tải xong * 4. Dừng interval sau khi callback được gọi * * @param {string} urlContains - Chuỗi con trong URL để identify file CSS cần theo dõi * @param {function} callback - Hàm callback nhận thời gian chờ tính bằng ms * * @example * waitForCSSLoaded('uigen', function(elapsed) { * console.log('CSS loaded in', elapsed, 'ms'); * }); */ function waitForCSSLoaded(urlContains, callback) { const startTime = performance.now(); const timer = setInterval(() => { const links = document.querySelectorAll('link[rel="stylesheet"]'); for (const link of links) { if (link.href.includes(urlContains)) { if (link.sheet) { clearInterval(timer); const elapsed = performance.now() - startTime; callback(elapsed); return; } else { link.onload = () => { clearInterval(timer); const elapsed = performance.now() - startTime; callback(elapsed); }; return; } } } }, 10); } /** * Đợi đến khi biến toàn cục xuất hiện trên window (đánh dấu script đã load xong). * * Thường dùng để detect khi script bên thứ 3 đã load xong bằng cách * kiểm tra biến global mà script đó tạo ra. * * @param {string} varName - Tên biến toàn cục cần kiểm tra (VD: 'naviman_version', 'jQuery') * @param {function} callback - Hàm callback nhận thời gian chờ tính bằng ms * * @example * waitForJSLoaded('naviman_version', function(elapsed) { * console.log('Naviman loaded in', elapsed, 'ms'); * }); */ function waitForJSLoaded(varName, callback) { const startTime = performance.now(); const timer = setInterval(() => { if (window[varName] !== undefined) { clearInterval(timer); const elapsed = performance.now() - startTime; callback(elapsed); } }, 10); } // Đợi file CSS có chứa "uigen" được load xong rồi thực hiện callback waitForCSSLoaded("uigen", function (elapsed) { timing.uigenCss = elapsed; logTiming(); }); // Đợi đến khi biến toàn cục "naviman_version" xuất hiện (file JS đã load xong), sau đó thực hiện callback waitForJSLoaded("naviman_version", function (elapsed) { timing.uigenJs = elapsed; logTiming(); }); return { /** * Lấy thông tin timing hiện tại. * @returns {Object} Object chứa start, uigenCss, uigenJs timing */ getTiming: function () { return timing; } }; })(); // ========== RESOURCE LOADING FUNCTIONS ========== /** * @private Biến tracking các tài nguyên đã được load để tránh duplicate */ var _loaded = { css: [], js: [], json: [] }; /** * @private Set tracking các JSON URLs đã được fetch để tránh duplicate requests */ var _json = new Set(); /** * Load file CSS vào trang web. * * Tính năng: * - Tự động thêm version parameter nếu được cung cấp * - Kiểm tra duplicate để tránh load cùng file CSS nhiều lần * - Tracking các file đã load trong _loaded.css * * @param {string} u - URL của file CSS * @param {string|null} v - Version parameter (sẽ được thêm vào URL nếu có) * * @example * navistart.loadCSS('https://cdn.example.com/style.css', '1.0'); * // Tạo: */ navistart.loadCSS = function(u, v) { const f = navistart.addVersionToUrl(u, v); if (document.querySelector(`link[href="${f}"]`)) return; _loaded.css.push(f); document.head.appendChild(Object.assign(document.createElement("link"), { rel: "stylesheet", href: f })); }; /** * Load và cache file JSON từ URL. * * Tính năng: * - Sử dụng fetch với cache "force-cache" để tối ưu hiệu năng * - Kiểm tra duplicate requests để tránh fetch cùng URL nhiều lần * - Silent fail (catch empty) vì JSON thường không critical * * @param {string} u - URL của file JSON cần fetch * * @example * navistart.loadJSON('https://api.example.com/config.json'); */ navistart.loadJSON = function(u) { if (!u || _json.has(u)) return; _loaded.json.push(u); _json.add(u); fetch(u, { cache: "force-cache" }).catch(() => { }); }; /** * Thêm version parameter vào URL để cache busting. * * Quy tắc: * - Nếu URL đã có query string (?): thêm &v=version * - Nếu URL chưa có query string: thêm ?v=version * - Nếu version null/undefined: trả về URL gốc * * @param {string} url - URL gốc * @param {string|null} v - Version string * @returns {string} URL với version parameter hoặc URL gốc * * @example * navistart.addVersionToUrl('style.css', '1.0'); // 'style.css?v=1.0' * navistart.addVersionToUrl('style.css?a=1', '1.0'); // 'style.css?a=1&v=1.0' * navistart.addVersionToUrl('style.css', null); // 'style.css' */ navistart.addVersionToUrl = function(url, v) { return v ? url + (url.includes('?') ? '&' : '?') + 'v=' + v : url; }; // ========== DOM AND SCRIPT UTILITIES ========== /** * @private Tên file start script dựa trên environment */ var startJs = 'start.js.php'; if (typeof _NAVIPLUS_VERSION !== 'undefined' && _NAVIPLUS_VERSION != 'DEV') startJs = "start.js"; /** * @private Registry để track embed_ids đã được xử lý, tránh duplicate loading * Sử dụng window scope để persist across multiple script loads */ window._processedEmbedIds = window._processedEmbedIds || {}; /** * Thêm container element naviman_app vào đầu body nếu chưa tồn tại. * * Container này dùng để chứa toàn bộ UI của Naviplus. * Chỉ thêm một lần, không tạo duplicate elements. */ navistart.addNavimanApp = function() { // Đã thử đặt style="display: none;" - Không chạy tốt vì lý do nào đó, tìm cách khác if (!document.getElementById("naviman_app")) document.body.insertAdjacentHTML("afterbegin", ''); }; /** * Tìm script element hiện tại đang được thực thi. * * Ưu tiên sử dụng document.currentScript (standard API). * Fallback: tìm script cuối cùng có src chứa startJs nếu currentScript không khả dụng. * * @returns {HTMLScriptElement|null} Script element hiện tại hoặc null nếu không tìm thấy */ navistart.findCurrentScript = function() { let s = document.currentScript; if (!s) { const sc = document.querySelectorAll('script[src*="' + startJs + '"]'); if (sc.length) s = sc[sc.length - 1]; } return s; }; /** * Lấy giá trị version từ attribute của script element. * * Xử lý đặc biệt cho attribute 'src': * - Parse version từ query parameter v=... * - VD: 'start.js.php?v=1.0&other=1' -> '1.0' * * Với các attribute khác: trả về giá trị trực tiếp. * * @param {HTMLScriptElement} s - Script element * @param {string} attr - Tên attribute cần lấy ('src', 'embed_id', etc.) * @returns {string|null} Giá trị version hoặc null nếu không tìm thấy */ navistart.getVersionFromScript = function(s, attr) { if (!s) return null; const val = s.getAttribute(attr); return attr === 'src' ? (val.match(/[?&]v=([^&]+)/) || [])[1] || null : val; }; /** * Khởi tạo DOM environment cho Naviplus. * * Chỉ thực hiện một lần trong toàn bộ session. * Thêm naviman_app container vào body. * * Nếu document.body đã sẵn sàng: thêm ngay lập tức * Nếu chưa: sử dụng MutationObserver để đợi body được tạo */ navistart.initDOM = function() { if (window._NAVIPLUS_APP_LOADED) return; window._NAVIPLUS_APP_LOADED = true; if (document.body) { navistart.addNavimanApp(); } else { new MutationObserver((m, o) => { if (document.body) { navistart.addNavimanApp(); o.disconnect(); } }).observe(document.documentElement, { childList: true }); } }; /** * Copy các attributes từ naviSetting vào script element. * * Luôn set các attributes cơ bản: embed_id, shop, env * Chỉ set các attributes nâng cao (not_sticky, embed_title, etc.) * khi embed_id không rỗng (có nghĩa là có embed thực sự). * * @param {HTMLScriptElement} script - Script element cần set attributes * @param {Object} naviSetting - Object chứa các setting */ navistart.copyAttribute = function(script, naviSetting) { script.setAttribute('embed_id', navistart.getNaviSetting(naviSetting, 'embed_id', '')); script.setAttribute('shop', navistart.getNaviSetting(naviSetting, 'shop', '')); script.setAttribute('token', navistart.getNaviSetting(naviSetting, 'token', '')); script.setAttribute('env', navistart.getNaviSetting(naviSetting, 'env', '')); script.setAttribute('multisite', navistart.getNaviSetting(naviSetting, 'multisite', '')); if(navistart.getNaviSetting(naviSetting, 'embed_id', '') != '') { script.setAttribute('not_sticky', navistart.getNaviSetting(naviSetting, 'not_sticky', true)); script.setAttribute('embed_title', navistart.getNaviSetting(naviSetting, 'embed_title', '')); script.setAttribute('embed_is_full', navistart.getNaviSetting(naviSetting, 'embed_is_full', false)); script.setAttribute('embed_margin', navistart.getNaviSetting(naviSetting, 'embed_margin', '0 0 0 0')); } }; /** * Set defer attributes cho script element. * * Loại bỏ async attribute (nếu có) và set defer = true. * Điều này đảm bảo script sẽ load và execute theo thứ tự, * sau khi DOM đã được parse xong. * * @param {HTMLScriptElement} script - Script element cần set defer */ /** * Validate URL dành riêng cho UI Generator scripts. * * Kiểm tra các điều kiện cơ bản của URL và log với context cụ thể * cho UI generator loading. * * @param {*} url - URL của UI generator script cần validate * @returns {boolean} true nếu URL valid, throw error nếu invalid * @throws {Error} Nếu URL không valid * * @example * navistart.validateUIgenUrl('https://cdn.example.com/uigen.js'); * // Returns: true (nếu valid) * // Throws: Error (nếu invalid) */ navistart.validateUIgenUrl = function(url) { if (!url || typeof url !== 'string' || url.trim() === '') { console.error("loadUIGenJS: Invalid URL", url); return false; } return true; }; /** * Fallback mechanism khi script load thất bại. * * Tạo script backup từ CDN khác và append vào head. * Kiểm tra duplicate để tránh tạo multiple scripts giống nhau. * * @param {string} scriptSrc - URL của script thất bại * @param {HTMLScriptElement} currentScript - Script element hiện tại * @param {string} version - Version của app (VD: 'DEV', '1.0', etc.) */ navistart.failOver = function (scriptSrc, failedScript, version) { console.warn("[Navi+] Load failed, fallback:", scriptSrc); // ---- Build fallback URL ---- var backupVersion = version || "live-999"; var backupSrc = "https://flov.b-cdn.net/live/" + backupVersion + "/uigen.min.js"; if (_NAVIPLUS_VERSION == 'DEV') backupSrc = "https://dev-shopify.naviplus.app/naviplus/frontend/uigen_flov.js.php"; if (!failedScript) { console.error("[Navi+] Failover aborted: failedScript is null"); return; } // ---- Identity by embed_id ---- var currentEmbedId = failedScript.hasAttribute('embed_id') ? String(failedScript.getAttribute('embed_id')) : null; // ---- Check existing fallback scripts ---- // Only skip if we have a WORKING fallback script with same embed_id var scripts = document.getElementsByTagName("script"); for (var i = 0; i < scripts.length; i++) { var s = scripts[i]; if (!s.src) continue; if (!s.src.includes("/uigen.min.js")) continue; var existingEmbedId = s.getAttribute("embed_id"); // Check if this is the same embed_id AND the script is actually working if (currentEmbedId !== null && existingEmbedId === currentEmbedId) { // Check if script is loaded and working (has navistart object) if (typeof window.navistart !== 'undefined' && window.navistart.initialized) { console.warn("[Navi+] Working fallback already exists for embed_id:", currentEmbedId, "skip:", backupSrc); return; } else { console.warn("[Navi+] Found existing fallback script but it's not working, will create new one"); // Continue to create new fallback since existing one is broken } } } // ---- Create fallback script ---- var backupScript = document.createElement("script"); // ---- Copy attributes safely (NamedNodeMap) ---- var attrs = failedScript.attributes; for (var j = 0; j < attrs.length; j++) { var attr = attrs[j]; if (!attr || !attr.name) continue; if (attr.name === "src" || attr.name === "onerror") continue; backupScript.setAttribute(attr.name, attr.value); } // ---- Apply src & defer ---- backupScript.src = backupSrc; navistart.setAttributeDefer(backupScript); // ---- Mark this script as a fallback attempt ---- backupScript.setAttribute('data-naviplus-fallback', 'true'); // ---- Fallback failure logging ---- backupScript.onerror = function () { console.error("[Navi+] Fallback uigen failed:", { embed_id: currentEmbedId, src: backupSrc }); // Mark this script as failed so future checks know it's not working backupScript.setAttribute('data-naviplus-failed', 'true'); }; // ---- Success callback ---- backupScript.onload = function () { console.log("[Navi+] Fallback uigen loaded successfully:", backupSrc); }; // ---- Append ---- document.head.appendChild(backupScript); }; /** * Load thư viện TLDTS (Top Level Domain TypeScript) nếu cần thiết. * * TLDTS được sử dụng để parse domain một cách chính xác. * Chỉ load khi: * - Environment không phải "shopify" * - Thư viện tldts chưa có sẵn * * @param {string} env - Environment hiện tại ('shopify', 'woocommerce', etc.) */ navistart.loadTLDTS = function(env) { if (env !== "shopify" && !window.tldts) { const script = document.createElement("script"); script.src = "https://cdn.jsdelivr.net/npm/tldts/dist/tldts.min.js"; script.async = true; document.head.appendChild(script); } }; /** * Lấy naviSetting từ queue _navi_setting. * * Xử lý queue các config đã được push vào window._navi_setting * và trả về config đầu tiên (nếu có). * * @returns {Object|null} Config object từ queue hoặc null nếu queue rỗng */ navistart.getNaviSettingFromQueue = function() { const queue = window._navi_setting || []; if (queue.length > 0) { return queue.shift(); } return null; }; /** * Cập nhật shop và thay thế placeholder {shop} trong URLs tài nguyên. * * Nếu shop có giá trị: * - Cập nhật _NAVIPLUS.SHOP * - Thay thế {shop} trong tất cả URLs trong _NAVIPLUS.VARS * * @param {string} shop - Tên shop đã được chuẩn hóa * @param {Object} naviplus - Object _NAVIPLUS chứa SHOP và VARS */ navistart.updateShopAndResources = function(shop, naviplus) { if (shop) { naviplus.SHOP = shop; naviplus.VARS = naviplus.VARS.map(url => typeof url === 'string' && url.includes('{shop}') ? url.replace('{shop}', shop) : url ); } }; /** * Kiểm tra queue _navi_setting có còn phần tử nào sau 3 giây không. * Chỉ setup validation 1 lần duy nhất trong toàn bộ session. * Dùng để detect naviSettings bị bỏ sót không được xử lý. */ navistart.checkQueueEmpty = function() { if (!window._NAVIPLUS_QUEUE_VALIDATION_SETUP) { window._NAVIPLUS_QUEUE_VALIDATION_SETUP = true; setTimeout(function() { var remainingQueue = window._navi_setting || []; if (remainingQueue.length > 0) { console.error("ERROR: Unprocessed naviSettings found in queue after 5 seconds:", { remainingCount: remainingQueue.length, remainingItems: remainingQueue, processedEmbedIds: window._processedEmbedIds || {} }); } }, 5000); } }; /** * Đặt attribute defer cho thẻ script và đảm bảo không dùng async. * Dùng để script tải tuần tự, after DOM hoàn tất. * IMPORTANT: Theo như ChatGPT nói thì cái defer này ko có tác dụng gì cả. */ navistart.setAttributeDefer = function(script) { script.removeAttribute('async'); script.async = false; script.defer = true; }; /** * Khởi tạo và load tất cả tài nguyên cần thiết cho Naviplus. * * Phân loại và load các loại tài nguyên: * - JSON files: sử dụng loadJSON * - CSS files: sử dụng loadCSS (có version nếu là uigen) * - JS files: sử dụng loadJSFunc callback (có version nếu là uigen) * * @param {string|string[]} vars - URL hoặc array các URLs cần load * @param {Object} naviSetting - Setting object để truyền vào JS loader * @param {function} loadJSFunc - Function để load JavaScript files * * @example * navistart.initResources([ * 'style.css', * 'uigen.js', * 'config.json' * ], setting, customJSLoader); */ navistart.initResources = function(vars, naviSetting, loadJSFunc) { const cs = navistart.findCurrentScript(); if (!cs) return; const ver = navistart.getVersionFromScript(cs, 'src'); const resources = Array.isArray(vars) ? vars : [vars]; resources.forEach(url => { if (!url || typeof url !== 'string') return; const isUigen = url.includes('/uigen.js') || url.includes('/uigen.css'); if (url.includes('.json')) { navistart.loadJSON(url); } else if (url.includes('.css')) { navistart.loadCSS(url, isUigen ? ver : null); } else if (url.includes('.js') || url.includes('javascript')) { if (loadJSFunc && typeof loadJSFunc === 'function') { loadJSFunc(url, isUigen ? ver : null, naviSetting); } } }); }; /** * Kiểm tra shop có hợp lệ để sử dụng Naviplus không. * - Nếu env là "shopify": bỏ qua kiểm tra (trả true). * - Nếu env khác "shopify": shop phải trùng với root domain của trang hiện tại. * (chuẩn hóa cả 2 theo quy tắc normalizeDomainSync) * In log trạng thái các bước kiểm tra cho gỡ lỗi. * * @param {string} shop - Tên shop hoặc domain. * @param {string} env - Môi trường platform (shopify, woocommerce, v.v.). * @returns {boolean} - true nếu hợp lệ, false nếu không hợp lệ. */ navistart.isValidatedShop = function(shop, env) { if (env !== "shopify") { var shopRoot = navistart.normalizeDomainSync(shop); var currentDomain = navistart.normalizeDomainSync(window.location.hostname); if (shopRoot !== currentDomain) { console.error("Shop not valid: ", { shop, env, currentDomain }); return false; } } return true; }; /** * Lấy naviSetting một cách tiện dụng, đảm bảo tương thích cả cách cũ (gắn attribute trực tiếp trên thẻ script) * lẫn cách mới (push vào queue _navi_setting hoặc naviplusSetting). * - Ưu tiên lấy qua attribute trên script nếu có (giữ cho các shop cũ không bị lỗi). * - Nếu không, fallback sang lấy từ hàng đợi (queue) hiện đại. * - Có cơ chế chống trùng lặp embed_id để tránh load lại nhiều lần. * * @returns {Object|null} setting object chứa thông tin shop, env, embed_id, lang, ver, mode (nếu có) */ navistart.getNaviSettingCompat = function() { var curScript = document.currentScript; if (!curScript) { var scripts = document.getElementsByTagName('script'); curScript = scripts[scripts.length - 1]; } // Kiểm tra cách cũ: attribute trên script if (curScript && curScript.hasAttribute('shop')) { var setting = {}; ['shop', 'token', 'env', 'embed_id', 'embed_title', 'embed_is_full', 'embed_margin', 'not_sticky'].forEach(function(attr) { var val = curScript.getAttribute(attr); if (val !== null) setting[attr] = val; }); // Kiểm tra duplicate embed_id var embedId = setting.embed_id || ''; if (window._processedEmbedIds[embedId]) { console.warn("Skip duplicate embed_id:", embedId, "- already processed"); return null; // Trả về null để skip processing } window._processedEmbedIds[embedId] = true; console.warn("Warn: Get setting by old way (not queue) by attribute on script: ", setting); return setting; } // Fallback sang queue (cách mới) var setting = navistart.getNaviSettingFromQueue(); if (setting) { /* QUAN TRỌNG! */ /* Nếu ko chịu khai báo shop thì lấy domain hiện tại để khởi tạo shop ****/ if(!setting["shop"] || setting["shop"] == "" ) setting["shop"] = navistart.normalizeDomainSync(window.location.hostname); /* End of if *************************************************************/ // Kiểm tra duplicate embed_id cho queue cũng var embedId = setting.embed_id || ''; if (window._processedEmbedIds[embedId]) { console.warn("Skip duplicate embed_id from queue:", embedId, "- already processed"); return null; // Trả về null để skip processing } window._processedEmbedIds[embedId] = true; } return setting; }; } // End of if (!window._NAVIPLUS_START) /** Chuẩn bị _NAVIPLUS và môi trường sử dụng ******************************************************/ (function () { // Bước 1: Lấy naviSetting từ 2 cách: attribute cũ trên thẻ script và queue _navi_setting var naviSetting = navistart.getNaviSettingCompat(); if( naviSetting != null ) { // Bước 2: Lấy thuộc tính shop và env từ thẻ script let shop = navistart.normalizeShop (navistart.getNaviSetting(naviSetting, 'shop', '')); let env = navistart.getNaviSetting(naviSetting, 'env', ''); // Bước 3: Nếu có shop, cập nhật _NAVIPLUS.SHOP và thay thế {shop} trên các url tài nguyên navistart.updateShopAndResources(shop, _NAVIPLUS); // Bước 4: Nếu không phải Shopify và chưa có tldts, load thư viện tldts (async) từ CDN navistart.loadTLDTS(env); // Bước 5: Nếu shop hợp lệ, khởi tạo Navi+ if( navistart.isValidatedShop(shop, env) ) { _NAVIPLUS.GO.start(_NAVIPLUS.VARS, naviSetting); navistart.consoleOnce(_NAVIPLUS, '_CONSOLE_NAVIPLUS_START'); } } // Bước PL: Include file debug.js để kiểm tra lỗi Safari /** Kiểm tra lỗi Safari ****************************************************************************/ // Nó bắt toàn bộ lỗi chưa được xử lý, chỉ để debug phần lỗi trong Safari mà ko nói gì. // --> Cần phải bỏ đi sau khi fix xong lỗi trong Safari!!! /* window.addEventListener("error", e => { console.error("Global Error:", e.message, e.filename, e.lineno); }); window.addEventListener("unhandledrejection", e => { console.error("Unhandled Promise:", e.reason); }); */ // Bước PL: Auto-run queue validation khi navistart được load navistart.checkQueueEmpty(); })();