PageSourceSearch

https://www.dlsite.com/js/dlsite-countdown.js

js dlsite.com collected 2026-10-01 09:00:13 UTC 11,521 bytes, 366 lines download raw bytes

1/**
2 * DLsite カウントダウン表示
3 *
4 * カウントダウン表示と「?」ボタンの実装(ロールアニメーション付き)
5 */
6(function () {
7  'use strict';
8
9  // === 定数定義 ===
10
11  // 表示期間(JST)
12  const START_DATE = new Date('2026-04-01T00:00:00+09:00');
13  const END_DATE = new Date('2026-11-02T14:00:00+09:00');
14
15  // カウントダウン配置変更日時(JST)
16  const MOVE_DATE = new Date('2026-04-02T00:00:00+09:00');
17
18  // デバッグ用時刻オフセット
19  const TIME_OFFSET = (function () {
20    if (!/(?:^|;\s*)dlsite_debug=1/.test(document.cookie)) {
21      return 0;
22    }
23    const params = new URLSearchParams(window.location.search);
24    const time_param = params.get('time');
25    if (!time_param || !/^\d{12}$/.test(time_param)) {
26      return 0;
27    }
28    const y = time_param.slice(0, 4);
29    const m = time_param.slice(4, 6);
30    const d = time_param.slice(6, 8);
31    const h = time_param.slice(8, 10);
32    const min = time_param.slice(10, 12);
33    const debug_date = new Date(`${y}-${m}-${d}T${h}:${min}:00+09:00`);
34    if (isNaN(debug_date.getTime())) {
35      return 0; // 無効な日付の場合はオフセットなし
36    }
37    return debug_date.getTime() - Date.now();
38  })();
39
40  // 多言語対応(X投稿テキスト)
41  // cspell:disable
42  const SHARE_TEXT = {
43    'ja-jp': 'このカウントダウンは何?\n#DLsiteカウントダウン',
44    'en-us': 'Anyone know what this countdown is about?\n#DLsiteCountDown',
45    'zh-cn': '这是什么的倒计时?\n#DLsite倒计时',
46    'zh-tw': '這是什麼倒數?\n#DLsite倒數計時',
47    'ko-kr': '슨 카운트다운일까요?\n#DLsite카운트다운',
48  };
49  // cspell:enable
50
51  // X投稿URL(ダミーページ経由でOGP出し分け)
52  const SHARE_URL = 'https://dlsharing.com/modpub/lp/home/countdown/';
53
54  // リールアニメーション設定
55  const DIGIT_HEIGHT = 24; // 1桁あたりの高さ(px)= line-height
56  const UNITS = [3, 2, 2, 2]; // 日(3桁), 時(2桁), 分(2桁), 秒(2桁)
57
58  // リール用スタイル定数
59  // cspell:disable-next-line
60  const FONT_STYLE = '\'Familjen Grotesk\', Arial, sans-serif';
61  const TEXT_COLOR = '#0b2a83';
62
63  // === グローバル変数 ===
64  let countdown_interval = null;
65  let reels_map = new Map(); // コンテナごとのリール配列を管理 Map<Element, Array>
66  let is_built = false; // リールDOM構築済みフラグ
67
68  // === 関数定義 ===
69
70  /**
71   * 現在時刻を取得(デバッグ用 ?time= があればオフセットを加算)
72   * @returns {Date}
73   */
74  function getNow() {
75    return new Date(Date.now() + TIME_OFFSET);
76  }
77
78  /**
79   * ロケール判定
80   * @returns {string} ロケール文字列(例: 'ja-jp')
81   */
82  function getLocale() {
83    const cookies = document.cookie.split('; ');
84    for (let i = 0; i < cookies.length; i++) {
85      const cookie = cookies[i].split('=');
86      if (cookie[0] === 'locale') {
87        const locale = decodeURIComponent(cookie[1]);
88        if (/^[a-z]{2}-[a-z]{2}$/i.test(locale)) {
89          return locale.toLowerCase();
90        }
91      }
92    }
93
94    // fallback 1: window.dlsite.getLocale()
95    if (window.dlsite && typeof window.dlsite.getLocale === 'function') {
96      return window.dlsite.getLocale();
97    }
98
99    // fallback 2: <html lang="..."> 属性から取得
100    const html_lang = document.documentElement.lang || 'ja-jp';
101    return html_lang.toLowerCase();
102  }
103
104  /**
105   * X投稿テキスト取得(多言語対応)
106   * @returns {string} X投稿用テキスト
107   */
108  function getShareText() {
109    const locale = getLocale();
110    return SHARE_TEXT[locale] || SHARE_TEXT['en-us']; // 主要5言語以外は英語
111  }
112
113  /**
114   * X投稿URL生成
115   * @returns {string} X投稿フォームURL
116   */
117  function generateShareURL() {
118    const text = encodeURIComponent(getShareText());
119    const url = encodeURIComponent(SHARE_URL);
120    return `https://x.com/intent/tweet?text=${text}&url=${url}`;
121  }
122
123  /**
124   * 表示期間判定
125   * @returns {boolean} 表示期間内であればtrue
126   */
127  function checkVisibility() {
128    const now = getNow();
129    return now >= START_DATE && now < END_DATE;
130  }
131
132  /**
133   * カウントダウン計算
134   * @returns {Object|null} { days, hours, minutes, seconds } または null(表示期間外)
135   */
136  function calculateCountdown() {
137    if (!checkVisibility()) {
138      return null;
139    }
140
141    const now = getNow();
142    const diff = END_DATE - now; // ミリ秒単位の差分
143
144    if (diff <= 0) {
145      return null; // カウントダウン終了
146    }
147
148    const seconds = Math.floor((diff / 1000) % 60);
149    const minutes = Math.floor((diff / 1000 / 60) % 60);
150    const hours = Math.floor((diff / 1000 / 60 / 60) % 24);
151    const days = Math.floor(diff / 1000 / 60 / 60 / 24);
152
153    return { days, hours, minutes, seconds };
154  }
155
156  /**
157   * リールDOMを構築(初回のみ)
158   * 各桁に0〜9を縦に並べたリールを作成し、overflow:hiddenで1桁分だけ表示する
159   * ※ SCSSビルド不要で動作するようインラインスタイルで完結
160   */
161  function buildReels() {
162    if (is_built) {
163      return;
164    }
165
166    const text_containers = document.querySelectorAll('[data-countdown="text"]');
167    if (text_containers.length === 0) {
168      return;
169    }
170
171    text_containers.forEach(function (text_container) {
172      text_container.textContent = ''; // 初期テキストをクリア
173      text_container.style.display = 'inline-flex';
174      text_container.style.alignItems = 'center';
175      text_container.style.gap = '2.4px';
176
177      const container_reels = [];
178
179      UNITS.forEach(function (digits, ui) {
180        for (let i = 0; i < digits; i++) {
181          // 桁の枠(はみ出た数字を隠す)
182          const wrap = document.createElement('span');
183          wrap.style.display = 'inline-block';
184          wrap.style.height = DIGIT_HEIGHT + 'px';
185          wrap.style.overflow = 'hidden';
186
187          // 0〜9を縦に並べたリール
188          const reel = document.createElement('span');
189          reel.style.display = 'block';
190          reel.style.transition = 'transform 0.35s cubic-bezier(0.4, 0, 0.2, 1)';
191          for (let n = 0; n <= 9; n++) {
192            const s = document.createElement('span');
193            s.textContent = n; // XSS対策: textContent使用
194            s.style.display = 'block';
195            s.style.height = DIGIT_HEIGHT + 'px';
196            s.style.lineHeight = DIGIT_HEIGHT + 'px';
197            s.style.fontFamily = FONT_STYLE;
198            s.style.fontSize = '24px';
199            s.style.textAlign = 'center';
200            s.style.color = TEXT_COLOR;
201            reel.appendChild(s);
202          }
203
204          wrap.appendChild(reel);
205          text_container.appendChild(wrap);
206          container_reels.push({ reel: reel, current: -1 });
207        }
208
209        // ユニット間にコロン区切りを追加
210        if (ui < UNITS.length - 1) {
211          const sep = document.createElement('span');
212          sep.style.fontFamily = FONT_STYLE;
213          sep.style.fontSize = '24px';
214          sep.style.lineHeight = DIGIT_HEIGHT + 'px';
215          sep.style.color = TEXT_COLOR;
216          sep.textContent = ':';
217          text_container.appendChild(sep);
218        }
219      });
220
221      reels_map.set(text_container, container_reels);
222    });
223
224    is_built = true;
225  }
226
227  /**
228   * 指定桁のリールをスクロール(値が変わったときだけアニメーション)
229   * @param {Array} reels リール配列
230   * @param {number} idx リールのインデックス
231   * @param {number} val 表示する数字(0〜9)
232   */
233  function setDigit(reels, idx, val) {
234    const r = reels[idx];
235    if (r.current === val) {
236      return; // 値が変わっていなければ何もしない
237    }
238    r.current = val;
239    r.reel.style.transform = 'translateY(' + (val * -DIGIT_HEIGHT) + 'px)';
240  }
241
242  /**
243   * カウントダウンデータを各桁に反映
244   * @param {Object} data { days, hours, minutes, seconds }
245   */
246  function renderReels(data) {
247    const parts = [data.days, data.hours, data.minutes, data.seconds];
248
249    // 各コンテナのリールを更新
250    reels_map.forEach(function (reels) {
251      let ri = 0;
252      UNITS.forEach(function (digits, ui) {
253        const str = String(parts[ui]).padStart(digits, '0');
254        for (let ci = 0; ci < str.length; ci++) {
255          setDigit(reels, ri++, +str[ci]);
256        }
257      });
258    });
259  }
260
261  /**
262   * カウントダウン表示更新
263   */
264  function updateDisplay() {
265    const countdown_data = calculateCountdown();
266    const containers = document.querySelectorAll('[data-countdown="container"]');
267
268    if (containers.length === 0) {
269      return; // コンテナが存在しない場合は何もしない
270    }
271
272    if (!countdown_data) {
273      // 表示期間外 or カウントダウン終了
274      containers.forEach(function (container) {
275        container.style.display = 'none';
276      });
277      cleanup();
278      return;
279    }
280
281    // 表示期間内 - すべてのコンテナを表示
282    containers.forEach(function (container) {
283      container.style.display = '';
284    });
285
286    // リールDOM構築(初回のみ)
287    buildReels();
288
289    // リールアニメーションでカウントダウン表示更新
290    renderReels(countdown_data);
291
292    // X投稿リンクのhref更新 - すべてのリンクを更新
293    const share_links = document.querySelectorAll('[data-countdown="share"]');
294    share_links.forEach(function (share_link) {
295      share_link.href = generateShareURL();
296    });
297  }
298
299  /**
300   * 日付に応じてカウントダウンの表示位置を調整
301   *   PC版: .inner_creator の直後
302   *   SP版: ul.index_floor_link.creator の直後
303   */
304  function positionCountdowns() {
305    const now = getNow();
306    if (now < MOVE_DATE) {
307      return;
308    }
309
310    // クリエイター向けセクションの直後に移動
311    const countdown_elements = document.querySelectorAll('[data-countdown="container"]');
312    countdown_elements.forEach(function (countdown_element) {
313      const parent = countdown_element.parentElement;
314      if (!parent) {
315        return;
316      }
317      // PC版: .inner_creator の直後
318      const inner_creator = parent.querySelector('.inner_creator');
319      if (inner_creator) {
320        inner_creator.insertAdjacentElement('afterend', countdown_element);
321        return;
322      }
323      // SP版: ul.index_floor_link.creator の直後
324      const sp_creator = parent.querySelector('ul.index_floor_link.creator');
325      if (sp_creator) {
326        sp_creator.insertAdjacentElement('afterend', countdown_element);
327      }
328    });
329  }
330
331  /**
332   * カウントダウン初期化
333   */
334  function initCountdown() {
335    // 日付に応じた配置調整
336    positionCountdowns();
337
338    // 初回表示更新
339    updateDisplay();
340
341    // 1秒ごとに表示更新
342    countdown_interval = setInterval(updateDisplay, 1000);
343  }
344
345  /**
346   * クリーンアップ(メモリリーク防止)
347   */
348  function cleanup() {
349    if (countdown_interval) {
350      clearInterval(countdown_interval);
351      countdown_interval = null;
352    }
353  }
354
355  // === 実行 ===
356
357  // DOMContentLoaded後に自動実行
358  if (document.readyState === 'loading') {
359    document.addEventListener('DOMContentLoaded', initCountdown);
360  } else {
361    initCountdown();
362  }
363
364  // ページ離脱時にクリーンアップ(メモリリーク防止)
365  window.addEventListener('beforeunload', cleanup);
366})();

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.