v1.0.0.8
Publisium
- (function () {
- 'use strict';
-
- var THEME_KEY = 'publisium_theme';
-
- function applyTheme(theme) {
- document.body.setAttribute('data-theme', theme);
- }
-
- function initTheme() {
- var stored = null;
- try {
- stored = localStorage.getItem(THEME_KEY);
- } catch (error) {
- }
- if (stored === 'dark' || stored === 'light') {
- applyTheme(stored);
- return;
- }
- var prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
- applyTheme(prefersDark ? 'dark' : 'light');
- }
-
- function toggleTheme() {
- var current = document.body.getAttribute('data-theme') || 'light';
- var next = current === 'dark' ? 'light' : 'dark';
- applyTheme(next);
- try {
- localStorage.setItem(THEME_KEY, next);
- } catch (error) {
- }
- }
-
- var CONSENT_KEY = 'publisium_consent';
-
- // The optional categories the banner offers. A category that isn't offered is never allowed.
- function consentCategories() {
- var banner = document.getElementById('consent-banner');
- return banner ? (banner.getAttribute('data-categories') || '').split(/\s+/).filter(Boolean) : [];
- }
-
- // Returns the visitor's choice per offered category, plus "asked": the categories they have answered.
- function readConsentChoice() {
- var raw = null;
- try {
- raw = window.localStorage.getItem(CONSENT_KEY);
- } catch (error) {
- return null;
- }
- if (raw === null) {
- return null;
- }
- var stored = null;
- // Older versions stored a plain "accepted" or "declined".
- if (raw === 'accepted' || raw === 'declined') {
- stored = { analytics: raw === 'accepted', marketing: raw === 'accepted' };
- } else {
- try {
- stored = JSON.parse(raw);
- } catch (error) {
- return null;
- }
- }
- if (!stored || typeof stored !== 'object') {
- return null;
- }
- var offered = consentCategories();
- var choice = { asked: [] };
- ['analytics', 'marketing'].forEach(function (category) {
- choice[category] = stored[category] === true && offered.indexOf(category) !== -1;
- if (typeof stored[category] === 'boolean') {
- choice.asked.push(category);
- }
- });
- return choice;
- }
-
- // Scripts added through innerHTML never run, so each one is recreated. Like on a normal page,
- // code after a <script src> without async waits until that file has loaded.
- // The reference node must stay in the page until everything is inserted.
- function insertHtmlBefore(reference, html) {
- var template = document.createElement('template');
- template.innerHTML = html;
- var nodes = Array.prototype.slice.call(template.content.childNodes);
-
- function next() {
- while (nodes.length) {
- var node = activateScripts(nodes.shift());
- var wait = node.nodeType === 1 && node.tagName === 'SCRIPT' && node.hasAttribute('src') && !node.hasAttribute('async');
- if (wait) {
- node.async = false;
- node.addEventListener('load', next);
- node.addEventListener('error', next);
- }
- reference.parentNode.insertBefore(node, reference);
- if (wait) {
- return;
- }
- }
- }
-
- next();
- }
-
- function activateScripts(node) {
- if (node.nodeType !== 1) {
- return node;
- }
- if (node.tagName === 'SCRIPT') {
- var script = document.createElement('script');
- Array.prototype.forEach.call(node.attributes, function (attr) {
- script.setAttribute(attr.name, attr.value);
- });
- script.textContent = node.textContent;
- return script;
- }
- Array.prototype.slice.call(node.querySelectorAll('script')).forEach(function (old) {
- old.parentNode.replaceChild(activateScripts(old), old);
- });
- return node;
- }
-
- function initConsent() {
- var banner = document.getElementById('consent-banner');
- var categories = consentCategories();
- var readConsent = readConsentChoice;
-
- function writeConsent(choice) {
- var stored = { savedAt: Date.now() };
- categories.forEach(function (category) {
- stored[category] = !!choice[category];
- });
- try {
- window.localStorage.setItem(CONSENT_KEY, JSON.stringify(stored));
- } catch (error) {
- }
- }
-
- function allOffered(value) {
- var choice = {};
- categories.forEach(function (category) {
- choice[category] = value;
- });
- return choice;
- }
-
- // Gated code is stored as HTML (for example <script src> tags), so it's parsed as HTML.
- // A script can need several categories ("analytics marketing"); it runs only when all are allowed.
- function runGatedScripts(choice) {
- document.querySelectorAll('script.consent-gated-script, template.consent-gated').forEach(function (holder) {
- var needed = (holder.getAttribute('data-consent-category') || 'analytics').split(/\s+/).filter(Boolean);
- var allowed = needed.every(function (category) {
- return choice[category] === true;
- });
- if (!allowed) {
- return;
- }
- var marker = document.createComment('consent');
- holder.parentNode.replaceChild(marker, holder);
- insertHtmlBefore(marker, holder.tagName === 'TEMPLATE' ? holder.innerHTML : holder.textContent.replace(/<\\\/script>/gi, '</script>'));
- });
- }
-
- var current = readConsent();
- var applied = { analytics: false, marketing: false };
-
- function apply(choice) {
- runGatedScripts(choice);
- categories.forEach(function (category) {
- if (choice[category]) {
- applied[category] = true;
- }
- });
- }
-
- if (current) {
- apply(current);
- }
-
- if (!banner) {
- return;
- }
-
- var options = document.getElementById('consent-options');
- var manageBtn = document.getElementById('consent-manage');
- var saveBtn = document.getElementById('consent-save');
- var acceptBtn = document.getElementById('consent-accept');
- var declineBtn = document.getElementById('consent-decline');
- var reopenBtn = document.getElementById('consent-reopen-btn');
-
- function showManage(open) {
- if (!options) {
- return;
- }
- options.hidden = !open;
- if (manageBtn) {
- manageBtn.hidden = open;
- }
- if (saveBtn) {
- saveBtn.hidden = !open;
- }
- if (open) {
- var state = readConsent() || { analytics: false, marketing: false };
- categories.forEach(function (category) {
- var box = document.getElementById('consent-opt-' + category);
- if (box) {
- box.checked = !!state[category];
- }
- });
- }
- }
-
- function hideBanner() {
- banner.style.display = 'none';
- showManage(false);
- }
-
- function decide(choice) {
- writeConsent(choice);
- // Scripts that already ran can't be stopped, so taking consent back needs a fresh page.
- var revoked = categories.some(function (category) {
- return applied[category] && !choice[category];
- });
- if (revoked) {
- window.location.reload();
- return;
- }
- apply(choice);
- hideBanner();
- document.dispatchEvent(new CustomEvent('publisium:consent'));
- }
-
- // Ask again only when the site now offers a category the visitor hasn't answered yet.
- var answeredAll = !!current && categories.every(function (category) {
- return current.asked.indexOf(category) !== -1;
- });
- if (answeredAll) {
- hideBanner();
- }
-
- if (acceptBtn) {
- acceptBtn.addEventListener('click', function () {
- decide(allOffered(true));
- });
- }
- if (declineBtn) {
- declineBtn.addEventListener('click', function () {
- decide(allOffered(false));
- });
- }
- if (manageBtn) {
- manageBtn.addEventListener('click', function () {
- showManage(true);
- });
- }
- if (saveBtn) {
- saveBtn.addEventListener('click', function () {
- var choice = {};
- categories.forEach(function (category) {
- var box = document.getElementById('consent-opt-' + category);
- choice[category] = !!(box && box.checked);
- });
- decide(choice);
- });
- }
- if (reopenBtn) {
- reopenBtn.addEventListener('click', function () {
- banner.style.display = '';
- showManage(true);
- });
- }
- }
-
- function initPagination() {
- var list = document.getElementById('article-list');
- if (!list) {
- return;
- }
-
- var source = list.dataset.paginationSource;
- if (source !== 'home' && source !== 'category') {
- return;
- }
-
- var categorySlug = list.dataset.categorySlug || '';
- var loading = false;
-
- function fetchPage(page) {
- if (loading) {
- return;
- }
- loading = true;
-
- var url = 'articles-fragment.php?source=' + encodeURIComponent(source) + '&page=' + encodeURIComponent(page);
- if (source === 'category') {
- url += '&slug=' + encodeURIComponent(categorySlug);
- }
-
- fetch(url)
- .then(function (response) {
- if (!response.ok) {
- throw new Error('Request failed');
- }
- return response.text();
- })
- .then(function (html) {
- var temp = document.createElement('div');
- temp.innerHTML = html;
-
- var newCards = temp.querySelectorAll('.article-card');
- newCards.forEach(function (card) {
- list.appendChild(card);
- });
-
- var container = document.getElementById('pagination-container');
- if (container) {
- var newControl = temp.querySelector('.pagination-load-more, .pagination-infinite-marker, .pagination-end');
- container.innerHTML = '';
- if (newControl) {
- container.appendChild(newControl);
- bindControl(newControl);
- }
- }
-
- loading = false;
- })
- .catch(function () {
- loading = false;
- });
- }
-
- function bindControl(control) {
- if (!control) {
- return;
- }
-
- if (control.classList.contains('pagination-load-more')) {
- var btn = control.querySelector('.pagination-load-more-btn');
- if (btn) {
- btn.addEventListener('click', function () {
- fetchPage(control.dataset.nextPage);
- });
- }
- return;
- }
-
- if (control.classList.contains('pagination-infinite-marker')) {
- if (!('IntersectionObserver' in window)) {
- return;
- }
- var observer = new IntersectionObserver(function (entries) {
- entries.forEach(function (entry) {
- if (entry.isIntersecting) {
- observer.disconnect();
- fetchPage(control.dataset.nextPage);
- }
- });
- }, { rootMargin: '200px' });
- observer.observe(control);
- }
- }
-
- var container = document.getElementById('pagination-container');
- if (container) {
- bindControl(container.querySelector('.pagination-load-more, .pagination-infinite-marker'));
- }
- }
-
- function initBrandShortName() {
- var brand = document.querySelector('.site-brand[data-short-name]');
- var header = document.querySelector('.site-header');
-
- if (!brand || !header) {
- return;
- }
-
- function overflows() {
- return brand.scrollWidth > brand.clientWidth + 1 || header.scrollWidth > header.clientWidth + 1;
- }
-
- function update() {
- brand.classList.remove('use-short');
- if (overflows()) {
- brand.classList.add('use-short');
- }
- }
-
- update();
- window.addEventListener('resize', update);
- window.addEventListener('load', update);
- }
-
- function initMobileNav() {
- var toggle = document.getElementById('site-menu-toggle');
- var nav = document.getElementById('site-nav');
- var backdrop = document.getElementById('site-nav-backdrop');
- var header = document.querySelector('.site-header');
-
- if (!toggle || !nav || !backdrop) {
- return;
- }
-
- var lockedScrollY = 0;
-
- function openNav() {
- nav.classList.add('is-open');
- backdrop.classList.add('is-open');
- toggle.setAttribute('aria-expanded', 'true');
- lockedScrollY = window.scrollY || window.pageYOffset || 0;
- document.body.style.position = 'fixed';
- document.body.style.top = -lockedScrollY + 'px';
- document.body.style.left = '0';
- document.body.style.right = '0';
- if (header) {
- header.classList.add('site-nav-drawer-open');
- }
- }
-
- function closeNav() {
- nav.classList.remove('is-open');
- backdrop.classList.remove('is-open');
- toggle.setAttribute('aria-expanded', 'false');
- document.body.style.position = '';
- document.body.style.top = '';
- document.body.style.left = '';
- document.body.style.right = '';
- window.scrollTo(0, lockedScrollY);
- if (header) {
- header.classList.remove('site-nav-drawer-open');
- }
- }
-
- toggle.addEventListener('click', function () {
- if (nav.classList.contains('is-open')) {
- closeNav();
- } else {
- openNav();
- }
- });
-
- backdrop.addEventListener('click', closeNav);
-
- nav.querySelectorAll('a').forEach(function (link) {
- link.addEventListener('click', closeNav);
- });
-
- window.addEventListener('resize', function () {
- if (window.innerWidth > 860) {
- closeNav();
- }
- });
- }
-
- function initShare() {
- var toggle = document.getElementById('share-toggle-btn');
- var panel = document.getElementById('share-panel');
- var copyBtn = document.getElementById('share-copy-btn');
-
- if (!toggle || !panel) {
- return;
- }
-
- function closePanel() {
- panel.classList.remove('is-open');
- toggle.setAttribute('aria-expanded', 'false');
- }
-
- function openPanel() {
- panel.classList.add('is-open');
- toggle.setAttribute('aria-expanded', 'true');
- }
-
- toggle.addEventListener('click', function (event) {
- event.stopPropagation();
- if (panel.classList.contains('is-open')) {
- closePanel();
- } else {
- openPanel();
- }
- });
-
- document.addEventListener('click', function (event) {
- if (panel.classList.contains('is-open') && !panel.contains(event.target) && event.target !== toggle) {
- closePanel();
- }
- });
-
- document.addEventListener('keydown', function (event) {
- if (event.key === 'Escape') {
- closePanel();
- }
- });
-
- if (copyBtn) {
- copyBtn.addEventListener('click', function () {
- var url = copyBtn.getAttribute('data-share-url') || window.location.href;
- var label = copyBtn.querySelector('.js-share-copy-label');
- var defaultText = copyBtn.getAttribute('data-default-text') || '';
- var copiedText = copyBtn.getAttribute('data-copied-text') || '';
-
- function showCopied() {
- if (label && copiedText) {
- label.textContent = copiedText;
- setTimeout(function () {
- label.textContent = defaultText;
- }, 2000);
- }
- }
-
- if (navigator.clipboard && navigator.clipboard.writeText) {
- navigator.clipboard.writeText(url).then(showCopied);
- } else {
- var tempInput = document.createElement('input');
- tempInput.value = url;
- document.body.appendChild(tempInput);
- tempInput.select();
- document.execCommand('copy');
- document.body.removeChild(tempInput);
- showCopied();
- }
- });
- }
- }
-
- function initAppBanner() {
- var banner = document.getElementById('app-banner');
- if (!banner) {
- return;
- }
-
- var DISMISS_KEY = 'publisium_app_banner_dismissed';
-
- try {
- if (localStorage.getItem(DISMISS_KEY) === '1') {
- banner.remove();
- return;
- }
- } catch (e) {}
-
- var userAgent = navigator.userAgent || '';
- var isAndroid = /Android/i.test(userAgent);
- var isIOS = /iPhone|iPad|iPod/i.test(userAgent);
- var androidUrl = banner.getAttribute('data-android-url') || '';
- var iosUrl = banner.getAttribute('data-ios-url') || '';
-
- var targetUrl = '';
- if (isAndroid && androidUrl) {
- targetUrl = androidUrl;
- } else if (isIOS && iosUrl) {
- targetUrl = iosUrl;
- }
-
- if (!targetUrl) {
- banner.remove();
- return;
- }
-
- var link = document.getElementById('app-banner-link');
- if (link) {
- link.setAttribute('href', targetUrl);
- }
-
- banner.classList.add('is-visible');
-
- var closeBtn = document.getElementById('app-banner-close');
- if (closeBtn) {
- closeBtn.addEventListener('click', function () {
- banner.classList.remove('is-visible');
- try {
- localStorage.setItem(DISMISS_KEY, '1');
- } catch (e) {}
- });
- }
- }
-
- function initBackToTop() {
- var button = document.getElementById('back-to-top');
- if (!button) {
- return;
- }
-
- var label = button.querySelector('.back-to-top-progress');
- var content = button.getAttribute('data-progress') === '1' ? document.querySelector('.article-content') : null;
- var template = button.getAttribute('data-left-text') || '{percent}%';
- var reduceMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
- var ticking = false;
-
- if (content && label) {
- button.classList.add('has-progress');
- label.hidden = false;
- }
-
- function update() {
- ticking = false;
-
- var visible = (window.pageYOffset || 0) > 400;
- button.classList.toggle('is-visible', visible);
- button.tabIndex = visible ? 0 : -1;
-
- if (content && label) {
- var rect = content.getBoundingClientRect();
- var read = rect.height > 0 ? Math.min(1, Math.max(0, (window.innerHeight - rect.top) / rect.height)) : 1;
- var percentRead = Math.round(read * 100);
- label.textContent = template.replace('{percent}', String(100 - percentRead));
- button.style.setProperty('--btt-read', percentRead + '%');
- }
- }
-
- function schedule() {
- if (!ticking) {
- ticking = true;
- requestAnimationFrame(update);
- }
- }
-
- window.addEventListener('scroll', schedule, { passive: true });
- window.addEventListener('resize', schedule);
- button.addEventListener('click', function () {
- window.scrollTo({ top: 0, behavior: reduceMotion ? 'auto' : 'smooth' });
- });
- update();
- }
-
- // Comment services wait in a <template> until consent (if needed) and a click (if needed) allow them.
- function initComments() {
- document.querySelectorAll('.comments-embed').forEach(function (box) {
- var template = box.querySelector('.comments-embed-code');
- var target = box.querySelector('.comments-embed-target');
- var notice = box.querySelector('.comments-embed-notice');
- var clickBox = box.querySelector('.comments-embed-click');
- var category = box.getAttribute('data-consent') || '';
- var clicked = box.getAttribute('data-load') !== 'click';
- var loaded = false;
-
- function evaluate() {
- if (loaded || !template || !target) {
- return;
- }
- var choice = readConsentChoice();
- var allowed = category === '' || !!(choice && choice[category]);
- notice.hidden = allowed;
- clickBox.hidden = !allowed || clicked;
- if (allowed && clicked) {
- loaded = true;
- var marker = document.createComment('comments');
- target.appendChild(marker);
- insertHtmlBefore(marker, template.innerHTML);
- }
- }
-
- var loadBtn = box.querySelector('.comments-embed-load');
- if (loadBtn) {
- loadBtn.addEventListener('click', function () {
- clicked = true;
- evaluate();
- });
- }
-
- var consentBtn = box.querySelector('.comments-embed-consent');
- if (consentBtn) {
- consentBtn.addEventListener('click', function () {
- var reopen = document.getElementById('consent-reopen-btn');
- var banner = document.getElementById('consent-banner');
- if (reopen) {
- reopen.click();
- } else if (banner) {
- banner.style.display = '';
- var manage = document.getElementById('consent-manage');
- if (manage) {
- manage.click();
- }
- }
- });
- }
-
- document.addEventListener('publisium:consent', evaluate);
- evaluate();
- });
- }
-
- function initSiteComments() {
- var form = document.querySelector('.site-comments-form form');
- if (!form) {
- return;
- }
- var parentInput = form.querySelector('.site-comments-parent');
- var replying = form.querySelector('.site-comments-replying');
- var body = form.querySelector('textarea');
-
- document.querySelectorAll('.site-comment-reply').forEach(function (button) {
- button.hidden = false;
- button.addEventListener('click', function () {
- parentInput.value = button.getAttribute('data-comment-id') || '';
- replying.querySelector('strong').textContent = button.getAttribute('data-comment-name') || '';
- replying.hidden = false;
- form.scrollIntoView({ behavior: 'smooth', block: 'center' });
- if (body) {
- body.focus({ preventScroll: true });
- }
- });
- });
-
- var cancel = form.querySelector('.site-comments-cancel');
- if (cancel) {
- cancel.addEventListener('click', function () {
- parentInput.value = '';
- replying.hidden = true;
- });
- }
- }
-
- document.addEventListener('DOMContentLoaded', function () {
- initTheme();
- initBackToTop();
- var toggleBtn = document.getElementById('theme-toggle-btn');
- if (toggleBtn) {
- toggleBtn.addEventListener('click', toggleTheme);
- }
- initConsent();
- initComments();
- initSiteComments();
- initPagination();
- initMobileNav();
- initBrandShortName();
- initShare();
- initAppBanner();
- });
- })();
-
- (function () {
- var switcher = document.querySelector('[data-language-switcher]');
- if (!switcher) {
- return;
- }
-
- document.addEventListener('click', function (event) {
- if (switcher.open && !switcher.contains(event.target)) {
- switcher.open = false;
- }
- });
-
- document.addEventListener('keydown', function (event) {
- if (event.key === 'Escape' && switcher.open) {
- switcher.open = false;
- switcher.querySelector('summary').focus();
- }
- });
- })();
-