feat(entries): Precedent / Suivant parmi les entrees ouvertes

Fleches dans l'en-tete du panneau et Alt+fleches, comme un navigateur :
un clic par erreur dans une liste filtree ne force plus a refaire la
recherche. Historique en memoire (50), vide au verrouillage ; la navigation
repasse par openSlideOver et sa confirmation de modifications non
enregistrees. Logique pure testee (history.test.js).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-10-10 17:39:24 +01:00
parent 28f8a42846
commit 63e532d5bb
6 changed files with 120 additions and 0 deletions
+54
View File
@@ -733,6 +733,9 @@ const state = {
selectedId: null,
theme: localStorage.getItem('theme') || 'dark',
locked: false, // true after user clicks Lock (token still valid server-side)
// Entrees ouvertes, pour Precedent / Suivant (fleches du panneau, Alt+fleches).
// En memoire seulement, videe au verrouillage.
entryHistory: { ids: [], pos: -1 },
// POURQUOI LE COFFRE S'EST FERME, et depuis quand la page vit. Un
// verrouillage automatique etait muet ; quand le remplissage repondait
// "Vault is locked", rien ne disait si c'etait l'inactivite, une veille de
@@ -1437,6 +1440,7 @@ function lockVault(reason) {
state.cryptoKey = null;
state.entries = [];
state.trashed = [];
state.entryHistory = { ids: [], pos: -1 };
state.locked = true;
state.justRecovered = false;
state.avatarDataUri = ''; // reloaded from server on next unlock
@@ -4025,6 +4029,45 @@ const ENTRY_TEMPLATES = {
// Open the slideover for either an existing entry (id = number) or a
// brand-new one (id = null). opts: { presetTitle, presetSite, template }
// pre-fill the corresponding fields for the new-entry path.
// HISTORIQUE DES ENTREES OUVERTES, comme celui d'un navigateur. Fonctions
// pures sur {ids, pos} : testables sans DOM (tests/history.test.js).
const ENTRY_HISTORY_MAX = 50;
function entryHistoryPush(h, id) {
if (h.ids[h.pos] === id) return; // meme entree : pas de doublon
h.ids = h.ids.slice(0, h.pos + 1); // revenir puis ouvrir ailleurs oublie les suivants
h.ids.push(id);
if (h.ids.length > ENTRY_HISTORY_MAX) h.ids.shift();
h.pos = h.ids.length - 1;
}
// Indice de la prochaine entree qui existe encore dans le sens dir (-1 / +1),
// ou -1. Ne bouge rien : l'ouverture peut etre refusee (modifications non
// enregistrees), la position ne change qu'une fois l'entree vraiment ouverte.
function entryHistoryTarget(h, dir, exists) {
let p = h.pos + dir;
while (p >= 0 && p < h.ids.length && !exists(h.ids[p])) p += dir;
return (p >= 0 && p < h.ids.length) ? p : -1;
}
async function entryHistoryGo(dir) {
const h = state.entryHistory;
const exists = id => state.entries.some(x => x.id === id);
// Panneau ferme : Precedent rouvre d'abord l'entree qu'on vient de quitter.
let p = (dir < 0 && !$('#slideover').classList.contains('is-open') &&
exists(h.ids[h.pos])) ? h.pos : entryHistoryTarget(h, dir, exists);
if (p < 0) return;
if (await openSlideOver(h.ids[p], { fromHistory: true }) === false) return;
h.pos = p;
renderEntryHistoryNav();
}
function renderEntryHistoryNav() {
const h = state.entryHistory;
const exists = id => state.entries.some(x => x.id === id);
const back = $('#slideoverBack'), fwd = $('#slideoverFwd');
if (back) back.disabled = entryHistoryTarget(h, -1, exists) < 0;
if (fwd) fwd.disabled = entryHistoryTarget(h, +1, exists) < 0;
}
async function openSlideOver(id, opts) {
opts = opts || {};
// A blocking operation owns the screen (export, sync, rotation). The busy
@@ -4062,6 +4105,8 @@ async function openSlideOver(id, opts) {
if (!ok) return false; // caller must not act on a panel that didn't switch
}
if (!isNew && !e) return false;
if (!isNew && !opts.fromHistory) entryHistoryPush(state.entryHistory, id);
renderEntryHistoryNav();
// The editor must be visible when it opens: Settings sits ABOVE the
// slideover, so a hotkey (Ctrl+Shift+A) fired with Settings open left
@@ -7593,6 +7638,13 @@ async function init() {
if (mod && /^[uUjJhHsSpPtTnNrR]$/.test(e.key)) return e.preventDefault(); // View source / Downloads / History / Save / Print / New tab+win / Reload
if (mod && e.shiftKey && /^[nNwW]$/.test(e.key)) return e.preventDefault(); // New incognito / Close window
if (mod && e.shiftKey && e.key === 'Delete') return e.preventDefault(); // Clear browsing data
// Alt+fleches : Precedent / Suivant parmi les entrees ouvertes. Le
// preventDefault empeche aussi la page elle-meme de "reculer".
if (e.altKey && !mod && (e.key === 'ArrowLeft' || e.key === 'ArrowRight')) {
e.preventDefault();
if (!state.locked && state.cryptoKey && !document.querySelector('.modal:not(.is-hidden)'))
entryHistoryGo(e.key === 'ArrowLeft' ? -1 : +1);
}
});
document.addEventListener('keydown', handleCardCursorKey);
@@ -7798,6 +7850,8 @@ async function init() {
// Slide-over
$('#slideoverClose').addEventListener('click', () => requestCloseSlideOver());
$('#slideoverBack').addEventListener('click', () => entryHistoryGo(-1));
$('#slideoverFwd').addEventListener('click', () => entryHistoryGo(+1));
// Click outside the slide-over closes it. Clicks on cards re-open it for
// another entry (so we don't close in that case; the card's own handler
// will switch state.selectedId).