02.00
**Major Release** — introduces the long-awaited **Dark Mode Toggle**, streamlining accessibility and usability enhancements. ##Added * **Dark Mode Toggle** * Frontend toggle switch included in template. * JavaScript handles switching between light/dark modes. * Dark mode CSS rules applied across template styles. * Automatic persistence of user choice (via localStorage). * **Header Parameters Update** * Added **logo parameter support** in template settings. * Updated metadata & copyright header. * **Expanded TOC (Table of Contents)** * Automatic TOC injection when enabled. * User selects placement via article > options > layout (`toc-left` or `toc-right`). ##Improved * Cleaned up `index.php` by removing **skip-to-content** duplicate calls. * Consolidated JavaScript asset loading (ensuring dark-mode script is loaded correctly from external JS file). * Streamlined CSS for **toggle switch**, ensuring it inherits Bootstrap/Cassiopeia defaults. * General accessibility refinements in typography and color contrast. ##Fixed * Fixed missing **logo param** in header output. * Corrected stylesheet inconsistencies between Bootstrap 5 helpers and template overrides. * Patched redundant calls in script includes.
This commit is contained in:
@@ -1,147 +1,170 @@
|
||||
/**
|
||||
* darkmode-toggle.js — Floating theme switch (class-based, CSP-proof)
|
||||
* @version 2.2.1
|
||||
* Storage key: "theme" -> "light" | "dark"
|
||||
* Corner from <body data-theme-fab-pos="br|bl|tr|tl"> (default br)
|
||||
/* =========================================================================
|
||||
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
|
||||
*
|
||||
* This file is part of a Moko Consulting project.
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/ .
|
||||
* =========================================================================
|
||||
* FILE INFORMATION
|
||||
* DEFGROUP: Joomla
|
||||
* INGROUP: Moko-Cassiopeia
|
||||
* PATH: media/templates/site/moko-cassiopeia/js/darkmode-toggle.js
|
||||
* VERSION: 02.00
|
||||
* BRIEF: JavaScript logic for dark mode toggle functionality in Moko-Cassiopeia
|
||||
* =========================================================================
|
||||
*/
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
'use strict';
|
||||
|
||||
var STORAGE_KEY = 'theme';
|
||||
var docEl = document.documentElement;
|
||||
var mql = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
var STORAGE_KEY = 'theme';
|
||||
var docEl = document.documentElement;
|
||||
var mql = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
|
||||
function getStored() { try { return localStorage.getItem(STORAGE_KEY); } catch (e) { return null; } }
|
||||
function setStored(v) { try { localStorage.setItem(STORAGE_KEY, v); } catch (e) {} }
|
||||
function clearStored() { try { localStorage.removeItem(STORAGE_KEY); } catch (e) {} }
|
||||
function systemTheme() { return mql.matches ? 'dark' : 'light'; }
|
||||
function getStored() { try { return localStorage.getItem(STORAGE_KEY); } catch (e) { return null; } }
|
||||
function setStored(v) { try { localStorage.setItem(STORAGE_KEY, v); } catch (e) {} }
|
||||
function clearStored() { try { localStorage.removeItem(STORAGE_KEY); } catch (e) {} }
|
||||
function systemTheme() { return mql.matches ? 'dark' : 'light'; }
|
||||
|
||||
function applyTheme(theme) {
|
||||
docEl.setAttribute('data-bs-theme', theme);
|
||||
docEl.setAttribute('data-aria-theme', theme);
|
||||
var meta = document.querySelector('meta[name="theme-color"]');
|
||||
if (meta) {
|
||||
meta.setAttribute('content', theme === 'dark' ? '#0f1115' : '#ffffff');
|
||||
}
|
||||
var sw = document.getElementById('mokoThemeSwitch');
|
||||
if (sw) {
|
||||
sw.setAttribute('aria-checked', theme === 'dark' ? 'true' : 'false');
|
||||
}
|
||||
}
|
||||
function applyTheme(theme) {
|
||||
docEl.setAttribute('data-bs-theme', theme);
|
||||
docEl.setAttribute('data-aria-theme', theme);
|
||||
var meta = document.querySelector('meta[name="theme-color"]');
|
||||
if (meta) {
|
||||
meta.setAttribute('content', theme === 'dark' ? '#0f1115' : '#ffffff');
|
||||
}
|
||||
var sw = document.getElementById('mokoThemeSwitch');
|
||||
if (sw) {
|
||||
sw.setAttribute('aria-checked', theme === 'dark' ? 'true' : 'false');
|
||||
}
|
||||
}
|
||||
|
||||
function initTheme() {
|
||||
var stored = getStored();
|
||||
applyTheme(stored ? stored : systemTheme());
|
||||
}
|
||||
function initTheme() {
|
||||
var stored = getStored();
|
||||
applyTheme(stored ? stored : systemTheme());
|
||||
}
|
||||
|
||||
function posClassFromBody() {
|
||||
var pos = (document.body.getAttribute('data-theme-fab-pos') || 'br').toLowerCase();
|
||||
if (!/^(br|bl|tr|tl)$/.test(pos)) pos = 'br';
|
||||
return 'pos-' + pos;
|
||||
}
|
||||
function posClassFromBody() {
|
||||
var pos = (document.body.getAttribute('data-theme-fab-pos') || 'br').toLowerCase();
|
||||
if (!/^(br|bl|tr|tl)$/.test(pos)) pos = 'br';
|
||||
return 'pos-' + pos;
|
||||
}
|
||||
|
||||
function buildToggle() {
|
||||
if (document.getElementById('mokoThemeFab')) return;
|
||||
function buildToggle() {
|
||||
if (document.getElementById('mokoThemeFab')) return;
|
||||
|
||||
var wrap = document.createElement('div');
|
||||
wrap.id = 'mokoThemeFab';
|
||||
wrap.className = posClassFromBody();
|
||||
var wrap = document.createElement('div');
|
||||
wrap.id = 'mokoThemeFab';
|
||||
wrap.className = posClassFromBody();
|
||||
|
||||
// Light label
|
||||
var lblL = document.createElement('span');
|
||||
lblL.className = 'label';
|
||||
lblL.textContent = 'Light';
|
||||
// Light label
|
||||
var lblL = document.createElement('span');
|
||||
lblL.className = 'label';
|
||||
lblL.textContent = 'Light';
|
||||
|
||||
// Switch
|
||||
var switchWrap = document.createElement('button');
|
||||
switchWrap.id = 'mokoThemeSwitch';
|
||||
switchWrap.type = 'button';
|
||||
switchWrap.setAttribute('role', 'switch');
|
||||
switchWrap.setAttribute('aria-label', 'Toggle dark mode');
|
||||
switchWrap.setAttribute('aria-checked', 'false'); // updated after init
|
||||
// Switch
|
||||
var switchWrap = document.createElement('button');
|
||||
switchWrap.id = 'mokoThemeSwitch';
|
||||
switchWrap.type = 'button';
|
||||
switchWrap.setAttribute('role', 'switch');
|
||||
switchWrap.setAttribute('aria-label', 'Toggle dark mode');
|
||||
switchWrap.setAttribute('aria-checked', 'false'); // updated after init
|
||||
|
||||
var track = document.createElement('span');
|
||||
track.className = 'switch';
|
||||
var knob = document.createElement('span');
|
||||
knob.className = 'knob';
|
||||
track.appendChild(knob);
|
||||
switchWrap.appendChild(track);
|
||||
var track = document.createElement('span');
|
||||
track.className = 'switch';
|
||||
var knob = document.createElement('span');
|
||||
knob.className = 'knob';
|
||||
track.appendChild(knob);
|
||||
switchWrap.appendChild(track);
|
||||
|
||||
// Dark label
|
||||
var lblD = document.createElement('span');
|
||||
lblD.className = 'label';
|
||||
lblD.textContent = 'Dark';
|
||||
// Dark label
|
||||
var lblD = document.createElement('span');
|
||||
lblD.className = 'label';
|
||||
lblD.textContent = 'Dark';
|
||||
|
||||
// Auto button
|
||||
var auto = document.createElement('button');
|
||||
auto.id = 'mokoThemeAuto';
|
||||
auto.type = 'button';
|
||||
auto.className = 'btn btn-sm btn-link text-decoration-none px-2';
|
||||
auto.setAttribute('aria-label', 'Follow system theme');
|
||||
auto.textContent = 'Auto';
|
||||
// Auto button
|
||||
var auto = document.createElement('button');
|
||||
auto.id = 'mokoThemeAuto';
|
||||
auto.type = 'button';
|
||||
auto.className = 'btn btn-sm btn-link text-decoration-none px-2';
|
||||
auto.setAttribute('aria-label', 'Follow system theme');
|
||||
auto.textContent = 'Auto';
|
||||
|
||||
// Behavior
|
||||
switchWrap.addEventListener('click', function () {
|
||||
var current = (docEl.getAttribute('data-bs-theme') || 'light').toLowerCase();
|
||||
var next = current === 'dark' ? 'light' : 'dark';
|
||||
applyTheme(next);
|
||||
setStored(next);
|
||||
});
|
||||
// Behavior
|
||||
switchWrap.addEventListener('click', function () {
|
||||
var current = (docEl.getAttribute('data-bs-theme') || 'light').toLowerCase();
|
||||
var next = current === 'dark' ? 'light' : 'dark';
|
||||
applyTheme(next);
|
||||
setStored(next);
|
||||
});
|
||||
|
||||
auto.addEventListener('click', function () {
|
||||
clearStored();
|
||||
applyTheme(systemTheme());
|
||||
});
|
||||
auto.addEventListener('click', function () {
|
||||
clearStored();
|
||||
applyTheme(systemTheme());
|
||||
});
|
||||
|
||||
// Respond to OS changes only when not user-forced
|
||||
var onMql = function () {
|
||||
if (!getStored()) applyTheme(systemTheme());
|
||||
};
|
||||
if (typeof mql.addEventListener === 'function') mql.addEventListener('change', onMql);
|
||||
else if (typeof mql.addListener === 'function') mql.addListener(onMql);
|
||||
// Respond to OS changes only when not user-forced
|
||||
var onMql = function () {
|
||||
if (!getStored()) applyTheme(systemTheme());
|
||||
};
|
||||
if (typeof mql.addEventListener === 'function') mql.addEventListener('change', onMql);
|
||||
else if (typeof mql.addListener === 'function') mql.addListener(onMql);
|
||||
|
||||
// Initial state
|
||||
var initial = getStored() || systemTheme();
|
||||
switchWrap.setAttribute('aria-checked', initial === 'dark' ? 'true' : 'false');
|
||||
// Initial state
|
||||
var initial = getStored() || systemTheme();
|
||||
switchWrap.setAttribute('aria-checked', initial === 'dark' ? 'true' : 'false');
|
||||
|
||||
// Mount
|
||||
wrap.appendChild(lblL);
|
||||
wrap.appendChild(switchWrap);
|
||||
wrap.appendChild(lblD);
|
||||
wrap.appendChild(auto);
|
||||
document.body.appendChild(wrap);
|
||||
// Mount
|
||||
wrap.appendChild(lblL);
|
||||
wrap.appendChild(switchWrap);
|
||||
wrap.appendChild(lblD);
|
||||
wrap.appendChild(auto);
|
||||
document.body.appendChild(wrap);
|
||||
|
||||
// Debug helper
|
||||
window.mokoThemeFabStatus = function () {
|
||||
var el = document.getElementById('mokoThemeFab');
|
||||
if (!el) return { mounted: false };
|
||||
var r = el.getBoundingClientRect();
|
||||
return {
|
||||
mounted: true,
|
||||
rect: { top: r.top, left: r.left, width: r.width, height: r.height },
|
||||
zIndex: window.getComputedStyle(el).zIndex,
|
||||
posClass: el.className
|
||||
};
|
||||
};
|
||||
// Debug helper
|
||||
window.mokoThemeFabStatus = function () {
|
||||
var el = document.getElementById('mokoThemeFab');
|
||||
if (!el) return { mounted: false };
|
||||
var r = el.getBoundingClientRect();
|
||||
return {
|
||||
mounted: true,
|
||||
rect: { top: r.top, left: r.left, width: r.width, height: r.height },
|
||||
zIndex: window.getComputedStyle(el).zIndex,
|
||||
posClass: el.className
|
||||
};
|
||||
};
|
||||
|
||||
// Outline if invisible
|
||||
setTimeout(function () {
|
||||
var r = wrap.getBoundingClientRect();
|
||||
if (r.width < 10 || r.height < 10) {
|
||||
wrap.classList.add('debug-outline');
|
||||
console.warn('[moko] Theme FAB mounted but appears too small — check CSS collisions.');
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
// Outline if invisible
|
||||
setTimeout(function () {
|
||||
var r = wrap.getBoundingClientRect();
|
||||
if (r.width < 10 || r.height < 10) {
|
||||
wrap.classList.add('debug-outline');
|
||||
console.warn('[moko] Theme FAB mounted but appears too small — check CSS collisions.');
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
|
||||
function init() {
|
||||
initTheme();
|
||||
buildToggle();
|
||||
}
|
||||
function init() {
|
||||
initTheme();
|
||||
buildToggle();
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -0,0 +1,347 @@
|
||||
/*
|
||||
=========================================================================
|
||||
Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
|
||||
This file is part of a Moko Consulting project.
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see https://www.gnu.org/licenses/ .
|
||||
=========================================================================
|
||||
FILE INFORMATION
|
||||
DEFGROUP: Joomla Template
|
||||
FILE: media/templates/site/moko-cassiopeia/js/gtm.js
|
||||
HEADER VERSION: 1.0
|
||||
VERSION: 2.0
|
||||
BRIEF: Safe, configurable Google Tag Manager loader for Moko-Cassiopeia.
|
||||
PATH: media/templates/site/moko-cassiopeia/js/gtm.js
|
||||
NOTE: Place the <noscript> fallback iframe in your HTML template (index.php). A JS file
|
||||
cannot provide a true no-JS fallback by definition.
|
||||
VARIABLES:
|
||||
- window.MOKO_GTM_ID (string) // Optional global GTM container ID (e.g., "GTM-XXXXXXX")
|
||||
- window.MOKO_GTM_OPTIONS (object) // Optional global options (see JSDoc below)
|
||||
- data- attributes on the script tag or <html>/<body>:
|
||||
data-gtm-id, data-data-layer, data-debug, data-ignore-dnt,
|
||||
data-env-auth, data-env-preview, data-block-on-dev
|
||||
=========================================================================
|
||||
*/
|
||||
|
||||
/* global window, document, navigator */
|
||||
(() => {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* @typedef {Object} MokoGtmOptions
|
||||
* @property {string} [id] GTM container ID (e.g., "GTM-XXXXXXX")
|
||||
* @property {string} [dataLayerName] Custom dataLayer name (default: "dataLayer")
|
||||
* @property {boolean} [debug] Log debug messages to console (default: false)
|
||||
* @property {boolean} [ignoreDNT] Ignore Do Not Track and always load (default: false)
|
||||
* @property {boolean} [blockOnDev] Block loading on localhost/*.test/127.0.0.1 (default: true)
|
||||
* @property {string} [envAuth] GTM Environment auth string (optional)
|
||||
* @property {string} [envPreview] GTM Environment preview name (optional)
|
||||
* @property {Record<string,'granted'|'denied'>} [consentDefault]
|
||||
* Default Consent Mode v2 map. Keys like:
|
||||
* analytics_storage, ad_storage, ad_user_data, ad_personalization, functionality_storage, security_storage
|
||||
* (default: {analytics_storage:'granted', functionality_storage:'granted', security_storage:'granted'})
|
||||
* @property {() => (Record<string, any>|void)} [pageVars]
|
||||
* Function returning extra page variables to push on init (optional)
|
||||
*/
|
||||
|
||||
const PKG = "moko-gtm";
|
||||
const PREFIX = `[${PKG}]`;
|
||||
const WIN = window;
|
||||
|
||||
// Public API placeholder (attached to window at the end)
|
||||
/** @type {{
|
||||
* init: (opts?: Partial<MokoGtmOptions>) => void,
|
||||
* setConsent: (updates: Record<string,'granted'|'denied'>) => void,
|
||||
* push: (...args:any[]) => void,
|
||||
* isLoaded: () => boolean,
|
||||
* config: () => Required<MokoGtmOptions>
|
||||
* }} */
|
||||
const API = {};
|
||||
|
||||
// ---- Utilities ---------------------------------------------------------
|
||||
|
||||
const isDevHost = () => {
|
||||
const h = WIN.location && WIN.location.hostname || "";
|
||||
return (
|
||||
h === "localhost" ||
|
||||
h === "127.0.0.1" ||
|
||||
h.endsWith(".local") ||
|
||||
h.endsWith(".test")
|
||||
);
|
||||
};
|
||||
|
||||
const dntEnabled = () => {
|
||||
// Different browsers expose DNT differently; treat "1" or "yes" as enabled.
|
||||
const n = navigator;
|
||||
const v = (n.doNotTrack || n.msDoNotTrack || (n.navigator && n.navigator.doNotTrack) || "").toString().toLowerCase();
|
||||
return v === "1" || v === "yes";
|
||||
};
|
||||
|
||||
const getCurrentScript = () => {
|
||||
// document.currentScript is best; fallback to last <script> whose src ends with /gtm.js
|
||||
const cs = document.currentScript;
|
||||
if (cs) return cs;
|
||||
const scripts = Array.from(document.getElementsByTagName("script"));
|
||||
return scripts.reverse().find(s => (s.getAttribute("src") || "").includes("/gtm.js")) || null;
|
||||
};
|
||||
|
||||
const getAttr = (el, name) => el ? el.getAttribute(name) : null;
|
||||
|
||||
const readDatasetCascade = (name) => {
|
||||
// Check <script>, <html>, <body>, then <meta name="moko:gtm-<name>">
|
||||
const script = getCurrentScript();
|
||||
const html = document.documentElement;
|
||||
const body = document.body;
|
||||
const meta = document.querySelector(`meta[name="moko:gtm-${name}"]`);
|
||||
return (
|
||||
(script && script.dataset && script.dataset[name]) ||
|
||||
(html && html.dataset && html.dataset[name]) ||
|
||||
(body && body.dataset && body.dataset[name]) ||
|
||||
(meta && meta.getAttribute("content")) ||
|
||||
null
|
||||
);
|
||||
};
|
||||
|
||||
const parseBool = (v, fallback = false) => {
|
||||
if (v == null) return fallback;
|
||||
const s = String(v).trim().toLowerCase();
|
||||
if (["1","true","yes","y","on"].includes(s)) return true;
|
||||
if (["0","false","no","n","off"].includes(s)) return false;
|
||||
return fallback;
|
||||
};
|
||||
|
||||
const debugLog = (...args) => {
|
||||
if (STATE.debug) {
|
||||
try { console.info(PREFIX, ...args); } catch (_) {}
|
||||
}
|
||||
};
|
||||
|
||||
// ---- Configuration & State --------------------------------------------
|
||||
|
||||
/** @type {Required<MokoGtmOptions>} */
|
||||
const STATE = {
|
||||
id: "",
|
||||
dataLayerName: "dataLayer",
|
||||
debug: false,
|
||||
ignoreDNT: false,
|
||||
blockOnDev: true,
|
||||
envAuth: "",
|
||||
envPreview: "",
|
||||
consentDefault: {
|
||||
analytics_storage: "granted",
|
||||
functionality_storage: "granted",
|
||||
security_storage: "granted",
|
||||
// The following default to "denied" unless the site explicitly opts-in:
|
||||
ad_storage: "denied",
|
||||
ad_user_data: "denied",
|
||||
ad_personalization: "denied",
|
||||
},
|
||||
pageVars: () => ({})
|
||||
};
|
||||
|
||||
const mergeOptions = (base, extra = {}) => {
|
||||
const out = {...base};
|
||||
for (const k in extra) {
|
||||
if (!Object.prototype.hasOwnProperty.call(extra, k)) continue;
|
||||
const v = extra[k];
|
||||
if (v && typeof v === "object" && !Array.isArray(v)) {
|
||||
out[k] = {...(out[k] || {}), ...v};
|
||||
} else if (v !== undefined) {
|
||||
out[k] = v;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
const detectOptions = () => {
|
||||
// 1) Global window options
|
||||
/** @type {Partial<MokoGtmOptions>} */
|
||||
const globalOpts = (WIN.MOKO_GTM_OPTIONS && typeof WIN.MOKO_GTM_OPTIONS === "object") ? WIN.MOKO_GTM_OPTIONS : {};
|
||||
|
||||
// 2) Dataset / meta
|
||||
const idFromData = readDatasetCascade("id") || WIN.MOKO_GTM_ID || "";
|
||||
const dlFromData = readDatasetCascade("dataLayer") || "";
|
||||
const dbgFromData = readDatasetCascade("debug");
|
||||
const dntFromData = readDatasetCascade("ignoreDnt");
|
||||
const devFromData = readDatasetCascade("blockOnDev");
|
||||
const authFromData = readDatasetCascade("envAuth") || "";
|
||||
const prevFromData = readDatasetCascade("envPreview") || "";
|
||||
|
||||
// 3) Combine
|
||||
/** @type {Partial<MokoGtmOptions>} */
|
||||
const detected = {
|
||||
id: idFromData || globalOpts.id || "",
|
||||
dataLayerName: dlFromData || globalOpts.dataLayerName || undefined,
|
||||
debug: parseBool(dbgFromData, !!globalOpts.debug),
|
||||
ignoreDNT: parseBool(dntFromData, !!globalOpts.ignoreDNT),
|
||||
blockOnDev: parseBool(devFromData, (globalOpts.blockOnDev ?? true)),
|
||||
envAuth: authFromData || globalOpts.envAuth || "",
|
||||
envPreview: prevFromData || globalOpts.envPreview || "",
|
||||
consentDefault: globalOpts.consentDefault || undefined,
|
||||
pageVars: typeof globalOpts.pageVars === "function" ? globalOpts.pageVars : undefined
|
||||
};
|
||||
|
||||
return detected;
|
||||
};
|
||||
|
||||
// ---- dataLayer / gtag helpers -----------------------------------------
|
||||
|
||||
const ensureDataLayer = () => {
|
||||
const l = STATE.dataLayerName;
|
||||
WIN[l] = WIN[l] || [];
|
||||
return WIN[l];
|
||||
};
|
||||
|
||||
/** gtag wrapper backed by dataLayer. */
|
||||
const gtag = (...args) => {
|
||||
const dl = ensureDataLayer();
|
||||
dl.push(arguments.length > 1 ? args : args[0]);
|
||||
debugLog("gtag push:", args);
|
||||
};
|
||||
|
||||
API.push = (...args) => gtag(...args);
|
||||
|
||||
API.setConsent = (updates) => {
|
||||
gtag("consent", "update", updates || {});
|
||||
};
|
||||
|
||||
API.isLoaded = () => {
|
||||
const hasScript = !!document.querySelector('script[src*="googletagmanager.com/gtm.js"]');
|
||||
return hasScript;
|
||||
};
|
||||
|
||||
API.config = () => ({...STATE});
|
||||
|
||||
// ---- Loader ------------------------------------------------------------
|
||||
|
||||
const buildEnvQuery = () => {
|
||||
const qp = [];
|
||||
if (STATE.envAuth) qp.push(`gtm_auth=${encodeURIComponent(STATE.envAuth)}`);
|
||||
if (STATE.envPreview) qp.push(`gtm_preview=${encodeURIComponent(STATE.envPreview)}`, "gtm_cookies_win=x");
|
||||
return qp.length ? `&${qp.join("&")}` : "";
|
||||
};
|
||||
|
||||
const injectScript = () => {
|
||||
if (!STATE.id) {
|
||||
debugLog("GTM ID missing; aborting load.");
|
||||
return;
|
||||
}
|
||||
if (API.isLoaded()) {
|
||||
debugLog("GTM already loaded; skipping duplicate injection.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Standard GTM bootstrap timing event
|
||||
const dl = ensureDataLayer();
|
||||
dl.push({ "gtm.start": new Date().getTime(), event: "gtm.js" });
|
||||
|
||||
const f = document.getElementsByTagName("script")[0];
|
||||
const j = document.createElement("script");
|
||||
j.async = true;
|
||||
j.src = `https://www.googletagmanager.com/gtm.js?id=${encodeURIComponent(STATE.id)}${STATE.dataLayerName !== "dataLayer" ? `&l=${encodeURIComponent(STATE.dataLayerName)}` : ""}${buildEnvQuery()}`;
|
||||
if (f && f.parentNode) {
|
||||
f.parentNode.insertBefore(j, f);
|
||||
} else {
|
||||
(document.head || document.documentElement).appendChild(j);
|
||||
}
|
||||
debugLog("Injected GTM script:", j.src);
|
||||
};
|
||||
|
||||
const applyDefaultConsent = () => {
|
||||
// Consent Mode v2 default
|
||||
gtag("consent", "default", STATE.consentDefault);
|
||||
debugLog("Applied default consent:", STATE.consentDefault);
|
||||
};
|
||||
|
||||
const pushInitialVars = () => {
|
||||
// Minimal page vars; allow site to add more via pageVars()
|
||||
const vars = {
|
||||
event: "moko.page_init",
|
||||
page_title: document.title || "",
|
||||
page_language: (document.documentElement && document.documentElement.lang) || "",
|
||||
...(typeof STATE.pageVars === "function" ? (STATE.pageVars() || {}) : {})
|
||||
};
|
||||
gtag(vars);
|
||||
};
|
||||
|
||||
const shouldLoad = () => {
|
||||
if (!STATE.ignoreDNT && dntEnabled()) {
|
||||
debugLog("DNT is enabled; blocking GTM load (set ignoreDNT=true to override).");
|
||||
return false;
|
||||
}
|
||||
if (STATE.blockOnDev && isDevHost()) {
|
||||
debugLog("Development host detected; blocking GTM load (set blockOnDev=false to override).");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// ---- Public init -------------------------------------------------------
|
||||
|
||||
API.init = (opts = {}) => {
|
||||
// Merge: defaults <- detected <- passed opts
|
||||
const detected = detectOptions();
|
||||
const merged = mergeOptions(STATE, mergeOptions(detected, opts));
|
||||
|
||||
// Commit back to STATE
|
||||
Object.assign(STATE, merged);
|
||||
|
||||
debugLog("Config:", STATE);
|
||||
|
||||
// Prepare dataLayer/gtag and consent
|
||||
ensureDataLayer();
|
||||
applyDefaultConsent();
|
||||
pushInitialVars();
|
||||
|
||||
// Load GTM if allowed
|
||||
if (shouldLoad()) {
|
||||
injectScript();
|
||||
} else {
|
||||
debugLog("GTM load prevented by configuration or environment.");
|
||||
}
|
||||
};
|
||||
|
||||
// ---- Auto-init on DOMContentLoaded (safe even if deferred) -------------
|
||||
|
||||
const autoInit = () => {
|
||||
// Only auto-init if we have some ID from globals/datasets.
|
||||
const detected = detectOptions();
|
||||
const hasId = !!(detected.id || WIN.MOKO_GTM_ID);
|
||||
if (hasId) {
|
||||
API.init(); // use detected/global defaults
|
||||
} else {
|
||||
debugLog("No GTM ID detected; awaiting manual init via window.mokoGTM.init({ id: 'GTM-XXXXXXX' }).");
|
||||
}
|
||||
};
|
||||
|
||||
if (document.readyState === "complete" || document.readyState === "interactive") {
|
||||
// Defer to ensure <body> exists for any late consumers.
|
||||
setTimeout(autoInit, 0);
|
||||
} else {
|
||||
document.addEventListener("DOMContentLoaded", autoInit, { once: true });
|
||||
}
|
||||
|
||||
// Expose API
|
||||
WIN.mokoGTM = API;
|
||||
|
||||
// Helpful console hint (only if debug true after detection)
|
||||
try {
|
||||
const detected = detectOptions();
|
||||
if (parseBool(detected.debug, false)) {
|
||||
STATE.debug = true;
|
||||
debugLog("Ready. You can call window.mokoGTM.init({ id: 'GTM-XXXXXXX' }).");
|
||||
}
|
||||
} catch (_) {}
|
||||
})();
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
/**
|
||||
* @package Joomla.Site
|
||||
* @subpackage Templates.Moko-Cassiopeia
|
||||
* @file /media/templates/site/moko-cassiopeia/js/mod_gabble.js
|
||||
* @copyright (C) 2025 Jonathan Miler || Moko Consulting <https://mokoconsulting.tech>
|
||||
* @website: https://mokoconsulting.tech
|
||||
* @email: hello@mokoconsulting.tech
|
||||
* @phone: +1 (931) 279-6313
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
* @note This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*/
|
||||
@@ -1,118 +0,0 @@
|
||||
<!--
|
||||
* Copyright (C) 2025 Moko Consulting <jmiller@mokoconsulting.tech>
|
||||
*
|
||||
* This file is part of a Moko Consulting project.
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<!--FILE INFORMATION
|
||||
* DEFGROUP: Joomla.Site
|
||||
* INGROUP: Templates.Moko-Cassiopeia
|
||||
* FILE: index.html
|
||||
* BRIEF: Security redirect page to block folder access and forward to site root.
|
||||
-->
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Redirecting…</title>
|
||||
|
||||
<!-- Search engines: do not index this placeholder redirect page -->
|
||||
<meta name="robots" content="noindex, nofollow, noarchive" />
|
||||
|
||||
<!-- Instant redirect fallback even if JavaScript is disabled -->
|
||||
<meta http-equiv="refresh" content="0; url=/" />
|
||||
|
||||
<!-- Canonical root reference -->
|
||||
<link rel="canonical" href="/" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<script>
|
||||
/**
|
||||
* @defgroup Dolibarr
|
||||
* @file index.html (embedded script)
|
||||
* @version 1.0.0
|
||||
* @brief Security redirect logic. Replaces the current history entry with the site root.
|
||||
* @details This script computes the absolute root URL using `location.origin` and
|
||||
* forwards the user immediately. It prevents leaving the protected folder
|
||||
* in the browser history by default.
|
||||
*
|
||||
* @section VARIABLES
|
||||
* @var {Object} opts Configuration options for the redirect behavior.
|
||||
* @var {string} opts.fallbackPath Path used when `location.origin` cannot be determined.
|
||||
* @var {number} opts.delayMs Optional delay in milliseconds before redirecting.
|
||||
* @var {"replace"|"assign"} opts.behavior Navigation method used for the redirect.
|
||||
*
|
||||
* @section OPTIONS
|
||||
* - opts.fallbackPath: default "/" (root path)
|
||||
* - opts.delayMs: default 0 (immediate)
|
||||
* - opts.behavior: one of
|
||||
* * "replace" — calls `location.replace(url)`; does not keep the folder page in history.
|
||||
* * "assign" — calls `location.assign(url)`; keeps an extra history entry.
|
||||
*/
|
||||
(function redirectToRoot() {
|
||||
// Configuration object with safe defaults.
|
||||
var opts = {
|
||||
fallbackPath: "/", // string: fallback destination if origin is unavailable
|
||||
delayMs: 0, // number: delay before redirect in ms (0 = immediate)
|
||||
behavior: "replace" // enum: "replace" | "assign"
|
||||
};
|
||||
|
||||
// Determine absolute origin in all mainstream browsers.
|
||||
var origin = (typeof location.origin === "string" && location.origin)
|
||||
|| (location.protocol + "//" + location.host);
|
||||
|
||||
// Final destination: absolute root of the current site, or fallback path.
|
||||
var destination = origin ? origin + "/" : opts.fallbackPath;
|
||||
|
||||
function go() {
|
||||
if (opts.behavior === "assign") {
|
||||
location.assign(destination);
|
||||
} else {
|
||||
location.replace(destination);
|
||||
}
|
||||
}
|
||||
|
||||
// Execute redirect, optionally after a short delay.
|
||||
if (opts.delayMs > 0) {
|
||||
setTimeout(go, opts.delayMs);
|
||||
} else {
|
||||
go();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!--
|
||||
Secondary meta-refresh for no-JS environments is already set above.
|
||||
Some very old crawlers may ignore JS; the meta refresh ensures coverage.
|
||||
-->
|
||||
|
||||
<noscript>
|
||||
<!-- Extra defense-in-depth: if JS is disabled, meta refresh (above) handles redirect. -->
|
||||
<style>
|
||||
html, body { height:100%; }
|
||||
body { display:flex; align-items:center; justify-content:center; margin:0; font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
|
||||
.msg { opacity: .75; text-align: center; }
|
||||
</style>
|
||||
</noscript>
|
||||
</head>
|
||||
<body>
|
||||
<div class="msg">Redirecting to the site root… If you are not redirected, <a href="/">click here</a>.</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,13 +1,30 @@
|
||||
/**
|
||||
* @package Joomla.Site
|
||||
* @subpackage Templates.Moko-Cassiopeia
|
||||
* @file /media/templates/site/moko-cassiopeia/js/mod_menu/menu-metismenu-es5.js
|
||||
* @copyright (C) 2025 Jonathan Miler || Moko Consulting <https://mokoconsulting.tech>
|
||||
* @website: https://mokoconsulting.tech
|
||||
* @email: hello@mokoconsulting.tech
|
||||
* @phone: +1 (931) 279-6313
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
* @note This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
/* =========================================================================
|
||||
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
|
||||
*
|
||||
* This file is part of a Moko Consulting project.
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/ .
|
||||
* =========================================================================
|
||||
* FILE INFORMATION
|
||||
* DEFGROUP: Joomla
|
||||
* INGROUP: Moko-Cassiopeia
|
||||
* PATH: media/templates/site/moko-cassiopeia/js/mod_menu/menu-metismenu-es5.js
|
||||
* VERSION: 02.00
|
||||
* BRIEF: ES5-compatible MetisMenu script for Joomla mod_menu in Moko-Cassiopeia
|
||||
* =========================================================================
|
||||
*/
|
||||
|
||||
(function () {
|
||||
|
||||
@@ -1,13 +1,30 @@
|
||||
/**
|
||||
* @package Joomla.Site
|
||||
* @subpackage Templates.Moko-Cassiopeia
|
||||
* @file /media/templates/site/moko-cassiopeia/js/mod_menu/menu-metismenu.js
|
||||
* @copyright (C) 2025 Jonathan Miler || Moko Consulting <https://mokoconsulting.tech>
|
||||
* @website: https://mokoconsulting.tech
|
||||
* @email: hello@mokoconsulting.tech
|
||||
* @phone: +1 (931) 279-6313
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
* @note This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
/* =========================================================================
|
||||
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
|
||||
*
|
||||
* This file is part of a Moko Consulting project.
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/ .
|
||||
* =========================================================================
|
||||
* FILE INFORMATION
|
||||
* DEFGROUP: Joomla
|
||||
* INGROUP: Moko-Cassiopeia
|
||||
* PATH: media/templates/site/moko-cassiopeia/js/mod_menu/menu-metismenu.js
|
||||
* VERSION: 02.00
|
||||
* BRIEF: Modern MetisMenu script for Joomla mod_menu in Moko-Cassiopeia
|
||||
* =========================================================================
|
||||
*/
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
@@ -1,25 +1,14 @@
|
||||
/**
|
||||
* template.js — Custom JavaScript for the Moko Cassiopeia Joomla template
|
||||
*
|
||||
* @package Joomla.Site
|
||||
* @subpackage Templates.Moko-Cassiopeia
|
||||
* @file /media/templates/site/moko-cassiopeia/js/template.js
|
||||
* @version 2.0
|
||||
*
|
||||
* @copyright (C) 2025 Moko Consulting
|
||||
* @author Jonathan Miller
|
||||
* @website https://mokoconsulting.tech
|
||||
* @email hello@mokoconsulting.tech
|
||||
* @phone +1 (931) 279-6313
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
/* =========================================================================
|
||||
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
|
||||
*
|
||||
* This file is part of a Moko Consulting project.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
@@ -27,7 +16,15 @@
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/ .
|
||||
* =========================================================================
|
||||
* FILE INFORMATION
|
||||
* DEFGROUP: Joomla
|
||||
* INGROUP: Moko-Cassiopeia
|
||||
* PATH: media/templates/site/moko-cassiopeia/js/template.js
|
||||
* VERSION: 02.00
|
||||
* BRIEF: Core JavaScript utilities and behaviors for Moko-Cassiopeia template
|
||||
* =========================================================================
|
||||
*/
|
||||
|
||||
(function (win, doc) {
|
||||
|
||||
@@ -1,25 +1,14 @@
|
||||
/**
|
||||
* theme-init.js — Light/Dark mode initialization for Moko Cassiopeia
|
||||
*
|
||||
* @package Joomla.Site
|
||||
* @subpackage Templates.Moko-Cassiopeia
|
||||
* @file /media/templates/site/moko-cassiopeia/js/theme-init.js
|
||||
* @version 2.0
|
||||
*
|
||||
* @copyright (C) 2025 Moko Consulting
|
||||
* @author Jonathan Miller
|
||||
* @website https://mokoconsulting.tech
|
||||
* @email hello@mokoconsulting.tech
|
||||
* @phone +1 (931) 279-6313
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
/* =========================================================================
|
||||
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
|
||||
*
|
||||
* This file is part of a Moko Consulting project.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
@@ -27,7 +16,15 @@
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/ .
|
||||
* =========================================================================
|
||||
* FILE INFORMATION
|
||||
* DEFGROUP: Joomla
|
||||
* INGROUP: Moko-Cassiopeia
|
||||
* PATH: media/templates/site/moko-cassiopeia/js/theme-init.js
|
||||
* VERSION: 02.00
|
||||
* BRIEF: Initialization script for Moko-Cassiopeia theme features and behaviors
|
||||
* =========================================================================
|
||||
*/
|
||||
|
||||
(function (win, doc) {
|
||||
|
||||
@@ -1,25 +1,14 @@
|
||||
/**
|
||||
* user.js — User Custom JS File for Moko Cassiopeia
|
||||
*
|
||||
* @package Joomla.Site
|
||||
* @subpackage Templates.Moko-Cassiopeia
|
||||
* @file /media/templates/site/moko-cassiopeia/js/user.js
|
||||
* @version 2.0
|
||||
*
|
||||
* @copyright (C) 2025 Moko Consulting
|
||||
* @author Jonathan Miller
|
||||
* @website https://mokoconsulting.tech
|
||||
* @email hello@mokoconsulting.tech
|
||||
* @phone +1 (931) 279-6313
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
/* =========================================================================
|
||||
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
|
||||
*
|
||||
* This file is part of a Moko Consulting project.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
@@ -27,5 +16,13 @@
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/ .
|
||||
* =========================================================================
|
||||
* FILE INFORMATION
|
||||
* DEFGROUP: Joomla
|
||||
* INGROUP: Moko-Cassiopeia
|
||||
* PATH: media/templates/site/moko-cassiopeia/js/user.js
|
||||
* VERSION: 02.00
|
||||
* BRIEF: JavaScript for handling user-specific interactions in Moko-Cassiopeia template
|
||||
* =========================================================================
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user