doc-survival-kit 1.1.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"
@@ -26,7 +30,7 @@ var lastClickShapeId = null;
26
30
  var lastClickArrowId = null;
27
31
  var editingArrowId = null;
28
32
  var boardLocked = false;
29
- var diagDragSrcIdx = null;
33
+ var diagDragSrcId = null;
30
34
  var historyStack = [];
31
35
  var MAX_HISTORY = 50;
32
36
 
@@ -56,6 +60,7 @@ var COLORS = {
56
60
  "t-sky": { fill: "rgba(224,242,254,0.75)", stroke: "#0284c7", text: "#0369a1" },
57
61
  "t-rose": { fill: "rgba(255,228,230,0.75)", stroke: "#e11d48", text: "#be123c" },
58
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" },
59
64
  };
60
65
  var DEFAULT_COLOR = "t-sky";
61
66
 
@@ -131,17 +136,103 @@ function wrapPostitLines(text, maxWidth, fontSize) {
131
136
  return result;
132
137
  }
133
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
+
134
204
  function getCurrentDiagram() {
135
- 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;
136
210
  }
137
211
 
138
212
  // ── Persistance localStorage ──
139
213
  function loadDiagrammes() {
140
214
  try {
141
215
  var stored = JSON.parse(localStorage.getItem("mes_diagrammes"));
142
- 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
+ }
143
226
  } catch (e) {}
144
- 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;
145
236
  }
146
237
 
147
238
  function saveDiagrammes() {
@@ -157,7 +248,9 @@ function pushHistory() {
157
248
  function undoAction() {
158
249
  if (historyStack.length === 0) return;
159
250
  diagramsList = JSON.parse(historyStack.pop());
160
- if (currentDiagramIdx >= diagramsList.length) currentDiagramIdx = diagramsList.length - 1;
251
+ if (!findDiagramById(currentDiagramId, diagramsList)) {
252
+ currentDiagramId = diagramsList[0] ? String(diagramsList[0].id) : null;
253
+ }
161
254
  saveDiagrammes();
162
255
  renderAll();
163
256
  renderDiagramList();
@@ -177,14 +270,14 @@ function getLockMap() {
177
270
  try { return JSON.parse(localStorage.getItem("diagrammes_lock") || "{}"); } catch(e) { return {}; }
178
271
  }
179
272
  function saveCurrentLock() {
180
- var diag = diagramsList[currentDiagramIdx];
273
+ var diag = getCurrentDiagram();
181
274
  if (!diag) return;
182
275
  var map = getLockMap();
183
276
  map[diag.id] = boardLocked;
184
277
  localStorage.setItem("diagrammes_lock", JSON.stringify(map));
185
278
  }
186
- function restoreLockForDiagram(idx) {
187
- var diag = diagramsList[idx];
279
+ function restoreLockForDiagram(diagId) {
280
+ var diag = findDiagramById(diagId, diagramsList);
188
281
  if (!diag) return;
189
282
  var map = getLockMap();
190
283
  boardLocked = map[diag.id] === true;
@@ -213,14 +306,14 @@ function getZoomMap() {
213
306
  try { return JSON.parse(localStorage.getItem("diagrammes_zoom") || "{}"); } catch(e) { return {}; }
214
307
  }
215
308
  function saveCurrentZoom() {
216
- var diag = diagramsList[currentDiagramIdx];
309
+ var diag = getCurrentDiagram();
217
310
  if (!diag) return;
218
311
  var map = getZoomMap();
219
312
  map[diag.id] = viewTransform.scale;
220
313
  localStorage.setItem("diagrammes_zoom", JSON.stringify(map));
221
314
  }
222
- function restoreZoomForDiagram(idx) {
223
- var diag = diagramsList[idx];
315
+ function restoreZoomForDiagram(diagId) {
316
+ var diag = findDiagramById(diagId, diagramsList);
224
317
  if (!diag) return;
225
318
  var map = getZoomMap();
226
319
  viewTransform.scale = map[diag.id] !== undefined ? map[diag.id] : 1;
@@ -864,6 +957,36 @@ function renderShape(shape) {
864
957
  // ── Texte non rendu pour les images ──
865
958
  if (shape.type === "image") return g;
866
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
+
867
990
  // ── Points de connexion (visibles au hover et en mode flèche — sauf postit) ──
868
991
  if (shape.type !== "postit") {
869
992
  var hcx = shape.x + shape.w / 2, hcy = shape.y + shape.h / 2;
@@ -891,16 +1014,35 @@ function renderShape(shape) {
891
1014
  function getEdgePoint(shape, targetX, targetY) {
892
1015
  var cx = shape.x + shape.w / 2;
893
1016
  var cy = shape.y + shape.h / 2;
894
- var dx = targetX - cx, dy = targetY - cy;
895
- if (dx === 0 && dy === 0) return { x: cx, y: shape.y };
896
- var hw = shape.w / 2, hh = shape.h / 2;
897
- if (Math.abs(dx) * hh > Math.abs(dy) * hw) {
898
- var sx = dx > 0 ? 1 : -1;
899
- 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 };
900
1029
  } else {
901
- var sy = dy > 0 ? 1 : -1;
902
- 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
+ }
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 };
903
1044
  }
1045
+ return p;
904
1046
  }
905
1047
 
906
1048
  function renderArrow(arrow, shapes) {
@@ -989,104 +1131,93 @@ function renderAll() {
989
1131
  updateTableOverlay();
990
1132
  }
991
1133
 
992
- function renderDiagramList() {
993
- var el = document.getElementById("diagramList");
994
- el.innerHTML = diagramsList.map(function (d, i) {
995
- var active = i === currentDiagramIdx ? " active" : "";
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
+ : '';
996
1143
  return (
997
- '<div class="diagram-list-item' + active + '" draggable="true"' +
998
- ' ondragstart="onDiagDragStart(event,' + i + ')"' +
999
- ' ondragover="onDiagDragOver(event,' + i + ')"' +
1000
- ' ondragleave="onDiagDragLeave(event)"' +
1001
- ' ondrop="onDiagDrop(event,' + i + ')"' +
1002
- ' ondragend="onDiagDragEnd()"' +
1003
- ' onclick="selectDiagramme(' + i + ')">' +
1004
- '<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>' +
1005
1151
  '<span class="diagram-list-name">' + escDiag(d.titre) + '</span>' +
1006
- (diagramsList.length > 1
1007
- ? '<button class="diagram-list-del" onclick="event.stopPropagation();supprimerDiagramme(' + i + ')">×</button>'
1008
- : "") +
1009
- "</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>'
1010
1159
  );
1011
- }).join("");
1012
- }
1013
-
1014
- function onDiagDragStart(e, idx) {
1015
- diagDragSrcIdx = idx;
1016
- e.dataTransfer.effectAllowed = "move";
1017
- e.currentTarget.classList.add("dragging");
1160
+ }).join('');
1018
1161
  }
1019
1162
 
1020
- function onDiagDragOver(e, idx) {
1021
- e.preventDefault();
1022
- e.dataTransfer.dropEffect = "move";
1023
- document.querySelectorAll(".diagram-list-item").forEach(function (el) {
1024
- el.classList.remove("drag-over-after", "drag-over-before");
1025
- });
1026
- if (idx !== diagDragSrcIdx) {
1027
- var goingDown = diagDragSrcIdx < idx;
1028
- e.currentTarget.classList.add(goingDown ? "drag-over-after" : "drag-over-before");
1029
- }
1030
- }
1031
-
1032
- function onDiagDragLeave(e) {
1033
- 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();
1034
1168
  }
1035
1169
 
1036
- function onDiagDrop(e, idx) {
1037
- e.preventDefault();
1038
- if (diagDragSrcIdx === null || diagDragSrcIdx === idx) {
1039
- onDiagDragEnd();
1040
- return;
1041
- }
1042
- var src = diagDragSrcIdx;
1043
- var item = diagramsList.splice(src, 1)[0];
1044
-
1045
- // insertAt = idx dans les deux cas :
1046
- // - descente (src < idx) : après remove, la cible est à idx-1, on insère à idx = après elle ✓
1047
- // - montée (src > idx) : après remove, la cible est toujours à idx, on insère à idx = avant elle ✓
1048
- diagramsList.splice(idx, 0, item);
1049
-
1050
- var cur = currentDiagramIdx;
1051
- if (cur === src) {
1052
- currentDiagramIdx = idx;
1170
+ function toggleDiagExpand(id) {
1171
+ var key = String(id);
1172
+ if (diagExpandedIds[key]) {
1173
+ delete diagExpandedIds[key];
1053
1174
  } else {
1054
- var adjustedCur = cur > src ? cur - 1 : cur;
1055
- currentDiagramIdx = adjustedCur >= idx ? adjustedCur + 1 : adjustedCur;
1175
+ diagExpandedIds[key] = true;
1056
1176
  }
1057
- localStorage.setItem("current_diagram_idx", currentDiagramIdx);
1058
- diagDragSrcIdx = null;
1059
- saveDiagrammes();
1060
1177
  renderDiagramList();
1061
1178
  }
1062
1179
 
1063
- function onDiagDragEnd() {
1064
- diagDragSrcIdx = null;
1065
- document.querySelectorAll(".diagram-list-item").forEach(function (el) {
1066
- el.classList.remove("dragging", "drag-over-after", "drag-over-before");
1067
- });
1068
- }
1069
-
1070
1180
  // ── Gestion des diagrammes ──
1071
- function selectDiagramme(idx) {
1181
+ function selectDiagramme(id, clearStack) {
1182
+ if (clearStack) diagNavStack = [];
1072
1183
  saveCurrentZoom();
1073
1184
  saveCurrentLock();
1074
- currentDiagramIdx = idx;
1075
- localStorage.setItem("current_diagram_idx", idx);
1076
- selectedId = null; selectedType = null;
1185
+ currentDiagramId = String(id);
1186
+ localStorage.setItem("current_diagram_id", String(id));
1187
+ selectedId = null; selectedType = null;
1077
1188
  selectedIds = [];
1078
- restoreZoomForDiagram(idx);
1079
- restoreLockForDiagram(idx);
1189
+ pendingNavDiagId = null;
1190
+ restoreZoomForDiagram(id);
1191
+ restoreLockForDiagram(id);
1080
1192
  viewTransform.x = 60;
1081
1193
  viewTransform.y = 60;
1082
1194
  document.getElementById("colorPanel").style.display = "none";
1083
1195
  renderAll();
1084
1196
  updateLockBtn();
1197
+ updateBackBtn();
1085
1198
  document.getElementById("diagramListPanel").classList.remove("open");
1086
1199
  }
1087
1200
 
1201
+ function goBackDiagram() {
1202
+ if (diagNavStack.length === 0) return;
1203
+ var prevId = diagNavStack.pop();
1204
+ selectDiagramme(prevId);
1205
+ }
1206
+
1088
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);
1089
1219
  pendingNewDiagram = true;
1220
+ diagExpandedIds[String(parentId)] = true;
1090
1221
  var input = document.getElementById("diagramTitle");
1091
1222
  input.value = "";
1092
1223
  input.placeholder = window.t ? window.t.diag_new_diagram : "Nouveau diagramme";
@@ -1100,10 +1231,21 @@ function confirmerNouveauDiagramme() {
1100
1231
  var input = document.getElementById("diagramTitle");
1101
1232
  var titre = input.value.trim() || (window.t ? window.t.diag_new_diagram : "Nouveau diagramme");
1102
1233
  input.placeholder = "";
1103
- var d = { id: Date.now(), titre: titre, shapes: [], arrows: [] };
1104
- 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
+ }
1105
1247
  saveDiagrammes();
1106
- selectDiagramme(diagramsList.length - 1);
1248
+ selectDiagramme(d.id);
1107
1249
  document.getElementById("diagramTitle").blur();
1108
1250
  }
1109
1251
 
@@ -1117,18 +1259,31 @@ function annulerNouveauDiagramme() {
1117
1259
  input.blur();
1118
1260
  }
1119
1261
 
1120
- function supprimerDiagramme(idx) {
1121
- if (diagramsList.length <= 1) return;
1262
+ function supprimerDiagramme(id) {
1263
+ var info = findParentListOf(id, diagramsList);
1264
+ if (!info) return;
1265
+ if (info.list === diagramsList && info.list.length <= 1) return;
1122
1266
  pushHistory();
1123
- diagramsList.splice(idx, 1);
1124
- if (currentDiagramIdx >= diagramsList.length) currentDiagramIdx = diagramsList.length - 1;
1125
- localStorage.setItem("current_diagram_idx", currentDiagramIdx);
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
+ }
1126
1272
  saveDiagrammes();
1127
1273
  renderAll();
1128
1274
  }
1129
1275
 
1130
1276
  function toggleDiagramList() {
1131
- 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
+ }
1132
1287
  }
1133
1288
 
1134
1289
  // ── Outil actif ──
@@ -1297,6 +1452,7 @@ function applyPickMode(srcShape) {
1297
1452
  shape.h = srcShape.h;
1298
1453
  shape.textAlign = srcShape.textAlign || "center";
1299
1454
  shape.textValign = srcShape.textValign || "middle";
1455
+ shape.rotation = srcShape.rotation || 0;
1300
1456
  }
1301
1457
  });
1302
1458
  selectedIds = pickTargetIds.slice();
@@ -1305,6 +1461,42 @@ function applyPickMode(srcShape) {
1305
1461
  document.getElementById("colorPanel").style.display = "flex";
1306
1462
  }
1307
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
+
1308
1500
  // ── Édition texte inline ──
1309
1501
  function startTextEdit(shapeId) {
1310
1502
  var diag = getCurrentDiagram();
@@ -1560,7 +1752,143 @@ function updateTableOverlay() {
1560
1752
  ar.style.display = rr.style.display = "flex";
1561
1753
  }
1562
1754
 
1563
- function syncColorPanel() {}
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
+ }
1564
1892
 
1565
1893
  function startArrowTextEdit(arrowId) {
1566
1894
  var diag = getCurrentDiagram();
@@ -1601,7 +1929,16 @@ function shapeAt(x, y) {
1601
1929
  if (!diag) return null;
1602
1930
  for (var i = diag.shapes.length - 1; i >= 0; i--) {
1603
1931
  var s = diag.shapes[i];
1604
- 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;
1605
1942
  }
1606
1943
  return null;
1607
1944
  }
@@ -1899,6 +2236,31 @@ function onMouseMove(e) {
1899
2236
  function onMouseUp(e) {
1900
2237
  var pt = svgPoint(e.clientX, e.clientY);
1901
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
+
1902
2264
  // Fin de tracé de flèche via conn-dot
1903
2265
  if (arrowSrcId && !dragState) {
1904
2266
  document.getElementById("tempArrow").style.display = "none";
@@ -1931,6 +2293,9 @@ function onMouseUp(e) {
1931
2293
  }
1932
2294
 
1933
2295
  if (dragState) {
2296
+ var wasMoved = dragState.moved;
2297
+ var draggedId = dragState.id;
2298
+ var dragType = dragState.type;
1934
2299
  if (dragState.moved && dragState.snapshot) {
1935
2300
  historyStack.push(dragState.snapshot);
1936
2301
  if (historyStack.length > MAX_HISTORY) historyStack.shift();
@@ -1938,6 +2303,24 @@ function onMouseUp(e) {
1938
2303
  saveDiagrammes();
1939
2304
  dragState = null;
1940
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
+ }
1941
2324
  }
1942
2325
  panStart = null;
1943
2326
  }
@@ -1977,14 +2360,22 @@ function pasteShapes() {
1977
2360
  document.addEventListener("DOMContentLoaded", function () {
1978
2361
  diagramsList = loadDiagrammes();
1979
2362
  if (!localStorage.getItem("mes_diagrammes")) saveDiagrammes();
1980
- var savedIdx = parseInt(localStorage.getItem("current_diagram_idx"), 10);
1981
- if (!isNaN(savedIdx) && savedIdx >= 0 && savedIdx < diagramsList.length) {
1982
- 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
+ }
1983
2373
  }
1984
- restoreLockForDiagram(currentDiagramIdx);
2374
+ restoreLockForDiagram(currentDiagramId);
1985
2375
  checkDiffDiagrammes();
1986
2376
  renderAll();
1987
2377
  updateLockBtn();
2378
+ updateBackBtn();
1988
2379
 
1989
2380
  var canvas = document.getElementById("canvas");
1990
2381
  canvas.addEventListener("mousedown", onMouseDown);
@@ -2038,15 +2429,25 @@ document.addEventListener("DOMContentLoaded", function () {
2038
2429
  var rows = shape.rows || 3;
2039
2430
  var cols = shape.cols || 3;
2040
2431
  var nextRow = editingTableCell.row;
2041
- var nextCol = editingTableCell.col + 1;
2042
- if (nextCol >= cols) { nextRow++; nextCol = 0; }
2043
- // Dernière cellule ajouter une ligne
2044
- if (nextRow >= rows) {
2045
- pushHistory();
2046
- shape.rows = rows + 1;
2047
- if (!shape.cells) shape.cells = [];
2048
- while (shape.cells.length < shape.rows) shape.cells.push([]);
2049
- shape.cells[shape.rows - 1] = new Array(cols).fill("");
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
+ }
2050
2451
  }
2051
2452
  var shapeId = editingShapeId;
2052
2453
  saveDiagrammes();
@@ -2062,12 +2463,40 @@ document.addEventListener("DOMContentLoaded", function () {
2062
2463
  document.addEventListener("keydown", function (e) {
2063
2464
  if (document.activeElement.tagName === "INPUT" || document.activeElement.tagName === "TEXTAREA") return;
2064
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
+ }
2065
2479
  if (boardLocked) return;
2066
2480
  if (e.key === "Delete" || e.key === "Backspace") deleteSelected();
2067
2481
  if (e.key === "Escape") {
2482
+ hideLinkPicker();
2068
2483
  arrowSrcId = null;
2069
2484
  document.getElementById("tempArrow").style.display = "none";
2070
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;
2071
2500
  }
2072
2501
  if ((e.ctrlKey || e.metaKey) && e.key === "c") {
2073
2502
  e.preventDefault();
@@ -2112,6 +2541,11 @@ document.addEventListener("DOMContentLoaded", function () {
2112
2541
 
2113
2542
 
2114
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
+ }
2115
2549
  var panel = document.getElementById("diagramListPanel");
2116
2550
  if (!panel.classList.contains("open")) return;
2117
2551
  var burgerBtn = document.querySelector(".diagram-tool[onclick=\"toggleDiagramList()\"]");