@sequent-org/moodboard 1.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.
- package/package.json +44 -0
- package/src/assets/icons/README.md +105 -0
- package/src/assets/icons/attachments.svg +3 -0
- package/src/assets/icons/clear.svg +5 -0
- package/src/assets/icons/comments.svg +3 -0
- package/src/assets/icons/emoji.svg +6 -0
- package/src/assets/icons/frame.svg +3 -0
- package/src/assets/icons/image.svg +3 -0
- package/src/assets/icons/note.svg +3 -0
- package/src/assets/icons/pan.svg +3 -0
- package/src/assets/icons/pencil.svg +3 -0
- package/src/assets/icons/redo.svg +3 -0
- package/src/assets/icons/select.svg +9 -0
- package/src/assets/icons/shapes.svg +3 -0
- package/src/assets/icons/text-add.svg +3 -0
- package/src/assets/icons/topbar/README.md +39 -0
- package/src/assets/icons/topbar/grid-cross.svg +6 -0
- package/src/assets/icons/topbar/grid-dot.svg +3 -0
- package/src/assets/icons/topbar/grid-line.svg +3 -0
- package/src/assets/icons/topbar/grid-off.svg +3 -0
- package/src/assets/icons/topbar/paint.svg +3 -0
- package/src/assets/icons/undo.svg +3 -0
- package/src/core/ApiClient.js +309 -0
- package/src/core/EventBus.js +42 -0
- package/src/core/HistoryManager.js +261 -0
- package/src/core/KeyboardManager.js +710 -0
- package/src/core/PixiEngine.js +439 -0
- package/src/core/SaveManager.js +381 -0
- package/src/core/StateManager.js +64 -0
- package/src/core/commands/BaseCommand.js +68 -0
- package/src/core/commands/CopyObjectCommand.js +44 -0
- package/src/core/commands/CreateObjectCommand.js +46 -0
- package/src/core/commands/DeleteObjectCommand.js +146 -0
- package/src/core/commands/EditFileNameCommand.js +107 -0
- package/src/core/commands/GroupMoveCommand.js +47 -0
- package/src/core/commands/GroupReorderZCommand.js +74 -0
- package/src/core/commands/GroupResizeCommand.js +37 -0
- package/src/core/commands/GroupRotateCommand.js +41 -0
- package/src/core/commands/MoveObjectCommand.js +89 -0
- package/src/core/commands/PasteObjectCommand.js +103 -0
- package/src/core/commands/ReorderZCommand.js +45 -0
- package/src/core/commands/ResizeObjectCommand.js +135 -0
- package/src/core/commands/RotateObjectCommand.js +70 -0
- package/src/core/commands/index.js +14 -0
- package/src/core/events/Events.js +147 -0
- package/src/core/index.js +1632 -0
- package/src/core/rendering/GeometryUtils.js +89 -0
- package/src/core/rendering/HitTestManager.js +186 -0
- package/src/core/rendering/LayerManager.js +137 -0
- package/src/core/rendering/ObjectRenderer.js +363 -0
- package/src/core/rendering/PixiRenderer.js +140 -0
- package/src/core/rendering/index.js +9 -0
- package/src/grid/BaseGrid.js +164 -0
- package/src/grid/CrossGrid.js +75 -0
- package/src/grid/DotGrid.js +148 -0
- package/src/grid/GridFactory.js +173 -0
- package/src/grid/LineGrid.js +115 -0
- package/src/index.js +2 -0
- package/src/moodboard/ActionHandler.js +114 -0
- package/src/moodboard/DataManager.js +114 -0
- package/src/moodboard/MoodBoard.js +359 -0
- package/src/moodboard/WorkspaceManager.js +103 -0
- package/src/objects/BaseObject.js +1 -0
- package/src/objects/CommentObject.js +115 -0
- package/src/objects/DrawingObject.js +114 -0
- package/src/objects/EmojiObject.js +98 -0
- package/src/objects/FileObject.js +318 -0
- package/src/objects/FrameObject.js +127 -0
- package/src/objects/ImageObject.js +72 -0
- package/src/objects/NoteObject.js +227 -0
- package/src/objects/ObjectFactory.js +61 -0
- package/src/objects/ShapeObject.js +134 -0
- package/src/objects/StampObject.js +0 -0
- package/src/objects/StickerObject.js +0 -0
- package/src/objects/TextObject.js +123 -0
- package/src/services/BoardService.js +85 -0
- package/src/services/FileUploadService.js +398 -0
- package/src/services/FrameService.js +138 -0
- package/src/services/ImageUploadService.js +246 -0
- package/src/services/ZOrderManager.js +50 -0
- package/src/services/ZoomPanController.js +78 -0
- package/src/src.7z +0 -0
- package/src/src.zip +0 -0
- package/src/src2.zip +0 -0
- package/src/tools/AlignmentGuides.js +326 -0
- package/src/tools/BaseTool.js +257 -0
- package/src/tools/ResizeHandles.js +381 -0
- package/src/tools/ToolManager.js +580 -0
- package/src/tools/board-tools/PanTool.js +43 -0
- package/src/tools/board-tools/ZoomTool.js +393 -0
- package/src/tools/object-tools/DrawingTool.js +404 -0
- package/src/tools/object-tools/PlacementTool.js +1005 -0
- package/src/tools/object-tools/SelectTool.js +2183 -0
- package/src/tools/object-tools/TextTool.js +416 -0
- package/src/tools/object-tools/selection/BoxSelectController.js +105 -0
- package/src/tools/object-tools/selection/GeometryUtils.js +101 -0
- package/src/tools/object-tools/selection/GroupDragController.js +61 -0
- package/src/tools/object-tools/selection/GroupResizeController.js +90 -0
- package/src/tools/object-tools/selection/GroupRotateController.js +61 -0
- package/src/tools/object-tools/selection/HandlesSync.js +96 -0
- package/src/tools/object-tools/selection/ResizeController.js +68 -0
- package/src/tools/object-tools/selection/RotateController.js +58 -0
- package/src/tools/object-tools/selection/SelectionModel.js +42 -0
- package/src/tools/object-tools/selection/SimpleDragController.js +45 -0
- package/src/ui/CommentPopover.js +187 -0
- package/src/ui/ContextMenu.js +340 -0
- package/src/ui/FilePropertiesPanel.js +298 -0
- package/src/ui/FramePropertiesPanel.js +462 -0
- package/src/ui/HtmlHandlesLayer.js +778 -0
- package/src/ui/HtmlTextLayer.js +279 -0
- package/src/ui/MapPanel.js +290 -0
- package/src/ui/NotePropertiesPanel.js +502 -0
- package/src/ui/SaveStatus.js +250 -0
- package/src/ui/TextPropertiesPanel.js +911 -0
- package/src/ui/Toolbar.js +1118 -0
- package/src/ui/Topbar.js +220 -0
- package/src/ui/ZoomPanel.js +116 -0
- package/src/ui/styles/workspace.css +854 -0
- package/src/utils/colors.js +0 -0
- package/src/utils/geometry.js +0 -0
- package/src/utils/iconLoader.js +270 -0
- package/src/utils/objectIdGenerator.js +17 -0
- package/src/utils/topbarIconLoader.js +114 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
// src/core/ApiClient.js
|
|
2
|
+
export class ApiClient {
|
|
3
|
+
constructor(baseUrl, authToken = null) {
|
|
4
|
+
this.baseUrl = baseUrl;
|
|
5
|
+
this.authToken = authToken;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
async getBoard(boardId) {
|
|
9
|
+
try {
|
|
10
|
+
const response = await fetch(`/api/moodboard/${boardId}`, {
|
|
11
|
+
method: 'GET',
|
|
12
|
+
headers: {
|
|
13
|
+
'Accept': 'application/json',
|
|
14
|
+
'X-Requested-With': 'XMLHttpRequest'
|
|
15
|
+
},
|
|
16
|
+
credentials: 'same-origin'
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
if (!response.ok) {
|
|
20
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const result = await response.json();
|
|
24
|
+
|
|
25
|
+
if (result.success) {
|
|
26
|
+
return { data: result.data };
|
|
27
|
+
} else {
|
|
28
|
+
throw new Error(result.message || 'Ошибка загрузки доски');
|
|
29
|
+
}
|
|
30
|
+
} catch (error) {
|
|
31
|
+
console.warn('API: Ошибка загрузки доски, используем заглушку:', error);
|
|
32
|
+
// Fallback к заглушке
|
|
33
|
+
return {
|
|
34
|
+
data: {
|
|
35
|
+
id: boardId,
|
|
36
|
+
name: 'Demo Board',
|
|
37
|
+
objects: []
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async saveBoard(boardId, boardData) {
|
|
44
|
+
try {
|
|
45
|
+
// Фильтруем объекты изображений и файлов - убираем избыточные данные
|
|
46
|
+
const cleanedData = this._cleanObjectData(boardData);
|
|
47
|
+
|
|
48
|
+
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
|
49
|
+
|
|
50
|
+
const response = await fetch('/api/moodboard/save', {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
headers: {
|
|
53
|
+
'Content-Type': 'application/json',
|
|
54
|
+
'Accept': 'application/json',
|
|
55
|
+
'X-CSRF-TOKEN': csrfToken,
|
|
56
|
+
'X-Requested-With': 'XMLHttpRequest'
|
|
57
|
+
},
|
|
58
|
+
credentials: 'same-origin',
|
|
59
|
+
body: JSON.stringify({
|
|
60
|
+
boardId: boardId,
|
|
61
|
+
boardData: cleanedData
|
|
62
|
+
})
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (!response.ok) {
|
|
66
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const result = await response.json();
|
|
70
|
+
|
|
71
|
+
if (result.success) {
|
|
72
|
+
return { success: true, data: result };
|
|
73
|
+
} else {
|
|
74
|
+
throw new Error(result.message || 'Ошибка сохранения доски');
|
|
75
|
+
}
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.error('ApiClient: Ошибка сохранения доски:', error);
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Очищает данные объектов от избыточной информации
|
|
84
|
+
* @private
|
|
85
|
+
*/
|
|
86
|
+
_cleanObjectData(boardData) {
|
|
87
|
+
if (!boardData || !boardData.objects) {
|
|
88
|
+
return boardData;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const cleanedObjects = boardData.objects.map(obj => {
|
|
92
|
+
if (obj.type === 'image') {
|
|
93
|
+
console.log('🧹 DEBUG _cleanImageData: обрабатываем изображение:', {
|
|
94
|
+
id: obj.id,
|
|
95
|
+
imageId: obj.imageId,
|
|
96
|
+
hasSrc: !!obj.src,
|
|
97
|
+
hasPropertiesSrc: !!obj.properties?.src,
|
|
98
|
+
srcIsBase64: !!(obj.src && obj.src.startsWith('data:')),
|
|
99
|
+
propertiesSrcIsBase64: !!(obj.properties?.src && obj.properties.src.startsWith('data:'))
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const cleanedObj = { ...obj };
|
|
103
|
+
|
|
104
|
+
// Если есть imageId, убираем src для экономии места
|
|
105
|
+
if (obj.imageId && typeof obj.imageId === 'string' && obj.imageId.trim().length > 0) {
|
|
106
|
+
console.log('🧹 DEBUG _cleanImageData: у изображения есть imageId, убираем src');
|
|
107
|
+
|
|
108
|
+
// Убираем src с верхнего уровня
|
|
109
|
+
if (cleanedObj.src) {
|
|
110
|
+
delete cleanedObj.src;
|
|
111
|
+
console.log('🧹 DEBUG: удален src с верхнего уровня');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Убираем src из properties
|
|
115
|
+
if (cleanedObj.properties?.src) {
|
|
116
|
+
cleanedObj.properties = { ...cleanedObj.properties };
|
|
117
|
+
delete cleanedObj.properties.src;
|
|
118
|
+
console.log('🧹 DEBUG: удален src из properties');
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// Если нет imageId, предупреждаем о base64
|
|
122
|
+
else {
|
|
123
|
+
console.log('🧹 DEBUG _cleanImageData: у изображения НЕТ imageId, оставляем src как есть');
|
|
124
|
+
if (cleanedObj.properties?.src && cleanedObj.properties.src.startsWith('data:')) {
|
|
125
|
+
console.warn('❌ Изображение сохраняется с base64 в properties, так как нет imageId:', cleanedObj.id);
|
|
126
|
+
}
|
|
127
|
+
if (cleanedObj.src && cleanedObj.src.startsWith('data:')) {
|
|
128
|
+
console.warn('❌ Изображение сохраняется с base64 в src, так как нет imageId:', cleanedObj.id);
|
|
129
|
+
}
|
|
130
|
+
if (!obj.imageId) {
|
|
131
|
+
console.warn('❌ У изображения отсутствует imageId:', cleanedObj.id);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return cleanedObj;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (obj.type === 'file') {
|
|
139
|
+
console.log('🧹 DEBUG _cleanObjectData: обрабатываем файл:', {
|
|
140
|
+
id: obj.id,
|
|
141
|
+
fileId: obj.fileId,
|
|
142
|
+
hasContent: !!obj.content,
|
|
143
|
+
hasPropertiesContent: !!obj.properties?.content
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const cleanedObj = { ...obj };
|
|
147
|
+
|
|
148
|
+
// Если есть fileId, убираем content для экономии места
|
|
149
|
+
if (obj.fileId && typeof obj.fileId === 'string' && obj.fileId.trim().length > 0) {
|
|
150
|
+
console.log('🧹 DEBUG _cleanObjectData: у файла есть fileId, убираем content');
|
|
151
|
+
|
|
152
|
+
// Убираем content с верхнего уровня
|
|
153
|
+
if (cleanedObj.content) {
|
|
154
|
+
delete cleanedObj.content;
|
|
155
|
+
console.log('🧹 DEBUG: удален content с верхнего уровня');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Убираем content из properties
|
|
159
|
+
if (cleanedObj.properties?.content) {
|
|
160
|
+
cleanedObj.properties = { ...cleanedObj.properties };
|
|
161
|
+
delete cleanedObj.properties.content;
|
|
162
|
+
console.log('🧹 DEBUG: удален content из properties');
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// Если нет fileId, предупреждаем о наличии content
|
|
166
|
+
else {
|
|
167
|
+
console.log('🧹 DEBUG _cleanObjectData: у файла НЕТ fileId, оставляем content как есть');
|
|
168
|
+
if (cleanedObj.properties?.content) {
|
|
169
|
+
console.warn('❌ Файл сохраняется с content в properties, так как нет fileId:', cleanedObj.id);
|
|
170
|
+
}
|
|
171
|
+
if (cleanedObj.content) {
|
|
172
|
+
console.warn('❌ Файл сохраняется с content, так как нет fileId:', cleanedObj.id);
|
|
173
|
+
}
|
|
174
|
+
if (!obj.fileId) {
|
|
175
|
+
console.warn('❌ У файла отсутствует fileId:', cleanedObj.id);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return cleanedObj;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return obj;
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
...boardData,
|
|
187
|
+
objects: cleanedObjects
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Восстанавливает URL изображений и файлов при загрузке
|
|
193
|
+
*/
|
|
194
|
+
async restoreObjectUrls(boardData) {
|
|
195
|
+
if (!boardData || !boardData.objects) {
|
|
196
|
+
return boardData;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const restoredObjects = await Promise.all(
|
|
200
|
+
boardData.objects.map(async (obj) => {
|
|
201
|
+
if (obj.type === 'image') {
|
|
202
|
+
console.log('🔗 DEBUG restoreImageUrls: обрабатываем изображение:', {
|
|
203
|
+
id: obj.id,
|
|
204
|
+
imageId: obj.imageId,
|
|
205
|
+
hasSrc: !!obj.src,
|
|
206
|
+
hasPropertiesSrc: !!obj.properties?.src
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
if (obj.imageId && (!obj.src && !obj.properties?.src)) {
|
|
210
|
+
console.log('🔗 DEBUG: восстанавливаем URL для изображения');
|
|
211
|
+
try {
|
|
212
|
+
// Формируем URL изображения
|
|
213
|
+
const imageUrl = `/api/images/${obj.imageId}/file`;
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
...obj,
|
|
217
|
+
src: imageUrl,
|
|
218
|
+
properties: {
|
|
219
|
+
...obj.properties,
|
|
220
|
+
src: imageUrl
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
} catch (error) {
|
|
224
|
+
console.warn(`Не удалось восстановить URL для изображения ${obj.imageId}:`, error);
|
|
225
|
+
return obj;
|
|
226
|
+
}
|
|
227
|
+
} else {
|
|
228
|
+
console.log('🔗 DEBUG: изображение уже имеет URL или нет imageId, оставляем как есть');
|
|
229
|
+
return obj;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (obj.type === 'file') {
|
|
234
|
+
console.log('🔗 DEBUG restoreObjectUrls: обрабатываем файл:', {
|
|
235
|
+
id: obj.id,
|
|
236
|
+
fileId: obj.fileId,
|
|
237
|
+
hasUrl: !!obj.url,
|
|
238
|
+
hasPropertiesUrl: !!obj.properties?.url
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
if (obj.fileId) {
|
|
242
|
+
console.log('🔗 DEBUG: восстанавливаем данные для файла');
|
|
243
|
+
try {
|
|
244
|
+
// Формируем URL файла для скачивания
|
|
245
|
+
const fileUrl = `/api/files/${obj.fileId}/download`;
|
|
246
|
+
|
|
247
|
+
// Создаем обновленный объект с восстановленными данными
|
|
248
|
+
const restoredObj = {
|
|
249
|
+
...obj,
|
|
250
|
+
url: fileUrl,
|
|
251
|
+
properties: {
|
|
252
|
+
...obj.properties,
|
|
253
|
+
url: fileUrl
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
// Пытаемся восстановить актуальные метаданные файла с сервера
|
|
258
|
+
// (Это будет выполнено асинхронно, чтобы не блокировать загрузку)
|
|
259
|
+
setTimeout(async () => {
|
|
260
|
+
try {
|
|
261
|
+
const response = await fetch(`/api/files/${obj.fileId}`, {
|
|
262
|
+
headers: {
|
|
263
|
+
'Accept': 'application/json',
|
|
264
|
+
'X-Requested-With': 'XMLHttpRequest'
|
|
265
|
+
},
|
|
266
|
+
credentials: 'same-origin'
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
if (response.ok) {
|
|
270
|
+
const result = await response.json();
|
|
271
|
+
if (result.success && result.data) {
|
|
272
|
+
console.log('🔄 Обновляем метаданные файла с сервера:', result.data);
|
|
273
|
+
// Эмитим событие для обновления метаданных файла в состоянии
|
|
274
|
+
// (это будет обработано в core, если EventBus доступен)
|
|
275
|
+
if (typeof window !== 'undefined' && window.moodboardEventBus) {
|
|
276
|
+
window.moodboardEventBus.emit('file:metadata:updated', {
|
|
277
|
+
objectId: obj.id,
|
|
278
|
+
fileId: obj.fileId,
|
|
279
|
+
metadata: result.data
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
} catch (error) {
|
|
285
|
+
console.warn(`Не удалось обновить метаданные файла ${obj.fileId}:`, error);
|
|
286
|
+
}
|
|
287
|
+
}, 100);
|
|
288
|
+
|
|
289
|
+
return restoredObj;
|
|
290
|
+
} catch (error) {
|
|
291
|
+
console.warn(`Не удалось восстановить данные для файла ${obj.fileId}:`, error);
|
|
292
|
+
return obj;
|
|
293
|
+
}
|
|
294
|
+
} else {
|
|
295
|
+
console.log('🔗 DEBUG: файл не имеет fileId, оставляем как есть');
|
|
296
|
+
return obj;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return obj;
|
|
301
|
+
})
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
return {
|
|
305
|
+
...boardData,
|
|
306
|
+
objects: restoredObjects
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export class EventBus {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.events = new Map();
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
on(event, callback) {
|
|
7
|
+
if (!this.events.has(event)) {
|
|
8
|
+
this.events.set(event, new Set());
|
|
9
|
+
}
|
|
10
|
+
this.events.get(event).add(callback);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
off(event, callback) {
|
|
14
|
+
const callbacks = this.events.get(event);
|
|
15
|
+
if (callbacks) {
|
|
16
|
+
callbacks.delete(callback);
|
|
17
|
+
|
|
18
|
+
// Если callback'ов больше нет, удаляем событие из Map
|
|
19
|
+
if (callbacks.size === 0) {
|
|
20
|
+
this.events.delete(event);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
emit(event, data) {
|
|
26
|
+
const callbacks = this.events.get(event);
|
|
27
|
+
if (callbacks) {
|
|
28
|
+
callbacks.forEach(callback => {
|
|
29
|
+
try {
|
|
30
|
+
callback(data);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
// Логируем ошибку, но продолжаем выполнение
|
|
33
|
+
console.error(`Error in event callback for '${event}':`, error);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
removeAllListeners() {
|
|
40
|
+
this.events.clear();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Управляет историей команд для функции Undo/Redo
|
|
3
|
+
*/
|
|
4
|
+
import { Events } from './events/Events.js';
|
|
5
|
+
export class HistoryManager {
|
|
6
|
+
constructor(eventBus, options = {}) {
|
|
7
|
+
this.eventBus = eventBus;
|
|
8
|
+
this.options = {
|
|
9
|
+
maxHistorySize: 50, // Максимальное количество команд в истории
|
|
10
|
+
mergeTimeout: 1000, // Время в мс для объединения похожих команд
|
|
11
|
+
...options
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// История выполненных команд
|
|
15
|
+
this.history = [];
|
|
16
|
+
// Текущая позиция в истории
|
|
17
|
+
this.currentIndex = -1;
|
|
18
|
+
// Флаг для предотвращения зацикливания при undo/redo
|
|
19
|
+
this.isExecutingCommand = false;
|
|
20
|
+
|
|
21
|
+
this.initEventListeners();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
initEventListeners() {
|
|
25
|
+
// Слушаем события клавиатуры
|
|
26
|
+
this.eventBus.on(Events.Keyboard.Undo, () => {
|
|
27
|
+
this.undo();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
this.eventBus.on(Events.Keyboard.Redo, () => {
|
|
31
|
+
this.redo();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Для отладки
|
|
35
|
+
this.eventBus.on(Events.History.Debug, () => {
|
|
36
|
+
this.debugHistory();
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Выполнить команду и добавить в историю
|
|
42
|
+
*/
|
|
43
|
+
executeCommand(command) {
|
|
44
|
+
if (this.isExecutingCommand) {
|
|
45
|
+
// Если мы в процессе undo/redo, не добавляем в историю
|
|
46
|
+
this._executeCommandSafely(command);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
// Проверяем, можно ли объединить с последней командой
|
|
53
|
+
const lastCommand = this.getLastCommand();
|
|
54
|
+
if (lastCommand &&
|
|
55
|
+
lastCommand.canMergeWith(command) &&
|
|
56
|
+
(command.timestamp - lastCommand.timestamp) < this.options.mergeTimeout) {
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
lastCommand.mergeWith(command);
|
|
60
|
+
this.eventBus.emit('history:changed', {
|
|
61
|
+
canUndo: this.canUndo(),
|
|
62
|
+
canRedo: this.canRedo(),
|
|
63
|
+
historySize: this.history.length
|
|
64
|
+
});
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Выполняем команду
|
|
69
|
+
this._executeCommandSafely(command);
|
|
70
|
+
|
|
71
|
+
// Удаляем все команды после текущей позиции (если есть)
|
|
72
|
+
if (this.currentIndex < this.history.length - 1) {
|
|
73
|
+
this.history = this.history.slice(0, this.currentIndex + 1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Добавляем новую команду
|
|
77
|
+
this.history.push(command);
|
|
78
|
+
this.currentIndex++;
|
|
79
|
+
|
|
80
|
+
// Ограничиваем размер истории
|
|
81
|
+
if (this.history.length > this.options.maxHistorySize) {
|
|
82
|
+
this.history.shift();
|
|
83
|
+
this.currentIndex--;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Уведомляем об изменении истории
|
|
87
|
+
this.eventBus.emit(Events.History.Changed, {
|
|
88
|
+
canUndo: this.canUndo(),
|
|
89
|
+
canRedo: this.canRedo(),
|
|
90
|
+
historySize: this.history.length,
|
|
91
|
+
currentCommand: command.toString()
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Безопасно выполняет команду (поддерживает синхронные и асинхронные команды)
|
|
99
|
+
* @private
|
|
100
|
+
*/
|
|
101
|
+
_executeCommandSafely(command) {
|
|
102
|
+
try {
|
|
103
|
+
const result = command.execute();
|
|
104
|
+
// Если команда возвращает Promise, обрабатываем асинхронно
|
|
105
|
+
if (result && typeof result.then === 'function') {
|
|
106
|
+
result.catch(error => {
|
|
107
|
+
console.error('Ошибка выполнения асинхронной команды:', error);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
} catch (error) {
|
|
111
|
+
console.error('Ошибка выполнения команды:', error);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Отменить последнюю команду
|
|
117
|
+
*/
|
|
118
|
+
undo() {
|
|
119
|
+
if (!this.canUndo()) {
|
|
120
|
+
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const command = this.history[this.currentIndex];
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
this.isExecutingCommand = true;
|
|
128
|
+
try {
|
|
129
|
+
command.undo();
|
|
130
|
+
this.currentIndex--;
|
|
131
|
+
|
|
132
|
+
this.eventBus.emit(Events.History.Changed, {
|
|
133
|
+
canUndo: this.canUndo(),
|
|
134
|
+
canRedo: this.canRedo(),
|
|
135
|
+
historySize: this.history.length,
|
|
136
|
+
lastUndone: command.toString()
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
return true;
|
|
141
|
+
} catch (error) {
|
|
142
|
+
console.error('❌ Ошибка при отмене команды:', error);
|
|
143
|
+
return false;
|
|
144
|
+
} finally {
|
|
145
|
+
this.isExecutingCommand = false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Повторить отмененную команду
|
|
151
|
+
*/
|
|
152
|
+
redo() {
|
|
153
|
+
if (!this.canRedo()) {
|
|
154
|
+
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
this.currentIndex++;
|
|
159
|
+
const command = this.history[this.currentIndex];
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
this.isExecutingCommand = true;
|
|
163
|
+
try {
|
|
164
|
+
this._executeCommandSafely(command);
|
|
165
|
+
|
|
166
|
+
this.eventBus.emit(Events.History.Changed, {
|
|
167
|
+
canUndo: this.canUndo(),
|
|
168
|
+
canRedo: this.canRedo(),
|
|
169
|
+
historySize: this.history.length,
|
|
170
|
+
lastRedone: command.toString()
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
return true;
|
|
175
|
+
} catch (error) {
|
|
176
|
+
console.error('❌ Ошибка при повторе команды:', error);
|
|
177
|
+
this.currentIndex--; // Откатываем индекс при ошибке
|
|
178
|
+
return false;
|
|
179
|
+
} finally {
|
|
180
|
+
this.isExecutingCommand = false;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Можно ли отменить команду
|
|
186
|
+
*/
|
|
187
|
+
canUndo() {
|
|
188
|
+
return this.currentIndex >= 0;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Можно ли повторить команду
|
|
193
|
+
*/
|
|
194
|
+
canRedo() {
|
|
195
|
+
return this.currentIndex < this.history.length - 1;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Получить последнюю команду
|
|
200
|
+
*/
|
|
201
|
+
getLastCommand() {
|
|
202
|
+
if (this.history.length === 0) return null;
|
|
203
|
+
return this.history[this.history.length - 1];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Очистить историю
|
|
208
|
+
*/
|
|
209
|
+
clear() {
|
|
210
|
+
this.history = [];
|
|
211
|
+
this.currentIndex = -1;
|
|
212
|
+
|
|
213
|
+
this.eventBus.emit(Events.History.Changed, {
|
|
214
|
+
canUndo: false,
|
|
215
|
+
canRedo: false,
|
|
216
|
+
historySize: 0
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Получить информацию об истории (для отладки)
|
|
224
|
+
*/
|
|
225
|
+
getHistoryInfo() {
|
|
226
|
+
return {
|
|
227
|
+
totalCommands: this.history.length,
|
|
228
|
+
currentIndex: this.currentIndex,
|
|
229
|
+
canUndo: this.canUndo(),
|
|
230
|
+
canRedo: this.canRedo(),
|
|
231
|
+
commands: this.history.map((cmd, index) => ({
|
|
232
|
+
index,
|
|
233
|
+
isCurrent: index === this.currentIndex,
|
|
234
|
+
command: cmd.toString()
|
|
235
|
+
}))
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Вывести историю в консоль (для отладки)
|
|
241
|
+
*/
|
|
242
|
+
debugHistory() {
|
|
243
|
+
// Отладочная информация об истории команд
|
|
244
|
+
const info = this.getHistoryInfo();
|
|
245
|
+
console.group('📚 История команд');
|
|
246
|
+
console.table(info.commands);
|
|
247
|
+
console.log(`Позиция: ${this.currentIndex + 1}/${this.history.length}`);
|
|
248
|
+
console.log(`Undo: ${this.canUndo()}, Redo: ${this.canRedo()}`);
|
|
249
|
+
console.groupEnd();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Уничтожить менеджер истории
|
|
254
|
+
*/
|
|
255
|
+
destroy() {
|
|
256
|
+
this.clear();
|
|
257
|
+
this.eventBus.removeAllListeners(Events.Keyboard.Undo);
|
|
258
|
+
this.eventBus.removeAllListeners(Events.Keyboard.Redo);
|
|
259
|
+
this.eventBus.removeAllListeners(Events.History.Debug);
|
|
260
|
+
}
|
|
261
|
+
}
|