doc-survival-kit 2.0.0 → 3.11.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 doc-survival-kit might be problematic. Click here for more details.
- package/diagram/diagrams.js +240 -0
- package/diagram/events.js +603 -0
- package/diagram/globals.js +137 -0
- package/diagram/links.js +141 -0
- package/diagram/persistence.js +295 -0
- package/diagram/render.js +633 -0
- package/diagram/shape-ops.js +236 -0
- package/diagram/text-edit.js +292 -0
- package/diagram.html +8 -1
- package/package.json +8 -8
- package/diagram.js +0 -2556
package/diagram.js
DELETED
|
@@ -1,2556 +0,0 @@
|
|
|
1
|
-
// ══════════════════════════════════════
|
|
2
|
-
// mesDiagrammes — Éditeur SVG
|
|
3
|
-
// ══════════════════════════════════════
|
|
4
|
-
|
|
5
|
-
// ── État global ──
|
|
6
|
-
var currentTool = "select";
|
|
7
|
-
var diagramsList = [];
|
|
8
|
-
var currentDiagramId = null;
|
|
9
|
-
var diagNavStack = [];
|
|
10
|
-
var diagExpandedIds = {};
|
|
11
|
-
var pendingParentId = null;
|
|
12
|
-
var pendingNavDiagId = null;
|
|
13
|
-
var viewTransform = { x: 60, y: 60, scale: 1 };
|
|
14
|
-
var selectedId = null;
|
|
15
|
-
var selectedType = null; // "shape" | "arrow"
|
|
16
|
-
var selectedIds = []; // multi-sélection (ids de formes)
|
|
17
|
-
var rubberBandState = null; // { sx, sy } pendant le lasso
|
|
18
|
-
var clipboard = []; // formes copiées (Ctrl+C / Ctrl+V)
|
|
19
|
-
var pendingImageBlob = null; // image en attente de sélection du dossier
|
|
20
|
-
var pendingNewDiagram = false; // true pendant la saisie du nom d'un nouveau diagramme
|
|
21
|
-
var dragState = null;
|
|
22
|
-
var panStart = null;
|
|
23
|
-
var arrowSrcId = null;
|
|
24
|
-
var editingShapeId = null;
|
|
25
|
-
var editingTableCell = null; // { row, col } | null
|
|
26
|
-
var pickMode = null; // "fontSize" | "fullStyle" | null
|
|
27
|
-
var pickTargetIds = []; // selectedIds sauvegardés pendant le pick
|
|
28
|
-
var lastClickTime = 0;
|
|
29
|
-
var lastClickShapeId = null;
|
|
30
|
-
var lastClickArrowId = null;
|
|
31
|
-
var editingArrowId = null;
|
|
32
|
-
var boardLocked = false;
|
|
33
|
-
var diagDragSrcId = null;
|
|
34
|
-
var historyStack = [];
|
|
35
|
-
var MAX_HISTORY = 50;
|
|
36
|
-
|
|
37
|
-
// ── Helpers tableau ──
|
|
38
|
-
function getColWidths(shape) {
|
|
39
|
-
var cols = shape.cols || 3;
|
|
40
|
-
if (shape.colWidths && shape.colWidths.length === cols) return shape.colWidths;
|
|
41
|
-
var w = shape.w / cols;
|
|
42
|
-
var result = [];
|
|
43
|
-
for (var i = 0; i < cols; i++) result.push(w);
|
|
44
|
-
return result;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// xOffsets[i] = x du début de la colonne i (relatif à shape.x)
|
|
48
|
-
function getColOffsets(shape) {
|
|
49
|
-
var cw = getColWidths(shape);
|
|
50
|
-
var offsets = [0];
|
|
51
|
-
for (var i = 0; i < cw.length - 1; i++) offsets.push(offsets[i] + cw[i]);
|
|
52
|
-
return offsets;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// ── Palette ──
|
|
56
|
-
var COLORS = {
|
|
57
|
-
"t-green": { fill: "rgba(209,250,229,0.75)", stroke: "#059669", text: "#047857" },
|
|
58
|
-
"t-violet": { fill: "rgba(237,233,254,0.75)", stroke: "#7c3aed", text: "#6d28d9" },
|
|
59
|
-
"t-amber": { fill: "rgba(254,243,199,0.75)", stroke: "#d97706", text: "#b45309" },
|
|
60
|
-
"t-sky": { fill: "rgba(224,242,254,0.75)", stroke: "#0284c7", text: "#0369a1" },
|
|
61
|
-
"t-rose": { fill: "rgba(255,228,230,0.75)", stroke: "#e11d48", text: "#be123c" },
|
|
62
|
-
"t-teal": { fill: "rgba(204,251,241,0.75)", stroke: "#0d9488", text: "#0f766e" },
|
|
63
|
-
"t-white": { fill: "rgba(255,255,255,0.95)", stroke: "#d4d4d4", text: "#404040" },
|
|
64
|
-
};
|
|
65
|
-
var DEFAULT_COLOR = "t-sky";
|
|
66
|
-
|
|
67
|
-
var DEFAULT_SIZES = {
|
|
68
|
-
rect: { w: 130, h: 50 },
|
|
69
|
-
rounded: { w: 130, h: 50 },
|
|
70
|
-
db: { w: 100, h: 65 },
|
|
71
|
-
cloud: { w: 110, h: 65 },
|
|
72
|
-
nuage: { w: 121, h: 96 },
|
|
73
|
-
text: { w: 110, h: 34 },
|
|
74
|
-
postit: { w: 130, h: 110 },
|
|
75
|
-
actor: { w: 60, h: 90 },
|
|
76
|
-
table: { w: 210, h: 120 },
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
// ── Helpers ──
|
|
80
|
-
function escDiag(s) {
|
|
81
|
-
return String(s)
|
|
82
|
-
.replace(/&/g, "&")
|
|
83
|
-
.replace(/</g, "<")
|
|
84
|
-
.replace(/>/g, ">")
|
|
85
|
-
.replace(/"/g, """);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function createSVGEl(tag) {
|
|
89
|
-
return document.createElementNS("http://www.w3.org/2000/svg", tag);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Découpe le texte d'un post-it en lignes qui tiennent dans maxWidth px.
|
|
93
|
-
// Respecte les \n explicites, puis fait du word-wrap sur les mots.
|
|
94
|
-
var _measureCanvas = null;
|
|
95
|
-
function measureText(str, fontSize, fontWeight) {
|
|
96
|
-
if (!_measureCanvas) _measureCanvas = document.createElement("canvas");
|
|
97
|
-
var ctx = _measureCanvas.getContext("2d");
|
|
98
|
-
ctx.font = (fontWeight || "600") + " " + (fontSize || 12) + "px \"Segoe UI\",system-ui,sans-serif";
|
|
99
|
-
return ctx.measureText(str).width;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function wrapPostitLines(text, maxWidth, fontSize) {
|
|
103
|
-
var fs = fontSize || 12;
|
|
104
|
-
var result = [];
|
|
105
|
-
(text || "").split("\n").forEach(function (line) {
|
|
106
|
-
if (line === "") { result.push(""); return; }
|
|
107
|
-
var words = line.split(" ");
|
|
108
|
-
var current = "";
|
|
109
|
-
words.forEach(function (word) {
|
|
110
|
-
// Si le mot seul dépasse maxWidth, le couper caractère par caractère
|
|
111
|
-
if (measureText(word, fs, "600") > maxWidth) {
|
|
112
|
-
if (current !== "") { result.push(current); current = ""; }
|
|
113
|
-
var chunk = "";
|
|
114
|
-
for (var ci = 0; ci < word.length; ci++) {
|
|
115
|
-
var chunkTest = chunk + word[ci];
|
|
116
|
-
if (measureText(chunkTest, fs, "600") > maxWidth) {
|
|
117
|
-
if (chunk !== "") result.push(chunk);
|
|
118
|
-
chunk = word[ci];
|
|
119
|
-
} else {
|
|
120
|
-
chunk = chunkTest;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
current = chunk;
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
var test = current ? current + " " + word : word;
|
|
127
|
-
if (current && measureText(test, fs, "600") > maxWidth) {
|
|
128
|
-
result.push(current);
|
|
129
|
-
current = word;
|
|
130
|
-
} else {
|
|
131
|
-
current = test;
|
|
132
|
-
}
|
|
133
|
-
});
|
|
134
|
-
if (current !== "") result.push(current);
|
|
135
|
-
});
|
|
136
|
-
return result;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function findDiagramById(id, list) {
|
|
140
|
-
if (!list) return null;
|
|
141
|
-
for (var i = 0; i < list.length; i++) {
|
|
142
|
-
if (String(list[i].id) === String(id)) return list[i];
|
|
143
|
-
var found = findDiagramById(id, list[i].children);
|
|
144
|
-
if (found) return found;
|
|
145
|
-
}
|
|
146
|
-
return null;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function findParentListOf(id, list) {
|
|
150
|
-
if (!list) return null;
|
|
151
|
-
for (var i = 0; i < list.length; i++) {
|
|
152
|
-
if (String(list[i].id) === String(id)) return { list: list, index: i };
|
|
153
|
-
var found = findParentListOf(id, list[i].children);
|
|
154
|
-
if (found) return found;
|
|
155
|
-
}
|
|
156
|
-
return null;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function flattenDiagrams(list, result) {
|
|
160
|
-
result = result || [];
|
|
161
|
-
(list || []).forEach(function (d) {
|
|
162
|
-
result.push(d);
|
|
163
|
-
flattenDiagrams(d.children, result);
|
|
164
|
-
});
|
|
165
|
-
return result;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function calcMaxExpandedDepth(list, depth) {
|
|
169
|
-
var max = depth;
|
|
170
|
-
(list || []).forEach(function (d) {
|
|
171
|
-
if (d.children && d.children.length > 0 && diagExpandedIds[String(d.id)]) {
|
|
172
|
-
var childMax = calcMaxExpandedDepth(d.children, depth + 1);
|
|
173
|
-
max = Math.max(max, childMax);
|
|
174
|
-
}
|
|
175
|
-
});
|
|
176
|
-
return max;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function updateSidebarWidth() {
|
|
180
|
-
var depth = calcMaxExpandedDepth(diagramsList, 0);
|
|
181
|
-
var totalW = 220 + depth * 14;
|
|
182
|
-
var panel = document.getElementById("diagramListPanel");
|
|
183
|
-
if (!panel) return;
|
|
184
|
-
panel.style.width = totalW + "px";
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function getAncestorPath(targetId, list, path) {
|
|
188
|
-
path = path || [];
|
|
189
|
-
for (var i = 0; i < (list || []).length; i++) {
|
|
190
|
-
var d = list[i];
|
|
191
|
-
if (String(d.id) === String(targetId)) return path;
|
|
192
|
-
var childPath = getAncestorPath(targetId, d.children, path.concat([String(d.id)]));
|
|
193
|
-
if (childPath !== null) return childPath;
|
|
194
|
-
}
|
|
195
|
-
return null;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function updateBackBtn() {
|
|
199
|
-
var btn = document.getElementById("btnDiagBack");
|
|
200
|
-
if (!btn) return;
|
|
201
|
-
btn.style.display = diagNavStack.length > 0 ? "inline-flex" : "none";
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
function getCurrentDiagram() {
|
|
205
|
-
if (currentDiagramId !== null) {
|
|
206
|
-
var found = findDiagramById(currentDiagramId, diagramsList);
|
|
207
|
-
if (found) return found;
|
|
208
|
-
}
|
|
209
|
-
return diagramsList[0] || null;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// ── Persistance localStorage ──
|
|
213
|
-
function loadDiagrammes() {
|
|
214
|
-
try {
|
|
215
|
-
var stored = JSON.parse(localStorage.getItem("mes_diagrammes"));
|
|
216
|
-
if (stored && stored.length) {
|
|
217
|
-
var migrate = function (list) {
|
|
218
|
-
list.forEach(function (d) {
|
|
219
|
-
if (!d.children) d.children = [];
|
|
220
|
-
migrate(d.children);
|
|
221
|
-
});
|
|
222
|
-
};
|
|
223
|
-
migrate(stored);
|
|
224
|
-
return stored;
|
|
225
|
-
}
|
|
226
|
-
} catch (e) {}
|
|
227
|
-
var defaults = JSON.parse(JSON.stringify(diagrammesDefaut));
|
|
228
|
-
var migrate2 = function (list) {
|
|
229
|
-
list.forEach(function (d) {
|
|
230
|
-
if (!d.children) d.children = [];
|
|
231
|
-
migrate2(d.children);
|
|
232
|
-
});
|
|
233
|
-
};
|
|
234
|
-
migrate2(defaults);
|
|
235
|
-
return defaults;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function saveDiagrammes() {
|
|
239
|
-
localStorage.setItem("mes_diagrammes", JSON.stringify(diagramsList));
|
|
240
|
-
checkDiffDiagrammes();
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function pushHistory() {
|
|
244
|
-
historyStack.push(JSON.stringify(diagramsList));
|
|
245
|
-
if (historyStack.length > MAX_HISTORY) historyStack.shift();
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function undoAction() {
|
|
249
|
-
if (historyStack.length === 0) return;
|
|
250
|
-
diagramsList = JSON.parse(historyStack.pop());
|
|
251
|
-
if (!findDiagramById(currentDiagramId, diagramsList)) {
|
|
252
|
-
currentDiagramId = diagramsList[0] ? String(diagramsList[0].id) : null;
|
|
253
|
-
}
|
|
254
|
-
saveDiagrammes();
|
|
255
|
-
renderAll();
|
|
256
|
-
renderDiagramList();
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function checkDiffDiagrammes() {
|
|
260
|
-
var stored = localStorage.getItem("mes_diagrammes");
|
|
261
|
-
var diff = stored !== JSON.stringify(diagrammesDefaut);
|
|
262
|
-
document.getElementById("btnSaveDiagram").style.display = diff ? "inline-flex" : "none";
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// ── Verrouillage par diagramme ──
|
|
266
|
-
var LOCK_OPEN_SVG = '<svg width="14" height="16" viewBox="0 0 14 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="1" y="7" width="12" height="8" rx="2"/><path d="M4 7V4.5a3 3 0 0 1 6 0"/></svg>';
|
|
267
|
-
var LOCK_CLOSED_SVG = '<svg width="14" height="16" viewBox="0 0 14 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="1" y="7" width="12" height="8" rx="2"/><path d="M4 7V5a3 3 0 0 1 6 0v2"/></svg>';
|
|
268
|
-
|
|
269
|
-
function getLockMap() {
|
|
270
|
-
try { return JSON.parse(localStorage.getItem("diagrammes_lock") || "{}"); } catch(e) { return {}; }
|
|
271
|
-
}
|
|
272
|
-
function saveCurrentLock() {
|
|
273
|
-
var diag = getCurrentDiagram();
|
|
274
|
-
if (!diag) return;
|
|
275
|
-
var map = getLockMap();
|
|
276
|
-
map[diag.id] = boardLocked;
|
|
277
|
-
localStorage.setItem("diagrammes_lock", JSON.stringify(map));
|
|
278
|
-
}
|
|
279
|
-
function restoreLockForDiagram(diagId) {
|
|
280
|
-
var diag = findDiagramById(diagId, diagramsList);
|
|
281
|
-
if (!diag) return;
|
|
282
|
-
var map = getLockMap();
|
|
283
|
-
boardLocked = map[diag.id] === true;
|
|
284
|
-
}
|
|
285
|
-
function toggleBoardLock() {
|
|
286
|
-
boardLocked = !boardLocked;
|
|
287
|
-
saveCurrentLock();
|
|
288
|
-
if (boardLocked) {
|
|
289
|
-
selectedId = null; selectedType = null;
|
|
290
|
-
selectedIds = [];
|
|
291
|
-
document.getElementById("colorPanel").style.display = "none";
|
|
292
|
-
renderAll();
|
|
293
|
-
}
|
|
294
|
-
updateLockBtn();
|
|
295
|
-
}
|
|
296
|
-
function updateLockBtn() {
|
|
297
|
-
var btn = document.getElementById("btnLock");
|
|
298
|
-
if (!btn) return;
|
|
299
|
-
btn.innerHTML = boardLocked ? LOCK_CLOSED_SVG : LOCK_OPEN_SVG;
|
|
300
|
-
btn.title = boardLocked ? "Déverrouiller le diagramme" : "Verrouiller le diagramme";
|
|
301
|
-
btn.classList.toggle("active", boardLocked);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// ── Zoom par diagramme ──
|
|
305
|
-
function getZoomMap() {
|
|
306
|
-
try { return JSON.parse(localStorage.getItem("diagrammes_zoom") || "{}"); } catch(e) { return {}; }
|
|
307
|
-
}
|
|
308
|
-
function saveCurrentZoom() {
|
|
309
|
-
var diag = getCurrentDiagram();
|
|
310
|
-
if (!diag) return;
|
|
311
|
-
var map = getZoomMap();
|
|
312
|
-
map[diag.id] = viewTransform.scale;
|
|
313
|
-
localStorage.setItem("diagrammes_zoom", JSON.stringify(map));
|
|
314
|
-
}
|
|
315
|
-
function restoreZoomForDiagram(diagId) {
|
|
316
|
-
var diag = findDiagramById(diagId, diagramsList);
|
|
317
|
-
if (!diag) return;
|
|
318
|
-
var map = getZoomMap();
|
|
319
|
-
viewTransform.scale = map[diag.id] !== undefined ? map[diag.id] : 1;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// ── File System Access API ──
|
|
323
|
-
var IDB_KEY_DIAG = "diagrammes";
|
|
324
|
-
var IDB_KEY_DIR = "diagrammes_dir";
|
|
325
|
-
|
|
326
|
-
function ouvrirDB_Diag() {
|
|
327
|
-
return new Promise(function (resolve, reject) {
|
|
328
|
-
var req = indexedDB.open("doc-survival-kit-db", 1);
|
|
329
|
-
req.onupgradeneeded = function (e) {
|
|
330
|
-
e.target.result.createObjectStore("fileHandles");
|
|
331
|
-
};
|
|
332
|
-
req.onsuccess = function (e) { resolve(e.target.result); };
|
|
333
|
-
req.onerror = function (e) { reject(e.target.error); };
|
|
334
|
-
});
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
function sauvegarderHandle_Diag(handle) {
|
|
338
|
-
return ouvrirDB_Diag().then(function (db) {
|
|
339
|
-
return new Promise(function (resolve, reject) {
|
|
340
|
-
var tx = db.transaction("fileHandles", "readwrite");
|
|
341
|
-
tx.objectStore("fileHandles").put(handle, IDB_KEY_DIAG);
|
|
342
|
-
tx.oncomplete = resolve;
|
|
343
|
-
tx.onerror = function (e) { reject(e.target.error); };
|
|
344
|
-
});
|
|
345
|
-
});
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
function recupererHandle_Diag() {
|
|
349
|
-
return ouvrirDB_Diag().then(function (db) {
|
|
350
|
-
return new Promise(function (resolve, reject) {
|
|
351
|
-
var tx = db.transaction("fileHandles", "readonly");
|
|
352
|
-
var req = tx.objectStore("fileHandles").get(IDB_KEY_DIAG);
|
|
353
|
-
req.onsuccess = function (e) { resolve(e.target.result || null); };
|
|
354
|
-
req.onerror = function (e) { reject(e.target.error); };
|
|
355
|
-
});
|
|
356
|
-
});
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
function sauvegarderDirHandle(handle) {
|
|
360
|
-
return ouvrirDB_Diag().then(function (db) {
|
|
361
|
-
return new Promise(function (resolve, reject) {
|
|
362
|
-
var tx = db.transaction("fileHandles", "readwrite");
|
|
363
|
-
tx.objectStore("fileHandles").put(handle, IDB_KEY_DIR);
|
|
364
|
-
tx.oncomplete = resolve;
|
|
365
|
-
tx.onerror = function (e) { reject(e.target.error); };
|
|
366
|
-
});
|
|
367
|
-
});
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
function recupererDirHandle() {
|
|
371
|
-
return ouvrirDB_Diag().then(function (db) {
|
|
372
|
-
return new Promise(function (resolve, reject) {
|
|
373
|
-
var tx = db.transaction("fileHandles", "readonly");
|
|
374
|
-
var req = tx.objectStore("fileHandles").get(IDB_KEY_DIR);
|
|
375
|
-
req.onsuccess = function (e) { resolve(e.target.result || null); };
|
|
376
|
-
req.onerror = function (e) { reject(e.target.error); };
|
|
377
|
-
});
|
|
378
|
-
});
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
function enregistrerDiagrammes() {
|
|
382
|
-
if (!("showDirectoryPicker" in window)) return;
|
|
383
|
-
recupererHandle_Diag().then(function (handle) {
|
|
384
|
-
if (!handle) {
|
|
385
|
-
document.getElementById("erreurFichierDiag").style.display = "none";
|
|
386
|
-
document.getElementById("modalPremiereSauvegardeDiag").classList.add("open");
|
|
387
|
-
} else {
|
|
388
|
-
ecrireFichierDiag(handle);
|
|
389
|
-
}
|
|
390
|
-
});
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
function fermerModalSauvegardeDiag() {
|
|
394
|
-
document.getElementById("modalPremiereSauvegardeDiag").classList.remove("open");
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
function ouvrirSelecteurFichierDiag() {
|
|
398
|
-
fermerModalSauvegardeDiag();
|
|
399
|
-
var capturedDir = null;
|
|
400
|
-
window.showDirectoryPicker()
|
|
401
|
-
.then(function (dir) {
|
|
402
|
-
capturedDir = dir;
|
|
403
|
-
sauvegarderDirHandle(dir);
|
|
404
|
-
return dir.getFileHandle("diagrammes.js");
|
|
405
|
-
})
|
|
406
|
-
.then(function (handle) {
|
|
407
|
-
document.getElementById("erreurFichierDiag").style.display = "none";
|
|
408
|
-
return sauvegarderHandle_Diag(handle).then(function () {
|
|
409
|
-
return ecrireFichierDiag(handle);
|
|
410
|
-
}).then(function () {
|
|
411
|
-
if (pendingImageBlob && capturedDir) {
|
|
412
|
-
var blob = pendingImageBlob;
|
|
413
|
-
pendingImageBlob = null;
|
|
414
|
-
enregistrerImageDansBoard(blob, capturedDir);
|
|
415
|
-
}
|
|
416
|
-
});
|
|
417
|
-
})
|
|
418
|
-
.catch(function (err) {
|
|
419
|
-
if (err.name === "NotFoundError") {
|
|
420
|
-
document.getElementById("erreurFichierDiag").style.display = "block";
|
|
421
|
-
document.getElementById("modalPremiereSauvegardeDiag").classList.add("open");
|
|
422
|
-
} else if (err.name !== "AbortError") {
|
|
423
|
-
console.error(err);
|
|
424
|
-
}
|
|
425
|
-
});
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
function ecrireFichierDiag(handle) {
|
|
429
|
-
return handle.queryPermission({ mode: "readwrite" })
|
|
430
|
-
.then(function (p) {
|
|
431
|
-
if (p !== "granted") return handle.requestPermission({ mode: "readwrite" });
|
|
432
|
-
return p;
|
|
433
|
-
})
|
|
434
|
-
.then(function (p) {
|
|
435
|
-
if (p !== "granted") return;
|
|
436
|
-
var content = "var diagrammesDefaut = " + JSON.stringify(diagramsList, null, 2) + ";\n";
|
|
437
|
-
return handle.createWritable().then(function (w) {
|
|
438
|
-
return w.write(content).then(function () { return w.close(); });
|
|
439
|
-
}).then(function () {
|
|
440
|
-
diagrammesDefaut = JSON.parse(JSON.stringify(diagramsList));
|
|
441
|
-
checkDiffDiagrammes();
|
|
442
|
-
});
|
|
443
|
-
})
|
|
444
|
-
.catch(function (err) { console.error(err); });
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
// ── Image paste ──
|
|
448
|
-
function handleImagePaste(blob) {
|
|
449
|
-
recupererDirHandle().then(function (dirHandle) {
|
|
450
|
-
if (!dirHandle) {
|
|
451
|
-
pendingImageBlob = blob;
|
|
452
|
-
document.getElementById("erreurFichierDiag").style.display = "none";
|
|
453
|
-
document.getElementById("modalPremiereSauvegardeDiag").classList.add("open");
|
|
454
|
-
} else {
|
|
455
|
-
dirHandle.queryPermission({ mode: "readwrite" }).then(function (p) {
|
|
456
|
-
if (p !== "granted") return dirHandle.requestPermission({ mode: "readwrite" }).then(function (p2) { return p2; });
|
|
457
|
-
return p;
|
|
458
|
-
}).then(function (p) {
|
|
459
|
-
if (p === "granted") enregistrerImageDansBoard(blob, dirHandle);
|
|
460
|
-
});
|
|
461
|
-
}
|
|
462
|
-
});
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
function enregistrerImageDansBoard(blob, dirHandle) {
|
|
466
|
-
var filename = "img_" + Date.now() + ".png";
|
|
467
|
-
var src = "images/" + filename;
|
|
468
|
-
dirHandle.getDirectoryHandle("images", { create: true })
|
|
469
|
-
.then(function (imagesDir) {
|
|
470
|
-
return imagesDir.getFileHandle(filename, { create: true });
|
|
471
|
-
})
|
|
472
|
-
.then(function (fileHandle) {
|
|
473
|
-
return fileHandle.createWritable().then(function (w) {
|
|
474
|
-
return w.write(blob).then(function () { return w.close(); });
|
|
475
|
-
});
|
|
476
|
-
})
|
|
477
|
-
.then(function () {
|
|
478
|
-
// Lire les dimensions réelles de l'image
|
|
479
|
-
var url = URL.createObjectURL(blob);
|
|
480
|
-
var img = new Image();
|
|
481
|
-
img.onload = function () {
|
|
482
|
-
URL.revokeObjectURL(url);
|
|
483
|
-
var maxW = 600;
|
|
484
|
-
var w = Math.min(img.naturalWidth, maxW);
|
|
485
|
-
var h = Math.round(img.naturalHeight * (w / img.naturalWidth));
|
|
486
|
-
// Centrer sur la vue courante
|
|
487
|
-
var svg = document.getElementById("canvas");
|
|
488
|
-
var r = svg.getBoundingClientRect();
|
|
489
|
-
var cx = Math.round((r.width / 2 - viewTransform.x) / viewTransform.scale - w / 2);
|
|
490
|
-
var cy = Math.round((r.height / 2 - viewTransform.y) / viewTransform.scale - h / 2);
|
|
491
|
-
var diag = getCurrentDiagram();
|
|
492
|
-
var shape = { id: "s" + Date.now(), type: "image", x: cx, y: cy, w: w, h: h, text: "", color: "", src: src };
|
|
493
|
-
diag.shapes.push(shape);
|
|
494
|
-
selectedIds = [shape.id]; selectedId = shape.id; selectedType = "shape";
|
|
495
|
-
saveDiagrammes();
|
|
496
|
-
renderAll();
|
|
497
|
-
document.getElementById("colorPanel").style.display = "none";
|
|
498
|
-
};
|
|
499
|
-
img.src = url;
|
|
500
|
-
})
|
|
501
|
-
.catch(function (err) { console.error("Image paste error:", err); });
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
// ── Coordonnées SVG ──
|
|
505
|
-
function svgPoint(clientX, clientY) {
|
|
506
|
-
var svg = document.getElementById("canvas");
|
|
507
|
-
var rect = svg.getBoundingClientRect();
|
|
508
|
-
return {
|
|
509
|
-
x: (clientX - rect.left - viewTransform.x) / viewTransform.scale,
|
|
510
|
-
y: (clientY - rect.top - viewTransform.y) / viewTransform.scale,
|
|
511
|
-
};
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
function updateViewport() {
|
|
515
|
-
var vp = document.getElementById("viewport");
|
|
516
|
-
vp.setAttribute(
|
|
517
|
-
"transform",
|
|
518
|
-
"translate(" + viewTransform.x + "," + viewTransform.y + ") scale(" + viewTransform.scale + ")"
|
|
519
|
-
);
|
|
520
|
-
document.getElementById("zoomLevel").textContent =
|
|
521
|
-
Math.round(viewTransform.scale * 100) + "%";
|
|
522
|
-
updateTableOverlay();
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
// ── Zoom ──
|
|
526
|
-
function zoomIn() { applyZoom(1.2, null, null); }
|
|
527
|
-
function zoomOut() { applyZoom(1 / 1.2, null, null); }
|
|
528
|
-
function resetZoom() { viewTransform = { x: 60, y: 60, scale: 1 }; updateViewport(); saveCurrentZoom(); }
|
|
529
|
-
|
|
530
|
-
function applyZoom(factor, cx, cy) {
|
|
531
|
-
var newScale = Math.min(4, Math.max(0.15, viewTransform.scale * factor));
|
|
532
|
-
if (cx !== null && cy !== null) {
|
|
533
|
-
viewTransform.x = cx - (cx - viewTransform.x) * (newScale / viewTransform.scale);
|
|
534
|
-
viewTransform.y = cy - (cy - viewTransform.y) * (newScale / viewTransform.scale);
|
|
535
|
-
}
|
|
536
|
-
viewTransform.scale = newScale;
|
|
537
|
-
updateViewport();
|
|
538
|
-
saveCurrentZoom();
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
// ── Rendu des formes ──
|
|
542
|
-
function renderShape(shape) {
|
|
543
|
-
var c = COLORS[shape.color] || COLORS[DEFAULT_COLOR];
|
|
544
|
-
var isSel = selectedIds.indexOf(shape.id) !== -1;
|
|
545
|
-
var stroke = isSel ? "#f97316" : c.stroke;
|
|
546
|
-
var sw = isSel ? 2.5 : 1.5;
|
|
547
|
-
|
|
548
|
-
var g = createSVGEl("g");
|
|
549
|
-
g.setAttribute("data-id", shape.id);
|
|
550
|
-
g.setAttribute("data-type", "shape");
|
|
551
|
-
g.classList.add("shape-group");
|
|
552
|
-
|
|
553
|
-
// ── Fond selon le type ──
|
|
554
|
-
if (shape.type === "rect" || shape.type === "rounded") {
|
|
555
|
-
var rx = shape.type === "rounded" ? 12 : 3;
|
|
556
|
-
var r = createSVGEl("rect");
|
|
557
|
-
r.setAttribute("x", shape.x); r.setAttribute("y", shape.y);
|
|
558
|
-
r.setAttribute("width", shape.w); r.setAttribute("height", shape.h);
|
|
559
|
-
r.setAttribute("rx", rx);
|
|
560
|
-
r.setAttribute("fill", c.fill); r.setAttribute("stroke", stroke);
|
|
561
|
-
r.setAttribute("stroke-width", sw);
|
|
562
|
-
g.appendChild(r);
|
|
563
|
-
|
|
564
|
-
} else if (shape.type === "db") {
|
|
565
|
-
var ry = Math.min(12, shape.h * 0.2);
|
|
566
|
-
var cx = shape.x + shape.w / 2;
|
|
567
|
-
// Corps : path latéral + arc bas
|
|
568
|
-
var body = createSVGEl("path");
|
|
569
|
-
body.setAttribute("d", [
|
|
570
|
-
"M", shape.x, shape.y + ry,
|
|
571
|
-
"L", shape.x, shape.y + shape.h - ry,
|
|
572
|
-
"A", shape.w / 2, ry, 0, 0, 0, shape.x + shape.w, shape.y + shape.h - ry,
|
|
573
|
-
"L", shape.x + shape.w, shape.y + ry,
|
|
574
|
-
"A", shape.w / 2, ry, 0, 0, 0, shape.x, shape.y + ry,
|
|
575
|
-
"Z",
|
|
576
|
-
].join(" "));
|
|
577
|
-
body.setAttribute("fill", c.fill);
|
|
578
|
-
body.setAttribute("stroke", stroke);
|
|
579
|
-
body.setAttribute("stroke-width", sw);
|
|
580
|
-
g.appendChild(body);
|
|
581
|
-
// Ellipse du haut (face visible)
|
|
582
|
-
var topEl = createSVGEl("ellipse");
|
|
583
|
-
topEl.setAttribute("cx", cx); topEl.setAttribute("cy", shape.y + ry);
|
|
584
|
-
topEl.setAttribute("rx", shape.w / 2); topEl.setAttribute("ry", ry);
|
|
585
|
-
topEl.setAttribute("fill", c.fill);
|
|
586
|
-
topEl.setAttribute("stroke", stroke); topEl.setAttribute("stroke-width", sw);
|
|
587
|
-
g.appendChild(topEl);
|
|
588
|
-
|
|
589
|
-
} else if (shape.type === "cloud") {
|
|
590
|
-
// Ellipse en trait continu = service externe / cloud
|
|
591
|
-
var el = createSVGEl("ellipse");
|
|
592
|
-
el.setAttribute("cx", shape.x + shape.w / 2);
|
|
593
|
-
el.setAttribute("cy", shape.y + shape.h / 2);
|
|
594
|
-
el.setAttribute("rx", shape.w / 2);
|
|
595
|
-
el.setAttribute("ry", shape.h / 2);
|
|
596
|
-
el.setAttribute("fill", c.fill);
|
|
597
|
-
el.setAttribute("stroke", stroke);
|
|
598
|
-
el.setAttribute("stroke-width", sw);
|
|
599
|
-
g.appendChild(el);
|
|
600
|
-
|
|
601
|
-
} else if (shape.type === "nuage") {
|
|
602
|
-
// Nuage — contour extérieur du Bootstrap cloud icon (viewBox 0 0 16 16)
|
|
603
|
-
// Le path se referme exactement : endpoint = startpoint, Z explicite
|
|
604
|
-
var nuageG = createSVGEl("g");
|
|
605
|
-
nuageG.setAttribute("transform",
|
|
606
|
-
"translate(" + shape.x + "," + shape.y + ") scale(" + (shape.w / 16) + "," + (shape.h / 16) + ")"
|
|
607
|
-
);
|
|
608
|
-
var nuagePathEl = createSVGEl("path");
|
|
609
|
-
nuagePathEl.setAttribute("d",
|
|
610
|
-
"M4.406 3.342A5.53 5.53 0 0 1 8 2c2.69 0 4.923 2 5.166 4.579" +
|
|
611
|
-
"C14.758 6.804 16 8.137 16 9.773 16 11.569 14.502 13 12.687 13" +
|
|
612
|
-
"H3.781C1.708 13 0 11.366 0 9.318" +
|
|
613
|
-
"c0-1.763 1.266-3.223 2.942-3.593.143-.863.698-1.723 1.464-2.383Z"
|
|
614
|
-
);
|
|
615
|
-
nuagePathEl.setAttribute("fill", c.fill);
|
|
616
|
-
nuagePathEl.setAttribute("stroke", stroke);
|
|
617
|
-
nuagePathEl.setAttribute("stroke-width", sw);
|
|
618
|
-
nuagePathEl.setAttribute("vector-effect", "non-scaling-stroke");
|
|
619
|
-
nuageG.appendChild(nuagePathEl);
|
|
620
|
-
g.appendChild(nuageG);
|
|
621
|
-
|
|
622
|
-
} else if (shape.type === "actor") {
|
|
623
|
-
// Bonhomme UML (stick figure) — pas de fond, juste les traits
|
|
624
|
-
var acx = shape.x + shape.w / 2;
|
|
625
|
-
var aHeadR = Math.min(shape.w * 0.20, shape.h * 0.14);
|
|
626
|
-
var aHeadCY = shape.y + aHeadR + shape.h * 0.02;
|
|
627
|
-
// Tête
|
|
628
|
-
var aHead = createSVGEl("circle");
|
|
629
|
-
aHead.setAttribute("cx", acx);
|
|
630
|
-
aHead.setAttribute("cy", aHeadCY);
|
|
631
|
-
aHead.setAttribute("r", aHeadR);
|
|
632
|
-
aHead.setAttribute("fill", c.fill);
|
|
633
|
-
aHead.setAttribute("stroke", stroke);
|
|
634
|
-
aHead.setAttribute("stroke-width", sw);
|
|
635
|
-
g.appendChild(aHead);
|
|
636
|
-
// Corps
|
|
637
|
-
var aBodyTop = aHeadCY + aHeadR;
|
|
638
|
-
var aBodyBot = shape.y + shape.h * 0.60;
|
|
639
|
-
var aBody = createSVGEl("line");
|
|
640
|
-
aBody.setAttribute("x1", acx); aBody.setAttribute("y1", aBodyTop);
|
|
641
|
-
aBody.setAttribute("x2", acx); aBody.setAttribute("y2", aBodyBot);
|
|
642
|
-
aBody.setAttribute("stroke", stroke); aBody.setAttribute("stroke-width", sw);
|
|
643
|
-
aBody.setAttribute("stroke-linecap", "round");
|
|
644
|
-
g.appendChild(aBody);
|
|
645
|
-
// Bras
|
|
646
|
-
var aArmY = shape.y + shape.h * 0.38;
|
|
647
|
-
var aArmLX = shape.x + shape.w * 0.10;
|
|
648
|
-
var aArmRX = shape.x + shape.w * 0.90;
|
|
649
|
-
var aArms = createSVGEl("line");
|
|
650
|
-
aArms.setAttribute("x1", aArmLX); aArms.setAttribute("y1", aArmY);
|
|
651
|
-
aArms.setAttribute("x2", aArmRX); aArms.setAttribute("y2", aArmY);
|
|
652
|
-
aArms.setAttribute("stroke", stroke); aArms.setAttribute("stroke-width", sw);
|
|
653
|
-
aArms.setAttribute("stroke-linecap", "round");
|
|
654
|
-
g.appendChild(aArms);
|
|
655
|
-
// Jambe gauche
|
|
656
|
-
var aLegLX = shape.x + shape.w * 0.18;
|
|
657
|
-
var aLegRX = shape.x + shape.w * 0.82;
|
|
658
|
-
var aLegBotY = shape.y + shape.h * 0.82;
|
|
659
|
-
var aLegL = createSVGEl("line");
|
|
660
|
-
aLegL.setAttribute("x1", acx); aLegL.setAttribute("y1", aBodyBot);
|
|
661
|
-
aLegL.setAttribute("x2", aLegLX); aLegL.setAttribute("y2", aLegBotY);
|
|
662
|
-
aLegL.setAttribute("stroke", stroke); aLegL.setAttribute("stroke-width", sw);
|
|
663
|
-
aLegL.setAttribute("stroke-linecap", "round");
|
|
664
|
-
g.appendChild(aLegL);
|
|
665
|
-
// Jambe droite
|
|
666
|
-
var aLegR = createSVGEl("line");
|
|
667
|
-
aLegR.setAttribute("x1", acx); aLegR.setAttribute("y1", aBodyBot);
|
|
668
|
-
aLegR.setAttribute("x2", aLegRX); aLegR.setAttribute("y2", aLegBotY);
|
|
669
|
-
aLegR.setAttribute("stroke", stroke); aLegR.setAttribute("stroke-width", sw);
|
|
670
|
-
aLegR.setAttribute("stroke-linecap", "round");
|
|
671
|
-
g.appendChild(aLegR);
|
|
672
|
-
|
|
673
|
-
} else if (shape.type === "table") {
|
|
674
|
-
var tRows = shape.rows || 3;
|
|
675
|
-
var tCols = shape.cols || 3;
|
|
676
|
-
var tCells = shape.cells || [];
|
|
677
|
-
var tColWidths = getColWidths(shape);
|
|
678
|
-
var tColOffsets = getColOffsets(shape);
|
|
679
|
-
var cellH = shape.h / tRows;
|
|
680
|
-
var tfs = shape.fontSize || 12;
|
|
681
|
-
// Fond
|
|
682
|
-
var tBg = createSVGEl("rect");
|
|
683
|
-
tBg.setAttribute("x", shape.x); tBg.setAttribute("y", shape.y);
|
|
684
|
-
tBg.setAttribute("width", shape.w); tBg.setAttribute("height", shape.h);
|
|
685
|
-
tBg.setAttribute("rx", 3);
|
|
686
|
-
tBg.setAttribute("fill", c.fill); tBg.setAttribute("stroke", stroke);
|
|
687
|
-
tBg.setAttribute("stroke-width", sw);
|
|
688
|
-
g.appendChild(tBg);
|
|
689
|
-
// Lignes horizontales internes
|
|
690
|
-
for (var tri = 1; tri < tRows; tri++) {
|
|
691
|
-
var thl = createSVGEl("line");
|
|
692
|
-
thl.setAttribute("x1", shape.x); thl.setAttribute("y1", shape.y + tri * cellH);
|
|
693
|
-
thl.setAttribute("x2", shape.x + shape.w); thl.setAttribute("y2", shape.y + tri * cellH);
|
|
694
|
-
thl.setAttribute("stroke", stroke); thl.setAttribute("stroke-width", sw * 0.5);
|
|
695
|
-
g.appendChild(thl);
|
|
696
|
-
}
|
|
697
|
-
// Lignes verticales internes
|
|
698
|
-
for (var tci = 1; tci < tCols; tci++) {
|
|
699
|
-
var tvl = createSVGEl("line");
|
|
700
|
-
var tvlX = shape.x + tColOffsets[tci];
|
|
701
|
-
tvl.setAttribute("x1", tvlX); tvl.setAttribute("y1", shape.y);
|
|
702
|
-
tvl.setAttribute("x2", tvlX); tvl.setAttribute("y2", shape.y + shape.h);
|
|
703
|
-
tvl.setAttribute("stroke", stroke); tvl.setAttribute("stroke-width", sw * 0.5);
|
|
704
|
-
g.appendChild(tvl);
|
|
705
|
-
}
|
|
706
|
-
// Textes des cellules avec word wrap
|
|
707
|
-
var clineH = Math.round(tfs * 1.33);
|
|
708
|
-
for (var tri2 = 0; tri2 < tRows; tri2++) {
|
|
709
|
-
for (var tci2 = 0; tci2 < tCols; tci2++) {
|
|
710
|
-
var cellVal = (tCells[tri2] && tCells[tri2][tci2]) || "";
|
|
711
|
-
if (!cellVal) continue;
|
|
712
|
-
var cellW2 = tColWidths[tci2];
|
|
713
|
-
var cpad = 6;
|
|
714
|
-
var clines = wrapPostitLines(cellVal, cellW2 - cpad * 2, tfs);
|
|
715
|
-
var cCenterY = shape.y + tri2 * cellH + cellH / 2;
|
|
716
|
-
var cfirstY = cCenterY - clines.length * clineH / 2 + clineH * 0.5 + tfs * 0.5;
|
|
717
|
-
var cellCenterX = shape.x + tColOffsets[tci2] + cellW2 / 2;
|
|
718
|
-
var ctxt = createSVGEl("text");
|
|
719
|
-
ctxt.setAttribute("x", cellCenterX);
|
|
720
|
-
ctxt.setAttribute("y", cfirstY);
|
|
721
|
-
ctxt.setAttribute("text-anchor", "middle");
|
|
722
|
-
ctxt.setAttribute("fill", c.text);
|
|
723
|
-
ctxt.setAttribute("font-size", tfs);
|
|
724
|
-
ctxt.setAttribute("data-cell", tri2 + "-" + tci2);
|
|
725
|
-
ctxt.setAttribute("font-family", '"Segoe UI",system-ui,sans-serif');
|
|
726
|
-
ctxt.setAttribute("font-weight", "600");
|
|
727
|
-
ctxt.setAttribute("pointer-events", "none");
|
|
728
|
-
(function(cx, lines) {
|
|
729
|
-
lines.forEach(function(line, i) {
|
|
730
|
-
var ts = createSVGEl("tspan");
|
|
731
|
-
ts.setAttribute("x", cx);
|
|
732
|
-
if (i > 0) ts.setAttribute("dy", clineH + "px");
|
|
733
|
-
ts.textContent = line || " ";
|
|
734
|
-
ctxt.appendChild(ts);
|
|
735
|
-
});
|
|
736
|
-
})(cellCenterX, clines);
|
|
737
|
-
g.appendChild(ctxt);
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
// Poignées de redimensionnement des colonnes (chevrons au-dessus de chaque séparateur)
|
|
741
|
-
if (isSel && selectedIds.length === 1) {
|
|
742
|
-
for (var tsi = 1; tsi < tCols; tsi++) {
|
|
743
|
-
var hx = shape.x + tColOffsets[tsi];
|
|
744
|
-
var hy = shape.y - 13;
|
|
745
|
-
var hg = createSVGEl("g");
|
|
746
|
-
hg.setAttribute("data-col-sep", tsi - 1);
|
|
747
|
-
hg.setAttribute("data-shape-id", shape.id);
|
|
748
|
-
hg.style.cursor = "col-resize";
|
|
749
|
-
// Zone de clic transparente
|
|
750
|
-
var hArea = createSVGEl("rect");
|
|
751
|
-
hArea.setAttribute("x", hx - 9); hArea.setAttribute("y", hy - 7);
|
|
752
|
-
hArea.setAttribute("width", 18); hArea.setAttribute("height", 22);
|
|
753
|
-
hArea.setAttribute("fill", "transparent");
|
|
754
|
-
hArea.setAttribute("data-col-sep", tsi - 1);
|
|
755
|
-
hArea.setAttribute("data-shape-id", shape.id);
|
|
756
|
-
hg.appendChild(hArea);
|
|
757
|
-
// Chevron ∨
|
|
758
|
-
var chev = createSVGEl("path");
|
|
759
|
-
chev.setAttribute("d", "M" + (hx - 5) + "," + (hy - 1) + " L" + hx + "," + (hy + 5) + " L" + (hx + 5) + "," + (hy - 1));
|
|
760
|
-
chev.setAttribute("fill", "none");
|
|
761
|
-
chev.setAttribute("stroke", "#f97316");
|
|
762
|
-
chev.setAttribute("stroke-width", "1.8");
|
|
763
|
-
chev.setAttribute("stroke-linecap", "round");
|
|
764
|
-
chev.setAttribute("stroke-linejoin", "round");
|
|
765
|
-
chev.setAttribute("pointer-events", "none");
|
|
766
|
-
hg.appendChild(chev);
|
|
767
|
-
// Trait pointillé vers la table
|
|
768
|
-
var hvl = createSVGEl("line");
|
|
769
|
-
hvl.setAttribute("x1", hx); hvl.setAttribute("y1", hy + 5);
|
|
770
|
-
hvl.setAttribute("x2", hx); hvl.setAttribute("y2", shape.y);
|
|
771
|
-
hvl.setAttribute("stroke", "#f97316");
|
|
772
|
-
hvl.setAttribute("stroke-width", "1");
|
|
773
|
-
hvl.setAttribute("stroke-dasharray", "3,2");
|
|
774
|
-
hvl.setAttribute("pointer-events", "none");
|
|
775
|
-
hg.appendChild(hvl);
|
|
776
|
-
g.appendChild(hg);
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
// Poignée de redimensionnement
|
|
780
|
-
if (isSel && selectedIds.length === 1) {
|
|
781
|
-
var tGrip = createSVGEl("rect");
|
|
782
|
-
tGrip.setAttribute("x", shape.x + shape.w - 5); tGrip.setAttribute("y", shape.y + shape.h - 5);
|
|
783
|
-
tGrip.setAttribute("width", 10); tGrip.setAttribute("height", 10);
|
|
784
|
-
tGrip.setAttribute("rx", 2);
|
|
785
|
-
tGrip.setAttribute("fill", "#f97316");
|
|
786
|
-
tGrip.setAttribute("stroke", "#fff"); tGrip.setAttribute("stroke-width", 1.5);
|
|
787
|
-
tGrip.classList.add("resize-grip");
|
|
788
|
-
tGrip.setAttribute("data-shape-id", shape.id);
|
|
789
|
-
g.appendChild(tGrip);
|
|
790
|
-
}
|
|
791
|
-
// Points de connexion
|
|
792
|
-
var thcx = shape.x + shape.w / 2, thcy = shape.y + shape.h / 2;
|
|
793
|
-
[[thcx, shape.y], [thcx, shape.y + shape.h], [shape.x, thcy], [shape.x + shape.w, thcy]]
|
|
794
|
-
.forEach(function (pt) {
|
|
795
|
-
var tdot = createSVGEl("circle");
|
|
796
|
-
tdot.setAttribute("cx", pt[0]); tdot.setAttribute("cy", pt[1]);
|
|
797
|
-
tdot.setAttribute("r", 5);
|
|
798
|
-
tdot.setAttribute("fill", "#f97316");
|
|
799
|
-
tdot.setAttribute("stroke", "#fff"); tdot.setAttribute("stroke-width", 1.5);
|
|
800
|
-
tdot.classList.add("conn-dot");
|
|
801
|
-
tdot.setAttribute("data-shape-id", shape.id);
|
|
802
|
-
g.appendChild(tdot);
|
|
803
|
-
});
|
|
804
|
-
return g;
|
|
805
|
-
|
|
806
|
-
} else if (shape.type === "postit") {
|
|
807
|
-
var fold = 18;
|
|
808
|
-
var body = createSVGEl("path");
|
|
809
|
-
body.setAttribute("d", [
|
|
810
|
-
"M", shape.x, shape.y,
|
|
811
|
-
"L", shape.x + shape.w - fold, shape.y,
|
|
812
|
-
"L", shape.x + shape.w, shape.y + fold,
|
|
813
|
-
"L", shape.x + shape.w, shape.y + shape.h,
|
|
814
|
-
"L", shape.x, shape.y + shape.h,
|
|
815
|
-
"Z",
|
|
816
|
-
].join(" "));
|
|
817
|
-
body.setAttribute("fill", c.fill);
|
|
818
|
-
body.setAttribute("stroke", stroke);
|
|
819
|
-
body.setAttribute("stroke-width", sw);
|
|
820
|
-
g.appendChild(body);
|
|
821
|
-
// Triangle du coin replié
|
|
822
|
-
var foldTri = createSVGEl("path");
|
|
823
|
-
foldTri.setAttribute("d", [
|
|
824
|
-
"M", shape.x + shape.w - fold, shape.y,
|
|
825
|
-
"L", shape.x + shape.w, shape.y + fold,
|
|
826
|
-
"L", shape.x + shape.w - fold, shape.y + fold,
|
|
827
|
-
"Z",
|
|
828
|
-
].join(" "));
|
|
829
|
-
foldTri.setAttribute("fill", c.stroke);
|
|
830
|
-
foldTri.setAttribute("fill-opacity", "0.25");
|
|
831
|
-
foldTri.setAttribute("stroke", stroke);
|
|
832
|
-
foldTri.setAttribute("stroke-width", sw * 0.7);
|
|
833
|
-
g.appendChild(foldTri);
|
|
834
|
-
|
|
835
|
-
} else if (shape.type === "image") {
|
|
836
|
-
var imgEl = createSVGEl("image");
|
|
837
|
-
imgEl.setAttribute("x", shape.x); imgEl.setAttribute("y", shape.y);
|
|
838
|
-
imgEl.setAttribute("width", shape.w); imgEl.setAttribute("height", shape.h);
|
|
839
|
-
imgEl.setAttribute("href", shape.src);
|
|
840
|
-
imgEl.setAttribute("preserveAspectRatio", "xMidYMid meet");
|
|
841
|
-
g.appendChild(imgEl);
|
|
842
|
-
// Bordure de sélection
|
|
843
|
-
if (isSel) {
|
|
844
|
-
var selRect = createSVGEl("rect");
|
|
845
|
-
selRect.setAttribute("x", shape.x); selRect.setAttribute("y", shape.y);
|
|
846
|
-
selRect.setAttribute("width", shape.w); selRect.setAttribute("height", shape.h);
|
|
847
|
-
selRect.setAttribute("fill", "none");
|
|
848
|
-
selRect.setAttribute("stroke", "#f97316"); selRect.setAttribute("stroke-width", "2");
|
|
849
|
-
selRect.setAttribute("stroke-dasharray", "6,3");
|
|
850
|
-
selRect.setAttribute("pointer-events", "none");
|
|
851
|
-
g.appendChild(selRect);
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
} else if (shape.type === "text") {
|
|
855
|
-
// pas de fond — juste le texte
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
// ── Texte ──
|
|
859
|
-
var textCy = shape.type === "db"
|
|
860
|
-
? shape.y + shape.h * 0.62
|
|
861
|
-
: shape.type === "actor"
|
|
862
|
-
? shape.y + shape.h - 2
|
|
863
|
-
: shape.y + shape.h / 2;
|
|
864
|
-
var ta = shape.textAlign || "center";
|
|
865
|
-
var textAnchor = ta === "left" ? "start" : ta === "right" ? "end" : "middle";
|
|
866
|
-
var txt = createSVGEl("text");
|
|
867
|
-
txt.setAttribute("x", shape.x + shape.w / 2);
|
|
868
|
-
txt.setAttribute("y", textCy);
|
|
869
|
-
txt.setAttribute("text-anchor", textAnchor);
|
|
870
|
-
txt.setAttribute("dominant-baseline", "middle");
|
|
871
|
-
var fs = shape.fontSize || (shape.type === "text" ? 13 : 12);
|
|
872
|
-
txt.setAttribute("fill", shape.type === "text" ? "#292524" : c.text);
|
|
873
|
-
txt.setAttribute("font-size", fs);
|
|
874
|
-
txt.setAttribute("font-family", '"Segoe UI",system-ui,sans-serif');
|
|
875
|
-
txt.setAttribute("font-weight", shape.type === "text" ? "400" : "600");
|
|
876
|
-
txt.setAttribute("pointer-events", "none");
|
|
877
|
-
var tv = shape.textValign || "middle";
|
|
878
|
-
if (shape.type === "postit") {
|
|
879
|
-
var pad = 14;
|
|
880
|
-
var lines = wrapPostitLines(shape.text || "", shape.w - pad * 2, fs);
|
|
881
|
-
var lineH = Math.round(fs * 1.42);
|
|
882
|
-
var firstY = tv === "top"
|
|
883
|
-
? shape.y + pad + lineH * 0.5
|
|
884
|
-
: tv === "bottom"
|
|
885
|
-
? shape.y + shape.h - pad - (lines.length - 1) * lineH - fs * 0.2
|
|
886
|
-
: shape.y + (shape.h - lines.length * lineH) / 2 + lineH * 0.5 + fs * 0.3;
|
|
887
|
-
var tspanX = ta === "left" ? shape.x + pad : ta === "right" ? shape.x + shape.w - pad : shape.x + shape.w / 2;
|
|
888
|
-
txt.setAttribute("y", firstY);
|
|
889
|
-
txt.removeAttribute("dominant-baseline");
|
|
890
|
-
lines.forEach(function (line, i) {
|
|
891
|
-
var ts = createSVGEl("tspan");
|
|
892
|
-
ts.setAttribute("x", tspanX);
|
|
893
|
-
if (i > 0) ts.setAttribute("dy", lineH + "px");
|
|
894
|
-
ts.textContent = line || " ";
|
|
895
|
-
txt.appendChild(ts);
|
|
896
|
-
});
|
|
897
|
-
} else if (shape.type === "actor") {
|
|
898
|
-
var alines = wrapPostitLines(shape.text || "", shape.w * 1.2, fs);
|
|
899
|
-
var alineH = Math.round(fs * 1.33);
|
|
900
|
-
txt.setAttribute("text-anchor", "middle");
|
|
901
|
-
txt.removeAttribute("dominant-baseline");
|
|
902
|
-
alines.forEach(function (line, i) {
|
|
903
|
-
var ts = createSVGEl("tspan");
|
|
904
|
-
ts.setAttribute("x", shape.x + shape.w / 2);
|
|
905
|
-
if (i > 0) ts.setAttribute("dy", alineH + "px");
|
|
906
|
-
ts.textContent = line || " ";
|
|
907
|
-
txt.appendChild(ts);
|
|
908
|
-
});
|
|
909
|
-
} else if (shape.type === "rect" || shape.type === "rounded" || shape.type === "db" || shape.type === "cloud" || shape.type === "nuage") {
|
|
910
|
-
var wpad = shape.type === "cloud" ? Math.round(shape.w * 0.2) : shape.type === "nuage" ? Math.round(shape.w * 0.12) : 12;
|
|
911
|
-
// Pour nuage : vpad = offset depuis le haut visible (y=2/16) + marge interne
|
|
912
|
-
var wvpad = shape.type === "cloud" ? Math.round(shape.h * 0.12)
|
|
913
|
-
: shape.type === "nuage" ? Math.round(shape.h * 2 / 16) + 6
|
|
914
|
-
: 8;
|
|
915
|
-
var wlines = wrapPostitLines(shape.text || "", shape.w - wpad * 2, fs);
|
|
916
|
-
var wlineH = Math.round(fs * 1.33);
|
|
917
|
-
// Pour nuage : centre de la forme visible = milieu entre y=2/16 et y=13/16
|
|
918
|
-
var wcenterY = shape.type === "db" ? shape.y + shape.h * 0.62
|
|
919
|
-
: shape.type === "nuage" ? shape.y + shape.h * ((2 + 13) / 2 / 16)
|
|
920
|
-
: shape.y + shape.h / 2;
|
|
921
|
-
var dbCapH = shape.type === "db" ? Math.min(12, shape.h * 0.2) * 2 : 0;
|
|
922
|
-
var wfirstY = tv === "top"
|
|
923
|
-
? shape.y + dbCapH + wvpad + wlineH * 0.5
|
|
924
|
-
: tv === "bottom"
|
|
925
|
-
? shape.y + shape.h - wvpad - (wlines.length - 1) * wlineH - fs * 0.2
|
|
926
|
-
: wcenterY - wlines.length * wlineH / 2 + wlineH * 0.5 + fs * 0.5;
|
|
927
|
-
var wtspanX = ta === "left" ? shape.x + wpad : ta === "right" ? shape.x + shape.w - wpad : shape.x + shape.w / 2;
|
|
928
|
-
txt.setAttribute("x", wtspanX);
|
|
929
|
-
txt.setAttribute("y", wfirstY);
|
|
930
|
-
txt.removeAttribute("dominant-baseline");
|
|
931
|
-
wlines.forEach(function (line, i) {
|
|
932
|
-
var ts = createSVGEl("tspan");
|
|
933
|
-
ts.setAttribute("x", wtspanX);
|
|
934
|
-
if (i > 0) ts.setAttribute("dy", wlineH + "px");
|
|
935
|
-
ts.textContent = line || " ";
|
|
936
|
-
txt.appendChild(ts);
|
|
937
|
-
});
|
|
938
|
-
} else {
|
|
939
|
-
txt.textContent = shape.text || "";
|
|
940
|
-
}
|
|
941
|
-
g.appendChild(txt);
|
|
942
|
-
|
|
943
|
-
// ── Poignée de redimensionnement (coin bas-droit, sélection unique seulement) ──
|
|
944
|
-
if (isSel && selectedIds.length === 1) {
|
|
945
|
-
var grip = createSVGEl("rect");
|
|
946
|
-
grip.setAttribute("x", shape.x + shape.w - 5);
|
|
947
|
-
grip.setAttribute("y", shape.y + shape.h - 5);
|
|
948
|
-
grip.setAttribute("width", 10); grip.setAttribute("height", 10);
|
|
949
|
-
grip.setAttribute("rx", 2);
|
|
950
|
-
grip.setAttribute("fill", "#f97316");
|
|
951
|
-
grip.setAttribute("stroke", "#fff"); grip.setAttribute("stroke-width", 1.5);
|
|
952
|
-
grip.classList.add("resize-grip");
|
|
953
|
-
grip.setAttribute("data-shape-id", shape.id);
|
|
954
|
-
g.appendChild(grip);
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
// ── Texte non rendu pour les images ──
|
|
958
|
-
if (shape.type === "image") return g;
|
|
959
|
-
|
|
960
|
-
// ── Indicateur de lien (diagramme enfant ou lien externe) ──
|
|
961
|
-
if ((shape.linkedDiagramId || shape.externalUrl) && shape.type !== "image") {
|
|
962
|
-
var lnkColor = shape.externalUrl ? "#0284c7" : "#f97316";
|
|
963
|
-
var lnkCirc = createSVGEl("circle");
|
|
964
|
-
lnkCirc.setAttribute("cx", shape.x + shape.w - 5);
|
|
965
|
-
lnkCirc.setAttribute("cy", shape.y + 5);
|
|
966
|
-
lnkCirc.setAttribute("r", 6);
|
|
967
|
-
lnkCirc.setAttribute("fill", lnkColor);
|
|
968
|
-
lnkCirc.setAttribute("stroke", "#fff");
|
|
969
|
-
lnkCirc.setAttribute("stroke-width", 1.5);
|
|
970
|
-
lnkCirc.setAttribute("pointer-events", "none");
|
|
971
|
-
g.appendChild(lnkCirc);
|
|
972
|
-
var lnkTxt = createSVGEl("text");
|
|
973
|
-
lnkTxt.setAttribute("x", shape.x + shape.w - 5);
|
|
974
|
-
lnkTxt.setAttribute("y", shape.y + 8.5);
|
|
975
|
-
lnkTxt.setAttribute("text-anchor", "middle");
|
|
976
|
-
lnkTxt.setAttribute("font-size", "8");
|
|
977
|
-
lnkTxt.setAttribute("font-weight", "bold");
|
|
978
|
-
lnkTxt.setAttribute("fill", "#fff");
|
|
979
|
-
lnkTxt.setAttribute("pointer-events", "none");
|
|
980
|
-
lnkTxt.textContent = "\u2197";
|
|
981
|
-
g.appendChild(lnkTxt);
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
// ── Rotation ──
|
|
985
|
-
if (shape.rotation) {
|
|
986
|
-
var rcx = shape.x + shape.w / 2, rcy = shape.y + shape.h / 2;
|
|
987
|
-
g.setAttribute("transform", "rotate(" + shape.rotation + "," + rcx + "," + rcy + ")");
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
// ── Points de connexion (visibles au hover et en mode flèche — sauf postit) ──
|
|
991
|
-
if (shape.type !== "postit") {
|
|
992
|
-
var hcx = shape.x + shape.w / 2, hcy = shape.y + shape.h / 2;
|
|
993
|
-
[
|
|
994
|
-
[hcx, shape.y],
|
|
995
|
-
[hcx, shape.y + shape.h],
|
|
996
|
-
[shape.x, hcy],
|
|
997
|
-
[shape.x + shape.w, hcy],
|
|
998
|
-
].forEach(function (pt) {
|
|
999
|
-
var dot = createSVGEl("circle");
|
|
1000
|
-
dot.setAttribute("cx", pt[0]); dot.setAttribute("cy", pt[1]);
|
|
1001
|
-
dot.setAttribute("r", 5);
|
|
1002
|
-
dot.setAttribute("fill", "#f97316");
|
|
1003
|
-
dot.setAttribute("stroke", "#fff"); dot.setAttribute("stroke-width", 1.5);
|
|
1004
|
-
dot.classList.add("conn-dot");
|
|
1005
|
-
dot.setAttribute("data-shape-id", shape.id);
|
|
1006
|
-
g.appendChild(dot);
|
|
1007
|
-
});
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
return g;
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
// ── Rendu d'une flèche ──
|
|
1014
|
-
function getEdgePoint(shape, targetX, targetY) {
|
|
1015
|
-
var cx = shape.x + shape.w / 2;
|
|
1016
|
-
var cy = shape.y + shape.h / 2;
|
|
1017
|
-
var lTargetX = targetX, lTargetY = targetY;
|
|
1018
|
-
if (shape.rotation) {
|
|
1019
|
-
var rad = -shape.rotation * Math.PI / 180;
|
|
1020
|
-
var cos = Math.cos(rad), sin = Math.sin(rad);
|
|
1021
|
-
var ddx = targetX - cx, ddy = targetY - cy;
|
|
1022
|
-
lTargetX = cx + ddx * cos - ddy * sin;
|
|
1023
|
-
lTargetY = cy + ddx * sin + ddy * cos;
|
|
1024
|
-
}
|
|
1025
|
-
var dx = lTargetX - cx, dy = lTargetY - cy;
|
|
1026
|
-
var p;
|
|
1027
|
-
if (dx === 0 && dy === 0) {
|
|
1028
|
-
p = { x: cx, y: shape.y };
|
|
1029
|
-
} else {
|
|
1030
|
-
var hw = shape.w / 2, hh = shape.h / 2;
|
|
1031
|
-
if (Math.abs(dx) * hh > Math.abs(dy) * hw) {
|
|
1032
|
-
var sx = dx > 0 ? 1 : -1;
|
|
1033
|
-
p = { x: cx + sx * hw, y: cy + dy * hw / Math.abs(dx) };
|
|
1034
|
-
} else {
|
|
1035
|
-
var sy = dy > 0 ? 1 : -1;
|
|
1036
|
-
p = { x: cx + dx * hh / Math.abs(dy), y: cy + sy * hh };
|
|
1037
|
-
}
|
|
1038
|
-
}
|
|
1039
|
-
if (shape.rotation) {
|
|
1040
|
-
var rad2 = shape.rotation * Math.PI / 180;
|
|
1041
|
-
var cos2 = Math.cos(rad2), sin2 = Math.sin(rad2);
|
|
1042
|
-
var ex = p.x - cx, ey = p.y - cy;
|
|
1043
|
-
p = { x: cx + ex * cos2 - ey * sin2, y: cy + ex * sin2 + ey * cos2 };
|
|
1044
|
-
}
|
|
1045
|
-
return p;
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
function renderArrow(arrow, shapes) {
|
|
1049
|
-
var from = shapes.find(function (s) { return s.id === arrow.from; });
|
|
1050
|
-
var to = shapes.find(function (s) { return s.id === arrow.to; });
|
|
1051
|
-
if (!from || !to) return null;
|
|
1052
|
-
|
|
1053
|
-
var toCx = to.x + to.w / 2, toCy = to.y + to.h / 2;
|
|
1054
|
-
var frCx = from.x + from.w / 2, frCy = from.y + from.h / 2;
|
|
1055
|
-
var fp = getEdgePoint(from, toCx, toCy);
|
|
1056
|
-
var tp = getEdgePoint(to, frCx, frCy);
|
|
1057
|
-
|
|
1058
|
-
// Raccourcir légèrement la pointe pour ne pas chevauchner la forme
|
|
1059
|
-
var dx = tp.x - fp.x, dy = tp.y - fp.y;
|
|
1060
|
-
var len = Math.sqrt(dx * dx + dy * dy);
|
|
1061
|
-
if (len > 16) {
|
|
1062
|
-
var off = 7 / len;
|
|
1063
|
-
tp = { x: tp.x - dx * off, y: tp.y - dy * off };
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
var isSel = selectedId === arrow.id && selectedType === "arrow";
|
|
1067
|
-
var stroke = isSel ? "#f97316" : "#a8a29e";
|
|
1068
|
-
var markerId = isSel ? "arrowhead-sel" : "arrowhead";
|
|
1069
|
-
|
|
1070
|
-
var g = createSVGEl("g");
|
|
1071
|
-
g.setAttribute("data-id", arrow.id);
|
|
1072
|
-
g.setAttribute("data-type", "arrow");
|
|
1073
|
-
g.classList.add("arrow-group");
|
|
1074
|
-
|
|
1075
|
-
var line = createSVGEl("line");
|
|
1076
|
-
line.setAttribute("x1", fp.x); line.setAttribute("y1", fp.y);
|
|
1077
|
-
line.setAttribute("x2", tp.x); line.setAttribute("y2", tp.y);
|
|
1078
|
-
line.setAttribute("stroke", stroke);
|
|
1079
|
-
line.setAttribute("stroke-width", isSel ? 2 : 1.5);
|
|
1080
|
-
line.setAttribute("marker-end", "url(#" + markerId + ")");
|
|
1081
|
-
g.appendChild(line);
|
|
1082
|
-
|
|
1083
|
-
// Zone de clic élargie
|
|
1084
|
-
var hit = createSVGEl("line");
|
|
1085
|
-
hit.setAttribute("x1", fp.x); hit.setAttribute("y1", fp.y);
|
|
1086
|
-
hit.setAttribute("x2", tp.x); hit.setAttribute("y2", tp.y);
|
|
1087
|
-
hit.setAttribute("stroke", "transparent");
|
|
1088
|
-
hit.setAttribute("stroke-width", 14);
|
|
1089
|
-
g.appendChild(hit);
|
|
1090
|
-
|
|
1091
|
-
// Label
|
|
1092
|
-
if (arrow.label) {
|
|
1093
|
-
var mx = (fp.x + tp.x) / 2, my = (fp.y + tp.y) / 2;
|
|
1094
|
-
var lbl = createSVGEl("text");
|
|
1095
|
-
lbl.setAttribute("x", mx); lbl.setAttribute("y", my - 7);
|
|
1096
|
-
lbl.setAttribute("text-anchor", "middle");
|
|
1097
|
-
lbl.setAttribute("font-size", "10");
|
|
1098
|
-
lbl.setAttribute("font-family", '"Cascadia Code","SF Mono",Consolas,monospace');
|
|
1099
|
-
lbl.setAttribute("fill", "#a8a29e");
|
|
1100
|
-
lbl.setAttribute("pointer-events", "none");
|
|
1101
|
-
lbl.textContent = arrow.label;
|
|
1102
|
-
g.appendChild(lbl);
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
return g;
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
// ── Rendu complet ──
|
|
1109
|
-
function renderAll() {
|
|
1110
|
-
var diag = getCurrentDiagram();
|
|
1111
|
-
if (!diag) return;
|
|
1112
|
-
|
|
1113
|
-
document.getElementById("diagramTitle").value = diag.titre;
|
|
1114
|
-
|
|
1115
|
-
var arrowsLayer = document.getElementById("arrowsLayer");
|
|
1116
|
-
arrowsLayer.innerHTML = "";
|
|
1117
|
-
(diag.arrows || []).forEach(function (a) {
|
|
1118
|
-
var el = renderArrow(a, diag.shapes);
|
|
1119
|
-
if (el) arrowsLayer.appendChild(el);
|
|
1120
|
-
});
|
|
1121
|
-
|
|
1122
|
-
var shapesLayer = document.getElementById("shapesLayer");
|
|
1123
|
-
shapesLayer.innerHTML = "";
|
|
1124
|
-
(diag.shapes || []).forEach(function (s) {
|
|
1125
|
-
shapesLayer.appendChild(renderShape(s));
|
|
1126
|
-
});
|
|
1127
|
-
|
|
1128
|
-
updateViewport();
|
|
1129
|
-
renderDiagramList();
|
|
1130
|
-
syncColorPanel();
|
|
1131
|
-
updateTableOverlay();
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
|
-
function renderDiagramListLevel(list, depth) {
|
|
1135
|
-
return (list || []).map(function (d) {
|
|
1136
|
-
var isActive = String(d.id) === String(currentDiagramId);
|
|
1137
|
-
var isExpanded = !!diagExpandedIds[String(d.id)];
|
|
1138
|
-
var hasChildren = d.children && d.children.length > 0;
|
|
1139
|
-
var indent = 10 + depth * 14;
|
|
1140
|
-
var childrenHtml = (isExpanded && hasChildren)
|
|
1141
|
-
? '<div class="diagram-list-children">' + renderDiagramListLevel(d.children, depth + 1) + '</div>'
|
|
1142
|
-
: '';
|
|
1143
|
-
return (
|
|
1144
|
-
'<div class="diagram-list-group">' +
|
|
1145
|
-
'<div class="diagram-list-item' + (isActive ? ' active' : '') + '"' +
|
|
1146
|
-
' style="padding-left:' + indent + 'px"' +
|
|
1147
|
-
' onclick="selectDiagramme(\'' + d.id + '\', true)">' +
|
|
1148
|
-
'<span class="diagram-list-expand" onclick="event.stopPropagation();toggleDiagExpand(\'' + d.id + '\')">' +
|
|
1149
|
-
(hasChildren ? (isExpanded ? '▼' : '►') : '<span style="display:inline-block;width:0.7em"></span>') +
|
|
1150
|
-
'</span>' +
|
|
1151
|
-
'<span class="diagram-list-name">' + escDiag(d.titre) + '</span>' +
|
|
1152
|
-
'<button class="diagram-list-add" onclick="event.stopPropagation();creerEnfantDiagramme(\'' + d.id + '\')" title="Ajouter un diagramme enfant">+</button>' +
|
|
1153
|
-
(depth > 0 || list.length > 1
|
|
1154
|
-
? '<button class="diagram-list-del" onclick="event.stopPropagation();supprimerDiagramme(\'' + d.id + '\')">×</button>'
|
|
1155
|
-
: '') +
|
|
1156
|
-
'</div>' +
|
|
1157
|
-
childrenHtml +
|
|
1158
|
-
'</div>'
|
|
1159
|
-
);
|
|
1160
|
-
}).join('');
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
function renderDiagramList() {
|
|
1164
|
-
var el = document.getElementById("diagramList");
|
|
1165
|
-
if (!el) return;
|
|
1166
|
-
el.innerHTML = renderDiagramListLevel(diagramsList, 0);
|
|
1167
|
-
updateSidebarWidth();
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1170
|
-
function toggleDiagExpand(id) {
|
|
1171
|
-
var key = String(id);
|
|
1172
|
-
if (diagExpandedIds[key]) {
|
|
1173
|
-
delete diagExpandedIds[key];
|
|
1174
|
-
} else {
|
|
1175
|
-
diagExpandedIds[key] = true;
|
|
1176
|
-
}
|
|
1177
|
-
renderDiagramList();
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
// ── Gestion des diagrammes ──
|
|
1181
|
-
function selectDiagramme(id, clearStack) {
|
|
1182
|
-
if (clearStack) diagNavStack = [];
|
|
1183
|
-
saveCurrentZoom();
|
|
1184
|
-
saveCurrentLock();
|
|
1185
|
-
currentDiagramId = String(id);
|
|
1186
|
-
localStorage.setItem("current_diagram_id", String(id));
|
|
1187
|
-
selectedId = null; selectedType = null;
|
|
1188
|
-
selectedIds = [];
|
|
1189
|
-
pendingNavDiagId = null;
|
|
1190
|
-
restoreZoomForDiagram(id);
|
|
1191
|
-
restoreLockForDiagram(id);
|
|
1192
|
-
viewTransform.x = 60;
|
|
1193
|
-
viewTransform.y = 60;
|
|
1194
|
-
document.getElementById("colorPanel").style.display = "none";
|
|
1195
|
-
renderAll();
|
|
1196
|
-
updateLockBtn();
|
|
1197
|
-
updateBackBtn();
|
|
1198
|
-
document.getElementById("diagramListPanel").classList.remove("open");
|
|
1199
|
-
}
|
|
1200
|
-
|
|
1201
|
-
function goBackDiagram() {
|
|
1202
|
-
if (diagNavStack.length === 0) return;
|
|
1203
|
-
var prevId = diagNavStack.pop();
|
|
1204
|
-
selectDiagramme(prevId);
|
|
1205
|
-
}
|
|
1206
|
-
|
|
1207
|
-
function creerDiagramme() {
|
|
1208
|
-
pendingParentId = null;
|
|
1209
|
-
pendingNewDiagram = true;
|
|
1210
|
-
var input = document.getElementById("diagramTitle");
|
|
1211
|
-
input.value = "";
|
|
1212
|
-
input.placeholder = window.t ? window.t.diag_new_diagram : "Nouveau diagramme";
|
|
1213
|
-
input.focus();
|
|
1214
|
-
input.select();
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
function creerEnfantDiagramme(parentId) {
|
|
1218
|
-
pendingParentId = String(parentId);
|
|
1219
|
-
pendingNewDiagram = true;
|
|
1220
|
-
diagExpandedIds[String(parentId)] = true;
|
|
1221
|
-
var input = document.getElementById("diagramTitle");
|
|
1222
|
-
input.value = "";
|
|
1223
|
-
input.placeholder = window.t ? window.t.diag_new_diagram : "Nouveau diagramme";
|
|
1224
|
-
input.focus();
|
|
1225
|
-
input.select();
|
|
1226
|
-
}
|
|
1227
|
-
|
|
1228
|
-
function confirmerNouveauDiagramme() {
|
|
1229
|
-
if (!pendingNewDiagram) return;
|
|
1230
|
-
pendingNewDiagram = false;
|
|
1231
|
-
var input = document.getElementById("diagramTitle");
|
|
1232
|
-
var titre = input.value.trim() || (window.t ? window.t.diag_new_diagram : "Nouveau diagramme");
|
|
1233
|
-
input.placeholder = "";
|
|
1234
|
-
var d = { id: Date.now(), titre: titre, shapes: [], arrows: [], children: [] };
|
|
1235
|
-
if (pendingParentId) {
|
|
1236
|
-
var parent = findDiagramById(pendingParentId, diagramsList);
|
|
1237
|
-
if (parent) {
|
|
1238
|
-
if (!parent.children) parent.children = [];
|
|
1239
|
-
parent.children.push(d);
|
|
1240
|
-
} else {
|
|
1241
|
-
diagramsList.push(d);
|
|
1242
|
-
}
|
|
1243
|
-
pendingParentId = null;
|
|
1244
|
-
} else {
|
|
1245
|
-
diagramsList.push(d);
|
|
1246
|
-
}
|
|
1247
|
-
saveDiagrammes();
|
|
1248
|
-
selectDiagramme(d.id);
|
|
1249
|
-
document.getElementById("diagramTitle").blur();
|
|
1250
|
-
}
|
|
1251
|
-
|
|
1252
|
-
function annulerNouveauDiagramme() {
|
|
1253
|
-
if (!pendingNewDiagram) return;
|
|
1254
|
-
pendingNewDiagram = false;
|
|
1255
|
-
var diag = getCurrentDiagram();
|
|
1256
|
-
var input = document.getElementById("diagramTitle");
|
|
1257
|
-
input.value = diag ? diag.titre : "";
|
|
1258
|
-
input.placeholder = "";
|
|
1259
|
-
input.blur();
|
|
1260
|
-
}
|
|
1261
|
-
|
|
1262
|
-
function supprimerDiagramme(id) {
|
|
1263
|
-
var info = findParentListOf(id, diagramsList);
|
|
1264
|
-
if (!info) return;
|
|
1265
|
-
if (info.list === diagramsList && info.list.length <= 1) return;
|
|
1266
|
-
pushHistory();
|
|
1267
|
-
info.list.splice(info.index, 1);
|
|
1268
|
-
if (!findDiagramById(currentDiagramId, diagramsList)) {
|
|
1269
|
-
currentDiagramId = diagramsList[0] ? String(diagramsList[0].id) : null;
|
|
1270
|
-
localStorage.setItem("current_diagram_id", currentDiagramId || "");
|
|
1271
|
-
}
|
|
1272
|
-
saveDiagrammes();
|
|
1273
|
-
renderAll();
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
|
-
function toggleDiagramList() {
|
|
1277
|
-
var panel = document.getElementById("diagramListPanel");
|
|
1278
|
-
var isOpening = !panel.classList.contains("open");
|
|
1279
|
-
panel.classList.toggle("open");
|
|
1280
|
-
if (isOpening && currentDiagramId) {
|
|
1281
|
-
var path = getAncestorPath(currentDiagramId, diagramsList);
|
|
1282
|
-
if (path) {
|
|
1283
|
-
path.forEach(function (id) { diagExpandedIds[id] = true; });
|
|
1284
|
-
renderDiagramList();
|
|
1285
|
-
}
|
|
1286
|
-
}
|
|
1287
|
-
}
|
|
1288
|
-
|
|
1289
|
-
// ── Outil actif ──
|
|
1290
|
-
function setTool(tool) {
|
|
1291
|
-
currentTool = tool;
|
|
1292
|
-
document.querySelectorAll(".diagram-tool[id^='tool']").forEach(function (btn) {
|
|
1293
|
-
btn.classList.remove("active");
|
|
1294
|
-
});
|
|
1295
|
-
var btnId = "tool" + tool.charAt(0).toUpperCase() + tool.slice(1);
|
|
1296
|
-
var btn = document.getElementById(btnId);
|
|
1297
|
-
if (btn) btn.classList.add("active");
|
|
1298
|
-
|
|
1299
|
-
arrowSrcId = null;
|
|
1300
|
-
selectedIds = [];
|
|
1301
|
-
document.getElementById("tempArrow").style.display = "none";
|
|
1302
|
-
document.getElementById("canvas").setAttribute(
|
|
1303
|
-
"data-tool", tool
|
|
1304
|
-
);
|
|
1305
|
-
if (tool !== "select") {
|
|
1306
|
-
selectedId = null; selectedType = null;
|
|
1307
|
-
document.getElementById("colorPanel").style.display = "none";
|
|
1308
|
-
}
|
|
1309
|
-
renderAll();
|
|
1310
|
-
}
|
|
1311
|
-
|
|
1312
|
-
// ── Ajout d'une forme ──
|
|
1313
|
-
function addShape(type, x, y) {
|
|
1314
|
-
pushHistory();
|
|
1315
|
-
var diag = getCurrentDiagram();
|
|
1316
|
-
var size = DEFAULT_SIZES[type] || { w: 120, h: 50 };
|
|
1317
|
-
var shape = {
|
|
1318
|
-
id: "s" + Date.now(),
|
|
1319
|
-
type: type,
|
|
1320
|
-
x: Math.round(x - size.w / 2),
|
|
1321
|
-
y: Math.round(y - size.h / 2),
|
|
1322
|
-
w: size.w,
|
|
1323
|
-
h: size.h,
|
|
1324
|
-
text: "",
|
|
1325
|
-
color: type === "postit" ? "t-amber" : DEFAULT_COLOR,
|
|
1326
|
-
};
|
|
1327
|
-
if (type === "table") {
|
|
1328
|
-
shape.rows = 3; shape.cols = 3;
|
|
1329
|
-
shape.cells = [["","",""],["","",""],["","",""]];
|
|
1330
|
-
var initW = shape.w / 3;
|
|
1331
|
-
shape.colWidths = [initW, initW, initW];
|
|
1332
|
-
}
|
|
1333
|
-
diag.shapes.push(shape);
|
|
1334
|
-
saveDiagrammes();
|
|
1335
|
-
selectedId = shape.id; selectedType = "shape";
|
|
1336
|
-
selectedIds = [shape.id];
|
|
1337
|
-
renderAll();
|
|
1338
|
-
document.getElementById("colorPanel").style.display = "flex";
|
|
1339
|
-
syncColorPanel();
|
|
1340
|
-
if (type !== "table") startTextEdit(shape.id);
|
|
1341
|
-
}
|
|
1342
|
-
|
|
1343
|
-
// ── Suppression ──
|
|
1344
|
-
function deleteSelected() {
|
|
1345
|
-
var diag = getCurrentDiagram();
|
|
1346
|
-
if (selectedIds.length === 0 && !(selectedId && selectedType === "arrow")) return;
|
|
1347
|
-
pushHistory();
|
|
1348
|
-
if (selectedIds.length > 0) {
|
|
1349
|
-
diag.shapes = diag.shapes.filter(function (s) { return selectedIds.indexOf(s.id) === -1; });
|
|
1350
|
-
diag.arrows = diag.arrows.filter(function (a) {
|
|
1351
|
-
return selectedIds.indexOf(a.from) === -1 && selectedIds.indexOf(a.to) === -1;
|
|
1352
|
-
});
|
|
1353
|
-
selectedIds = [];
|
|
1354
|
-
selectedId = null; selectedType = null;
|
|
1355
|
-
} else if (selectedId && selectedType === "arrow") {
|
|
1356
|
-
diag.arrows = diag.arrows.filter(function (a) { return a.id !== selectedId; });
|
|
1357
|
-
selectedId = null; selectedType = null;
|
|
1358
|
-
} else {
|
|
1359
|
-
return;
|
|
1360
|
-
}
|
|
1361
|
-
document.getElementById("colorPanel").style.display = "none";
|
|
1362
|
-
saveDiagrammes();
|
|
1363
|
-
renderAll();
|
|
1364
|
-
}
|
|
1365
|
-
|
|
1366
|
-
// ── Couleur ──
|
|
1367
|
-
function setShapeColor(color) {
|
|
1368
|
-
if (selectedIds.length === 0) return;
|
|
1369
|
-
pushHistory();
|
|
1370
|
-
var diag = getCurrentDiagram();
|
|
1371
|
-
selectedIds.forEach(function (id) {
|
|
1372
|
-
var shape = diag.shapes.find(function (s) { return s.id === id; });
|
|
1373
|
-
if (shape) shape.color = color;
|
|
1374
|
-
});
|
|
1375
|
-
saveDiagrammes();
|
|
1376
|
-
renderAll();
|
|
1377
|
-
document.getElementById("colorPanel").style.display = "flex";
|
|
1378
|
-
}
|
|
1379
|
-
|
|
1380
|
-
function setShapeTextValign(valign) {
|
|
1381
|
-
if (selectedIds.length === 0) return;
|
|
1382
|
-
pushHistory();
|
|
1383
|
-
var diag = getCurrentDiagram();
|
|
1384
|
-
selectedIds.forEach(function (id) {
|
|
1385
|
-
var shape = diag.shapes.find(function (s) { return s.id === id; });
|
|
1386
|
-
if (shape) shape.textValign = valign;
|
|
1387
|
-
});
|
|
1388
|
-
saveDiagrammes();
|
|
1389
|
-
renderAll();
|
|
1390
|
-
document.getElementById("colorPanel").style.display = "flex";
|
|
1391
|
-
}
|
|
1392
|
-
|
|
1393
|
-
function setShapeTextAlign(align) {
|
|
1394
|
-
if (selectedIds.length === 0) return;
|
|
1395
|
-
pushHistory();
|
|
1396
|
-
var diag = getCurrentDiagram();
|
|
1397
|
-
selectedIds.forEach(function (id) {
|
|
1398
|
-
var shape = diag.shapes.find(function (s) { return s.id === id; });
|
|
1399
|
-
if (shape) shape.textAlign = align;
|
|
1400
|
-
});
|
|
1401
|
-
saveDiagrammes();
|
|
1402
|
-
renderAll();
|
|
1403
|
-
document.getElementById("colorPanel").style.display = "flex";
|
|
1404
|
-
}
|
|
1405
|
-
|
|
1406
|
-
function changeShapeFontSize(delta) {
|
|
1407
|
-
if (selectedIds.length === 0) return;
|
|
1408
|
-
pushHistory();
|
|
1409
|
-
var diag = getCurrentDiagram();
|
|
1410
|
-
selectedIds.forEach(function (id) {
|
|
1411
|
-
var shape = diag.shapes.find(function (s) { return s.id === id; });
|
|
1412
|
-
if (!shape) return;
|
|
1413
|
-
var current = shape.fontSize || (shape.type === "text" ? 13 : 12);
|
|
1414
|
-
shape.fontSize = Math.max(8, Math.min(28, current + delta));
|
|
1415
|
-
});
|
|
1416
|
-
saveDiagrammes();
|
|
1417
|
-
renderAll();
|
|
1418
|
-
document.getElementById("colorPanel").style.display = "flex";
|
|
1419
|
-
}
|
|
1420
|
-
|
|
1421
|
-
// ── Copie de style (pick mode) ──
|
|
1422
|
-
function startPickMode(mode) {
|
|
1423
|
-
pickTargetIds = selectedIds.slice();
|
|
1424
|
-
pickMode = mode;
|
|
1425
|
-
document.getElementById("canvas").style.cursor = "crosshair";
|
|
1426
|
-
document.getElementById("btnPickFont").classList.toggle("diagram-pick-active", mode === "fontSize");
|
|
1427
|
-
document.getElementById("btnPickStyle").classList.toggle("diagram-pick-active", mode === "fullStyle");
|
|
1428
|
-
}
|
|
1429
|
-
|
|
1430
|
-
function cancelPickMode() {
|
|
1431
|
-
pickMode = null;
|
|
1432
|
-
pickTargetIds = [];
|
|
1433
|
-
document.getElementById("canvas").style.cursor = "";
|
|
1434
|
-
document.getElementById("btnPickFont").classList.remove("diagram-pick-active");
|
|
1435
|
-
document.getElementById("btnPickStyle").classList.remove("diagram-pick-active");
|
|
1436
|
-
}
|
|
1437
|
-
|
|
1438
|
-
function applyPickMode(srcShape) {
|
|
1439
|
-
pushHistory();
|
|
1440
|
-
var diag = getCurrentDiagram();
|
|
1441
|
-
pickTargetIds.forEach(function (id) {
|
|
1442
|
-
if (id === srcShape.id) return;
|
|
1443
|
-
var shape = diag.shapes.find(function (s) { return s.id === id; });
|
|
1444
|
-
if (!shape) return;
|
|
1445
|
-
if (pickMode === "fontSize") {
|
|
1446
|
-
shape.fontSize = srcShape.fontSize || (srcShape.type === "text" ? 13 : 12);
|
|
1447
|
-
} else if (pickMode === "fullStyle") {
|
|
1448
|
-
shape.fontSize = srcShape.fontSize || (srcShape.type === "text" ? 13 : 12);
|
|
1449
|
-
shape.color = srcShape.color;
|
|
1450
|
-
shape.type = srcShape.type;
|
|
1451
|
-
shape.w = srcShape.w;
|
|
1452
|
-
shape.h = srcShape.h;
|
|
1453
|
-
shape.textAlign = srcShape.textAlign || "center";
|
|
1454
|
-
shape.textValign = srcShape.textValign || "middle";
|
|
1455
|
-
shape.rotation = srcShape.rotation || 0;
|
|
1456
|
-
}
|
|
1457
|
-
});
|
|
1458
|
-
selectedIds = pickTargetIds.slice();
|
|
1459
|
-
saveDiagrammes();
|
|
1460
|
-
renderAll();
|
|
1461
|
-
document.getElementById("colorPanel").style.display = "flex";
|
|
1462
|
-
}
|
|
1463
|
-
|
|
1464
|
-
function rotateShape(delta) {
|
|
1465
|
-
var diag = getCurrentDiagram();
|
|
1466
|
-
if (!diag || selectedIds.length === 0) return;
|
|
1467
|
-
pushHistory();
|
|
1468
|
-
selectedIds.forEach(function (id) {
|
|
1469
|
-
var s = diag.shapes.find(function (sh) { return sh.id === id; });
|
|
1470
|
-
if (!s || s.type === "image") return;
|
|
1471
|
-
s.rotation = ((s.rotation || 0) + delta + 360) % 360;
|
|
1472
|
-
});
|
|
1473
|
-
saveDiagrammes();
|
|
1474
|
-
renderAll();
|
|
1475
|
-
}
|
|
1476
|
-
|
|
1477
|
-
function changeShapeOrder(delta) {
|
|
1478
|
-
var diag = getCurrentDiagram();
|
|
1479
|
-
if (!diag || selectedIds.length === 0) return;
|
|
1480
|
-
pushHistory();
|
|
1481
|
-
if (delta > 0) {
|
|
1482
|
-
// Avancer : parcourir de la fin vers le début
|
|
1483
|
-
for (var i = diag.shapes.length - 1; i >= 0; i--) {
|
|
1484
|
-
if (selectedIds.indexOf(diag.shapes[i].id) !== -1 && i < diag.shapes.length - 1) {
|
|
1485
|
-
var tmp = diag.shapes[i]; diag.shapes[i] = diag.shapes[i + 1]; diag.shapes[i + 1] = tmp;
|
|
1486
|
-
}
|
|
1487
|
-
}
|
|
1488
|
-
} else {
|
|
1489
|
-
// Reculer : parcourir du début vers la fin
|
|
1490
|
-
for (var i = 0; i < diag.shapes.length; i++) {
|
|
1491
|
-
if (selectedIds.indexOf(diag.shapes[i].id) !== -1 && i > 0) {
|
|
1492
|
-
var tmp = diag.shapes[i]; diag.shapes[i] = diag.shapes[i - 1]; diag.shapes[i - 1] = tmp;
|
|
1493
|
-
}
|
|
1494
|
-
}
|
|
1495
|
-
}
|
|
1496
|
-
saveDiagrammes();
|
|
1497
|
-
renderAll();
|
|
1498
|
-
}
|
|
1499
|
-
|
|
1500
|
-
// ── Édition texte inline ──
|
|
1501
|
-
function startTextEdit(shapeId) {
|
|
1502
|
-
var diag = getCurrentDiagram();
|
|
1503
|
-
var shape = diag.shapes.find(function (s) { return s.id === shapeId; });
|
|
1504
|
-
if (!shape || shape.type === "image") return;
|
|
1505
|
-
if (shape.type === "table") return;
|
|
1506
|
-
editingShapeId = shapeId;
|
|
1507
|
-
|
|
1508
|
-
var svg = document.getElementById("canvas");
|
|
1509
|
-
var sr = svg.getBoundingClientRect();
|
|
1510
|
-
var sx = shape.x * viewTransform.scale + viewTransform.x + sr.left;
|
|
1511
|
-
var sy = shape.y * viewTransform.scale + viewTransform.y + sr.top;
|
|
1512
|
-
var sw = shape.w * viewTransform.scale;
|
|
1513
|
-
var sh = shape.h * viewTransform.scale;
|
|
1514
|
-
var c = COLORS[shape.color] || COLORS[DEFAULT_COLOR];
|
|
1515
|
-
|
|
1516
|
-
var useTextarea = shape.type === "postit" || shape.type === "rect" || shape.type === "rounded" || shape.type === "db" || shape.type === "cloud" || shape.type === "nuage";
|
|
1517
|
-
if (useTextarea) {
|
|
1518
|
-
var ta = document.getElementById("postitTextInput");
|
|
1519
|
-
ta.value = shape.text || "";
|
|
1520
|
-
var hpad = shape.type === "cloud" ? Math.round(sw * 0.15) : shape.type === "nuage" ? Math.round(sw * 0.14) : 8;
|
|
1521
|
-
var vtop, vheight;
|
|
1522
|
-
if (shape.type === "cloud") {
|
|
1523
|
-
vtop = sy + Math.round(sh * 0.12);
|
|
1524
|
-
vheight = sh - Math.round(sh * 0.12) * 2;
|
|
1525
|
-
} else if (shape.type === "nuage") {
|
|
1526
|
-
// Le nuage Bootstrap : haut visible ≈ y=2/16, bas de la zone texte ≈ y=10/16
|
|
1527
|
-
var nuageTop = Math.round(sh * 2 / 16);
|
|
1528
|
-
var nuageBot = Math.round(sh * 10 / 16);
|
|
1529
|
-
vtop = sy + nuageTop;
|
|
1530
|
-
vheight = nuageBot - nuageTop - 4;
|
|
1531
|
-
} else if (shape.type === "db") {
|
|
1532
|
-
var dbRy = Math.min(12, shape.h * 0.2) * viewTransform.scale;
|
|
1533
|
-
vtop = sy + dbRy * 2 + 4;
|
|
1534
|
-
vheight = sh - dbRy * 2 - 12;
|
|
1535
|
-
} else {
|
|
1536
|
-
vtop = sy + 8;
|
|
1537
|
-
vheight = sh - 16;
|
|
1538
|
-
}
|
|
1539
|
-
ta.style.left = (sx + hpad) + "px";
|
|
1540
|
-
ta.style.top = vtop + "px";
|
|
1541
|
-
ta.style.width = (sw - hpad * 2) + "px";
|
|
1542
|
-
ta.style.height = vheight + "px";
|
|
1543
|
-
var editFs = shape.fontSize || (shape.type === "text" ? 13 : 12);
|
|
1544
|
-
ta.style.fontSize = Math.max(10, editFs * viewTransform.scale) + "px";
|
|
1545
|
-
ta.style.color = c.text;
|
|
1546
|
-
ta.style.display = "block";
|
|
1547
|
-
document.getElementById("shapeTextInput").style.display = "none";
|
|
1548
|
-
document.getElementById("textOverlay").style.display = "block";
|
|
1549
|
-
// Masquer le texte SVG pour éviter la double lecture
|
|
1550
|
-
var sg = document.querySelector('#shapesLayer [data-id="' + shapeId + '"]');
|
|
1551
|
-
if (sg) { var tx = sg.querySelector("text"); if (tx) tx.style.visibility = "hidden"; }
|
|
1552
|
-
setTimeout(function () { ta.focus(); ta.select(); }, 10);
|
|
1553
|
-
} else {
|
|
1554
|
-
var input = document.getElementById("shapeTextInput");
|
|
1555
|
-
input.value = shape.text || "";
|
|
1556
|
-
if (shape.type === "actor") {
|
|
1557
|
-
input.style.left = sx + "px";
|
|
1558
|
-
input.style.top = (sy + sh * 0.84) + "px";
|
|
1559
|
-
input.style.width = sw + "px";
|
|
1560
|
-
} else {
|
|
1561
|
-
input.style.left = (sx + sw * 0.08) + "px";
|
|
1562
|
-
input.style.top = (sy + sh * 0.22) + "px";
|
|
1563
|
-
input.style.width = (sw * 0.84) + "px";
|
|
1564
|
-
}
|
|
1565
|
-
var editFs2 = shape.fontSize || 13;
|
|
1566
|
-
input.style.fontSize = Math.max(10, editFs2 * viewTransform.scale) + "px";
|
|
1567
|
-
input.style.color = c.text;
|
|
1568
|
-
input.style.display = "block";
|
|
1569
|
-
document.getElementById("postitTextInput").style.display = "none";
|
|
1570
|
-
document.getElementById("textOverlay").style.display = "block";
|
|
1571
|
-
setTimeout(function () { input.focus(); input.select(); }, 10);
|
|
1572
|
-
}
|
|
1573
|
-
}
|
|
1574
|
-
|
|
1575
|
-
function confirmTextEdit() {
|
|
1576
|
-
var input = document.getElementById("shapeTextInput");
|
|
1577
|
-
var diag = getCurrentDiagram();
|
|
1578
|
-
if (editingShapeId) {
|
|
1579
|
-
var shape = diag.shapes.find(function (s) { return s.id === editingShapeId; });
|
|
1580
|
-
if (shape) {
|
|
1581
|
-
if (shape.type === "table" && editingTableCell !== null) {
|
|
1582
|
-
var tval = document.getElementById("postitTextInput").value;
|
|
1583
|
-
var oldCellVal = (shape.cells && shape.cells[editingTableCell.row] && shape.cells[editingTableCell.row][editingTableCell.col]) || "";
|
|
1584
|
-
if (tval !== oldCellVal) pushHistory();
|
|
1585
|
-
if (!shape.cells) shape.cells = [];
|
|
1586
|
-
while (shape.cells.length <= editingTableCell.row) shape.cells.push([]);
|
|
1587
|
-
shape.cells[editingTableCell.row][editingTableCell.col] = tval;
|
|
1588
|
-
editingTableCell = null;
|
|
1589
|
-
} else {
|
|
1590
|
-
var usesTextarea = shape.type === "postit" || shape.type === "rect" || shape.type === "rounded" || shape.type === "db" || shape.type === "cloud" || shape.type === "nuage";
|
|
1591
|
-
var val = usesTextarea
|
|
1592
|
-
? document.getElementById("postitTextInput").value
|
|
1593
|
-
: input.value;
|
|
1594
|
-
if (val !== (shape.text || "")) pushHistory();
|
|
1595
|
-
shape.text = val;
|
|
1596
|
-
}
|
|
1597
|
-
saveDiagrammes();
|
|
1598
|
-
renderAll();
|
|
1599
|
-
}
|
|
1600
|
-
editingShapeId = null;
|
|
1601
|
-
} else if (editingArrowId) {
|
|
1602
|
-
var arrow = (diag.arrows || []).find(function (a) { return a.id === editingArrowId; });
|
|
1603
|
-
if (arrow) {
|
|
1604
|
-
if (input.value !== (arrow.label || "")) pushHistory();
|
|
1605
|
-
arrow.label = input.value; saveDiagrammes(); renderAll();
|
|
1606
|
-
}
|
|
1607
|
-
editingArrowId = null;
|
|
1608
|
-
}
|
|
1609
|
-
document.getElementById("textOverlay").style.display = "none";
|
|
1610
|
-
document.getElementById("postitTextInput").style.display = "none";
|
|
1611
|
-
document.getElementById("shapeTextInput").style.display = "block";
|
|
1612
|
-
}
|
|
1613
|
-
|
|
1614
|
-
function startTableCellEdit(shapeId, row, col) {
|
|
1615
|
-
var diag = getCurrentDiagram();
|
|
1616
|
-
var shape = diag.shapes.find(function (s) { return s.id === shapeId; });
|
|
1617
|
-
if (!shape) return;
|
|
1618
|
-
editingShapeId = shapeId;
|
|
1619
|
-
editingTableCell = { row: row, col: col };
|
|
1620
|
-
|
|
1621
|
-
var tRows = shape.rows || 3;
|
|
1622
|
-
var scColWidths = getColWidths(shape);
|
|
1623
|
-
var scColOffsets = getColOffsets(shape);
|
|
1624
|
-
var cellW = scColWidths[col], cellH = shape.h / tRows;
|
|
1625
|
-
var cellX = shape.x + scColOffsets[col], cellY = shape.y + row * cellH;
|
|
1626
|
-
|
|
1627
|
-
var svgEl = document.getElementById("canvas");
|
|
1628
|
-
var sr = svgEl.getBoundingClientRect();
|
|
1629
|
-
var csx = cellX * viewTransform.scale + viewTransform.x + sr.left;
|
|
1630
|
-
var csy = cellY * viewTransform.scale + viewTransform.y + sr.top;
|
|
1631
|
-
var csw = cellW * viewTransform.scale, csh = cellH * viewTransform.scale;
|
|
1632
|
-
var cc = COLORS[shape.color] || COLORS[DEFAULT_COLOR];
|
|
1633
|
-
|
|
1634
|
-
var ta = document.getElementById("postitTextInput");
|
|
1635
|
-
ta.value = (shape.cells && shape.cells[row] && shape.cells[row][col]) || "";
|
|
1636
|
-
ta.style.left = (csx + 2) + "px";
|
|
1637
|
-
ta.style.top = (csy + 2) + "px";
|
|
1638
|
-
ta.style.width = (csw - 4) + "px";
|
|
1639
|
-
ta.style.height = (csh - 4) + "px";
|
|
1640
|
-
var tfs = shape.fontSize || 12;
|
|
1641
|
-
ta.style.fontSize = Math.max(10, tfs * viewTransform.scale) + "px";
|
|
1642
|
-
ta.style.color = cc.text;
|
|
1643
|
-
ta.style.display = "block";
|
|
1644
|
-
document.getElementById("shapeTextInput").style.display = "none";
|
|
1645
|
-
document.getElementById("textOverlay").style.display = "block";
|
|
1646
|
-
var sg = document.querySelector('#shapesLayer [data-id="' + shapeId + '"]');
|
|
1647
|
-
if (sg) { var cellTx = sg.querySelector('text[data-cell="' + row + '-' + col + '"]'); if (cellTx) cellTx.style.visibility = "hidden"; }
|
|
1648
|
-
setTimeout(function () { ta.focus(); ta.select(); }, 10);
|
|
1649
|
-
}
|
|
1650
|
-
|
|
1651
|
-
function addTableRow() {
|
|
1652
|
-
var diag = getCurrentDiagram();
|
|
1653
|
-
var shape = selectedIds.length === 1 ? diag.shapes.find(function (s) { return s.id === selectedIds[0]; }) : null;
|
|
1654
|
-
if (!shape || shape.type !== "table") return;
|
|
1655
|
-
pushHistory();
|
|
1656
|
-
shape.rows = (shape.rows || 3) + 1;
|
|
1657
|
-
if (!shape.cells) shape.cells = [];
|
|
1658
|
-
while (shape.cells.length < shape.rows) shape.cells.push([]);
|
|
1659
|
-
shape.cells[shape.rows - 1] = new Array(shape.cols || 3).fill("");
|
|
1660
|
-
saveDiagrammes(); renderAll();
|
|
1661
|
-
document.getElementById("colorPanel").style.display = "flex";
|
|
1662
|
-
syncColorPanel();
|
|
1663
|
-
}
|
|
1664
|
-
|
|
1665
|
-
function removeTableRow() {
|
|
1666
|
-
var diag = getCurrentDiagram();
|
|
1667
|
-
var shape = selectedIds.length === 1 ? diag.shapes.find(function (s) { return s.id === selectedIds[0]; }) : null;
|
|
1668
|
-
if (!shape || shape.type !== "table" || (shape.rows || 3) <= 1) return;
|
|
1669
|
-
pushHistory();
|
|
1670
|
-
shape.rows = (shape.rows || 3) - 1;
|
|
1671
|
-
if (shape.cells && shape.cells.length > shape.rows) shape.cells.splice(shape.rows);
|
|
1672
|
-
saveDiagrammes(); renderAll();
|
|
1673
|
-
document.getElementById("colorPanel").style.display = "flex";
|
|
1674
|
-
syncColorPanel();
|
|
1675
|
-
}
|
|
1676
|
-
|
|
1677
|
-
function addTableCol() {
|
|
1678
|
-
var diag = getCurrentDiagram();
|
|
1679
|
-
var shape = selectedIds.length === 1 ? diag.shapes.find(function (s) { return s.id === selectedIds[0]; }) : null;
|
|
1680
|
-
if (!shape || shape.type !== "table") return;
|
|
1681
|
-
pushHistory();
|
|
1682
|
-
var oldCols = shape.cols || 3;
|
|
1683
|
-
shape.cols = oldCols + 1;
|
|
1684
|
-
if (!shape.cells) shape.cells = [];
|
|
1685
|
-
var rows = shape.rows || 3;
|
|
1686
|
-
for (var ri = 0; ri < rows; ri++) {
|
|
1687
|
-
if (!shape.cells[ri]) shape.cells[ri] = [];
|
|
1688
|
-
shape.cells[ri].push("");
|
|
1689
|
-
}
|
|
1690
|
-
// Répartir la largeur : réduire les colonnes existantes proportionnellement
|
|
1691
|
-
var oldWidths = getColWidths({ cols: oldCols, w: shape.w, colWidths: shape.colWidths });
|
|
1692
|
-
var newColW = shape.w / shape.cols;
|
|
1693
|
-
var scale = (shape.w - newColW) / shape.w;
|
|
1694
|
-
shape.colWidths = oldWidths.map(function (w) { return w * scale; });
|
|
1695
|
-
shape.colWidths.push(newColW);
|
|
1696
|
-
saveDiagrammes(); renderAll();
|
|
1697
|
-
document.getElementById("colorPanel").style.display = "flex";
|
|
1698
|
-
syncColorPanel();
|
|
1699
|
-
}
|
|
1700
|
-
|
|
1701
|
-
function removeTableCol() {
|
|
1702
|
-
var diag = getCurrentDiagram();
|
|
1703
|
-
var shape = selectedIds.length === 1 ? diag.shapes.find(function (s) { return s.id === selectedIds[0]; }) : null;
|
|
1704
|
-
if (!shape || shape.type !== "table" || (shape.cols || 3) <= 1) return;
|
|
1705
|
-
pushHistory();
|
|
1706
|
-
var oldCols = shape.cols || 3;
|
|
1707
|
-
shape.cols = oldCols - 1;
|
|
1708
|
-
if (shape.cells) shape.cells.forEach(function (row) { if (row.length > shape.cols) row.splice(shape.cols); });
|
|
1709
|
-
// Retirer la dernière colonne et rescaler les restantes pour remplir shape.w
|
|
1710
|
-
var cw = getColWidths({ cols: oldCols, w: shape.w, colWidths: shape.colWidths });
|
|
1711
|
-
cw.splice(shape.cols);
|
|
1712
|
-
var total = cw.reduce(function (s, v) { return s + v; }, 0);
|
|
1713
|
-
shape.colWidths = cw.map(function (w) { return w * shape.w / total; });
|
|
1714
|
-
saveDiagrammes(); renderAll();
|
|
1715
|
-
document.getElementById("colorPanel").style.display = "flex";
|
|
1716
|
-
syncColorPanel();
|
|
1717
|
-
}
|
|
1718
|
-
|
|
1719
|
-
// ── Overlay tableau (boutons +/− ligne/colonne sur le canvas) ──
|
|
1720
|
-
function updateTableOverlay() {
|
|
1721
|
-
var ids = ["tcBtnAddCol","tcBtnRemoveCol","tcBtnAddRow","tcBtnRemoveRow"];
|
|
1722
|
-
var hide = function () { ids.forEach(function (id) { var el = document.getElementById(id); if (el) el.style.display = "none"; }); };
|
|
1723
|
-
if (boardLocked || selectedIds.length !== 1 || selectedType !== "shape") { hide(); return; }
|
|
1724
|
-
var diag = getCurrentDiagram();
|
|
1725
|
-
if (!diag) { hide(); return; }
|
|
1726
|
-
var shape = diag.shapes.find(function (s) { return s.id === selectedIds[0]; });
|
|
1727
|
-
if (!shape || shape.type !== "table") { hide(); return; }
|
|
1728
|
-
|
|
1729
|
-
var svg = document.getElementById("canvas");
|
|
1730
|
-
var sr = svg.getBoundingClientRect();
|
|
1731
|
-
var sc = viewTransform.scale;
|
|
1732
|
-
var tx = viewTransform.x, ty = viewTransform.y;
|
|
1733
|
-
var right = (shape.x + shape.w) * sc + tx + sr.left;
|
|
1734
|
-
var bottom = (shape.y + shape.h) * sc + ty + sr.top;
|
|
1735
|
-
var rows = shape.rows || 3;
|
|
1736
|
-
var cellH = shape.h * sc / rows;
|
|
1737
|
-
var BW = 22, GAP = 4;
|
|
1738
|
-
|
|
1739
|
-
// +/− col : à droite de la dernière ligne, centrés verticalement dans cette ligne
|
|
1740
|
-
var lastRowCy = bottom - cellH / 2;
|
|
1741
|
-
var ac = document.getElementById("tcBtnAddCol");
|
|
1742
|
-
var rc = document.getElementById("tcBtnRemoveCol");
|
|
1743
|
-
ac.style.left = (right + GAP) + "px"; ac.style.top = (lastRowCy - BW - 2) + "px";
|
|
1744
|
-
rc.style.left = (right + GAP) + "px"; rc.style.top = (lastRowCy + 2) + "px";
|
|
1745
|
-
ac.style.display = rc.style.display = "flex";
|
|
1746
|
-
|
|
1747
|
-
// +/− row : sous la dernière ligne, alignés à droite de la table
|
|
1748
|
-
var ar = document.getElementById("tcBtnAddRow");
|
|
1749
|
-
var rr = document.getElementById("tcBtnRemoveRow");
|
|
1750
|
-
ar.style.left = (right - BW * 2 - GAP) + "px"; ar.style.top = (bottom + GAP) + "px";
|
|
1751
|
-
rr.style.left = (right - BW - 2) + "px"; rr.style.top = (bottom + GAP) + "px";
|
|
1752
|
-
ar.style.display = rr.style.display = "flex";
|
|
1753
|
-
}
|
|
1754
|
-
|
|
1755
|
-
function syncColorPanel() {
|
|
1756
|
-
var btn = document.getElementById("btnShapeLink");
|
|
1757
|
-
if (!btn) return;
|
|
1758
|
-
if (selectedIds.length === 1 && selectedType === "shape") {
|
|
1759
|
-
var diag = getCurrentDiagram();
|
|
1760
|
-
var shape = diag ? diag.shapes.find(function (s) { return s.id === selectedIds[0]; }) : null;
|
|
1761
|
-
if (shape && shape.linkedDiagramId) {
|
|
1762
|
-
btn.classList.add("active");
|
|
1763
|
-
var linked = findDiagramById(shape.linkedDiagramId, diagramsList);
|
|
1764
|
-
btn.title = linked ? "Lié à : " + linked.titre + " (cliquer pour délier)" : "Supprimer le lien";
|
|
1765
|
-
} else if (shape && shape.externalUrl) {
|
|
1766
|
-
btn.classList.add("active");
|
|
1767
|
-
btn.title = "Lien externe : " + shape.externalUrl + " (cliquer pour délier)";
|
|
1768
|
-
} else {
|
|
1769
|
-
btn.classList.remove("active");
|
|
1770
|
-
btn.title = "Lier à un diagramme enfant ou URL externe";
|
|
1771
|
-
}
|
|
1772
|
-
btn.style.display = "";
|
|
1773
|
-
} else {
|
|
1774
|
-
btn.classList.remove("active");
|
|
1775
|
-
btn.style.display = "none";
|
|
1776
|
-
}
|
|
1777
|
-
}
|
|
1778
|
-
|
|
1779
|
-
function toggleShapeLink() {
|
|
1780
|
-
if (selectedIds.length !== 1 || selectedType !== "shape") return;
|
|
1781
|
-
var diag = getCurrentDiagram();
|
|
1782
|
-
var shape = diag ? diag.shapes.find(function (s) { return s.id === selectedIds[0]; }) : null;
|
|
1783
|
-
if (!shape) return;
|
|
1784
|
-
if (shape.linkedDiagramId || shape.externalUrl) {
|
|
1785
|
-
pushHistory();
|
|
1786
|
-
delete shape.linkedDiagramId;
|
|
1787
|
-
delete shape.externalUrl;
|
|
1788
|
-
saveDiagrammes();
|
|
1789
|
-
renderAll();
|
|
1790
|
-
syncColorPanel();
|
|
1791
|
-
hideLinkPicker();
|
|
1792
|
-
} else {
|
|
1793
|
-
showLinkPicker();
|
|
1794
|
-
}
|
|
1795
|
-
}
|
|
1796
|
-
|
|
1797
|
-
function showLinkPicker() {
|
|
1798
|
-
var panel = document.getElementById("linkPickerPanel");
|
|
1799
|
-
if (!panel) return;
|
|
1800
|
-
var btn = document.getElementById("btnShapeLink");
|
|
1801
|
-
var r = btn ? btn.getBoundingClientRect() : { bottom: 100, left: 200 };
|
|
1802
|
-
panel.style.top = (r.bottom + 6) + "px";
|
|
1803
|
-
panel.style.left = r.left + "px";
|
|
1804
|
-
var all = flattenDiagrams(diagramsList);
|
|
1805
|
-
var curId = String(currentDiagramId);
|
|
1806
|
-
var items = all.filter(function (d) { return String(d.id) !== curId; });
|
|
1807
|
-
var html = items.map(function (d) {
|
|
1808
|
-
return '<div class="link-picker-item" onclick="lierForme(\'' + d.id + '\')">' + escDiag(d.titre) + '</div>';
|
|
1809
|
-
}).join("");
|
|
1810
|
-
html += '<div class="link-picker-new" onclick="creerEnfantEtLier()">+ Nouveau diagramme enfant</div>';
|
|
1811
|
-
html += '<div class="link-picker-new link-picker-ext-toggle" onclick="showExternalLinkInput()" style="display:flex;align-items:center;gap:6px;">'
|
|
1812
|
-
+ '<svg width="14" height="14" viewBox="0 0 14 14"><circle cx="7" cy="7" r="6" fill="#0284c7" stroke="#fff" stroke-width="1.5"/><text x="7" y="10.5" text-anchor="middle" font-size="8" font-weight="bold" fill="#fff">\u2197</text></svg>'
|
|
1813
|
-
+ 'Lien externe (URL)</div>';
|
|
1814
|
-
html += '<div id="linkPickerExtRow" style="display:none;padding:6px 8px;border-top:1px solid #e7e5e4;">'
|
|
1815
|
-
+ '<input id="linkPickerExtUrl" type="text" placeholder="https://..." '
|
|
1816
|
-
+ 'style="width:100%;box-sizing:border-box;padding:4px 6px;border:1px solid #d4d4d4;border-radius:4px;font-size:12px;" '
|
|
1817
|
-
+ 'onkeydown="if(event.key===\'Enter\')lierFormeExterne()">'
|
|
1818
|
-
+ '<div id="linkPickerExtErr" style="color:#e11d48;font-size:11px;margin-top:3px;display:none;">URL invalide (http://, https:// ou file://)</div>'
|
|
1819
|
-
+ '</div>';
|
|
1820
|
-
document.getElementById("linkPickerList").innerHTML = html;
|
|
1821
|
-
panel.style.display = "block";
|
|
1822
|
-
}
|
|
1823
|
-
|
|
1824
|
-
function showExternalLinkInput() {
|
|
1825
|
-
var row = document.getElementById("linkPickerExtRow");
|
|
1826
|
-
if (!row) return;
|
|
1827
|
-
row.style.display = "block";
|
|
1828
|
-
var input = document.getElementById("linkPickerExtUrl");
|
|
1829
|
-
if (input) setTimeout(function () { input.focus(); }, 10);
|
|
1830
|
-
}
|
|
1831
|
-
|
|
1832
|
-
function lierFormeExterne() {
|
|
1833
|
-
var input = document.getElementById("linkPickerExtUrl");
|
|
1834
|
-
if (!input) return;
|
|
1835
|
-
var url = input.value.trim();
|
|
1836
|
-
var err = document.getElementById("linkPickerExtErr");
|
|
1837
|
-
if (!/^(https?|file):\/\//i.test(url)) {
|
|
1838
|
-
if (err) err.style.display = "block";
|
|
1839
|
-
return;
|
|
1840
|
-
}
|
|
1841
|
-
if (err) err.style.display = "none";
|
|
1842
|
-
if (selectedIds.length !== 1) return;
|
|
1843
|
-
var diag = getCurrentDiagram();
|
|
1844
|
-
var shape = diag ? diag.shapes.find(function (s) { return s.id === selectedIds[0]; }) : null;
|
|
1845
|
-
if (!shape) return;
|
|
1846
|
-
pushHistory();
|
|
1847
|
-
shape.externalUrl = url;
|
|
1848
|
-
delete shape.linkedDiagramId;
|
|
1849
|
-
hideLinkPicker();
|
|
1850
|
-
saveDiagrammes();
|
|
1851
|
-
renderAll();
|
|
1852
|
-
syncColorPanel();
|
|
1853
|
-
}
|
|
1854
|
-
|
|
1855
|
-
function hideLinkPicker() {
|
|
1856
|
-
var panel = document.getElementById("linkPickerPanel");
|
|
1857
|
-
if (panel) panel.style.display = "none";
|
|
1858
|
-
}
|
|
1859
|
-
|
|
1860
|
-
function lierForme(diagId) {
|
|
1861
|
-
if (selectedIds.length !== 1) return;
|
|
1862
|
-
var diag = getCurrentDiagram();
|
|
1863
|
-
var shape = diag ? diag.shapes.find(function (s) { return s.id === selectedIds[0]; }) : null;
|
|
1864
|
-
if (!shape) return;
|
|
1865
|
-
pushHistory();
|
|
1866
|
-
shape.linkedDiagramId = String(diagId);
|
|
1867
|
-
delete shape.externalUrl;
|
|
1868
|
-
hideLinkPicker();
|
|
1869
|
-
saveDiagrammes();
|
|
1870
|
-
renderAll();
|
|
1871
|
-
syncColorPanel();
|
|
1872
|
-
}
|
|
1873
|
-
|
|
1874
|
-
function creerEnfantEtLier() {
|
|
1875
|
-
if (selectedIds.length !== 1) return;
|
|
1876
|
-
var diag = getCurrentDiagram();
|
|
1877
|
-
var shape = diag ? diag.shapes.find(function (s) { return s.id === selectedIds[0]; }) : null;
|
|
1878
|
-
if (!shape) return;
|
|
1879
|
-
hideLinkPicker();
|
|
1880
|
-
var titre = shape.text || "Sous-diagramme";
|
|
1881
|
-
var child = { id: Date.now(), titre: titre, shapes: [], arrows: [], children: [] };
|
|
1882
|
-
if (!diag.children) diag.children = [];
|
|
1883
|
-
diag.children.push(child);
|
|
1884
|
-
diagExpandedIds[String(diag.id)] = true;
|
|
1885
|
-
pushHistory();
|
|
1886
|
-
shape.linkedDiagramId = String(child.id);
|
|
1887
|
-
saveDiagrammes();
|
|
1888
|
-
renderAll();
|
|
1889
|
-
renderDiagramList();
|
|
1890
|
-
syncColorPanel();
|
|
1891
|
-
}
|
|
1892
|
-
|
|
1893
|
-
function startArrowTextEdit(arrowId) {
|
|
1894
|
-
var diag = getCurrentDiagram();
|
|
1895
|
-
var arrow = (diag.arrows || []).find(function (a) { return a.id === arrowId; });
|
|
1896
|
-
if (!arrow) return;
|
|
1897
|
-
var from = diag.shapes.find(function (s) { return s.id === arrow.from; });
|
|
1898
|
-
var to = diag.shapes.find(function (s) { return s.id === arrow.to; });
|
|
1899
|
-
if (!from || !to) return;
|
|
1900
|
-
|
|
1901
|
-
var toCx = to.x + to.w / 2, toCy = to.y + to.h / 2;
|
|
1902
|
-
var frCx = from.x + from.w / 2, frCy = from.y + from.h / 2;
|
|
1903
|
-
var fp = getEdgePoint(from, toCx, toCy);
|
|
1904
|
-
var tp = getEdgePoint(to, frCx, frCy);
|
|
1905
|
-
var mx = (fp.x + tp.x) / 2;
|
|
1906
|
-
var my = (fp.y + tp.y) / 2;
|
|
1907
|
-
|
|
1908
|
-
var svg = document.getElementById("canvas");
|
|
1909
|
-
var sr = svg.getBoundingClientRect();
|
|
1910
|
-
var sx = mx * viewTransform.scale + viewTransform.x + sr.left;
|
|
1911
|
-
var sy = my * viewTransform.scale + viewTransform.y + sr.top;
|
|
1912
|
-
|
|
1913
|
-
editingArrowId = arrowId;
|
|
1914
|
-
var input = document.getElementById("shapeTextInput");
|
|
1915
|
-
input.value = arrow.label || "";
|
|
1916
|
-
input.style.left = (sx - 60) + "px";
|
|
1917
|
-
input.style.top = (sy - 12) + "px";
|
|
1918
|
-
input.style.width = "120px";
|
|
1919
|
-
input.style.fontSize = Math.max(10, 11 * viewTransform.scale) + "px";
|
|
1920
|
-
input.style.color = "#78716c";
|
|
1921
|
-
|
|
1922
|
-
document.getElementById("textOverlay").style.display = "block";
|
|
1923
|
-
setTimeout(function () { input.focus(); input.select(); }, 10);
|
|
1924
|
-
}
|
|
1925
|
-
|
|
1926
|
-
// ── Hit-test ──
|
|
1927
|
-
function shapeAt(x, y) {
|
|
1928
|
-
var diag = getCurrentDiagram();
|
|
1929
|
-
if (!diag) return null;
|
|
1930
|
-
for (var i = diag.shapes.length - 1; i >= 0; i--) {
|
|
1931
|
-
var s = diag.shapes[i];
|
|
1932
|
-
var lx = x, ly = y;
|
|
1933
|
-
if (s.rotation) {
|
|
1934
|
-
var scx = s.x + s.w / 2, scy = s.y + s.h / 2;
|
|
1935
|
-
var rad = -s.rotation * Math.PI / 180;
|
|
1936
|
-
var cos = Math.cos(rad), sin = Math.sin(rad);
|
|
1937
|
-
var ddx = x - scx, ddy = y - scy;
|
|
1938
|
-
lx = scx + ddx * cos - ddy * sin;
|
|
1939
|
-
ly = scy + ddx * sin + ddy * cos;
|
|
1940
|
-
}
|
|
1941
|
-
if (lx >= s.x && lx <= s.x + s.w && ly >= s.y && ly <= s.y + s.h) return s;
|
|
1942
|
-
}
|
|
1943
|
-
return null;
|
|
1944
|
-
}
|
|
1945
|
-
|
|
1946
|
-
function arrowIdAt(x, y) {
|
|
1947
|
-
var groups = document.querySelectorAll(".arrow-group");
|
|
1948
|
-
for (var i = 0; i < groups.length; i++) {
|
|
1949
|
-
var lines = groups[i].querySelectorAll("line");
|
|
1950
|
-
if (lines.length < 2) continue;
|
|
1951
|
-
var hit = lines[lines.length - 1];
|
|
1952
|
-
var x1 = +hit.getAttribute("x1"), y1 = +hit.getAttribute("y1");
|
|
1953
|
-
var x2 = +hit.getAttribute("x2"), y2 = +hit.getAttribute("y2");
|
|
1954
|
-
if (distToSeg(x, y, x1, y1, x2, y2) < 8) {
|
|
1955
|
-
return groups[i].getAttribute("data-id");
|
|
1956
|
-
}
|
|
1957
|
-
}
|
|
1958
|
-
return null;
|
|
1959
|
-
}
|
|
1960
|
-
|
|
1961
|
-
function distToSeg(px, py, x1, y1, x2, y2) {
|
|
1962
|
-
var dx = x2 - x1, dy = y2 - y1;
|
|
1963
|
-
var lenSq = dx * dx + dy * dy;
|
|
1964
|
-
if (lenSq === 0) return Math.hypot(px - x1, py - y1);
|
|
1965
|
-
var t = Math.max(0, Math.min(1, ((px - x1) * dx + (py - y1) * dy) / lenSq));
|
|
1966
|
-
return Math.hypot(px - (x1 + t * dx), py - (y1 + t * dy));
|
|
1967
|
-
}
|
|
1968
|
-
|
|
1969
|
-
// ── Créer une flèche ──
|
|
1970
|
-
function createArrow(fromId, toId) {
|
|
1971
|
-
var diag = getCurrentDiagram();
|
|
1972
|
-
if (diag.arrows.some(function (a) { return a.from === fromId && a.to === toId; })) return;
|
|
1973
|
-
pushHistory();
|
|
1974
|
-
var arrow = { id: "a" + Date.now(), from: fromId, to: toId, label: "" };
|
|
1975
|
-
diag.arrows.push(arrow);
|
|
1976
|
-
saveDiagrammes();
|
|
1977
|
-
selectedId = arrow.id; selectedType = "arrow";
|
|
1978
|
-
selectedIds = [];
|
|
1979
|
-
renderAll();
|
|
1980
|
-
startArrowTextEdit(arrow.id);
|
|
1981
|
-
}
|
|
1982
|
-
|
|
1983
|
-
// ── Renommer le diagramme ──
|
|
1984
|
-
function onTitleChange() {
|
|
1985
|
-
if (pendingNewDiagram) return;
|
|
1986
|
-
var diag = getCurrentDiagram();
|
|
1987
|
-
if (!diag) return;
|
|
1988
|
-
diag.titre = document.getElementById("diagramTitle").value || "Diagramme";
|
|
1989
|
-
saveDiagrammes();
|
|
1990
|
-
renderDiagramList();
|
|
1991
|
-
}
|
|
1992
|
-
|
|
1993
|
-
// ── Événements souris ──
|
|
1994
|
-
function onMouseDown(e) {
|
|
1995
|
-
if (e.button !== 0) return;
|
|
1996
|
-
if (editingShapeId || editingArrowId) { confirmTextEdit(); return; }
|
|
1997
|
-
|
|
1998
|
-
// Mode verrouillé : pan uniquement
|
|
1999
|
-
if (boardLocked) {
|
|
2000
|
-
panStart = { cx: e.clientX, cy: e.clientY, px: viewTransform.x, py: viewTransform.y };
|
|
2001
|
-
return;
|
|
2002
|
-
}
|
|
2003
|
-
|
|
2004
|
-
var pt = svgPoint(e.clientX, e.clientY);
|
|
2005
|
-
|
|
2006
|
-
// Mode pick (copie de style)
|
|
2007
|
-
if (pickMode) {
|
|
2008
|
-
var srcShape = shapeAt(pt.x, pt.y);
|
|
2009
|
-
if (srcShape) applyPickMode(srcShape);
|
|
2010
|
-
cancelPickMode();
|
|
2011
|
-
return;
|
|
2012
|
-
}
|
|
2013
|
-
|
|
2014
|
-
// Clic sur un point de connexion → début de flèche
|
|
2015
|
-
var connDot = e.target.closest(".conn-dot");
|
|
2016
|
-
if (connDot) {
|
|
2017
|
-
arrowSrcId = connDot.getAttribute("data-shape-id");
|
|
2018
|
-
var srcShape = getCurrentDiagram().shapes.find(function (s) { return s.id === arrowSrcId; });
|
|
2019
|
-
var ta = document.getElementById("tempArrow");
|
|
2020
|
-
ta.setAttribute("x1", srcShape.x + srcShape.w / 2);
|
|
2021
|
-
ta.setAttribute("y1", srcShape.y + srcShape.h / 2);
|
|
2022
|
-
ta.setAttribute("x2", pt.x); ta.setAttribute("y2", pt.y);
|
|
2023
|
-
ta.style.display = "";
|
|
2024
|
-
return;
|
|
2025
|
-
}
|
|
2026
|
-
|
|
2027
|
-
// Clic sur un chevron de redimensionnement de colonne
|
|
2028
|
-
var colSepEl = e.target.closest("[data-col-sep]");
|
|
2029
|
-
if (colSepEl) {
|
|
2030
|
-
var csColIdx = parseInt(colSepEl.getAttribute("data-col-sep"), 10);
|
|
2031
|
-
var csSid = colSepEl.getAttribute("data-shape-id");
|
|
2032
|
-
var csShape = getCurrentDiagram().shapes.find(function (s) { return s.id === csSid; });
|
|
2033
|
-
if (csShape) {
|
|
2034
|
-
var csCw = getColWidths(csShape);
|
|
2035
|
-
dragState = { type: "col-resize", id: csSid, colIdx: csColIdx, sx: pt.x, origWidths: csCw.slice(), snapshot: JSON.stringify(diagramsList) };
|
|
2036
|
-
}
|
|
2037
|
-
return;
|
|
2038
|
-
}
|
|
2039
|
-
|
|
2040
|
-
// Clic sur la poignée de resize
|
|
2041
|
-
var grip = e.target.closest(".resize-grip");
|
|
2042
|
-
if (grip) {
|
|
2043
|
-
var sid = grip.getAttribute("data-shape-id");
|
|
2044
|
-
var shape = getCurrentDiagram().shapes.find(function (s) { return s.id === sid; });
|
|
2045
|
-
if (shape) {
|
|
2046
|
-
dragState = { type: "resize", id: sid, sx: pt.x, sy: pt.y, ow: shape.w, oh: shape.h, snapshot: JSON.stringify(diagramsList) };
|
|
2047
|
-
}
|
|
2048
|
-
return;
|
|
2049
|
-
}
|
|
2050
|
-
|
|
2051
|
-
var shape = shapeAt(pt.x, pt.y);
|
|
2052
|
-
|
|
2053
|
-
if (currentTool === "select") {
|
|
2054
|
-
if (shape) {
|
|
2055
|
-
// Détection du double-clic par horodatage (avant tout renderAll)
|
|
2056
|
-
var now = Date.now();
|
|
2057
|
-
if (now - lastClickTime < 350 && lastClickShapeId === shape.id) {
|
|
2058
|
-
lastClickTime = 0; lastClickShapeId = null;
|
|
2059
|
-
if (shape.type === "table") {
|
|
2060
|
-
var tDblRows = shape.rows || 3;
|
|
2061
|
-
var tDblOffsets = getColOffsets(shape);
|
|
2062
|
-
var tDblCw = getColWidths(shape);
|
|
2063
|
-
var tDblRx = pt.x - shape.x;
|
|
2064
|
-
var tDblC = tDblCw.length - 1;
|
|
2065
|
-
for (var tci3 = 0; tci3 < tDblCw.length; tci3++) {
|
|
2066
|
-
if (tDblRx < tDblOffsets[tci3] + tDblCw[tci3]) { tDblC = tci3; break; }
|
|
2067
|
-
}
|
|
2068
|
-
var tDblR = Math.min(tDblRows - 1, Math.max(0, Math.floor((pt.y - shape.y) / (shape.h / tDblRows))));
|
|
2069
|
-
startTableCellEdit(shape.id, tDblR, tDblC);
|
|
2070
|
-
} else {
|
|
2071
|
-
startTextEdit(shape.id);
|
|
2072
|
-
}
|
|
2073
|
-
return;
|
|
2074
|
-
}
|
|
2075
|
-
lastClickTime = now; lastClickShapeId = shape.id; lastClickArrowId = null;
|
|
2076
|
-
|
|
2077
|
-
if (e.shiftKey) {
|
|
2078
|
-
// Shift+clic : ajouter/retirer de la multi-sélection
|
|
2079
|
-
var idx = selectedIds.indexOf(shape.id);
|
|
2080
|
-
if (idx === -1) {
|
|
2081
|
-
selectedIds.push(shape.id);
|
|
2082
|
-
selectedId = shape.id; selectedType = "shape";
|
|
2083
|
-
} else {
|
|
2084
|
-
selectedIds.splice(idx, 1);
|
|
2085
|
-
selectedId = selectedIds.length > 0 ? selectedIds[selectedIds.length - 1] : null;
|
|
2086
|
-
selectedType = selectedId ? "shape" : null;
|
|
2087
|
-
}
|
|
2088
|
-
document.getElementById("colorPanel").style.display = selectedIds.length > 0 ? "flex" : "none";
|
|
2089
|
-
renderAll();
|
|
2090
|
-
} else if (selectedIds.indexOf(shape.id) !== -1 && selectedIds.length > 1) {
|
|
2091
|
-
// Clic sur une forme déjà dans la multi-sélection → multi-déplacement
|
|
2092
|
-
var diag = getCurrentDiagram();
|
|
2093
|
-
dragState = {
|
|
2094
|
-
type: "multi-move",
|
|
2095
|
-
sx: pt.x, sy: pt.y,
|
|
2096
|
-
origPositions: selectedIds.map(function (id) {
|
|
2097
|
-
var s = diag.shapes.find(function (sh) { return sh.id === id; });
|
|
2098
|
-
return { id: id, ox: s ? s.x : 0, oy: s ? s.y : 0 };
|
|
2099
|
-
}),
|
|
2100
|
-
snapshot: JSON.stringify(diagramsList),
|
|
2101
|
-
};
|
|
2102
|
-
} else {
|
|
2103
|
-
// Clic simple → sélection unique
|
|
2104
|
-
selectedIds = [shape.id];
|
|
2105
|
-
selectedId = shape.id; selectedType = "shape";
|
|
2106
|
-
dragState = { type: "move", id: shape.id, sx: pt.x, sy: pt.y, ox: shape.x, oy: shape.y, snapshot: JSON.stringify(diagramsList) };
|
|
2107
|
-
document.getElementById("colorPanel").style.display = "flex";
|
|
2108
|
-
renderAll();
|
|
2109
|
-
}
|
|
2110
|
-
} else {
|
|
2111
|
-
var aid = arrowIdAt(pt.x, pt.y);
|
|
2112
|
-
if (aid) {
|
|
2113
|
-
// Détection du double-clic sur une flèche
|
|
2114
|
-
var now2 = Date.now();
|
|
2115
|
-
if (now2 - lastClickTime < 350 && lastClickArrowId === aid) {
|
|
2116
|
-
lastClickTime = 0; lastClickArrowId = null;
|
|
2117
|
-
selectedId = aid; selectedType = "arrow";
|
|
2118
|
-
selectedIds = [];
|
|
2119
|
-
renderAll();
|
|
2120
|
-
startArrowTextEdit(aid);
|
|
2121
|
-
return;
|
|
2122
|
-
}
|
|
2123
|
-
lastClickTime = now2; lastClickArrowId = aid;
|
|
2124
|
-
|
|
2125
|
-
selectedId = aid; selectedType = "arrow";
|
|
2126
|
-
selectedIds = [];
|
|
2127
|
-
document.getElementById("colorPanel").style.display = "none";
|
|
2128
|
-
renderAll();
|
|
2129
|
-
} else if (e.shiftKey) {
|
|
2130
|
-
// Shift+drag sur fond vide → lasso de sélection
|
|
2131
|
-
rubberBandState = { sx: pt.x, sy: pt.y };
|
|
2132
|
-
} else {
|
|
2133
|
-
// Pan
|
|
2134
|
-
panStart = { cx: e.clientX, cy: e.clientY, px: viewTransform.x, py: viewTransform.y };
|
|
2135
|
-
selectedId = null; selectedType = null;
|
|
2136
|
-
selectedIds = [];
|
|
2137
|
-
document.getElementById("colorPanel").style.display = "none";
|
|
2138
|
-
renderAll();
|
|
2139
|
-
}
|
|
2140
|
-
}
|
|
2141
|
-
|
|
2142
|
-
} else if (currentTool === "arrow") {
|
|
2143
|
-
if (shape && shape.type !== "postit") {
|
|
2144
|
-
if (!arrowSrcId) {
|
|
2145
|
-
arrowSrcId = shape.id;
|
|
2146
|
-
var ta = document.getElementById("tempArrow");
|
|
2147
|
-
ta.setAttribute("x1", shape.x + shape.w / 2);
|
|
2148
|
-
ta.setAttribute("y1", shape.y + shape.h / 2);
|
|
2149
|
-
ta.setAttribute("x2", pt.x); ta.setAttribute("y2", pt.y);
|
|
2150
|
-
ta.style.display = "";
|
|
2151
|
-
} else if (arrowSrcId !== shape.id) {
|
|
2152
|
-
createArrow(arrowSrcId, shape.id);
|
|
2153
|
-
arrowSrcId = null;
|
|
2154
|
-
document.getElementById("tempArrow").style.display = "none";
|
|
2155
|
-
}
|
|
2156
|
-
} else {
|
|
2157
|
-
arrowSrcId = null;
|
|
2158
|
-
document.getElementById("tempArrow").style.display = "none";
|
|
2159
|
-
}
|
|
2160
|
-
|
|
2161
|
-
} else {
|
|
2162
|
-
// Outils forme : placer au clic
|
|
2163
|
-
addShape(currentTool, pt.x, pt.y);
|
|
2164
|
-
setTool("select");
|
|
2165
|
-
}
|
|
2166
|
-
}
|
|
2167
|
-
|
|
2168
|
-
function onMouseMove(e) {
|
|
2169
|
-
var pt = svgPoint(e.clientX, e.clientY);
|
|
2170
|
-
|
|
2171
|
-
// Mise à jour de la flèche temporaire
|
|
2172
|
-
if (arrowSrcId) {
|
|
2173
|
-
var ta = document.getElementById("tempArrow");
|
|
2174
|
-
if (ta.style.display !== "none") {
|
|
2175
|
-
ta.setAttribute("x2", pt.x); ta.setAttribute("y2", pt.y);
|
|
2176
|
-
}
|
|
2177
|
-
}
|
|
2178
|
-
|
|
2179
|
-
if (rubberBandState) {
|
|
2180
|
-
var rb = document.getElementById("rubberBand");
|
|
2181
|
-
var rbX = Math.min(rubberBandState.sx, pt.x);
|
|
2182
|
-
var rbY = Math.min(rubberBandState.sy, pt.y);
|
|
2183
|
-
rb.setAttribute("x", rbX); rb.setAttribute("y", rbY);
|
|
2184
|
-
rb.setAttribute("width", Math.abs(pt.x - rubberBandState.sx));
|
|
2185
|
-
rb.setAttribute("height", Math.abs(pt.y - rubberBandState.sy));
|
|
2186
|
-
rb.style.display = "";
|
|
2187
|
-
} else if (dragState) {
|
|
2188
|
-
dragState.moved = true;
|
|
2189
|
-
var diag = getCurrentDiagram();
|
|
2190
|
-
if (dragState.type === "multi-move") {
|
|
2191
|
-
var dx = pt.x - dragState.sx;
|
|
2192
|
-
var dy = pt.y - dragState.sy;
|
|
2193
|
-
dragState.origPositions.forEach(function (op) {
|
|
2194
|
-
var s = diag.shapes.find(function (sh) { return sh.id === op.id; });
|
|
2195
|
-
if (s) { s.x = Math.round(op.ox + dx); s.y = Math.round(op.oy + dy); }
|
|
2196
|
-
});
|
|
2197
|
-
renderAll();
|
|
2198
|
-
} else {
|
|
2199
|
-
var shape = diag.shapes.find(function (s) { return s.id === dragState.id; });
|
|
2200
|
-
if (!shape) return;
|
|
2201
|
-
if (dragState.type === "move") {
|
|
2202
|
-
shape.x = Math.round(dragState.ox + pt.x - dragState.sx);
|
|
2203
|
-
shape.y = Math.round(dragState.oy + pt.y - dragState.sy);
|
|
2204
|
-
} else if (dragState.type === "resize") {
|
|
2205
|
-
var newW = Math.max(60, Math.round(dragState.ow + pt.x - dragState.sx));
|
|
2206
|
-
var newH = Math.max(30, Math.round(dragState.oh + pt.y - dragState.sy));
|
|
2207
|
-
if (shape.type === "table" && shape.colWidths && shape.colWidths.length === (shape.cols || 3)) {
|
|
2208
|
-
var oldW2 = shape.w;
|
|
2209
|
-
shape.colWidths = shape.colWidths.map(function (cw) { return cw * newW / oldW2; });
|
|
2210
|
-
}
|
|
2211
|
-
shape.w = newW;
|
|
2212
|
-
shape.h = newH;
|
|
2213
|
-
} else if (dragState.type === "col-resize") {
|
|
2214
|
-
var crCw = dragState.origWidths.slice();
|
|
2215
|
-
var crDx = pt.x - dragState.sx;
|
|
2216
|
-
var crCi = dragState.colIdx;
|
|
2217
|
-
var MIN_COL = 20;
|
|
2218
|
-
var crLeft = Math.max(MIN_COL, crCw[crCi] + crDx);
|
|
2219
|
-
var crRight = Math.max(MIN_COL, crCw[crCi + 1] - crDx);
|
|
2220
|
-
var crTotal = crCw[crCi] + crCw[crCi + 1];
|
|
2221
|
-
crLeft = Math.min(crTotal - MIN_COL, crLeft);
|
|
2222
|
-
crRight = crTotal - crLeft;
|
|
2223
|
-
crCw[crCi] = crLeft;
|
|
2224
|
-
crCw[crCi + 1] = crRight;
|
|
2225
|
-
shape.colWidths = crCw;
|
|
2226
|
-
}
|
|
2227
|
-
renderAll();
|
|
2228
|
-
}
|
|
2229
|
-
} else if (panStart) {
|
|
2230
|
-
viewTransform.x = panStart.px + (e.clientX - panStart.cx);
|
|
2231
|
-
viewTransform.y = panStart.py + (e.clientY - panStart.cy);
|
|
2232
|
-
updateViewport();
|
|
2233
|
-
}
|
|
2234
|
-
}
|
|
2235
|
-
|
|
2236
|
-
function onMouseUp(e) {
|
|
2237
|
-
var pt = svgPoint(e.clientX, e.clientY);
|
|
2238
|
-
|
|
2239
|
-
// En mode verrouillé : détecter un clic (sans déplacement) sur une forme liée
|
|
2240
|
-
if (boardLocked && panStart) {
|
|
2241
|
-
var movedPx = Math.abs(e.clientX - panStart.cx) + Math.abs(e.clientY - panStart.cy);
|
|
2242
|
-
if (movedPx < 5 && !e.shiftKey) {
|
|
2243
|
-
var shape = shapeAt(pt.x, pt.y);
|
|
2244
|
-
if (shape && shape.linkedDiagramId) {
|
|
2245
|
-
var target = findDiagramById(shape.linkedDiagramId, diagramsList);
|
|
2246
|
-
if (target) {
|
|
2247
|
-
hideLinkPicker();
|
|
2248
|
-
diagNavStack.push(currentDiagramId);
|
|
2249
|
-
if (diagNavStack.length > 30) diagNavStack.shift();
|
|
2250
|
-
panStart = null;
|
|
2251
|
-
selectDiagramme(shape.linkedDiagramId);
|
|
2252
|
-
return;
|
|
2253
|
-
}
|
|
2254
|
-
} else if (shape && shape.externalUrl) {
|
|
2255
|
-
panStart = null;
|
|
2256
|
-
window.open(shape.externalUrl, "_blank");
|
|
2257
|
-
return;
|
|
2258
|
-
}
|
|
2259
|
-
}
|
|
2260
|
-
panStart = null;
|
|
2261
|
-
return;
|
|
2262
|
-
}
|
|
2263
|
-
|
|
2264
|
-
// Fin de tracé de flèche via conn-dot
|
|
2265
|
-
if (arrowSrcId && !dragState) {
|
|
2266
|
-
document.getElementById("tempArrow").style.display = "none";
|
|
2267
|
-
if (currentTool !== "arrow") {
|
|
2268
|
-
var target = shapeAt(pt.x, pt.y);
|
|
2269
|
-
if (target && target.id !== arrowSrcId && target.type !== "postit") {
|
|
2270
|
-
createArrow(arrowSrcId, target.id);
|
|
2271
|
-
}
|
|
2272
|
-
arrowSrcId = null;
|
|
2273
|
-
}
|
|
2274
|
-
}
|
|
2275
|
-
|
|
2276
|
-
if (rubberBandState) {
|
|
2277
|
-
document.getElementById("rubberBand").style.display = "none";
|
|
2278
|
-
var minX = Math.min(rubberBandState.sx, pt.x);
|
|
2279
|
-
var minY = Math.min(rubberBandState.sy, pt.y);
|
|
2280
|
-
var maxX = Math.max(rubberBandState.sx, pt.x);
|
|
2281
|
-
var maxY = Math.max(rubberBandState.sy, pt.y);
|
|
2282
|
-
if (maxX - minX > 4 || maxY - minY > 4) {
|
|
2283
|
-
var diag = getCurrentDiagram();
|
|
2284
|
-
selectedIds = (diag.shapes || []).filter(function (s) {
|
|
2285
|
-
return s.x < maxX && s.x + s.w > minX && s.y < maxY && s.y + s.h > minY;
|
|
2286
|
-
}).map(function (s) { return s.id; });
|
|
2287
|
-
selectedId = selectedIds.length > 0 ? selectedIds[0] : null;
|
|
2288
|
-
selectedType = selectedIds.length > 0 ? "shape" : null;
|
|
2289
|
-
document.getElementById("colorPanel").style.display = selectedIds.length > 0 ? "flex" : "none";
|
|
2290
|
-
}
|
|
2291
|
-
rubberBandState = null;
|
|
2292
|
-
renderAll();
|
|
2293
|
-
}
|
|
2294
|
-
|
|
2295
|
-
if (dragState) {
|
|
2296
|
-
var wasMoved = dragState.moved;
|
|
2297
|
-
var draggedId = dragState.id;
|
|
2298
|
-
var dragType = dragState.type;
|
|
2299
|
-
if (dragState.moved && dragState.snapshot) {
|
|
2300
|
-
historyStack.push(dragState.snapshot);
|
|
2301
|
-
if (historyStack.length > MAX_HISTORY) historyStack.shift();
|
|
2302
|
-
}
|
|
2303
|
-
saveDiagrammes();
|
|
2304
|
-
dragState = null;
|
|
2305
|
-
renderAll();
|
|
2306
|
-
// Naviguer vers le diagramme lié si clic sans déplacement
|
|
2307
|
-
if (!wasMoved && dragType === "move" && draggedId && !e.shiftKey) {
|
|
2308
|
-
var diag = getCurrentDiagram();
|
|
2309
|
-
var clickedShape = diag ? diag.shapes.find(function (s) { return s.id === draggedId; }) : null;
|
|
2310
|
-
if (clickedShape && clickedShape.linkedDiagramId) {
|
|
2311
|
-
var target = findDiagramById(clickedShape.linkedDiagramId, diagramsList);
|
|
2312
|
-
if (target) {
|
|
2313
|
-
hideLinkPicker();
|
|
2314
|
-
diagNavStack.push(currentDiagramId);
|
|
2315
|
-
if (diagNavStack.length > 30) diagNavStack.shift();
|
|
2316
|
-
selectDiagramme(clickedShape.linkedDiagramId);
|
|
2317
|
-
return;
|
|
2318
|
-
}
|
|
2319
|
-
} else if (clickedShape && clickedShape.externalUrl) {
|
|
2320
|
-
window.open(clickedShape.externalUrl, "_blank");
|
|
2321
|
-
return;
|
|
2322
|
-
}
|
|
2323
|
-
}
|
|
2324
|
-
}
|
|
2325
|
-
panStart = null;
|
|
2326
|
-
}
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
function onWheel(e) {
|
|
2330
|
-
e.preventDefault();
|
|
2331
|
-
var svg = document.getElementById("canvas");
|
|
2332
|
-
var r = svg.getBoundingClientRect();
|
|
2333
|
-
applyZoom(e.deltaY < 0 ? 1.12 : 1 / 1.12, e.clientX - r.left, e.clientY - r.top);
|
|
2334
|
-
}
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
// ── Coller des formes ──
|
|
2338
|
-
function pasteShapes() {
|
|
2339
|
-
if (clipboard.length === 0) return;
|
|
2340
|
-
pushHistory();
|
|
2341
|
-
var diag = getCurrentDiagram();
|
|
2342
|
-
var now = Date.now();
|
|
2343
|
-
var pasted = clipboard.map(function (s, i) {
|
|
2344
|
-
return Object.assign({}, JSON.parse(JSON.stringify(s)), {
|
|
2345
|
-
id: "s" + (now + i),
|
|
2346
|
-
x: s.x + 20,
|
|
2347
|
-
y: s.y + 20,
|
|
2348
|
-
});
|
|
2349
|
-
});
|
|
2350
|
-
pasted.forEach(function (s) { diag.shapes.push(s); });
|
|
2351
|
-
selectedIds = pasted.map(function (s) { return s.id; });
|
|
2352
|
-
selectedId = selectedIds[0]; selectedType = "shape";
|
|
2353
|
-
clipboard = pasted.map(function (s) { return JSON.parse(JSON.stringify(s)); });
|
|
2354
|
-
saveDiagrammes();
|
|
2355
|
-
renderAll();
|
|
2356
|
-
document.getElementById("colorPanel").style.display = "flex";
|
|
2357
|
-
}
|
|
2358
|
-
|
|
2359
|
-
// ── Init ──
|
|
2360
|
-
document.addEventListener("DOMContentLoaded", function () {
|
|
2361
|
-
diagramsList = loadDiagrammes();
|
|
2362
|
-
if (!localStorage.getItem("mes_diagrammes")) saveDiagrammes();
|
|
2363
|
-
var savedDiagId = localStorage.getItem("current_diagram_id");
|
|
2364
|
-
if (savedDiagId && findDiagramById(savedDiagId, diagramsList)) {
|
|
2365
|
-
currentDiagramId = savedDiagId;
|
|
2366
|
-
} else {
|
|
2367
|
-
var savedIdx = parseInt(localStorage.getItem("current_diagram_idx"), 10);
|
|
2368
|
-
if (!isNaN(savedIdx) && savedIdx >= 0 && savedIdx < diagramsList.length) {
|
|
2369
|
-
currentDiagramId = String(diagramsList[savedIdx].id);
|
|
2370
|
-
} else {
|
|
2371
|
-
currentDiagramId = diagramsList[0] ? String(diagramsList[0].id) : null;
|
|
2372
|
-
}
|
|
2373
|
-
}
|
|
2374
|
-
restoreLockForDiagram(currentDiagramId);
|
|
2375
|
-
checkDiffDiagrammes();
|
|
2376
|
-
renderAll();
|
|
2377
|
-
updateLockBtn();
|
|
2378
|
-
updateBackBtn();
|
|
2379
|
-
|
|
2380
|
-
var canvas = document.getElementById("canvas");
|
|
2381
|
-
canvas.addEventListener("mousedown", onMouseDown);
|
|
2382
|
-
canvas.addEventListener("mousemove", onMouseMove);
|
|
2383
|
-
canvas.addEventListener("mouseup", onMouseUp);
|
|
2384
|
-
canvas.addEventListener("wheel", onWheel, { passive: false });
|
|
2385
|
-
|
|
2386
|
-
var titleInput = document.getElementById("diagramTitle");
|
|
2387
|
-
titleInput.addEventListener("change", onTitleChange);
|
|
2388
|
-
titleInput.addEventListener("blur", onTitleChange);
|
|
2389
|
-
titleInput.addEventListener("keydown", function (e) {
|
|
2390
|
-
if (e.key === "Enter") {
|
|
2391
|
-
e.preventDefault();
|
|
2392
|
-
if (pendingNewDiagram) confirmerNouveauDiagramme();
|
|
2393
|
-
else titleInput.blur();
|
|
2394
|
-
}
|
|
2395
|
-
if (e.key === "Escape" && pendingNewDiagram) annulerNouveauDiagramme();
|
|
2396
|
-
});
|
|
2397
|
-
|
|
2398
|
-
var textInput = document.getElementById("shapeTextInput");
|
|
2399
|
-
textInput.addEventListener("keydown", function (e) {
|
|
2400
|
-
if (e.key === "Enter") { e.preventDefault(); confirmTextEdit(); }
|
|
2401
|
-
if (e.key === "Escape") {
|
|
2402
|
-
document.getElementById("textOverlay").style.display = "none";
|
|
2403
|
-
editingShapeId = null;
|
|
2404
|
-
}
|
|
2405
|
-
});
|
|
2406
|
-
textInput.addEventListener("blur", function () {
|
|
2407
|
-
// Small delay to allow click on modal-confirm etc.
|
|
2408
|
-
setTimeout(confirmTextEdit, 100);
|
|
2409
|
-
});
|
|
2410
|
-
|
|
2411
|
-
var postitInput = document.getElementById("postitTextInput");
|
|
2412
|
-
postitInput.addEventListener("keydown", function (e) {
|
|
2413
|
-
if (e.key === "Escape") {
|
|
2414
|
-
document.getElementById("textOverlay").style.display = "none";
|
|
2415
|
-
postitInput.style.display = "none";
|
|
2416
|
-
document.getElementById("shapeTextInput").style.display = "block";
|
|
2417
|
-
editingShapeId = null;
|
|
2418
|
-
renderAll(); // restaure la visibilité du texte SVG
|
|
2419
|
-
}
|
|
2420
|
-
if (e.key === "Tab" && editingTableCell !== null) {
|
|
2421
|
-
e.preventDefault();
|
|
2422
|
-
var diag = getCurrentDiagram();
|
|
2423
|
-
var shape = diag.shapes.find(function (s) { return s.id === editingShapeId; });
|
|
2424
|
-
if (!shape) return;
|
|
2425
|
-
// Sauvegarder la cellule courante
|
|
2426
|
-
if (!shape.cells) shape.cells = [];
|
|
2427
|
-
while (shape.cells.length <= editingTableCell.row) shape.cells.push([]);
|
|
2428
|
-
shape.cells[editingTableCell.row][editingTableCell.col] = postitInput.value;
|
|
2429
|
-
var rows = shape.rows || 3;
|
|
2430
|
-
var cols = shape.cols || 3;
|
|
2431
|
-
var nextRow = editingTableCell.row;
|
|
2432
|
-
var nextCol = editingTableCell.col;
|
|
2433
|
-
if (e.shiftKey) {
|
|
2434
|
-
// Shift+Tab : cellule précédente, s'arrête à la première
|
|
2435
|
-
nextCol -= 1;
|
|
2436
|
-
if (nextCol < 0) {
|
|
2437
|
-
if (nextRow > 0) { nextRow--; nextCol = cols - 1; }
|
|
2438
|
-
else { nextCol = 0; }
|
|
2439
|
-
}
|
|
2440
|
-
} else {
|
|
2441
|
-
// Tab : cellule suivante, ajoute une ligne à la fin
|
|
2442
|
-
nextCol += 1;
|
|
2443
|
-
if (nextCol >= cols) { nextRow++; nextCol = 0; }
|
|
2444
|
-
if (nextRow >= rows) {
|
|
2445
|
-
pushHistory();
|
|
2446
|
-
shape.rows = rows + 1;
|
|
2447
|
-
if (!shape.cells) shape.cells = [];
|
|
2448
|
-
while (shape.cells.length < shape.rows) shape.cells.push([]);
|
|
2449
|
-
shape.cells[shape.rows - 1] = new Array(cols).fill("");
|
|
2450
|
-
}
|
|
2451
|
-
}
|
|
2452
|
-
var shapeId = editingShapeId;
|
|
2453
|
-
saveDiagrammes();
|
|
2454
|
-
renderAll();
|
|
2455
|
-
startTableCellEdit(shapeId, nextRow, nextCol);
|
|
2456
|
-
}
|
|
2457
|
-
// Enter ajoute un saut de ligne (comportement natif textarea — pas de preventDefault)
|
|
2458
|
-
});
|
|
2459
|
-
postitInput.addEventListener("blur", function () {
|
|
2460
|
-
setTimeout(confirmTextEdit, 100);
|
|
2461
|
-
});
|
|
2462
|
-
|
|
2463
|
-
document.addEventListener("keydown", function (e) {
|
|
2464
|
-
if (document.activeElement.tagName === "INPUT" || document.activeElement.tagName === "TEXTAREA") return;
|
|
2465
|
-
if ((e.ctrlKey || e.metaKey) && e.key === "z") { e.preventDefault(); undoAction(); return; }
|
|
2466
|
-
if ((e.ctrlKey || e.metaKey) && e.key === "a") {
|
|
2467
|
-
e.preventDefault();
|
|
2468
|
-
var diag = getCurrentDiagram();
|
|
2469
|
-
if (diag && diag.shapes.length > 0) {
|
|
2470
|
-
selectedIds = diag.shapes.map(function (s) { return s.id; });
|
|
2471
|
-
selectedType = "shape";
|
|
2472
|
-
selectedId = selectedIds[selectedIds.length - 1];
|
|
2473
|
-
renderAll();
|
|
2474
|
-
document.getElementById("colorPanel").style.display = "flex";
|
|
2475
|
-
syncColorPanel();
|
|
2476
|
-
}
|
|
2477
|
-
return;
|
|
2478
|
-
}
|
|
2479
|
-
if (boardLocked) return;
|
|
2480
|
-
if (e.key === "Delete" || e.key === "Backspace") deleteSelected();
|
|
2481
|
-
if (e.key === "Escape") {
|
|
2482
|
-
hideLinkPicker();
|
|
2483
|
-
arrowSrcId = null;
|
|
2484
|
-
document.getElementById("tempArrow").style.display = "none";
|
|
2485
|
-
setTool("select");
|
|
2486
|
-
selectedIds = []; selectedId = null; selectedType = null;
|
|
2487
|
-
document.getElementById("colorPanel").style.display = "none";
|
|
2488
|
-
renderAll();
|
|
2489
|
-
}
|
|
2490
|
-
if ((e.ctrlKey || e.metaKey) && e.key === "x") {
|
|
2491
|
-
e.preventDefault();
|
|
2492
|
-
if (selectedIds.length === 0) return;
|
|
2493
|
-
var diag = getCurrentDiagram();
|
|
2494
|
-
clipboard = selectedIds.map(function (id) {
|
|
2495
|
-
var s = diag.shapes.find(function (sh) { return sh.id === id; });
|
|
2496
|
-
return s ? JSON.parse(JSON.stringify(s)) : null;
|
|
2497
|
-
}).filter(Boolean);
|
|
2498
|
-
deleteSelected();
|
|
2499
|
-
return;
|
|
2500
|
-
}
|
|
2501
|
-
if ((e.ctrlKey || e.metaKey) && e.key === "c") {
|
|
2502
|
-
e.preventDefault();
|
|
2503
|
-
if (selectedIds.length === 0) return;
|
|
2504
|
-
var diag = getCurrentDiagram();
|
|
2505
|
-
clipboard = selectedIds.map(function (id) {
|
|
2506
|
-
var s = diag.shapes.find(function (sh) { return sh.id === id; });
|
|
2507
|
-
return s ? JSON.parse(JSON.stringify(s)) : null;
|
|
2508
|
-
}).filter(Boolean);
|
|
2509
|
-
}
|
|
2510
|
-
if ((e.ctrlKey || e.metaKey) && e.key === "v") {
|
|
2511
|
-
e.preventDefault();
|
|
2512
|
-
if (clipboard.length > 0) {
|
|
2513
|
-
pasteShapes();
|
|
2514
|
-
return;
|
|
2515
|
-
}
|
|
2516
|
-
if (!navigator.clipboard || !navigator.clipboard.read) return;
|
|
2517
|
-
navigator.clipboard.read().then(function (clipItems) {
|
|
2518
|
-
for (var i = 0; i < clipItems.length; i++) {
|
|
2519
|
-
for (var j = 0; j < clipItems[i].types.length; j++) {
|
|
2520
|
-
if (clipItems[i].types[j].indexOf("image") !== -1) {
|
|
2521
|
-
clipItems[i].getType(clipItems[i].types[j]).then(function (blob) {
|
|
2522
|
-
handleImagePaste(blob);
|
|
2523
|
-
});
|
|
2524
|
-
return;
|
|
2525
|
-
}
|
|
2526
|
-
}
|
|
2527
|
-
}
|
|
2528
|
-
}).catch(function () {});
|
|
2529
|
-
}
|
|
2530
|
-
});
|
|
2531
|
-
|
|
2532
|
-
// Quand la fenêtre reprend le focus, l'utilisateur revient d'une autre app
|
|
2533
|
-
// où il a peut-être copié une image → vider le clipboard interne
|
|
2534
|
-
window.addEventListener("focus", function () {
|
|
2535
|
-
clipboard = [];
|
|
2536
|
-
});
|
|
2537
|
-
|
|
2538
|
-
document.getElementById("modalPremiereSauvegardeDiag").addEventListener("click", function (e) {
|
|
2539
|
-
if (e.target === this) this.classList.remove("open");
|
|
2540
|
-
});
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
document.addEventListener("mousedown", function (e) {
|
|
2544
|
-
var lp = document.getElementById("linkPickerPanel");
|
|
2545
|
-
if (lp && lp.style.display !== "none") {
|
|
2546
|
-
var lbtn = document.getElementById("btnShapeLink");
|
|
2547
|
-
if (!lp.contains(e.target) && e.target !== lbtn) hideLinkPicker();
|
|
2548
|
-
}
|
|
2549
|
-
var panel = document.getElementById("diagramListPanel");
|
|
2550
|
-
if (!panel.classList.contains("open")) return;
|
|
2551
|
-
var burgerBtn = document.querySelector(".diagram-tool[onclick=\"toggleDiagramList()\"]");
|
|
2552
|
-
if (!panel.contains(e.target) && (!burgerBtn || !burgerBtn.contains(e.target))) {
|
|
2553
|
-
panel.classList.remove("open");
|
|
2554
|
-
}
|
|
2555
|
-
});
|
|
2556
|
-
});
|