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