From 3918b05e676187db3ca5f908292e22ba1928bdc6 Mon Sep 17 00:00:00 2001 From: SuDC Date: Wed, 10 Dec 2025 10:47:54 +0700 Subject: [PATCH 1/4] Add scroll position fix script --- .../vendor/exment/js/scroll-position-fix.js | 195 ++++++++++++++++++ src/Middleware/Bootstrap.php | 1 + 2 files changed, 196 insertions(+) create mode 100644 public/vendor/exment/js/scroll-position-fix.js diff --git a/public/vendor/exment/js/scroll-position-fix.js b/public/vendor/exment/js/scroll-position-fix.js new file mode 100644 index 000000000..02c289a2c --- /dev/null +++ b/public/vendor/exment/js/scroll-position-fix.js @@ -0,0 +1,195 @@ +/** + * Fix scroll position restore issue after browser back button + * For custom table list view in Exment + */ +(function() { + 'use strict'; + + // Storage key for scroll positions + const SCROLL_POSITION_KEY = 'exment_scroll_positions'; + const MAX_HISTORY = 50; // Keep last 50 positions + const CLASSNAME_CUSTOM_VALUE_GRID = 'block_custom_value_grid'; + + // Get current page identifier + function getPageKey() { + return window.location.pathname + window.location.search; + } + + // Get all scroll positions from sessionStorage + function getScrollPositions() { + try { + const data = sessionStorage.getItem(SCROLL_POSITION_KEY); + return data ? JSON.parse(data) : {}; + } catch (e) { + console.warn('Failed to load scroll positions:', e); + return {}; + } + } + + // Save scroll positions to sessionStorage + function saveScrollPositions(positions) { + try { + // Limit the number of stored positions + const keys = Object.keys(positions); + if (keys.length > MAX_HISTORY) { + // Remove oldest entries + keys.slice(0, keys.length - MAX_HISTORY).forEach(key => { + delete positions[key]; + }); + } + sessionStorage.setItem(SCROLL_POSITION_KEY, JSON.stringify(positions)); + } catch (e) { + console.warn('Failed to save scroll positions:', e); + } + } + + // Save current scroll position + function saveCurrentScrollPosition() { + const pageKey = getPageKey(); + const scrollTop = $(window).scrollTop() || document.documentElement.scrollTop || document.body.scrollTop || 0; + + const positions = getScrollPositions(); + positions[pageKey] = { + scrollTop: scrollTop, + timestamp: Date.now() + }; + saveScrollPositions(positions); + + // Also store in history state if available + if (window.history && window.history.replaceState) { + const currentState = window.history.state || {}; + currentState.scrollTop = scrollTop; + try { + window.history.replaceState(currentState, document.title); + } catch (e) { + console.warn('Failed to update history state:', e); + } + } + } + + // Restore scroll position + function restoreScrollPosition() { + const pageKey = getPageKey(); + const positions = getScrollPositions(); + const savedPosition = positions[pageKey]; + + let scrollTop = 0; + + // Try to get from history state first + if (window.history && window.history.state && typeof window.history.state.scrollTop === 'number') { + scrollTop = window.history.state.scrollTop; + } + // Fallback to sessionStorage + else if (savedPosition && typeof savedPosition.scrollTop === 'number') { + scrollTop = savedPosition.scrollTop; + } + + if (scrollTop > 0) { + // Use setTimeout to ensure DOM is ready + setTimeout(function() { + $('html, body').scrollTop(scrollTop); + $(window).scrollTop(scrollTop); + document.documentElement.scrollTop = scrollTop; + document.body.scrollTop = scrollTop; + }, 0); + + // Fallback restore after a bit longer delay + setTimeout(function() { + if (Math.abs($(window).scrollTop() - scrollTop) > 50) { + $('html, body').scrollTop(scrollTop); + } + }, 100); + } + } + + // Clear old scroll positions (older than 1 hour) + function clearOldPositions() { + const positions = getScrollPositions(); + const now = Date.now(); + const oneHour = 60 * 60 * 1000; + + Object.keys(positions).forEach(key => { + if (now - positions[key].timestamp > oneHour) { + delete positions[key]; + } + }); + + saveScrollPositions(positions); + } + + // Initialize on document ready + $(function() { + // Disable browser's default scroll restoration + if ('scrollRestoration' in history) { + history.scrollRestoration = 'manual'; + } + + // Clear old positions on page load + clearOldPositions(); + + // Restore scroll position on page load + restoreScrollPosition(); + + // Save scroll position periodically while scrolling + let scrollTimer; + $(window).on('scroll', function() { + clearTimeout(scrollTimer); + scrollTimer = setTimeout(function() { + // Only save if we're on a list page (grid view) + if ($('.' + CLASSNAME_CUSTOM_VALUE_GRID).length > 0) { + saveCurrentScrollPosition(); + } + }, 150); + }); + + // Save scroll position before leaving page + $(window).on('beforeunload', function() { + if ($('.' + CLASSNAME_CUSTOM_VALUE_GRID).length > 0) { + saveCurrentScrollPosition(); + } + }); + + // Handle pjax events + if ($.pjax) { + // Save scroll position before pjax request + $(document).on('pjax:send', function(event, xhr, options) { + if ($('.' + CLASSNAME_CUSTOM_VALUE_GRID).length > 0) { + saveCurrentScrollPosition(); + } + }); + + // Restore scroll position after pjax complete + $(document).on('pjax:complete', function(event, xhr, textStatus, options) { + // Wait for content to be rendered + setTimeout(function() { + if ($('.' + CLASSNAME_CUSTOM_VALUE_GRID).length > 0) { + restoreScrollPosition(); + } + }, 50); + }); + + // Also try to restore on pjax:end + $(document).on('pjax:end', function(event, xhr, options) { + setTimeout(function() { + if ($('.' + CLASSNAME_CUSTOM_VALUE_GRID).length > 0) { + restoreScrollPosition(); + } + }, 100); + }); + } + + // Handle browser back/forward buttons + $(window).on('popstate', function(event) { + setTimeout(function() { + if ($('.' + CLASSNAME_CUSTOM_VALUE_GRID).length > 0) { + restoreScrollPosition(); + } + }, 50); + }); + + // Save position when clicking on links in grid + $('.' + CLASSNAME_CUSTOM_VALUE_GRID).on('click', 'a', function() { + saveCurrentScrollPosition(); + }); + }); +})(); diff --git a/src/Middleware/Bootstrap.php b/src/Middleware/Bootstrap.php index 4c57caa8d..507ba13d0 100644 --- a/src/Middleware/Bootstrap.php +++ b/src/Middleware/Bootstrap.php @@ -77,6 +77,7 @@ protected function setCssJs(Request $request, \Closure $next) 'vendor/exment/jstree/jstree.min.js', 'vendor/exment/js/common_all.js', 'vendor/exment/js/common.js', + 'vendor/exment/js/scroll-position-fix.js', 'vendor/exment/js/search.js', 'vendor/exment/js/calc.js', 'vendor/exment/js/notify_navbar.js', From 29edfc1ad5f9e07e572c313baa74b857aceb92f0 Mon Sep 17 00:00:00 2001 From: SuDC Date: Mon, 15 Dec 2025 15:09:06 +0700 Subject: [PATCH 2/4] Sort and limit stored scroll positions by timestamp --- public/vendor/exment/js/scroll-position-fix.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/public/vendor/exment/js/scroll-position-fix.js b/public/vendor/exment/js/scroll-position-fix.js index 02c289a2c..06f49c0ed 100644 --- a/public/vendor/exment/js/scroll-position-fix.js +++ b/public/vendor/exment/js/scroll-position-fix.js @@ -32,8 +32,11 @@ // Limit the number of stored positions const keys = Object.keys(positions); if (keys.length > MAX_HISTORY) { - // Remove oldest entries - keys.slice(0, keys.length - MAX_HISTORY).forEach(key => { + // Sort by timestamp and remove oldest entries + const sortedKeys = keys.sort((a, b) => { + return (positions[a].timestamp || 0) - (positions[b].timestamp || 0); + }); + sortedKeys.slice(0, keys.length - MAX_HISTORY).forEach(key => { delete positions[key]; }); } From f3152f78c6434b88c6466d9c501eb34d748b5556 Mon Sep 17 00:00:00 2001 From: SuDC Date: Mon, 15 Dec 2025 16:08:40 +0700 Subject: [PATCH 3/4] Enhance scroll position restoration and validation logic --- .../vendor/exment/js/scroll-position-fix.js | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/public/vendor/exment/js/scroll-position-fix.js b/public/vendor/exment/js/scroll-position-fix.js index 06f49c0ed..8d2bb8b7d 100644 --- a/public/vendor/exment/js/scroll-position-fix.js +++ b/public/vendor/exment/js/scroll-position-fix.js @@ -90,18 +90,15 @@ if (scrollTop > 0) { // Use setTimeout to ensure DOM is ready setTimeout(function() { - $('html, body').scrollTop(scrollTop); - $(window).scrollTop(scrollTop); - document.documentElement.scrollTop = scrollTop; - document.body.scrollTop = scrollTop; + window.scrollTo(0, scrollTop); + // Verify and retry if needed + setTimeout(function() { + const currentScroll = window.pageYOffset || document.documentElement.scrollTop; + if (Math.abs(currentScroll - scrollTop) > 50) { + window.scrollTo(0, scrollTop); + } + }, 100); }, 0); - - // Fallback restore after a bit longer delay - setTimeout(function() { - if (Math.abs($(window).scrollTop() - scrollTop) > 50) { - $('html, body').scrollTop(scrollTop); - } - }, 100); } } @@ -112,7 +109,13 @@ const oneHour = 60 * 60 * 1000; Object.keys(positions).forEach(key => { - if (now - positions[key].timestamp > oneHour) { + try { + const position = positions[key]; + if (!position || typeof position !== 'object' || !position.timestamp || now - position.timestamp > oneHour) { + delete positions[key]; + } + } catch (e) { + // Remove invalid entry delete positions[key]; } }); @@ -122,6 +125,12 @@ // Initialize on document ready $(function() { + // Prevent multiple initialization + if (window.exmentScrollFixInitialized) { + return; + } + window.exmentScrollFixInitialized = true; + // Disable browser's default scroll restoration if ('scrollRestoration' in history) { history.scrollRestoration = 'manual'; From cedab8a700abcffc43e3a759854b57473224ef3c Mon Sep 17 00:00:00 2001 From: SuDC Date: Tue, 16 Dec 2025 14:55:20 +0700 Subject: [PATCH 4/4] Refactor scroll position handling --- .../vendor/exment/js/scroll-position-fix.js | 126 +++--------------- 1 file changed, 19 insertions(+), 107 deletions(-) diff --git a/public/vendor/exment/js/scroll-position-fix.js b/public/vendor/exment/js/scroll-position-fix.js index 8d2bb8b7d..1aa509387 100644 --- a/public/vendor/exment/js/scroll-position-fix.js +++ b/public/vendor/exment/js/scroll-position-fix.js @@ -5,60 +5,13 @@ (function() { 'use strict'; - // Storage key for scroll positions - const SCROLL_POSITION_KEY = 'exment_scroll_positions'; - const MAX_HISTORY = 50; // Keep last 50 positions const CLASSNAME_CUSTOM_VALUE_GRID = 'block_custom_value_grid'; - // Get current page identifier - function getPageKey() { - return window.location.pathname + window.location.search; - } - - // Get all scroll positions from sessionStorage - function getScrollPositions() { - try { - const data = sessionStorage.getItem(SCROLL_POSITION_KEY); - return data ? JSON.parse(data) : {}; - } catch (e) { - console.warn('Failed to load scroll positions:', e); - return {}; - } - } - - // Save scroll positions to sessionStorage - function saveScrollPositions(positions) { - try { - // Limit the number of stored positions - const keys = Object.keys(positions); - if (keys.length > MAX_HISTORY) { - // Sort by timestamp and remove oldest entries - const sortedKeys = keys.sort((a, b) => { - return (positions[a].timestamp || 0) - (positions[b].timestamp || 0); - }); - sortedKeys.slice(0, keys.length - MAX_HISTORY).forEach(key => { - delete positions[key]; - }); - } - sessionStorage.setItem(SCROLL_POSITION_KEY, JSON.stringify(positions)); - } catch (e) { - console.warn('Failed to save scroll positions:', e); - } - } - // Save current scroll position function saveCurrentScrollPosition() { - const pageKey = getPageKey(); const scrollTop = $(window).scrollTop() || document.documentElement.scrollTop || document.body.scrollTop || 0; - const positions = getScrollPositions(); - positions[pageKey] = { - scrollTop: scrollTop, - timestamp: Date.now() - }; - saveScrollPositions(positions); - - // Also store in history state if available + // Store in history state if available if (window.history && window.history.replaceState) { const currentState = window.history.state || {}; currentState.scrollTop = scrollTop; @@ -70,21 +23,13 @@ } } - // Restore scroll position + // Restore scroll position from history state function restoreScrollPosition() { - const pageKey = getPageKey(); - const positions = getScrollPositions(); - const savedPosition = positions[pageKey]; - let scrollTop = 0; - // Try to get from history state first + // Only restore from history state (for back button) if (window.history && window.history.state && typeof window.history.state.scrollTop === 'number') { scrollTop = window.history.state.scrollTop; - } - // Fallback to sessionStorage - else if (savedPosition && typeof savedPosition.scrollTop === 'number') { - scrollTop = savedPosition.scrollTop; } if (scrollTop > 0) { @@ -102,27 +47,6 @@ } } - // Clear old scroll positions (older than 1 hour) - function clearOldPositions() { - const positions = getScrollPositions(); - const now = Date.now(); - const oneHour = 60 * 60 * 1000; - - Object.keys(positions).forEach(key => { - try { - const position = positions[key]; - if (!position || typeof position !== 'object' || !position.timestamp || now - position.timestamp > oneHour) { - delete positions[key]; - } - } catch (e) { - // Remove invalid entry - delete positions[key]; - } - }); - - saveScrollPositions(positions); - } - // Initialize on document ready $(function() { // Prevent multiple initialization @@ -136,12 +60,8 @@ history.scrollRestoration = 'manual'; } - // Clear old positions on page load - clearOldPositions(); - - // Restore scroll position on page load restoreScrollPosition(); - + // Save scroll position periodically while scrolling let scrollTimer; $(window).on('scroll', function() { @@ -163,6 +83,12 @@ // Handle pjax events if ($.pjax) { + let isPjaxPopstate = false; + + $(document).on('pjax:popstate', function() { + isPjaxPopstate = true; + }); + // Save scroll position before pjax request $(document).on('pjax:send', function(event, xhr, options) { if ($('.' + CLASSNAME_CUSTOM_VALUE_GRID).length > 0) { @@ -170,35 +96,21 @@ } }); - // Restore scroll position after pjax complete - $(document).on('pjax:complete', function(event, xhr, textStatus, options) { - // Wait for content to be rendered - setTimeout(function() { - if ($('.' + CLASSNAME_CUSTOM_VALUE_GRID).length > 0) { - restoreScrollPosition(); - } - }, 50); - }); - - // Also try to restore on pjax:end + // Handle scroll position after pjax complete $(document).on('pjax:end', function(event, xhr, options) { - setTimeout(function() { - if ($('.' + CLASSNAME_CUSTOM_VALUE_GRID).length > 0) { + if ($('.' + CLASSNAME_CUSTOM_VALUE_GRID).length > 0) { + if (isPjaxPopstate) { + // If it's a back/forward navigation, restore scroll restoreScrollPosition(); + isPjaxPopstate = false; + } else { + // If it's a new navigation (pagination, sort, etc), scroll to top + window.scrollTo(0, 0); } - }, 100); + } }); } - // Handle browser back/forward buttons - $(window).on('popstate', function(event) { - setTimeout(function() { - if ($('.' + CLASSNAME_CUSTOM_VALUE_GRID).length > 0) { - restoreScrollPosition(); - } - }, 50); - }); - // Save position when clicking on links in grid $('.' + CLASSNAME_CUSTOM_VALUE_GRID).on('click', 'a', function() { saveCurrentScrollPosition();