PageSourceSearch

https://invested.ph/wp-content/themes/invested/assets/js/main.js

js invested.ph collected 2026-10-07 00:04:09 UTC 13,743 bytes, 447 lines download raw bytes

1/**
2 * This is an automatic html-based animation system.
3 * How to use AnimationControls:
4 * Step 1. Define an animation root by assigning [data-animate-root] to an element
5 * Step 2. Set triggers by adding data-animate-trigger="click" to an triggering element
6 * Step 3: Set the trigger's targets by assigning data-animate-select"{selector}" on the triggering element
7 * Step 4: Set the effects on the target by assigning data-animate-effect="{AnimationEffect}" on a target element
8 *   Set up any additional required attributes for the effeect
9 *   For AnimationEffect.Classify, set data-animate-class="myclass"
10 *   For AnimationEffect.Accordion, set data-animate-siblings="{selector}"
11 * Step 5: Set the initial state of the animation effect directly in the HTML.
12 *   For AnimationEffect.Slide, set data-animation-active to make the element visible
13 *   For AnimationEffect.Classify, set data-animation-active if you want it to be "active"
14 *     - This distinction only matters if you're programmatically planning to deactivate things
15 *   For AnimationEffect.Accordion, set data-animation-active to make the element visible
16 */
17
18/**
19 * @readonly
20 * @enum {string}
21 */
22const AnimationEffect = {
23  Slide: 'slide',
24  Classify: 'classify',
25  Accordion: 'accordion',
26};
27
28const AnimationResizer = new ResizeObserver((entries) => {
29  for (const entry of entries) {
30    const measure = entry.target;
31    const target = measure.parentNode;
32    target.style.height = `${measure.scrollHeight}px`;
33  }
34});
35
36class AnimationControls {
37  static setupAllAnimationGroups() {
38    const roots = document.querySelectorAll('[data-animate-root]');
39    roots.forEach(AnimationControls.setupSingleAnimationGroup)
40  }
41
42  /**
43   * @param {HTMLElement} root
44   */
45  static setupSingleAnimationGroup(root) {
46    const triggers = [...root.querySelectorAll('[data-animate-trigger]')].filter((trigger) => {
47      const ancestor = AnimationControls.findAncestor(trigger, ['[data-animate-root]']);
48      return ancestor == root;
49    });
50    triggers.forEach((trigger) => {
51      AnimationControls.setupAnimationTrigger(root, trigger);
52    });
53  }
54
55  /**
56   * @param {HTMLElement} root
57   * @param {HTMLElement} trigger
58   * @returns {() => void}
59   */
60  static setupAnimationTrigger(root, trigger) {
61    const selector = trigger.getAttribute('data-animate-select');
62    const targets = AnimationControls.selectTargets(root, selector);
63
64    for (const target of targets) {
65      /**
66       * @type {AnimationEffect | null}
67       */
68      const effect = target.getAttribute('data-animate-effect');
69      if (effect != null) {
70        AnimationControls.initSingleAnimation(target, effect);
71      }
72    }
73
74    const event = trigger.getAttribute('data-animate-trigger');
75    const animateTargets = AnimationControls.makeAnimationHandler(root, targets);
76    if (event == 'click') {
77      trigger.addEventListener('click', animateTargets);
78    }
79    return animateTargets;
80  }
81
82  /**
83   * @param {HTMLElement} root
84   * @param {HTMLElement[]} targets
85   * @returns {(event: MouseEvent) => void}
86   */
87  static makeAnimationHandler(root, targets) {
88    return (_event) => {
89      for (const target of targets) {
90        const effect = target.getAttribute('data-animate-effect');
91        if (effect == null) {
92          continue;
93        }
94        AnimationControls.runSingleAnimation(root, target, effect);
95      }
96    };
97  }
98
99  /**
100   * @param {HTMLElement} root
101   * @param {HTMLElement} target
102   * @param {AnimationEffect} effect
103   */
104  static runSingleAnimation(root, target, effect) {
105    if (effect === AnimationEffect.Slide) {
106      const active = target.getAttribute('data-animate-active');
107      if (active) {
108        AnimationControls.slideShrink(target);
109      } else {
110        AnimationControls.slideExpand(target, true);
111      }
112    } else if (effect === AnimationEffect.Accordion) {
113      const active = target.getAttribute('data-animate-active');
114      if (active) {
115        AnimationControls.slideShrink(target);
116      } else {
117        const selector = target.getAttribute('data-animate-siblings');
118        const activeSiblings = AnimationControls.selectTargets(root, selector).filter((sib) => {
119          return (
120            sib.getAttribute('data-animate-effect') === AnimationEffect.Accordion &&
121            sib.getAttribute('data-animate-active')
122          );
123        });
124        for (const sibling of activeSiblings) {
125          AnimationControls.slideShrink(sibling);
126        }
127        AnimationControls.slideExpand(target);
128      }
129    } else if (effect === AnimationEffect.Classify) {
130      const active = target.getAttribute('data-animate-active');
131      if (active) {
132        AnimationControls.unclassify(target);
133      } else {
134        AnimationControls.classify(target);
135      }
136    }
137  }
138
139  /**
140   * @param {HTMLElement} target
141   * @param {AnimationEffect} effect
142   */
143  static initSingleAnimation(target, effect) {
144    if (effect === AnimationEffect.Slide) {
145      const measure = AnimationControls.createMeasure(target);
146      const active = target.getAttribute('data-animate-active');
147      if (active) {
148        target.style.height = `${measure.scrollHeight}px`;
149        AnimationResizer.observe(measure);
150      } else {
151        target.style.height = '0';
152      }
153    } else if (effect === AnimationEffect.Accordion) {
154      const measure = AnimationControls.createMeasure(target);
155      const active = target.getAttribute('data-animate-active');
156      if (active) {
157        target.style.height = `${measure.scrollHeight}px`;
158        AnimationResizer.observe(measure);
159      } else {
160        target.style.height = '0';
161      }
162    } else if (effect === AnimationEffect.Classify) {
163      // Do nothing
164    }
165  }
166
167  /**
168   * @param {HTMLElement} root
169   * @param {string} selector
170   * @returns {HTMLElement[]}
171   */
172  static selectTargets(root, selector) {
173    const targets = [
174      ...root.querySelectorAll(selector),
175    ];
176    if (root.matches(selector)) {
177      targets.push(root);
178    }
179    return targets;
180  }
181
182  /**
183   * @param {HTMLElement} target
184   */
185  static slideExpand(target) {
186    target.setAttribute('data-animate-active', true);
187    target.style.height = `${target.scrollHeight}px`;
188    const measure = target.querySelector('.measure');
189    AnimationResizer.observe(measure);
190  }
191
192  /**
193   * @param {HTMLElement} target
194   */
195  static slideShrink(target) {
196    target.removeAttribute('data-animate-active');
197    const measure = target.querySelector('.measure');
198    AnimationResizer.unobserve(measure);
199    target.style.height = '0';
200  }
201
202  /**
203   * @param {HTMLElement} target
204   */
205  static classify(target) {
206    if (target.hasAttribute('data-animate-active')) {
207      return;
208    }
209    const klass = target.getAttribute('data-animate-class');
210    target.setAttribute('data-animate-active', true);
211    if (target.classList.contains(klass)) {
212      target.classList.remove(klass);
213    } else {
214      target.classList.add(klass);
215    }
216  }
217
218  /**
219   * @param {HTMLElement} target
220   */
221  static unclassify(target) {
222    if (!target.hasAttribute('data-animate-active')) {
223      return;
224    }
225    const klass = target.getAttribute('data-animate-class');
226    target.removeAttribute('data-animate-active');
227    if (target.classList.contains(klass)) {
228      target.classList.remove(klass);
229    } else {
230      target.classList.add(klass);
231    }
232  }
233
234  /**
235   * @param {HTMLElement} node
236   * @param {selector} string
237   * @returns {HTMLElement}
238   */
239  static findAncestor(node, selector) {
240    while (node != null) {
241      if (node.matches(selector)) {
242        return node;
243      } else {
244        node = node.parentElement;
245      }
246    }
247    return null;
248  }
249
250  /**
251   * @param {HTMLElement} node
252   * @returns {HTMLElement}
253   */
254  static createMeasure(node) {
255    const measure = document.createElement('div');
256    measure.className = 'measure';
257    while (node.childNodes.length) {
258      measure.appendChild(node.firstChild);
259    }
260    node.appendChild(measure);
261    return measure;
262  }
263}
264
265function updateHeaderShadow(header) {
266  if (window.scrollY == 0) {
267    header.classList.add('noshadow');
268  } else {
269    header.classList.remove('noshadow');
270  }
271  return true;
272}
273
274function onDocumentScrollNavbarUpdate(header, actions) {
275  // Maybe the header shadow needs updating
276  updateHeaderShadow(header);
277
278  // If the navbar actions are open, then it means we need to close everything
279  // This just an optimizzation, technically this block is idempotent
280  if (actions.getAttribute('data-animate-active')) {
281    // Collapse all the top navbar menu accordions
282    const navAccordions = header.querySelectorAll('.top-menu-accordion');
283    for (const nava of navAccordions) {
284      AnimationControls.slideShrink(nava);
285    }
286
287    // Simulate the nav-toggle disabling if needed
288    const toggler = header.querySelector('.nav-toggle');
289    const targets = header.querySelectorAll(toggler.getAttribute('data-animate-select'));
290    for (const target of targets) {
291      const effect = target.getAttribute('data-animate-effect');
292      const active = target.getAttribute('data-animate-active');
293      if (!active) {
294        continue;
295      } if (effect == AnimationEffect.Slide) {
296        AnimationControls.slideShrink(target);
297      } else if (effect == AnimationEffect.Accordion) {
298        AnimationControls.slideShrink(target);
299      } else if (effect == AnimationEffect.Classify) {
300        AnimationControls.unclassify(target);
301      }
302    }
303  }
304}
305
306/**
307 * @param {HTMLElement} header
308 */
309function setupHeaderLogic(header) {
310  // Setup special case for site-header-actions to prevent it from being wimbly wombly
311  // due to nested accordions resizing
312  const actions = header.querySelector('.site-header-actions');
313  const content = actions.querySelector('.site-header-actions-content');
314  const nested = new ResizeObserver(() => {
315    if (actions.getAttribute('data-animate-active')) {
316      actions.style.transition = 'none';
317      requestAnimationFrame(() => {
318        actions.style.removeProperty('transition');
319      });
320    }
321  });
322  nested.observe(content);
323
324  // Collapse all header accordions whenever nav is toggled
325  const toggler = header.querySelector('.nav-toggle');
326  toggler.addEventListener('click', () => {
327    requestAnimationFrame(() => {
328      const isOpen = actions.getAttribute('data-animate-active');
329      if (isOpen) {
330        return;
331      }
332      // Collapse all the top navbar menu accordions
333      const navAccordions = header.querySelectorAll('.top-menu-accordion');
334      for (const nava of navAccordions) {
335        AnimationControls.slideShrink(nava);
336      }
337    });
338  });
339
340  // Collapse all navbar when document is scrolled
341  window.addEventListener('scroll', () => onDocumentScrollNavbarUpdate(header, actions));
342}
343
344function setupAnalytics() {
345  function analyticsButtonClick(event) {
346    const gevent = {
347      page: document.title.trim().replace(/\s+/g, ' '),
348    };
349    if (event.target.id) {
350      gevent.anchor = event.target.id.trim();
351    }
352    if (event.target.textContent) {
353      gevent.text = event.target.textContent.trim().replace(/\s+/g, ' ');
354    }
355    gtag('event', 'button_click', gevent);
356  }
357  const wpbuttons = document.querySelectorAll(".wp-block-button,.track-click");
358  for (const button of wpbuttons) {
359    button.addEventListener('click', analyticsButtonClick);
360  }
361}
362
363function setupLinkScrolling() {
364  /**
365   * Helper function to remove trailing slash from URL paths
366   * @param {string} url
367   * @returns {string}
368   */
369  function noslash(url) {
370    return url.replace(/\/$/, '');
371  }
372
373  // Select all anchor links
374  const anchorLinks = document.querySelectorAll('a');
375
376  anchorLinks.forEach(link => {
377    // Get the full URL of the link
378    const linkHref = link.getAttribute('href');
379    const currentUrl = noslash(window.location.href.split('#')[0]);
380    const targetUrl = new URL(linkHref, currentUrl);
381
382    // Ignore if the link doesn't point to the same page
383    if (
384      targetUrl.origin !== window.location.origin ||
385      noslash(targetUrl.pathname) !== noslash(window.location.pathname)
386    ) {
387      return;
388    }
389
390    link.addEventListener('click', function (event) {
391      // Prevent default link behavior
392      event.preventDefault();
393
394      // Get the target element by ID
395      const targetId = targetUrl.hash.substring(1);
396      const targetElement = document.getElementById(targetId);
397
398      if (targetElement) {
399        // Calculate the position of the target element
400        const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
401        const targetPosition = scrollTop + targetElement.getBoundingClientRect().top;
402
403        const magicOffset = 150;
404        window.scrollTo({
405          top: targetPosition - magicOffset,
406          behavior: 'smooth'
407        });
408
409        // Update the URL hash without jumping
410        history.pushState(null, null, `#${targetId}`);
411      } else if (targetId === "") {
412        // Handle the case where the href is just "#" or "#"
413        // Scroll to the top of the page
414        window.scrollTo({
415          top: 0,
416          behavior: 'smooth'
417        });
418
419        // Update the URL hash without jumping
420        history.pushState(null, null, "#");
421      }
422    });
423  });
424}
425
426function setupEverything() {
427  const header = document.querySelector('.site-header');
428  if (header != null) {
429    updateHeaderShadow(header);
430    setupHeaderLogic(header);
431  }
432
433  AnimationControls.setupAllAnimationGroups();
434
435  const videos = document.querySelectorAll('video');
436  for (const video of videos) {
437    new Plyr(video, {
438      iconUrl: '/wp-content/themes/invested/vendor/plyr.svg',
439    });
440  }
441
442  setupLinkScrolling();
443
444  setupAnalytics();
445}
446
447document.addEventListener("DOMContentLoaded", setupEverything);

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.