@sequent-org/moodboard 1.2.101 → 1.2.103

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.101",
3
+ "version": "1.2.103",
4
4
  "type": "module",
5
5
  "description": "Interactive moodboard",
6
6
  "main": "./src/index.js",
@@ -477,6 +477,14 @@ export class MoodBoard {
477
477
 
478
478
  this._safeDestroy(this.alignmentGuides, 'alignmentGuides');
479
479
  this.alignmentGuides = null;
480
+
481
+ // HTML-слои (текст и ручки) также нужно корректно уничтожать,
482
+ // чтобы удалить DOM и отписаться от глобальных слушателей resize/DPR
483
+ this._safeDestroy(this.htmlTextLayer, 'htmlTextLayer');
484
+ this.htmlTextLayer = null;
485
+
486
+ this._safeDestroy(this.htmlHandlesLayer, 'htmlHandlesLayer');
487
+ this.htmlHandlesLayer = null;
480
488
 
481
489
  this._safeDestroy(this.commentPopover, 'commentPopover');
482
490
  this.commentPopover = null;
@@ -507,6 +515,16 @@ export class MoodBoard {
507
515
  this.container.classList.remove('moodboard-root');
508
516
  }
509
517
  this.container = null;
518
+
519
+ // Сбрасываем глобальные ссылки на слои, если они устанавливались
520
+ if (typeof window !== 'undefined') {
521
+ if (window.moodboardHtmlTextLayer === this.htmlTextLayer) {
522
+ window.moodboardHtmlTextLayer = null;
523
+ }
524
+ if (window.moodboardHtmlHandlesLayer === this.htmlHandlesLayer) {
525
+ window.moodboardHtmlHandlesLayer = null;
526
+ }
527
+ }
510
528
 
511
529
  // Вызываем коллбек onDestroy
512
530
  if (typeof this.options.onDestroy === 'function') {
@@ -22,7 +22,12 @@ export class HtmlHandlesLayer {
22
22
  this.target = { type: 'none', id: null, bounds: null };
23
23
  this.handles = {};
24
24
  this._drag = null;
25
- this._handlesSuppressed = false; // скрывать ручки во время перетаскивания/трансформаций
25
+ this._handlesSuppressed = false; // скрывать ручки во время перетаскивания/трансформаций
26
+
27
+ // Ссылки на обработчики, чтобы корректно отписаться при destroy()
28
+ this._onWindowResize = null;
29
+ this._onDprChange = null;
30
+ this._dprMediaQuery = null;
26
31
  }
27
32
 
28
33
  attach() {
@@ -31,16 +36,19 @@ export class HtmlHandlesLayer {
31
36
  this.container.appendChild(this.layer);
32
37
 
33
38
  // Обновление при изменении размеров окна/масштаба (DPR)
34
- window.addEventListener('resize', () => this.update(), { passive: true });
39
+ this._onWindowResize = () => this.update();
40
+ window.addEventListener('resize', this._onWindowResize, { passive: true });
35
41
  // Некоторые браузеры меняют devicePixelRatio без resize — страхуемся
36
42
  if (typeof window !== 'undefined' && 'matchMedia' in window) {
37
43
  try {
38
44
  // media-query, реагирующая на изменение DPR
39
- const mq = window.matchMedia(`(resolution: ${window.devicePixelRatio || 1}dppx)`);
40
- if (mq && mq.addEventListener) {
41
- mq.addEventListener('change', () => this.update());
42
- } else if (mq && mq.addListener) {
43
- mq.addListener(() => this.update());
45
+ const mq = window.matchMedia(`(resolution: ${window.devicePixelRatio || 1}dppx)`);
46
+ this._dprMediaQuery = mq;
47
+ this._onDprChange = () => this.update();
48
+ if (mq && mq.addEventListener) {
49
+ mq.addEventListener('change', this._onDprChange);
50
+ } else if (mq && mq.addListener) {
51
+ mq.addListener(this._onDprChange);
44
52
  }
45
53
  } catch (_) {}
46
54
  }
@@ -102,12 +110,34 @@ export class HtmlHandlesLayer {
102
110
  }
103
111
 
104
112
  destroy() {
105
- if (this.layer) this.layer.remove();
106
- this.layer = null;
113
+ // Отписываемся от глобальных событий окна/DPR,
114
+ // чтобы старые инстансы не продолжали реагировать после destroy()
115
+ if (this._onWindowResize) {
116
+ window.removeEventListener('resize', this._onWindowResize);
117
+ this._onWindowResize = null;
118
+ }
119
+ if (this._dprMediaQuery && this._onDprChange) {
120
+ try {
121
+ if (this._dprMediaQuery.removeEventListener) {
122
+ this._dprMediaQuery.removeEventListener('change', this._onDprChange);
123
+ } else if (this._dprMediaQuery.removeListener) {
124
+ this._dprMediaQuery.removeListener(this._onDprChange);
125
+ }
126
+ } catch (_) {}
127
+ this._dprMediaQuery = null;
128
+ this._onDprChange = null;
129
+ }
130
+
131
+ if (this.layer) {
132
+ this.layer.remove();
133
+ }
134
+ this.layer = null;
107
135
  }
108
136
 
109
137
  update() {
110
- if (!this.core) return;
138
+ // Дополнительная защита: если слой или core уже уничтожены,
139
+ // выходим, чтобы не получить ошибок при resize/смене DPR
140
+ if (!this.core || !this.core.pixi || !this.core.pixi.app || !this.layer) return;
111
141
  const selectTool = this.core?.selectTool;
112
142
  const ids = selectTool ? Array.from(selectTool.selectedObjects || []) : [];
113
143
  if (!ids || ids.length === 0) { this.hide(); return; }
@@ -225,13 +255,16 @@ export class HtmlHandlesLayer {
225
255
  }
226
256
 
227
257
  // Вычисляем позицию и размер через математику сцены (toGlobal) и переводим в CSS px.
228
- // toGlobal() возвращает координаты в device-пикселях, поэтому делим на rendererRes.
258
+ // Важно: toGlobal() уже возвращает координаты в логических экранных пикселях
259
+ // (учитывая позицию/масштаб world), поэтому ДЕЛИТЬ их на renderer.resolution не нужно.
260
+ // Деление приводило к эффекту 1 / resolution (например, при res = 0.8 рамка
261
+ // становилась больше и съезжала относительно объекта).
229
262
  const tl = world.toGlobal(new PIXI.Point(worldBounds.x, worldBounds.y));
230
263
  const br = world.toGlobal(new PIXI.Point(worldBounds.x + worldBounds.width, worldBounds.y + worldBounds.height));
231
- const cssX = offsetLeft + tl.x / rendererRes;
232
- const cssY = offsetTop + tl.y / rendererRes;
233
- const cssWidth = Math.max(1, (br.x - tl.x) / rendererRes);
234
- const cssHeight = Math.max(1, (br.y - tl.y) / rendererRes);
264
+ const cssX = offsetLeft + tl.x;
265
+ const cssY = offsetTop + tl.y;
266
+ const cssWidth = Math.max(1, (br.x - tl.x));
267
+ const cssHeight = Math.max(1, (br.y - tl.y));
235
268
 
236
269
  const left = Math.round(cssX);
237
270
  const top = Math.round(cssY);
@@ -325,12 +325,15 @@ export class HtmlTextLayer {
325
325
  const tl = worldLayer.toGlobal(new PIXI.Point(x, y));
326
326
  const br = worldLayer.toGlobal(new PIXI.Point(x + w, y + h));
327
327
 
328
- // toGlobal() возвращает координаты в device-пикселях с учётом resolution.
329
- // Для CSS нам нужны логические пиксели, поэтому делим на res.
330
- const left = offsetLeft + tl.x / res;
331
- const top = offsetTop + tl.y / res;
332
- const width = Math.max(1, (br.x - tl.x) / res);
333
- const height = Math.max(1, (br.y - tl.y) / res);
328
+ // ВАЖНО: toGlobal() уже возвращает координаты в логических экранных пикселях
329
+ // (как и для HtmlHandlesLayer), поэтому делить их на renderer.resolution
330
+ // при вычислении CSS-позиции и размеров не нужно. Деление приводило к тому,
331
+ // что при res < 1 (масштаб браузера ≠ 100%) HTML-текст уезжал относительно
332
+ // собственных рамок и PIXI-объекта.
333
+ const left = offsetLeft + tl.x;
334
+ const top = offsetTop + tl.y;
335
+ const width = Math.max(1, (br.x - tl.x));
336
+ const height = Math.max(1, (br.y - tl.y));
334
337
 
335
338
  // Применяем к элементу
336
339
  el.style.left = `${left}px`;