Panneau gauche à onglets : "Pages" (gestion complète) et "Mise en page" (bibliothèque)
Build and deploy / test-python (push) Successful in 10m10s
Build and deploy / test-js (push) Successful in 51s
Build and deploy / lint-python (push) Successful in 4m44s
Build and deploy / lint-js (push) Failing after 1m27s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Build and deploy / sonarqube (push) Failing after 4m8s
Build and deploy / test-python (push) Successful in 10m10s
Build and deploy / test-js (push) Successful in 51s
Build and deploy / lint-python (push) Successful in 4m44s
Build and deploy / lint-js (push) Failing after 1m27s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Build and deploy / sonarqube (push) Failing after 4m8s
Remplace le carrousel une-page-à-la-fois par un panneau dédié plein hauteur : liste verticale scrollable de toutes les pages, réordonnage par glisser OU boutons haut/bas (accessibilité clavier), renommer (crayon, édition en ligne), supprimer (protégé contre la suppression de la dernière page), ajouter. La bibliothèque d'éléments passe dans un second onglet "Mise en page", contenu inchangé. "Mise en page" actif par défaut au chargement. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
14ca9e32a8
commit
b4e4e80b7c
+1
-1
@@ -152,7 +152,7 @@ SonarQube : voir section 2, sous-section "SonarQube" — CI restaurée (non-bloq
|
|||||||
| `static/game/js/play/offline/filter-repeater-rows.js:24` (`forgeDecodeClauses`) | `javascript:S2486` | **Corrigé, même patron que ci-dessus** : `_filtres_json` est un attribut rendu par le serveur, jamais tapé à la main — un JSON invalide y trahit presque toujours un bug côté éditeur/serveur. `console.warn('_filtres_json invalide, filtre ignoré', e)` ajouté, comportement inchangé (repli sur l'ancien format à 2 filtres fixes ou aucun filtre). Couvert par un nouveau test (`filter-repeater-rows.test.js`, `forgeDecodeClauses — _filtres_json invalide`). | Lot 4 "modernisation JS", 18/09/2026 |
|
| `static/game/js/play/offline/filter-repeater-rows.js:24` (`forgeDecodeClauses`) | `javascript:S2486` | **Corrigé, même patron que ci-dessus** : `_filtres_json` est un attribut rendu par le serveur, jamais tapé à la main — un JSON invalide y trahit presque toujours un bug côté éditeur/serveur. `console.warn('_filtres_json invalide, filtre ignoré', e)` ajouté, comportement inchangé (repli sur l'ancien format à 2 filtres fixes ou aucun filtre). Couvert par un nouveau test (`filter-repeater-rows.test.js`, `forgeDecodeClauses — _filtres_json invalide`). | Lot 4 "modernisation JS", 18/09/2026 |
|
||||||
| `static/game/js/play/offline/filter-repeater-rows.js:59`, `:70` (`forgeResolveVariablePath`, JSON.parse + navigation `.champ`/`[index]`) | `javascript:S2486` | **Documenté, pas corrigé — nature différente du cas ci-dessus** : ici `rawValue` est la VALEUR ACTUELLE d'une variable de jeu (modifiable librement par n'importe quelle action "Modifier une variable"), pas une config interne à l'éditeur — un chemin qui ne correspond pas à sa forme actuelle est un cas normal et attendu (ex. variable encore à sa valeur par défaut non-JSON), déjà explicitement documenté par le commentaire de la fonction ("Ne lève jamais... même convention que côté serveur"). Un `console.warn` ici bruiterait la console à chaque usage légitime. | Lot 4 "modernisation JS", 18/09/2026 |
|
| `static/game/js/play/offline/filter-repeater-rows.js:59`, `:70` (`forgeResolveVariablePath`, JSON.parse + navigation `.champ`/`[index]`) | `javascript:S2486` | **Documenté, pas corrigé — nature différente du cas ci-dessus** : ici `rawValue` est la VALEUR ACTUELLE d'une variable de jeu (modifiable librement par n'importe quelle action "Modifier une variable"), pas une config interne à l'éditeur — un chemin qui ne correspond pas à sa forme actuelle est un cas normal et attendu (ex. variable encore à sa valeur par défaut non-JSON), déjà explicitement documenté par le commentaire de la fonction ("Ne lève jamais... même convention que côté serveur"). Un `console.warn` ici bruiterait la console à chaque usage légitime. | Lot 4 "modernisation JS", 18/09/2026 |
|
||||||
| `static/document/js/document-editor.js` (`FORGE_DOC_STYLE_PRESETS`, `FORGE_DOC_SHAPE_KINDS`, `FORGE_DOC_SNAP_SIZE` — 3 sites) | `eslint:no-var`, `eslint:vars-on-top` | Constantes de premier niveau déclarées en `var` plutôt que `const` : un `<script src>` de page est rejoué TEL QUEL par `pjax.js` (`runScriptsIn`) à chaque navigation interne — une redéclaration `let`/`const` au premier niveau lèverait `SyntaxError: already declared` à la 2e exécution (voir l'en-tête de `static/pjax.js`, et le commentaire d'en-tête de ce fichier). `var` est le seul mot-clé sûr à ce niveau ; tout le reste du fichier (état mutable, y compris à l'intérieur des fonctions) est bien en `let`/`const`, porté par `window.forgeDocState` plutôt que par des variables de module (même convention que `static/game/js/scenes/scene-editor.js` et les autres scripts de page existants, qui n'ont eux aucune constante de ce genre à déclarer). | Session du 20/09/2026 |
|
| `static/document/js/document-editor.js` (`FORGE_DOC_STYLE_PRESETS`, `FORGE_DOC_SHAPE_KINDS`, `FORGE_DOC_SNAP_SIZE` — 3 sites) | `eslint:no-var`, `eslint:vars-on-top` | Constantes de premier niveau déclarées en `var` plutôt que `const` : un `<script src>` de page est rejoué TEL QUEL par `pjax.js` (`runScriptsIn`) à chaque navigation interne — une redéclaration `let`/`const` au premier niveau lèverait `SyntaxError: already declared` à la 2e exécution (voir l'en-tête de `static/pjax.js`, et le commentaire d'en-tête de ce fichier). `var` est le seul mot-clé sûr à ce niveau ; tout le reste du fichier (état mutable, y compris à l'intérieur des fonctions) est bien en `let`/`const`, porté par `window.forgeDocState` plutôt que par des variables de module (même convention que `static/game/js/scenes/scene-editor.js` et les autres scripts de page existants, qui n'ont eux aucune constante de ce genre à déclarer). | Session du 20/09/2026 |
|
||||||
| `templates/document/document_edit.html` (`.docPageListItem`, rangées de la section "Pages" du panneau gauche) + `static/document/js/document-editor.js` (`forgeDocRenderPageList`) | `Web:S6819`, `Web:MouseEventWithoutKeyboardEquivalentCheck` (Sonar) | `div role="button" tabindex="0"` volontaire : chaque rangée contient de vrais `<button>` d'action (monter/descendre/renommer/supprimer, voir `.docPageListActions`), qu'un `<button>` englobant ne pourrait pas contenir validement (imbrication de `<button>` invalide, le parseur HTML referme le bouton englobant trop tôt — même défaut structurel déjà rencontré et corrigé de la même façon ailleurs dans ce fichier). L'équivalent clavier (Entrée/Espace déclenche `forgeDocSwitchPage`, même effet que le clic) est posé côté JS (`item.addEventListener('keydown', ...)`), donc le finding clavier de Sonar est un faux positif : l'analyseur statique ne voit pas les `addEventListener` attachés dynamiquement. Vérifié par un test jsdom dédié (rôle `button`, équivalent clavier fonctionnel). | Session du 21/09/2026 |
|
| `templates/document/document_edit.html` (`.docPageRow`, rangées de l'onglet "Pages" du panneau gauche) + `static/document/js/document-editor.js` (`forgeDocRenderPageManagerList`) | `Web:S6819`, `Web:MouseEventWithoutKeyboardEquivalentCheck` (Sonar) | `div role="button" tabindex="0"` volontaire : chaque rangée contient de vrais `<button>` d'action (monter/descendre/renommer/supprimer, voir `.docPageRowActions`), qu'un `<button>` englobant ne pourrait pas contenir validement (imbrication de `<button>` invalide, le parseur HTML referme le bouton englobant trop tôt — même défaut structurel déjà rencontré et corrigé de la même façon ailleurs dans ce fichier). L'équivalent clavier (Entrée/Espace déclenche `forgeDocSwitchPage`, même effet que le clic) est posé côté JS (`row.addEventListener('keydown', ...)`), donc le finding clavier de Sonar est un faux positif : l'analyseur statique ne voit pas les `addEventListener` attachés dynamiquement. Vérifié par un test jsdom dédié (rôle `button`, équivalent clavier fonctionnel). | Session du 21/09/2026 ; renommé (panneau à onglets) le 23/09/2026 |
|
||||||
|
|
||||||
### Détail — `javascript:S8786` (ReDoS), lot 1 "modernisation JS"
|
### Détail — `javascript:S8786` (ReDoS), lot 1 "modernisation JS"
|
||||||
|
|
||||||
|
|||||||
+123
-120
@@ -1162,183 +1162,186 @@ img.docImage {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- Section "Pages" du panneau GAUCHE (retour utilisateur du
|
/* ---- Panneau GAUCHE à ONGLETS (retour utilisateur du 23/09/2026 :
|
||||||
21/09/2026 : "une section qui s'ajoute dans le panneau de gauche pour
|
"il dois avoir deux onglets [...] un onglet page [...] un onglet mise
|
||||||
ajouter une page et naviguer entre elles", à la place de l'ex-
|
en page avec les fonctionnalité et la disposition actuel sans la
|
||||||
catégorie "Mise en page"/formes libres, supprimée) — un CARROUSEL
|
section page") : "Pages" (gestion complète, panneau dédié plein
|
||||||
une-page-à-la-fois (retour utilisateur du 22/09/2026 : "un seul carré
|
hauteur) et "Mise en page" (bibliothèque d'éléments, inchangée). ---- */
|
||||||
page visible, les flèches ne servent pas qu'à naviguer mais aussi à
|
.docSidebarTabs {
|
||||||
changer de page") : un seul carré = la page ACTIVE, hauteur fixe quel
|
|
||||||
que soit le nombre de pages. Carte texte (pas de vignette visuelle). ---- */
|
|
||||||
.docPageSection {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ‹/› CHANGENT la page affichée (voir forgeDocBindPageStripNav) — à ne
|
|
||||||
pas confondre avec les petites flèches ←/→ DANS la carte, qui elles
|
|
||||||
réordonnent la page active parmi ses sœurs sans changer l'affichage. */
|
|
||||||
.docPageStrip {
|
|
||||||
display: flex;
|
|
||||||
align-items: stretch;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.docPageStripNav {
|
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
width: 22px;
|
border-bottom: 1px solid var(--doc-border);
|
||||||
border: 1px solid var(--doc-border);
|
margin: -20px -16px 0;
|
||||||
border-radius: 6px;
|
padding: 0 8px;
|
||||||
background: var(--doc-card);
|
|
||||||
color: var(--doc-muted);
|
|
||||||
font-size: 15px;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.docPageStripNav:hover:not(:disabled) {
|
.docSidebarTab {
|
||||||
border-color: var(--doc-accent);
|
flex: 1;
|
||||||
|
padding: 10px 8px;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
background: none;
|
||||||
|
color: var(--doc-muted);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docSidebarTab:hover {
|
||||||
color: var(--doc-text);
|
color: var(--doc-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.docPageStripNav:disabled {
|
.docSidebarTab.is-active {
|
||||||
opacity: 0.3;
|
color: var(--doc-accent);
|
||||||
cursor: default;
|
border-bottom-color: var(--doc-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.docPageList {
|
.docSidebarTabPanel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 22px;
|
||||||
|
min-height: 0;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
}
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
#docTabPanelPages {
|
||||||
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.docPageListItem {
|
.docPageManagerList {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
width: 100%;
|
overflow-y: auto;
|
||||||
padding: 6px;
|
}
|
||||||
|
|
||||||
|
.docPageRow {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 7px 8px;
|
||||||
border: 1px solid var(--doc-border);
|
border: 1px solid var(--doc-border);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: var(--doc-card);
|
background: var(--doc-card);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.docPageListItem:hover {
|
.docPageRow.is-dragging {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docPageRow:hover {
|
||||||
border-color: var(--doc-accent);
|
border-color: var(--doc-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.docPageListItem.is-active {
|
.docPageRow.is-active {
|
||||||
border-color: var(--doc-accent);
|
border-color: var(--doc-accent);
|
||||||
box-shadow: 0 0 0 1px var(--doc-accent);
|
box-shadow: 0 0 0 1px var(--doc-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.docPageListItem:focus-visible {
|
.docPageRow:focus-visible {
|
||||||
outline: 2px solid var(--doc-accent);
|
outline: 2px solid var(--doc-accent);
|
||||||
outline-offset: 1px;
|
outline-offset: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.docPageListActions {
|
.docPageRowHandle {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--doc-muted);
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docPageRowNumber {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: var(--doc-bg-2);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 2px;
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--doc-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.docPageListActions button {
|
.docPageRow.is-active .docPageRowNumber {
|
||||||
width: 16px;
|
color: var(--doc-accent);
|
||||||
height: 16px;
|
}
|
||||||
|
|
||||||
|
/* Titre tronqué, qui défile au survol si tronqué (retour utilisateur du
|
||||||
|
21/09/2026) — .has-overflow n'est posé que si le texte dépasse
|
||||||
|
réellement (voir forgeDocApplyTitleMarquee), jamais un défilement
|
||||||
|
systématique qui ferait sauter les titres courts. */
|
||||||
|
.docPageRowTitle {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docPageRowTitleText {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 12.5px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--doc-muted);
|
||||||
|
transition: transform 0.6s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docPageRow.is-active .docPageRowTitleText {
|
||||||
|
color: var(--doc-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.docPageRowTitle.has-overflow:hover .docPageRowTitleText {
|
||||||
|
transform: translateX(calc(-1 * var(--marquee-distance, 0px)));
|
||||||
|
}
|
||||||
|
|
||||||
|
.docPageRowTitle[contenteditable="true"] {
|
||||||
|
outline: none;
|
||||||
|
cursor: text;
|
||||||
|
user-select: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docPageRowTitle[contenteditable="true"] .docPageRowTitleText {
|
||||||
|
border-bottom: 1px solid var(--doc-accent);
|
||||||
|
color: var(--doc-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.docPageRowActions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docPageRowActions button {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 5px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--doc-muted);
|
color: var(--doc-muted);
|
||||||
font-size: 9px;
|
font-size: 11px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.docPageListActions button:hover:not(:disabled) {
|
.docPageRowActions button:hover:not(:disabled) {
|
||||||
background: var(--doc-bg-2);
|
background: var(--doc-bg-2);
|
||||||
color: var(--doc-text);
|
color: var(--doc-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.docPageListActions button:disabled {
|
.docPageRowActions button:disabled {
|
||||||
opacity: 0.3;
|
opacity: 0.3;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
.docPageListDelete:hover:not(:disabled) {
|
.docPageRowDelete:hover:not(:disabled) {
|
||||||
color: var(--doc-quiz-danger-border);
|
color: var(--doc-quiz-danger-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.docPageListSquare {
|
|
||||||
width: 100%;
|
|
||||||
aspect-ratio: 1;
|
|
||||||
border-radius: 6px;
|
|
||||||
background: var(--doc-bg-2);
|
|
||||||
border: 1px solid var(--doc-border);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--doc-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.docPageListItem.is-active .docPageListSquare {
|
|
||||||
border-color: var(--doc-accent);
|
|
||||||
color: var(--doc-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Titre SOUS le carré, tronqué, qui défile au survol si tronqué (retour
|
|
||||||
utilisateur du 21/09/2026) — .has-overflow n'est posé que si le texte
|
|
||||||
dépasse réellement (voir forgeDocApplyTitleMarquee), jamais un
|
|
||||||
défilement systématique qui ferait sauter les titres courts. */
|
|
||||||
.docPageListTitle {
|
|
||||||
width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.docPageListTitleText {
|
|
||||||
display: inline-block;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--doc-muted);
|
|
||||||
transition: transform 0.6s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.docPageListItem.is-active .docPageListTitleText {
|
|
||||||
color: var(--doc-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.docPageListTitle.has-overflow:hover .docPageListTitleText {
|
|
||||||
transform: translateX(calc(-1 * var(--marquee-distance, 0px)));
|
|
||||||
}
|
|
||||||
|
|
||||||
.docPageListTitle[contenteditable="true"] {
|
|
||||||
outline: none;
|
|
||||||
cursor: text;
|
|
||||||
user-select: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
.docPageListTitle[contenteditable="true"] .docPageListTitleText {
|
|
||||||
border-bottom: 1px solid var(--doc-accent);
|
|
||||||
color: var(--doc-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.docPageListCounter {
|
|
||||||
font-size: 10px;
|
|
||||||
color: var(--doc-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---- RIGHT SIDEBAR : Propriétés ---- */
|
/* ---- RIGHT SIDEBAR : Propriétés ---- */
|
||||||
.docField {
|
.docField {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -403,16 +403,18 @@ function forgeDocBindCanvasDropZone() {
|
|||||||
/* ---------------------------------------------------------------------
|
/* ---------------------------------------------------------------------
|
||||||
* Pages — un support est composé de plusieurs pages (retour utilisateur
|
* Pages — un support est composé de plusieurs pages (retour utilisateur
|
||||||
* du 21/09/2026 : "il faut implémenter un système de page"), chacune un
|
* du 21/09/2026 : "il faut implémenter un système de page"), chacune un
|
||||||
* document indépendant affiché SEUL sur le canevas à la fois. Section
|
* document indépendant affiché SEUL sur le canevas à la fois. Onglet
|
||||||
* dédiée dans le panneau GAUCHE (retour utilisateur du 21/09/2026 :
|
* "Pages" dédié dans le panneau GAUCHE (retour utilisateur du
|
||||||
* "une section qui s'ajoute dans le panneau de gauche pour ajouter une
|
* 23/09/2026 : "il dois avoir deux onglets [...] un onglet page qui
|
||||||
* page et naviguer entre elles", à la place de l'ex-catégorie "Mise en
|
* liste toute la page, permet de changer l'ordre des pages, d'ajouter,
|
||||||
* page"/formes libres, supprimée) — une simple liste, jamais de vignette
|
* supprimer ou modifier le nom") — panneau plein-écran (jamais partagé
|
||||||
* visuelle (voir templates/document/document_edit.html, .docPageList) :
|
* avec la bibliothèque comme les itérations précédentes), liste
|
||||||
* créer/renommer (bouton crayon)/réordonner (boutons haut/bas)/supprimer.
|
* verticale de rangées texte (voir templates/document/document_edit.html,
|
||||||
* Changer de page VIDE la pile Annuler/Rétablir (voir forgeDocSwitchPage)
|
* .docPageManagerList) : créer/renommer (bouton crayon)/réordonner
|
||||||
* : une commande empilée sur une autre page rejouerait un appel AJAX
|
* (glisser OU boutons haut/bas)/supprimer. Changer de page VIDE la pile
|
||||||
* ciblant des éléments qui ne sont plus affichés, aucun sens à la garder.
|
* Annuler/Rétablir (voir forgeDocSwitchPage) : une commande empilée sur
|
||||||
|
* une autre page rejouerait un appel AJAX ciblant des éléments qui ne
|
||||||
|
* sont plus affichés, aucun sens à la garder.
|
||||||
* ------------------------------------------------------------------- */
|
* ------------------------------------------------------------------- */
|
||||||
|
|
||||||
function forgeDocApiPageAdd(title) {
|
function forgeDocApiPageAdd(title) {
|
||||||
@@ -469,7 +471,7 @@ function forgeDocSwitchPage(pageId) {
|
|||||||
forgeDocUpdateHistoryButtons();
|
forgeDocUpdateHistoryButtons();
|
||||||
window.forgeDocState.selectedId = null;
|
window.forgeDocState.selectedId = null;
|
||||||
forgeDocRenderProps(null);
|
forgeDocRenderProps(null);
|
||||||
forgeDocRenderPageList();
|
forgeDocRenderPageManagerList();
|
||||||
forgeDocUpdatePageNavPill();
|
forgeDocUpdatePageNavPill();
|
||||||
return forgeDocRefreshCanvas();
|
return forgeDocRefreshCanvas();
|
||||||
}
|
}
|
||||||
@@ -487,7 +489,7 @@ function forgeDocRenamePage(pageId, title) {
|
|||||||
return forgeDocApiPageRename(pageId, title).then((result) => {
|
return forgeDocApiPageRename(pageId, title).then((result) => {
|
||||||
const page = window.forgeDocState.pages.find((p) => p.id === pageId);
|
const page = window.forgeDocState.pages.find((p) => p.id === pageId);
|
||||||
if (page) page.title = result.title;
|
if (page) page.title = result.title;
|
||||||
forgeDocRenderPageList();
|
forgeDocRenderPageManagerList();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -505,7 +507,7 @@ function forgeDocDeletePage(pageId) {
|
|||||||
if (window.forgeDocState.activePageId === pageId) {
|
if (window.forgeDocState.activePageId === pageId) {
|
||||||
return forgeDocSwitchPage(forgeDocOrderedPages()[0].id);
|
return forgeDocSwitchPage(forgeDocOrderedPages()[0].id);
|
||||||
}
|
}
|
||||||
forgeDocRenderPageList();
|
forgeDocRenderPageManagerList();
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -523,7 +525,7 @@ function forgeDocMovePage(pageId, direction) {
|
|||||||
rest.splice(newIndex, 0, moved);
|
rest.splice(newIndex, 0, moved);
|
||||||
rest.forEach((p, i) => { p.order_index = i; });
|
rest.forEach((p, i) => { p.order_index = i; });
|
||||||
window.forgeDocState.pages = rest;
|
window.forgeDocState.pages = rest;
|
||||||
forgeDocRenderPageList();
|
forgeDocRenderPageManagerList();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -536,9 +538,9 @@ function forgeDocSelectAllText(el) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function forgeDocStartRenamingPage(pageId) {
|
function forgeDocStartRenamingPage(pageId) {
|
||||||
const item = document.querySelector(`.docPageListItem[data-page-id="${pageId}"]`);
|
const item = document.querySelector(`.docPageRow[data-page-id="${pageId}"]`);
|
||||||
if (!item) return;
|
if (!item) return;
|
||||||
const titleEl = item.querySelector('.docPageListTitle');
|
const titleEl = item.querySelector('.docPageRowTitle');
|
||||||
const original = titleEl.textContent;
|
const original = titleEl.textContent;
|
||||||
titleEl.setAttribute('contenteditable', 'true');
|
titleEl.setAttribute('contenteditable', 'true');
|
||||||
titleEl.focus();
|
titleEl.focus();
|
||||||
@@ -580,7 +582,7 @@ function forgeDocApplyTitleMarquee(titleEl) {
|
|||||||
// seuls les titres RÉELLEMENT tronqués défilent au survol — mesuré ici
|
// seuls les titres RÉELLEMENT tronqués défilent au survol — mesuré ici
|
||||||
// (scrollWidth vs clientWidth), jamais un défilement systématique qui
|
// (scrollWidth vs clientWidth), jamais un défilement systématique qui
|
||||||
// ferait sauter les titres courts sans raison.
|
// ferait sauter les titres courts sans raison.
|
||||||
const textEl = titleEl.querySelector('.docPageListTitleText');
|
const textEl = titleEl.querySelector('.docPageRowTitleText');
|
||||||
const overflow = textEl.scrollWidth - titleEl.clientWidth;
|
const overflow = textEl.scrollWidth - titleEl.clientWidth;
|
||||||
if (overflow > 0) {
|
if (overflow > 0) {
|
||||||
titleEl.classList.add('has-overflow');
|
titleEl.classList.add('has-overflow');
|
||||||
@@ -588,92 +590,130 @@ function forgeDocApplyTitleMarquee(titleEl) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function forgeDocRenderPageList() {
|
function forgeDocRenderPageManagerList() {
|
||||||
const container = document.getElementById('docPageList');
|
const container = document.getElementById('docPageManagerList');
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
const pages = forgeDocOrderedPages();
|
const pages = forgeDocOrderedPages();
|
||||||
const index = pages.findIndex((p) => p.id === window.forgeDocState.activePageId);
|
// div + role="button" ici, jamais <button> : la rangée contient de
|
||||||
const p = pages[index];
|
// vrais <button> d'action (monter/descendre/renommer/supprimer), qu'un
|
||||||
// Un seul carré visible : la page ACTIVE (retour utilisateur du
|
// <button> englobant ne pourrait pas contenir validement. L'équivalent
|
||||||
// 22/09/2026 : "un seul carré page visible") — les grandes flèches
|
// clavier (Entrée/Espace) est posé plus bas. Réordonnancement : glisser
|
||||||
// ‹/› de .docPageStrip CHANGENT la page affichée (voir
|
// (souris, voir forgeDocBindPageManagerDrag) ET boutons ↑/↓ (clavier),
|
||||||
// forgeDocBindPageStripNav), distinctes des petites flèches ←/→
|
// les deux appellent le même forgeDocMovePage.
|
||||||
// ci-dessous qui réordonnent la page active parmi ses sœurs sans
|
container.innerHTML = pages.map((p, index) => `
|
||||||
// changer l'affichage. div + role="button" ici, jamais <button> : la
|
<div class="docPageRow ${p.id === window.forgeDocState.activePageId ? 'is-active' : ''}"
|
||||||
// carte contient de vrais <button> d'action, qu'un <button> englobant
|
role="button"
|
||||||
// ne pourrait pas contenir validement. L'équivalent clavier
|
tabindex="0"
|
||||||
// (Entrée/Espace) est posé plus bas.
|
draggable="true"
|
||||||
container.innerHTML = `
|
data-page-id="${p.id}">
|
||||||
<div class="docPageListItem is-active" role="button" tabindex="0" data-page-id="${p.id}">
|
<span class="docPageRowHandle" aria-hidden="true">⠿</span>
|
||||||
<div class="docPageListActions">
|
<span class="docPageRowNumber">${index + 1}</span>
|
||||||
<button type="button" class="docPageListMove" data-dir="up" data-page-id="${p.id}" aria-label="Réordonner vers la gauche" title="Réordonner vers la gauche"${index === 0 ? ' disabled' : ''}>←</button>
|
<span class="docPageRowTitle"><span class="docPageRowTitleText">${forgeDocEscapeHtml(p.title)}</span></span>
|
||||||
<button type="button" class="docPageListMove" data-dir="down" data-page-id="${p.id}" aria-label="Réordonner vers la droite" title="Réordonner vers la droite"${index === pages.length - 1 ? ' disabled' : ''}>→</button>
|
<span class="docPageRowActions">
|
||||||
<button type="button" class="docPageListRename" data-page-id="${p.id}" aria-label="Renommer cette page" title="Renommer cette page">✎</button>
|
<button type="button" class="docPageRowMove" data-dir="up" data-page-id="${p.id}" aria-label="Monter" title="Monter"${index === 0 ? ' disabled' : ''}>↑</button>
|
||||||
<button type="button" class="docPageListDelete" data-page-id="${p.id}" aria-label="Supprimer cette page" title="Supprimer cette page"${pages.length <= 1 ? ' disabled' : ''}>✕</button>
|
<button type="button" class="docPageRowMove" data-dir="down" data-page-id="${p.id}" aria-label="Descendre" title="Descendre"${index === pages.length - 1 ? ' disabled' : ''}>↓</button>
|
||||||
</div>
|
<button type="button" class="docPageRowRename" data-page-id="${p.id}" aria-label="Renommer cette page" title="Renommer cette page">✎</button>
|
||||||
<div class="docPageListSquare">${index + 1}</div>
|
<button type="button" class="docPageRowDelete" data-page-id="${p.id}" aria-label="Supprimer cette page" title="Supprimer cette page"${pages.length <= 1 ? ' disabled' : ''}>✕</button>
|
||||||
<span class="docPageListTitle"><span class="docPageListTitleText">${forgeDocEscapeHtml(p.title)}</span></span>
|
</span>
|
||||||
<span class="docPageListCounter">${index + 1} / ${pages.length}</span>
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`).join('');
|
||||||
|
|
||||||
const item = container.querySelector('.docPageListItem[data-page-id]');
|
container.querySelectorAll('.docPageRow[data-page-id]').forEach((row) => {
|
||||||
item.addEventListener('click', (e) => {
|
row.addEventListener('click', (e) => {
|
||||||
if (e.target.closest('.docPageListActions')) return;
|
if (e.target.closest('.docPageRowActions')) return;
|
||||||
forgeDocSwitchPage(Number(item.dataset.pageId));
|
forgeDocSwitchPage(Number(row.dataset.pageId));
|
||||||
|
});
|
||||||
|
row.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key !== 'Enter' && e.key !== ' ') return;
|
||||||
|
e.preventDefault();
|
||||||
|
forgeDocSwitchPage(Number(row.dataset.pageId));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
item.addEventListener('keydown', (e) => {
|
container.querySelectorAll('.docPageRowMove').forEach((btn) => {
|
||||||
if (e.key !== 'Enter' && e.key !== ' ') return;
|
|
||||||
e.preventDefault();
|
|
||||||
forgeDocSwitchPage(Number(item.dataset.pageId));
|
|
||||||
});
|
|
||||||
container.querySelectorAll('.docPageListMove').forEach((btn) => {
|
|
||||||
btn.addEventListener('click', (e) => {
|
btn.addEventListener('click', (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
forgeDocMovePage(Number(btn.dataset.pageId), btn.dataset.dir);
|
forgeDocMovePage(Number(btn.dataset.pageId), btn.dataset.dir);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
container.querySelectorAll('.docPageListRename').forEach((btn) => {
|
container.querySelectorAll('.docPageRowRename').forEach((btn) => {
|
||||||
btn.addEventListener('click', (e) => {
|
btn.addEventListener('click', (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
forgeDocStartRenamingPage(Number(btn.dataset.pageId));
|
forgeDocStartRenamingPage(Number(btn.dataset.pageId));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
container.querySelectorAll('.docPageListDelete').forEach((btn) => {
|
container.querySelectorAll('.docPageRowDelete').forEach((btn) => {
|
||||||
btn.addEventListener('click', (e) => {
|
btn.addEventListener('click', (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
forgeDocDeletePage(Number(btn.dataset.pageId));
|
forgeDocDeletePage(Number(btn.dataset.pageId));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
container.querySelectorAll('.docPageListTitle').forEach(forgeDocApplyTitleMarquee);
|
container.querySelectorAll('.docPageRowTitle').forEach(forgeDocApplyTitleMarquee);
|
||||||
|
|
||||||
const prevBtn = document.getElementById('docPageStripPrev');
|
const addBtn = document.getElementById('docPageManagerAdd');
|
||||||
const nextBtn = document.getElementById('docPageStripNext');
|
|
||||||
if (prevBtn) prevBtn.disabled = index <= 0;
|
|
||||||
if (nextBtn) nextBtn.disabled = index >= pages.length - 1;
|
|
||||||
|
|
||||||
const addBtn = document.getElementById('docPageListAdd');
|
|
||||||
if (addBtn) addBtn.onclick = () => forgeDocAddPage();
|
if (addBtn) addBtn.onclick = () => forgeDocAddPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
function forgeDocBindPageStripNav() {
|
function forgeDocBindPageManagerDrag() {
|
||||||
const prevBtn = document.getElementById('docPageStripPrev');
|
// Délégation sur le CONTENEUR (jamais les rangées elles-mêmes,
|
||||||
const nextBtn = document.getElementById('docPageStripNext');
|
// recréées à chaque forgeDocRenderPageManagerList) : un seul jeu de
|
||||||
if (!prevBtn || !nextBtn) return;
|
// listeners pour toute la session, même principe que
|
||||||
// Les flèches ‹/› CHANGENT la page affichée (retour utilisateur du
|
// forgeDocMotsBindGlobalMouseup.
|
||||||
// 22/09/2026 : "les flèches ne servent pas qu'à naviguer mais aussi à
|
const container = document.getElementById('docPageManagerList');
|
||||||
// changer de page") — lecture dynamique de l'ordre courant à chaque
|
if (!container) return;
|
||||||
// clic (même principe que forgeDocBindPageNavPill), jamais un index
|
let draggedId = null;
|
||||||
// figé au moment du binding.
|
|
||||||
prevBtn.addEventListener('click', () => {
|
container.addEventListener('dragstart', (e) => {
|
||||||
const pages = forgeDocOrderedPages();
|
const row = e.target.closest('.docPageRow');
|
||||||
const index = pages.findIndex((p) => p.id === window.forgeDocState.activePageId);
|
if (!row || !row.dataset.pageId) return;
|
||||||
if (index > 0) forgeDocSwitchPage(pages[index - 1].id);
|
draggedId = Number(row.dataset.pageId);
|
||||||
|
row.classList.add('is-dragging');
|
||||||
|
e.dataTransfer.effectAllowed = 'move';
|
||||||
});
|
});
|
||||||
nextBtn.addEventListener('click', () => {
|
container.addEventListener('dragend', (e) => {
|
||||||
const pages = forgeDocOrderedPages();
|
const row = e.target.closest('.docPageRow');
|
||||||
const index = pages.findIndex((p) => p.id === window.forgeDocState.activePageId);
|
if (row) row.classList.remove('is-dragging');
|
||||||
if (index < pages.length - 1) forgeDocSwitchPage(pages[index + 1].id);
|
});
|
||||||
|
container.addEventListener('dragover', (e) => {
|
||||||
|
if (draggedId !== null) e.preventDefault();
|
||||||
|
});
|
||||||
|
container.addEventListener('drop', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (draggedId === null) return;
|
||||||
|
const targetRow = e.target.closest('.docPageRow[data-page-id]');
|
||||||
|
const ordered = forgeDocOrderedPages();
|
||||||
|
const newIndex = targetRow
|
||||||
|
? ordered.findIndex((p) => p.id === Number(targetRow.dataset.pageId))
|
||||||
|
: ordered.length - 1;
|
||||||
|
const movedId = draggedId;
|
||||||
|
draggedId = null;
|
||||||
|
if (newIndex < 0) return;
|
||||||
|
forgeDocApiPageMove(movedId, newIndex).then(() => {
|
||||||
|
const moved = window.forgeDocState.pages.find((p) => p.id === movedId);
|
||||||
|
const rest = window.forgeDocState.pages
|
||||||
|
.filter((p) => p.id !== movedId)
|
||||||
|
.sort((a, b) => a.order_index - b.order_index);
|
||||||
|
rest.splice(newIndex, 0, moved);
|
||||||
|
rest.forEach((p, i) => { p.order_index = i; });
|
||||||
|
window.forgeDocState.pages = rest;
|
||||||
|
forgeDocRenderPageManagerList();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function forgeDocSwitchSidebarTab(tabName) {
|
||||||
|
const tabs = { pages: document.getElementById('docTabPages'), layout: document.getElementById('docTabLayout') };
|
||||||
|
const panels = { pages: document.getElementById('docTabPanelPages'), layout: document.getElementById('docTabPanelLayout') };
|
||||||
|
Object.keys(tabs).forEach((name) => {
|
||||||
|
const isActive = name === tabName;
|
||||||
|
tabs[name].classList.toggle('is-active', isActive);
|
||||||
|
tabs[name].setAttribute('aria-selected', String(isActive));
|
||||||
|
panels[name].hidden = !isActive;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function forgeDocBindSidebarTabs() {
|
||||||
|
document.querySelectorAll('.docSidebarTab').forEach((tab) => {
|
||||||
|
tab.addEventListener('click', () => forgeDocSwitchSidebarTab(tab.dataset.tab));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2800,8 +2840,9 @@ function forgeDocInit() {
|
|||||||
forgeDocBindMobileNav();
|
forgeDocBindMobileNav();
|
||||||
forgeDocBindKeyboardShortcuts();
|
forgeDocBindKeyboardShortcuts();
|
||||||
forgeDocBindScenarioTreeModal();
|
forgeDocBindScenarioTreeModal();
|
||||||
forgeDocRenderPageList();
|
forgeDocBindSidebarTabs();
|
||||||
forgeDocBindPageStripNav();
|
forgeDocRenderPageManagerList();
|
||||||
|
forgeDocBindPageManagerDrag();
|
||||||
forgeDocBindPageNavPill();
|
forgeDocBindPageNavPill();
|
||||||
forgeDocUpdateHistoryButtons();
|
forgeDocUpdateHistoryButtons();
|
||||||
|
|
||||||
|
|||||||
@@ -95,82 +95,99 @@
|
|||||||
<div class="docBodyWrap">
|
<div class="docBodyWrap">
|
||||||
<div class="docSidebarClose" id="docCloseLeft">← Retour au document</div>
|
<div class="docSidebarClose" id="docCloseLeft">← Retour au document</div>
|
||||||
<div class="docSidebar docSidebarLeft" id="docSidebarLeft">
|
<div class="docSidebar docSidebarLeft" id="docSidebarLeft">
|
||||||
<div class="docPageSection">
|
{# Panneau gauche à onglets (retour utilisateur du 23/09/2026 :
|
||||||
<div class="docSectionLabel">Pages</div>
|
"il dois avoir deux onglets") : "Mise en page" (bibliothèque
|
||||||
<div class="docPageStrip">
|
d'éléments, contenu inchangé) actif par défaut — le panneau
|
||||||
<button type="button"
|
"Pages" (gestion complète des pages : liste/réordonner/ajouter/
|
||||||
class="docPageStripNav"
|
supprimer/renommer) est un espace dédié plein-écran plutôt que
|
||||||
id="docPageStripPrev"
|
de se partager la hauteur avec la bibliothèque comme les
|
||||||
aria-label="Page précédente"
|
itérations précédentes (bande de vignettes, puis carrousel). #}
|
||||||
title="Page précédente">‹</button>
|
<div class="docSidebarTabs" role="tablist">
|
||||||
<div class="docPageList" id="docPageList">
|
<button type="button"
|
||||||
{# active_page est toujours pages[0] au chargement initial (voir
|
class="docSidebarTab"
|
||||||
routes/document/document_edit.py) — index 0 fixe, jamais recalculé
|
id="docTabPages"
|
||||||
ici ; un changement de page ensuite est entièrement géré côté
|
role="tab"
|
||||||
client par forgeDocRenderPageList, qui recalcule le VRAI index. #}
|
aria-selected="false"
|
||||||
{% set active_index = 0 %}
|
aria-controls="docTabPanelPages"
|
||||||
<div class="docPageListItem is-active" role="button" tabindex="0" data-page-id="{{ active_page.id }}">
|
data-tab="pages">Pages</button>
|
||||||
|
<button type="button"
|
||||||
|
class="docSidebarTab is-active"
|
||||||
|
id="docTabLayout"
|
||||||
|
role="tab"
|
||||||
|
aria-selected="true"
|
||||||
|
aria-controls="docTabPanelLayout"
|
||||||
|
data-tab="layout">Mise en page</button>
|
||||||
|
</div>
|
||||||
|
<div class="docSidebarTabPanel"
|
||||||
|
id="docTabPanelPages"
|
||||||
|
role="tabpanel"
|
||||||
|
aria-labelledby="docTabPages"
|
||||||
|
hidden>
|
||||||
|
<div class="docPageManagerList" id="docPageManagerList">
|
||||||
|
{% for page in pages %}
|
||||||
|
<div class="docPageRow {{ 'is-active' if page.id == active_page.id else '' }}"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
draggable="true"
|
||||||
|
data-page-id="{{ page.id }}">
|
||||||
{# //NOSONAR Web:S6819,Web:MouseEventWithoutKeyboardEquivalentCheck - div+role=button
|
{# //NOSONAR Web:S6819,Web:MouseEventWithoutKeyboardEquivalentCheck - div+role=button
|
||||||
volontaire : la rangée contient de vrais <button> d'action (réordonner/renommer/
|
volontaire : la rangée contient de vrais <button> d'action (réordonner/renommer/
|
||||||
supprimer), qu'un <button> englobant ne pourrait pas contenir validement.
|
supprimer), qu'un <button> englobant ne pourrait pas contenir validement.
|
||||||
L'équivalent clavier (Entrée/Espace) est posé côté JS (forgeDocRenderPageList),
|
L'équivalent clavier (Entrée/Espace) est posé côté JS (forgeDocRenderPageManagerList),
|
||||||
donc le finding clavier de Sonar est un faux positif : l'analyseur statique ne
|
donc le finding clavier de Sonar est un faux positif : l'analyseur statique ne
|
||||||
voit pas les addEventListener dynamiques. #}
|
voit pas les addEventListener dynamiques. #}
|
||||||
<div class="docPageListActions">
|
<span class="docPageRowHandle" aria-hidden="true">⠿</span>
|
||||||
|
<span class="docPageRowNumber">{{ loop.index }}</span>
|
||||||
|
<span class="docPageRowTitle"><span class="docPageRowTitleText">{{ page.title }}</span></span>
|
||||||
|
<span class="docPageRowActions">
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="docPageListMove"
|
class="docPageRowMove"
|
||||||
data-dir="up"
|
data-dir="up"
|
||||||
data-page-id="{{ active_page.id }}"
|
data-page-id="{{ page.id }}"
|
||||||
aria-label="Réordonner vers la gauche"
|
aria-label="Monter"
|
||||||
title="Réordonner vers la gauche"
|
title="Monter"
|
||||||
{{ 'disabled' if active_index == 0 else '' }}>←</button>
|
{{ 'disabled' if loop.first else '' }}>↑</button>
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="docPageListMove"
|
class="docPageRowMove"
|
||||||
data-dir="down"
|
data-dir="down"
|
||||||
data-page-id="{{ active_page.id }}"
|
data-page-id="{{ page.id }}"
|
||||||
aria-label="Réordonner vers la droite"
|
aria-label="Descendre"
|
||||||
title="Réordonner vers la droite"
|
title="Descendre"
|
||||||
{{ 'disabled' if active_index == pages|length - 1 else '' }}>→</button>
|
{{ 'disabled' if loop.last else '' }}>↓</button>
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="docPageListRename"
|
class="docPageRowRename"
|
||||||
data-page-id="{{ active_page.id }}"
|
data-page-id="{{ page.id }}"
|
||||||
aria-label="Renommer cette page"
|
aria-label="Renommer cette page"
|
||||||
title="Renommer cette page">✎</button>
|
title="Renommer cette page">✎</button>
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="docPageListDelete"
|
class="docPageRowDelete"
|
||||||
data-page-id="{{ active_page.id }}"
|
data-page-id="{{ page.id }}"
|
||||||
aria-label="Supprimer cette page"
|
aria-label="Supprimer cette page"
|
||||||
title="Supprimer cette page"
|
title="Supprimer cette page"
|
||||||
{{ 'disabled' if pages|length <= 1 else '' }}>✕</button>
|
{{ 'disabled' if pages|length <= 1 else '' }}>✕</button>
|
||||||
</div>
|
</span>
|
||||||
<div class="docPageListSquare">{{ active_index + 1 }}</div>
|
</div>
|
||||||
<span class="docPageListTitle"><span class="docPageListTitleText">{{ active_page.title }}</span></span>
|
{% endfor %}
|
||||||
<span class="docPageListCounter">{{ active_index + 1 }} / {{ pages|length }}</span>
|
</div>
|
||||||
|
<button type="button" class="docBtnSecondary docPageManagerAdd" id="docPageManagerAdd">+ Ajouter une page</button>
|
||||||
|
</div>
|
||||||
|
<div class="docSidebarTabPanel" id="docTabPanelLayout" role="tabpanel" aria-labelledby="docTabLayout">
|
||||||
|
{% for category_key, category in element_library.items() %}
|
||||||
|
<div>
|
||||||
|
<div class="docSectionLabel">{{ category.label }}</div>
|
||||||
|
<div class="docLibList">
|
||||||
|
{% for kind in category.kinds %}
|
||||||
|
<div class="docLibItem docLibItem--row"
|
||||||
|
draggable="true"
|
||||||
|
data-add="{{ kind }}">
|
||||||
|
<span class="docLibIcon docLibIcon--{{ kind }}"></span>
|
||||||
|
<span>{{ element_kind_labels[kind] }}</span>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button type="button"
|
{% endfor %}
|
||||||
class="docPageStripNav"
|
|
||||||
id="docPageStripNext"
|
|
||||||
aria-label="Page suivante"
|
|
||||||
title="Page suivante">›</button>
|
|
||||||
</div>
|
|
||||||
<button type="button" class="docBtnSecondary docPageListAdd" id="docPageListAdd">+ Ajouter une page</button>
|
|
||||||
</div>
|
</div>
|
||||||
{% for category_key, category in element_library.items() %}
|
|
||||||
<div>
|
|
||||||
<div class="docSectionLabel">{{ category.label }}</div>
|
|
||||||
<div class="docLibList">
|
|
||||||
{% for kind in category.kinds %}
|
|
||||||
<div class="docLibItem docLibItem--row"
|
|
||||||
draggable="true"
|
|
||||||
data-add="{{ kind }}">
|
|
||||||
<span class="docLibIcon docLibIcon--{{ kind }}"></span>
|
|
||||||
<span>{{ element_kind_labels[kind] }}</span>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="docCanvasArea" id="docCanvasArea">
|
<div class="docCanvasArea" id="docCanvasArea">
|
||||||
<div class="docPage" id="docPage">
|
<div class="docPage" id="docPage">
|
||||||
|
|||||||
Reference in New Issue
Block a user