From 4d29eabffd08ff8ebe3309e20062d88783b6141d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gw=C3=A9na=C3=ABl=20Rault?= Date: Wed, 18 Feb 2026 14:54:14 +0100 Subject: [PATCH 1/2] Fix return on previous page --- app/assets/javascripts/ajax.js | 4 ++-- app/assets/javascripts/application.js | 24 ++++++++++++++++--- .../javascripts/deliverables_by_vehicles.js | 8 ++++--- app/assets/javascripts/order_arrays.js | 2 +- app/assets/javascripts/plannings.js | 6 ++--- .../javascripts/plannings_by_destinations.js | 2 +- app/assets/javascripts/scaffolds.js | 22 +++++++++++++++++ app/assets/javascripts/vehicle_usage_sets.js | 5 +++- app/assets/javascripts/zonings.js | 8 +++---- app/controllers/concerns/link_back.rb | 6 +++++ 10 files changed, 68 insertions(+), 19 deletions(-) diff --git a/app/assets/javascripts/ajax.js b/app/assets/javascripts/ajax.js index 2dc279018..b8d6f5cd1 100644 --- a/app/assets/javascripts/ajax.js +++ b/app/assets/javascripts/ajax.js @@ -179,9 +179,9 @@ export const progressDialog = function(delayedJob, dialog, url, callback, option }); }, 200); - $(document).on('page:before-change', function() { + $(document).on('turbolinks:before-cache', function cleanupProgressDialog() { clearTimeout(progressDialogTimerId); - $(document).off('page:before-change'); + $(document).off('turbolinks:before-cache', cleanupProgressDialog); }); } diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 432208139..23072c7e6 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -97,6 +97,24 @@ $(document).on('turbolinks:load', function() { Paloma.start(); }); -// $(document).on('page:restore', function() { -// Paloma.start(); -// }); +// Fallback for popstate events that Turbolinks ignores (e.g. history.state +// was wiped by location.replace or replaceState from third-party libs). +// Turbolinks only handles popstate when event.state has its restorationIdentifier. +// When missing, the URL changes but the body is never swapped. +(function() { + var popstateHandled = false; + + document.addEventListener('turbolinks:before-render', function() { + popstateHandled = true; + }); + + window.addEventListener('popstate', function(event) { + popstateHandled = false; + + setTimeout(function() { + if (!popstateHandled) { + Turbolinks.visit(window.location.href, { action: 'replace' }); + } + }, 50); + }); +})(); diff --git a/app/assets/javascripts/deliverables_by_vehicles.js b/app/assets/javascripts/deliverables_by_vehicles.js index 85dc636bd..d05822847 100644 --- a/app/assets/javascripts/deliverables_by_vehicles.js +++ b/app/assets/javascripts/deliverables_by_vehicles.js @@ -42,7 +42,7 @@ const deliverable_by_vehicle_show = function(params) { pushState: function(p, cb) { if (!hasState) { - return location.assign(p.URL); + return Turbolinks.visit(p.URL); } history.pushState(p.state, p.title, p.URL); cb(p.state); @@ -52,7 +52,9 @@ const deliverable_by_vehicle_show = function(params) { const popStateHandler = function(event) { const state = event.state; - if (!state.vehicleId) { + if (!state || !state.vehicleId) { + // Orphan history entry (e.g. navigated away via Turbolinks then came back) + // Let the browser handle it normally return; } $("#vehicle_id") @@ -62,7 +64,7 @@ const deliverable_by_vehicle_show = function(params) { }; window.addEventListener('popstate', popStateHandler); - $(document).on('page:before-change', function() { + $(document).on('turbolinks:before-cache', function() { window.removeEventListener('popstate', popStateHandler); }); diff --git a/app/assets/javascripts/order_arrays.js b/app/assets/javascripts/order_arrays.js index bc667e31e..42553c9f4 100644 --- a/app/assets/javascripts/order_arrays.js +++ b/app/assets/javascripts/order_arrays.js @@ -391,7 +391,7 @@ const order_arrays_edit = function(params) { complete: completeWaiting, error: ajaxError, success: function() { - window.location = '/plannings/' + planning_id + '/edit'; + Turbolinks.visit('/plannings/' + planning_id + '/edit'); } }); }); diff --git a/app/assets/javascripts/plannings.js b/app/assets/javascripts/plannings.js index de0c58e9e..ddcd33c38 100644 --- a/app/assets/javascripts/plannings.js +++ b/app/assets/javascripts/plannings.js @@ -690,7 +690,7 @@ export const plannings_edit = function(params) { } initMarkers(); backgroundTaskIntervalId = setInterval(backgroundTask, 60000); - $(document).on('page:before-change', function() { + $(document).on('turbolinks:before-cache', function() { clearInterval(backgroundTaskIntervalId); }); } @@ -3048,7 +3048,7 @@ var plannings_index = function(params) { return warning(I18n.t('plannings.index.vehicle_select_error')); } planning_ids = $('[name^=planning]:checked').map(function() { return $(this).val(); }).toArray().join(','); - location.assign('/routes_by_vehicles/' + vehicle_id + '?planning_ids=' + planning_ids); + Turbolinks.visit('/routes_by_vehicles/' + vehicle_id + '?planning_ids=' + planning_ids); }); $('#deliverables-by-vehicle').on('click', function(e) { @@ -3057,7 +3057,7 @@ var plannings_index = function(params) { return warning(I18n.t('plannings.index.vehicle_select_error')); } planning_ids = $('[name^=planning]:checked').map(function() { return $(this).val(); }).toArray().join(','); - location.assign('/deliverables_by_vehicles/' + vehicle_id + '?planning_ids=' + planning_ids); + Turbolinks.visit('/deliverables_by_vehicles/' + vehicle_id + '?planning_ids=' + planning_ids); }); }; diff --git a/app/assets/javascripts/plannings_by_destinations.js b/app/assets/javascripts/plannings_by_destinations.js index 12286bfeb..aa61d35a9 100644 --- a/app/assets/javascripts/plannings_by_destinations.js +++ b/app/assets/javascripts/plannings_by_destinations.js @@ -30,7 +30,7 @@ const planningsShow = function(params) { const countMovedDestinations = function(max) { movedDestinations++; $('.progress-bar').css('width', (movedDestinations * 100 / max) + '%'); - if (movedDestinations == max) location.href = ''; + if (movedDestinations == max) Turbolinks.visit(window.location.pathname, { action: 'replace' }); }; const getQuantitiesByPlanning = function() { diff --git a/app/assets/javascripts/scaffolds.js b/app/assets/javascripts/scaffolds.js index 8e1cad543..e83e4f7a7 100644 --- a/app/assets/javascripts/scaffolds.js +++ b/app/assets/javascripts/scaffolds.js @@ -364,6 +364,26 @@ export const mapInitialize = function(params) { return map; }; +// Patch leaflet-hash to preserve Turbolinks history state. +// The original uses location.replace(hash) which wipes history.state, +// breaking Turbolinks back/forward navigation (restorationIdentifier lost). +function patchLeafletHash() { + if (typeof L !== 'undefined' && L.Hash && L.Hash.prototype) { + L.Hash.prototype.onMapMove = function() { + if (this.movingMap || !this.map._loaded) { + return false; + } + var hash = this.formatHash(this.map); + if (this.lastHash != hash) { + history.replaceState(history.state, '', hash); + this.lastHash = hash; + } + }; + } +} + +patchLeafletHash(); + // FIXME initOnly used for api-web because Firefox doesn't support hash replace (in Leaflet Hash) within an iframe. A new url is fetched by Turbolinks. Chrome works. export const initializeMapHash = function(map, initOnly) { if (initOnly) { @@ -374,7 +394,9 @@ export const initializeMapHash = function(map, initOnly) { } // FIXME when turbolinks get updated to work with Edge else if (navigator.userAgent.indexOf('Edge') === -1) { + patchLeafletHash(); map.addHash(); + var removeHash = function() { if (map.removeHash) { map.removeHash(); diff --git a/app/assets/javascripts/vehicle_usage_sets.js b/app/assets/javascripts/vehicle_usage_sets.js index 60fca429a..c41026290 100644 --- a/app/assets/javascripts/vehicle_usage_sets.js +++ b/app/assets/javascripts/vehicle_usage_sets.js @@ -29,7 +29,10 @@ const vehicle_usage_sets_index = function(params) { // override accordion collapse bootstrap code $('a.accordion-toggle').click(function() { var id = $(this).attr('href'); - window.location.hash = id; + // Use replaceState to track accordion state without creating history entries + if (history.replaceState) { + history.replaceState(null, '', id); + } var allCollapsed = $('.accordion-body.collapse.in').size() ? true : false; $('.accordion-body.collapse.in').each(function() { var $this = $(this); diff --git a/app/assets/javascripts/zonings.js b/app/assets/javascripts/zonings.js index 2fbf5078a..6c39c302b 100644 --- a/app/assets/javascripts/zonings.js +++ b/app/assets/javascripts/zonings.js @@ -123,13 +123,11 @@ export const zonings_edit = function(params) { e.preventDefault(); } } - $(document).on('page:change', function() { - $(document).off('page:before-change', checkZoningChanges); - }); } - $(document).on('turbolinks:load', function() { - $(document).on('page:before-change', checkZoningChanges); + $(document).on('turbolinks:before-visit', checkZoningChanges); + $(document).on('turbolinks:before-cache', function() { + $(document).off('turbolinks:before-visit', checkZoningChanges); }); map.on('pm:drawstart', function(e) { diff --git a/app/controllers/concerns/link_back.rb b/app/controllers/concerns/link_back.rb index cabf888ce..714ad77d0 100644 --- a/app/controllers/concerns/link_back.rb +++ b/app/controllers/concerns/link_back.rb @@ -5,6 +5,7 @@ module LinkBack included do after_action :save_link_back, only: [:new, :edit] + before_action :clear_stale_link_back, except: [:new, :edit, :create, :update] end private @@ -27,6 +28,11 @@ def save_link_back end end + # Expire stale link_back when navigating away from new/edit/create/update + def clear_stale_link_back + session.delete(:link_back) if session[:link_back] + end + def link_back session.delete(:link_back) end From 907406a4b1a7d1e1155a69c9676e032b7bc1a093 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gw=C3=A9na=C3=ABl=20Rault?= Date: Wed, 18 Feb 2026 15:04:56 +0100 Subject: [PATCH 2/2] Fix map drift --- app/assets/javascripts/scaffolds.js | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/scaffolds.js b/app/assets/javascripts/scaffolds.js index e83e4f7a7..3d5229747 100644 --- a/app/assets/javascripts/scaffolds.js +++ b/app/assets/javascripts/scaffolds.js @@ -364,9 +364,13 @@ export const mapInitialize = function(params) { return map; }; -// Patch leaflet-hash to preserve Turbolinks history state. -// The original uses location.replace(hash) which wipes history.state, -// breaking Turbolinks back/forward navigation (restorationIdentifier lost). +// Patch leaflet-hash for two issues: +// 1. The original uses location.replace(hash) which wipes history.state, +// breaking Turbolinks back/forward navigation (restorationIdentifier lost). +// 2. ControlledBounds overrides setView to add an offset for controls/sidebar, +// but getCenter() returns the raw container center (already offset). This +// causes double-offset on hash restore: the hash stores offset coords, and +// setView applies the offset again, shifting the map by menu-left/planbar. function patchLeafletHash() { if (typeof L !== 'undefined' && L.Hash && L.Hash.prototype) { L.Hash.prototype.onMapMove = function() { @@ -379,6 +383,21 @@ function patchLeafletHash() { this.lastHash = hash; } }; + + var originalFormatHash = L.Hash.formatHash; + L.Hash.formatHash = function(map) { + if (map._deoffsetLatLng) { + var center = map._deoffsetLatLng(map.getCenter()); + var zoom = map.getZoom(); + var precision = Math.max(0, Math.ceil(Math.log(zoom) / Math.LN2)); + return '#' + [zoom, + center.lat.toFixed(precision), + center.lng.toFixed(precision) + ].join('/'); + } + return originalFormatHash.call(this, map); + }; + L.Hash.prototype.formatHash = L.Hash.formatHash; } }