PageSourceSearch

https://fortune-cookie.tokyo/assets/js/stock-gauge.js?v=1.0.610

js fortune-cookie.tokyo collected 2026-10-07 17:58:17 UTC 5,207 bytes, 146 lines download raw bytes

1/**
2 * 在庫ゲージ (stock-gauge) のスクロールインアニメ init
3 *
4 * 元は resources/views/components/stock-gauge.blade.php L196-327 の @once 内 script。
5 * 差分リロード (fragment swap) で新規カード内のゲージにも init を走らせるため外部 JS 化。
6 *
7 * 公開 API:
8 *   - window.StockGauge.rescan(root?) — root スコープ内の未 init ゲージを observe に登録
9 *
10 * 冪等化:
11 *   - window.__STOCK_GAUGE_LOADED__ ガードで module 二重ロード防止
12 *   - 各 gauge 要素に data-scroll-animate-init="1" マーカーで再 observe をスキップ
13 *
14 * イベント:
15 *   - document.addEventListener('top:gacha-list-updated') で fragment swap 後に再スキャン
16 *
17 * Spec: docs/features/top-main-gacha-partial-reload/spec.md §3.D-3
18 */
19(function () {
20    if (window.__STOCK_GAUGE_LOADED__) return;
21    window.__STOCK_GAUGE_LOADED__ = true;
22
23    var FIRE_DELAY_MS = 300;
24    var ANIMATION_DURATION_MS = 800;
25    var FALLBACK_MS = 1000;
26
27    // 単一の共有 observer + fallbackTimers を module スコープに持つ
28    // (rescan 時に旧 observer を disconnect せず、新 gauge だけ追加 observe する)
29    var observer = null;
30    var fallbackTimers = new WeakMap();
31
32    function scaleFor(target) {
33        return Math.max(0, Math.min(1, target / 100));
34    }
35
36    function applyImmediate(gauges) {
37        for (var i = 0; i < gauges.length; i++) {
38            var g = gauges[i];
39            var target = parseFloat(g.getAttribute('data-percentage')) || 0;
40            var bar = g.querySelector('.stock-gauge-bar-fill');
41            if (bar) {
42                bar.style.transformOrigin = 'left';
43                bar.style.transform = 'scaleX(' + scaleFor(target) + ')';
44            }
45            g.setAttribute('data-scroll-animate-init', '1');
46        }
47    }
48
49    function animateGauge(g) {
50        var target = parseFloat(g.getAttribute('data-percentage')) || 0;
51        var bar = g.querySelector('.stock-gauge-bar-fill');
52        if (!bar) return;
53        bar.style.transformOrigin = 'left';
54        bar.classList.add('scroll-animating');
55        requestAnimationFrame(function () {
56            requestAnimationFrame(function () {
57                bar.style.transform = 'scaleX(' + scaleFor(target) + ')';
58            });
59        });
60        setTimeout(function () {
61            bar.classList.remove('scroll-animating');
62        }, ANIMATION_DURATION_MS + 100);
63    }
64
65    function scheduleFire(g) {
66        if (g.getAttribute('data-scroll-animate-init') === '1') return;
67        g.setAttribute('data-scroll-animate-init', '1');
68        var t = fallbackTimers.get(g);
69        if (t) {
70            clearTimeout(t);
71            fallbackTimers.delete(g);
72        }
73        if (observer) observer.unobserve(g);
74        setTimeout(function () {
75            animateGauge(g);
76        }, FIRE_DELAY_MS);
77    }
78
79    function ensureObserver() {
80        if (observer) return observer;
81        observer = new IntersectionObserver(function (entries) {
82            entries.forEach(function (entry) {
83                var g = entry.target;
84                if (g.getAttribute('data-scroll-animate-init') === '1') {
85                    observer.unobserve(g);
86                    return;
87                }
88                if (!entry.isIntersecting) {
89                    var t = fallbackTimers.get(g);
90                    if (t) {
91                        clearTimeout(t);
92                        fallbackTimers.delete(g);
93                    }
94                    return;
95                }
96                if (entry.intersectionRatio >= 0.8) {
97                    scheduleFire(g);
98                } else if (!fallbackTimers.has(g)) {
99                    fallbackTimers.set(g, setTimeout(function () {
100                        fallbackTimers.delete(g);
101                        scheduleFire(g);
102                    }, FALLBACK_MS));
103                }
104            });
105        }, { threshold: [0.3, 0.8] });
106        return observer;
107    }
108
109    function rescan(root) {
110        var scope = root || document;
111        var gauges = scope.querySelectorAll(
112            '.stock-gauge-container[data-scroll-animate="1"]:not([data-scroll-animate-init])'
113        );
114        if (gauges.length === 0) return;
115
116        var prefersReduced = window.matchMedia
117            && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
118        var noObserver = !('IntersectionObserver' in window);
119
120        if (prefersReduced || noObserver) {
121            applyImmediate(gauges);
122            return;
123        }
124
125        var obs = ensureObserver();
126        for (var j = 0; j < gauges.length; j++) {
127            obs.observe(gauges[j]);
128        }
129    }
130
131    // 公開 API
132    window.StockGauge = { rescan: rescan };
133
134    // 初期ロード時の自動 init
135    if (document.readyState === 'loading') {
136        document.addEventListener('DOMContentLoaded', function () { rescan(); });
137    } else {
138        rescan();
139    }
140
141    // fragment swap 後の再スキャン
142    document.addEventListener('top:gacha-list-updated', function (e) {
143        var root = (e && e.detail && e.detail.container) ? e.detail.container : document;
144        rescan(root);
145    });
146})();

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.