@sequent-org/moodboard 1.2.74 → 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sequent-org/moodboard",
3
- "version": "1.2.74",
3
+ "version": "1.2.75",
4
4
  "type": "module",
5
5
  "description": "Interactive moodboard",
6
6
  "main": "./src/index.js",
@@ -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
- saveDelay: 1500, // Оптимальная задержка 1.5 секунды
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
- this.saveImmediately();
81
- // Предупреждаем о несохраненных изменениях
82
- e.preventDefault();
83
- e.returnValue = 'У вас есть несохраненные изменения. Вы уверены, что хотите покинуть страницу?';
84
- return e.returnValue;
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
  */