living-documentation 3.4.0 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of living-documentation might be problematic. Click here for more details.
- package/dist/src/frontend/admin.html +10 -0
- package/dist/src/frontend/diagram/clipboard.js +58 -0
- package/dist/src/frontend/diagram/constants.js +32 -0
- package/dist/src/frontend/diagram/debug.js +43 -0
- package/dist/src/frontend/diagram/edge-panel.js +61 -0
- package/dist/src/frontend/diagram/edge-rendering.js +12 -0
- package/dist/src/frontend/diagram/grid.js +68 -0
- package/dist/src/frontend/diagram/label-editor.js +90 -0
- package/dist/src/frontend/diagram/main.js +158 -0
- package/dist/src/frontend/diagram/network.js +168 -0
- package/dist/src/frontend/diagram/node-panel.js +73 -0
- package/dist/src/frontend/diagram/node-rendering.js +113 -0
- package/dist/src/frontend/diagram/persistence.js +138 -0
- package/dist/src/frontend/diagram/selection-overlay.js +149 -0
- package/dist/src/frontend/diagram/state.js +29 -0
- package/dist/src/frontend/diagram/zoom.js +20 -0
- package/dist/src/frontend/diagram.html +746 -1051
- package/dist/src/lib/config.d.ts +1 -0
- package/dist/src/lib/config.d.ts.map +1 -1
- package/dist/src/lib/config.js +1 -0
- package/dist/src/lib/config.js.map +1 -1
- package/dist/src/routes/config.d.ts.map +1 -1
- package/dist/src/routes/config.js +1 -0
- package/dist/src/routes/config.js.map +1 -1
- package/package.json +1 -1
|
@@ -5,1179 +5,874 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>Diagram — Living Documentation</title>
|
|
7
7
|
<script src="https://cdn.tailwindcss.com?plugins=typography"></script>
|
|
8
|
-
<script>
|
|
8
|
+
<script>
|
|
9
|
+
tailwind.config = { darkMode: "class", theme: { extend: {} } };
|
|
10
|
+
</script>
|
|
9
11
|
<script src="https://unpkg.com/vis-network@9.1.9/standalone/umd/vis-network.min.js"></script>
|
|
10
12
|
<style>
|
|
11
|
-
#vis-canvas > div {
|
|
13
|
+
#vis-canvas > div {
|
|
14
|
+
border: none !important;
|
|
15
|
+
}
|
|
12
16
|
.tool-btn {
|
|
13
|
-
display: flex;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
display: flex;
|
|
18
|
+
align-items: center;
|
|
19
|
+
justify-content: center;
|
|
20
|
+
width: 2rem;
|
|
21
|
+
height: 2rem;
|
|
22
|
+
border-radius: 0.5rem;
|
|
23
|
+
font-size: 0.875rem;
|
|
24
|
+
cursor: pointer;
|
|
25
|
+
flex-shrink: 0;
|
|
26
|
+
transition:
|
|
27
|
+
background-color 0.15s,
|
|
28
|
+
color 0.15s;
|
|
29
|
+
color: #4b5563;
|
|
30
|
+
background: none;
|
|
31
|
+
border: none;
|
|
32
|
+
}
|
|
33
|
+
.dark .tool-btn {
|
|
34
|
+
color: #9ca3af;
|
|
35
|
+
}
|
|
36
|
+
.tool-btn:hover {
|
|
37
|
+
background: #f3f4f6;
|
|
38
|
+
}
|
|
39
|
+
.dark .tool-btn:hover {
|
|
40
|
+
background: #1f2937;
|
|
41
|
+
}
|
|
42
|
+
.tool-active {
|
|
43
|
+
background: #fff7ed !important;
|
|
44
|
+
color: #ea580c !important;
|
|
45
|
+
}
|
|
46
|
+
.dark .tool-active {
|
|
47
|
+
background: rgba(124, 45, 18, 0.3) !important;
|
|
48
|
+
color: #fb923c !important;
|
|
49
|
+
}
|
|
50
|
+
#vis-canvas.cursor-crosshair canvas {
|
|
51
|
+
cursor: crosshair !important;
|
|
18
52
|
}
|
|
19
|
-
.dark .tool-btn { color: #9ca3af; }
|
|
20
|
-
.tool-btn:hover { background: #f3f4f6; }
|
|
21
|
-
.dark .tool-btn:hover { background: #1f2937; }
|
|
22
|
-
.tool-active { background: #fff7ed !important; color: #ea580c !important; }
|
|
23
|
-
.dark .tool-active { background: rgba(124,45,18,0.3) !important; color: #fb923c !important; }
|
|
24
|
-
#vis-canvas.cursor-crosshair canvas { cursor: crosshair !important; }
|
|
25
53
|
|
|
26
54
|
/* Floating panels */
|
|
27
55
|
.float-panel {
|
|
28
|
-
position: absolute;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
56
|
+
position: absolute;
|
|
57
|
+
top: 0.75rem;
|
|
58
|
+
left: 50%;
|
|
59
|
+
transform: translateX(-50%);
|
|
60
|
+
display: flex;
|
|
61
|
+
align-items: center;
|
|
62
|
+
gap: 0.25rem;
|
|
63
|
+
padding: 0.375rem 0.5rem;
|
|
64
|
+
background: white;
|
|
65
|
+
border: 1px solid #e5e7eb;
|
|
66
|
+
border-radius: 0.5rem;
|
|
67
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
68
|
+
z-index: 10;
|
|
69
|
+
}
|
|
70
|
+
.dark .float-panel {
|
|
71
|
+
background: #1f2937;
|
|
72
|
+
border-color: #374151;
|
|
73
|
+
}
|
|
74
|
+
.float-panel.hidden {
|
|
75
|
+
display: none !important;
|
|
76
|
+
}
|
|
77
|
+
.panel-sep {
|
|
78
|
+
width: 1px;
|
|
79
|
+
height: 1rem;
|
|
80
|
+
background: #e5e7eb;
|
|
81
|
+
margin: 0 0.125rem;
|
|
82
|
+
flex-shrink: 0;
|
|
83
|
+
}
|
|
84
|
+
.dark .panel-sep {
|
|
85
|
+
background: #374151;
|
|
86
|
+
}
|
|
87
|
+
.edge-btn-active {
|
|
88
|
+
background: #fff7ed !important;
|
|
89
|
+
color: #ea580c !important;
|
|
90
|
+
}
|
|
91
|
+
.dark .edge-btn-active {
|
|
92
|
+
background: rgba(124, 45, 18, 0.3) !important;
|
|
93
|
+
color: #fb923c !important;
|
|
32
94
|
}
|
|
33
|
-
.dark .float-panel { background: #1f2937; border-color: #374151; }
|
|
34
|
-
.float-panel.hidden { display: none !important; }
|
|
35
|
-
.panel-sep { width:1px; height:1rem; background:#e5e7eb; margin:0 .125rem; flex-shrink:0; }
|
|
36
|
-
.dark .panel-sep { background:#374151; }
|
|
37
|
-
.edge-btn-active { background:#fff7ed!important; color:#ea580c!important; }
|
|
38
|
-
.dark .edge-btn-active { background:rgba(124,45,18,.3)!important; color:#fb923c!important; }
|
|
39
95
|
|
|
40
96
|
/* Floating label textarea */
|
|
41
97
|
#labelInput {
|
|
42
|
-
position: absolute;
|
|
43
|
-
|
|
44
|
-
|
|
98
|
+
position: absolute;
|
|
99
|
+
z-index: 20;
|
|
100
|
+
padding: 4px 8px;
|
|
101
|
+
font-size: 13px;
|
|
102
|
+
font-family:
|
|
103
|
+
system-ui,
|
|
104
|
+
-apple-system,
|
|
105
|
+
sans-serif;
|
|
106
|
+
font-weight: 500;
|
|
45
107
|
text-align: center;
|
|
46
|
-
background: rgba(255,255,255
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
108
|
+
background: rgba(255, 255, 255, 0.95);
|
|
109
|
+
color: #1f2937;
|
|
110
|
+
border: 2px solid #3b82f6;
|
|
111
|
+
border-radius: 0.5rem;
|
|
112
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
|
113
|
+
outline: none;
|
|
114
|
+
resize: none;
|
|
115
|
+
overflow: hidden;
|
|
116
|
+
line-height: 1.4;
|
|
117
|
+
min-width: 80px;
|
|
118
|
+
min-height: 28px;
|
|
119
|
+
}
|
|
120
|
+
.dark #labelInput {
|
|
121
|
+
background: rgba(17, 24, 39, 0.95);
|
|
122
|
+
color: #f3f4f6;
|
|
123
|
+
}
|
|
124
|
+
#labelInput.hidden {
|
|
125
|
+
display: none;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/* Debug overlay layer */
|
|
129
|
+
#debugLayer {
|
|
130
|
+
position: absolute;
|
|
131
|
+
inset: 0;
|
|
132
|
+
pointer-events: none;
|
|
133
|
+
z-index: 12;
|
|
134
|
+
overflow: hidden;
|
|
135
|
+
}
|
|
136
|
+
.debug-box {
|
|
137
|
+
position: absolute;
|
|
138
|
+
pointer-events: auto;
|
|
139
|
+
font: 10px/1.4 monospace;
|
|
140
|
+
white-space: pre;
|
|
141
|
+
user-select: text;
|
|
142
|
+
cursor: text;
|
|
143
|
+
padding: 3px 6px;
|
|
144
|
+
border: 1px solid #f97316;
|
|
145
|
+
border-radius: 3px;
|
|
146
|
+
color: #c2410c;
|
|
147
|
+
background: rgba(255, 255, 255, 0.9);
|
|
148
|
+
}
|
|
149
|
+
.dark .debug-box {
|
|
150
|
+
color: #fbbf24;
|
|
151
|
+
background: rgba(0, 0, 0, 0.78);
|
|
51
152
|
}
|
|
52
|
-
.dark #labelInput { background: rgba(17,24,39,.95); color: #f3f4f6; }
|
|
53
|
-
#labelInput.hidden { display: none; }
|
|
54
153
|
|
|
55
154
|
/* Resize / selection overlay */
|
|
56
155
|
#selectionOverlay {
|
|
57
|
-
position: absolute;
|
|
58
|
-
|
|
59
|
-
|
|
156
|
+
position: absolute;
|
|
157
|
+
display: none;
|
|
158
|
+
border: 2px dashed #f97316;
|
|
159
|
+
border-radius: 3px;
|
|
160
|
+
pointer-events: none;
|
|
161
|
+
z-index: 15;
|
|
162
|
+
box-sizing: border-box;
|
|
60
163
|
}
|
|
61
164
|
.resize-handle {
|
|
62
|
-
position: absolute;
|
|
63
|
-
|
|
64
|
-
|
|
165
|
+
position: absolute;
|
|
166
|
+
width: 10px;
|
|
167
|
+
height: 10px;
|
|
168
|
+
background: white;
|
|
169
|
+
border: 2px solid #f97316;
|
|
170
|
+
border-radius: 2px;
|
|
171
|
+
pointer-events: all;
|
|
172
|
+
z-index: 1;
|
|
173
|
+
}
|
|
174
|
+
.dark .resize-handle {
|
|
175
|
+
background: #374151;
|
|
65
176
|
}
|
|
66
|
-
.dark .resize-handle { background: #374151; }
|
|
67
177
|
</style>
|
|
68
178
|
</head>
|
|
69
|
-
<body
|
|
70
|
-
|
|
179
|
+
<body
|
|
180
|
+
class="bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 flex flex-col h-screen overflow-hidden"
|
|
181
|
+
>
|
|
71
182
|
<script>
|
|
72
|
-
const _d = localStorage.getItem(
|
|
73
|
-
document.documentElement.classList.toggle(
|
|
183
|
+
const _d = localStorage.getItem("ld-dark") === "true";
|
|
184
|
+
document.documentElement.classList.toggle("dark", _d);
|
|
74
185
|
</script>
|
|
75
186
|
|
|
76
187
|
<!-- ── Top bar ── -->
|
|
77
|
-
<header
|
|
78
|
-
|
|
188
|
+
<header
|
|
189
|
+
class="flex items-center gap-1 px-2 h-12 shrink-0 border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 shadow-sm z-10"
|
|
190
|
+
>
|
|
191
|
+
<a
|
|
192
|
+
href="/"
|
|
193
|
+
class="tool-btn !w-auto px-2 text-xs font-medium text-gray-500 dark:text-gray-400"
|
|
194
|
+
>← Docs</a
|
|
195
|
+
>
|
|
79
196
|
<div class="w-px h-6 bg-gray-200 dark:bg-gray-700 mx-0.5"></div>
|
|
80
|
-
<button
|
|
197
|
+
<button id="btnSidebar" class="tool-btn" title="Mes diagrammes">
|
|
198
|
+
☰
|
|
199
|
+
</button>
|
|
81
200
|
<div class="w-px h-6 bg-gray-200 dark:bg-gray-700 mx-0.5"></div>
|
|
82
201
|
|
|
83
|
-
<button
|
|
84
|
-
|
|
202
|
+
<button
|
|
203
|
+
id="toolSelect"
|
|
204
|
+
class="tool-btn tool-active"
|
|
205
|
+
title="Sélectionner (S)"
|
|
206
|
+
>
|
|
207
|
+
<svg
|
|
208
|
+
width="11"
|
|
209
|
+
height="13"
|
|
210
|
+
viewBox="0 0 11 13"
|
|
211
|
+
fill="currentColor"
|
|
212
|
+
stroke="none"
|
|
213
|
+
>
|
|
214
|
+
<path d="M1 1 L1 12 L4 9 L6.5 13 L8 12.2 L5.5 8.2 L10 8.2 Z" />
|
|
215
|
+
</svg>
|
|
85
216
|
</button>
|
|
86
217
|
<div class="w-px h-6 bg-gray-200 dark:bg-gray-700 mx-0.5"></div>
|
|
87
218
|
|
|
88
|
-
<button
|
|
89
|
-
|
|
219
|
+
<button
|
|
220
|
+
id="toolBox"
|
|
221
|
+
class="tool-btn"
|
|
222
|
+
title="Rectangle (R)"
|
|
223
|
+
>
|
|
224
|
+
<svg
|
|
225
|
+
width="15"
|
|
226
|
+
height="10"
|
|
227
|
+
viewBox="0 0 15 10"
|
|
228
|
+
fill="none"
|
|
229
|
+
stroke="currentColor"
|
|
230
|
+
stroke-width="1.5"
|
|
231
|
+
>
|
|
232
|
+
<rect x="1" y="1" width="13" height="8" rx="1" />
|
|
233
|
+
</svg>
|
|
90
234
|
</button>
|
|
91
|
-
<button
|
|
92
|
-
|
|
235
|
+
<button
|
|
236
|
+
id="toolEllipse"
|
|
237
|
+
class="tool-btn"
|
|
238
|
+
title="Ellipse (E)"
|
|
239
|
+
>
|
|
240
|
+
<svg
|
|
241
|
+
width="16"
|
|
242
|
+
height="10"
|
|
243
|
+
viewBox="0 0 16 10"
|
|
244
|
+
fill="none"
|
|
245
|
+
stroke="currentColor"
|
|
246
|
+
stroke-width="1.5"
|
|
247
|
+
>
|
|
248
|
+
<ellipse cx="8" cy="5" rx="7" ry="4" />
|
|
249
|
+
</svg>
|
|
93
250
|
</button>
|
|
94
|
-
<button
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
251
|
+
<button
|
|
252
|
+
id="toolDatabase"
|
|
253
|
+
class="tool-btn"
|
|
254
|
+
title="Base de données (D)"
|
|
255
|
+
>
|
|
256
|
+
<svg
|
|
257
|
+
width="12"
|
|
258
|
+
height="16"
|
|
259
|
+
viewBox="0 0 12 16"
|
|
260
|
+
fill="none"
|
|
261
|
+
stroke="currentColor"
|
|
262
|
+
stroke-width="1.5"
|
|
263
|
+
stroke-linecap="round"
|
|
264
|
+
>
|
|
265
|
+
<ellipse cx="6" cy="3" rx="5" ry="2" />
|
|
266
|
+
<path d="M1 3v10c0 1.1 2.2 2 5 2s5-.9 5-2V3" />
|
|
267
|
+
<path d="M11 7.5c0 1.1-2.2 2-5 2s-5-.9-5-2" />
|
|
99
268
|
</svg>
|
|
100
269
|
</button>
|
|
101
|
-
<button
|
|
102
|
-
|
|
270
|
+
<button
|
|
271
|
+
id="toolCircle"
|
|
272
|
+
class="tool-btn"
|
|
273
|
+
title="Cercle (C)"
|
|
274
|
+
>
|
|
275
|
+
<svg
|
|
276
|
+
width="13"
|
|
277
|
+
height="13"
|
|
278
|
+
viewBox="0 0 13 13"
|
|
279
|
+
fill="none"
|
|
280
|
+
stroke="currentColor"
|
|
281
|
+
stroke-width="1.5"
|
|
282
|
+
>
|
|
283
|
+
<circle cx="6.5" cy="6.5" r="5.5" />
|
|
284
|
+
</svg>
|
|
103
285
|
</button>
|
|
104
|
-
<button
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
286
|
+
<button
|
|
287
|
+
id="toolActor"
|
|
288
|
+
class="tool-btn"
|
|
289
|
+
title="Acteur (A)"
|
|
290
|
+
>
|
|
291
|
+
<svg
|
|
292
|
+
width="12"
|
|
293
|
+
height="17"
|
|
294
|
+
viewBox="0 0 12 17"
|
|
295
|
+
fill="none"
|
|
296
|
+
stroke="currentColor"
|
|
297
|
+
stroke-width="1.5"
|
|
298
|
+
stroke-linecap="round"
|
|
299
|
+
>
|
|
300
|
+
<circle cx="6" cy="3" r="2.2" />
|
|
301
|
+
<line x1="6" y1="5.2" x2="6" y2="10" />
|
|
302
|
+
<line x1="1.5" y1="7.5" x2="10.5" y2="7.5" />
|
|
303
|
+
<line x1="6" y1="10" x2="2.5" y2="15" />
|
|
304
|
+
<line x1="6" y1="10" x2="9.5" y2="15" />
|
|
111
305
|
</svg>
|
|
112
306
|
</button>
|
|
113
307
|
<div class="w-px h-6 bg-gray-200 dark:bg-gray-700 mx-0.5"></div>
|
|
114
308
|
|
|
115
|
-
<button
|
|
309
|
+
<button
|
|
310
|
+
id="toolArrow"
|
|
311
|
+
class="tool-btn"
|
|
312
|
+
title="Flèche (F)"
|
|
313
|
+
>
|
|
314
|
+
→
|
|
315
|
+
</button>
|
|
116
316
|
<div class="w-px h-6 bg-gray-200 dark:bg-gray-700 mx-0.5"></div>
|
|
117
317
|
|
|
118
|
-
<button
|
|
119
|
-
|
|
120
|
-
|
|
318
|
+
<button
|
|
319
|
+
id="btnDelete"
|
|
320
|
+
class="tool-btn"
|
|
321
|
+
title="Supprimer (Suppr)"
|
|
322
|
+
>
|
|
323
|
+
<svg
|
|
324
|
+
width="11"
|
|
325
|
+
height="11"
|
|
326
|
+
viewBox="0 0 11 11"
|
|
327
|
+
fill="none"
|
|
328
|
+
stroke="currentColor"
|
|
329
|
+
stroke-width="1.8"
|
|
330
|
+
stroke-linecap="round"
|
|
331
|
+
>
|
|
332
|
+
<line x1="1" y1="1" x2="10" y2="10" />
|
|
333
|
+
<line x1="10" y1="1" x2="1" y2="10" />
|
|
121
334
|
</svg>
|
|
122
335
|
</button>
|
|
123
336
|
<div class="w-px h-6 bg-gray-200 dark:bg-gray-700 mx-0.5"></div>
|
|
124
337
|
|
|
125
|
-
<button
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
338
|
+
<button
|
|
339
|
+
id="btnPhysics"
|
|
340
|
+
class="tool-btn"
|
|
341
|
+
title="Auto-espacement actif"
|
|
342
|
+
>
|
|
343
|
+
<svg
|
|
344
|
+
width="14"
|
|
345
|
+
height="14"
|
|
346
|
+
viewBox="0 0 14 14"
|
|
347
|
+
fill="none"
|
|
348
|
+
stroke="currentColor"
|
|
349
|
+
stroke-width="1.4"
|
|
350
|
+
>
|
|
351
|
+
<circle cx="7" cy="7" r="2" />
|
|
352
|
+
<path
|
|
353
|
+
d="M7 1.5v2M7 10.5v2M1.5 7h2M10.5 7h2M3.4 3.4l1.4 1.4M9.2 9.2l1.4 1.4M10.6 3.4l-1.4 1.4M4.8 9.2l-1.4 1.4"
|
|
354
|
+
/>
|
|
129
355
|
</svg>
|
|
130
356
|
</button>
|
|
131
|
-
<button
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
357
|
+
<button
|
|
358
|
+
id="btnGrid"
|
|
359
|
+
class="tool-btn"
|
|
360
|
+
title="Grille / Snap to grid (G)"
|
|
361
|
+
>
|
|
362
|
+
<svg
|
|
363
|
+
width="14"
|
|
364
|
+
height="14"
|
|
365
|
+
viewBox="0 0 14 14"
|
|
366
|
+
fill="none"
|
|
367
|
+
stroke="currentColor"
|
|
368
|
+
stroke-width="1.2"
|
|
369
|
+
>
|
|
370
|
+
<line x1="5" y1="1" x2="5" y2="13" />
|
|
371
|
+
<line x1="9" y1="1" x2="9" y2="13" />
|
|
372
|
+
<line x1="1" y1="5" x2="13" y2="5" />
|
|
373
|
+
<line x1="1" y1="9" x2="13" y2="9" />
|
|
374
|
+
<rect x="1" y="1" width="12" height="12" rx="1" />
|
|
136
375
|
</svg>
|
|
137
376
|
</button>
|
|
138
377
|
<div class="w-px h-6 bg-gray-200 dark:bg-gray-700 mx-0.5"></div>
|
|
139
378
|
|
|
140
|
-
<input
|
|
141
|
-
|
|
379
|
+
<input
|
|
380
|
+
id="diagramTitle"
|
|
381
|
+
type="text"
|
|
382
|
+
placeholder="Titre du diagramme"
|
|
383
|
+
class="flex-1 min-w-0 px-2 py-1 text-sm bg-transparent border-0 focus:outline-none focus:ring-1 focus:ring-blue-500 rounded text-gray-700 dark:text-gray-300 placeholder:text-gray-400"
|
|
384
|
+
/>
|
|
142
385
|
<div class="w-px h-6 bg-gray-200 dark:bg-gray-700 mx-0.5"></div>
|
|
143
386
|
|
|
144
|
-
<button
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
<
|
|
387
|
+
<button id="btnZoomOut" class="tool-btn" title="Zoom −">
|
|
388
|
+
−
|
|
389
|
+
</button>
|
|
390
|
+
<span
|
|
391
|
+
id="zoomLevel"
|
|
392
|
+
class="text-xs text-gray-500 dark:text-gray-400 w-10 text-center tabular-nums select-none"
|
|
393
|
+
>100%</span
|
|
394
|
+
>
|
|
395
|
+
<button id="btnZoomIn" class="tool-btn" title="Zoom +">
|
|
396
|
+
+
|
|
397
|
+
</button>
|
|
398
|
+
<button id="btnZoomReset" class="tool-btn" title="Ajuster la vue">
|
|
399
|
+
⊡
|
|
400
|
+
</button>
|
|
148
401
|
<div class="w-px h-6 bg-gray-200 dark:bg-gray-700 mx-0.5"></div>
|
|
149
402
|
|
|
150
|
-
<button
|
|
403
|
+
<button id="btnDark" class="tool-btn">
|
|
404
|
+
<span id="darkIcon">☽</span>
|
|
405
|
+
</button>
|
|
151
406
|
<div class="w-px h-6 bg-gray-200 dark:bg-gray-700 mx-0.5"></div>
|
|
152
407
|
|
|
153
|
-
<button
|
|
154
|
-
|
|
408
|
+
<button
|
|
409
|
+
id="btnDebug"
|
|
410
|
+
class="hidden tool-btn text-xs font-mono"
|
|
411
|
+
title="Debug overlay"
|
|
412
|
+
>
|
|
413
|
+
dbg
|
|
414
|
+
</button>
|
|
415
|
+
<div
|
|
416
|
+
id="sepDebug"
|
|
417
|
+
class="hidden w-px h-6 bg-gray-200 dark:bg-gray-700 mx-0.5"
|
|
418
|
+
></div>
|
|
419
|
+
|
|
420
|
+
<button
|
|
421
|
+
id="btnSave"
|
|
422
|
+
disabled
|
|
423
|
+
class="px-3 py-1.5 text-xs font-semibold rounded-lg bg-blue-600 hover:bg-blue-700 text-white disabled:opacity-40 disabled:cursor-not-allowed shrink-0 transition-colors"
|
|
424
|
+
>
|
|
155
425
|
Enregistrer
|
|
156
426
|
</button>
|
|
157
427
|
</header>
|
|
158
428
|
|
|
159
429
|
<!-- ── Body ── -->
|
|
160
430
|
<div class="flex flex-1 overflow-hidden">
|
|
161
|
-
|
|
162
431
|
<!-- Sidebar -->
|
|
163
|
-
<div
|
|
432
|
+
<div
|
|
433
|
+
id="sidebar"
|
|
164
434
|
class="w-56 shrink-0 border-r border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 flex flex-col overflow-hidden"
|
|
165
|
-
style="transition: width 0.2s ease
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
435
|
+
style="transition: width 0.2s ease"
|
|
436
|
+
>
|
|
437
|
+
<div
|
|
438
|
+
class="flex items-center justify-between px-3 py-2 border-b border-gray-200 dark:border-gray-700 shrink-0"
|
|
439
|
+
>
|
|
440
|
+
<span
|
|
441
|
+
class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider"
|
|
442
|
+
>Diagrammes</span
|
|
443
|
+
>
|
|
444
|
+
<button
|
|
445
|
+
id="btnNewDiagram"
|
|
446
|
+
class="text-xs font-medium text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 transition-colors"
|
|
447
|
+
>
|
|
448
|
+
+ Nouveau
|
|
449
|
+
</button>
|
|
169
450
|
</div>
|
|
170
451
|
<div id="diagramList" class="flex-1 overflow-y-auto py-1"></div>
|
|
171
452
|
</div>
|
|
172
453
|
|
|
173
454
|
<!-- Canvas area -->
|
|
174
455
|
<div class="relative flex-1 overflow-hidden bg-gray-50 dark:bg-gray-950">
|
|
175
|
-
|
|
176
456
|
<div id="vis-canvas" class="w-full h-full"></div>
|
|
177
457
|
|
|
458
|
+
<!-- Debug overlay layer -->
|
|
459
|
+
<div id="debugLayer"></div>
|
|
460
|
+
|
|
178
461
|
<!-- Selection / resize overlay -->
|
|
179
462
|
<div id="selectionOverlay">
|
|
180
|
-
<div
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
463
|
+
<div
|
|
464
|
+
id="rh-tl"
|
|
465
|
+
class="resize-handle"
|
|
466
|
+
style="top: -5px; left: -5px; cursor: nw-resize"
|
|
467
|
+
></div>
|
|
468
|
+
<div
|
|
469
|
+
id="rh-tr"
|
|
470
|
+
class="resize-handle"
|
|
471
|
+
style="top: -5px; right: -5px; cursor: ne-resize"
|
|
472
|
+
></div>
|
|
473
|
+
<div
|
|
474
|
+
id="rh-bl"
|
|
475
|
+
class="resize-handle"
|
|
476
|
+
style="bottom: -5px; left: -5px; cursor: sw-resize"
|
|
477
|
+
></div>
|
|
478
|
+
<div
|
|
479
|
+
id="rh-br"
|
|
480
|
+
class="resize-handle"
|
|
481
|
+
style="bottom: -5px; right: -5px; cursor: se-resize"
|
|
482
|
+
></div>
|
|
184
483
|
</div>
|
|
185
484
|
|
|
186
485
|
<!-- Node panel -->
|
|
187
486
|
<div id="nodePanel" class="float-panel hidden">
|
|
188
|
-
<button
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
<button
|
|
194
|
-
|
|
487
|
+
<button
|
|
488
|
+
data-color="c-gray"
|
|
489
|
+
class="w-5 h-5 rounded-full border-2 border-stone-400 bg-stone-200 hover:scale-125 transition-transform"
|
|
490
|
+
title="Gris"
|
|
491
|
+
></button>
|
|
492
|
+
<button
|
|
493
|
+
data-color="c-slate"
|
|
494
|
+
class="w-5 h-5 rounded-full border-2 border-slate-500 bg-slate-100 hover:scale-125 transition-transform"
|
|
495
|
+
title="Ardoise"
|
|
496
|
+
></button>
|
|
497
|
+
<button
|
|
498
|
+
data-color="c-blue"
|
|
499
|
+
class="w-5 h-5 rounded-full border-2 border-blue-500 bg-blue-200 hover:scale-125 transition-transform"
|
|
500
|
+
title="Bleu"
|
|
501
|
+
></button>
|
|
502
|
+
<button
|
|
503
|
+
data-color="c-sky"
|
|
504
|
+
class="w-5 h-5 rounded-full border-2 border-sky-500 bg-sky-200 hover:scale-125 transition-transform"
|
|
505
|
+
title="Ciel"
|
|
506
|
+
></button>
|
|
507
|
+
<button
|
|
508
|
+
data-color="c-cyan"
|
|
509
|
+
class="w-5 h-5 rounded-full border-2 border-cyan-500 bg-cyan-200 hover:scale-125 transition-transform"
|
|
510
|
+
title="Cyan"
|
|
511
|
+
></button>
|
|
512
|
+
<button
|
|
513
|
+
data-color="c-teal"
|
|
514
|
+
class="w-5 h-5 rounded-full border-2 border-teal-500 bg-teal-200 hover:scale-125 transition-transform"
|
|
515
|
+
title="Teal"
|
|
516
|
+
></button>
|
|
517
|
+
<button
|
|
518
|
+
data-color="c-green"
|
|
519
|
+
class="w-5 h-5 rounded-full border-2 border-green-500 bg-green-200 hover:scale-125 transition-transform"
|
|
520
|
+
title="Vert"
|
|
521
|
+
></button>
|
|
522
|
+
<button
|
|
523
|
+
data-color="c-lime"
|
|
524
|
+
class="w-5 h-5 rounded-full border-2 border-lime-500 bg-lime-200 hover:scale-125 transition-transform"
|
|
525
|
+
title="Citron"
|
|
526
|
+
></button>
|
|
527
|
+
<button
|
|
528
|
+
data-color="c-amber"
|
|
529
|
+
class="w-5 h-5 rounded-full border-2 border-amber-500 bg-amber-200 hover:scale-125 transition-transform"
|
|
530
|
+
title="Ambre"
|
|
531
|
+
></button>
|
|
532
|
+
<button
|
|
533
|
+
data-color="c-orange"
|
|
534
|
+
class="w-5 h-5 rounded-full border-2 border-orange-500 bg-orange-200 hover:scale-125 transition-transform"
|
|
535
|
+
title="Orange"
|
|
536
|
+
></button>
|
|
537
|
+
<button
|
|
538
|
+
data-color="c-red"
|
|
539
|
+
class="w-5 h-5 rounded-full border-2 border-red-500 bg-red-200 hover:scale-125 transition-transform"
|
|
540
|
+
title="Rouge"
|
|
541
|
+
></button>
|
|
542
|
+
<button
|
|
543
|
+
data-color="c-rose"
|
|
544
|
+
class="w-5 h-5 rounded-full border-2 border-rose-500 bg-rose-200 hover:scale-125 transition-transform"
|
|
545
|
+
title="Rose"
|
|
546
|
+
></button>
|
|
547
|
+
<button
|
|
548
|
+
data-color="c-pink"
|
|
549
|
+
class="w-5 h-5 rounded-full border-2 border-pink-500 bg-pink-200 hover:scale-125 transition-transform"
|
|
550
|
+
title="Fuschia"
|
|
551
|
+
></button>
|
|
552
|
+
<button
|
|
553
|
+
data-color="c-purple"
|
|
554
|
+
class="w-5 h-5 rounded-full border-2 border-purple-500 bg-purple-200 hover:scale-125 transition-transform"
|
|
555
|
+
title="Violet"
|
|
556
|
+
></button>
|
|
557
|
+
<button
|
|
558
|
+
data-color="c-indigo"
|
|
559
|
+
class="w-5 h-5 rounded-full border-2 border-indigo-500 bg-indigo-200 hover:scale-125 transition-transform"
|
|
560
|
+
title="Indigo"
|
|
561
|
+
></button>
|
|
195
562
|
<div class="panel-sep"></div>
|
|
196
|
-
<button
|
|
563
|
+
<button
|
|
564
|
+
id="btnNodeLabelEdit"
|
|
565
|
+
class="tool-btn !w-6 !h-6"
|
|
566
|
+
title="Modifier le texte (double-clic)"
|
|
567
|
+
>
|
|
568
|
+
✎
|
|
569
|
+
</button>
|
|
197
570
|
<div class="panel-sep"></div>
|
|
198
|
-
<button
|
|
199
|
-
|
|
571
|
+
<button
|
|
572
|
+
id="btnNodeFontDecrease"
|
|
573
|
+
class="tool-btn !w-8 !h-6"
|
|
574
|
+
style="font-size: 10px"
|
|
575
|
+
title="Réduire la police"
|
|
576
|
+
>
|
|
577
|
+
Aa−
|
|
578
|
+
</button>
|
|
579
|
+
<button
|
|
580
|
+
id="btnNodeFontIncrease"
|
|
581
|
+
class="tool-btn !w-8 !h-6"
|
|
582
|
+
style="font-size: 10px"
|
|
583
|
+
title="Agrandir la police"
|
|
584
|
+
>
|
|
585
|
+
Aa+
|
|
586
|
+
</button>
|
|
200
587
|
<div class="panel-sep"></div>
|
|
201
588
|
<!-- Horizontal align -->
|
|
202
|
-
<button
|
|
203
|
-
|
|
204
|
-
|
|
589
|
+
<button
|
|
590
|
+
id="btnAlignLeft"
|
|
591
|
+
class="tool-btn !w-6 !h-6"
|
|
592
|
+
title="Aligner à gauche"
|
|
593
|
+
>
|
|
594
|
+
<svg
|
|
595
|
+
width="12"
|
|
596
|
+
height="10"
|
|
597
|
+
viewBox="0 0 12 10"
|
|
598
|
+
fill="none"
|
|
599
|
+
stroke="currentColor"
|
|
600
|
+
stroke-width="1.4"
|
|
601
|
+
stroke-linecap="round"
|
|
602
|
+
>
|
|
603
|
+
<line x1="1" y1="2" x2="11" y2="2" />
|
|
604
|
+
<line x1="1" y1="5" x2="7" y2="5" />
|
|
605
|
+
<line x1="1" y1="8" x2="9" y2="8" />
|
|
205
606
|
</svg>
|
|
206
607
|
</button>
|
|
207
|
-
<button
|
|
208
|
-
|
|
209
|
-
|
|
608
|
+
<button
|
|
609
|
+
id="btnAlignCenter"
|
|
610
|
+
class="tool-btn !w-6 !h-6"
|
|
611
|
+
title="Centrer horizontalement"
|
|
612
|
+
>
|
|
613
|
+
<svg
|
|
614
|
+
width="12"
|
|
615
|
+
height="10"
|
|
616
|
+
viewBox="0 0 12 10"
|
|
617
|
+
fill="none"
|
|
618
|
+
stroke="currentColor"
|
|
619
|
+
stroke-width="1.4"
|
|
620
|
+
stroke-linecap="round"
|
|
621
|
+
>
|
|
622
|
+
<line x1="1" y1="2" x2="11" y2="2" />
|
|
623
|
+
<line x1="2.5" y1="5" x2="9.5" y2="5" />
|
|
624
|
+
<line x1="1.5" y1="8" x2="10.5" y2="8" />
|
|
210
625
|
</svg>
|
|
211
626
|
</button>
|
|
212
|
-
<button
|
|
213
|
-
|
|
214
|
-
|
|
627
|
+
<button
|
|
628
|
+
id="btnAlignRight"
|
|
629
|
+
class="tool-btn !w-6 !h-6"
|
|
630
|
+
title="Aligner à droite"
|
|
631
|
+
>
|
|
632
|
+
<svg
|
|
633
|
+
width="12"
|
|
634
|
+
height="10"
|
|
635
|
+
viewBox="0 0 12 10"
|
|
636
|
+
fill="none"
|
|
637
|
+
stroke="currentColor"
|
|
638
|
+
stroke-width="1.4"
|
|
639
|
+
stroke-linecap="round"
|
|
640
|
+
>
|
|
641
|
+
<line x1="1" y1="2" x2="11" y2="2" />
|
|
642
|
+
<line x1="5" y1="5" x2="11" y2="5" />
|
|
643
|
+
<line x1="3" y1="8" x2="11" y2="8" />
|
|
215
644
|
</svg>
|
|
216
645
|
</button>
|
|
217
646
|
<div class="panel-sep"></div>
|
|
218
647
|
<!-- Vertical align -->
|
|
219
|
-
<button
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
648
|
+
<button
|
|
649
|
+
id="btnValignTop"
|
|
650
|
+
class="tool-btn !w-6 !h-6"
|
|
651
|
+
title="Aligner en haut"
|
|
652
|
+
>
|
|
653
|
+
<svg
|
|
654
|
+
width="10"
|
|
655
|
+
height="12"
|
|
656
|
+
viewBox="0 0 10 12"
|
|
657
|
+
fill="none"
|
|
658
|
+
stroke="currentColor"
|
|
659
|
+
stroke-linecap="round"
|
|
660
|
+
>
|
|
661
|
+
<line x1="1" y1="1.5" x2="9" y2="1.5" stroke-width="2" />
|
|
662
|
+
<line x1="1" y1="5" x2="8" y2="5" stroke-width="1.3" />
|
|
663
|
+
<line x1="1" y1="8.5" x2="6" y2="8.5" stroke-width="1.3" />
|
|
224
664
|
</svg>
|
|
225
665
|
</button>
|
|
226
|
-
<button
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
666
|
+
<button
|
|
667
|
+
id="btnValignMiddle"
|
|
668
|
+
class="tool-btn !w-6 !h-6"
|
|
669
|
+
title="Centrer verticalement"
|
|
670
|
+
>
|
|
671
|
+
<svg
|
|
672
|
+
width="10"
|
|
673
|
+
height="12"
|
|
674
|
+
viewBox="0 0 10 12"
|
|
675
|
+
fill="none"
|
|
676
|
+
stroke="currentColor"
|
|
677
|
+
stroke-linecap="round"
|
|
678
|
+
>
|
|
679
|
+
<line x1="1" y1="2.5" x2="8" y2="2.5" stroke-width="1.3" />
|
|
680
|
+
<line x1="1" y1="6" x2="9" y2="6" stroke-width="2" />
|
|
681
|
+
<line x1="1" y1="9.5" x2="6" y2="9.5" stroke-width="1.3" />
|
|
231
682
|
</svg>
|
|
232
683
|
</button>
|
|
233
|
-
<button
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
684
|
+
<button
|
|
685
|
+
id="btnValignBottom"
|
|
686
|
+
class="tool-btn !w-6 !h-6"
|
|
687
|
+
title="Aligner en bas"
|
|
688
|
+
>
|
|
689
|
+
<svg
|
|
690
|
+
width="10"
|
|
691
|
+
height="12"
|
|
692
|
+
viewBox="0 0 10 12"
|
|
693
|
+
fill="none"
|
|
694
|
+
stroke="currentColor"
|
|
695
|
+
stroke-linecap="round"
|
|
696
|
+
>
|
|
697
|
+
<line x1="1" y1="3.5" x2="8" y2="3.5" stroke-width="1.3" />
|
|
698
|
+
<line x1="1" y1="7" x2="6" y2="7" stroke-width="1.3" />
|
|
699
|
+
<line x1="1" y1="10.5" x2="9" y2="10.5" stroke-width="2" />
|
|
238
700
|
</svg>
|
|
239
701
|
</button>
|
|
240
702
|
<div class="panel-sep"></div>
|
|
241
|
-
<button
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
703
|
+
<button
|
|
704
|
+
id="btnZOrderBack"
|
|
705
|
+
class="tool-btn !w-7 !h-6"
|
|
706
|
+
title="Envoyer en arrière"
|
|
707
|
+
>
|
|
708
|
+
<svg
|
|
709
|
+
width="14"
|
|
710
|
+
height="14"
|
|
711
|
+
viewBox="0 0 14 14"
|
|
712
|
+
fill="none"
|
|
713
|
+
stroke="currentColor"
|
|
714
|
+
stroke-width="1.4"
|
|
715
|
+
stroke-linecap="round"
|
|
716
|
+
stroke-linejoin="round"
|
|
717
|
+
>
|
|
718
|
+
<rect x="1" y="5" width="8" height="8" rx="1" />
|
|
719
|
+
<rect
|
|
720
|
+
x="5"
|
|
721
|
+
y="1"
|
|
722
|
+
width="8"
|
|
723
|
+
height="8"
|
|
724
|
+
rx="1"
|
|
725
|
+
stroke-opacity="0.35"
|
|
726
|
+
/>
|
|
245
727
|
</svg>
|
|
246
728
|
</button>
|
|
247
|
-
<button
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
729
|
+
<button
|
|
730
|
+
id="btnZOrderFront"
|
|
731
|
+
class="tool-btn !w-7 !h-6"
|
|
732
|
+
title="Amener au premier plan"
|
|
733
|
+
>
|
|
734
|
+
<svg
|
|
735
|
+
width="14"
|
|
736
|
+
height="14"
|
|
737
|
+
viewBox="0 0 14 14"
|
|
738
|
+
fill="none"
|
|
739
|
+
stroke="currentColor"
|
|
740
|
+
stroke-width="1.4"
|
|
741
|
+
stroke-linecap="round"
|
|
742
|
+
stroke-linejoin="round"
|
|
743
|
+
>
|
|
744
|
+
<rect
|
|
745
|
+
x="1"
|
|
746
|
+
y="5"
|
|
747
|
+
width="8"
|
|
748
|
+
height="8"
|
|
749
|
+
rx="1"
|
|
750
|
+
stroke-opacity="0.35"
|
|
751
|
+
/>
|
|
752
|
+
<rect x="5" y="1" width="8" height="8" rx="1" />
|
|
251
753
|
</svg>
|
|
252
754
|
</button>
|
|
253
755
|
</div>
|
|
254
756
|
|
|
255
757
|
<!-- Edge panel -->
|
|
256
758
|
<div id="edgePanel" class="float-panel hidden">
|
|
257
|
-
<button
|
|
258
|
-
|
|
259
|
-
|
|
759
|
+
<button
|
|
760
|
+
id="edgeBtnNone"
|
|
761
|
+
class="tool-btn !w-7 !h-6 font-mono text-xs"
|
|
762
|
+
title="Trait simple"
|
|
763
|
+
>
|
|
764
|
+
—
|
|
765
|
+
</button>
|
|
766
|
+
<button
|
|
767
|
+
id="edgeBtnTo"
|
|
768
|
+
class="tool-btn !w-7 !h-6 text-xs"
|
|
769
|
+
title="Flèche directionnelle"
|
|
770
|
+
>
|
|
771
|
+
→
|
|
772
|
+
</button>
|
|
773
|
+
<button
|
|
774
|
+
id="edgeBtnBoth"
|
|
775
|
+
class="tool-btn !w-8 !h-6 text-xs"
|
|
776
|
+
title="Bidirectionnelle"
|
|
777
|
+
>
|
|
778
|
+
←→
|
|
779
|
+
</button>
|
|
260
780
|
<div class="panel-sep"></div>
|
|
261
|
-
<button
|
|
262
|
-
|
|
781
|
+
<button
|
|
782
|
+
id="edgeBtnSolid"
|
|
783
|
+
class="tool-btn !w-8 !h-6"
|
|
784
|
+
title="Trait plein"
|
|
785
|
+
>
|
|
786
|
+
<svg width="22" height="4" viewBox="0 0 22 4">
|
|
787
|
+
<line
|
|
788
|
+
x1="1"
|
|
789
|
+
y1="2"
|
|
790
|
+
x2="21"
|
|
791
|
+
y2="2"
|
|
792
|
+
stroke="currentColor"
|
|
793
|
+
stroke-width="2"
|
|
794
|
+
stroke-linecap="round"
|
|
795
|
+
/>
|
|
796
|
+
</svg>
|
|
263
797
|
</button>
|
|
264
|
-
<button
|
|
265
|
-
|
|
798
|
+
<button
|
|
799
|
+
id="edgeBtnDashed"
|
|
800
|
+
class="tool-btn !w-8 !h-6"
|
|
801
|
+
title="Pointillé"
|
|
802
|
+
>
|
|
803
|
+
<svg width="22" height="4" viewBox="0 0 22 4">
|
|
804
|
+
<line
|
|
805
|
+
x1="1"
|
|
806
|
+
y1="2"
|
|
807
|
+
x2="21"
|
|
808
|
+
y2="2"
|
|
809
|
+
stroke="currentColor"
|
|
810
|
+
stroke-width="2"
|
|
811
|
+
stroke-linecap="round"
|
|
812
|
+
stroke-dasharray="4,3"
|
|
813
|
+
/>
|
|
814
|
+
</svg>
|
|
266
815
|
</button>
|
|
267
816
|
<div class="panel-sep"></div>
|
|
268
|
-
<button
|
|
269
|
-
|
|
817
|
+
<button
|
|
818
|
+
id="btnEdgeFontDecrease"
|
|
819
|
+
class="tool-btn !w-8 !h-6"
|
|
820
|
+
style="font-size: 10px"
|
|
821
|
+
title="Réduire la police"
|
|
822
|
+
>
|
|
823
|
+
Aa−
|
|
824
|
+
</button>
|
|
825
|
+
<button
|
|
826
|
+
id="btnEdgeFontIncrease"
|
|
827
|
+
class="tool-btn !w-8 !h-6"
|
|
828
|
+
style="font-size: 10px"
|
|
829
|
+
title="Agrandir la police"
|
|
830
|
+
>
|
|
831
|
+
Aa+
|
|
832
|
+
</button>
|
|
270
833
|
<div class="panel-sep"></div>
|
|
271
|
-
<button
|
|
834
|
+
<button
|
|
835
|
+
id="btnEdgeLabelEdit"
|
|
836
|
+
class="tool-btn !w-6 !h-6"
|
|
837
|
+
title="Modifier le libellé de la flèche"
|
|
838
|
+
>
|
|
839
|
+
✎
|
|
840
|
+
</button>
|
|
272
841
|
</div>
|
|
273
842
|
|
|
274
843
|
<!-- Floating label textarea -->
|
|
275
|
-
<textarea
|
|
844
|
+
<textarea
|
|
845
|
+
id="labelInput"
|
|
846
|
+
class="hidden"
|
|
847
|
+
rows="1"
|
|
848
|
+
placeholder="Label…"
|
|
849
|
+
></textarea>
|
|
276
850
|
|
|
277
851
|
<!-- Empty state -->
|
|
278
|
-
<div
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
852
|
+
<div
|
|
853
|
+
id="emptyState"
|
|
854
|
+
class="absolute inset-0 flex flex-col items-center justify-center text-gray-400 dark:text-gray-600 pointer-events-none select-none"
|
|
855
|
+
>
|
|
856
|
+
<svg
|
|
857
|
+
width="52"
|
|
858
|
+
height="44"
|
|
859
|
+
viewBox="0 0 52 44"
|
|
860
|
+
fill="none"
|
|
861
|
+
stroke="currentColor"
|
|
862
|
+
stroke-width="1.5"
|
|
863
|
+
class="mb-3 opacity-50"
|
|
864
|
+
>
|
|
865
|
+
<rect x="2" y="4" width="18" height="12" rx="2" />
|
|
866
|
+
<rect x="32" y="28" width="18" height="12" rx="2" />
|
|
867
|
+
<line x1="20" y1="10" x2="32" y2="34" stroke-dasharray="4,3" />
|
|
868
|
+
<rect x="17" y="16" width="18" height="12" rx="2" />
|
|
869
|
+
<line x1="26" y1="16" x2="26" y2="10" stroke-dasharray="4,3" />
|
|
286
870
|
</svg>
|
|
287
871
|
<p class="text-sm">Sélectionne ou crée un diagramme</p>
|
|
288
872
|
</div>
|
|
289
873
|
</div>
|
|
290
874
|
</div>
|
|
291
875
|
|
|
292
|
-
<script>
|
|
293
|
-
// ── State ─────────────────────────────────────────────────────────────────
|
|
294
|
-
let network = null;
|
|
295
|
-
let nodes = null;
|
|
296
|
-
let edges = null;
|
|
297
|
-
let diagrams = [];
|
|
298
|
-
let currentDiagramId = null;
|
|
299
|
-
let currentTool = 'select';
|
|
300
|
-
let pendingShape = 'box';
|
|
301
|
-
let selectedNodeIds = [];
|
|
302
|
-
let selectedEdgeIds = [];
|
|
303
|
-
let physicsEnabled = true;
|
|
304
|
-
let gridEnabled = false;
|
|
305
|
-
const GRID_SIZE = 40;
|
|
306
|
-
let isDirty = false;
|
|
307
|
-
let sidebarOpen = true;
|
|
308
|
-
let editingNodeId = null;
|
|
309
|
-
let editingEdgeId = null;
|
|
310
|
-
let resizeDrag = null;
|
|
311
|
-
let clipboard = null; // { nodes: [], edges: [] }
|
|
312
|
-
let _canonicalOrder = []; // canonical z-order (DataSet-independent, immune to vis.js hover reordering)
|
|
313
|
-
|
|
314
|
-
// ── Colors ────────────────────────────────────────────────────────────────
|
|
315
|
-
const NODE_COLORS = {
|
|
316
|
-
'c-gray': { bg:'#f5f5f4', border:'#a8a29e', font:'#292524', hbg:'#e7e5e4', hborder:'#78716c' },
|
|
317
|
-
'c-blue': { bg:'#dbeafe', border:'#3b82f6', font:'#1e40af', hbg:'#bfdbfe', hborder:'#2563eb' },
|
|
318
|
-
'c-green': { bg:'#dcfce7', border:'#22c55e', font:'#166534', hbg:'#bbf7d0', hborder:'#16a34a' },
|
|
319
|
-
'c-amber': { bg:'#fef9c3', border:'#f59e0b', font:'#78350f', hbg:'#fef08a', hborder:'#d97706' },
|
|
320
|
-
'c-rose': { bg:'#ffe4e6', border:'#f43f5e', font:'#881337', hbg:'#fecdd3', hborder:'#e11d48' },
|
|
321
|
-
'c-purple': { bg:'#ede9fe', border:'#8b5cf6', font:'#4c1d95', hbg:'#ddd6fe', hborder:'#7c3aed' },
|
|
322
|
-
'c-teal': { bg:'#ccfbf1', border:'#14b8a6', font:'#134e4a', hbg:'#99f6e4', hborder:'#0d9488' },
|
|
323
|
-
};
|
|
324
|
-
|
|
325
|
-
// ── Actor ctxRenderer ─────────────────────────────────────────────────────
|
|
326
|
-
function makeActorRenderer(colorKey) {
|
|
327
|
-
return function({ ctx, x, y, state: { selected } }) {
|
|
328
|
-
const c = NODE_COLORS[colorKey] || NODE_COLORS['c-gray'];
|
|
329
|
-
return {
|
|
330
|
-
drawNode() {
|
|
331
|
-
ctx.save();
|
|
332
|
-
ctx.strokeStyle = selected ? '#f97316' : c.border;
|
|
333
|
-
ctx.fillStyle = selected ? c.hbg : c.bg;
|
|
334
|
-
ctx.lineWidth = 2; ctx.lineCap = 'round';
|
|
335
|
-
ctx.beginPath(); ctx.arc(x, y - 20, 8, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
|
|
336
|
-
ctx.beginPath(); ctx.moveTo(x, y - 12); ctx.lineTo(x, y + 8); ctx.stroke();
|
|
337
|
-
ctx.beginPath(); ctx.moveTo(x - 13, y - 3); ctx.lineTo(x + 13, y - 3); ctx.stroke();
|
|
338
|
-
ctx.beginPath(); ctx.moveTo(x, y + 8); ctx.lineTo(x - 10, y + 24); ctx.stroke();
|
|
339
|
-
ctx.beginPath(); ctx.moveTo(x, y + 8); ctx.lineTo(x + 10, y + 24); ctx.stroke();
|
|
340
|
-
ctx.restore();
|
|
341
|
-
},
|
|
342
|
-
nodeDimensions: { width: 30, height: 52 },
|
|
343
|
-
};
|
|
344
|
-
};
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// ── Build vis.js node props ───────────────────────────────────────────────
|
|
348
|
-
function computeVadjust(textValign, nodeHeight, fontSize) {
|
|
349
|
-
if (!textValign || textValign === 'middle') return 0;
|
|
350
|
-
const h = nodeHeight || 50;
|
|
351
|
-
const fs = fontSize || 13;
|
|
352
|
-
const pad = 8;
|
|
353
|
-
if (textValign === 'top') return -(h / 2 - fs / 2 - pad);
|
|
354
|
-
if (textValign === 'bottom') return (h / 2 - fs / 2 - pad);
|
|
355
|
-
return 0;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
function visNodeProps(shapeType, colorKey, nodeWidth, nodeHeight, fontSize, textAlign, textValign) {
|
|
359
|
-
const c = NODE_COLORS[colorKey] || NODE_COLORS['c-gray'];
|
|
360
|
-
const size = fontSize || 13;
|
|
361
|
-
const align = textAlign || 'center';
|
|
362
|
-
const vadjust = computeVadjust(textValign, nodeHeight, size);
|
|
363
|
-
const colorP = {
|
|
364
|
-
color: {
|
|
365
|
-
background: c.bg, border: c.border,
|
|
366
|
-
highlight: { background: c.hbg, border: c.hborder },
|
|
367
|
-
hover: { background: c.hbg, border: c.hborder },
|
|
368
|
-
},
|
|
369
|
-
font: { color: c.font, size, face: 'system-ui,-apple-system,sans-serif', align, vadjust },
|
|
370
|
-
};
|
|
371
|
-
const sizeP = {};
|
|
372
|
-
if (nodeWidth) sizeP.widthConstraint = { minimum: nodeWidth, maximum: nodeWidth };
|
|
373
|
-
if (nodeHeight) sizeP.heightConstraint = { minimum: nodeHeight, maximum: nodeHeight };
|
|
374
|
-
|
|
375
|
-
if (shapeType === 'actor') {
|
|
376
|
-
return { shape: 'custom', ctxRenderer: makeActorRenderer(colorKey), ...colorP, ...sizeP };
|
|
377
|
-
}
|
|
378
|
-
return { shape: shapeType, ...colorP, ...sizeP };
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
// ── Build vis.js edge props ───────────────────────────────────────────────
|
|
382
|
-
function visEdgeProps(arrowDir, dashes) {
|
|
383
|
-
return {
|
|
384
|
-
arrows: {
|
|
385
|
-
to: { enabled: arrowDir === 'to' || arrowDir === 'both', scaleFactor: 0.7 },
|
|
386
|
-
from: { enabled: arrowDir === 'both', scaleFactor: 0.7 },
|
|
387
|
-
},
|
|
388
|
-
dashes: dashes === true,
|
|
389
|
-
};
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// ── Network init ──────────────────────────────────────────────────────────
|
|
393
|
-
function initNetwork(savedNodes, savedEdges) {
|
|
394
|
-
const container = document.getElementById('vis-canvas');
|
|
395
|
-
|
|
396
|
-
nodes = new vis.DataSet(savedNodes.map(n => ({
|
|
397
|
-
...n,
|
|
398
|
-
...visNodeProps(n.shapeType || 'box', n.colorKey || 'c-gray', n.nodeWidth, n.nodeHeight, n.fontSize, n.textAlign, n.textValign),
|
|
399
|
-
})));
|
|
400
|
-
edges = new vis.DataSet(savedEdges.map(e => ({
|
|
401
|
-
...e,
|
|
402
|
-
...visEdgeProps(e.arrowDir ?? 'to', e.dashes ?? false),
|
|
403
|
-
...(e.fontSize ? { font: { size: e.fontSize, align: 'middle', color: '#6b7280' } } : {}),
|
|
404
|
-
})));
|
|
405
|
-
|
|
406
|
-
const options = {
|
|
407
|
-
physics: {
|
|
408
|
-
enabled: physicsEnabled, stabilization: { enabled: false },
|
|
409
|
-
barnesHut: { gravitationalConstant: -3000, centralGravity: 0.3, springLength: 150, springConstant: 0.04, damping: 0.09 },
|
|
410
|
-
},
|
|
411
|
-
interaction: {
|
|
412
|
-
hover: true, navigationButtons: false, keyboard: false,
|
|
413
|
-
multiselect: true, // Ctrl/Cmd+click for multi-select
|
|
414
|
-
},
|
|
415
|
-
nodes: {
|
|
416
|
-
font: { size: 13, face: 'system-ui,-apple-system,sans-serif' },
|
|
417
|
-
borderWidth: 1.5, borderWidthSelected: 2.5, shadow: false,
|
|
418
|
-
widthConstraint: { minimum: 60 }, heightConstraint: { minimum: 28 },
|
|
419
|
-
},
|
|
420
|
-
edges: {
|
|
421
|
-
smooth: { type: 'continuous' },
|
|
422
|
-
color: { color: '#a8a29e', highlight: '#f97316', hover: '#f97316' },
|
|
423
|
-
width: 1.5, selectionWidth: 2.5,
|
|
424
|
-
font: { size: 11, align: 'middle', color: '#6b7280' },
|
|
425
|
-
},
|
|
426
|
-
manipulation: {
|
|
427
|
-
enabled: false,
|
|
428
|
-
addEdge(data, callback) {
|
|
429
|
-
data.id = 'e' + Date.now();
|
|
430
|
-
data.arrowDir = 'to';
|
|
431
|
-
data.dashes = false;
|
|
432
|
-
Object.assign(data, visEdgeProps('to', false));
|
|
433
|
-
callback(data);
|
|
434
|
-
markDirty();
|
|
435
|
-
// Stay in arrow mode
|
|
436
|
-
setTimeout(() => network.addEdgeMode(), 0);
|
|
437
|
-
},
|
|
438
|
-
},
|
|
439
|
-
};
|
|
440
|
-
|
|
441
|
-
if (network) network.destroy();
|
|
442
|
-
network = new vis.Network(container, { nodes, edges }, options);
|
|
443
|
-
|
|
444
|
-
// Preserve canonical z-order by patching the renderer instead of nodeIndices.
|
|
445
|
-
// vis.js _drawNodes does 3 passes (normal → selected → hovered), always drawing
|
|
446
|
-
// hover/selected nodes on top regardless of z-order. We replace it with a single
|
|
447
|
-
// pass in _canonicalOrder so the user-defined stacking is always respected.
|
|
448
|
-
_canonicalOrder = [...network.body.nodeIndices];
|
|
449
|
-
network.renderer._drawNodes = function(ctx, alwaysShow = false) {
|
|
450
|
-
const bodyNodes = this.body.nodes;
|
|
451
|
-
const margin = 20;
|
|
452
|
-
const topLeft = this.canvas.DOMtoCanvas({ x: -margin, y: -margin });
|
|
453
|
-
const bottomRight = this.canvas.DOMtoCanvas({
|
|
454
|
-
x: this.canvas.frame.canvas.clientWidth + margin,
|
|
455
|
-
y: this.canvas.frame.canvas.clientHeight + margin,
|
|
456
|
-
});
|
|
457
|
-
const viewableArea = {
|
|
458
|
-
top: topLeft.y, left: topLeft.x,
|
|
459
|
-
bottom: bottomRight.y, right: bottomRight.x,
|
|
460
|
-
};
|
|
461
|
-
const drawExternalLabelCallbacks = [];
|
|
462
|
-
for (const id of _canonicalOrder) {
|
|
463
|
-
const node = bodyNodes[id];
|
|
464
|
-
if (!node) continue;
|
|
465
|
-
if (alwaysShow === true) {
|
|
466
|
-
const r = node.draw(ctx);
|
|
467
|
-
if (r.drawExternalLabel != null) drawExternalLabelCallbacks.push(r.drawExternalLabel);
|
|
468
|
-
} else if (node.isBoundingBoxOverlappingWith(viewableArea) === true) {
|
|
469
|
-
const r = node.draw(ctx);
|
|
470
|
-
if (r.drawExternalLabel != null) drawExternalLabelCallbacks.push(r.drawExternalLabel);
|
|
471
|
-
} else {
|
|
472
|
-
node.updateBoundingBox(ctx, node.selected);
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
return {
|
|
476
|
-
drawExternalLabels() {
|
|
477
|
-
for (const draw of drawExternalLabelCallbacks) draw();
|
|
478
|
-
},
|
|
479
|
-
};
|
|
480
|
-
};
|
|
481
|
-
|
|
482
|
-
// Keep _canonicalOrder in sync when nodes are added or removed via the DataSet
|
|
483
|
-
nodes.on('add', (_, { items }) => {
|
|
484
|
-
const existing = new Set(_canonicalOrder);
|
|
485
|
-
items.forEach(id => { if (!existing.has(id)) _canonicalOrder.push(id); });
|
|
486
|
-
});
|
|
487
|
-
nodes.on('remove', (_, { items }) => {
|
|
488
|
-
const removed = new Set(items);
|
|
489
|
-
_canonicalOrder = _canonicalOrder.filter(id => !removed.has(id));
|
|
490
|
-
});
|
|
491
|
-
|
|
492
|
-
network.on('doubleClick', onDoubleClick);
|
|
493
|
-
network.on('selectNode', onSelectNode);
|
|
494
|
-
network.on('deselectNode', onDeselectAll);
|
|
495
|
-
network.on('selectEdge', onSelectEdge);
|
|
496
|
-
network.on('deselectEdge', onDeselectAll);
|
|
497
|
-
network.on('zoom', updateZoomDisplay);
|
|
498
|
-
network.on('dragEnd', onDragEnd);
|
|
499
|
-
network.on('beforeDrawing', drawGrid);
|
|
500
|
-
network.on('afterDrawing', updateSelectionOverlay);
|
|
501
|
-
|
|
502
|
-
document.getElementById('emptyState').classList.add('hidden');
|
|
503
|
-
updateZoomDisplay();
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
// ── Network events ────────────────────────────────────────────────────────
|
|
507
|
-
function onDoubleClick(params) {
|
|
508
|
-
if (params.nodes.length > 0) {
|
|
509
|
-
// Edit label of double-clicked node
|
|
510
|
-
selectedNodeIds = params.nodes;
|
|
511
|
-
network.selectNodes(selectedNodeIds);
|
|
512
|
-
showNodePanel();
|
|
513
|
-
startLabelEdit();
|
|
514
|
-
} else if (params.edges.length > 0 && params.nodes.length === 0) {
|
|
515
|
-
// Edit label of double-clicked edge
|
|
516
|
-
selectedEdgeIds = [params.edges[0]];
|
|
517
|
-
showEdgePanel();
|
|
518
|
-
startEdgeLabelEdit();
|
|
519
|
-
} else if (currentTool === 'addNode') {
|
|
520
|
-
// Add node at double-click position
|
|
521
|
-
const id = 'n' + Date.now();
|
|
522
|
-
nodes.add({
|
|
523
|
-
id, label: 'Node',
|
|
524
|
-
shapeType: pendingShape, colorKey: 'c-gray',
|
|
525
|
-
nodeWidth: null, nodeHeight: null, fontSize: null,
|
|
526
|
-
x: params.pointer.canvas.x, y: params.pointer.canvas.y,
|
|
527
|
-
...visNodeProps(pendingShape, 'c-gray', null, null, null, null, null),
|
|
528
|
-
});
|
|
529
|
-
markDirty();
|
|
530
|
-
setTimeout(() => {
|
|
531
|
-
network.selectNodes([id]);
|
|
532
|
-
selectedNodeIds = [id];
|
|
533
|
-
showNodePanel();
|
|
534
|
-
startLabelEdit();
|
|
535
|
-
}, 50);
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
function onSelectNode(params) {
|
|
540
|
-
selectedNodeIds = params.nodes;
|
|
541
|
-
selectedEdgeIds = [];
|
|
542
|
-
hideEdgePanel();
|
|
543
|
-
showNodePanel();
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
function onSelectEdge(params) {
|
|
547
|
-
if (selectedNodeIds.length > 0) return; // node takes priority
|
|
548
|
-
selectedEdgeIds = params.edges;
|
|
549
|
-
selectedNodeIds = [];
|
|
550
|
-
hideNodePanel();
|
|
551
|
-
showEdgePanel();
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
function onDeselectAll() {
|
|
555
|
-
selectedNodeIds = [];
|
|
556
|
-
selectedEdgeIds = [];
|
|
557
|
-
hideNodePanel();
|
|
558
|
-
hideEdgePanel();
|
|
559
|
-
commitLabelEdit();
|
|
560
|
-
hideLabelInput();
|
|
561
|
-
hideSelectionOverlay();
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
// ── Node panel ────────────────────────────────────────────────────────────
|
|
565
|
-
function showNodePanel() { document.getElementById('nodePanel').classList.remove('hidden'); }
|
|
566
|
-
function hideNodePanel() { document.getElementById('nodePanel').classList.add('hidden'); }
|
|
567
|
-
|
|
568
|
-
function setNodeColor(colorKey) {
|
|
569
|
-
if (!selectedNodeIds.length) return;
|
|
570
|
-
selectedNodeIds.forEach(id => {
|
|
571
|
-
const n = nodes.get(id);
|
|
572
|
-
if (!n) return;
|
|
573
|
-
nodes.update({ id, colorKey, ...visNodeProps(n.shapeType||'box', colorKey, n.nodeWidth, n.nodeHeight, n.fontSize, n.textAlign, n.textValign) });
|
|
574
|
-
});
|
|
575
|
-
markDirty();
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
function changeNodeFontSize(delta) {
|
|
579
|
-
if (!selectedNodeIds.length) return;
|
|
580
|
-
selectedNodeIds.forEach(id => {
|
|
581
|
-
const n = nodes.get(id);
|
|
582
|
-
if (!n) return;
|
|
583
|
-
const newSize = Math.max(8, Math.min(48, (n.fontSize || 13) + delta));
|
|
584
|
-
nodes.update({ id, fontSize: newSize, ...visNodeProps(n.shapeType||'box', n.colorKey||'c-gray', n.nodeWidth, n.nodeHeight, newSize, n.textAlign, n.textValign) });
|
|
585
|
-
});
|
|
586
|
-
markDirty();
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
function setTextAlign(align) {
|
|
590
|
-
if (!selectedNodeIds.length) return;
|
|
591
|
-
selectedNodeIds.forEach(id => {
|
|
592
|
-
const n = nodes.get(id); if (!n) return;
|
|
593
|
-
nodes.update({ id, textAlign: align, ...visNodeProps(n.shapeType||'box', n.colorKey||'c-gray', n.nodeWidth, n.nodeHeight, n.fontSize, align, n.textValign) });
|
|
594
|
-
});
|
|
595
|
-
markDirty();
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
function setTextValign(valign) {
|
|
599
|
-
if (!selectedNodeIds.length) return;
|
|
600
|
-
selectedNodeIds.forEach(id => {
|
|
601
|
-
const n = nodes.get(id); if (!n) return;
|
|
602
|
-
nodes.update({ id, textValign: valign, ...visNodeProps(n.shapeType||'box', n.colorKey||'c-gray', n.nodeWidth, n.nodeHeight, n.fontSize, n.textAlign, valign) });
|
|
603
|
-
});
|
|
604
|
-
markDirty();
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
function changeZOrder(direction) {
|
|
608
|
-
// direction: +1 = bring to front (last in _canonicalOrder = drawn on top)
|
|
609
|
-
// -1 = send to back (first in _canonicalOrder = drawn below)
|
|
610
|
-
if (!selectedNodeIds.length) return;
|
|
611
|
-
selectedNodeIds.forEach(id => {
|
|
612
|
-
const idx = _canonicalOrder.indexOf(id);
|
|
613
|
-
if (idx === -1) return;
|
|
614
|
-
_canonicalOrder.splice(idx, 1);
|
|
615
|
-
if (direction > 0) {
|
|
616
|
-
_canonicalOrder.push(id); // bring to front
|
|
617
|
-
} else {
|
|
618
|
-
_canonicalOrder.unshift(id); // send to back
|
|
619
|
-
}
|
|
620
|
-
});
|
|
621
|
-
network.redraw();
|
|
622
|
-
network.selectNodes(selectedNodeIds);
|
|
623
|
-
markDirty();
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
// ── Edge panel ────────────────────────────────────────────────────────────
|
|
627
|
-
function showEdgePanel() {
|
|
628
|
-
if (!selectedEdgeIds.length) return;
|
|
629
|
-
const e = edges.get(selectedEdgeIds[0]);
|
|
630
|
-
if (!e) return;
|
|
631
|
-
const dir = e.arrowDir ?? 'to';
|
|
632
|
-
const dashes = e.dashes ?? false;
|
|
633
|
-
['edgeBtnNone','edgeBtnTo','edgeBtnBoth'].forEach(id => document.getElementById(id).classList.remove('edge-btn-active'));
|
|
634
|
-
document.getElementById({ none:'edgeBtnNone', to:'edgeBtnTo', both:'edgeBtnBoth' }[dir] || 'edgeBtnTo').classList.add('edge-btn-active');
|
|
635
|
-
['edgeBtnSolid','edgeBtnDashed'].forEach(id => document.getElementById(id).classList.remove('edge-btn-active'));
|
|
636
|
-
document.getElementById(dashes ? 'edgeBtnDashed' : 'edgeBtnSolid').classList.add('edge-btn-active');
|
|
637
|
-
document.getElementById('edgePanel').classList.remove('hidden');
|
|
638
|
-
}
|
|
639
|
-
function hideEdgePanel() { document.getElementById('edgePanel').classList.add('hidden'); }
|
|
640
|
-
|
|
641
|
-
function setEdgeArrow(dir) {
|
|
642
|
-
if (!selectedEdgeIds.length) return;
|
|
643
|
-
selectedEdgeIds.forEach(id => {
|
|
644
|
-
const e = edges.get(id); if (!e) return;
|
|
645
|
-
edges.update({ id, arrowDir: dir, ...visEdgeProps(dir, e.dashes ?? false) });
|
|
646
|
-
});
|
|
647
|
-
showEdgePanel(); markDirty();
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
function setEdgeDashes(dashes) {
|
|
651
|
-
if (!selectedEdgeIds.length) return;
|
|
652
|
-
selectedEdgeIds.forEach(id => {
|
|
653
|
-
const e = edges.get(id); if (!e) return;
|
|
654
|
-
edges.update({ id, dashes, ...visEdgeProps(e.arrowDir ?? 'to', dashes) });
|
|
655
|
-
});
|
|
656
|
-
showEdgePanel(); markDirty();
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
function changeEdgeFontSize(delta) {
|
|
660
|
-
if (!selectedEdgeIds.length) return;
|
|
661
|
-
selectedEdgeIds.forEach(id => {
|
|
662
|
-
const e = edges.get(id); if (!e) return;
|
|
663
|
-
const newSize = Math.max(8, Math.min(48, (e.fontSize || 11) + delta));
|
|
664
|
-
edges.update({ id, fontSize: newSize, font: { size: newSize, align: 'middle', color: '#6b7280' } });
|
|
665
|
-
});
|
|
666
|
-
markDirty();
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
// ── Label editor ──────────────────────────────────────────────────────────
|
|
670
|
-
function startLabelEdit() {
|
|
671
|
-
if (!selectedNodeIds.length || !network) return;
|
|
672
|
-
const nodeId = selectedNodeIds[0];
|
|
673
|
-
const n = nodes.get(nodeId);
|
|
674
|
-
if (!n) return;
|
|
675
|
-
editingNodeId = nodeId; editingEdgeId = null;
|
|
676
|
-
const pos = network.getPositions([nodeId])[nodeId] || { x: 0, y: 0 };
|
|
677
|
-
const dom = network.canvasToDOM(pos);
|
|
678
|
-
const ta = document.getElementById('labelInput');
|
|
679
|
-
ta.value = n.label || '';
|
|
680
|
-
ta.style.left = (dom.x - 75) + 'px';
|
|
681
|
-
ta.style.top = (dom.y - 30) + 'px';
|
|
682
|
-
ta.style.width = '150px';
|
|
683
|
-
ta.classList.remove('hidden');
|
|
684
|
-
autoResizeTextarea(ta);
|
|
685
|
-
ta.focus(); ta.select();
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
function startEdgeLabelEdit() {
|
|
689
|
-
if (!selectedEdgeIds.length || !network) return;
|
|
690
|
-
const edgeId = selectedEdgeIds[0];
|
|
691
|
-
const e = edges.get(edgeId);
|
|
692
|
-
if (!e) return;
|
|
693
|
-
editingEdgeId = edgeId; editingNodeId = null;
|
|
694
|
-
|
|
695
|
-
// Midpoint between the two endpoints in canvas coords
|
|
696
|
-
const positions = network.getPositions([e.from, e.to]);
|
|
697
|
-
const fp = positions[e.from] || { x: 0, y: 0 };
|
|
698
|
-
const tp = positions[e.to] || { x: 0, y: 0 };
|
|
699
|
-
const mid = network.canvasToDOM({ x: (fp.x + tp.x) / 2, y: (fp.y + tp.y) / 2 });
|
|
700
|
-
|
|
701
|
-
const ta = document.getElementById('labelInput');
|
|
702
|
-
ta.value = e.label || '';
|
|
703
|
-
ta.style.left = (mid.x - 60) + 'px';
|
|
704
|
-
ta.style.top = (mid.y - 14) + 'px';
|
|
705
|
-
ta.style.width = '120px';
|
|
706
|
-
ta.classList.remove('hidden');
|
|
707
|
-
autoResizeTextarea(ta);
|
|
708
|
-
ta.focus(); ta.select();
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
function commitLabelEdit() {
|
|
712
|
-
const ta = document.getElementById('labelInput');
|
|
713
|
-
if (editingNodeId) { nodes.update({ id: editingNodeId, label: ta.value }); markDirty(); }
|
|
714
|
-
else if (editingEdgeId) { edges.update({ id: editingEdgeId, label: ta.value }); markDirty(); }
|
|
715
|
-
editingNodeId = null; editingEdgeId = null;
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
function hideLabelInput() {
|
|
719
|
-
document.getElementById('labelInput').classList.add('hidden');
|
|
720
|
-
editingNodeId = null; editingEdgeId = null;
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
function autoResizeTextarea(ta) {
|
|
724
|
-
ta.style.height = 'auto';
|
|
725
|
-
ta.style.height = ta.scrollHeight + 'px';
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
document.getElementById('labelInput').addEventListener('keydown', e => {
|
|
729
|
-
if (e.key === 'Enter' && !e.shiftKey) { commitLabelEdit(); hideLabelInput(); e.preventDefault(); }
|
|
730
|
-
else if (e.key === 'Escape') { hideLabelInput(); }
|
|
731
|
-
});
|
|
732
|
-
document.getElementById('labelInput').addEventListener('input', e => autoResizeTextarea(e.target));
|
|
733
|
-
document.getElementById('labelInput').addEventListener('blur', () => { commitLabelEdit(); hideLabelInput(); });
|
|
734
|
-
|
|
735
|
-
// ── Selection / resize overlay ─────────────────────────────────────────────
|
|
736
|
-
function updateSelectionOverlay() {
|
|
737
|
-
if (!network || !selectedNodeIds.length) { hideSelectionOverlay(); return; }
|
|
738
|
-
|
|
739
|
-
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
740
|
-
for (const id of selectedNodeIds) {
|
|
741
|
-
try {
|
|
742
|
-
const bb = network.getBoundingBox(id);
|
|
743
|
-
minX = Math.min(minX, bb.left); minY = Math.min(minY, bb.top);
|
|
744
|
-
maxX = Math.max(maxX, bb.right); maxY = Math.max(maxY, bb.bottom);
|
|
745
|
-
} catch(_) { /* node still being created */ }
|
|
746
|
-
}
|
|
747
|
-
if (minX === Infinity) { hideSelectionOverlay(); return; }
|
|
748
|
-
|
|
749
|
-
const PAD = 10;
|
|
750
|
-
const tl = network.canvasToDOM({ x: minX, y: minY });
|
|
751
|
-
const br = network.canvasToDOM({ x: maxX, y: maxY });
|
|
752
|
-
const ov = document.getElementById('selectionOverlay');
|
|
753
|
-
ov.style.display = 'block';
|
|
754
|
-
ov.style.left = (tl.x - PAD) + 'px';
|
|
755
|
-
ov.style.top = (tl.y - PAD) + 'px';
|
|
756
|
-
ov.style.width = (br.x - tl.x + PAD * 2) + 'px';
|
|
757
|
-
ov.style.height = (br.y - tl.y + PAD * 2) + 'px';
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
function hideSelectionOverlay() {
|
|
761
|
-
document.getElementById('selectionOverlay').style.display = 'none';
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
// Wire up corner handles
|
|
765
|
-
['tl','tr','bl','br'].forEach(corner => {
|
|
766
|
-
document.getElementById('rh-' + corner).addEventListener('mousedown', e => onResizeStart(e, corner));
|
|
767
|
-
});
|
|
768
|
-
|
|
769
|
-
function onResizeStart(e, corner) {
|
|
770
|
-
if (!selectedNodeIds.length || !network) return;
|
|
771
|
-
e.preventDefault(); e.stopPropagation();
|
|
772
|
-
|
|
773
|
-
const startBBs = selectedNodeIds.map(id => {
|
|
774
|
-
const bb = network.getBoundingBox(id);
|
|
775
|
-
const n = nodes.get(id);
|
|
776
|
-
return { id, node: n,
|
|
777
|
-
initW: n.nodeWidth || Math.round(bb.right - bb.left),
|
|
778
|
-
initH: n.nodeHeight || Math.round(bb.bottom - bb.top) };
|
|
779
|
-
});
|
|
780
|
-
|
|
781
|
-
const allBBs = selectedNodeIds.map(id => network.getBoundingBox(id));
|
|
782
|
-
const initBoxW = Math.max(...allBBs.map(b => b.right)) - Math.min(...allBBs.map(b => b.left));
|
|
783
|
-
const initBoxH = Math.max(...allBBs.map(b => b.bottom)) - Math.min(...allBBs.map(b => b.top));
|
|
784
|
-
|
|
785
|
-
resizeDrag = { corner, startMouse: { x: e.clientX, y: e.clientY }, startBBs, initBoxW, initBoxH };
|
|
786
|
-
|
|
787
|
-
// Pin nodes during resize so physics doesn't fight us
|
|
788
|
-
selectedNodeIds.forEach(id => nodes.update({ id, fixed: true }));
|
|
789
|
-
// Block vis.js from receiving mouse events during drag
|
|
790
|
-
document.getElementById('vis-canvas').style.pointerEvents = 'none';
|
|
791
|
-
|
|
792
|
-
document.addEventListener('mousemove', onResizeDrag);
|
|
793
|
-
document.addEventListener('mouseup', onResizeEnd);
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
function onResizeDrag(e) {
|
|
797
|
-
if (!resizeDrag || !network) return;
|
|
798
|
-
const scale = network.getScale();
|
|
799
|
-
const cdx = (e.clientX - resizeDrag.startMouse.x) / scale;
|
|
800
|
-
const cdy = (e.clientY - resizeDrag.startMouse.y) / scale;
|
|
801
|
-
const MIN = 40;
|
|
802
|
-
const c = resizeDrag.corner;
|
|
803
|
-
|
|
804
|
-
if (resizeDrag.startBBs.length === 1) {
|
|
805
|
-
// Single node: direct resize
|
|
806
|
-
const { id, node, initW, initH } = resizeDrag.startBBs[0];
|
|
807
|
-
let nW = initW, nH = initH;
|
|
808
|
-
if (c==='br'){nW=initW+cdx; nH=initH+cdy;}
|
|
809
|
-
if (c==='bl'){nW=initW-cdx; nH=initH+cdy;}
|
|
810
|
-
if (c==='tr'){nW=initW+cdx; nH=initH-cdy;}
|
|
811
|
-
if (c==='tl'){nW=initW-cdx; nH=initH-cdy;}
|
|
812
|
-
nW = Math.max(MIN, Math.round(nW));
|
|
813
|
-
nH = Math.max(MIN, Math.round(nH));
|
|
814
|
-
nodes.update({ id, nodeWidth: nW, nodeHeight: nH,
|
|
815
|
-
...visNodeProps(node.shapeType||'box', node.colorKey||'c-gray', nW, nH, node.fontSize, node.textAlign, node.textValign) });
|
|
816
|
-
} else {
|
|
817
|
-
// Multi: proportional scale
|
|
818
|
-
const { initBoxW, initBoxH } = resizeDrag;
|
|
819
|
-
let sx = 1, sy = 1;
|
|
820
|
-
if (c==='br'){sx=(initBoxW+cdx)/initBoxW; sy=(initBoxH+cdy)/initBoxH;}
|
|
821
|
-
if (c==='bl'){sx=(initBoxW-cdx)/initBoxW; sy=(initBoxH+cdy)/initBoxH;}
|
|
822
|
-
if (c==='tr'){sx=(initBoxW+cdx)/initBoxW; sy=(initBoxH-cdy)/initBoxH;}
|
|
823
|
-
if (c==='tl'){sx=(initBoxW-cdx)/initBoxW; sy=(initBoxH-cdy)/initBoxH;}
|
|
824
|
-
sx = Math.max(0.1, sx); sy = Math.max(0.1, sy);
|
|
825
|
-
for (const { id, node, initW, initH } of resizeDrag.startBBs) {
|
|
826
|
-
const nW = Math.max(MIN, Math.round(initW * sx));
|
|
827
|
-
const nH = Math.max(MIN, Math.round(initH * sy));
|
|
828
|
-
nodes.update({ id, nodeWidth: nW, nodeHeight: nH,
|
|
829
|
-
...visNodeProps(node.shapeType||'box', node.colorKey||'c-gray', nW, nH, node.fontSize, node.textAlign, node.textValign) });
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
updateSelectionOverlay();
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
function onResizeEnd() {
|
|
836
|
-
if (!resizeDrag) return;
|
|
837
|
-
selectedNodeIds.forEach(id => nodes.update({ id, fixed: false }));
|
|
838
|
-
document.getElementById('vis-canvas').style.pointerEvents = '';
|
|
839
|
-
document.removeEventListener('mousemove', onResizeDrag);
|
|
840
|
-
document.removeEventListener('mouseup', onResizeEnd);
|
|
841
|
-
resizeDrag = null;
|
|
842
|
-
markDirty();
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
// ── Tools ─────────────────────────────────────────────────────────────────
|
|
846
|
-
const TOOL_BTN_MAP = {
|
|
847
|
-
'select': 'toolSelect',
|
|
848
|
-
'addNode:box': 'toolBox',
|
|
849
|
-
'addNode:ellipse': 'toolEllipse',
|
|
850
|
-
'addNode:database': 'toolDatabase',
|
|
851
|
-
'addNode:circle': 'toolCircle',
|
|
852
|
-
'addNode:actor': 'toolActor',
|
|
853
|
-
'addEdge': 'toolArrow',
|
|
854
|
-
};
|
|
855
|
-
|
|
856
|
-
function setTool(tool, shape) {
|
|
857
|
-
currentTool = tool;
|
|
858
|
-
if (shape) pendingShape = shape;
|
|
859
|
-
|
|
860
|
-
document.querySelectorAll('.tool-btn').forEach(b => b.classList.remove('tool-active'));
|
|
861
|
-
if (physicsEnabled) document.getElementById('btnPhysics').classList.add('tool-active');
|
|
862
|
-
const key = tool === 'addNode' ? `addNode:${shape || pendingShape}` : tool;
|
|
863
|
-
const btn = document.getElementById(TOOL_BTN_MAP[key]);
|
|
864
|
-
if (btn) btn.classList.add('tool-active');
|
|
865
|
-
|
|
866
|
-
document.getElementById('vis-canvas').classList.toggle('cursor-crosshair', tool === 'addNode' || tool === 'addEdge');
|
|
867
|
-
|
|
868
|
-
if (tool === 'addEdge' && network) network.addEdgeMode();
|
|
869
|
-
else if (network) network.disableEditMode();
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
function deleteSelected() {
|
|
873
|
-
if (!network) return;
|
|
874
|
-
network.deleteSelected();
|
|
875
|
-
hideNodePanel(); hideEdgePanel(); hideLabelInput(); hideSelectionOverlay();
|
|
876
|
-
editingNodeId = null; editingEdgeId = null;
|
|
877
|
-
markDirty();
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
// ── Physics ───────────────────────────────────────────────────────────────
|
|
881
|
-
function togglePhysics() {
|
|
882
|
-
physicsEnabled = !physicsEnabled;
|
|
883
|
-
if (network) network.setOptions({ physics: { enabled: physicsEnabled } });
|
|
884
|
-
const btn = document.getElementById('btnPhysics');
|
|
885
|
-
btn.classList.toggle('tool-active', physicsEnabled);
|
|
886
|
-
btn.title = physicsEnabled ? 'Auto-espacement actif' : 'Auto-espacement inactif';
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
// ── Grid ─────────────────────────────────────────────────────────────────
|
|
890
|
-
function toggleGrid() {
|
|
891
|
-
gridEnabled = !gridEnabled;
|
|
892
|
-
const btn = document.getElementById('btnGrid');
|
|
893
|
-
btn.classList.toggle('tool-active', gridEnabled);
|
|
894
|
-
btn.title = gridEnabled ? 'Grille activée (snap on)' : 'Grille / Snap to grid (G)';
|
|
895
|
-
if (network) network.redraw();
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
function drawGrid(ctx) {
|
|
899
|
-
if (!gridEnabled || !network) return;
|
|
900
|
-
const isDark = document.documentElement.classList.contains('dark');
|
|
901
|
-
const color = isDark ? 'rgba(255,255,255,0.18)' : 'rgba(0,0,0,0.15)';
|
|
902
|
-
const scale = network.getScale();
|
|
903
|
-
const center = network.getViewPosition();
|
|
904
|
-
const canvas = ctx.canvas;
|
|
905
|
-
const W = canvas.width, H = canvas.height;
|
|
906
|
-
|
|
907
|
-
// Canvas-space grid step
|
|
908
|
-
const step = GRID_SIZE * scale;
|
|
909
|
-
|
|
910
|
-
// Offset so grid aligns to canvas-space multiples of GRID_SIZE
|
|
911
|
-
// Use positive modulo to avoid negative offsets when panning left/up
|
|
912
|
-
const offsetX = ((W / 2 - center.x * scale) % step + step) % step;
|
|
913
|
-
const offsetY = ((H / 2 - center.y * scale) % step + step) % step;
|
|
914
|
-
|
|
915
|
-
ctx.save();
|
|
916
|
-
// Reset transform — we work in screen (canvas pixel) space
|
|
917
|
-
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
918
|
-
ctx.strokeStyle = color;
|
|
919
|
-
ctx.lineWidth = 1;
|
|
920
|
-
ctx.beginPath();
|
|
921
|
-
for (let x = offsetX; x < W; x += step) { ctx.moveTo(x, 0); ctx.lineTo(x, H); }
|
|
922
|
-
for (let y = offsetY; y < H; y += step) { ctx.moveTo(0, y); ctx.lineTo(W, y); }
|
|
923
|
-
ctx.stroke();
|
|
924
|
-
ctx.restore();
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
function snapToGrid(v) {
|
|
928
|
-
return Math.round(v / GRID_SIZE) * GRID_SIZE;
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
function onDragEnd(params) {
|
|
932
|
-
if (gridEnabled && params.nodes && params.nodes.length > 0) {
|
|
933
|
-
const positions = network.getPositions(params.nodes);
|
|
934
|
-
params.nodes.forEach(id => {
|
|
935
|
-
const p = positions[id];
|
|
936
|
-
if (p) network.moveNode(id, snapToGrid(p.x), snapToGrid(p.y));
|
|
937
|
-
});
|
|
938
|
-
}
|
|
939
|
-
markDirty();
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
// ── Zoom ──────────────────────────────────────────────────────────────────
|
|
943
|
-
function adjustZoom(delta) {
|
|
944
|
-
if (!network) return;
|
|
945
|
-
network.moveTo({ scale: Math.max(0.1, Math.min(3, network.getScale() + delta)), animation: false });
|
|
946
|
-
updateZoomDisplay();
|
|
947
|
-
}
|
|
948
|
-
function resetZoom() {
|
|
949
|
-
if (!network) return;
|
|
950
|
-
network.fit({ animation: { duration: 300, easingFunction: 'easeInOutQuad' } });
|
|
951
|
-
setTimeout(updateZoomDisplay, 350);
|
|
952
|
-
}
|
|
953
|
-
function updateZoomDisplay() {
|
|
954
|
-
if (!network) return;
|
|
955
|
-
document.getElementById('zoomLevel').textContent = Math.round(network.getScale() * 100) + '%';
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
// ── Sidebar ───────────────────────────────────────────────────────────────
|
|
959
|
-
function toggleSidebar() {
|
|
960
|
-
sidebarOpen = !sidebarOpen;
|
|
961
|
-
const sb = document.getElementById('sidebar');
|
|
962
|
-
sb.style.width = sidebarOpen ? '14rem' : '0';
|
|
963
|
-
sb.style.overflow = sidebarOpen ? '' : 'hidden';
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
// ── Dirty state ───────────────────────────────────────────────────────────
|
|
967
|
-
function markDirty() {
|
|
968
|
-
isDirty = true;
|
|
969
|
-
document.getElementById('btnSave').disabled = false;
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
// ── Diagram list ──────────────────────────────────────────────────────────
|
|
973
|
-
function renderDiagramList() {
|
|
974
|
-
const list = document.getElementById('diagramList');
|
|
975
|
-
list.innerHTML = '';
|
|
976
|
-
if (!diagrams.length) {
|
|
977
|
-
list.innerHTML = '<p class="text-xs text-gray-400 dark:text-gray-600 px-3 py-3">Aucun diagramme</p>';
|
|
978
|
-
return;
|
|
979
|
-
}
|
|
980
|
-
diagrams.forEach(d => {
|
|
981
|
-
const isActive = d.id === currentDiagramId;
|
|
982
|
-
const item = document.createElement('div');
|
|
983
|
-
item.className = [
|
|
984
|
-
'group flex items-center gap-1 px-2 py-1.5 mx-1 my-0.5 rounded-md cursor-pointer',
|
|
985
|
-
'hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors',
|
|
986
|
-
isActive ? 'bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300' : 'text-gray-700 dark:text-gray-300',
|
|
987
|
-
].join(' ');
|
|
988
|
-
item.innerHTML = `
|
|
989
|
-
<span class="flex-1 text-sm truncate">${escapeHtml(d.title)}</span>
|
|
990
|
-
<button onclick="deleteDiagram('${escapeHtml(d.id)}', event)" title="Supprimer"
|
|
991
|
-
class="hidden group-hover:flex items-center justify-center w-4 h-4 rounded text-gray-400 hover:text-red-500 shrink-0">✕</button>`;
|
|
992
|
-
item.addEventListener('click', e => { if (e.target.tagName === 'BUTTON') return; openDiagram(d.id); });
|
|
993
|
-
list.appendChild(item);
|
|
994
|
-
});
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
// ── Persistence ───────────────────────────────────────────────────────────
|
|
998
|
-
async function loadDiagramList() {
|
|
999
|
-
const res = await fetch('/api/diagrams');
|
|
1000
|
-
diagrams = await res.json();
|
|
1001
|
-
renderDiagramList();
|
|
1002
|
-
if (diagrams.length > 0) openDiagram(diagrams[0].id);
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
async function openDiagram(id) {
|
|
1006
|
-
const res = await fetch(`/api/diagrams/${id}`);
|
|
1007
|
-
const diagram = await res.json();
|
|
1008
|
-
currentDiagramId = id;
|
|
1009
|
-
isDirty = false;
|
|
1010
|
-
document.getElementById('btnSave').disabled = true;
|
|
1011
|
-
document.getElementById('diagramTitle').value = diagram.title || '';
|
|
1012
|
-
initNetwork(diagram.nodes || [], diagram.edges || []);
|
|
1013
|
-
renderDiagramList();
|
|
1014
|
-
document.getElementById('diagramTitle').oninput = () => markDirty();
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
async function newDiagram() {
|
|
1018
|
-
const id = 'd' + Date.now();
|
|
1019
|
-
await fetch(`/api/diagrams/${id}`, {
|
|
1020
|
-
method: 'PUT', headers: { 'Content-Type': 'application/json' },
|
|
1021
|
-
body: JSON.stringify({ title: 'Nouveau diagramme', nodes: [], edges: [] }),
|
|
1022
|
-
});
|
|
1023
|
-
const res = await fetch('/api/diagrams');
|
|
1024
|
-
diagrams = await res.json();
|
|
1025
|
-
renderDiagramList();
|
|
1026
|
-
openDiagram(id);
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
async function deleteDiagram(id, event) {
|
|
1030
|
-
event.stopPropagation();
|
|
1031
|
-
if (!confirm('Supprimer ce diagramme ?')) return;
|
|
1032
|
-
await fetch(`/api/diagrams/${id}`, { method: 'DELETE' });
|
|
1033
|
-
const res = await fetch('/api/diagrams');
|
|
1034
|
-
diagrams = await res.json();
|
|
1035
|
-
if (currentDiagramId === id) {
|
|
1036
|
-
currentDiagramId = null;
|
|
1037
|
-
if (network) { network.destroy(); network = null; }
|
|
1038
|
-
nodes = null; edges = null;
|
|
1039
|
-
document.getElementById('diagramTitle').value = '';
|
|
1040
|
-
document.getElementById('btnSave').disabled = true;
|
|
1041
|
-
hideNodePanel(); hideEdgePanel(); hideLabelInput(); hideSelectionOverlay();
|
|
1042
|
-
if (diagrams.length > 0) openDiagram(diagrams[0].id);
|
|
1043
|
-
else document.getElementById('emptyState').classList.remove('hidden');
|
|
1044
|
-
}
|
|
1045
|
-
renderDiagramList();
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
async function saveDiagram() {
|
|
1049
|
-
if (!currentDiagramId || !network) return;
|
|
1050
|
-
const positions = network.getPositions();
|
|
1051
|
-
// Keep only our custom fields, strip vis.js computed ones.
|
|
1052
|
-
// Use _canonicalOrder so the saved array preserves z-order on reload.
|
|
1053
|
-
const nodeData = _canonicalOrder.map(id => nodes.get(id)).filter(Boolean).map(n => ({
|
|
1054
|
-
id: n.id, label: n.label,
|
|
1055
|
-
shapeType: n.shapeType || 'box',
|
|
1056
|
-
colorKey: n.colorKey || 'c-gray',
|
|
1057
|
-
nodeWidth: n.nodeWidth || null,
|
|
1058
|
-
nodeHeight: n.nodeHeight || null,
|
|
1059
|
-
fontSize: n.fontSize || null,
|
|
1060
|
-
textAlign: n.textAlign || null,
|
|
1061
|
-
textValign: n.textValign || null,
|
|
1062
|
-
x: positions[n.id]?.x ?? n.x,
|
|
1063
|
-
y: positions[n.id]?.y ?? n.y,
|
|
1064
|
-
}));
|
|
1065
|
-
const edgeData = edges.get().map(e => ({
|
|
1066
|
-
id: e.id, from: e.from, to: e.to,
|
|
1067
|
-
label: e.label || '',
|
|
1068
|
-
arrowDir: e.arrowDir || 'to',
|
|
1069
|
-
dashes: e.dashes || false,
|
|
1070
|
-
fontSize: e.fontSize || null,
|
|
1071
|
-
}));
|
|
1072
|
-
const title = document.getElementById('diagramTitle').value || 'Sans titre';
|
|
1073
|
-
await fetch(`/api/diagrams/${currentDiagramId}`, {
|
|
1074
|
-
method: 'PUT', headers: { 'Content-Type': 'application/json' },
|
|
1075
|
-
body: JSON.stringify({ title, nodes: nodeData, edges: edgeData }),
|
|
1076
|
-
});
|
|
1077
|
-
isDirty = false;
|
|
1078
|
-
document.getElementById('btnSave').disabled = true;
|
|
1079
|
-
const res = await fetch('/api/diagrams');
|
|
1080
|
-
diagrams = await res.json();
|
|
1081
|
-
renderDiagramList();
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
// ── Dark mode ─────────────────────────────────────────────────────────────
|
|
1085
|
-
function toggleDark() {
|
|
1086
|
-
const dark = document.documentElement.classList.toggle('dark');
|
|
1087
|
-
localStorage.setItem('ld-dark', dark);
|
|
1088
|
-
document.getElementById('darkIcon').textContent = dark ? '☀' : '☽';
|
|
1089
|
-
}
|
|
1090
|
-
document.getElementById('darkIcon').textContent =
|
|
1091
|
-
document.documentElement.classList.contains('dark') ? '☀' : '☽';
|
|
1092
|
-
|
|
1093
|
-
// ── Utils ─────────────────────────────────────────────────────────────────
|
|
1094
|
-
function escapeHtml(s) {
|
|
1095
|
-
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
// ── Copy / Paste ──────────────────────────────────────────────────────────
|
|
1099
|
-
function copySelected() {
|
|
1100
|
-
if (!network || !selectedNodeIds.length) return;
|
|
1101
|
-
|
|
1102
|
-
const positions = network.getPositions(selectedNodeIds);
|
|
1103
|
-
const copiedNodes = selectedNodeIds.map(id => {
|
|
1104
|
-
const n = nodes.get(id);
|
|
1105
|
-
const { ctxRenderer, ...rest } = n;
|
|
1106
|
-
return { ...rest, x: positions[id]?.x ?? n.x, y: positions[id]?.y ?? n.y };
|
|
1107
|
-
});
|
|
1108
|
-
|
|
1109
|
-
// Copy edges where BOTH endpoints are in the selection
|
|
1110
|
-
const selectedSet = new Set(selectedNodeIds);
|
|
1111
|
-
const copiedEdges = edges.get().filter(e =>
|
|
1112
|
-
selectedSet.has(e.from) && selectedSet.has(e.to)
|
|
1113
|
-
);
|
|
1114
|
-
|
|
1115
|
-
clipboard = { nodes: copiedNodes, edges: copiedEdges };
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
function pasteClipboard() {
|
|
1119
|
-
if (!clipboard || !clipboard.nodes.length || !network) return;
|
|
1120
|
-
|
|
1121
|
-
const OFFSET = 40;
|
|
1122
|
-
// Map old id → new id
|
|
1123
|
-
const idMap = {};
|
|
1124
|
-
clipboard.nodes.forEach(n => { idMap[n.id] = 'n' + Date.now() + Math.random().toString(36).slice(2); });
|
|
1125
|
-
|
|
1126
|
-
const newNodes = clipboard.nodes.map(n => ({
|
|
1127
|
-
...n,
|
|
1128
|
-
id: idMap[n.id],
|
|
1129
|
-
x: (n.x || 0) + OFFSET,
|
|
1130
|
-
y: (n.y || 0) + OFFSET,
|
|
1131
|
-
...visNodeProps(n.shapeType || 'box', n.colorKey || 'c-gray', n.nodeWidth, n.nodeHeight, n.fontSize, n.textAlign, n.textValign),
|
|
1132
|
-
}));
|
|
1133
|
-
|
|
1134
|
-
const newEdges = clipboard.edges.map(e => ({
|
|
1135
|
-
...e,
|
|
1136
|
-
id: 'e' + Date.now() + Math.random().toString(36).slice(2),
|
|
1137
|
-
from: idMap[e.from],
|
|
1138
|
-
to: idMap[e.to],
|
|
1139
|
-
...visEdgeProps(e.arrowDir ?? 'to', e.dashes ?? false),
|
|
1140
|
-
...(e.fontSize ? { font: { size: e.fontSize, align: 'middle', color: '#6b7280' } } : {}),
|
|
1141
|
-
}));
|
|
1142
|
-
|
|
1143
|
-
nodes.add(newNodes);
|
|
1144
|
-
edges.add(newEdges);
|
|
1145
|
-
|
|
1146
|
-
// Select the pasted nodes
|
|
1147
|
-
const newIds = newNodes.map(n => n.id);
|
|
1148
|
-
network.selectNodes(newIds);
|
|
1149
|
-
selectedNodeIds = newIds;
|
|
1150
|
-
selectedEdgeIds = [];
|
|
1151
|
-
showNodePanel();
|
|
1152
|
-
markDirty();
|
|
1153
|
-
|
|
1154
|
-
// Offset clipboard for next paste
|
|
1155
|
-
clipboard = {
|
|
1156
|
-
nodes: clipboard.nodes.map(n => ({ ...n, x: (n.x || 0) + OFFSET, y: (n.y || 0) + OFFSET })),
|
|
1157
|
-
edges: clipboard.edges,
|
|
1158
|
-
};
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1161
|
-
// ── Keyboard shortcuts ────────────────────────────────────────────────────
|
|
1162
|
-
document.addEventListener('keydown', e => {
|
|
1163
|
-
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
|
1164
|
-
if (e.key === 'Delete' || e.key === 'Backspace') { deleteSelected(); return; }
|
|
1165
|
-
if ((e.metaKey || e.ctrlKey) && e.key === 'c') { e.preventDefault(); copySelected(); return; }
|
|
1166
|
-
if ((e.metaKey || e.ctrlKey) && e.key === 'v') { e.preventDefault(); pasteClipboard(); return; }
|
|
1167
|
-
if ((e.metaKey || e.ctrlKey) && e.key === 's') { e.preventDefault(); saveDiagram(); return; }
|
|
1168
|
-
if (e.key === 'Escape') { setTool('select'); return; }
|
|
1169
|
-
if (e.key === 's' || e.key === 'S') { setTool('select'); return; }
|
|
1170
|
-
if (e.key === 'r' || e.key === 'R') { setTool('addNode','box'); return; }
|
|
1171
|
-
if (e.key === 'e' || e.key === 'E') { setTool('addNode','ellipse'); return; }
|
|
1172
|
-
if (e.key === 'd' || e.key === 'D') { setTool('addNode','database');return; }
|
|
1173
|
-
if (e.key === 'c' || e.key === 'C') { setTool('addNode','circle'); return; }
|
|
1174
|
-
if (e.key === 'a' || e.key === 'A') { setTool('addNode','actor'); return; }
|
|
1175
|
-
if (e.key === 'f' || e.key === 'F') { setTool('addEdge'); return; }
|
|
1176
|
-
if (e.key === 'g' || e.key === 'G') { toggleGrid(); return; }
|
|
1177
|
-
});
|
|
1178
|
-
|
|
1179
|
-
// ── Init ──────────────────────────────────────────────────────────────────
|
|
1180
|
-
loadDiagramList();
|
|
1181
|
-
</script>
|
|
876
|
+
<script type="module" src="/diagram/main.js"></script>
|
|
1182
877
|
</body>
|
|
1183
878
|
</html>
|