PageSourceSearch

https://www.escargotdecrete.com/gshop/modules/p9marquee/views/js/p9marquee.js

js escargotdecrete.com collected 2026-10-06 17:59:25 UTC 8,990 bytes, 278 lines download raw bytes

1(function () {
2  function debounce(fn, ms) {
3    var t;
4    return function () {
5      clearTimeout(t);
6      t = setTimeout(fn, ms);
7    };
8  }
9
10  function hashString(str) {
11    // djb2
12    var h = 5381;
13    for (var i = 0; i < str.length; i++) {
14      h = ((h << 5) + h) + str.charCodeAt(i);
15      h = h & 0xffffffff;
16    }
17    return String(h >>> 0);
18  }
19
20  function canUseStorage(s) {
21    try {
22      if (!s) return false;
23      var k = '__p9m_test__';
24      s.setItem(k, '1');
25      s.removeItem(k);
26      return true;
27    } catch (e) {
28      return false;
29    }
30  }
31
32  var storeSession = canUseStorage(window.sessionStorage) ? window.sessionStorage : null;
33  var storeLocal = canUseStorage(window.localStorage) ? window.localStorage : null;
34
35  function cookieGet(name) {
36    try {
37      var m = document.cookie.match(new RegExp('(?:^|; )' + name.replace(/([.$?*|{}()[\]\\\/\+^])/g, '\\$1') + '=([^;]*)'));
38      return m ? decodeURIComponent(m[1]) : null;
39    } catch (e) {
40      return null;
41    }
42  }
43
44  function cookieSet(name, value) {
45    try {
46      document.cookie = name + '=' + encodeURIComponent(value) + '; path=/; SameSite=Lax';
47    } catch (e) {}
48  }
49
50  function anchorKey(root, item, speed, gap, direction) {
51    var dk = root.getAttribute('data-key');
52    if (dk && dk.trim()) return 'p9marquee_anchor_' + dk.trim();
53
54    var sig = String(speed) + '|' + String(gap) + '|' + direction + '|' + (item.textContent || '').trim().slice(0, 200);
55    return 'p9marquee_anchor_' + hashString(sig);
56  }
57
58  function getObj(key) {
59    var v = null;
60    try { if (storeSession) v = storeSession.getItem(key); } catch (e) {}
61    if (!v) { try { if (storeLocal) v = storeLocal.getItem(key); } catch (e) {} }
62    if (!v) v = cookieGet(key);
63    if (!v) return null;
64    try { return JSON.parse(v); } catch (e) { return null; }
65  }
66
67  function setObj(key, obj) {
68    var v = '';
69    try { v = JSON.stringify(obj); } catch (e) { return; }
70    try { if (storeSession) storeSession.setItem(key, v); } catch (e) {}
71    try { if (storeLocal) storeLocal.setItem(key, v); } catch (e) {}
72    cookieSet(key, v);
73  }
74
75  function prefersReducedMotion() {
76    try {
77      return !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches);
78    } catch (e) {
79      return false;
80    }
81  }
82
83  function initMarquee(root) {
84    if (!root) return;
85
86    var viewport = root.querySelector('.p9marquee__viewport');
87    var track = root.querySelector('.p9marquee__track');
88    var item = track ? track.querySelector('.p9marquee__item') : null;
89
90    if (!viewport || !track || !item) return;
91    if (prefersReducedMotion()) return;
92
93    // Cancel any previous RAF loop for this element
94    if (root.__p9m && root.__p9m.rafId) {
95      cancelAnimationFrame(root.__p9m.rafId);
96    }
97    root.__p9m = root.__p9m || {};
98    root.__p9m.rafId = 0;
99    root.__p9m.paused = false;
100
101    // Remove previous clones (if any)
102    Array.prototype.slice.call(track.querySelectorAll('.p9marquee__item--clone')).forEach(function (n) {
103      n.parentNode && n.parentNode.removeChild(n);
104    });
105
106    var gap = parseInt(root.getAttribute('data-gap'), 10);
107    if (isNaN(gap) || gap < 0) gap = 40;
108
109    var speed = parseInt(root.getAttribute('data-speed'), 10);
110    if (isNaN(speed) || speed < 10) speed = 80;
111
112    var direction = root.getAttribute('data-direction') === 'right' ? 'right' : 'left';
113    var dirSign = direction === 'right' ? 1 : -1;
114
115    var pauseHover = root.getAttribute('data-pausehover') === '1';
116
117    track.style.setProperty('--p9marquee-gap', gap + 'px');
118
119    // Force layout
120    var itemRect = item.getBoundingClientRect();
121    var viewportRect = viewport.getBoundingClientRect();
122
123    var itemWidth = itemRect.width;
124    var viewportWidth = viewportRect.width;
125
126    if (!itemWidth || !viewportWidth) return;
127
128    var segmentWidth = itemWidth + gap;
129
130    // How many repeats needed so the viewport is always filled
131    var repeats = Math.max(2, Math.ceil((viewportWidth + segmentWidth) / segmentWidth) + 2);
132
133    for (var i = 0; i < repeats; i++) {
134      var clone = item.cloneNode(true);
135      clone.classList.add('p9marquee__item--clone');
136      clone.setAttribute('aria-hidden', 'true');
137      track.appendChild(clone);
138    }
139
140    // IMPORTANT: we no longer rely on CSS animation for the loop,
141    // because some mobile browsers appear to restart CSS animations on 
141navigation.
142    // We drive the loop with requestAnimationFrame and a shared timestamp.
143
144    var key = anchorKey(root, item, speed, gap, direction);
145    var st = getObj(key);
146    var now = Date.now();
147
148    var startOffset = (dirSign === -1) ? viewportWidth : -viewportWidth;
149    var introDuration = Math.max(0.2, Math.abs(startOffset) / speed); // seconds
150    var loopStartTs = (st && st.loopStartTs) ? parseInt(st.loopStartTs, 10) : NaN;
151
152    if (isNaN(loopStartTs) || loopStartTs <= 0) {
153      loopStartTs = now + Math.round(introDuration * 1000);
154      setObj(key, {
155        loopStartTs: loopStartTs,
156        createdAt: now,
157        introDuration: introDuration,
158        startOffset: startOffset
159      });
160      st = getObj(key);
161    }
162
163    // Helper: start RAF loop, with phase continuity based on loopStartTs.
164    function startLoop() {
165      // Stop CSS animations just in case
166      track.classList.remove('is-intro');
167      track.classList.remove('is-animated');
168      track.style.animation = 'none';
169      track.style.animationDelay = '';
170      track.style.willChange = 'transform';
171
172      function tick() {
173        if (!root.isConnected) return;
174
175        if (!root.__p9m) root.__p9m = {};
176        if (root.__p9m.paused || document.visibilityState === 'hidden') {
177          root.__p9m.rafId = requestAnimationFrame(tick);
178          return;
179        }
180
181        var tNow = Date.now();
182        var elapsed = (tNow - loopStartTs) / 1000;
183        if (elapsed < 0) elapsed = 0;
184
185        var dist = (elapsed * speed) % segmentWidth;
186        var tx = dirSign * dist;
187
188        track.style.transform = 'translate3d(' + tx + 'px,0,0)';
189        root.__p9m.rafId = requestAnimationFrame(tick);
190      }
191
192      root.__p9m.rafId = requestAnimationFrame(tick);
193    }
194
195    // Intro: start empty line then reach position 0 at loopStartTs, then start loop.
196    function runIntroIfNeeded() {
197      var tNow = Date.now();
198      if (tNow >= loopStartTs) {
199        startLoop();
200        return;
201      }
202
203      // Reset state for intro
204      track.classList.remove('is-animated');
205      track.classList.remove('is-intro');
206      track.style.animation = 'none';
207      track.style.animationDelay = '';
208      track.style.willChange = 'transform';
209
210      function introTick() {
211        if (!root.isConnected) return;
212
213        if (!root.__p9m) root.__p9m = {};
214        if (root.__p9m.paused || document.visibilityState === 'hidden') {
215          root.__p9m.rafId = requestAnimationFrame(introTick);
216          return;
217        }
218
219        var now2 = Date.now();
220        var remainingMs = loopStartTs - now2;
221
222        if (remainingMs <= 0) {
223          track.style.transform = 'translate3d(0px,0,0)';
224          startLoop();
225          return;
226        }
227
228        var remaining = remainingMs / 1000;
229        var ratio = introDuration > 0 ? (remaining / introDuration) : 0;
230        ratio = Math.max(0, Math.min(1, ratio));
231        var currentOffset = startOffset * ratio;
232
233        track.style.transform = 'translate3d(' + currentOffset + 'px,0,0)';
234        root.__p9m.rafId = requestAnimationFrame(introTick);
235      }
236
237      root.__p9m.rafId = requestAnimationFrame(introTick);
238    }
239
240    // Pause on hover (desktop); on touch devices it simply does nothing.
241    if (pauseHover) {
242      root.addEventListener('mouseenter', function () { if (root.__p9m) root.__p9m.paused = true; }, { passive: true });
243      root.addEventListener('mouseleave', function () { if (root.__p9m) root.__p9m.paused = false; }, { passive: true });
244    }
245
246    runIntroIfNeeded();
247  }
248
249  function initAll() {
250    var nodes = document.querySelectorAll('.p9marquee');
251    Array.prototype.forEach.call(nodes, initMarquee);
252  }
253
254  // On mobile Chromium browsers (incl. Brave), the address bar show/hide can fire resize events
255  // during scroll because the viewport height changes. Only re-init when layout width changes.
256  function layoutWidth() {
257    return Math.round(document.documentElement.clientWidth || window.innerWidth || 0);
258  }
259
260  var lastW = layoutWidth();
261
262  function reinitIfWidthChanged() {
263    var w = layoutWidth();
264    if (!w) return;
265    if (w === lastW) return;
266    lastW = w;
267    initAll();
268  }
269
270  window.addEventListener('load', initAll);
271  window.addEventListener('pageshow', initAll);
272  window.addEventListener('resize', debounce(reinitIfWidthChanged, 150));
273  window.addEventListener('orientationchange', debounce(function () { lastW = layoutWidth(); initAll(); }, 150));
274
275  if (window.visualViewport && window.visualViewport.addEventListener) {
276    window.visualViewport.addEventListener('resize', debounce(reinitIfWidthChanged, 150));
277  }
278})();

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.