@sequent-org/moodboard 1.2.73 → 1.2.75
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
CHANGED
package/src/core/SaveManager.js
CHANGED
|
@@ -7,13 +7,19 @@ export class SaveManager {
|
|
|
7
7
|
this.eventBus = eventBus;
|
|
8
8
|
this.apiClient = null; // Будет установлен позже через setApiClient
|
|
9
9
|
this.options = {
|
|
10
|
-
// Фиксированные настройки автосохранения
|
|
10
|
+
// Фиксированные настройки автосохранения данных
|
|
11
11
|
autoSave: true,
|
|
12
|
-
|
|
12
|
+
// Уменьшенная задержка, чтобы изменения чаще успевали сохраняться
|
|
13
|
+
// (раньше было 1500 мс)
|
|
14
|
+
saveDelay: 400,
|
|
13
15
|
maxRetries: 3,
|
|
14
16
|
retryDelay: 1000,
|
|
15
17
|
periodicSaveInterval: 30000, // Периодическое сохранение каждые 30 сек
|
|
16
|
-
|
|
18
|
+
|
|
19
|
+
// Фоновая отправка при закрытии вкладки/перезагрузке страницы
|
|
20
|
+
// (используется navigator.sendBeacon при поддержке браузером)
|
|
21
|
+
useBeaconOnUnload: true,
|
|
22
|
+
|
|
17
23
|
// Настраиваемые эндпоинты (берем из options)
|
|
18
24
|
saveEndpoint: options.saveEndpoint || '/api/moodboard/save',
|
|
19
25
|
loadEndpoint: options.loadEndpoint || '/api/moodboard/load'
|
|
@@ -74,16 +80,24 @@ export class SaveManager {
|
|
|
74
80
|
|
|
75
81
|
// Отслеживание перемещений теперь происходит через команды и state:changed
|
|
76
82
|
|
|
77
|
-
// Сохранение при закрытии страницы
|
|
83
|
+
// Сохранение при закрытии страницы (в том числе при резком закрытии окна)
|
|
78
84
|
window.addEventListener('beforeunload', (e) => {
|
|
79
|
-
if (this.hasUnsavedChanges)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
if (!this.hasUnsavedChanges) return;
|
|
86
|
+
try {
|
|
87
|
+
if (this.options.useBeaconOnUnload) {
|
|
88
|
+
this._flushOnUnload();
|
|
89
|
+
} else {
|
|
90
|
+
this._flushSyncFallback();
|
|
91
|
+
}
|
|
92
|
+
} catch (_) { /* игнорируем, чтобы не блокировать закрытие */ }
|
|
93
|
+
|
|
94
|
+
// Сообщаем браузеру, что есть незафиксированные изменения
|
|
95
|
+
// (текст может быть проигнорирован, но событие задержит закрытие на доли секунды)
|
|
96
|
+
e.preventDefault();
|
|
97
|
+
e.returnedValue = '';
|
|
98
|
+
e.returnValue = '';
|
|
99
|
+
return '';
|
|
100
|
+
}, { capture: true });
|
|
87
101
|
|
|
88
102
|
// Периодическое автосохранение
|
|
89
103
|
if (this.options.autoSave) {
|
|
@@ -366,6 +380,84 @@ export class SaveManager {
|
|
|
366
380
|
};
|
|
367
381
|
}
|
|
368
382
|
|
|
383
|
+
/**
|
|
384
|
+
* Синхронно собирает текущие данные для сохранения через EventBus.
|
|
385
|
+
* Используется в обработчиках закрытия вкладки, где нет времени на ожидание async.
|
|
386
|
+
*/
|
|
387
|
+
_collectCurrentDataSync() {
|
|
388
|
+
try {
|
|
389
|
+
const requestData = { data: null };
|
|
390
|
+
this.eventBus && this.eventBus.emit(Events.Save.GetBoardData, requestData);
|
|
391
|
+
return requestData.data || null;
|
|
392
|
+
} catch (_) {
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Отправляет данные в фоне при закрытии вкладки (navigator.sendBeacon),
|
|
399
|
+
* а при отсутствии поддержки — выполняет синхронный XHR как запасной вариант.
|
|
400
|
+
*/
|
|
401
|
+
_flushOnUnload() {
|
|
402
|
+
const data = this._collectCurrentDataSync();
|
|
403
|
+
if (!data) return;
|
|
404
|
+
|
|
405
|
+
const boardId = data.id || 'default';
|
|
406
|
+
const payload = {
|
|
407
|
+
boardId,
|
|
408
|
+
// отправляем «сырой» снимок; на серверной стороне допускается приём JSON
|
|
409
|
+
boardData: data,
|
|
410
|
+
settings: data.settings || undefined,
|
|
411
|
+
// CSRF токен добавим в тело (для серверов, которые принимают _token из JSON)
|
|
412
|
+
_token: (typeof document !== 'undefined') ? (document.querySelector('meta[name="csrf-token"]')?.value || document.querySelector('meta[name="csrf-token"]')?.getAttribute('content')) : undefined
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
const body = JSON.stringify(payload);
|
|
416
|
+
|
|
417
|
+
let sent = false;
|
|
418
|
+
try {
|
|
419
|
+
if (typeof navigator !== 'undefined' && typeof navigator.sendBeacon === 'function') {
|
|
420
|
+
const blob = new Blob([body], { type: 'application/json' });
|
|
421
|
+
sent = navigator.sendBeacon(this.options.saveEndpoint, blob);
|
|
422
|
+
}
|
|
423
|
+
} catch (_) { /* игнорируем */ }
|
|
424
|
+
|
|
425
|
+
if (!sent) {
|
|
426
|
+
// Фолбэк: синхронный XHR (доступен в beforeunload, но может быть заблокирован политиками браузера)
|
|
427
|
+
this._flushSyncFallback(data);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Считаем, что попытались сохранить; снимаем флаг, чтобы не спамить повторно
|
|
431
|
+
this.hasUnsavedChanges = false;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Синхронная отправка запроса на сохранение (fallback для устаревших браузеров).
|
|
436
|
+
*/
|
|
437
|
+
_flushSyncFallback(existingData) {
|
|
438
|
+
try {
|
|
439
|
+
const data = existingData || this._collectCurrentDataSync();
|
|
440
|
+
if (!data) return;
|
|
441
|
+
|
|
442
|
+
const boardId = data.id || 'default';
|
|
443
|
+
const csrfToken = (typeof document !== 'undefined') ? (document.querySelector('meta[name="csrf-token"]').getAttribute('content')) : null;
|
|
444
|
+
|
|
445
|
+
const xhr = new XMLHttpRequest();
|
|
446
|
+
xhr.open('POST', this.options.saveEndpoint, false); // синхронно
|
|
447
|
+
xhr.setRequestHeader('Content-Type', 'application/json');
|
|
448
|
+
xhr.setRequestHeader('Accept', 'application/json');
|
|
449
|
+
if (csrfToken) xhr.setRequestHeader('X-CSRF-TOKEN', csrfToken);
|
|
450
|
+
|
|
451
|
+
const payload = {
|
|
452
|
+
b: boardId,
|
|
453
|
+
boardData: data,
|
|
454
|
+
settings: data.settings || undefined,
|
|
455
|
+
_token: csrfToken || undefined
|
|
456
|
+
};
|
|
457
|
+
try { xhr.send(JSON.stringify(payload)); } catch (_) { /* игнорируем */ }
|
|
458
|
+
} catch (_) { /* игнорируем */ }
|
|
459
|
+
}
|
|
460
|
+
|
|
369
461
|
/**
|
|
370
462
|
* Очистка ресурсов
|
|
371
463
|
*/
|
|
@@ -49,7 +49,8 @@ export class FileObject {
|
|
|
49
49
|
fill: 0x333333,
|
|
50
50
|
align: 'center',
|
|
51
51
|
wordWrap: true,
|
|
52
|
-
|
|
52
|
+
breakWords: true,
|
|
53
|
+
wordWrapWidth: Math.max(1, this.width - 24), // горизонтальный padding 12px с каждой стороны
|
|
53
54
|
lineHeight: 14,
|
|
54
55
|
resolution: (typeof window !== 'undefined' && window.devicePixelRatio) ? window.devicePixelRatio : 1
|
|
55
56
|
});
|
|
@@ -239,9 +240,11 @@ export class FileObject {
|
|
|
239
240
|
_updateTextPosition() {
|
|
240
241
|
if (!this.fileNameText) return;
|
|
241
242
|
|
|
242
|
-
// Обновляем стиль текста (ограничиваем по подложке и переносим
|
|
243
|
+
// Обновляем стиль текста (ограничиваем по подложке и переносим слова, учитываем padding)
|
|
244
|
+
const sidePad = 12;
|
|
243
245
|
this.fileNameText.style.wordWrap = true;
|
|
244
|
-
this.fileNameText.style.
|
|
246
|
+
this.fileNameText.style.breakWords = true;
|
|
247
|
+
this.fileNameText.style.wordWrapWidth = Math.max(1, this.width - sidePad * 2);
|
|
245
248
|
this.fileNameText.updateText();
|
|
246
249
|
|
|
247
250
|
// Параметры иконки и отступов
|
|
@@ -611,7 +611,8 @@ export class PlacementTool extends BaseTool {
|
|
|
611
611
|
fill: 0x333333,
|
|
612
612
|
align: 'center',
|
|
613
613
|
wordWrap: true,
|
|
614
|
-
|
|
614
|
+
breakWords: true,
|
|
615
|
+
wordWrapWidth: Math.max(1, width - 24) // padding 12px по бокам
|
|
615
616
|
});
|
|
616
617
|
nameText.anchor.set(0.5, 0);
|
|
617
618
|
nameText.x = width / 2;
|