PageSourceSearch

https://cp.xlc.com/templates/xlc-template-2026/js/common-header-footer.js?v=4416fa

js xlc.com collected 2026-10-08 10:14:18 UTC 13,483 bytes, 383 lines download raw bytes

1/**
2 * Common JS for Header, Support Section, and Footer.
3 * Runs after DOM ready. If sections are injected dynamically (header/support),
4 * listeners are attached when 'header-loaded' or 'support-section-loaded' fires.
5 * All DOM access guarded with null checks.
6 */
7(function () {
8  'use strict';
9
10  /**
11   * Wrap "&" in <span class="amp"> for Poppins (.amp in style.css).
12   * Skip svg / script / style / .amp (avoid double-wrap).
13   */
14  function wrapAmpersandsInRoot(root) {
15    if (!root) return;
16    var walker = document.createTreeWalker(
17      root,
18      NodeFilter.SHOW_TEXT,
19      {
20        acceptNode: function (node) {
21          var p = node.parentNode;
22          if (!p) return NodeFilter.FILTER_REJECT;
23          var tag = p.nodeName.toLowerCase();
24          if (tag === 'script' || tag === 'style' || tag === 'noscript') return NodeFilter.FILTER_REJECT;
25          if (p.closest && (p.closest('svg') || p.closest('.amp'))) return NodeFilter.FILTER_REJECT;
26          return NodeFilter.FILTER_ACCEPT;
27        }
28      },
29      false
30    );
31    var nodes = [];
32    while (walker.nextNode()) {
33      if (/&/.test(walker.currentNode.nodeValue)) nodes.push(walker.currentNode);
34    }
35    for (var i = 0; i < nodes.length; i++) {
36      var n = nodes[i];
37      var parts = n.nodeValue.split(/(&)/);
38      if (parts.length < 2) continue;
39      var frag = document.createDocumentFragment();
40      for (var k = 0; k < parts.length; k++) {
41        if (parts[k] === '&') {
42          var span = document.createElement('span');
43          span.className = 'amp';
44          span.textContent = '&';
45          frag.appendChild(span);
46        } else if (parts[k]) {
47          frag.appendChild(document.createTextNode(parts[k]));
48        }
49      }
50      n.parentNode.replaceChild(frag, n);
51    }
52  }
53
54  var ampObserverStarted;
55  var ampWrapRafScheduled = false;
56
57  /**
58   * 下一幀再包裝,減少 typewriter 逐字輸出時「先見舊字款 & 再變 Poppins」嘅閃爍
59   * (取代長 debounce:以前要等 350ms 靜止先包,肉眼好易察覺)
60   */
61  function scheduleAmpWrapNextFrame() {
62    if (ampWrapRafScheduled) return;
63    ampWrapRafScheduled = true;
64    requestAnimationFrame(function () {
65      ampWrapRafScheduled = false;
66      if (document.body) wrapAmpersandsInRoot(document.body);
67    });
68  }
69
70  function initAmpersandWrapping() {
71    if (!document.body) return;
72
73    wrapAmpersandsInRoot(document.body);
74
75    document.addEventListener(
76      'header-loaded',
77      function () {
78        var bar = document.getElementById('announce-bar');
79        var header = document.getElementById('site-header');
80        if (bar) wrapAmpersandsInRoot(bar);
81        if (header) wrapAmpersandsInRoot(header);
82        scheduleAmpWrapNextFrame();
83      },
84      false
85    );
86
87    if (ampObserverStarted) return;
88    ampObserverStarted = true;
89
90    var obs = new MutationObserver(function (mutations) {
91      for (var i = 0; i < mutations.length; i++) {
92        var n = mutations[i].target;
93        var el = n.nodeType === 3 ? n.parentElement : n;
94        if (!el || typeof el.closest !== 'function') {
95          scheduleAmpWrapNextFrame();
96          return;
97        }
98        if (!el.closest('.js-skip-amp-mutation')) {
99          scheduleAmpWrapNextFrame();
100          return;
101        }
102      }
103    });
104    obs.observe(document.body, { subtree: true, childList: true, characterData: true });
105  }
106
107  /* 本檔通常喺 </body> 前載入:一執行就同步包一次,早過 DOMContentLoaded 後先跑嘅 inline typewriter */
108  if (document.body) {
109    wrapAmpersandsInRoot(document.body);
110  }
111
112  window.xlcWrapAmpersandsIn = wrapAmpersandsInRoot;
113  window.xlcWrapAmpersandsAll = function () {
114    wrapAmpersandsInRoot(document.body);
115  };
116
117  function runWhenReady() {
118    if (document.readyState === 'loading') {
119      document.addEventListener('DOMContentLoaded', runWhenReady);
120      return;
121    }
122    initAmpersandWrapping();
123    initScrollToTop();
124    initAnnounceBar();
125    initNavbar();
126    initSupportTypewriter();
127    initRevealOnScroll();
128    // Defer so this runs after WHMCS binds .copy-to-clipboard to the original copy fn
129    setTimeout(enhanceClipboardCopy, 0);
130  }
131
132  /* ── Scroll to Top:約 40% 捲動後淡入(按鈕可能隨 footer 動態載入) ── */
133  function initScrollToTop() {
134    var btn = document.getElementById('scroll-to-top');
135    if (!btn) {
136      document.addEventListener('footer-loaded', initScrollToTop, { once: true });
137      return;
138    }
139    var threshold = 0.4;
140    function update() {
141      var maxScroll = document.documentElement.scrollHeight - window.innerHeight;
142      var show = maxScroll > 100 && window.scrollY >= maxScroll * threshold;
143      btn.classList.toggle('scroll-to-top--visible', show);
144    }
145    window.addEventListener('scroll', function () { update(); }, { passive: true });
146    window.addEventListener('resize', function () { update(); });
147    btn.addEventListener('click', function () {
148      window.scrollTo({ top: 0, behavior: 'smooth' });
149    });
150    update();
151  }
152
153  /* ── Announcement bar dismiss (runs when header is ready) ── */
154  function initAnnounceBar() {
155    var bar = document.getElementById('announce-bar');
156    var closeBtn = document.getElementById('announce-close');
157    if (!bar || !closeBtn) {
158      document.addEventListener('header-loaded', initAnnounceBar, { once: true });
159      return;
160    }
161    if (sessionStorage.getItem('xlc-announce-dismissed') === '1') {
162      bar.style.display = 'none';
163      return;
164    }
165    closeBtn.addEventListener('click', function () {
166      bar.style.overflow = 'hidden';
167      bar.style.maxHeight = bar.offsetHeight + 'px';
168      bar.offsetHeight;
169      bar.style.transition = 'max-height 0.35s ease, opacity 0.3s ease, padding 0.3s ease';
170      bar.style.maxHeight = '0';
171      bar.style.opacity = '0';
172      bar.style.paddingTop = '0';
173      bar.style.paddingBottom = '0';
174      setTimeout(function () {
175        if (bar) bar.style.display = 'none';
176      }, 360);
177      sessionStorage.setItem('xlc-announce-dismissed', '1');
178    });
179  }
180
181  /* ── Navbar: scroll shadow + mobile drawer + accordion ── */
182  function initNavbar() {
183    var header = document.getElementById('site-header');
184    var hamburger = header ? header.querySelector('.navbar__hamburger') : null;
185    var overlay = document.getElementById('nav-overlay');
186    if (!header) {
187      document.addEventListener('header-loaded', initNavbar, { once: true });
188      return;
189    }
190
191    function closeNav() {
192      if (!header) return;
193      header.classList.remove('nav-open');
194      if (hamburger) hamburger.setAttribute('aria-expanded', 'false');
195      document.body.style.overflow = '';
196      if (overlay) overlay.classList.remove('nav-overlay--visible');
197      var openItems = header.querySelectorAll('.navbar__item--open');
198      if (openItems.length) {
199        openItems.forEach(function (el) {
200          el.classList.remove('navbar__item--open');
201        });
202      }
203    }
204
205    var onScroll = function () {
206      if (header) header.classList.toggle('scrolled', window.scrollY > 8);
207    };
208    window.addEventListener('scroll', onScroll, { passive: true });
209    onScroll();
210
211    if (hamburger && header) {
212      hamburger.addEventListener('click', function () {
213        var isOpen = header.classList.toggle('nav-open');
214        hamburger.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
215        document.body.style.overflow = isOpen ? 'hidden' : '';
216        if (overlay) overlay.classList.toggle('nav-overlay--visible', isOpen);
217        if (!isOpen) {
218          var openItems = header.querySelectorAll('.navbar__item--open');
219          if (openItems.length) openItems.forEach(function (el) { el.classList.remove('navbar__item--open'); });
220        }
221      });
222    }
223
224    var dropdownLinks = header.querySelectorAll('.navbar__item--has-dropdown .navbar__link');
225    if (dropdownLinks.length) {
226      dropdownLinks.forEach(function (link) {
227        link.addEventListener('click', function (e) {
228          e.preventDefault();
229          if (window.innerWidth > 1200) return;
230          var item = this.closest('.navbar__item');
231          if (!item) return;
232          var isOpen = item.classList.contains('navbar__item--open');
233          var openItems = header.querySelectorAll('.navbar__item--open');
234          if (openItems.length) openItems.forEach(function (el) { el.classList.remove('navbar__item--open'); });
235          if (!isOpen) item.classList.add('navbar__item--open');
236        });
237      });
238    }
239
240    var navCloseLinks = header.querySelectorAll('.navbar__dropdown-link, .navbar__mobile-cta .btn');
241    if (navCloseLinks.length) {
242      navCloseLinks.forEach(function (el) {
243        el.addEventListener('click', closeNav);
244      });
245    }
246
247    if (overlay) overlay.addEventListener('click', closeNav);
248
249    document.addEventListener('click', function (e) {
250      if (header && header.classList.contains('nav-open') &&
251          !header.contains(e.target) && e.target !== overlay) {
252        closeNav();
253      }
254    });
255
256    document.addEventListener('keydown', function (e) {
257      if (e.key === 'Escape' && header && header.classList.contains('nav-open')) {
258        closeNav();
259      }
260    });
261  }
262
263  /* ── Support email typewriter (runs when support-section is ready) ── */
264  function initSupportTypewriter() {
265    var textEl = document.getElementById('support-email-typewriter');
266    var cursorEl = document.querySelector('.support__email-cursor');
267    if (!textEl || !cursorEl) {
268      document.addEventListener('support-section-loaded', initSupportTypewriter, { once: true });
269      return;
270    }
271    var fullText = textEl.getAttribute('data-text') || '';
272    var speed = 80;
273
274    function run() {
275      if (!textEl || !cursorEl) return;
276      textEl.textContent = '';
277      cursorEl.classList.add('support__email-cursor--on');
278      var i = 0;
279      (function tick() {
280        if (i < fullText.length && textEl) {
281          textEl.textContent += fullText.charAt(i);
282          i++;
283          setTimeout(tick, speed);
284        }
285      })();
286    }
287
288    var section = document.querySelector('.support-section');
289    if (section && 'IntersectionObserver' in window) {
290      var obs = new IntersectionObserver(function (entries) {
291        if (entries[0] && entries[0].isIntersecting) {
292          run();
293          obs.disconnect();
294        }
295      }, { threshold: 0.2 });
296      obs.observe(section);
297    } else {
298      setTimeout(run, 1200);
299    }
300  }
301
302  /* ── Reusable Scroll Reveal; re-run on support-section-loaded for injected .reveal-on-scroll ── */
303  function initRevealOnScroll() {
304    var observer = new IntersectionObserver(
305      function (entries) {
306        for (var i = 0; i < entries.length; i++) {
307          if (entries[i].isIntersecting) {
308            var el = entries[i].target;
309            if (!el) continue;
310            var delay = el.dataset.revealDelay;
311            if (delay) {
312              setTimeout(function (target) {
313                if (target) target.classList.add('reveal-on-scroll--in-view');
314              }, parseInt(delay, 10), el);
315            } else {
316              el.classList.add('reveal-on-scroll--in-view');
317            }
318            observer.unobserve(el);
319          }
320        }
321      },
322      { rootMargin: '0px 0px -60px 0px', threshold: 0.1 }
323    );
324
325    function observeRevealElements() {
326      var els = document.querySelectorAll('.reveal-on-scroll');
327      for (var j = 0; j < els.length; j++) {
328        if (els[j].dataset.revealObserved === '1') continue;
329        els[j].dataset.revealObserved = '1';
330        observer.observe(els[j]);
331      }
332    }
333
334    observeRevealElements();
335    document.addEventListener('support-section-loaded', observeRevealElements);
336  }
337
338  /**
339   * WHMCS clipboard.copy only supports data-clipboard-target (input value).
340   * Product details copy chips use data-clipboard-text — extend copy to handle both,
341   * then rebind so already-attached click handlers pick up the new function.
342   */
343  function enhanceClipboardCopy() {
344    if (!window.jQuery || !window.WHMCS || !WHMCS.ui || !WHMCS.ui.clipboard) return;
345    var $ = window.jQuery;
346    var oldCopy = WHMCS.ui.clipboard.copy;
347    var newCopy = function (e) {
348      e.preventDefault();
349      var trigger = $(e.currentTarget);
350      var contentElement = trigger.data('clipboard-target');
351      var clipboardText = trigger.attr('data-clipboard-text');
352      var tempElement;
353      var container;
354      try {
355        if (clipboardText) {
356          container = $('body');
357          tempElement = $('<textarea>')
358            .css({ position: 'fixed', opacity: '0', width: '1px', height: '1px' })
359            .val(clipboardText);
360        } else {
361          container = $(contentElement).parent();
362          tempElement = $('<textarea>')
363            .css({ position: 'fixed', opacity: '0', width: '1px', height: '1px' })
364            .val($(contentElement).val());
365        }
366        container.append(tempElement);
367        tempElement.focus().select();
368        document.execCommand('copy');
369      } finally {
370        if (tempElement) tempElement.remove();
371      }
372      trigger.tooltip({ trigger: 'click', placement: 'bottom' });
373      WHMCS.ui.toolTip.setTip(trigger, 'Copied!');
374      WHMCS.ui.toolTip.hideTip(trigger);
375    };
376    WHMCS.ui.clipboard.copy = newCopy;
377    $('.copy-to-clipboard').each(function () {
378      $(this).off('click', oldCopy).on('click', newCopy);
379    });
380  }
381
382  runWhenReady();
383})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.