doc-survival-kit 1.0.0 → 2.0.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.js CHANGED
@@ -5,7 +5,11 @@
5
5
  // ── État global ──
6
6
  var currentTool = "select";
7
7
  var diagramsList = [];
8
- var currentDiagramIdx = 0;
8
+ var currentDiagramId = null;
9
+ var diagNavStack = [];
10
+ var diagExpandedIds = {};
11
+ var pendingParentId = null;
12
+ var pendingNavDiagId = null;
9
13
  var viewTransform = { x: 60, y: 60, scale: 1 };
10
14
  var selectedId = null;
11
15
  var selectedType = null; // "shape" | "arrow"
@@ -18,6 +22,7 @@ var dragState = null;
18
22
  var panStart = null;
19
23
  var arrowSrcId = null;
20
24
  var editingShapeId = null;
25
+ var editingTableCell = null; // { row, col } | null
21
26
  var pickMode = null; // "fontSize" | "fullStyle" | null
22
27
  var pickTargetIds = []; // selectedIds sauvegardés pendant le pick
23
28
  var lastClickTime = 0;
@@ -25,7 +30,27 @@ var lastClickShapeId = null;
25
30
  var lastClickArrowId = null;
26
31
  var editingArrowId = null;
27
32
  var boardLocked = false;
28
- var diagDragSrcIdx = null;
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
+ }
29
54
 
30
55
  // ── Palette ──
31
56
  var COLORS = {
@@ -35,6 +60,7 @@ var COLORS = {
35
60
  "t-sky": { fill: "rgba(224,242,254,0.75)", stroke: "#0284c7", text: "#0369a1" },
36
61
  "t-rose": { fill: "rgba(255,228,230,0.75)", stroke: "#e11d48", text: "#be123c" },
37
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" },
38
64
  };
39
65
  var DEFAULT_COLOR = "t-sky";
40
66
 
@@ -43,8 +69,11 @@ var DEFAULT_SIZES = {
43
69
  rounded: { w: 130, h: 50 },
44
70
  db: { w: 100, h: 65 },
45
71
  cloud: { w: 110, h: 65 },
72
+ nuage: { w: 121, h: 96 },
46
73
  text: { w: 110, h: 34 },
47
74
  postit: { w: 130, h: 110 },
75
+ actor: { w: 60, h: 90 },
76
+ table: { w: 210, h: 120 },
48
77
  };
49
78
 
50
79
  // ── Helpers ──
@@ -78,6 +107,22 @@ function wrapPostitLines(text, maxWidth, fontSize) {
78
107
  var words = line.split(" ");
79
108
  var current = "";
80
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
+ }
81
126
  var test = current ? current + " " + word : word;
82
127
  if (current && measureText(test, fs, "600") > maxWidth) {
83
128
  result.push(current);
@@ -91,17 +136,103 @@ function wrapPostitLines(text, maxWidth, fontSize) {
91
136
  return result;
92
137
  }
93
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
+
94
204
  function getCurrentDiagram() {
95
- return diagramsList[currentDiagramIdx] || null;
205
+ if (currentDiagramId !== null) {
206
+ var found = findDiagramById(currentDiagramId, diagramsList);
207
+ if (found) return found;
208
+ }
209
+ return diagramsList[0] || null;
96
210
  }
97
211
 
98
212
  // ── Persistance localStorage ──
99
213
  function loadDiagrammes() {
100
214
  try {
101
215
  var stored = JSON.parse(localStorage.getItem("mes_diagrammes"));
102
- if (stored && stored.length) return stored;
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
+ }
103
226
  } catch (e) {}
104
- return JSON.parse(JSON.stringify(diagrammesDefaut));
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;
105
236
  }
106
237
 
107
238
  function saveDiagrammes() {
@@ -109,6 +240,22 @@ function saveDiagrammes() {
109
240
  checkDiffDiagrammes();
110
241
  }
111
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
+
112
259
  function checkDiffDiagrammes() {
113
260
  var stored = localStorage.getItem("mes_diagrammes");
114
261
  var diff = stored !== JSON.stringify(diagrammesDefaut);
@@ -123,14 +270,14 @@ function getLockMap() {
123
270
  try { return JSON.parse(localStorage.getItem("diagrammes_lock") || "{}"); } catch(e) { return {}; }
124
271
  }
125
272
  function saveCurrentLock() {
126
- var diag = diagramsList[currentDiagramIdx];
273
+ var diag = getCurrentDiagram();
127
274
  if (!diag) return;
128
275
  var map = getLockMap();
129
276
  map[diag.id] = boardLocked;
130
277
  localStorage.setItem("diagrammes_lock", JSON.stringify(map));
131
278
  }
132
- function restoreLockForDiagram(idx) {
133
- var diag = diagramsList[idx];
279
+ function restoreLockForDiagram(diagId) {
280
+ var diag = findDiagramById(diagId, diagramsList);
134
281
  if (!diag) return;
135
282
  var map = getLockMap();
136
283
  boardLocked = map[diag.id] === true;
@@ -159,14 +306,14 @@ function getZoomMap() {
159
306
  try { return JSON.parse(localStorage.getItem("diagrammes_zoom") || "{}"); } catch(e) { return {}; }
160
307
  }
161
308
  function saveCurrentZoom() {
162
- var diag = diagramsList[currentDiagramIdx];
309
+ var diag = getCurrentDiagram();
163
310
  if (!diag) return;
164
311
  var map = getZoomMap();
165
312
  map[diag.id] = viewTransform.scale;
166
313
  localStorage.setItem("diagrammes_zoom", JSON.stringify(map));
167
314
  }
168
- function restoreZoomForDiagram(idx) {
169
- var diag = diagramsList[idx];
315
+ function restoreZoomForDiagram(diagId) {
316
+ var diag = findDiagramById(diagId, diagramsList);
170
317
  if (!diag) return;
171
318
  var map = getZoomMap();
172
319
  viewTransform.scale = map[diag.id] !== undefined ? map[diag.id] : 1;
@@ -372,6 +519,7 @@ function updateViewport() {
372
519
  );
373
520
  document.getElementById("zoomLevel").textContent =
374
521
  Math.round(viewTransform.scale * 100) + "%";
522
+ updateTableOverlay();
375
523
  }
376
524
 
377
525
  // ── Zoom ──
@@ -439,7 +587,7 @@ function renderShape(shape) {
439
587
  g.appendChild(topEl);
440
588
 
441
589
  } else if (shape.type === "cloud") {
442
- // Ellipse à bordure pointillée = service externe / cloud
590
+ // Ellipse en trait continu = service externe / cloud
443
591
  var el = createSVGEl("ellipse");
444
592
  el.setAttribute("cx", shape.x + shape.w / 2);
445
593
  el.setAttribute("cy", shape.y + shape.h / 2);
@@ -450,6 +598,211 @@ function renderShape(shape) {
450
598
  el.setAttribute("stroke-width", sw);
451
599
  g.appendChild(el);
452
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
+
453
806
  } else if (shape.type === "postit") {
454
807
  var fold = 18;
455
808
  var body = createSVGEl("path");
@@ -505,7 +858,9 @@ function renderShape(shape) {
505
858
  // ── Texte ──
506
859
  var textCy = shape.type === "db"
507
860
  ? shape.y + shape.h * 0.62
508
- : shape.y + shape.h / 2;
861
+ : shape.type === "actor"
862
+ ? shape.y + shape.h - 2
863
+ : shape.y + shape.h / 2;
509
864
  var ta = shape.textAlign || "center";
510
865
  var textAnchor = ta === "left" ? "start" : ta === "right" ? "end" : "middle";
511
866
  var txt = createSVGEl("text");
@@ -539,12 +894,30 @@ function renderShape(shape) {
539
894
  ts.textContent = line || " ";
540
895
  txt.appendChild(ts);
541
896
  });
542
- } else if (shape.type === "rect" || shape.type === "rounded" || shape.type === "db" || shape.type === "cloud") {
543
- var wpad = shape.type === "cloud" ? Math.round(shape.w * 0.2) : 12;
544
- var wvpad = shape.type === "cloud" ? Math.round(shape.h * 0.12) : 8;
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;
545
915
  var wlines = wrapPostitLines(shape.text || "", shape.w - wpad * 2, fs);
546
916
  var wlineH = Math.round(fs * 1.33);
547
- var wcenterY = shape.type === "db" ? shape.y + shape.h * 0.62 : shape.y + shape.h / 2;
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;
548
921
  var dbCapH = shape.type === "db" ? Math.min(12, shape.h * 0.2) * 2 : 0;
549
922
  var wfirstY = tv === "top"
550
923
  ? shape.y + dbCapH + wvpad + wlineH * 0.5
@@ -584,6 +957,36 @@ function renderShape(shape) {
584
957
  // ── Texte non rendu pour les images ──
585
958
  if (shape.type === "image") return g;
586
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
+
587
990
  // ── Points de connexion (visibles au hover et en mode flèche — sauf postit) ──
588
991
  if (shape.type !== "postit") {
589
992
  var hcx = shape.x + shape.w / 2, hcy = shape.y + shape.h / 2;
@@ -611,16 +1014,35 @@ function renderShape(shape) {
611
1014
  function getEdgePoint(shape, targetX, targetY) {
612
1015
  var cx = shape.x + shape.w / 2;
613
1016
  var cy = shape.y + shape.h / 2;
614
- var dx = targetX - cx, dy = targetY - cy;
615
- if (dx === 0 && dy === 0) return { x: cx, y: shape.y };
616
- var hw = shape.w / 2, hh = shape.h / 2;
617
- if (Math.abs(dx) * hh > Math.abs(dy) * hw) {
618
- var sx = dx > 0 ? 1 : -1;
619
- return { x: cx + sx * hw, y: cy + dy * hw / Math.abs(dx) };
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 };
620
1029
  } else {
621
- var sy = dy > 0 ? 1 : -1;
622
- return { x: cx + dx * hh / Math.abs(dy), y: cy + sy * hh };
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
+ }
623
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;
624
1046
  }
625
1047
 
626
1048
  function renderArrow(arrow, shapes) {
@@ -705,106 +1127,97 @@ function renderAll() {
705
1127
 
706
1128
  updateViewport();
707
1129
  renderDiagramList();
708
- }
709
-
710
- function renderDiagramList() {
711
- var el = document.getElementById("diagramList");
712
- el.innerHTML = diagramsList.map(function (d, i) {
713
- var active = i === currentDiagramIdx ? " active" : "";
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
+ : '';
714
1143
  return (
715
- '<div class="diagram-list-item' + active + '" draggable="true"' +
716
- ' ondragstart="onDiagDragStart(event,' + i + ')"' +
717
- ' ondragover="onDiagDragOver(event,' + i + ')"' +
718
- ' ondragleave="onDiagDragLeave(event)"' +
719
- ' ondrop="onDiagDrop(event,' + i + ')"' +
720
- ' ondragend="onDiagDragEnd()"' +
721
- ' onclick="selectDiagramme(' + i + ')">' +
722
- '<span class="diagram-list-grip">⠿</span>' +
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 ? '&#9660;' : '&#9658;') : '<span style="display:inline-block;width:0.7em"></span>') +
1150
+ '</span>' +
723
1151
  '<span class="diagram-list-name">' + escDiag(d.titre) + '</span>' +
724
- (diagramsList.length > 1
725
- ? '<button class="diagram-list-del" onclick="event.stopPropagation();supprimerDiagramme(' + i + ')">×</button>'
726
- : "") +
727
- "</div>"
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>'
728
1159
  );
729
- }).join("");
730
- }
731
-
732
- function onDiagDragStart(e, idx) {
733
- diagDragSrcIdx = idx;
734
- e.dataTransfer.effectAllowed = "move";
735
- e.currentTarget.classList.add("dragging");
1160
+ }).join('');
736
1161
  }
737
1162
 
738
- function onDiagDragOver(e, idx) {
739
- e.preventDefault();
740
- e.dataTransfer.dropEffect = "move";
741
- document.querySelectorAll(".diagram-list-item").forEach(function (el) {
742
- el.classList.remove("drag-over-after", "drag-over-before");
743
- });
744
- if (idx !== diagDragSrcIdx) {
745
- var goingDown = diagDragSrcIdx < idx;
746
- e.currentTarget.classList.add(goingDown ? "drag-over-after" : "drag-over-before");
747
- }
748
- }
749
-
750
- function onDiagDragLeave(e) {
751
- e.currentTarget.classList.remove("drag-over-after", "drag-over-before");
1163
+ function renderDiagramList() {
1164
+ var el = document.getElementById("diagramList");
1165
+ if (!el) return;
1166
+ el.innerHTML = renderDiagramListLevel(diagramsList, 0);
1167
+ updateSidebarWidth();
752
1168
  }
753
1169
 
754
- function onDiagDrop(e, idx) {
755
- e.preventDefault();
756
- if (diagDragSrcIdx === null || diagDragSrcIdx === idx) {
757
- onDiagDragEnd();
758
- return;
759
- }
760
- var src = diagDragSrcIdx;
761
- var item = diagramsList.splice(src, 1)[0];
762
-
763
- // insertAt = idx dans les deux cas :
764
- // - descente (src < idx) : après remove, la cible est à idx-1, on insère à idx = après elle ✓
765
- // - montée (src > idx) : après remove, la cible est toujours à idx, on insère à idx = avant elle ✓
766
- diagramsList.splice(idx, 0, item);
767
-
768
- var cur = currentDiagramIdx;
769
- if (cur === src) {
770
- currentDiagramIdx = idx;
1170
+ function toggleDiagExpand(id) {
1171
+ var key = String(id);
1172
+ if (diagExpandedIds[key]) {
1173
+ delete diagExpandedIds[key];
771
1174
  } else {
772
- var adjustedCur = cur > src ? cur - 1 : cur;
773
- currentDiagramIdx = adjustedCur >= idx ? adjustedCur + 1 : adjustedCur;
1175
+ diagExpandedIds[key] = true;
774
1176
  }
775
- localStorage.setItem("current_diagram_idx", currentDiagramIdx);
776
- diagDragSrcIdx = null;
777
- saveDiagrammes();
778
1177
  renderDiagramList();
779
1178
  }
780
1179
 
781
- function onDiagDragEnd() {
782
- diagDragSrcIdx = null;
783
- document.querySelectorAll(".diagram-list-item").forEach(function (el) {
784
- el.classList.remove("dragging", "drag-over-after", "drag-over-before");
785
- });
786
- }
787
-
788
1180
  // ── Gestion des diagrammes ──
789
- function selectDiagramme(idx) {
1181
+ function selectDiagramme(id, clearStack) {
1182
+ if (clearStack) diagNavStack = [];
790
1183
  saveCurrentZoom();
791
1184
  saveCurrentLock();
792
- currentDiagramIdx = idx;
793
- localStorage.setItem("current_diagram_idx", idx);
794
- selectedId = null; selectedType = null;
1185
+ currentDiagramId = String(id);
1186
+ localStorage.setItem("current_diagram_id", String(id));
1187
+ selectedId = null; selectedType = null;
795
1188
  selectedIds = [];
796
- restoreZoomForDiagram(idx);
797
- restoreLockForDiagram(idx);
1189
+ pendingNavDiagId = null;
1190
+ restoreZoomForDiagram(id);
1191
+ restoreLockForDiagram(id);
798
1192
  viewTransform.x = 60;
799
1193
  viewTransform.y = 60;
800
1194
  document.getElementById("colorPanel").style.display = "none";
801
1195
  renderAll();
802
1196
  updateLockBtn();
1197
+ updateBackBtn();
803
1198
  document.getElementById("diagramListPanel").classList.remove("open");
804
1199
  }
805
1200
 
1201
+ function goBackDiagram() {
1202
+ if (diagNavStack.length === 0) return;
1203
+ var prevId = diagNavStack.pop();
1204
+ selectDiagramme(prevId);
1205
+ }
1206
+
806
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);
807
1219
  pendingNewDiagram = true;
1220
+ diagExpandedIds[String(parentId)] = true;
808
1221
  var input = document.getElementById("diagramTitle");
809
1222
  input.value = "";
810
1223
  input.placeholder = window.t ? window.t.diag_new_diagram : "Nouveau diagramme";
@@ -818,10 +1231,21 @@ function confirmerNouveauDiagramme() {
818
1231
  var input = document.getElementById("diagramTitle");
819
1232
  var titre = input.value.trim() || (window.t ? window.t.diag_new_diagram : "Nouveau diagramme");
820
1233
  input.placeholder = "";
821
- var d = { id: Date.now(), titre: titre, shapes: [], arrows: [] };
822
- diagramsList.push(d);
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
+ }
823
1247
  saveDiagrammes();
824
- selectDiagramme(diagramsList.length - 1);
1248
+ selectDiagramme(d.id);
825
1249
  document.getElementById("diagramTitle").blur();
826
1250
  }
827
1251
 
@@ -835,17 +1259,31 @@ function annulerNouveauDiagramme() {
835
1259
  input.blur();
836
1260
  }
837
1261
 
838
- function supprimerDiagramme(idx) {
839
- if (diagramsList.length <= 1) return;
840
- diagramsList.splice(idx, 1);
841
- if (currentDiagramIdx >= diagramsList.length) currentDiagramIdx = diagramsList.length - 1;
842
- localStorage.setItem("current_diagram_idx", currentDiagramIdx);
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
+ }
843
1272
  saveDiagrammes();
844
1273
  renderAll();
845
1274
  }
846
1275
 
847
1276
  function toggleDiagramList() {
848
- document.getElementById("diagramListPanel").classList.toggle("open");
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
+ }
849
1287
  }
850
1288
 
851
1289
  // ── Outil actif ──
@@ -873,9 +1311,9 @@ function setTool(tool) {
873
1311
 
874
1312
  // ── Ajout d'une forme ──
875
1313
  function addShape(type, x, y) {
1314
+ pushHistory();
876
1315
  var diag = getCurrentDiagram();
877
1316
  var size = DEFAULT_SIZES[type] || { w: 120, h: 50 };
878
- var labels = { db: "Database", cloud: "Cloud", text: "Label", postit: "" };
879
1317
  var shape = {
880
1318
  id: "s" + Date.now(),
881
1319
  type: type,
@@ -883,21 +1321,30 @@ function addShape(type, x, y) {
883
1321
  y: Math.round(y - size.h / 2),
884
1322
  w: size.w,
885
1323
  h: size.h,
886
- text: type in labels ? labels[type] : "Service",
1324
+ text: "",
887
1325
  color: type === "postit" ? "t-amber" : DEFAULT_COLOR,
888
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
+ }
889
1333
  diag.shapes.push(shape);
890
1334
  saveDiagrammes();
891
1335
  selectedId = shape.id; selectedType = "shape";
892
1336
  selectedIds = [shape.id];
893
1337
  renderAll();
894
1338
  document.getElementById("colorPanel").style.display = "flex";
895
- startTextEdit(shape.id);
1339
+ syncColorPanel();
1340
+ if (type !== "table") startTextEdit(shape.id);
896
1341
  }
897
1342
 
898
1343
  // ── Suppression ──
899
1344
  function deleteSelected() {
900
1345
  var diag = getCurrentDiagram();
1346
+ if (selectedIds.length === 0 && !(selectedId && selectedType === "arrow")) return;
1347
+ pushHistory();
901
1348
  if (selectedIds.length > 0) {
902
1349
  diag.shapes = diag.shapes.filter(function (s) { return selectedIds.indexOf(s.id) === -1; });
903
1350
  diag.arrows = diag.arrows.filter(function (a) {
@@ -919,6 +1366,7 @@ function deleteSelected() {
919
1366
  // ── Couleur ──
920
1367
  function setShapeColor(color) {
921
1368
  if (selectedIds.length === 0) return;
1369
+ pushHistory();
922
1370
  var diag = getCurrentDiagram();
923
1371
  selectedIds.forEach(function (id) {
924
1372
  var shape = diag.shapes.find(function (s) { return s.id === id; });
@@ -931,6 +1379,7 @@ function setShapeColor(color) {
931
1379
 
932
1380
  function setShapeTextValign(valign) {
933
1381
  if (selectedIds.length === 0) return;
1382
+ pushHistory();
934
1383
  var diag = getCurrentDiagram();
935
1384
  selectedIds.forEach(function (id) {
936
1385
  var shape = diag.shapes.find(function (s) { return s.id === id; });
@@ -943,6 +1392,7 @@ function setShapeTextValign(valign) {
943
1392
 
944
1393
  function setShapeTextAlign(align) {
945
1394
  if (selectedIds.length === 0) return;
1395
+ pushHistory();
946
1396
  var diag = getCurrentDiagram();
947
1397
  selectedIds.forEach(function (id) {
948
1398
  var shape = diag.shapes.find(function (s) { return s.id === id; });
@@ -955,6 +1405,7 @@ function setShapeTextAlign(align) {
955
1405
 
956
1406
  function changeShapeFontSize(delta) {
957
1407
  if (selectedIds.length === 0) return;
1408
+ pushHistory();
958
1409
  var diag = getCurrentDiagram();
959
1410
  selectedIds.forEach(function (id) {
960
1411
  var shape = diag.shapes.find(function (s) { return s.id === id; });
@@ -985,6 +1436,7 @@ function cancelPickMode() {
985
1436
  }
986
1437
 
987
1438
  function applyPickMode(srcShape) {
1439
+ pushHistory();
988
1440
  var diag = getCurrentDiagram();
989
1441
  pickTargetIds.forEach(function (id) {
990
1442
  if (id === srcShape.id) return;
@@ -1000,6 +1452,7 @@ function applyPickMode(srcShape) {
1000
1452
  shape.h = srcShape.h;
1001
1453
  shape.textAlign = srcShape.textAlign || "center";
1002
1454
  shape.textValign = srcShape.textValign || "middle";
1455
+ shape.rotation = srcShape.rotation || 0;
1003
1456
  }
1004
1457
  });
1005
1458
  selectedIds = pickTargetIds.slice();
@@ -1008,11 +1461,48 @@ function applyPickMode(srcShape) {
1008
1461
  document.getElementById("colorPanel").style.display = "flex";
1009
1462
  }
1010
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
+
1011
1500
  // ── Édition texte inline ──
1012
1501
  function startTextEdit(shapeId) {
1013
1502
  var diag = getCurrentDiagram();
1014
1503
  var shape = diag.shapes.find(function (s) { return s.id === shapeId; });
1015
1504
  if (!shape || shape.type === "image") return;
1505
+ if (shape.type === "table") return;
1016
1506
  editingShapeId = shapeId;
1017
1507
 
1018
1508
  var svg = document.getElementById("canvas");
@@ -1023,15 +1513,21 @@ function startTextEdit(shapeId) {
1023
1513
  var sh = shape.h * viewTransform.scale;
1024
1514
  var c = COLORS[shape.color] || COLORS[DEFAULT_COLOR];
1025
1515
 
1026
- var useTextarea = shape.type === "postit" || shape.type === "rect" || shape.type === "rounded" || shape.type === "db" || shape.type === "cloud";
1516
+ var useTextarea = shape.type === "postit" || shape.type === "rect" || shape.type === "rounded" || shape.type === "db" || shape.type === "cloud" || shape.type === "nuage";
1027
1517
  if (useTextarea) {
1028
1518
  var ta = document.getElementById("postitTextInput");
1029
1519
  ta.value = shape.text || "";
1030
- var hpad = shape.type === "cloud" ? Math.round(sw * 0.15) : 8;
1520
+ var hpad = shape.type === "cloud" ? Math.round(sw * 0.15) : shape.type === "nuage" ? Math.round(sw * 0.14) : 8;
1031
1521
  var vtop, vheight;
1032
1522
  if (shape.type === "cloud") {
1033
1523
  vtop = sy + Math.round(sh * 0.12);
1034
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;
1035
1531
  } else if (shape.type === "db") {
1036
1532
  var dbRy = Math.min(12, shape.h * 0.2) * viewTransform.scale;
1037
1533
  vtop = sy + dbRy * 2 + 4;
@@ -1057,9 +1553,15 @@ function startTextEdit(shapeId) {
1057
1553
  } else {
1058
1554
  var input = document.getElementById("shapeTextInput");
1059
1555
  input.value = shape.text || "";
1060
- input.style.left = (sx + sw * 0.08) + "px";
1061
- input.style.top = (sy + sh * 0.22) + "px";
1062
- input.style.width = (sw * 0.84) + "px";
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
+ }
1063
1565
  var editFs2 = shape.fontSize || 13;
1064
1566
  input.style.fontSize = Math.max(10, editFs2 * viewTransform.scale) + "px";
1065
1567
  input.style.color = c.text;
@@ -1076,18 +1578,32 @@ function confirmTextEdit() {
1076
1578
  if (editingShapeId) {
1077
1579
  var shape = diag.shapes.find(function (s) { return s.id === editingShapeId; });
1078
1580
  if (shape) {
1079
- var usesTextarea = shape.type === "postit" || shape.type === "rect" || shape.type === "rounded" || shape.type === "db" || shape.type === "cloud";
1080
- var val = usesTextarea
1081
- ? document.getElementById("postitTextInput").value
1082
- : input.value;
1083
- shape.text = val;
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
+ }
1084
1597
  saveDiagrammes();
1085
1598
  renderAll();
1086
1599
  }
1087
1600
  editingShapeId = null;
1088
1601
  } else if (editingArrowId) {
1089
1602
  var arrow = (diag.arrows || []).find(function (a) { return a.id === editingArrowId; });
1090
- if (arrow) { arrow.label = input.value; saveDiagrammes(); renderAll(); }
1603
+ if (arrow) {
1604
+ if (input.value !== (arrow.label || "")) pushHistory();
1605
+ arrow.label = input.value; saveDiagrammes(); renderAll();
1606
+ }
1091
1607
  editingArrowId = null;
1092
1608
  }
1093
1609
  document.getElementById("textOverlay").style.display = "none";
@@ -1095,6 +1611,285 @@ function confirmTextEdit() {
1095
1611
  document.getElementById("shapeTextInput").style.display = "block";
1096
1612
  }
1097
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
+
1098
1893
  function startArrowTextEdit(arrowId) {
1099
1894
  var diag = getCurrentDiagram();
1100
1895
  var arrow = (diag.arrows || []).find(function (a) { return a.id === arrowId; });
@@ -1134,7 +1929,16 @@ function shapeAt(x, y) {
1134
1929
  if (!diag) return null;
1135
1930
  for (var i = diag.shapes.length - 1; i >= 0; i--) {
1136
1931
  var s = diag.shapes[i];
1137
- if (x >= s.x && x <= s.x + s.w && y >= s.y && y <= s.y + s.h) return s;
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;
1138
1942
  }
1139
1943
  return null;
1140
1944
  }
@@ -1166,6 +1970,7 @@ function distToSeg(px, py, x1, y1, x2, y2) {
1166
1970
  function createArrow(fromId, toId) {
1167
1971
  var diag = getCurrentDiagram();
1168
1972
  if (diag.arrows.some(function (a) { return a.from === fromId && a.to === toId; })) return;
1973
+ pushHistory();
1169
1974
  var arrow = { id: "a" + Date.now(), from: fromId, to: toId, label: "" };
1170
1975
  diag.arrows.push(arrow);
1171
1976
  saveDiagrammes();
@@ -1219,13 +2024,26 @@ function onMouseDown(e) {
1219
2024
  return;
1220
2025
  }
1221
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
+
1222
2040
  // Clic sur la poignée de resize
1223
2041
  var grip = e.target.closest(".resize-grip");
1224
2042
  if (grip) {
1225
2043
  var sid = grip.getAttribute("data-shape-id");
1226
2044
  var shape = getCurrentDiagram().shapes.find(function (s) { return s.id === sid; });
1227
2045
  if (shape) {
1228
- dragState = { type: "resize", id: sid, sx: pt.x, sy: pt.y, ow: shape.w, oh: shape.h };
2046
+ dragState = { type: "resize", id: sid, sx: pt.x, sy: pt.y, ow: shape.w, oh: shape.h, snapshot: JSON.stringify(diagramsList) };
1229
2047
  }
1230
2048
  return;
1231
2049
  }
@@ -1238,7 +2056,20 @@ function onMouseDown(e) {
1238
2056
  var now = Date.now();
1239
2057
  if (now - lastClickTime < 350 && lastClickShapeId === shape.id) {
1240
2058
  lastClickTime = 0; lastClickShapeId = null;
1241
- startTextEdit(shape.id);
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
+ }
1242
2073
  return;
1243
2074
  }
1244
2075
  lastClickTime = now; lastClickShapeId = shape.id; lastClickArrowId = null;
@@ -1266,12 +2097,13 @@ function onMouseDown(e) {
1266
2097
  var s = diag.shapes.find(function (sh) { return sh.id === id; });
1267
2098
  return { id: id, ox: s ? s.x : 0, oy: s ? s.y : 0 };
1268
2099
  }),
2100
+ snapshot: JSON.stringify(diagramsList),
1269
2101
  };
1270
2102
  } else {
1271
2103
  // Clic simple → sélection unique
1272
2104
  selectedIds = [shape.id];
1273
2105
  selectedId = shape.id; selectedType = "shape";
1274
- dragState = { type: "move", id: shape.id, sx: pt.x, sy: pt.y, ox: shape.x, oy: shape.y };
2106
+ dragState = { type: "move", id: shape.id, sx: pt.x, sy: pt.y, ox: shape.x, oy: shape.y, snapshot: JSON.stringify(diagramsList) };
1275
2107
  document.getElementById("colorPanel").style.display = "flex";
1276
2108
  renderAll();
1277
2109
  }
@@ -1353,6 +2185,7 @@ function onMouseMove(e) {
1353
2185
  rb.setAttribute("height", Math.abs(pt.y - rubberBandState.sy));
1354
2186
  rb.style.display = "";
1355
2187
  } else if (dragState) {
2188
+ dragState.moved = true;
1356
2189
  var diag = getCurrentDiagram();
1357
2190
  if (dragState.type === "multi-move") {
1358
2191
  var dx = pt.x - dragState.sx;
@@ -1369,8 +2202,27 @@ function onMouseMove(e) {
1369
2202
  shape.x = Math.round(dragState.ox + pt.x - dragState.sx);
1370
2203
  shape.y = Math.round(dragState.oy + pt.y - dragState.sy);
1371
2204
  } else if (dragState.type === "resize") {
1372
- shape.w = Math.max(60, Math.round(dragState.ow + pt.x - dragState.sx));
1373
- shape.h = Math.max(30, Math.round(dragState.oh + pt.y - dragState.sy));
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;
1374
2226
  }
1375
2227
  renderAll();
1376
2228
  }
@@ -1384,6 +2236,31 @@ function onMouseMove(e) {
1384
2236
  function onMouseUp(e) {
1385
2237
  var pt = svgPoint(e.clientX, e.clientY);
1386
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
+
1387
2264
  // Fin de tracé de flèche via conn-dot
1388
2265
  if (arrowSrcId && !dragState) {
1389
2266
  document.getElementById("tempArrow").style.display = "none";
@@ -1416,9 +2293,34 @@ function onMouseUp(e) {
1416
2293
  }
1417
2294
 
1418
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
+ }
1419
2303
  saveDiagrammes();
1420
2304
  dragState = null;
1421
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
+ }
1422
2324
  }
1423
2325
  panStart = null;
1424
2326
  }
@@ -1431,9 +2333,11 @@ function onWheel(e) {
1431
2333
  applyZoom(e.deltaY < 0 ? 1.12 : 1 / 1.12, e.clientX - r.left, e.clientY - r.top);
1432
2334
  }
1433
2335
 
2336
+
1434
2337
  // ── Coller des formes ──
1435
2338
  function pasteShapes() {
1436
2339
  if (clipboard.length === 0) return;
2340
+ pushHistory();
1437
2341
  var diag = getCurrentDiagram();
1438
2342
  var now = Date.now();
1439
2343
  var pasted = clipboard.map(function (s, i) {
@@ -1456,14 +2360,22 @@ function pasteShapes() {
1456
2360
  document.addEventListener("DOMContentLoaded", function () {
1457
2361
  diagramsList = loadDiagrammes();
1458
2362
  if (!localStorage.getItem("mes_diagrammes")) saveDiagrammes();
1459
- var savedIdx = parseInt(localStorage.getItem("current_diagram_idx"), 10);
1460
- if (!isNaN(savedIdx) && savedIdx >= 0 && savedIdx < diagramsList.length) {
1461
- currentDiagramIdx = savedIdx;
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
+ }
1462
2373
  }
1463
- restoreLockForDiagram(currentDiagramIdx);
2374
+ restoreLockForDiagram(currentDiagramId);
1464
2375
  checkDiffDiagrammes();
1465
2376
  renderAll();
1466
2377
  updateLockBtn();
2378
+ updateBackBtn();
1467
2379
 
1468
2380
  var canvas = document.getElementById("canvas");
1469
2381
  canvas.addEventListener("mousedown", onMouseDown);
@@ -1505,6 +2417,43 @@ document.addEventListener("DOMContentLoaded", function () {
1505
2417
  editingShapeId = null;
1506
2418
  renderAll(); // restaure la visibilité du texte SVG
1507
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
+ }
1508
2457
  // Enter ajoute un saut de ligne (comportement natif textarea — pas de preventDefault)
1509
2458
  });
1510
2459
  postitInput.addEventListener("blur", function () {
@@ -1513,12 +2462,41 @@ document.addEventListener("DOMContentLoaded", function () {
1513
2462
 
1514
2463
  document.addEventListener("keydown", function (e) {
1515
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
+ }
1516
2479
  if (boardLocked) return;
1517
2480
  if (e.key === "Delete" || e.key === "Backspace") deleteSelected();
1518
2481
  if (e.key === "Escape") {
2482
+ hideLinkPicker();
1519
2483
  arrowSrcId = null;
1520
2484
  document.getElementById("tempArrow").style.display = "none";
1521
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;
1522
2500
  }
1523
2501
  if ((e.ctrlKey || e.metaKey) && e.key === "c") {
1524
2502
  e.preventDefault();
@@ -1561,7 +2539,13 @@ document.addEventListener("DOMContentLoaded", function () {
1561
2539
  if (e.target === this) this.classList.remove("open");
1562
2540
  });
1563
2541
 
2542
+
1564
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
+ }
1565
2549
  var panel = document.getElementById("diagramListPanel");
1566
2550
  if (!panel.classList.contains("open")) return;
1567
2551
  var burgerBtn = document.querySelector(".diagram-tool[onclick=\"toggleDiagramList()\"]");