/** * 公開一覧(TOP新着・掲載中一覧・条件検索・駅ページ・エリアページ)共通: * 「詳細を見る」等で物件詳細へ遷移し、ブラウザの戻るで一覧へ戻ったとき、 * 見ていたカード付近へ復帰するための共通ユーティリティ。 * * 保存先: * - sessionStorage(タブごとに自然に分離される。別タブ・別ウィンドウの状態と混同しない) * - history.state(同一タブ内で「戻る操作による表示」かどうかを判定するためのフラグのみ) * * 通常アクセス・メニュー経由の遷移では history.state にフラグが無いため、 * 復帰処理は何もしない(先頭表示のまま)。 * * 正本: test/lib/list-scroll-restore.cjs * 同期先: php-backend/assets/public-list-js/lib/list-scroll-restore.cjs(rssg_ensure_public_list_js_trees 経由) */ (function (root, factory) { if (typeof module === 'object' && module.exports) { module.exports = factory(); } else { root.ListScrollRestore = factory(); } })(typeof globalThis !== 'undefined' ? globalThis : this, function () { 'use strict'; var STORAGE_PREFIX = 'rakusumo_list_scroll:'; var STATE_FLAG = '__rakusumoListScrollKey'; /** 保存状態の有効期限。古いタブ状態を誤って適用しないための安全策(無限に残さない)。 */ var MAX_AGE_MS = 30 * 60 * 1000; var HEADER_OFFSET_FALLBACK = 96; /** 画像読み込み等によるレイアウトずれの再補正タイミング(固定回数のみ・無限ループにしない)。 */ var RECHECK_DELAYS_MS = [120, 350, 900]; var HIGHLIGHT_MS = 2500; function safeSessionStorage() { try { var s = window.sessionStorage; var testKey = '__rakusumoScrollTest__'; s.setItem(testKey, '1'); s.removeItem(testKey); return s; } catch (_e) { return null; } } function currentListKey() { return String(location.pathname || '') + String(location.search || ''); } function headerOffset() { var header = document.querySelector('.tp-header') || document.querySelector('header[role="banner"]') || document.querySelector('header'); if (header) { var rect = header.getBoundingClientRect(); if (rect && rect.height > 0) return Math.ceil(rect.height) + 12; } return HEADER_OFFSET_FALLBACK; } /** * 「詳細を見る」等クリック時(遷移前)に呼ぶ。 * 一覧URL・スクロール位置・クリックしたカードID・呼び出し側の状態(ページ番号・並び順等)を保存する。 * @param {string} cardId クリックされたカード/号室の識別子(data-property-id 等) * @param {object} [extraState] ページ番号・並び順などJSON化可能な値のみ */ function captureBeforeNavigate(cardId, extraState) { var storage = safeSessionStorage(); if (!storage) return; var key = currentListKey(); var payload = { ts: Date.now(), url: key, scrollY: window.scrollY || window.pageYOffset || 0, cardId: String(cardId || ''), state: extraState && typeof extraState === 'object' ? extraState : {}, }; try { storage.setItem(STORAGE_PREFIX + key, JSON.stringify(payload)); } catch (_e) { return; } try { var nextState = Object.assign({}, history.state || {}); nextState[STATE_FLAG] = key; history.replaceState(nextState, '', location.href); } catch (_e2) { /* noop */ } } function readSavedState() { var storage = safeSessionStorage(); if (!storage) return null; var key = currentListKey(); var raw; try { raw = storage.getItem(STORAGE_PREFIX + key); } catch (_e) { return null; } if (!raw) return null; var parsed; try { parsed = JSON.parse(raw); } catch (_e2) { return null; } if (!parsed || typeof parsed !== 'object') return null; if (typeof parsed.ts !== 'number' || Date.now() - parsed.ts > MAX_AGE_MS) return null; if (parsed.url !== key) return null; return parsed; } /** 「戻る操作による表示」かどうか(history.state のフラグと現在URLのキーが一致するか)。 */ function isReturningNavigation() { var key = currentListKey(); try { return !!(history.state && history.state[STATE_FLAG] === key); } catch (_e) { return false; } } function findCardEl(cardId, cardAttr) { if (!cardId) return null; var attr = cardAttr || 'data-property-id'; try { return document.querySelector('[' + attr + '="' + String(cardId).replace(/"/g, '\\"') + '"]'); } catch (_e) { return null; } } function scrollToTarget(el, fallbackY) { if (el && typeof el.getBoundingClientRect === 'function') { var rect = el.getBoundingClientRect(); var offset = headerOffset(); var centerAdjust = Math.max(0, (window.innerHeight - rect.height) / 2 - offset); var top = rect.top + window.scrollY - offset - Math.min(centerAdjust, window.innerHeight * 0.25); window.scrollTo({ top: Math.max(0, top), behavior: 'auto' }); return true; } if (typeof fallbackY === 'number') { window.scrollTo({ top: Math.max(0, fallbackY), behavior: 'auto' }); return true; } return false; } function highlightCard(el) { if (!el || !el.classList) return; el.classList.add('is-return-highlight'); setTimeout(function () { el.classList.remove('is-return-highlight'); }, HIGHLIGHT_MS); } /** * 一覧の非同期描画完了後に呼ぶ。戻る操作による表示で、保存状態がある場合のみ復帰する。 * 通常アクセス・メニュー経由では何もしない(先頭表示のまま)。 * @param {object} [opts] { cardAttr?: string } * @returns {boolean} 復帰処理を行ったか */ function restoreAfterRender(opts) { opts = opts || {}; if (!isReturningNavigation()) return false; var saved = readSavedState(); if (!saved) return false; function apply() { var el = findCardEl(saved.cardId, opts.cardAttr); var applied = scrollToTarget(el, saved.scrollY); if (applied && el) highlightCard(el); return applied; } apply(); RECHECK_DELAYS_MS.forEach(function (delay) { setTimeout(apply, delay); }); return true; } /** * 一覧ページ読み込み時に一度だけ呼ぶ。戻る操作による表示の場合、保存済みの * ページ番号・並び順等を返す(呼び出し側が反映してから再フェッチ・再描画し、 * その後 restoreAfterRender を呼ぶこと)。戻る操作でない場合は null。 */ function readSavedListState() { if (!isReturningNavigation()) return null; var saved = readSavedState(); if (!saved || !saved.state) return null; return saved.state; } /** bfcache 復元(pageshow, event.persisted===true)専用。DOM は既に描画済みのため即座に復帰を試みる。 */ function bindBfcacheRestore(opts) { window.addEventListener('pageshow', function (e) { if (!e.persisted) return; restoreAfterRender(opts); }); } /** ブラウザ既定のスクロール自動復元と競合しないよう manual 化する。一覧ページの初期化時にのみ呼ぶこと。 */ function disableNativeScrollRestoration() { try { if ('scrollRestoration' in history) { history.scrollRestoration = 'manual'; } } catch (_e) { /* noop */ } } return { captureBeforeNavigate: captureBeforeNavigate, restoreAfterRender: restoreAfterRender, readSavedListState: readSavedListState, isReturningNavigation: isReturningNavigation, bindBfcacheRestore: bindBfcacheRestore, disableNativeScrollRestoration: disableNativeScrollRestoration, }; });