polarvo-layout 1.0.19 → 1.0.21

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.
Files changed (28) hide show
  1. package/package.json +1 -1
  2. package/src/components/FastMenu/DesignFastmenu.vue +2 -2
  3. package/src/components/FastMenu/DisplayFastMenu.vue +2 -2
  4. package/src/components/FastMenu/LayoutFastMenu.vue +18 -10
  5. package/src/components/Layout/CanvasContainer.vue +30 -6
  6. package/src/components/SideBar/ElementSideBar.vue +2 -2
  7. package/src/components/SideBar/LayoutSettingSideBar.vue +18 -10
  8. package/src/components/SideBar/LayoutSideBar.vue +19 -8
  9. package/src/configs/index.js +1 -0
  10. package/src/core/engines/DisplayEngine.js +72 -64
  11. package/src/core/engines/FreeDropEngine.js +40 -42
  12. package/src/core/engines/GridDropEngine.js +33 -43
  13. package/src/core/engines/HistoryEngine.js +55 -51
  14. package/src/core/engines/LayoutEngine.js +89 -68
  15. package/src/core/engines/originals/FreeDropEngine_0402.js +786 -0
  16. package/src/core/engines/originals/GridDropEngine_0402.js +557 -0
  17. package/src/core/managers/EngineManager.js +93 -131
  18. package/src/core/managers/originals/EngineManager_0402.js +826 -0
  19. package/src/library/DisplayLibrary.js +14 -17
  20. package/src/library/FreeDropLibrary.js +6 -4
  21. package/src/library/GridDropLibrary.js +3 -5
  22. package/src/library/LayoutLibrary.js +45 -48
  23. package/src/library/originals/DisplayLibrary_0402.js +137 -0
  24. package/src/library/originals/FreeDropLibrary_0402.js +185 -0
  25. package/src/library/originals/GridDropLibrary_0402.js +202 -0
  26. package/src/library/originals/HistoryLibrary_0402.js +98 -0
  27. package/src/library/originals/LayoutLibrary_0402.js +264 -0
  28. package/src/utils/index.js +8 -3
@@ -0,0 +1,557 @@
1
+ import { cloneDeep, debounce } from 'lodash-es';
2
+
3
+ // event 발행 시 'gridDrop'으로 시작하는 이름 사용
4
+ class GridDropEngine {
5
+ constructor({ eventBus, options }) {
6
+ this.eventBus = eventBus;
7
+ this.resource = options.resource;
8
+
9
+ this._elements = []; // 원본 요소 목록 (EngineManager과 동기화)
10
+ this._elementsUpdate = false; // 원본 요소 목록 변경 플래그
11
+ this._gridElements = []; // 로컬에서 사용하는 요소 목록
12
+
13
+ this._activeData = null;
14
+ this._activeSection = null;
15
+ this._activeElement = null;
16
+
17
+ this._targetCell = null; // 드래그 대상 셀
18
+
19
+ this._dragState = {
20
+ isDragging: false, // 드래그 상태
21
+ isResizing: false, // 리사이징 상태
22
+ activeCell: null, // 현재 선택한 셀 정보
23
+ };
24
+
25
+ this._startPos = { x: 0, y: 0 }; // 드래그 시작 위치
26
+ this._startSize = { w: 0, h: 0 }; // 드래그 시작 크기
27
+ this._alreadyExist = false; // 배치요소 기존재 여부
28
+
29
+ this._subscriptions = []; // 구독 해제 함수 저장용
30
+ this._setupSubscriptions();
31
+ }
32
+
33
+ /** ---------------------------------- 구독 관련 메소드 ---------------------------------- **/
34
+ /** [내부함수] 구독 및 unsubscribe 함수 저장
35
+ * @param {string} name - 이벤트 이름
36
+ * @param {function} handler - 이벤트 핸들러 함수
37
+ * @return {function} - unsubscribe 함수
38
+ */
39
+ _subscribe(name, handler) {
40
+ const unsubscribe = this.eventBus.subscribe(name, handler);
41
+ this._subscriptions.push(unsubscribe);
42
+ return unsubscribe;
43
+ }
44
+
45
+ /** [내부함수] 구독 설정 */
46
+ _setupSubscriptions() {
47
+ // 초기화 완료 이벤트 구독 - from EngineManager
48
+ this._subscribe('system:engineInitialized', () => {
49
+ this._initialize();
50
+ });
51
+
52
+ this._subscribe('system:enginesReset', () => {
53
+ this._initialize();
54
+ });
55
+
56
+ // layoutData 변경 감지
57
+ this._subscribe('system:updateLayoutData', ({ layoutData, activeData }) => {
58
+ this._activeData = activeData;
59
+ });
60
+
61
+ // activeSection 변경 감지 (activeData가 함께 초기화됨)
62
+ this._subscribe('system:updateActiveSection', ({ activeSection, activeData }) => {
63
+ this._activeSection = activeSection;
64
+ this._activeData = activeData;
65
+ // this.setActiveElement(null);
66
+ });
67
+
68
+ this._subscribe('system:setElements', ({ elements }) => {
69
+ this._elements = elements;
70
+ this._elementsUpdate = true;
71
+
72
+ // 활성화 요소 초기화
73
+ this.setActiveElement(null);
74
+
75
+ this.eventBus.emit('gridDrop:setElements', {
76
+ elements: cloneDeep(this.gridElements),
77
+ timestamp: Date.now(),
78
+ });
79
+ });
80
+
81
+ this._subscribe('system:updateActiveSectionConfig', ({ activeData }) => {
82
+ if (activeData?.mode == 'grid') {
83
+ this._activeData = activeData;
84
+ }
85
+ });
86
+
87
+ this._subscribe('system:requestUpdateData', ({ action, elements, elementId, activeData }) => {
88
+ this._elements = elements;
89
+ this._elementsUpdate = true;
90
+ this.setActiveElement(this._elements.find((x) => x.id === elementId) || null, action);
91
+
92
+ if (activeData?.mode == 'grid') {
93
+ this._activeData = activeData;
94
+ }
95
+
96
+ this.eventBus.emit('gridDrop:requestUpdateData', {
97
+ elements: cloneDeep(this.gridElements),
98
+ timestamp: Date.now(),
99
+ });
100
+ });
101
+
102
+ this._subscribe('system:requestUpdateActiveElement', ({ elementId, element }) => {
103
+ const index = this._elements.findIndex((x) => x.id === elementId);
104
+
105
+ if (index !== -1) {
106
+ this._elements[index] = element; // 업데이트
107
+ this._elementsUpdate = true;
108
+ }
109
+
110
+ this.setActiveElement(element, 'update');
111
+
112
+ this.eventBus.emit('gridDrop:requestUpdateActiveElement', {
113
+ element: element,
114
+ timestamp: Date.now(),
115
+ });
116
+ });
117
+
118
+ this._subscribe('system:requestUpdateElements', ({ elements }) => {
119
+ this._elements = elements;
120
+ this._elementsUpdate = true;
121
+ });
122
+
123
+ this._subscribe('system:restoredState', ({ stateData }) => {
124
+ this._elements = stateData.elements;
125
+ this._elementsUpdate = true;
126
+ this.setActiveElement(null);
127
+
128
+ this.eventBus.emit('gridDrop:restoredState', {
129
+ elements: cloneDeep(this.gridElements),
130
+ timestamp: Date.now(),
131
+ });
132
+ });
133
+ }
134
+
135
+ /** 삭제 및 정리 */
136
+ destroy() {
137
+ this._subscriptions.forEach((unsubscribe) => {
138
+ if (typeof unsubscribe === 'function') {
139
+ unsubscribe();
140
+ }
141
+ });
142
+
143
+ this._initialize();
144
+
145
+ this._subscriptions = [];
146
+ this.eventBus = null;
147
+ this.resource = null;
148
+
149
+ console.log('[GridDropEngine] 삭제 완료');
150
+ }
151
+
152
+ /** 리소스 가져오기
153
+ * @return {Array} - 레이아웃 리소스의 요소 배열
154
+ */
155
+ get elementResource() {
156
+ if (!this.resource) {
157
+ console.error('[GridDropEngine] 리소스가 설정되지 않았습니다.');
158
+ return [];
159
+ }
160
+ return this.resource.elements || [];
161
+ }
162
+
163
+ /** 전체 요소 중 그리드 배치 요소만 반환
164
+ * @return {Array} - 그리드 배치 요소 배열
165
+ */
166
+ get gridElements() {
167
+ if (this._elementsUpdate) {
168
+ this._elementsUpdate = false;
169
+
170
+ const base = this._elements.filter((x) => x.mode === 'grid');
171
+
172
+ if (this._activeElement) {
173
+ const exists = base.find((x) => x.id === this._activeElement?.id);
174
+
175
+ // 기존 배치요소인 경우
176
+ if (exists) {
177
+ // 기존 배치요소와 activeElement 교체
178
+ this._gridElements = base.map((element) => (element.id === this._activeElement.id ? this._activeElement : element));
179
+ } else {
180
+ this._gridElements = [...base, this._activeElement];
181
+ }
182
+ } else {
183
+ this._gridElements = base;
184
+ }
185
+ }
186
+
187
+ return this._gridElements;
188
+ }
189
+
190
+ /** ---------------------------------- 공통 메소드 ---------------------------------- **/
191
+ /** [내부함수] 내부 변수 초기화 */
192
+ _initialize() {
193
+ this._dragState = {
194
+ isDragging: false,
195
+ isResizing: false,
196
+ };
197
+
198
+ this._startPos = { x: 0, y: 0 };
199
+ this._startSize = { w: 0, h: 0 };
200
+
201
+ this._activeData = null;
202
+ this._activeSection = null;
203
+ this._activeElement = null;
204
+
205
+ this._elementsUpdate = false;
206
+ }
207
+
208
+ /** 활성화 배치요소 변경
209
+ * @param {object|null} element - 활성화할 배치요소 객체 (null인 경우 비활성화)
210
+ * @param {string|null} action - 변경 액션 (예: 'click', 'reset' 등)
211
+ */
212
+ setActiveElement(element, action = null) {
213
+ // 이미 활성화된 요소를 다시 활성화하려는 경우 무시
214
+ if (element && this._activeElement?.id === element?.id) return;
215
+
216
+ this._activeElement = element;
217
+ this._elementsUpdate = true;
218
+
219
+ this.eventBus.emit('gridDrop:setActiveElement', {
220
+ action: action,
221
+ activeElement: this._activeElement,
222
+ // elements: cloneDeep(this.gridElements),
223
+ timestamp: Date.now(),
224
+ });
225
+ }
226
+
227
+ /** 그리드 요소 스타일 반환
228
+ * @param {string} type - 요소 타입 ('empty' 또는 'element')
229
+ * @param {object} cell - 셀 정보
230
+ * @returns {object} - 스타일 객체
231
+ */
232
+ getElementStyle(type, cell) {
233
+ if (type == 'empty') {
234
+ return {
235
+ gridArea: `${cell.row} / ${cell.col} / span 1 / span 1`,
236
+ };
237
+ } else if (type == 'element') {
238
+ const { row, col } = cell.position;
239
+ const { rowSpan, colSpan } = cell.size;
240
+
241
+ return {
242
+ gridArea: `${row} / ${col} / span ${rowSpan} / span ${colSpan}`,
243
+ };
244
+ }
245
+ }
246
+
247
+ /** isLocked 값 변경 */
248
+ toggleLock() {
249
+ if (!this._activeElement) {
250
+ console.error('[GridDropEngine] 활성화된 요소가 없습니다.');
251
+ return;
252
+ }
253
+ this._activeElement.isLocked = !this._activeElement.isLocked;
254
+ // this._elementsUpdate = true;
255
+
256
+ this.eventBus.emit('gridDrop:toggleLock', {
257
+ elementId: this._activeElement.id,
258
+ timestamp: Date.now(),
259
+ });
260
+ }
261
+
262
+ /** [내부함수] 드래그 계산
263
+ * @param {object} currentPos - 현재 마우스 위치 ({ x: , y: })
264
+ * @param {object} cellSize - 그리드 셀 크기 ({ w: , h: })
265
+ * @returns {object} - 드래그된 셀 수 ({ dx: , dy: })
266
+ */
267
+ _calculateDrag(currentPos, cellSize) {
268
+ const { gridGap } = this._activeData.config;
269
+
270
+ const dx = Math.round((currentPos.x - this._startPos.x) / (cellSize.w + gridGap));
271
+ const dy = Math.round((currentPos.y - this._startPos.y) / (cellSize.h + gridGap));
272
+
273
+ return { dx, dy };
274
+ }
275
+
276
+ /** [내부함수] 그리드 셀 너비 계산
277
+ * @param {HTMLElement} element - 활성 섹션 엘리먼트
278
+ * @returns {number} - 그리드 셀 너비
279
+ */
280
+ _getGridCellWidth(element) {
281
+ const { gridGap, gridColumns } = this._activeData.config;
282
+ const totalWidth = element.clientWidth - gridGap * (gridColumns - 1);
283
+ return totalWidth / gridColumns;
284
+ }
285
+
286
+ /** [내부함수] 그리드 셀 높이 계산
287
+ * @param {HTMLElement} element - 활성 섹션 엘리먼트
288
+ * @returns {number} - 그리드 셀 높이
289
+ */
290
+ _getGridCellHeight(element) {
291
+ const { gridGap, gridRows } = this._activeData.config;
292
+ const totalHeight = element.clientHeight - gridGap * (gridRows - 1);
293
+ return totalHeight / gridRows;
294
+ }
295
+
296
+ /** [내부함수] 기존 아이템과 충돌 여부 감지
297
+ * @param {object} position - 검사할 위치 정보 ({ column: , row: })
298
+ * @param {object} size - 검사할 크기 정보 ({ colSpan: , rowSpan: })
299
+ * @returns {boolean} - 충돌 여부
300
+ */
301
+ _detectElementCollision(position, size) {
302
+ if (!this._activeElement) {
303
+ console.error('[GridDropEngine] 충돌 검사를 위한 요소 정보를 찾을 수 없습니다:');
304
+ return false;
305
+ }
306
+
307
+ return this.gridElements
308
+ .filter((item) => item.section === this._activeElement.section)
309
+ .some((item) => {
310
+ if (item.id === this._activeElement.id) return false; // 자기 자신은 무시
311
+
312
+ const itemPosition = item.position;
313
+ const itemSize = item.size;
314
+
315
+ return !(
316
+ position.row >= itemPosition.row + itemSize.rowSpan ||
317
+ position.row + size.rowSpan <= itemPosition.row ||
318
+ position.col >= itemPosition.col + itemSize.colSpan ||
319
+ position.col + size.colSpan <= itemPosition.col
320
+ );
321
+ });
322
+ }
323
+
324
+ /** ---------------------------------- 배치요소 관련 메소드 ---------------------------------- **/
325
+ /** 배치요소 신규 추가 - 메뉴에서 추가 아이템 선택
326
+ * @param {string} elName - 추가할 아이템 이름
327
+ */
328
+ addElement(elName) {
329
+ if (!this._activeData || !this._dragState.activeCell) {
330
+ console.error('[GridDropEngine] 활성화된 데이터가 없습니다.');
331
+ return;
332
+ }
333
+ if (this._activeData.mode !== 'grid') return;
334
+
335
+ const element = {
336
+ id: elName + '_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 7),
337
+ type: elName,
338
+ mode: 'grid',
339
+ position: { ...this._dragState.activeCell },
340
+ size: { colSpan: 1, rowSpan: 1 },
341
+ section: this._activeSection,
342
+ isLocked: false,
343
+ };
344
+
345
+ this.setActiveElement(null, 'add');
346
+
347
+ this.eventBus.emit('gridDrop:requestAddElement', {
348
+ element: element,
349
+ timestamp: Date.now(),
350
+ });
351
+ }
352
+
353
+ /** [내부함수] 배치요소 삭제
354
+ * @param {string} id - 삭제할 배치요소 ID
355
+ */
356
+ _deleteElement(id) {
357
+ const element = this.gridElements.find((el) => el.id === id);
358
+ if (!element) {
359
+ console.error('[GridDropEngine] 삭제할 아이템을 찾을 수 없습니다:', id);
360
+ return;
361
+ }
362
+
363
+ if (confirm(`선택하신 ${element.type} 아이템을 삭제하시겠습니까?`)) {
364
+ this.eventBus.emit('gridDrop:requestDeleteElement', {
365
+ elementId: id,
366
+ timestamp: Date.now(),
367
+ });
368
+ }
369
+ }
370
+
371
+ /** 호버 중인 셀 감지
372
+ * @param {object} cell - 호버 중인 셀 정보
373
+ */
374
+ detectHoverCell(cell) {
375
+ if (!this._activeElement) return;
376
+ // if (!this._dragState.isDragging) return;
377
+ this._targetCell = cell;
378
+ }
379
+
380
+ /** ---------------------------------- 드래그 관련 메소드 ---------------------------------- **/
381
+ /** mousedown 실행 */
382
+ handleMouseDown(event, item = null) {
383
+ // 리사이징 중이면 드래그 불가
384
+ if (this._dragState.isResizing) return;
385
+
386
+ // 기존 아이템인 경우
387
+ if (item.id) {
388
+ // 우클릭 시 해당 아이템 삭제
389
+ if (event.button === 2) {
390
+ this._deleteElement(item.id);
391
+ return false;
392
+ }
393
+ const element = this.gridElements.find((el) => el.id === item.id);
394
+ this.setActiveElement(element, 'click');
395
+ this._dragState.activeCell = { ...element.position };
396
+ this._alreadyExist = true;
397
+
398
+ // 배치요소 잠겨 있으면 드래그/삭제 불가
399
+ if (element.isLocked) return;
400
+ } else {
401
+ this._dragState.activeCell = { ...item };
402
+ this._alreadyExist = false;
403
+ }
404
+
405
+ document.addEventListener('mousemove', this._detectDragIntent);
406
+ document.addEventListener('mouseup', this._cancelDragIntent);
407
+ }
408
+
409
+ _detectDragIntent = () => {
410
+ if (this._targetCell && this._activeElement) {
411
+ const { col, row } = this._targetCell;
412
+ if (this._activeElement.position.col !== col || this._activeElement.position.row !== row) {
413
+ // 의도 감지 리스너 제거
414
+ document.removeEventListener('mousemove', this._detectDragIntent);
415
+ document.removeEventListener('mouseup', this._cancelDragIntent);
416
+
417
+ this._dragState.isDragging = true;
418
+
419
+ this.eventBus.emit('gridDrop:startDrag', { timestamp: Date.now() });
420
+ document.addEventListener('mousemove', this._startDragMove);
421
+ document.addEventListener('mouseup', this._stopDrag);
422
+ }
423
+ }
424
+ };
425
+ _cancelDragIntent = () => {
426
+ if (!this._alreadyExist) {
427
+ this.eventBus.emit('gridDrop:requestOpenElementBar', {
428
+ timestamp: Date.now(),
429
+ });
430
+
431
+ this._activeElement = null;
432
+ }
433
+
434
+ document.removeEventListener('mousemove', this._detectDragIntent);
435
+ document.removeEventListener('mouseup', this._cancelDragIntent);
436
+ };
437
+
438
+ /** [내부함수] 드래그 진행 */
439
+ _startDragMove = () => {
440
+ if (this._dragState.isResizing || !this._targetCell) return;
441
+ // 단순 클릭 시에는 위치 변경 없음
442
+ const { col, row } = this._targetCell;
443
+ if (this._dragState.isDragging) {
444
+ this._activeElement.position.col = col;
445
+ this._activeElement.position.row = row;
446
+
447
+ this.eventBus.emit('gridDrop:requestUpdateLocalBounds', {
448
+ elementId: this._activeElement.id,
449
+ position: this._activeElement.position,
450
+ size: this._activeElement.size,
451
+ timestamp: Date.now(),
452
+ });
453
+ }
454
+ };
455
+
456
+ /** [내부함수] 드래그 종료 */
457
+ _stopDrag = () => {
458
+ if (this._dragState.isDragging) {
459
+ this.eventBus.emit('gridDrop:requestUpdateElement', {
460
+ historyEvent: true,
461
+ elementId: this._activeElement.id,
462
+ position: this._activeElement.position,
463
+ size: this._activeElement.size,
464
+ timestamp: Date.now(),
465
+ });
466
+ }
467
+
468
+ this._dragState.isDragging = false;
469
+ this._targetCell = null;
470
+
471
+ document.removeEventListener('mousemove', this._startDragMove);
472
+ document.removeEventListener('mouseup', this._stopDrag);
473
+ };
474
+
475
+ /** ---------------------------------- 리사이징 관련 메소드 ---------------------------------- **/
476
+ /** 리사이징 시작
477
+ * @param {MouseEvent} event - 마우스 이벤트
478
+ * @param {string} id - 배치요소 ID
479
+ */
480
+ startResize(event, id) {
481
+ if (!this._activeElement) {
482
+ console.error('[GridDropEngine] 활성화된 배치요소를 찾을 수 없습니다.');
483
+ return false;
484
+ }
485
+ if (this._activeElement.id !== id) return false;
486
+
487
+ // 배치요소 잠겨 있으면 리사이즈 불가
488
+ if (this._activeElement.isLocked) return;
489
+
490
+ this._startPos = { x: event.clientX, y: event.clientY };
491
+ this._startSize = { w: this._activeElement.size.colSpan, h: this._activeElement.size.rowSpan };
492
+ this._dragState.isResizing = true;
493
+
494
+ document.addEventListener('mousemove', this._startResizeMove);
495
+ document.addEventListener('mouseup', this._stopResize);
496
+ }
497
+
498
+ /** [내부함수] 리사이징 진행
499
+ * @param {MouseEvent} event - 마우스 이벤트
500
+ */
501
+ _startResizeMove = (event) => {
502
+ // 리사이징 중인 경우에만 드래그 가능
503
+ if (!this._dragState.isResizing) return;
504
+
505
+ const { gridColumns, gridRows } = this._activeData.config;
506
+
507
+ const activeSection = document.querySelector('.section-active');
508
+ const { dx, dy } = this._calculateDrag(
509
+ { x: event.clientX, y: event.clientY },
510
+ {
511
+ w: this._getGridCellWidth(activeSection),
512
+ h: this._getGridCellHeight(activeSection),
513
+ },
514
+ );
515
+
516
+ if (dx !== 0 || dy !== 0) {
517
+ const { col, row } = this._activeElement.position;
518
+ const newColSpan = Math.max(1, Math.min(gridColumns - col + 1, this._startSize.w + dx));
519
+ const newRowSpan = Math.max(1, Math.min(gridRows - row + 1, this._startSize.h + dy));
520
+
521
+ if (!this._detectElementCollision(this._activeElement.position, { colSpan: newColSpan, rowSpan: newRowSpan })) {
522
+ this._activeElement.size.colSpan = newColSpan;
523
+ this._activeElement.size.rowSpan = newRowSpan;
524
+
525
+ this.eventBus.emit('gridDrop:requestUpdateLocalBounds', {
526
+ elementId: this._activeElement.id,
527
+ position: this._activeElement.position,
528
+ size: this._activeElement.size,
529
+ timestamp: Date.now(),
530
+ });
531
+ }
532
+ }
533
+ };
534
+
535
+ /** [내부함수] 리사이징 종료 */
536
+ _stopResize = () => {
537
+ if (this._dragState.isResizing) {
538
+ this.eventBus.emit('gridDrop:requestUpdateElement', {
539
+ historyEvent: true,
540
+ elementId: this._activeElement.id,
541
+ position: this._activeElement.position,
542
+ size: this._activeElement.size,
543
+ timestamp: Date.now(),
544
+ });
545
+ }
546
+
547
+ this._dragState.isResizing = false;
548
+ this._targetCell = null;
549
+ this._startPos = { x: 0, y: 0 };
550
+ this._startSize = { w: 0, h: 0 };
551
+
552
+ document.removeEventListener('mousemove', this._startResizeMove);
553
+ document.removeEventListener('mouseup', this._stopResize);
554
+ };
555
+ }
556
+
557
+ export default GridDropEngine;