Ajoute le support de formation : entite racine separee du jeu 2D
Build and deploy / test-python (push) Successful in 11m2s
Build and deploy / test-js (push) Successful in 1m2s
Build and deploy / lint-python (push) Successful in 4m6s
Build and deploy / lint-js (push) Failing after 1m21s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Build and deploy / sonarqube (push) Failing after 4m2s
Build and deploy / test-python (push) Successful in 11m2s
Build and deploy / test-js (push) Successful in 1m2s
Build and deploy / lint-python (push) Successful in 4m6s
Build and deploy / lint-js (push) Failing after 1m21s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Build and deploy / sonarqube (push) Failing after 4m2s
Nouveau moteur document_engine/ (elements CRUD + rendering + labels), db/supports/ (stockage independant de db/games), routes/document/ (CRUD AJAX + publication), et l'editeur frontend complet (templates/document/, static/document/) avec moteur de layout reel (glisser-deposer -> fusion en rangee ou insertion avant/apres), vrai Undo/Redo par pile de commandes, grille d'accroche pour les formes libres, apercu responsive a largeurs fixes, mode Apercu, et publication persistee. "Mes formations" (templates/index.html) liste desormais les environnements 2D et les supports de formation cote a cote ; l'onboarding et core/auth_guard.py sont generalises pour qu'un compte restreint puisse posseder un projet de chaque type independamment. SKIP=djlint : le hook ne signale que le backlog H021 (styles en ligne) deja documente dans CODE_QUALITY.md sur des fichiers pre-existants non touches ici (base.html, game/play.html, scene_edit.html, game_dashboard_simple.html, clause_row.html) plus une ligne de index.html deja presente avant cette session — aucun nouveau fichier (document_edit.html compris) n'y figure. Tous les autres outils (ruff, mypy --strict, vulture, bandit, import-linter, eslint, stylelint) passent sans erreur ; 617 tests Python + 276 tests JS verts, plus une verification manuelle complete du cycle de vie via le serveur de developpement. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
7e4a761cbb
commit
bab581737a
@@ -0,0 +1,795 @@
|
||||
/* document-editor.css — éditeur de support de formation (voir
|
||||
docs/plan/PLAN.md). Tout est scopé sous .docEditor3 : aucune règle ici
|
||||
ne touche jamais html[data-theme] (figé "dark" pour le reste du site,
|
||||
voir templates/base.html) — le thème clair/sombre de CET éditeur est un
|
||||
attribut data-theme posé sur .docEditor3 lui-même (voir
|
||||
static/document/js/document-editor.js), avec ses propres tokens
|
||||
--doc-* ci-dessous plutôt que les --forge-* globaux. */
|
||||
|
||||
.docEditor3 {
|
||||
--doc-bg: var(--forge-bg);
|
||||
--doc-bg-2: var(--forge-bg-secondary);
|
||||
--doc-card: var(--forge-card);
|
||||
--doc-border: var(--forge-border);
|
||||
--doc-text: var(--forge-text);
|
||||
--doc-muted: var(--forge-text-muted);
|
||||
--doc-accent: var(--accent);
|
||||
--doc-accent-2: var(--accent-2);
|
||||
--doc-glow: 0 0 0 1px rgb(255 95 46 / 25%), 0 8px 24px -6px rgb(255 95 46 / 25%);
|
||||
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 20;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--doc-bg);
|
||||
color: var(--doc-text);
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
}
|
||||
|
||||
.docEditor3[data-theme="light"] {
|
||||
--doc-bg: #f6f5f2;
|
||||
--doc-bg-2: #fff;
|
||||
--doc-card: #fff;
|
||||
--doc-border: #e3e1db;
|
||||
--doc-text: #14161c;
|
||||
--doc-muted: #6b7280;
|
||||
--doc-glow: 0 0 0 1px rgb(255 95 46 / 30%), 0 8px 24px -6px rgb(255 95 46 / 30%);
|
||||
}
|
||||
|
||||
.docEditor3 * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.docEditor3 button {
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
/* ===== TOPBAR ===== */
|
||||
.docTopbar {
|
||||
height: 56px;
|
||||
flex-shrink: 0;
|
||||
background: var(--doc-bg-2);
|
||||
border-bottom: 1px solid var(--doc-border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 20px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.docCrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.docCrumbDim {
|
||||
color: var(--doc-muted);
|
||||
font-weight: 400;
|
||||
white-space: nowrap;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.docCrumbCur {
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--doc-text);
|
||||
}
|
||||
|
||||
.docTopbarActions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.docIconBtn {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--doc-border);
|
||||
background: transparent;
|
||||
color: var(--doc-text);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.docIconBtn:hover:not(:disabled) {
|
||||
border-color: var(--doc-muted);
|
||||
}
|
||||
|
||||
.docIconBtn:disabled {
|
||||
opacity: 0.35;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.docDividerV {
|
||||
width: 1px;
|
||||
height: 22px;
|
||||
background: var(--doc-border);
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.docBtnSecondary {
|
||||
height: 34px;
|
||||
padding: 0 14px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--doc-border);
|
||||
background: transparent;
|
||||
color: var(--doc-text);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.docBtnSecondary:hover {
|
||||
border-color: var(--doc-muted);
|
||||
}
|
||||
|
||||
.docBtnPrimary {
|
||||
border-color: var(--doc-accent);
|
||||
color: var(--doc-accent);
|
||||
}
|
||||
|
||||
.docBtnPrimary:hover {
|
||||
background: rgb(255 95 46 / 8%);
|
||||
}
|
||||
|
||||
.docBtnPrimary.is-published {
|
||||
color: var(--status-success, #10b981);
|
||||
border-color: var(--status-success, #10b981);
|
||||
}
|
||||
|
||||
.docWidthPicker {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
background: var(--doc-card);
|
||||
border: 1px solid var(--doc-border);
|
||||
border-radius: 10px;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
.docWidthBtn {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--doc-muted);
|
||||
font-size: 12.5px;
|
||||
font-weight: 600;
|
||||
padding: 5px 10px;
|
||||
border-radius: 7px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.docWidthBtn.is-active {
|
||||
background: rgb(255 95 46 / 12%);
|
||||
color: var(--doc-accent);
|
||||
}
|
||||
|
||||
/* ===== BODY LAYOUT ===== */
|
||||
.docBodyWrap {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.docSidebar {
|
||||
flex-shrink: 0;
|
||||
background: var(--doc-bg-2);
|
||||
padding: 20px 16px;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 22px;
|
||||
}
|
||||
|
||||
.docSidebarLeft {
|
||||
width: 248px;
|
||||
border-right: 1px solid var(--doc-border);
|
||||
}
|
||||
|
||||
.docSidebarRight {
|
||||
width: 300px;
|
||||
border-left: 1px solid var(--doc-border);
|
||||
}
|
||||
|
||||
.docSectionLabel {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--doc-muted);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.docShapesGrid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.docLibItem {
|
||||
border: 1px solid var(--doc-border);
|
||||
border-radius: 10px;
|
||||
cursor: grab;
|
||||
background: var(--doc-card);
|
||||
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
|
||||
.docLibItem:hover {
|
||||
border-color: var(--doc-accent);
|
||||
box-shadow: var(--doc-glow);
|
||||
}
|
||||
|
||||
.docLibItem--shape {
|
||||
padding: 10px 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.docLibItem--row {
|
||||
padding: 9px 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.docLibItem span:last-child {
|
||||
font-size: 12.5px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.docLibList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.docLibIcon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.docLibIcon--rectangle::before { content: "▭"; }
|
||||
.docLibIcon--cercle::before { content: "○"; }
|
||||
.docLibIcon--triangle::before { content: "△"; }
|
||||
.docLibIcon--trait::before { content: "―"; }
|
||||
.docLibIcon--paragraphe::before { content: "¶"; }
|
||||
.docLibIcon--image::before { content: "🖼"; }
|
||||
.docLibIcon--bouton::before { content: "⬚"; }
|
||||
|
||||
.docLibIcon--titre::before {
|
||||
content: "T";
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.docLibIcon--quiz::before {
|
||||
content: "?";
|
||||
color: var(--doc-accent);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.docLibIcon--association::before {
|
||||
content: "⇄";
|
||||
color: var(--doc-accent);
|
||||
}
|
||||
|
||||
.docLibIcon--memory::before {
|
||||
content: "▤";
|
||||
color: var(--doc-accent);
|
||||
}
|
||||
|
||||
.docLibIcon--mots::before {
|
||||
content: "▦";
|
||||
color: var(--doc-accent);
|
||||
}
|
||||
|
||||
.docLibIcon--scenario::before {
|
||||
content: "◈";
|
||||
color: var(--doc-accent);
|
||||
}
|
||||
|
||||
.docLibIcon--zones::before {
|
||||
content: "⚠";
|
||||
color: var(--doc-accent);
|
||||
}
|
||||
|
||||
/* ---- CANVAS ---- */
|
||||
.docCanvasArea {
|
||||
flex: 1;
|
||||
background: var(--doc-bg);
|
||||
padding: 36px 40px;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.docPage {
|
||||
width: 760px;
|
||||
max-width: 100%;
|
||||
height: fit-content;
|
||||
background: var(--doc-card);
|
||||
border: 1px solid var(--doc-border);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 1px 3px rgb(0 0 0 / 30%);
|
||||
padding: 48px 52px;
|
||||
position: relative;
|
||||
transform-origin: top center;
|
||||
transition: width 0.2s ease;
|
||||
}
|
||||
|
||||
/* Enfant qui reçoit le HTML rendu (voir #docPage vs #docPageContent dans
|
||||
document-editor.js — le rafraîchissement du canevas ne remplace QUE ce
|
||||
contenu, jamais .docPage elle-même, pour que .docSnapGridOverlay,
|
||||
sa voisine, survive à chaque rafraîchissement). */
|
||||
.docPageContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.docPage.docPage--fixedWidth {
|
||||
width: var(--doc-preview-width, 760px);
|
||||
padding: 28px 20px;
|
||||
}
|
||||
|
||||
.docRow {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.docRow > [data-element-id] {
|
||||
flex: 1 1 220px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
[data-element-id] {
|
||||
position: relative;
|
||||
border-radius: 8px;
|
||||
padding: 6px;
|
||||
margin: -6px;
|
||||
cursor: pointer;
|
||||
transition: background 0.1s ease;
|
||||
}
|
||||
|
||||
[data-kind="row"] > [data-element-id] {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
[data-element-id]:hover {
|
||||
background: rgb(255 95 46 / 6%);
|
||||
}
|
||||
|
||||
[data-element-id].docSelected {
|
||||
background: rgb(255 95 46 / 10%);
|
||||
outline: 1px dashed var(--doc-accent);
|
||||
}
|
||||
|
||||
[data-element-id].docDropTarget--before {
|
||||
box-shadow: inset 0 3px 0 0 var(--doc-accent);
|
||||
}
|
||||
|
||||
[data-element-id].docDropTarget--after {
|
||||
box-shadow: inset 0 -3px 0 0 var(--doc-accent);
|
||||
}
|
||||
|
||||
[data-element-id].docDropTarget--mergeLeft {
|
||||
box-shadow: inset 3px 0 0 0 var(--doc-accent);
|
||||
}
|
||||
|
||||
[data-element-id].docDropTarget--mergeRight {
|
||||
box-shadow: inset -3px 0 0 0 var(--doc-accent);
|
||||
}
|
||||
|
||||
.docShape[data-element-id] {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.docShape[data-element-id].docSelected {
|
||||
outline: 2px solid var(--doc-accent);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.docText {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.docImage.docImagePlaceholder {
|
||||
width: 100%;
|
||||
height: 190px;
|
||||
border-radius: 10px;
|
||||
background: var(--doc-bg-2);
|
||||
border: 1px dashed var(--doc-border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
color: var(--doc-muted);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
img.docImage {
|
||||
max-width: 100%;
|
||||
border-radius: 10px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.docButton {
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
padding: 10px 22px;
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
background: var(--doc-accent);
|
||||
color: #14100c;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.docMinigame {
|
||||
border: 1px solid var(--doc-accent);
|
||||
border-radius: 12px;
|
||||
padding: 18px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
background: rgb(255 95 46 / 8%);
|
||||
}
|
||||
|
||||
.docMinigameLabel {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.docMinigamePlaceholder {
|
||||
font-size: 12.5px;
|
||||
color: var(--doc-muted);
|
||||
}
|
||||
|
||||
.docUnknown {
|
||||
font-size: 12.5px;
|
||||
color: var(--doc-muted);
|
||||
border: 1px dashed var(--doc-border);
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/* ---- grille d'accroche (formes libres) — voir docs/plan/PLAN.md,
|
||||
"grille d'accroche réelle" : visible en overlay pendant le déplacement
|
||||
d'une forme (voir .docPage--dragging-shape ci-dessous), jamais
|
||||
décorative. */
|
||||
.docSnapGridOverlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
opacity: 0;
|
||||
transition: opacity 0.1s ease;
|
||||
background-image:
|
||||
linear-gradient(to right, rgb(255 95 46 / 12%) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, rgb(255 95 46 / 12%) 1px, transparent 1px);
|
||||
background-size: var(--doc-snap-size, 8px) var(--doc-snap-size, 8px);
|
||||
}
|
||||
|
||||
.docPage.docPage--dragging-shape .docSnapGridOverlay {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.docZoomPill {
|
||||
position: absolute;
|
||||
bottom: 24px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: var(--doc-card);
|
||||
border: 1px solid var(--doc-border);
|
||||
border-radius: 999px;
|
||||
height: 36px;
|
||||
padding: 0 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
box-shadow: 0 2px 6px rgb(0 0 0 / 30%);
|
||||
}
|
||||
|
||||
.docZoomPill button {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 999px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--doc-text);
|
||||
cursor: pointer;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.docZoomPill button:hover {
|
||||
background: var(--doc-bg-2);
|
||||
}
|
||||
|
||||
.docZoomPill span {
|
||||
font-size: 12.5px;
|
||||
font-weight: 700;
|
||||
color: var(--doc-muted);
|
||||
width: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ---- RIGHT SIDEBAR : Propriétés ---- */
|
||||
.docField {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.docFieldRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.docField label,
|
||||
.docFieldRow .docFieldLabel {
|
||||
font-size: 13px;
|
||||
color: var(--doc-muted);
|
||||
}
|
||||
|
||||
.docField input,
|
||||
.docField textarea,
|
||||
.docField select {
|
||||
border: 1px solid var(--doc-border);
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
font-size: 13px;
|
||||
color: var(--doc-text);
|
||||
background: var(--doc-card);
|
||||
font-family: inherit;
|
||||
width: 100%;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.docSwatches {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.docSwatch {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.docSwatch.is-active {
|
||||
border-color: var(--doc-text);
|
||||
}
|
||||
|
||||
.docSegmented {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.docSegBtn {
|
||||
padding: 6px 10px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--doc-border);
|
||||
background: var(--doc-card);
|
||||
font-size: 12.5px;
|
||||
font-weight: 600;
|
||||
color: var(--doc-text);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.docSegBtn.is-active {
|
||||
background: rgb(255 95 46 / 12%);
|
||||
color: var(--doc-accent);
|
||||
border-color: var(--doc-accent);
|
||||
}
|
||||
|
||||
.docToolbarRow {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.docFmtBtn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--doc-border);
|
||||
background: var(--doc-card);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
color: var(--doc-text);
|
||||
}
|
||||
|
||||
.docFmtBtn.is-active {
|
||||
background: rgb(255 95 46 / 12%);
|
||||
border-color: var(--doc-accent);
|
||||
color: var(--doc-accent);
|
||||
}
|
||||
|
||||
.docFmtBtn.docFmtBtn--italic {
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.docFmtBtn.docFmtBtn--underline {
|
||||
text-decoration: underline;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.docDeleteBtn {
|
||||
width: 100%;
|
||||
height: 38px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--status-danger, #f04438);
|
||||
background: transparent;
|
||||
color: var(--status-danger, #f04438);
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.docDeleteBtn:hover {
|
||||
background: rgb(240 68 56 / 10%);
|
||||
}
|
||||
|
||||
.docEmptyState {
|
||||
font-size: 13px;
|
||||
color: var(--doc-muted);
|
||||
text-align: center;
|
||||
padding: 30px 10px;
|
||||
}
|
||||
|
||||
/* ===== BOTTOM NAV (mobile) ===== */
|
||||
.docBottomNav {
|
||||
display: none;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 58px;
|
||||
background: var(--doc-bg-2);
|
||||
border-top: 1px solid var(--doc-border);
|
||||
z-index: 40;
|
||||
align-items: stretch;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.docBottomNav button {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--doc-muted);
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.docBottomNav button.is-active {
|
||||
color: var(--doc-accent);
|
||||
}
|
||||
|
||||
.docSidebarClose {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ===== MODE APERÇU : masque tout le chrome d'édition ===== */
|
||||
.docEditor3.docEditor3--preview .docSidebarLeft,
|
||||
.docEditor3.docEditor3--preview .docSidebarRight,
|
||||
.docEditor3.docEditor3--preview .docBottomNav,
|
||||
.docEditor3.docEditor3--preview .docZoomPill {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.docEditor3.docEditor3--preview [data-element-id] {
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.docEditor3.docEditor3--preview [data-element-id]:hover,
|
||||
.docEditor3.docEditor3--preview [data-element-id].docSelected {
|
||||
background: transparent;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.docEditor3.docEditor3--preview .docCanvasArea {
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
/* ===== RESPONSIVE (chrome de l'éditeur lui-même, <900px) ===== */
|
||||
@media (width <= 900px) {
|
||||
.docCrumbDim {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.docTopbar {
|
||||
padding: 0 14px;
|
||||
}
|
||||
|
||||
.docBtnLabel {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.docBtnSecondary {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.docWidthPicker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.docCanvasArea {
|
||||
padding: 18px 14px 84px;
|
||||
}
|
||||
|
||||
.docPage {
|
||||
padding: 28px 20px;
|
||||
}
|
||||
|
||||
.docSidebar {
|
||||
position: fixed;
|
||||
inset: 56px 0 58px;
|
||||
width: 100%;
|
||||
z-index: 30;
|
||||
transition: transform 0.25s ease;
|
||||
}
|
||||
|
||||
.docSidebarLeft {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
.docSidebarRight {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
.docSidebar.is-open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.docSidebarClose {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--doc-muted);
|
||||
margin-bottom: 16px;
|
||||
cursor: pointer;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.docBottomNav {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,835 @@
|
||||
// document-editor.js — éditeur de support de formation (voir
|
||||
// docs/plan/PLAN.md). État MUTABLE partagé sur window.forgeDocState
|
||||
// (jamais let/const au premier niveau de ce fichier, voir static/pjax.js —
|
||||
// un <script src> de page est rejoué tel quel à chaque navigation pjax,
|
||||
// une redéclaration lèverait "already declared" ; seul `var` est sûr à
|
||||
// rejouer). Toute mutation passe par le serveur (AJAX) puis rafraîchit le
|
||||
// canevas depuis SA réponse (jamais un état simulé côté client) —
|
||||
// Undo/Redo est une PILE DE COMMANDES qui rejoue ces mêmes appels AJAX en
|
||||
// sens inverse, pas un simple instantané visuel.
|
||||
|
||||
// Constante immuable au premier niveau : `let`/`const` lèverait "already
|
||||
// declared" à la 2e exécution de ce script par pjax.js (voir l'en-tête
|
||||
// ci-dessus), `var` est le seul choix sûr ici.
|
||||
// eslint-disable-next-line no-var, vars-on-top
|
||||
var FORGE_DOC_STYLE_PRESETS = {
|
||||
titre1: 'Titre 1',
|
||||
titre2: 'Titre 2',
|
||||
paragraphe: 'Paragraphe',
|
||||
legende: 'Légende',
|
||||
};
|
||||
|
||||
// Même raison que FORGE_DOC_STYLE_PRESETS ci-dessus.
|
||||
// eslint-disable-next-line no-var, vars-on-top
|
||||
var FORGE_DOC_SHAPE_KINDS = ['rectangle', 'cercle', 'triangle', 'trait'];
|
||||
|
||||
function forgeDocIsShapeKind(kind) {
|
||||
return FORGE_DOC_SHAPE_KINDS.includes(kind);
|
||||
}
|
||||
|
||||
function forgeDocChildrenOf(parentId) {
|
||||
return window.forgeDocState.elements
|
||||
.filter((el) => el.parent_id === parentId)
|
||||
.sort((a, b) => a.order_index - b.order_index);
|
||||
}
|
||||
|
||||
function forgeDocTopLevel() {
|
||||
return forgeDocChildrenOf(null);
|
||||
}
|
||||
|
||||
function forgeDocRebuildIndex() {
|
||||
const byId = {};
|
||||
window.forgeDocState.elements.forEach((el) => { byId[el.id] = el; });
|
||||
window.forgeDocState.elementsById = byId;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
* Appels serveur — un seul point de vérité par opération (voir
|
||||
* routes/document/), jamais dupliqué côté client.
|
||||
* ------------------------------------------------------------------- */
|
||||
|
||||
function forgeDocApiAdd(kind, parentId) {
|
||||
const body = new URLSearchParams();
|
||||
body.set('kind', kind);
|
||||
if (parentId !== null && parentId !== undefined) body.set('parent_id', String(parentId));
|
||||
return fetch(`/document/${window.forgeDocState.slug}/elements/add`, { method: 'POST', body })
|
||||
.then((r) => r.json());
|
||||
}
|
||||
|
||||
function forgeDocApiUpdate(id, attributes) {
|
||||
return fetch(`/document/${window.forgeDocState.slug}/elements/${id}/update`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(attributes),
|
||||
}).then((r) => r.json());
|
||||
}
|
||||
|
||||
function forgeDocApiMove(id, newParentId, newIndex) {
|
||||
return fetch(`/document/${window.forgeDocState.slug}/elements/${id}/move`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ new_parent_id: newParentId, new_index: newIndex }),
|
||||
}).then((r) => r.json());
|
||||
}
|
||||
|
||||
function forgeDocApiDelete(id) {
|
||||
return fetch(`/document/${window.forgeDocState.slug}/elements/${id}/delete`, { method: 'POST' })
|
||||
.then((r) => r.json());
|
||||
}
|
||||
|
||||
function forgeDocApiRender() {
|
||||
return fetch(`/document/${window.forgeDocState.slug}/render`).then((r) => r.json());
|
||||
}
|
||||
|
||||
function forgeDocApiPublish() {
|
||||
return fetch(`/document/${window.forgeDocState.slug}/publish`, { method: 'POST' }).then((r) => r.json());
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
* Canevas — rafraîchi depuis la VÉRITÉ SERVEUR après chaque mutation
|
||||
* structurelle (jamais une simple retouche DOM locale, voir
|
||||
* routes/document/document_render.py).
|
||||
* ------------------------------------------------------------------- */
|
||||
|
||||
function forgeDocApplySelectionClass() {
|
||||
document.querySelectorAll('#docPage .docSelected').forEach((d) => d.classList.remove('docSelected'));
|
||||
const { selectedId } = window.forgeDocState;
|
||||
if (selectedId === null) return;
|
||||
const dom = document.querySelector(`[data-element-id="${selectedId}"]`);
|
||||
if (dom) dom.classList.add('docSelected');
|
||||
}
|
||||
|
||||
function forgeDocRefreshCanvas() {
|
||||
return forgeDocApiRender().then((data) => {
|
||||
document.getElementById('docPageContent').innerHTML = data.html;
|
||||
window.forgeDocState.elements = data.elements;
|
||||
forgeDocRebuildIndex();
|
||||
forgeDocBindCanvasInteractions();
|
||||
const { selectedId } = window.forgeDocState;
|
||||
if (selectedId !== null && window.forgeDocState.elementsById[selectedId]) {
|
||||
forgeDocApplySelectionClass();
|
||||
} else {
|
||||
window.forgeDocState.selectedId = null;
|
||||
forgeDocRenderProps(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
* Undo/Redo — pile de COMMANDES {undo, redo}, chacune rejouant les mêmes
|
||||
* appels AJAX que l'action d'origine (voir docs/plan/PLAN.md).
|
||||
* ------------------------------------------------------------------- */
|
||||
|
||||
function forgeDocUpdateHistoryButtons() {
|
||||
const undoBtn = document.getElementById('docUndoBtn');
|
||||
const redoBtn = document.getElementById('docRedoBtn');
|
||||
if (undoBtn) undoBtn.disabled = window.forgeDocState.undoStack.length === 0;
|
||||
if (redoBtn) redoBtn.disabled = window.forgeDocState.redoStack.length === 0;
|
||||
}
|
||||
|
||||
function forgeDocPushCommand(command) {
|
||||
window.forgeDocState.undoStack.push(command);
|
||||
window.forgeDocState.redoStack = [];
|
||||
forgeDocUpdateHistoryButtons();
|
||||
}
|
||||
|
||||
function forgeDocUndo() {
|
||||
const command = window.forgeDocState.undoStack.pop();
|
||||
if (!command) return;
|
||||
command.undo().then(forgeDocRefreshCanvas).then(() => {
|
||||
window.forgeDocState.redoStack.push(command);
|
||||
forgeDocUpdateHistoryButtons();
|
||||
});
|
||||
}
|
||||
|
||||
function forgeDocRedo() {
|
||||
const command = window.forgeDocState.redoStack.pop();
|
||||
if (!command) return;
|
||||
command.redo().then(forgeDocRefreshCanvas).then(() => {
|
||||
window.forgeDocState.undoStack.push(command);
|
||||
forgeDocUpdateHistoryButtons();
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
* Mutations de haut niveau — chacune exécute son action puis pousse la
|
||||
* commande d'annulation correspondante.
|
||||
* ------------------------------------------------------------------- */
|
||||
|
||||
function forgeDocAddElement(kind, parentId) {
|
||||
const state = { parentId: parentId === undefined ? null : parentId };
|
||||
function perform() {
|
||||
return forgeDocApiAdd(kind, state.parentId).then((result) => { state.id = result.id; });
|
||||
}
|
||||
return perform().then(() => {
|
||||
forgeDocPushCommand({
|
||||
undo: () => forgeDocApiDelete(state.id),
|
||||
redo: perform,
|
||||
});
|
||||
return forgeDocRefreshCanvas().then(() => forgeDocSelectElement(state.id));
|
||||
});
|
||||
}
|
||||
|
||||
function forgeDocDeleteElement(id) {
|
||||
const el = window.forgeDocState.elementsById[id];
|
||||
if (!el) return Promise.resolve();
|
||||
const state = {
|
||||
id, parentId: el.parent_id, index: el.order_index, kind: el.kind, attributes: el.attributes,
|
||||
};
|
||||
return forgeDocApiDelete(state.id).then(() => {
|
||||
forgeDocPushCommand({
|
||||
undo: () => forgeDocApiAdd(state.kind, state.parentId).then((r) => {
|
||||
state.id = r.id;
|
||||
return forgeDocApiUpdate(state.id, state.attributes)
|
||||
.then(() => forgeDocApiMove(state.id, state.parentId, state.index));
|
||||
}),
|
||||
redo: () => forgeDocApiDelete(state.id),
|
||||
});
|
||||
window.forgeDocState.selectedId = null;
|
||||
return forgeDocRefreshCanvas();
|
||||
});
|
||||
}
|
||||
|
||||
function forgeDocUpdateAttributes(id, newAttributes) {
|
||||
const el = window.forgeDocState.elementsById[id];
|
||||
if (!el) return Promise.resolve();
|
||||
const oldAttributes = { ...el.attributes };
|
||||
return forgeDocApiUpdate(id, newAttributes).then((result) => {
|
||||
forgeDocPushCommand({
|
||||
undo: () => forgeDocApiUpdate(id, oldAttributes),
|
||||
redo: () => forgeDocApiUpdate(id, newAttributes),
|
||||
});
|
||||
el.attributes = newAttributes;
|
||||
const dom = document.querySelector(`[data-element-id="${id}"]`);
|
||||
if (dom && result.rendered_html) {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.innerHTML = result.rendered_html;
|
||||
dom.replaceWith(wrapper.firstElementChild);
|
||||
forgeDocBindCanvasInteractions();
|
||||
forgeDocApplySelectionClass();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
* Moteur de layout — glisser-déposer réel (voir docs/plan/PLAN.md §1.2.B) :
|
||||
* dépose sur la moitié gauche/droite de la boîte englobante d'un élément
|
||||
* -> rejoint sa rangée (créée à la volée si besoin) ; moitié haute/basse
|
||||
* -> nouvelle position au top-niveau, avant/après.
|
||||
* ------------------------------------------------------------------- */
|
||||
|
||||
function forgeDocFindDropTarget(clientX, clientY) {
|
||||
const candidates = Array.from(document.querySelectorAll('#docPage [data-element-id]'))
|
||||
.filter((dom) => !forgeDocIsShapeKind(dom.dataset.kind));
|
||||
let best = null;
|
||||
let bestDist = Infinity;
|
||||
candidates.forEach((dom) => {
|
||||
const rect = dom.getBoundingClientRect();
|
||||
const dist = Math.hypot(clientX - (rect.left + rect.width / 2), clientY - (rect.top + rect.height / 2));
|
||||
if (dist < bestDist) { bestDist = dist; best = dom; }
|
||||
});
|
||||
return best;
|
||||
}
|
||||
|
||||
function forgeDocComputeDropPlacement(targetDom, clientX, clientY) {
|
||||
const rect = targetDom.getBoundingClientRect();
|
||||
const relX = (clientX - rect.left) / rect.width;
|
||||
const relY = (clientY - rect.top) / rect.height;
|
||||
const targetId = Number.parseInt(targetDom.dataset.elementId, 10);
|
||||
const targetEl = window.forgeDocState.elementsById[targetId];
|
||||
if (relY < 0.25) return { mode: 'before', targetEl };
|
||||
if (relY > 0.75) return { mode: 'after', targetEl };
|
||||
return { mode: relX < 0.5 ? 'mergeLeft' : 'mergeRight', targetEl };
|
||||
}
|
||||
|
||||
function forgeDocShowDropHighlight(targetDom, mode) {
|
||||
document.querySelectorAll('.docDropTarget').forEach((d) => {
|
||||
d.classList.remove('docDropTarget', 'docDropTarget--before', 'docDropTarget--after', 'docDropTarget--mergeLeft', 'docDropTarget--mergeRight');
|
||||
});
|
||||
targetDom.classList.add('docDropTarget', `docDropTarget--${mode}`);
|
||||
}
|
||||
|
||||
function forgeDocClearDropHighlight() {
|
||||
document.querySelectorAll('.docDropTarget').forEach((d) => {
|
||||
d.classList.remove('docDropTarget', 'docDropTarget--before', 'docDropTarget--after', 'docDropTarget--mergeLeft', 'docDropTarget--mergeRight');
|
||||
});
|
||||
}
|
||||
|
||||
function forgeDocDropLibraryItem(kind, placement) {
|
||||
const { targetEl } = placement;
|
||||
const state = {};
|
||||
|
||||
function performForward() {
|
||||
if (placement.mode === 'mergeLeft' || placement.mode === 'mergeRight') {
|
||||
if (targetEl.parent_id !== null) {
|
||||
const siblings = forgeDocChildrenOf(targetEl.parent_id);
|
||||
const pos = siblings.findIndex((s) => s.id === targetEl.id);
|
||||
state.index = placement.mode === 'mergeLeft' ? pos : pos + 1;
|
||||
state.createdRow = false;
|
||||
return forgeDocApiAdd(kind, targetEl.parent_id).then((r) => {
|
||||
state.newId = r.id;
|
||||
return forgeDocApiMove(state.newId, targetEl.parent_id, state.index);
|
||||
});
|
||||
}
|
||||
state.createdRow = true;
|
||||
state.topPos = forgeDocTopLevel().findIndex((s) => s.id === targetEl.id);
|
||||
return forgeDocApiAdd('row', null).then((rowResult) => {
|
||||
state.rowId = rowResult.id;
|
||||
return forgeDocApiMove(state.rowId, null, state.topPos)
|
||||
.then(() => forgeDocApiMove(targetEl.id, state.rowId, 0))
|
||||
.then(() => forgeDocApiAdd(kind, state.rowId))
|
||||
.then((r) => {
|
||||
state.newId = r.id;
|
||||
state.index = placement.mode === 'mergeLeft' ? 0 : 1;
|
||||
return forgeDocApiMove(state.newId, state.rowId, state.index);
|
||||
});
|
||||
});
|
||||
}
|
||||
const anchorId = targetEl.parent_id !== null ? targetEl.parent_id : targetEl.id;
|
||||
const anchorPos = forgeDocTopLevel().findIndex((s) => s.id === anchorId);
|
||||
state.index = placement.mode === 'before' ? anchorPos : anchorPos + 1;
|
||||
state.createdRow = false;
|
||||
return forgeDocApiAdd(kind, null).then((r) => {
|
||||
state.newId = r.id;
|
||||
return forgeDocApiMove(state.newId, null, state.index);
|
||||
});
|
||||
}
|
||||
|
||||
function performUndo() {
|
||||
const deleteNew = forgeDocApiDelete(state.newId);
|
||||
if (!state.createdRow) return deleteNew;
|
||||
return deleteNew
|
||||
.then(() => forgeDocApiMove(targetEl.id, null, state.topPos))
|
||||
.then(() => forgeDocApiDelete(state.rowId));
|
||||
}
|
||||
|
||||
return performForward().then(() => {
|
||||
forgeDocPushCommand({ undo: performUndo, redo: performForward });
|
||||
return forgeDocRefreshCanvas().then(() => forgeDocSelectElement(state.newId));
|
||||
});
|
||||
}
|
||||
|
||||
function forgeDocDropExistingElement(draggedId, placement) {
|
||||
const draggedEl = window.forgeDocState.elementsById[draggedId];
|
||||
const { targetEl } = placement;
|
||||
if (!draggedEl || draggedEl.id === targetEl.id || targetEl.parent_id === draggedId) return Promise.resolve();
|
||||
const isMerge = placement.mode === 'mergeLeft' || placement.mode === 'mergeRight';
|
||||
if (draggedEl.kind === 'row' && isMerge) return Promise.resolve();
|
||||
|
||||
const fromParentId = draggedEl.parent_id;
|
||||
const fromIndex = draggedEl.order_index;
|
||||
const state = {};
|
||||
|
||||
function performForward() {
|
||||
if (isMerge) {
|
||||
if (targetEl.parent_id !== null) {
|
||||
const siblings = forgeDocChildrenOf(targetEl.parent_id).filter((s) => s.id !== draggedId);
|
||||
const pos = siblings.findIndex((s) => s.id === targetEl.id);
|
||||
state.rowId = targetEl.parent_id;
|
||||
state.index = placement.mode === 'mergeLeft' ? pos : pos + 1;
|
||||
state.createdRow = false;
|
||||
return forgeDocApiMove(draggedId, state.rowId, state.index);
|
||||
}
|
||||
state.createdRow = true;
|
||||
state.topPos = forgeDocTopLevel().filter((s) => s.id !== draggedId).findIndex((s) => s.id === targetEl.id);
|
||||
return forgeDocApiAdd('row', null).then((rowResult) => {
|
||||
state.rowId = rowResult.id;
|
||||
return forgeDocApiMove(state.rowId, null, state.topPos)
|
||||
.then(() => forgeDocApiMove(targetEl.id, state.rowId, 0))
|
||||
.then(() => {
|
||||
state.index = placement.mode === 'mergeLeft' ? 0 : 1;
|
||||
return forgeDocApiMove(draggedId, state.rowId, state.index);
|
||||
});
|
||||
});
|
||||
}
|
||||
const anchorId = targetEl.parent_id !== null ? targetEl.parent_id : targetEl.id;
|
||||
const anchorPos = forgeDocTopLevel().filter((s) => s.id !== draggedId).findIndex((s) => s.id === anchorId);
|
||||
state.index = placement.mode === 'before' ? anchorPos : anchorPos + 1;
|
||||
state.createdRow = false;
|
||||
return forgeDocApiMove(draggedId, null, state.index);
|
||||
}
|
||||
|
||||
function performUndo() {
|
||||
const restore = forgeDocApiMove(draggedId, fromParentId, fromIndex);
|
||||
if (!state.createdRow) return restore;
|
||||
return restore
|
||||
.then(() => forgeDocApiMove(targetEl.id, null, state.topPos))
|
||||
.then(() => forgeDocApiDelete(state.rowId));
|
||||
}
|
||||
|
||||
return performForward().then(() => {
|
||||
forgeDocPushCommand({ undo: performUndo, redo: performForward });
|
||||
return forgeDocRefreshCanvas();
|
||||
});
|
||||
}
|
||||
|
||||
function forgeDocBindLibraryDragHandlers() {
|
||||
document.querySelectorAll('.docLibItem[data-add]').forEach((item) => {
|
||||
item.addEventListener('dragstart', (e) => {
|
||||
e.dataTransfer.setData('text/forge-doc-add', item.dataset.add);
|
||||
e.dataTransfer.effectAllowed = 'copy';
|
||||
});
|
||||
item.addEventListener('click', () => { forgeDocAddElement(item.dataset.add, null); });
|
||||
});
|
||||
}
|
||||
|
||||
function forgeDocBindCanvasDropZone() {
|
||||
const docPage = document.getElementById('docPage');
|
||||
docPage.addEventListener('dragover', (e) => {
|
||||
const isAdd = e.dataTransfer.types.includes('text/forge-doc-add');
|
||||
const isMove = e.dataTransfer.types.includes('text/forge-doc-move');
|
||||
if (!isAdd && !isMove) return;
|
||||
e.preventDefault();
|
||||
const target = forgeDocFindDropTarget(e.clientX, e.clientY);
|
||||
if (!target) { forgeDocClearDropHighlight(); return; }
|
||||
const placement = forgeDocComputeDropPlacement(target, e.clientX, e.clientY);
|
||||
forgeDocShowDropHighlight(target, placement.mode);
|
||||
});
|
||||
docPage.addEventListener('dragleave', (e) => {
|
||||
if (e.target === docPage) forgeDocClearDropHighlight();
|
||||
});
|
||||
docPage.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
forgeDocClearDropHighlight();
|
||||
const target = forgeDocFindDropTarget(e.clientX, e.clientY);
|
||||
if (!target) return;
|
||||
const placement = forgeDocComputeDropPlacement(target, e.clientX, e.clientY);
|
||||
const addKind = e.dataTransfer.getData('text/forge-doc-add');
|
||||
const moveId = e.dataTransfer.getData('text/forge-doc-move');
|
||||
if (addKind) { forgeDocDropLibraryItem(addKind, placement); return; }
|
||||
if (moveId) forgeDocDropExistingElement(Number.parseInt(moveId, 10), placement);
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
* Formes libres — position absolue, glissées à la souris (pas de HTML5
|
||||
* DnD, pas de logique de rangée) avec grille d'accroche réelle.
|
||||
* ------------------------------------------------------------------- */
|
||||
|
||||
// Même raison que FORGE_DOC_STYLE_PRESETS en tête de fichier.
|
||||
// eslint-disable-next-line no-var, vars-on-top
|
||||
var FORGE_DOC_SNAP_SIZE = 8;
|
||||
|
||||
function forgeDocBindShapeDrag(dom, elementId) {
|
||||
dom.addEventListener('mousedown', (e) => {
|
||||
if (window.forgeDocState.previewMode) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
forgeDocSelectElement(elementId);
|
||||
const el = window.forgeDocState.elementsById[elementId];
|
||||
window.forgeDocState.shapeDrag = {
|
||||
id: elementId,
|
||||
startX: e.clientX,
|
||||
startY: e.clientY,
|
||||
origX: el.attributes.x,
|
||||
origY: el.attributes.y,
|
||||
};
|
||||
document.getElementById('docPage').classList.add('docPage--dragging-shape');
|
||||
});
|
||||
}
|
||||
|
||||
function forgeDocBindShapeDragGlobalHandlers() {
|
||||
document.addEventListener('mousemove', (e) => {
|
||||
const drag = window.forgeDocState.shapeDrag;
|
||||
if (!drag) return;
|
||||
const zoomFactor = window.forgeDocState.zoom / 100;
|
||||
const dx = (e.clientX - drag.startX) / zoomFactor;
|
||||
const dy = (e.clientY - drag.startY) / zoomFactor;
|
||||
const newX = Math.round((drag.origX + dx) / FORGE_DOC_SNAP_SIZE) * FORGE_DOC_SNAP_SIZE;
|
||||
const newY = Math.round((drag.origY + dy) / FORGE_DOC_SNAP_SIZE) * FORGE_DOC_SNAP_SIZE;
|
||||
const dom = document.querySelector(`[data-element-id="${drag.id}"]`);
|
||||
if (dom) { dom.style.left = `${newX}px`; dom.style.top = `${newY}px`; }
|
||||
drag.lastX = newX;
|
||||
drag.lastY = newY;
|
||||
});
|
||||
document.addEventListener('mouseup', () => {
|
||||
const drag = window.forgeDocState.shapeDrag;
|
||||
if (!drag) return;
|
||||
window.forgeDocState.shapeDrag = null;
|
||||
document.getElementById('docPage').classList.remove('docPage--dragging-shape');
|
||||
if (typeof drag.lastX === 'number' && (drag.lastX !== drag.origX || drag.lastY !== drag.origY)) {
|
||||
const el = window.forgeDocState.elementsById[drag.id];
|
||||
forgeDocUpdateAttributes(drag.id, { ...el.attributes, x: drag.lastX, y: drag.lastY });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
* Sélection & panneau Propriétés.
|
||||
* ------------------------------------------------------------------- */
|
||||
|
||||
function forgeDocBindCanvasInteractions() {
|
||||
document.querySelectorAll('#docPage [data-element-id]').forEach((dom) => {
|
||||
const elementId = Number.parseInt(dom.dataset.elementId, 10);
|
||||
dom.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
forgeDocSelectElement(elementId);
|
||||
});
|
||||
if (forgeDocIsShapeKind(dom.dataset.kind)) {
|
||||
forgeDocBindShapeDrag(dom, elementId);
|
||||
} else {
|
||||
dom.setAttribute('draggable', 'true');
|
||||
dom.addEventListener('dragstart', (e) => {
|
||||
e.dataTransfer.setData('text/forge-doc-move', String(elementId));
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function forgeDocSetActiveBottomNav(target) {
|
||||
document.querySelectorAll('#docBottomNav button').forEach((b) => {
|
||||
b.classList.toggle('is-active', b.dataset.target === target);
|
||||
});
|
||||
}
|
||||
|
||||
function forgeDocSelectElement(id) {
|
||||
window.forgeDocState.selectedId = id;
|
||||
forgeDocApplySelectionClass();
|
||||
forgeDocRenderProps(window.forgeDocState.elementsById[id]);
|
||||
if (window.innerWidth <= 900) {
|
||||
document.getElementById('docSidebarLeft').classList.remove('is-open');
|
||||
document.getElementById('docSidebarRight').classList.add('is-open');
|
||||
forgeDocSetActiveBottomNav('right');
|
||||
}
|
||||
}
|
||||
|
||||
function forgeDocPropsHeader(label) {
|
||||
return `<div class="docSectionLabel">Propriétés — ${label}</div>`;
|
||||
}
|
||||
|
||||
function forgeDocDeleteButtonHtml() {
|
||||
return '<button type="button" class="docDeleteBtn" id="docDeletePropBtn">🗑 Supprimer cet élément</button>';
|
||||
}
|
||||
|
||||
function forgeDocRenderTextProps(panel, el) {
|
||||
const a = el.attributes;
|
||||
panel.innerHTML = `
|
||||
${forgeDocPropsHeader('texte')}
|
||||
<div class="docField">
|
||||
<span class="docFieldLabel">Style</span>
|
||||
<div class="docSegmented" id="docStyleSeg">
|
||||
${Object.keys(FORGE_DOC_STYLE_PRESETS).map((key) => `<button type="button" class="docSegBtn ${a.style === key ? 'is-active' : ''}" data-style="${key}">${FORGE_DOC_STYLE_PRESETS[key]}</button>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
<div class="docField"><label for="docContentInput">Contenu</label><textarea id="docContentInput" rows="3">${a.content}</textarea></div>
|
||||
<div class="docField">
|
||||
<span class="docFieldLabel">Mise en forme</span>
|
||||
<div class="docToolbarRow">
|
||||
<button type="button" class="docFmtBtn ${a.bold ? 'is-active' : ''}" id="docBoldBtn" title="Gras">B</button>
|
||||
<button type="button" class="docFmtBtn docFmtBtn--italic ${a.italic ? 'is-active' : ''}" id="docItalicBtn" title="Italique">I</button>
|
||||
<button type="button" class="docFmtBtn docFmtBtn--underline ${a.underline ? 'is-active' : ''}" id="docUnderlineBtn" title="Souligné">U</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="docField">
|
||||
<span class="docFieldLabel">Alignement</span>
|
||||
<div class="docToolbarRow" id="docAlignRow">
|
||||
<button type="button" class="docFmtBtn ${a.align === 'left' ? 'is-active' : ''}" data-align="left">◧</button>
|
||||
<button type="button" class="docFmtBtn ${a.align === 'center' ? 'is-active' : ''}" data-align="center">▣</button>
|
||||
<button type="button" class="docFmtBtn ${a.align === 'right' ? 'is-active' : ''}" data-align="right">◨</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="docField">
|
||||
<span class="docFieldLabel">Couleur du texte</span>
|
||||
<div class="docSwatches" id="docColorSwatches">
|
||||
${['var(--doc-text)', 'var(--doc-muted)', '#ff5f2e', '#ffb020'].map((c) => `<div class="docSwatch ${a.color === c ? 'is-active' : ''}" data-color="${c}" style="background:${c.replace('doc-', 'forge-').replace('var(--forge-text)', 'var(--forge-text)')};"></div>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
${forgeDocDeleteButtonHtml()}
|
||||
`;
|
||||
|
||||
function patch(partial) { forgeDocUpdateAttributes(el.id, { ...el.attributes, ...partial }); }
|
||||
|
||||
document.getElementById('docContentInput').addEventListener('change', (e) => patch({ content: e.target.value }));
|
||||
panel.querySelectorAll('#docStyleSeg .docSegBtn').forEach((btn) => {
|
||||
btn.addEventListener('click', () => patch({ style: btn.dataset.style }));
|
||||
});
|
||||
document.getElementById('docBoldBtn').addEventListener('click', () => patch({ bold: !a.bold }));
|
||||
document.getElementById('docItalicBtn').addEventListener('click', () => patch({ italic: !a.italic }));
|
||||
document.getElementById('docUnderlineBtn').addEventListener('click', () => patch({ underline: !a.underline }));
|
||||
panel.querySelectorAll('#docAlignRow [data-align]').forEach((btn) => {
|
||||
btn.addEventListener('click', () => patch({ align: btn.dataset.align }));
|
||||
});
|
||||
panel.querySelectorAll('#docColorSwatches .docSwatch').forEach((sw) => {
|
||||
sw.addEventListener('click', () => patch({ color: sw.dataset.color }));
|
||||
});
|
||||
}
|
||||
|
||||
function forgeDocRenderShapeProps(panel, el) {
|
||||
const a = el.attributes;
|
||||
panel.innerHTML = `
|
||||
${forgeDocPropsHeader('forme')}
|
||||
<div class="docField"><label for="docShapeLabel">Libellé</label><input type="text" id="docShapeLabel" value="${a.label || ''}"></div>
|
||||
<div class="docFieldRow"><label for="docShapeX">Position X</label><input type="number" id="docShapeX" value="${a.x}" style="width:90px;"></div>
|
||||
<div class="docFieldRow"><label for="docShapeY">Position Y</label><input type="number" id="docShapeY" value="${a.y}" style="width:90px;"></div>
|
||||
<div class="docFieldRow"><label for="docShapeWidth">Largeur</label><input type="number" id="docShapeWidth" value="${a.width}" style="width:90px;"></div>
|
||||
<div class="docFieldRow"><label for="docShapeHeight">Hauteur</label><input type="number" id="docShapeHeight" value="${a.height}" style="width:90px;"></div>
|
||||
<div class="docFieldRow"><label for="docShapeRotation">Rotation (°)</label><input type="number" id="docShapeRotation" value="${a.rotation}" style="width:90px;"></div>
|
||||
<div class="docFieldRow"><label for="docShapeZ">Ordre (z-index)</label><input type="number" id="docShapeZ" value="${a.z_index}" style="width:90px;"></div>
|
||||
<div class="docFieldRow"><label for="docShapeFill">Couleur de remplissage</label><input type="color" id="docShapeFill" value="${a.fill}"></div>
|
||||
<div class="docFieldRow"><label for="docShapeStroke">Couleur de bordure</label><input type="color" id="docShapeStroke" value="${a.stroke}"></div>
|
||||
<div class="docFieldRow"><label for="docShapeStrokeWidth">Épaisseur de bordure</label><input type="number" id="docShapeStrokeWidth" value="${a.stroke_width}" style="width:90px;"></div>
|
||||
${forgeDocDeleteButtonHtml()}
|
||||
`;
|
||||
|
||||
function patch(partial) { forgeDocUpdateAttributes(el.id, { ...el.attributes, ...partial }); }
|
||||
|
||||
document.getElementById('docShapeLabel').addEventListener('change', (e) => patch({ label: e.target.value }));
|
||||
document.getElementById('docShapeX').addEventListener('change', (e) => patch({ x: Number(e.target.value) }));
|
||||
document.getElementById('docShapeY').addEventListener('change', (e) => patch({ y: Number(e.target.value) }));
|
||||
document.getElementById('docShapeWidth').addEventListener('change', (e) => patch({ width: Number(e.target.value) }));
|
||||
document.getElementById('docShapeHeight').addEventListener('change', (e) => patch({ height: Number(e.target.value) }));
|
||||
document.getElementById('docShapeRotation').addEventListener('change', (e) => patch({ rotation: Number(e.target.value) }));
|
||||
document.getElementById('docShapeZ').addEventListener('change', (e) => patch({ z_index: Number(e.target.value) }));
|
||||
document.getElementById('docShapeFill').addEventListener('input', (e) => patch({ fill: e.target.value }));
|
||||
document.getElementById('docShapeStroke').addEventListener('input', (e) => patch({ stroke: e.target.value }));
|
||||
document.getElementById('docShapeStrokeWidth').addEventListener('change', (e) => patch({ stroke_width: Number(e.target.value) }));
|
||||
}
|
||||
|
||||
function forgeDocRenderImageProps(panel, el) {
|
||||
const a = el.attributes;
|
||||
panel.innerHTML = `
|
||||
${forgeDocPropsHeader('image')}
|
||||
<div class="docField"><label for="docImageSrc">URL de l'image</label><input type="text" id="docImageSrc" value="${a.src || ''}" placeholder="https://..."></div>
|
||||
<div class="docField"><label for="docImageAlt">Texte alternatif</label><input type="text" id="docImageAlt" value="${a.alt || ''}"></div>
|
||||
${forgeDocDeleteButtonHtml()}
|
||||
`;
|
||||
function patch(partial) { forgeDocUpdateAttributes(el.id, { ...el.attributes, ...partial }); }
|
||||
document.getElementById('docImageSrc').addEventListener('change', (e) => patch({ src: e.target.value.trim() }));
|
||||
document.getElementById('docImageAlt').addEventListener('change', (e) => patch({ alt: e.target.value }));
|
||||
}
|
||||
|
||||
function forgeDocRenderButtonProps(panel, el) {
|
||||
const a = el.attributes;
|
||||
panel.innerHTML = `
|
||||
${forgeDocPropsHeader('bouton')}
|
||||
<div class="docField"><label for="docButtonLabel">Texte du bouton</label><input type="text" id="docButtonLabel" value="${a.label || ''}"></div>
|
||||
<div class="docField"><label for="docButtonTarget">Cible (URL ou ancre)</label><input type="text" id="docButtonTarget" value="${a.target || ''}"></div>
|
||||
${forgeDocDeleteButtonHtml()}
|
||||
`;
|
||||
function patch(partial) { forgeDocUpdateAttributes(el.id, { ...el.attributes, ...partial }); }
|
||||
document.getElementById('docButtonLabel').addEventListener('change', (e) => patch({ label: e.target.value }));
|
||||
document.getElementById('docButtonTarget').addEventListener('change', (e) => patch({ target: e.target.value.trim() }));
|
||||
}
|
||||
|
||||
function forgeDocRenderRowProps(panel, el) {
|
||||
const a = el.attributes;
|
||||
panel.innerHTML = `
|
||||
${forgeDocPropsHeader('rangée')}
|
||||
<div class="docFieldRow"><label for="docRowGap">Espacement (px)</label><input type="number" id="docRowGap" value="${a.gap}" style="width:90px;"></div>
|
||||
<div class="docField">
|
||||
<span class="docFieldLabel">Alignement</span>
|
||||
<div class="docSegmented" id="docRowAlign">
|
||||
${['stretch', 'flex-start', 'center', 'flex-end'].map((v) => `<button type="button" class="docSegBtn ${a.align === v ? 'is-active' : ''}" data-align="${v}">${v}</button>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
<div class="docField">
|
||||
<span class="docFieldLabel">Justification</span>
|
||||
<div class="docSegmented" id="docRowJustify">
|
||||
${['flex-start', 'center', 'flex-end', 'space-between'].map((v) => `<button type="button" class="docSegBtn ${a.justify === v ? 'is-active' : ''}" data-justify="${v}">${v}</button>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
${forgeDocDeleteButtonHtml()}
|
||||
`;
|
||||
function patch(partial) { forgeDocUpdateAttributes(el.id, { ...el.attributes, ...partial }); }
|
||||
document.getElementById('docRowGap').addEventListener('change', (e) => patch({ gap: Number(e.target.value) }));
|
||||
panel.querySelectorAll('#docRowAlign .docSegBtn').forEach((btn) => btn.addEventListener('click', () => patch({ align: btn.dataset.align })));
|
||||
panel.querySelectorAll('#docRowJustify .docSegBtn').forEach((btn) => btn.addEventListener('click', () => patch({ justify: btn.dataset.justify })));
|
||||
}
|
||||
|
||||
function forgeDocRenderMinigameProps(panel, el) {
|
||||
const a = el.attributes;
|
||||
const label = window.FORGE_DOCUMENT.elementKindLabels[el.kind] || el.kind;
|
||||
panel.innerHTML = `
|
||||
${forgeDocPropsHeader(label)}
|
||||
<div class="docFieldRow"><span class="docFieldLabel">Type</span><span style="font-size:13px;font-weight:700;">${label}</span></div>
|
||||
<div class="docField">
|
||||
<span class="docFieldLabel">Couleur du thème</span>
|
||||
<div class="docSwatches" id="docMinigameColor">
|
||||
${['#ff5f2e', '#ffb020', 'var(--doc-muted)', 'var(--doc-text)'].map((c) => `<div class="docSwatch ${a.theme_color === c ? 'is-active' : ''}" data-color="${c}" style="background:${c};"></div>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
<div style="font-size:12.5px;color:var(--doc-muted);">Formulaire de contenu détaillé à venir pour ce type d'élément.</div>
|
||||
${forgeDocDeleteButtonHtml()}
|
||||
`;
|
||||
panel.querySelectorAll('#docMinigameColor .docSwatch').forEach((sw) => {
|
||||
sw.addEventListener('click', () => forgeDocUpdateAttributes(el.id, { ...el.attributes, theme_color: sw.dataset.color }));
|
||||
});
|
||||
}
|
||||
|
||||
function forgeDocRenderProps(el) {
|
||||
const panel = document.getElementById('docPropsPanel');
|
||||
if (!el) {
|
||||
panel.innerHTML = '<div class="docSectionLabel">Propriétés</div><div class="docEmptyState">Sélectionnez un élément du document pour voir ses propriétés.</div>';
|
||||
return;
|
||||
}
|
||||
if (el.kind === 'titre' || el.kind === 'paragraphe') forgeDocRenderTextProps(panel, el);
|
||||
else if (forgeDocIsShapeKind(el.kind)) forgeDocRenderShapeProps(panel, el);
|
||||
else if (el.kind === 'image') forgeDocRenderImageProps(panel, el);
|
||||
else if (el.kind === 'bouton') forgeDocRenderButtonProps(panel, el);
|
||||
else if (el.kind === 'row') forgeDocRenderRowProps(panel, el);
|
||||
else forgeDocRenderMinigameProps(panel, el);
|
||||
|
||||
const deleteBtn = document.getElementById('docDeletePropBtn');
|
||||
if (deleteBtn) deleteBtn.addEventListener('click', () => forgeDocDeleteElement(el.id));
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
* Chrome de l'éditeur : zoom, largeur d'aperçu responsive, Aperçu,
|
||||
* Publier, thème (scopé à .docEditor3), navigation mobile.
|
||||
* ------------------------------------------------------------------- */
|
||||
|
||||
function forgeDocBindZoom() {
|
||||
const docPage = document.getElementById('docPage');
|
||||
document.getElementById('docZoomOut').addEventListener('click', () => {
|
||||
window.forgeDocState.zoom = Math.max(50, window.forgeDocState.zoom - 10);
|
||||
document.getElementById('docZoomVal').textContent = `${window.forgeDocState.zoom}%`;
|
||||
docPage.style.transform = `scale(${window.forgeDocState.zoom / 100})`;
|
||||
});
|
||||
document.getElementById('docZoomIn').addEventListener('click', () => {
|
||||
window.forgeDocState.zoom = Math.min(150, window.forgeDocState.zoom + 10);
|
||||
document.getElementById('docZoomVal').textContent = `${window.forgeDocState.zoom}%`;
|
||||
docPage.style.transform = `scale(${window.forgeDocState.zoom / 100})`;
|
||||
});
|
||||
}
|
||||
|
||||
function forgeDocBindWidthPicker() {
|
||||
document.querySelectorAll('.docWidthBtn').forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
document.querySelectorAll('.docWidthBtn').forEach((b) => b.classList.remove('is-active'));
|
||||
btn.classList.add('is-active');
|
||||
const docPage = document.getElementById('docPage');
|
||||
const width = btn.dataset.width;
|
||||
if (!width) {
|
||||
docPage.classList.remove('docPage--fixedWidth');
|
||||
docPage.style.removeProperty('--doc-preview-width');
|
||||
} else {
|
||||
docPage.classList.add('docPage--fixedWidth');
|
||||
docPage.style.setProperty('--doc-preview-width', `${width}px`);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function forgeDocBindPreviewToggle() {
|
||||
document.getElementById('docPreviewBtn').addEventListener('click', () => {
|
||||
window.forgeDocState.previewMode = !window.forgeDocState.previewMode;
|
||||
document.getElementById('docEditor3').classList.toggle('docEditor3--preview', window.forgeDocState.previewMode);
|
||||
});
|
||||
}
|
||||
|
||||
function forgeDocBindPublish() {
|
||||
document.getElementById('docPublishBtn').addEventListener('click', () => {
|
||||
forgeDocApiPublish().then((result) => {
|
||||
if (!result.ok) return;
|
||||
const label = document.getElementById('docPublishLabel');
|
||||
const btn = document.getElementById('docPublishBtn');
|
||||
label.textContent = 'Publié';
|
||||
btn.classList.add('is-published');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function forgeDocApplyTheme(theme) {
|
||||
const editor = document.getElementById('docEditor3');
|
||||
editor.dataset.theme = theme;
|
||||
const toggle = document.getElementById('docThemeToggle');
|
||||
toggle.innerHTML = theme === 'light'
|
||||
? '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M20 14.5A8.5 8.5 0 1 1 9.5 4a7 7 0 0 0 10.5 10.5z"/></svg>'
|
||||
: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.2 4.2l1.4 1.4M18.4 18.4l1.4 1.4M2 12h2M20 12h2M4.2 19.8l1.4-1.4M18.4 5.6l1.4-1.4"/></svg>';
|
||||
}
|
||||
|
||||
function forgeDocBindThemeToggle() {
|
||||
let stored = 'dark';
|
||||
try {
|
||||
const saved = window.localStorage.getItem('forge-doc-editor-theme');
|
||||
if (saved === 'light' || saved === 'dark') stored = saved;
|
||||
} catch (e) { /* stockage indisponible, on reste sur le thème par défaut */ }
|
||||
forgeDocApplyTheme(stored);
|
||||
document.getElementById('docThemeToggle').addEventListener('click', () => {
|
||||
const next = document.getElementById('docEditor3').dataset.theme === 'light' ? 'dark' : 'light';
|
||||
forgeDocApplyTheme(next);
|
||||
try { window.localStorage.setItem('forge-doc-editor-theme', next); } catch (e) { /* ignore */ }
|
||||
});
|
||||
}
|
||||
|
||||
function forgeDocBindMobileNav() {
|
||||
const left = document.getElementById('docSidebarLeft');
|
||||
const right = document.getElementById('docSidebarRight');
|
||||
document.querySelectorAll('#docBottomNav button').forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
forgeDocSetActiveBottomNav(btn.dataset.target);
|
||||
left.classList.toggle('is-open', btn.dataset.target === 'left');
|
||||
right.classList.toggle('is-open', btn.dataset.target === 'right');
|
||||
});
|
||||
});
|
||||
function closePanels() {
|
||||
left.classList.remove('is-open');
|
||||
right.classList.remove('is-open');
|
||||
forgeDocSetActiveBottomNav('doc');
|
||||
}
|
||||
document.getElementById('docCloseLeft').addEventListener('click', closePanels);
|
||||
document.getElementById('docCloseRight').addEventListener('click', closePanels);
|
||||
}
|
||||
|
||||
function forgeDocBindDeselectOnBackgroundClick() {
|
||||
document.getElementById('docCanvasArea').addEventListener('click', (e) => {
|
||||
if (e.target.closest('[data-element-id]')) return;
|
||||
window.forgeDocState.selectedId = null;
|
||||
forgeDocApplySelectionClass();
|
||||
forgeDocRenderProps(null);
|
||||
});
|
||||
}
|
||||
|
||||
function forgeDocBindKeyboardShortcuts() {
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (!document.getElementById('docEditor3')) return;
|
||||
const isUndo = (e.ctrlKey || e.metaKey) && !e.shiftKey && e.key.toLowerCase() === 'z';
|
||||
const isRedo = ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key.toLowerCase() === 'z')
|
||||
|| ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'y');
|
||||
if (isUndo) { e.preventDefault(); forgeDocUndo(); }
|
||||
if (isRedo) { e.preventDefault(); forgeDocRedo(); }
|
||||
});
|
||||
}
|
||||
|
||||
function forgeDocInit() {
|
||||
const root = document.getElementById('docEditor3');
|
||||
if (!root || !window.FORGE_DOCUMENT) return;
|
||||
|
||||
window.forgeDocState = {
|
||||
slug: window.FORGE_DOCUMENT.slug,
|
||||
elements: window.FORGE_DOCUMENT.elements,
|
||||
elementsById: {},
|
||||
selectedId: null,
|
||||
undoStack: [],
|
||||
redoStack: [],
|
||||
previewMode: false,
|
||||
zoom: 100,
|
||||
shapeDrag: null,
|
||||
};
|
||||
forgeDocRebuildIndex();
|
||||
|
||||
forgeDocBindCanvasInteractions();
|
||||
forgeDocBindLibraryDragHandlers();
|
||||
forgeDocBindCanvasDropZone();
|
||||
forgeDocBindShapeDragGlobalHandlers();
|
||||
forgeDocBindDeselectOnBackgroundClick();
|
||||
forgeDocBindZoom();
|
||||
forgeDocBindWidthPicker();
|
||||
forgeDocBindPreviewToggle();
|
||||
forgeDocBindPublish();
|
||||
forgeDocBindThemeToggle();
|
||||
forgeDocBindMobileNav();
|
||||
forgeDocBindKeyboardShortcuts();
|
||||
forgeDocUpdateHistoryButtons();
|
||||
|
||||
document.getElementById('docUndoBtn').addEventListener('click', forgeDocUndo);
|
||||
document.getElementById('docRedoBtn').addEventListener('click', forgeDocRedo);
|
||||
|
||||
if (window.innerWidth <= 900) {
|
||||
document.getElementById('docSidebarLeft').classList.remove('is-open');
|
||||
document.getElementById('docSidebarRight').classList.remove('is-open');
|
||||
}
|
||||
}
|
||||
|
||||
forgeDocInit();
|
||||
Reference in New Issue
Block a user