@sequent-org/moodboard 1.2.30 → 1.2.35

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.30",
3
+ "version": "1.2.35",
4
4
  "type": "module",
5
5
  "description": "Interactive moodboard",
6
6
  "main": "./src/index.js",
@@ -45,22 +45,6 @@ export class ApiClient {
45
45
  // Фильтруем объекты изображений и файлов - убираем избыточные данные
46
46
  const cleanedData = this._cleanObjectData(boardData);
47
47
 
48
- // Диагностика размеров изображений перед отправкой
49
- try {
50
- const images = Array.isArray(cleanedData?.objects)
51
- ? cleanedData.objects.filter(o => o && o.type === 'image')
52
- : [];
53
- images.forEach((o) => {
54
- console.log('🧪 SAVE image size', {
55
- id: o.id,
56
- width: o.width,
57
- height: o.height,
58
- propWidth: o.properties?.width,
59
- propHeight: o.properties?.height
60
- });
61
- });
62
- } catch (_) {}
63
-
64
48
  const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
65
49
 
66
50
  const response = await fetch('/api/moodboard/save', {
@@ -157,41 +157,6 @@ export class SaveManager {
157
157
  data: saveData,
158
158
  timestamp: new Date().toISOString()
159
159
  });
160
-
161
- // Диагностика: по флагу проверяем, что сервер отдает те же размеры на чтение
162
- try {
163
- if (typeof window !== 'undefined' && window.MOODBOARD_DEBUG_VERIFY_LOAD === true) {
164
- const boardId = saveData?.id;
165
- if (boardId) {
166
- const verifyUrl = `${this.options.loadEndpoint}/${boardId}`;
167
- const resp = await fetch(verifyUrl, {
168
- method: 'GET',
169
- headers: {
170
- 'Accept': 'application/json',
171
- 'X-Requested-With': 'XMLHttpRequest'
172
- }
173
- });
174
- if (resp.ok) {
175
- const result = await resp.json();
176
- const data = result?.data || result;
177
- const imgs = Array.isArray(data?.objects) ? data.objects.filter(o => o && o.type === 'image') : [];
178
- imgs.forEach((o) => {
179
- console.log('🧪 VERIFY LOAD after save image size', {
180
- id: o.id,
181
- width: o.width,
182
- height: o.height,
183
- propWidth: o.properties?.width,
184
- propHeight: o.properties?.height
185
- });
186
- });
187
- } else {
188
- console.warn('⚠️ VERIFY LOAD failed:', resp.status, resp.statusText);
189
- }
190
- }
191
- }
192
- } catch (e) {
193
- console.warn('⚠️ VERIFY LOAD exception:', e?.message || e);
194
- }
195
160
  } else {
196
161
  throw new Error(response.message || 'Ошибка сохранения');
197
162
  }
package/src/core/index.js CHANGED
@@ -1067,80 +1067,84 @@ export class CoreMoodBoard {
1067
1067
  const object = objects.find(obj => obj.id === data.object);
1068
1068
  const objectType = object ? object.type : null;
1069
1069
 
1070
- // Сохраняем пропорции для фреймов, кроме произвольных (lockedAspect=false)
1071
- if ((objectType === 'frame' || (objectType === 'image' && object?.properties?.isEmojiIcon)) && data.size) {
1072
- const lockedAspect = !!(object?.properties && (object.properties.lockedAspect === true));
1070
+ // Сохраняем пропорции:
1071
+ // - всегда для изображений (включая эмоджи-иконки, которые квадратные)
1072
+ // - для фреймов только если lockedAspect=true
1073
+ if (data.size && (objectType === 'image' || objectType === 'frame')) {
1073
1074
  const isEmoji = (objectType === 'image' && object?.properties?.isEmojiIcon);
1074
- if (!lockedAspect && !isEmoji) {
1075
- // произвольные фреймы без ограничений
1076
- } else {
1077
- const start = this._activeResize?.startSize || { width: object.width, height: object.height };
1078
- const aspect = isEmoji ? 1 : ((start.width > 0 && start.height > 0) ? (start.width / start.height) : (object.width / Math.max(1, object.height)));
1079
- let w = Math.max(1, data.size.width);
1080
- let h = Math.max(1, data.size.height);
1081
- const hndl = (this._activeResize?.handle || '').toLowerCase();
1082
- if (isEmoji) {
1083
- // Делаем квадрат, фиксируя противоположную сторону к старту
1084
- const s = Math.max(w, h);
1085
- // Вычислим позицию сразу, чтобы не было дрейфа правой/нижней границы
1075
+ const isImage = (objectType === 'image');
1076
+ const lockedAspect = objectType === 'frame'
1077
+ ? !!(object?.properties && object.properties.lockedAspect === true)
1078
+ : true; // для изображений всегда держим аспект
1079
+
1080
+ if (lockedAspect || isImage || isEmoji) {
1081
+ const start = this._activeResize?.startSize || { width: object.width, height: object.height };
1082
+ const startW = Math.max(1, start.width);
1083
+ const startH = Math.max(1, start.height);
1084
+ const aspect = isEmoji ? 1 : (startW / startH);
1085
+
1086
+ let w = Math.max(1, data.size.width);
1087
+ let h = Math.max(1, data.size.height);
1088
+ const hndl = (this._activeResize?.handle || '').toLowerCase();
1089
+
1090
+ if (isEmoji) {
1091
+ // Квадрат
1092
+ const s = Math.max(w, h);
1093
+ if (!data.position && this._activeResize && this._activeResize.objectId === data.object) {
1094
+ const startPos = this._activeResize.startPosition;
1095
+ const sw = this._activeResize.startSize.width;
1096
+ const sh = this._activeResize.startSize.height;
1097
+ let x = startPos.x;
1098
+ let y = startPos.y;
1099
+ if (hndl.includes('w')) { x = startPos.x + (sw - s); }
1100
+ if (hndl.includes('n')) { y = startPos.y + (sh - s); }
1101
+ const isEdge = ['n','s','e','w'].includes(hndl);
1102
+ if (isEdge) {
1103
+ if (hndl === 'n' || hndl === 's') x = startPos.x + Math.round((sw - s) / 2);
1104
+ if (hndl === 'e' || hndl === 'w') y = startPos.y + Math.round((sh - s) / 2);
1105
+ }
1106
+ data.position = { x: Math.round(x), y: Math.round(y) };
1107
+ }
1108
+ w = s; h = s;
1109
+ } else {
1110
+ // Поддержка аспекта (для images всегда; для frames — если lockedAspect)
1111
+ const dw = Math.abs(w - startW);
1112
+ const dh = Math.abs(h - startH);
1113
+ if (dw >= dh) { h = Math.round(w / aspect); } else { w = Math.round(h * aspect); }
1114
+ }
1115
+
1116
+ // Минимальная площадь — только для фреймов (как раньше)
1117
+ if (objectType === 'frame') {
1118
+ const minArea = 1800;
1119
+ const area = Math.max(1, w * h);
1120
+ if (area < minArea) {
1121
+ const scale = Math.sqrt(minArea / area);
1122
+ w = Math.round(w * scale);
1123
+ h = Math.round(h * scale);
1124
+ }
1125
+ }
1126
+
1127
+ data.size = { width: w, height: h };
1128
+
1129
+ // Компенсация позиции по зафиксированной стороне
1086
1130
  if (!data.position && this._activeResize && this._activeResize.objectId === data.object) {
1087
1131
  const startPos = this._activeResize.startPosition;
1088
1132
  const sw = this._activeResize.startSize.width;
1089
1133
  const sh = this._activeResize.startSize.height;
1090
1134
  let x = startPos.x;
1091
1135
  let y = startPos.y;
1092
- if (hndl.includes('w')) { x = startPos.x + (sw - s); }
1093
- if (hndl.includes('n')) { y = startPos.y + (sh - s); }
1136
+ if (hndl.includes('w')) { x = startPos.x + (sw - data.size.width); }
1137
+ if (hndl.includes('n')) { y = startPos.y + (sh - data.size.height); }
1094
1138
  const isEdge = ['n','s','e','w'].includes(hndl);
1095
1139
  if (isEdge) {
1096
- if (hndl === 'n' || hndl === 's') x = startPos.x + Math.round((sw - s) / 2);
1097
- if (hndl === 'e' || hndl === 'w') y = startPos.y + Math.round((sh - s) / 2);
1140
+ if (hndl === 'n' || hndl === 's') {
1141
+ x = startPos.x + Math.round((sw - data.size.width) / 2);
1142
+ } else if (hndl === 'e' || hndl === 'w') {
1143
+ y = startPos.y + Math.round((sh - data.size.height) / 2);
1144
+ }
1098
1145
  }
1099
1146
  data.position = { x: Math.round(x), y: Math.round(y) };
1100
1147
  }
1101
- w = s; h = s;
1102
- } else {
1103
- // Обычная поддержка аспекта для фреймов
1104
- const dw = Math.abs(w - start.width);
1105
- const dh = Math.abs(h - start.height);
1106
- if (dw >= dh) { h = Math.round(w / aspect); } else { w = Math.round(h * aspect); }
1107
- }
1108
- // Минимальная площадь — только для фреймов
1109
- if (!isEmoji) {
1110
- const minArea = 1800;
1111
- const area = Math.max(1, w * h);
1112
- if (area < minArea) {
1113
- const scale = Math.sqrt(minArea / area);
1114
- w = Math.round(w * scale);
1115
- h = Math.round(h * scale);
1116
- }
1117
- }
1118
- data.size = { width: w, height: h };
1119
-
1120
- // Если позиция известна (фиксированная противоположная сторона) — откорректируем её
1121
- if (!data.position && this._activeResize && this._activeResize.objectId === data.object) {
1122
- const hndl = (this._activeResize.handle || '').toLowerCase();
1123
- const startPos = this._activeResize.startPosition;
1124
- const sw = this._activeResize.startSize.width;
1125
- const sh = this._activeResize.startSize.height;
1126
- let x = startPos.x;
1127
- let y = startPos.y;
1128
- // Базовая привязка противоположной стороны
1129
- if (hndl.includes('w')) { x = startPos.x + (sw - w); }
1130
- if (hndl.includes('n')) { y = startPos.y + (sh - h); }
1131
- // Симметрическая компенсация по перпендикулярной оси для edge-хэндлов
1132
- const isEdge = ['n','s','e','w'].includes(hndl);
1133
- if (isEdge) {
1134
- if (hndl === 'n' || hndl === 's') {
1135
- // Вверх/вниз: верх или низ фиксируется, ширина меняется симметрично относительно центра
1136
- x = startPos.x + Math.round((sw - w) / 2);
1137
- } else if (hndl === 'e' || hndl === 'w') {
1138
- // Вправо/влево: левая или правая фиксируется, высота меняется симметрично относительно центра
1139
- y = startPos.y + Math.round((sh - h) / 2);
1140
- }
1141
- }
1142
- data.position = { x: Math.round(x), y: Math.round(y) };
1143
- }
1144
1148
  }
1145
1149
  }
1146
1150
 
@@ -1185,8 +1189,8 @@ export class CoreMoodBoard {
1185
1189
  // Принудительно сохраняем пропорции для фреймов (если lockedAspect=true)
1186
1190
  const objects = this.state.getObjects();
1187
1191
  const object = objects.find(obj => obj.id === data.object);
1188
- const objectType = object ? object.type : null;
1189
- if (objectType === 'frame' && !!(object?.properties && object.properties.lockedAspect === true)) {
1192
+ const objectType = object ? object : null;
1193
+ if (object && object.type === 'frame' && object.properties && object.properties.lockedAspect === true) {
1190
1194
  const start = this._activeResize?.startSize || { width: object.width, height: object.height };
1191
1195
  const aspect = (start.width > 0 && start.height > 0) ? (start.width / start.height) : (object.width / Math.max(1, object.height));
1192
1196
  let w = Math.max(1, data.newSize.width);
@@ -1194,7 +1198,7 @@ export class CoreMoodBoard {
1194
1198
  const dw = Math.abs(w - start.width);
1195
1199
  const dh = Math.abs(h - start.height);
1196
1200
  if (dw >= dh) { h = Math.round(w / aspect); } else { w = Math.round(h * aspect); }
1197
- // Минимальная площадь фрейма ~1800px²
1201
+ // Минимальная площадь фрейма ~х2 по сторонам
1198
1202
  const minArea = 1800;
1199
1203
  const area = Math.max(1, w * h);
1200
1204
  if (area < minArea) {
@@ -1204,7 +1208,7 @@ export class CoreMoodBoard {
1204
1208
  }
1205
1209
  data.newSize = { width: w, height: h };
1206
1210
  if (!data.newPosition && this._activeResize && this._activeResize.objectId === data.object) {
1207
- const hndl = (this._activeResize.handle || '').toLowerCase();
1211
+ const hndl = (this._activeResize?.full || this._activeResize?.handle || '').toLowerCase();
1208
1212
  const startPos = this._activeResize.startPosition;
1209
1213
  const sw = this._activeResize.startSize.width;
1210
1214
  const sh = this._activeResize.startSize.height;
@@ -1212,19 +1216,45 @@ export class CoreMoodBoard {
1212
1216
  let y = startPos.y;
1213
1217
  if (hndl.includes('w')) { x = startPos.x + (sw - w); }
1214
1218
  if (hndl.includes('n')) { y = startPos.y + (sh - h); }
1215
- const isEdge = ['n','s','e','w'].includes(hnl = hndl);
1219
+ const isEdge = ['n','s','e','w'].includes(hndl);
1216
1220
  if (isEdge) {
1217
- if (hnl === 'n' || hnl === 's') {
1218
- x = startPos.x + Math.round((sw - w) / 2);
1219
- } else if (hnl === 'e' || hnl === 'w') {
1220
- y = startPos.y + Math.round((sh - h) / 2);
1221
- }
1221
+ if (hndl === 'n' || hndl === 's') x = Math.round(startPos.x + (sw - w) / 2);
1222
+ if (hndl === 'e' || hndl === 'w') y = Math.round(startPos.y + (sh - h) / 2);
1222
1223
  }
1223
1224
  data.newPosition = { x: Math.round(x), y: Math.round(y) };
1224
1225
  }
1226
+ } else if (object && object.type === 'image') {
1227
+ // Для изображений всегда фиксируем исходное соотношение сторон
1228
+ const start = this._activeResize?.startSize || { width: object.width, height: object.height };
1229
+ const startW = Math.max(1, start.width);
1230
+ const startH = Math.max(1, start.height);
1231
+ const aspect = startW / startH;
1232
+ let w = Math.max(1, data.newSize.width);
1233
+ let h = Math.max(1, data.newSize.height);
1234
+ const dw = Math.abs(w - startW);
1235
+ const dh = Math.abs(h - startH);
1236
+ if (dw >= dh) { h = Math.round(w / aspect); } else { w = Math.round(h * aspect); }
1237
+ data.newSize = { width: w, height: h };
1238
+ if (!data.newPosition && this._activeResize && this._activeResize.objectId === data.object) {
1239
+ const hndl = (this.extent?.handle || this._activeResize?.handle || '').match ? (this._activeResize?.handle || '') : '';
1240
+ const handle = (this._activeResize?.handle || '').toString().toLowerCase();
1241
+ const startPos = this._activeResize.startPosition || { x: 0, y: 0 };
1242
+ const sw = this._activeResize.startSize?.width || startW;
1243
+ const sh = this._activeResize.startSize?.height || startH;
1244
+ let x = startPos.x;
1245
+ let y = startPos.y;
1246
+ if (handle.includes('w')) { x = startPos.x + (sw - w); }
1247
+ if (handle.includes('n')) { y = startPos.y + (sh - h); }
1248
+ const edge = ['n','s','e','w'].includes(handle);
1249
+ if (edge) {
1250
+ if (handle === 'n' || handle === 's') x = Math.round(startPos.x + (sw - w) / 2);
1251
+ if (handle === 'e' || handle === 'w') y = Math.round(startPos.y + (sh - h) / 2);
1252
+ }
1253
+ data.newPosition = { x: Math.floor(x), y: Math.floor(y) };
1254
+ }
1225
1255
  }
1226
1256
  // Для произвольных фреймов также обеспечим минимальную площадь
1227
- if (objectType === 'frame' && data.newSize && !(object?.properties && object.properties.lockedAspect === true)) {
1257
+ if (object && object.type === 'frame' && data.newSize && !(object.properties && object.properties === true)) {
1228
1258
  const minArea = 1800;
1229
1259
  const w0 = Math.max(1, data.newSize.width);
1230
1260
  const h0 = Math.max(1, data.newSize.height);
@@ -1235,22 +1265,20 @@ export class CoreMoodBoard {
1235
1265
  const h = Math.round(h0 * scale);
1236
1266
  data.newSize = { width: w, height: h };
1237
1267
  if (!data.newPosition && this._activeResize && this._activeResize.objectId === data.object) {
1238
- const hndl2 = (this._activeResize.handle || '').toLowerCase();
1239
- const startPos2 = this._activeResize.startPosition;
1268
+ const h2 = (this._activeResize?.handle || '').toLowerCase();
1269
+ const sPos2 = this._activeResize.startPosition;
1240
1270
  const sw2 = this._activeResize.startSize.width;
1241
1271
  const sh2 = this._activeResize.startSize.height;
1242
- let x2 = startPos2.x;
1243
- let y2 = startPos2.y;
1244
- if (hndl2.includes('w')) { x2 = startPos2.x + (sw2 - w); }
1245
- if (hndl2.includes('n')) { y2 = startPos2.y + (sh2 - h); }
1272
+ let x2 = sPos2.x;
1273
+ let y2 = sPos2.y;
1274
+ if (h2.includes('w')) { x2 = sPos2.x + (sw2 - w); }
1275
+ if (h2.includes('n')) { y2 = sPos2.y + (sh2 - h); }
1246
1276
  data.newPosition = { x: Math.round(x2), y: Math.round(y2) };
1247
1277
  }
1248
1278
  }
1249
1279
  }
1250
1280
  // Создаем команду только если размер действительно изменился
1251
- if (data.oldSize.width !== data.newSize.width ||
1252
- data.oldSize.height !== data.newSize.height) {
1253
-
1281
+ if (data.oldSize.width !== data.newSize.width || data.oldSize.height !== data.newSize.height) {
1254
1282
  console.log(`📝 Создаем ResizeObjectCommand:`, {
1255
1283
  object: data.object,
1256
1284
  oldSize: data.oldSize,
@@ -1258,14 +1286,13 @@ export class CoreMoodBoard {
1258
1286
  oldPosition: data.oldPosition,
1259
1287
  newPosition: data.newPosition
1260
1288
  });
1261
-
1262
1289
  // Гарантируем согласованность позиции: если UI не передал, вычислим
1263
1290
  let oldPos = data.oldPosition;
1264
1291
  let newPos = data.newPosition;
1265
1292
  if ((!oldPos || !newPos) && this._activeResize && this._activeResize.objectId === data.object) {
1266
- const h = (this._activeResize.handle || '').toLowerCase();
1293
+ const h = (this._activeResize?.handle || '').toLowerCase();
1267
1294
  const start = this._activeResize.startPosition;
1268
- const startSize = this._activeResize.startSize;
1295
+ const startSize = this.optimization?.startSize || this._activeResize.startSize;
1269
1296
  const dw = (data.newSize?.width || startSize.width) - startSize.width;
1270
1297
  const dh = (data.newSize?.height || startSize.height) - startSize.height;
1271
1298
  const calcNew = { x: start.x + (h.includes('w') ? dw : 0), y: start.y + (h.includes('n') ? dh : 0) };
@@ -1273,6 +1300,7 @@ export class CoreMoodBoard {
1273
1300
  if (!newPos) newPos = calcNew;
1274
1301
  }
1275
1302
  const command = new ResizeObjectCommand(
1303
+ this,
1276
1304
  this,
1277
1305
  data.object,
1278
1306
  data.oldSize,
@@ -9,18 +9,10 @@ export class DataManager {
9
9
  /**
10
10
  * Загружает данные в MoodBoard
11
11
  */
12
- async loadData(data) {
12
+ loadData(data) {
13
13
  if (!data) return;
14
14
 
15
- // Восстанавливаем отсутствующие URL изображений по imageId перед созданием объектов
16
- try {
17
- const apiClient = this.coreMoodboard?.apiClient;
18
- if (apiClient && typeof apiClient.restoreObjectUrls === 'function') {
19
- data = await apiClient.restoreObjectUrls(data);
20
- }
21
- } catch (e) {
22
- console.warn('⚠️ Не удалось восстановить URL изображений по imageId:', e?.message || e);
23
- }
15
+
24
16
 
25
17
  // Очищаем доску перед загрузкой
26
18
  this.clearBoard();
@@ -31,19 +23,6 @@ export class DataManager {
31
23
 
32
24
  data.objects.forEach((objectData, index) => {
33
25
  try {
34
- // Диагностика размеров изображений на загрузке до создания
35
- if (objectData && objectData.type === 'image') {
36
- try {
37
- console.log('🧪 LOAD image size (before create)', {
38
- id: objectData.id,
39
- width: objectData.width,
40
- height: objectData.height,
41
- propWidth: objectData.properties?.width,
42
- propHeight: objectData.properties?.height
43
- });
44
- } catch (_) {}
45
- }
46
-
47
26
  // Используем полные данные объекта, включая ID
48
27
  const createdObject = this.coreMoodboard.createObjectFromData(objectData);
49
28
 
@@ -344,7 +344,7 @@ export class MoodBoard {
344
344
 
345
345
  if (!boardId || !this.options.apiUrl) {
346
346
  console.log('📦 MoodBoard: нет boardId или apiUrl, загружаем пустую доску');
347
- await this.dataManager.loadData(this.data || { objects: [] });
347
+ this.dataManager.loadData(this.data || { objects: [] });
348
348
 
349
349
  // Вызываем коллбек onLoad
350
350
  if (typeof this.options.onLoad === 'function') {
@@ -374,25 +374,10 @@ export class MoodBoard {
374
374
  }
375
375
 
376
376
  const boardData = await response.json();
377
-
378
- // Диагностика: лог размеров изображений прямо из ответа API до любой обработки
379
- try {
380
- const objects = (boardData && boardData.data && Array.isArray(boardData.data.objects)) ? boardData.data.objects : [];
381
- const images = objects.filter(o => o && o.type === 'image');
382
- images.forEach((o) => {
383
- console.log('🧪 API LOAD payload image size', {
384
- id: o.id,
385
- width: o.width,
386
- height: o.height,
387
- propWidth: o.properties?.width,
388
- propHeight: o.properties?.height
389
- });
390
- });
391
- } catch (_) {}
392
377
 
393
378
  if (boardData && boardData.data) {
394
379
  console.log('✅ MoodBoard: данные загружены с сервера', boardData.data);
395
- await this.dataManager.loadData(boardData.data);
380
+ this.dataManager.loadData(boardData.data);
396
381
 
397
382
  // Вызываем коллбек onLoad
398
383
  if (typeof this.options.onLoad === 'function') {
@@ -400,7 +385,7 @@ export class MoodBoard {
400
385
  }
401
386
  } else {
402
387
  console.log('📦 MoodBoard: нет данных с сервера, загружаем пустую доску');
403
- await this.dataManager.loadData(this.data || { objects: [] });
388
+ this.dataManager.loadData(this.data || { objects: [] });
404
389
 
405
390
  // Вызываем коллбек onLoad
406
391
  if (typeof this.options.onLoad === 'function') {