Fix round 2 : fermer le cadre d'export sous la fiche détaillée en toutes circonstances, ne pas la recadrer au retour, réarmer BOOTSTRAPPING sur échec

panelVisibleTab() renvoie null tant que la fiche détaillée est ouverte,
quel que soit ce qui redéclenche panelRender (grip, redimensionnement,
changement phoneQuery…) : le cadre d'export ne rouvre plus derrière elle.
Rétablir l'onglet Export après un retour (restoreTabAfterDetails / M5)
repose désormais le cadre sans recadrer la vue ni le recentrer
(nouveau drapeau RESTORING_TAB, distingué d'une ouverture explicite par
l'utilisateur). BOOTSTRAPPING est aussi réarmé quand /api/map/meta échoue,
pour qu'une ouverture explicite ultérieure de l'onglet recadre bien la vue.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Antoine Jacquin
2026-09-27 19:21:50 +02:00
parent 72b10cd228
commit 72c50dca99
2 changed files with 72 additions and 16 deletions

View File

@ -522,21 +522,45 @@ def test_ui_phone_return_and_escape_keep_sheet_height():
def test_ui_saved_print_tab_does_not_rezoom_shared_link():
"""L'onglet Export mémorisé ne recadre pas la vue au tout premier rendu du
panneau (sinon il écrase le zoom d'un lien partagé ou de l'URL initiale) :
BOOTSTRAPPING passe à faux juste après ce premier rendu."""
BOOTSTRAPPING passe à faux juste après CE premier rendu — succès ET échec
du chargement de /api/map/meta (sinon un serveur injoignable au démarrage
bloque BOOTSTRAPPING à vrai pour toujours : plus aucun recadrage, même
après une ouverture explicite ultérieure de l'onglet)."""
from lidar_pipeline.mapui import _MAP_JS
assert "let BOOTSTRAPPING = true;" in _MAP_JS
assert "printSetOpen(e.detail.visible === 'print', !BOOTSTRAPPING);" in _MAP_JS
boot = _MAP_JS[_MAP_JS.index("panelRender();\n BOOTSTRAPPING = false;"):]
assert boot.startswith("panelRender();\n BOOTSTRAPPING = false;")
assert "printSetOpen(e.detail.visible === 'print', !BOOTSTRAPPING && !RESTORING_TAB);" in _MAP_JS
then_branch = _MAP_JS[_MAP_JS.index("panelRender();\n BOOTSTRAPPING = false;"):]
assert then_branch.startswith("panelRender();\n BOOTSTRAPPING = false;")
catch_branch = _MAP_JS[_MAP_JS.index(".catch(() => {\n toast('Serveur de tuiles injoignable."):]
assert "BOOTSTRAPPING = false;" in catch_branch[:400]
def test_ui_details_view_hides_print_frame():
"""Ouvrir la fiche détaillée pendant que l'onglet Export est affiché
referme le cadre d'export (sinon cadre + poignée restent visibles
derrière la fiche)."""
derrière la fiche) — et le reste tant que la fiche est ouverte, quel que
soit ce qui redéclenche un rendu du panneau (grip, redimensionnement,
changement phoneQuery…) puisque panelVisibleTab() renvoie null tant que
#tileView n'est pas masqué."""
from lidar_pipeline.mapui import _MAP_JS
open_details = _MAP_JS[_MAP_JS.index("function openTileDetails("):]
assert "printSetOpen(false);" in open_details[:600]
visible_tab = _MAP_JS[_MAP_JS.index("function panelVisibleTab("):]
assert "if (!el('tileView').hidden) return null;" in visible_tab[:300]
def test_ui_restoring_tab_does_not_refit_or_recentre_print_frame():
"""Rétablir l'onglet Export (retour de la fiche détaillée) rouvre le
cadre là où il était, sans recadrer la vue ni le recentrer sur l'écran —
au contraire d'une ouverture explicite de l'onglet par l'utilisateur."""
from lidar_pipeline.mapui import _MAP_JS
assert "let RESTORING_TAB = false;" in _MAP_JS
restore = _MAP_JS[_MAP_JS.index("function restoreTabAfterDetails("):]
assert "RESTORING_TAB = true;" in restore[:400]
assert "panelRender();\n RESTORING_TAB = false;" in restore[:400]
open_fn = _MAP_JS[_MAP_JS.index("function printSetOpen("):]
assert "explicit && !map.getBounds().contains(PRINT.center)" in open_fn[:700]
assert "printRefreshFrame(!!explicit);" in open_fn[:700]
def test_ui_colors_are_tokenised():

View File

@ -46,8 +46,15 @@ let META = null;
let STATE = null;
// Vrai jusqu'au premier rendu du panneau au démarrage : évite qu'un onglet
// Export mémorisé recadre la vue par-dessus un lien partagé ou l'URL initiale
// (cf. printSetOpen).
// (cf. printSetOpen). Remis à faux dans le .then() ET le .catch() du chargement
// de /api/map/meta (sinon un serveur injoignable au démarrage le laisse
// bloqué à vrai pour toujours : l'onglet Export ne recadrerait plus jamais).
let BOOTSTRAPPING = true;
// Vrai le temps d'un panelRender() qui RÉTABLIT un onglet déjà connu
// (restoreTabAfterDetails, retour de la fiche détaillée) plutôt que d'en
// ouvrir un nouveau à la demande de l'utilisateur : le cadre d'export ne doit
// alors ni recadrer la vue, ni recentrer le cadre lui-même (cf. printSetOpen).
let RESTORING_TAB = false;
// --- thème ------------------------------------------------------------------
// auto = suit le système ; ☀/☾ force clair ou sombre ; 3ᵉ appui : auto.
@ -659,7 +666,12 @@ function restoreTabAfterDetails() {
hideTileView();
PANEL.tab = PANEL.want = tileReturnTab || 'view';
panelSave();
// Ce rendu RÉTABLIT l'onglet Export tel qu'il était (cadre à sa place,
// vue inchangée) : ni recadrage de la vue, ni recentrage du cadre — cf.
// printSetOpen, qui lit ce drapeau via le listener panel:tab.
RESTORING_TAB = true;
panelRender();
RESTORING_TAB = false;
}
// Ferme la bulle, sinon la fiche ; renvoie true si quelque chose a été fermé.
@ -1162,16 +1174,23 @@ function printCenterHere() {
// Cadre affiché tant que l'onglet Export PDF est visible (événement panel:tab,
// émis à chaque rendu du panneau : on ne fait rien si l'état est inchangé).
// fit : recadrer la vue sur le cadre une fois posé — faux au tout premier
// rendu (onglet Export mémorisé) pour ne pas écraser le zoom d'un lien
// partagé ou de l'URL au chargement.
function printSetOpen(on, fit) {
// explicit : vrai seulement pour une ouverture voulue par l'utilisateur
// (clic sur l'onglet, raccourci clavier) — faux pour un rendu qui RÉTABLIT
// un état déjà connu (démarrage avec onglet mémorisé, retour de la fiche
// détaillée) : dans ce cas ni recadrage de la vue sur le cadre, ni
// recentrage du cadre lui-même sur la vue (sinon un cadre posé ailleurs sur
// le terrain saute au centre de l'écran, ou le zoom d'un lien partagé est
// écrasé — cf. printRefreshFrame(fit)).
function printSetOpen(on, explicit) {
if (PRINT.on === on) return;
PRINT.on = on;
if (on) {
// cadre gardé s'il est encore dans la vue, sinon posé au centre
if (!PRINT.center || !map.getBounds().contains(PRINT.center)) PRINT.center = map.getCenter();
printRefreshFrame(fit !== false);
// Cadre recentré sur la vue seulement s'il est hors champ ET que
// l'ouverture est explicite ; sinon gardé où il était (ou posé au centre
// s'il n'a jamais existé — premier passage sans lidar-print sauvegardé).
if (!PRINT.center) PRINT.center = map.getCenter();
else if (explicit && !map.getBounds().contains(PRINT.center)) PRINT.center = map.getCenter();
printRefreshFrame(!!explicit);
} else {
if (PRINT.rect) PRINT.rect.remove();
if (PRINT.handle) PRINT.handle.remove();
@ -1217,7 +1236,7 @@ el('printTitle').addEventListener('change', printSave);
printLoad();
document.addEventListener('panel:tab', (e) => {
printSetOpen(e.detail.visible === 'print', !BOOTSTRAPPING);
printSetOpen(e.detail.visible === 'print', !BOOTSTRAPPING && !RESTORING_TAB);
if (e.detail.visible !== 'gen' && genDrawing) genSetDrawing(false);
});
@ -1249,8 +1268,14 @@ function panelTabs() {
return ['view', 'print', 'gen', 'share'].filter(t => t !== 'gen' || (GEN.available && GEN.allowed));
}
// Onglet réellement visible, ou null (panneau replié, volet fermé).
// Onglet réellement visible, ou null (panneau replié, volet fermé, ou fiche
// détaillée affichée par-dessus — auquel cas AUCUN onglet n'est à l'écran,
// pas même celui mémorisé : panel:tab doit fermer le cadre d'export s'il
// était ouvert, quel que soit ce qui a déclenché ce rendu du panneau — grip
// glissée/touchée, redimensionnement, changement phoneQuery, le
// setTimeout(panelRender, 220) d'openTab…).
function panelVisibleTab() {
if (!el('tileView').hidden) return null;
if (isPhone() ? PANEL.sheet === 'closed' : PANEL.collapsed) return null;
return PANEL.tab;
}
@ -1383,7 +1408,14 @@ fetch('api/map/meta').then(r => r.json()).then(meta => {
panelRender();
if (show) { genUpdateStatus(d); genPollNow(); }
}).catch(() => {});
}).catch(() => { toast('Serveur de tuiles injoignable.', 10000); panelRender(); });
}).catch(() => {
toast('Serveur de tuiles injoignable.', 10000);
panelRender();
// Ce rendu-là compte aussi comme le premier (échec du chargement initial) :
// sans ce reset, un onglet Export mémorisé ne recadrerait plus JAMAIS la
// vue, même après une ouverture explicite ultérieure.
BOOTSTRAPPING = false;
});
let applyingHash = false;
window.addEventListener('hashchange', () => {