WebOrbiton
v1.0.0.9

Publisium

749 lines · 22.0 KB
  1. (function () {
  2. 'use strict';
  3. ​
  4. var THEME_KEY = 'publisium_theme';
  5. ​
  6. function applyTheme(theme) {
  7. document.body.setAttribute('data-theme', theme);
  8. }
  9. ​
  10. function initTheme() {
  11. var stored = null;
  12. try {
  13. stored = localStorage.getItem(THEME_KEY);
  14. } catch (error) {
  15. }
  16. if (stored === 'dark' || stored === 'light') {
  17. applyTheme(stored);
  18. return;
  19. }
  20. var prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
  21. applyTheme(prefersDark ? 'dark' : 'light');
  22. }
  23. ​
  24. function toggleTheme() {
  25. var current = document.body.getAttribute('data-theme') || 'light';
  26. var next = current === 'dark' ? 'light' : 'dark';
  27. applyTheme(next);
  28. try {
  29. localStorage.setItem(THEME_KEY, next);
  30. } catch (error) {
  31. }
  32. }
  33. ​
  34. var CONSENT_KEY = 'publisium_consent';
  35. ​
  36. // The optional categories the banner offers. A category that isn't offered is never allowed.
  37. function consentCategories() {
  38. var banner = document.getElementById('consent-banner');
  39. return banner ? (banner.getAttribute('data-categories') || '').split(/\s+/).filter(Boolean) : [];
  40. }
  41. ​
  42. // Returns the visitor's choice per offered category, plus "asked": the categories they have answered.
  43. function readConsentChoice() {
  44. var raw = null;
  45. try {
  46. raw = window.localStorage.getItem(CONSENT_KEY);
  47. } catch (error) {
  48. return null;
  49. }
  50. if (raw === null) {
  51. return null;
  52. }
  53. var stored = null;
  54. // Older versions stored a plain "accepted" or "declined".
  55. if (raw === 'accepted' || raw === 'declined') {
  56. stored = { analytics: raw === 'accepted', marketing: raw === 'accepted' };
  57. } else {
  58. try {
  59. stored = JSON.parse(raw);
  60. } catch (error) {
  61. return null;
  62. }
  63. }
  64. if (!stored || typeof stored !== 'object') {
  65. return null;
  66. }
  67. var offered = consentCategories();
  68. var choice = { asked: [] };
  69. ['analytics', 'marketing'].forEach(function (category) {
  70. choice[category] = stored[category] === true && offered.indexOf(category) !== -1;
  71. if (typeof stored[category] === 'boolean') {
  72. choice.asked.push(category);
  73. }
  74. });
  75. return choice;
  76. }
  77. ​
  78. // Scripts added through innerHTML never run, so each one is recreated. Like on a normal page,
  79. // code after a <script src> without async waits until that file has loaded.
  80. // The reference node must stay in the page until everything is inserted.
  81. function insertHtmlBefore(reference, html) {
  82. var template = document.createElement('template');
  83. template.innerHTML = html;
  84. var nodes = Array.prototype.slice.call(template.content.childNodes);
  85. ​
  86. function next() {
  87. while (nodes.length) {
  88. var node = activateScripts(nodes.shift());
  89. var wait = node.nodeType === 1 && node.tagName === 'SCRIPT' && node.hasAttribute('src') && !node.hasAttribute('async');
  90. if (wait) {
  91. node.async = false;
  92. node.addEventListener('load', next);
  93. node.addEventListener('error', next);
  94. }
  95. reference.parentNode.insertBefore(node, reference);
  96. if (wait) {
  97. return;
  98. }
  99. }
  100. }
  101. ​
  102. next();
  103. }
  104. ​
  105. function activateScripts(node) {
  106. if (node.nodeType !== 1) {
  107. return node;
  108. }
  109. if (node.tagName === 'SCRIPT') {
  110. var script = document.createElement('script');
  111. Array.prototype.forEach.call(node.attributes, function (attr) {
  112. script.setAttribute(attr.name, attr.value);
  113. });
  114. script.textContent = node.textContent;
  115. return script;
  116. }
  117. Array.prototype.slice.call(node.querySelectorAll('script')).forEach(function (old) {
  118. old.parentNode.replaceChild(activateScripts(old), old);
  119. });
  120. return node;
  121. }
  122. ​
  123. function initConsent() {
  124. var banner = document.getElementById('consent-banner');
  125. var categories = consentCategories();
  126. var readConsent = readConsentChoice;
  127. ​
  128. function writeConsent(choice) {
  129. var stored = { savedAt: Date.now() };
  130. categories.forEach(function (category) {
  131. stored[category] = !!choice[category];
  132. });
  133. try {
  134. window.localStorage.setItem(CONSENT_KEY, JSON.stringify(stored));
  135. } catch (error) {
  136. }
  137. }
  138. ​
  139. function allOffered(value) {
  140. var choice = {};
  141. categories.forEach(function (category) {
  142. choice[category] = value;
  143. });
  144. return choice;
  145. }
  146. ​
  147. // Gated code is stored as HTML (for example <script src> tags), so it's parsed as HTML.
  148. // A script can need several categories ("analytics marketing"); it runs only when all are allowed.
  149. function runGatedScripts(choice) {
  150. document.querySelectorAll('script.consent-gated-script, template.consent-gated').forEach(function (holder) {
  151. var needed = (holder.getAttribute('data-consent-category') || 'analytics').split(/\s+/).filter(Boolean);
  152. var allowed = needed.every(function (category) {
  153. return choice[category] === true;
  154. });
  155. if (!allowed) {
  156. return;
  157. }
  158. var marker = document.createComment('consent');
  159. holder.parentNode.replaceChild(marker, holder);
  160. insertHtmlBefore(marker, holder.tagName === 'TEMPLATE' ? holder.innerHTML : holder.textContent.replace(/<\\\/script>/gi, '</script>'));
  161. });
  162. }
  163. ​
  164. var current = readConsent();
  165. var applied = { analytics: false, marketing: false };
  166. ​
  167. function apply(choice) {
  168. runGatedScripts(choice);
  169. categories.forEach(function (category) {
  170. if (choice[category]) {
  171. applied[category] = true;
  172. }
  173. });
  174. }
  175. ​
  176. if (current) {
  177. apply(current);
  178. }
  179. ​
  180. if (!banner) {
  181. return;
  182. }
  183. ​
  184. var options = document.getElementById('consent-options');
  185. var manageBtn = document.getElementById('consent-manage');
  186. var saveBtn = document.getElementById('consent-save');
  187. var acceptBtn = document.getElementById('consent-accept');
  188. var declineBtn = document.getElementById('consent-decline');
  189. var reopenBtn = document.getElementById('consent-reopen-btn');
  190. ​
  191. function showManage(open) {
  192. if (!options) {
  193. return;
  194. }
  195. options.hidden = !open;
  196. if (manageBtn) {
  197. manageBtn.hidden = open;
  198. }
  199. if (saveBtn) {
  200. saveBtn.hidden = !open;
  201. }
  202. if (open) {
  203. var state = readConsent() || { analytics: false, marketing: false };
  204. categories.forEach(function (category) {
  205. var box = document.getElementById('consent-opt-' + category);
  206. if (box) {
  207. box.checked = !!state[category];
  208. }
  209. });
  210. }
  211. }
  212. ​
  213. function hideBanner() {
  214. banner.style.display = 'none';
  215. showManage(false);
  216. }
  217. ​
  218. function decide(choice) {
  219. writeConsent(choice);
  220. // Scripts that already ran can't be stopped, so taking consent back needs a fresh page.
  221. var revoked = categories.some(function (category) {
  222. return applied[category] && !choice[category];
  223. });
  224. if (revoked) {
  225. window.location.reload();
  226. return;
  227. }
  228. apply(choice);
  229. hideBanner();
  230. document.dispatchEvent(new CustomEvent('publisium:consent'));
  231. }
  232. ​
  233. // Ask again only when the site now offers a category the visitor hasn't answered yet.
  234. var answeredAll = !!current && categories.every(function (category) {
  235. return current.asked.indexOf(category) !== -1;
  236. });
  237. if (answeredAll) {
  238. hideBanner();
  239. }
  240. ​
  241. if (acceptBtn) {
  242. acceptBtn.addEventListener('click', function () {
  243. decide(allOffered(true));
  244. });
  245. }
  246. if (declineBtn) {
  247. declineBtn.addEventListener('click', function () {
  248. decide(allOffered(false));
  249. });
  250. }
  251. if (manageBtn) {
  252. manageBtn.addEventListener('click', function () {
  253. showManage(true);
  254. });
  255. }
  256. if (saveBtn) {
  257. saveBtn.addEventListener('click', function () {
  258. var choice = {};
  259. categories.forEach(function (category) {
  260. var box = document.getElementById('consent-opt-' + category);
  261. choice[category] = !!(box && box.checked);
  262. });
  263. decide(choice);
  264. });
  265. }
  266. if (reopenBtn) {
  267. reopenBtn.addEventListener('click', function () {
  268. banner.style.display = '';
  269. showManage(true);
  270. });
  271. }
  272. }
  273. ​
  274. function initPagination() {
  275. var list = document.getElementById('article-list');
  276. if (!list) {
  277. return;
  278. }
  279. ​
  280. var source = list.dataset.paginationSource;
  281. if (source !== 'home' && source !== 'category') {
  282. return;
  283. }
  284. ​
  285. var categorySlug = list.dataset.categorySlug || '';
  286. var loading = false;
  287. ​
  288. function fetchPage(page) {
  289. if (loading) {
  290. return;
  291. }
  292. loading = true;
  293. ​
  294. var url = 'articles-fragment.php?source=' + encodeURIComponent(source) + '&page=' + encodeURIComponent(page);
  295. if (source === 'category') {
  296. url += '&slug=' + encodeURIComponent(categorySlug);
  297. }
  298. ​
  299. fetch(url)
  300. .then(function (response) {
  301. if (!response.ok) {
  302. throw new Error('Request failed');
  303. }
  304. return response.text();
  305. })
  306. .then(function (html) {
  307. var temp = document.createElement('div');
  308. temp.innerHTML = html;
  309. ​
  310. var newCards = temp.querySelectorAll('.article-card');
  311. newCards.forEach(function (card) {
  312. list.appendChild(card);
  313. });
  314. ​
  315. var container = document.getElementById('pagination-container');
  316. if (container) {
  317. var newControl = temp.querySelector('.pagination-load-more, .pagination-infinite-marker, .pagination-end');
  318. container.innerHTML = '';
  319. if (newControl) {
  320. container.appendChild(newControl);
  321. bindControl(newControl);
  322. }
  323. }
  324. ​
  325. loading = false;
  326. })
  327. .catch(function () {
  328. loading = false;
  329. });
  330. }
  331. ​
  332. function bindControl(control) {
  333. if (!control) {
  334. return;
  335. }
  336. ​
  337. if (control.classList.contains('pagination-load-more')) {
  338. var btn = control.querySelector('.pagination-load-more-btn');
  339. if (btn) {
  340. btn.addEventListener('click', function () {
  341. fetchPage(control.dataset.nextPage);
  342. });
  343. }
  344. return;
  345. }
  346. ​
  347. if (control.classList.contains('pagination-infinite-marker')) {
  348. if (!('IntersectionObserver' in window)) {
  349. return;
  350. }
  351. var observer = new IntersectionObserver(function (entries) {
  352. entries.forEach(function (entry) {
  353. if (entry.isIntersecting) {
  354. observer.disconnect();
  355. fetchPage(control.dataset.nextPage);
  356. }
  357. });
  358. }, { rootMargin: '200px' });
  359. observer.observe(control);
  360. }
  361. }
  362. ​
  363. var container = document.getElementById('pagination-container');
  364. if (container) {
  365. bindControl(container.querySelector('.pagination-load-more, .pagination-infinite-marker'));
  366. }
  367. }
  368. ​
  369. function initBrandShortName() {
  370. var brand = document.querySelector('.site-brand[data-short-name]');
  371. var header = document.querySelector('.site-header');
  372. ​
  373. if (!brand || !header) {
  374. return;
  375. }
  376. ​
  377. function overflows() {
  378. return brand.scrollWidth > brand.clientWidth + 1 || header.scrollWidth > header.clientWidth + 1;
  379. }
  380. ​
  381. function update() {
  382. brand.classList.remove('use-short');
  383. if (overflows()) {
  384. brand.classList.add('use-short');
  385. }
  386. }
  387. ​
  388. update();
  389. window.addEventListener('resize', update);
  390. window.addEventListener('load', update);
  391. }
  392. ​
  393. function initMobileNav() {
  394. var toggle = document.getElementById('site-menu-toggle');
  395. var nav = document.getElementById('site-nav');
  396. var backdrop = document.getElementById('site-nav-backdrop');
  397. var header = document.querySelector('.site-header');
  398. ​
  399. if (!toggle || !nav || !backdrop) {
  400. return;
  401. }
  402. ​
  403. var lockedScrollY = 0;
  404. ​
  405. function openNav() {
  406. nav.classList.add('is-open');
  407. backdrop.classList.add('is-open');
  408. toggle.setAttribute('aria-expanded', 'true');
  409. lockedScrollY = window.scrollY || window.pageYOffset || 0;
  410. document.body.style.position = 'fixed';
  411. document.body.style.top = -lockedScrollY + 'px';
  412. document.body.style.left = '0';
  413. document.body.style.right = '0';
  414. if (header) {
  415. header.classList.add('site-nav-drawer-open');
  416. }
  417. }
  418. ​
  419. function closeNav() {
  420. nav.classList.remove('is-open');
  421. backdrop.classList.remove('is-open');
  422. toggle.setAttribute('aria-expanded', 'false');
  423. document.body.style.position = '';
  424. document.body.style.top = '';
  425. document.body.style.left = '';
  426. document.body.style.right = '';
  427. window.scrollTo(0, lockedScrollY);
  428. if (header) {
  429. header.classList.remove('site-nav-drawer-open');
  430. }
  431. }
  432. ​
  433. toggle.addEventListener('click', function () {
  434. if (nav.classList.contains('is-open')) {
  435. closeNav();
  436. } else {
  437. openNav();
  438. }
  439. });
  440. ​
  441. backdrop.addEventListener('click', closeNav);
  442. ​
  443. nav.querySelectorAll('a').forEach(function (link) {
  444. link.addEventListener('click', closeNav);
  445. });
  446. ​
  447. window.addEventListener('resize', function () {
  448. if (window.innerWidth > 860) {
  449. closeNav();
  450. }
  451. });
  452. }
  453. ​
  454. function initShare() {
  455. var toggle = document.getElementById('share-toggle-btn');
  456. var panel = document.getElementById('share-panel');
  457. var copyBtn = document.getElementById('share-copy-btn');
  458. ​
  459. if (!toggle || !panel) {
  460. return;
  461. }
  462. ​
  463. function closePanel() {
  464. panel.classList.remove('is-open');
  465. toggle.setAttribute('aria-expanded', 'false');
  466. }
  467. ​
  468. function openPanel() {
  469. panel.classList.add('is-open');
  470. toggle.setAttribute('aria-expanded', 'true');
  471. }
  472. ​
  473. toggle.addEventListener('click', function (event) {
  474. event.stopPropagation();
  475. if (panel.classList.contains('is-open')) {
  476. closePanel();
  477. } else {
  478. openPanel();
  479. }
  480. });
  481. ​
  482. document.addEventListener('click', function (event) {
  483. if (panel.classList.contains('is-open') && !panel.contains(event.target) && event.target !== toggle) {
  484. closePanel();
  485. }
  486. });
  487. ​
  488. document.addEventListener('keydown', function (event) {
  489. if (event.key === 'Escape') {
  490. closePanel();
  491. }
  492. });
  493. ​
  494. if (copyBtn) {
  495. copyBtn.addEventListener('click', function () {
  496. var url = copyBtn.getAttribute('data-share-url') || window.location.href;
  497. var label = copyBtn.querySelector('.js-share-copy-label');
  498. var defaultText = copyBtn.getAttribute('data-default-text') || '';
  499. var copiedText = copyBtn.getAttribute('data-copied-text') || '';
  500. ​
  501. function showCopied() {
  502. if (label && copiedText) {
  503. label.textContent = copiedText;
  504. setTimeout(function () {
  505. label.textContent = defaultText;
  506. }, 2000);
  507. }
  508. }
  509. ​
  510. if (navigator.clipboard && navigator.clipboard.writeText) {
  511. navigator.clipboard.writeText(url).then(showCopied);
  512. } else {
  513. var tempInput = document.createElement('input');
  514. tempInput.value = url;
  515. document.body.appendChild(tempInput);
  516. tempInput.select();
  517. document.execCommand('copy');
  518. document.body.removeChild(tempInput);
  519. showCopied();
  520. }
  521. });
  522. }
  523. }
  524. ​
  525. function initAppBanner() {
  526. var banner = document.getElementById('app-banner');
  527. if (!banner) {
  528. return;
  529. }
  530. ​
  531. var DISMISS_KEY = 'publisium_app_banner_dismissed';
  532. ​
  533. try {
  534. if (localStorage.getItem(DISMISS_KEY) === '1') {
  535. banner.remove();
  536. return;
  537. }
  538. } catch (e) {}
  539. ​
  540. var userAgent = navigator.userAgent || '';
  541. var isAndroid = /Android/i.test(userAgent);
  542. var isIOS = /iPhone|iPad|iPod/i.test(userAgent);
  543. var androidUrl = banner.getAttribute('data-android-url') || '';
  544. var iosUrl = banner.getAttribute('data-ios-url') || '';
  545. ​
  546. var targetUrl = '';
  547. if (isAndroid && androidUrl) {
  548. targetUrl = androidUrl;
  549. } else if (isIOS && iosUrl) {
  550. targetUrl = iosUrl;
  551. }
  552. ​
  553. if (!targetUrl) {
  554. banner.remove();
  555. return;
  556. }
  557. ​
  558. var link = document.getElementById('app-banner-link');
  559. if (link) {
  560. link.setAttribute('href', targetUrl);
  561. }
  562. ​
  563. banner.classList.add('is-visible');
  564. ​
  565. var closeBtn = document.getElementById('app-banner-close');
  566. if (closeBtn) {
  567. closeBtn.addEventListener('click', function () {
  568. banner.classList.remove('is-visible');
  569. try {
  570. localStorage.setItem(DISMISS_KEY, '1');
  571. } catch (e) {}
  572. });
  573. }
  574. }
  575. ​
  576. function initBackToTop() {
  577. var button = document.getElementById('back-to-top');
  578. if (!button) {
  579. return;
  580. }
  581. ​
  582. var label = button.querySelector('.back-to-top-progress');
  583. var content = button.getAttribute('data-progress') === '1' ? document.querySelector('.article-content') : null;
  584. var template = button.getAttribute('data-left-text') || '{percent}%';
  585. var reduceMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  586. var ticking = false;
  587. ​
  588. if (content && label) {
  589. button.classList.add('has-progress');
  590. label.hidden = false;
  591. }
  592. ​
  593. function update() {
  594. ticking = false;
  595. ​
  596. var visible = (window.pageYOffset || 0) > 400;
  597. button.classList.toggle('is-visible', visible);
  598. button.tabIndex = visible ? 0 : -1;
  599. ​
  600. if (content && label) {
  601. var rect = content.getBoundingClientRect();
  602. var read = rect.height > 0 ? Math.min(1, Math.max(0, (window.innerHeight - rect.top) / rect.height)) : 1;
  603. var percentRead = Math.round(read * 100);
  604. label.textContent = template.replace('{percent}', String(100 - percentRead));
  605. button.style.setProperty('--btt-read', percentRead + '%');
  606. }
  607. }
  608. ​
  609. function schedule() {
  610. if (!ticking) {
  611. ticking = true;
  612. requestAnimationFrame(update);
  613. }
  614. }
  615. ​
  616. window.addEventListener('scroll', schedule, { passive: true });
  617. window.addEventListener('resize', schedule);
  618. button.addEventListener('click', function () {
  619. window.scrollTo({ top: 0, behavior: reduceMotion ? 'auto' : 'smooth' });
  620. });
  621. update();
  622. }
  623. ​
  624. // Comment services wait in a <template> until consent (if needed) and a click (if needed) allow them.
  625. function initComments() {
  626. document.querySelectorAll('.comments-embed').forEach(function (box) {
  627. var template = box.querySelector('.comments-embed-code');
  628. var target = box.querySelector('.comments-embed-target');
  629. var notice = box.querySelector('.comments-embed-notice');
  630. var clickBox = box.querySelector('.comments-embed-click');
  631. var category = box.getAttribute('data-consent') || '';
  632. var clicked = box.getAttribute('data-load') !== 'click';
  633. var loaded = false;
  634. ​
  635. function evaluate() {
  636. if (loaded || !template || !target) {
  637. return;
  638. }
  639. var choice = readConsentChoice();
  640. var allowed = category === '' || !!(choice && choice[category]);
  641. notice.hidden = allowed;
  642. clickBox.hidden = !allowed || clicked;
  643. if (allowed && clicked) {
  644. loaded = true;
  645. var marker = document.createComment('comments');
  646. target.appendChild(marker);
  647. insertHtmlBefore(marker, template.innerHTML);
  648. }
  649. }
  650. ​
  651. var loadBtn = box.querySelector('.comments-embed-load');
  652. if (loadBtn) {
  653. loadBtn.addEventListener('click', function () {
  654. clicked = true;
  655. evaluate();
  656. });
  657. }
  658. ​
  659. var consentBtn = box.querySelector('.comments-embed-consent');
  660. if (consentBtn) {
  661. consentBtn.addEventListener('click', function () {
  662. var reopen = document.getElementById('consent-reopen-btn');
  663. var banner = document.getElementById('consent-banner');
  664. if (reopen) {
  665. reopen.click();
  666. } else if (banner) {
  667. banner.style.display = '';
  668. var manage = document.getElementById('consent-manage');
  669. if (manage) {
  670. manage.click();
  671. }
  672. }
  673. });
  674. }
  675. ​
  676. document.addEventListener('publisium:consent', evaluate);
  677. evaluate();
  678. });
  679. }
  680. ​
  681. function initSiteComments() {
  682. var form = document.querySelector('.site-comments-form form');
  683. if (!form) {
  684. return;
  685. }
  686. var parentInput = form.querySelector('.site-comments-parent');
  687. var replying = form.querySelector('.site-comments-replying');
  688. var body = form.querySelector('textarea');
  689. ​
  690. document.querySelectorAll('.site-comment-reply').forEach(function (button) {
  691. button.hidden = false;
  692. button.addEventListener('click', function () {
  693. parentInput.value = button.getAttribute('data-comment-id') || '';
  694. replying.querySelector('strong').textContent = button.getAttribute('data-comment-name') || '';
  695. replying.hidden = false;
  696. form.scrollIntoView({ behavior: 'smooth', block: 'center' });
  697. if (body) {
  698. body.focus({ preventScroll: true });
  699. }
  700. });
  701. });
  702. ​
  703. var cancel = form.querySelector('.site-comments-cancel');
  704. if (cancel) {
  705. cancel.addEventListener('click', function () {
  706. parentInput.value = '';
  707. replying.hidden = true;
  708. });
  709. }
  710. }
  711. ​
  712. document.addEventListener('DOMContentLoaded', function () {
  713. initTheme();
  714. initBackToTop();
  715. var toggleBtn = document.getElementById('theme-toggle-btn');
  716. if (toggleBtn) {
  717. toggleBtn.addEventListener('click', toggleTheme);
  718. }
  719. initConsent();
  720. initComments();
  721. initSiteComments();
  722. initPagination();
  723. initMobileNav();
  724. initBrandShortName();
  725. initShare();
  726. initAppBanner();
  727. });
  728. })();
  729. ​
  730. (function () {
  731. var switcher = document.querySelector('[data-language-switcher]');
  732. if (!switcher) {
  733. return;
  734. }
  735. ​
  736. document.addEventListener('click', function (event) {
  737. if (switcher.open && !switcher.contains(event.target)) {
  738. switcher.open = false;
  739. }
  740. });
  741. ​
  742. document.addEventListener('keydown', function (event) {
  743. if (event.key === 'Escape' && switcher.open) {
  744. switcher.open = false;
  745. switcher.querySelector('summary').focus();
  746. }
  747. });
  748. })();
  749. ​