PageSourceSearch

https://zerorank.ai/assets/js/pricing.js?v=20260923-7

js zerorank.ai collected 2026-09-30 10:43:55 UTC 5,299 bytes, 109 lines download raw bytes

1/* ZeroRank pricing.js — the ribbon video gate (861px), from pricing.html, plus the billing toggle (authored). */
2(function () {
3  "use strict";
4  /* The ribbon is display:none under 861px — but display:none does NOT stop a
5     <video> downloading. Chrome honours preload and fetches the whole clip
6     whether or not the element is ever painted, so a phone was paying 2.4MB
7     for something it could not see. The element therefore ships with no src
8     at all; it is handed one only once the breakpoint is genuinely met. The
9     mount above has already bound `loadeddata`, so play follows on its own. */
10  var v = document.getElementById('zr-hero-video');
11  if (!v || !v.dataset.src) return;
12
13  /* Starting playback is this block's job too. The shared mount above ran
14     before the element had a src at all, so its own play() attempt was
15     always going to be refused; it left a `loadeddata` listener behind, and
16     relying on that alone left the ribbon sitting on its first frame. Kick
17     it here, on the two events that mean there is something to play, and
18     again if the tab comes back — a muted loop that has been suspended does
19     not always resume on its own. */
20  var play = function () {
21    if (!v.src) return;
22    var p = v.play();
23    if (p && p.catch) p.catch(function () { /* first frame stands in */ });
24  };
25
26  var wide = matchMedia('(min-width: 861px)');
27  var load = function () {
28    if (!wide.matches || v.src) return;
29    v.src = v.dataset.src;
30    v.load();
31    play();
32  };
33  load();
34
35  v.addEventListener('loadeddata',  play);
36  v.addEventListener('canplay',     play);
37  document.addEventListener('visibilitychange', function () {
38    if (!document.hidden) play();
39  });
40  wide.addEventListener ? wide.addEventListener('change', load)
41                        : wide.addListener(load);
42})();
43
44
45/* ---------------------------------------------------------------- */
46
47/* ── billing toggle ─────────────────────────────────────────────────────
48   Monthly / Yearly. The legacy page shipped the markup and the CSS (the pill
49   travels on #zr-billing[data-period], the −20% chip recolours on
50   aria-pressed) but no script, so clicking Monthly did nothing. Authored for
51   the port, not sliced: each .zr-plan__amount counts from one price to the
52   other over 420ms (ease-out, rAF, exact value written by a backstop timer),
53   keeping its .zr-plan__per child; .zr-plan__save shows its data-save text on
54   yearly and empties on monthly (pricing.css hides :empty). Reduced motion
55   skips the tween. */
56(function () {
57  'use strict';
58  var group = document.getElementById('zr-billing');
59  if (!group) return;
60  var buttons = Array.prototype.slice.call(group.querySelectorAll('button[data-period]'));
61  var amounts = Array.prototype.slice.call(document.querySelectorAll('.zr-plan__amount[data-yearly][data-monthly]'));
62  var saves = Array.prototype.slice.call(document.querySelectorAll('.zr-plan__save[data-save]'));
63  var reduce = !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches);
64  var DURATION = 420;
65  var run = 0;
66
67  function parts(text) { /* "$71" → { prefix: "$", value: 71, suffix: "" } */
68    var m = /^([^0-9]*)([0-9][0-9,]*)(.*)$/.exec(text || '');
69    return m ? { prefix: m[1], value: parseInt(m[2].replace(/,/g, ''), 10), suffix: m[3] } : null;
70  }
71  function priceNode(el) { /* the first text node is the price; .zr-plan__per stays where it is */
72    for (var i = 0; i < el.childNodes.length; i++) if (el.childNodes[i].nodeType === 3) return el.childNodes[i];
73    var t = document.createTextNode(''); el.insertBefore(t, el.firstChild); return t;
74  }
75  function tween(node, from, to, target, id) {
76    var start = null;
77    var tick = function (now) {
78      if (id !== run) return;
79      if (start === null) start = now;
80      var p = Math.min(1, (now - start) / DURATION);
81      var e = 1 - Math.pow(1 - p, 3);
82      node.data = to.prefix + Math.round(from.value + (to.value - from.value) * e) + to.suffix;
83      if (p < 1) requestAnimationFrame(tick);
84    };
85    requestAnimationFrame(tick);
86    setTimeout(function () { if (id === run) node.data = target; }, DURATION + 80);
87  }
88  function setPeriod(period) {
89    var id = ++run;
90    group.setAttribute('data-period', period);
91    buttons.forEach(function (b) { b.setAttribute('aria-pressed', String(b.getAttribute('data-period') === period)); });
92    amounts.forEach(function (el) {
93      var target = el.getAttribute('data-' + period) || '';
94      var node = priceNode(el);
95      var from = parts(node.data), to = parts(target);
96      if (reduce || !from || !to || from.value === to.value) { node.data = target; return; }
97      tween(node, from, to, target, id);
98    });
99    saves.forEach(function (el) { el.textContent = period === 'yearly' ? el.getAttribute('data-save') : ''; });
100  }
101  group.addEventListener('click', function (e) {
102    var b = e.target.closest('button[data-period]');
103    if (!b || !group.contains(b)) return;
104    var period = b.getAttribute('data-period');
105    if (period !== 'monthly' && period !== 'yearly') return;
106    if (group.getAttribute('data-period') === period) return;
107    setPeriod(period);
108  });
109})();

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.