polarvo-layout 1.0.85 → 1.0.87

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": "polarvo-layout",
3
- "version": "1.0.85",
3
+ "version": "1.0.87",
4
4
  "type": "module",
5
5
  "author": "unigence <unigencelab@gmail.com>",
6
6
  "repository": {
@@ -131,6 +131,12 @@ function onHover(e) {
131
131
  return;
132
132
  }
133
133
 
134
+ // 선택 대상에서 제외하도록 마킹된 요소는 무시 (예: 폼 하단 고정 제출 버튼)
135
+ if (e.target.closest('[data-no-select]')) {
136
+ setHovered(null);
137
+ return;
138
+ }
139
+
134
140
  const leafEl = e.target.closest('[data-leaf]');
135
141
  const ownerEl = e.target.closest('[data-style-owner]');
136
142
  if (!leafEl || !ownerEl) {
@@ -145,11 +151,17 @@ function onClick(e) {
145
151
  const activeElRoot = isEligible(e);
146
152
  if (!activeElRoot) return;
147
153
 
154
+ // 선택 대상에서 제외하도록 마킹된 요소는 무시 (예: 폼 하단 고정 제출 버튼)
155
+ if (e.target.closest('[data-no-select]')) {
156
+ e.preventDefault();
157
+ e.stopPropagation();
158
+ return;
159
+ }
160
+
148
161
  const leafEl = e.target.closest('[data-leaf]');
149
162
  const ownerEl = e.target.closest('[data-style-owner]');
150
163
  if (!leafEl || !ownerEl) return;
151
164
 
152
-
153
165
  // leaf 내부의 실제 버튼/링크/폼 제출 등 기본 동작 막기
154
166
  e.preventDefault();
155
167
  e.stopPropagation();
@@ -5,12 +5,20 @@
5
5
  <BaseLayout :polarvo="polarvo" class="relative" />
6
6
  </div>
7
7
  </div>
8
- <div
9
- class="py-2 px-2 shrink-0 bg-white border border-gray-300 flex items-center justify-center text-gray-500 rounded-lg hover:bg-gray-100 cursor-pointer"
10
- :style="{ width: `${displayWidth}px`, margin: '8px auto ' }"
11
- @click="expandContainer()"
12
- >
13
- <font-awesome-icon :icon="['fas', 'plus']" class="h-8 text-xs" />
8
+ <div class="flex gap-2 shrink-0" :style="{ width: `${displayWidth}px`, margin: '8px auto' }">
9
+ <div
10
+ class="flex-1 py-2 px-2 bg-white border border-gray-300 flex items-center justify-center text-gray-500 rounded-lg hover:bg-gray-100 cursor-pointer"
11
+ @click="expandContainer()"
12
+ >
13
+ <font-awesome-icon :icon="['fas', 'plus']" class="h-8 text-xs" />
14
+ </div>
15
+ <div
16
+ class="flex-1 py-2 px-2 bg-white border border-gray-300 flex items-center justify-center rounded-lg"
17
+ :class="canShrink ? 'text-gray-500 hover:bg-gray-100 cursor-pointer' : 'text-gray-300 cursor-not-allowed'"
18
+ @click="shrinkContainer()"
19
+ >
20
+ <font-awesome-icon :icon="['fas', 'minus']" class="h-8 text-xs" />
21
+ </div>
14
22
  </div>
15
23
  </div>
16
24
  </template>
@@ -34,29 +42,55 @@ const DEFAULT_DISPLAY_SIZE = {
34
42
  };
35
43
 
36
44
  // height 관련 여유 공간 추가하여 계산함 (50, 200)
37
- const { displaySize } = toRefs(props.polarvo.display.state);
38
- const { elements } = toRefs(props.polarvo.layout.state);
45
+ const { displaySize, canvasSize } = toRefs(props.polarvo.display.state);
46
+ const { elements, activeMode } = toRefs(props.polarvo.layout.state);
47
+
39
48
  const defaultHeight = computed(() => {
40
- return (
49
+ const freeElementsBottom =
41
50
  elements.value?.reduce((max, el) => {
42
- const bottom = (el.position?.y || 0) + (el.size?.h || 0);
51
+ // 50은 버퍼값
52
+ let bottom = (el.position?.y || 0) + (el.size?.h || 0);
53
+ if (bottom > 750) {
54
+ bottom += 50;
55
+ }
43
56
  return Math.max(max, bottom);
44
- }, 750) || 750
45
- );
57
+ }, 750) || 750;
58
+
59
+ // 저장된 canvasSize가 없으면 free 요소 기준 계산값만 사용 (grid 섹션은 기본값 750으로 취급)
60
+ if (!canvasSize.value) return freeElementsBottom;
61
+
62
+ // grid: 라이브로 정확히 계산할 방법이 없어 저장된 값을 그대로 신뢰
63
+ if (activeMode.value === 'grid') {
64
+ return canvasSize.value.defaultHeight ?? freeElementsBottom;
65
+ }
66
+
67
+ // free: 라이브 계산값과 저장된 값 중 작은 쪽을 실제 최소 높이로 사용
68
+ return Math.min(freeElementsBottom, canvasSize.value.defaultHeight ?? freeElementsBottom);
46
69
  });
47
70
 
71
+ // canvasSize 저장 시점의 총 높이(defaultHeight+additionalHeight)는 유지하면서,
72
+ // 위에서 다시 계산한 defaultHeight와의 차이를 additionalHeight로 재배분
73
+ function restoreAdditionalHeight() {
74
+ if (!canvasSize.value) return 0;
75
+
76
+ if (activeMode.value === 'grid') {
77
+ return canvasSize.value.additionalHeight ?? 0;
78
+ }
79
+
80
+ const savedTotal = (canvasSize.value.defaultHeight ?? 0) + (canvasSize.value.additionalHeight ?? 0);
81
+ return Math.max(0, savedTotal - defaultHeight.value);
82
+ }
83
+
84
+ const additionalHeight = ref(restoreAdditionalHeight());
85
+
86
+ // canvasSize(저장된 캔버스 크기)가 바뀌면(=화면 전환) 그 값으로 복원, 없으면 0
48
87
  watch(
49
- () => defaultHeight.value,
50
- (newHeight, oldHeight) => {
51
- if (newHeight <= oldHeight) {
52
- additionalHeight.value = 0;
53
- return;
54
- }
55
- additionalHeight.value = 200;
88
+ () => canvasSize.value,
89
+ () => {
90
+ additionalHeight.value = restoreAdditionalHeight();
56
91
  },
57
92
  );
58
93
 
59
- const additionalHeight = ref(0);
60
94
  const displayHeight = computed(() => defaultHeight.value + additionalHeight.value);
61
95
  const displayWidth = computed(() => {
62
96
  const px = displaySize.value?.px ?? DEFAULT_DISPLAY_SIZE.px;
@@ -64,10 +98,6 @@ const displayWidth = computed(() => {
64
98
  return px * (percent / 100);
65
99
  });
66
100
 
67
- function expandContainer() {
68
- additionalHeight.value += 200;
69
- }
70
-
71
101
  const containerStyle = computed(() => {
72
102
  if (!displaySize.value) {
73
103
  return {
@@ -101,6 +131,18 @@ const containerStyle = computed(() => {
101
131
  backgroundPosition: '10px 10px', // BaseLayout의 p-[10px]와 맞춰 좌상단 기준으로 정렬 (gridSize와 무관한 고정값)
102
132
  };
103
133
  });
134
+
135
+ defineExpose({ defaultHeight, additionalHeight, displayHeight });
136
+
137
+ function expandContainer() {
138
+ additionalHeight.value += 200;
139
+ }
140
+
141
+ // displayHeight(=defaultHeight + additionalHeight)는 항상 defaultHeight 이상으로 유지
142
+ const canShrink = computed(() => additionalHeight.value > 0);
143
+ function shrinkContainer() {
144
+ additionalHeight.value = Math.max(0, additionalHeight.value - 200);
145
+ }
104
146
  </script>
105
147
 
106
148
  <style scoped lang="scss">
@@ -294,7 +294,17 @@ function initData() {
294
294
 
295
295
  const mergedData = ref([]); // 필드 데이터 + 빈 데이터 통합 리스트
296
296
  const filteredElements = computed(() => {
297
- return elements.value.filter((el) => props.elementIds?.includes(el.id) && el.section === props.sectionKey) ?? [];
297
+ return (
298
+ elements.value.filter((el) => {
299
+ if (!props.elementIds?.includes(el.id) || el.section !== props.sectionKey) return false;
300
+
301
+ // gridRows/gridColumns가 줄어든 뒤에도 예전 좌표를 들고 있는 요소는 제외 —
302
+ // 그대로 두면 CSS grid가 암묵적 행/열(grid-auto-rows/columns)을 만들어 그리드 박스 밖으로 밀려난다
303
+ const { row, col } = el.position;
304
+ const { rowSpan, colSpan } = el.size;
305
+ return row >= 1 && col >= 1 && row + rowSpan - 1 <= gridRows.value && col + colSpan - 1 <= gridColumns.value;
306
+ }) ?? []
307
+ );
298
308
  });
299
309
  watch(
300
310
  () => filteredElements.value,
@@ -22,6 +22,7 @@ class DisplayEngine {
22
22
  this.activeDesign = null; // 현재 활성화된 디자인 메뉴
23
23
  this.displayMode = 'desktop'; // 현재 디스플레이 모드 (초기값 = 'desktop')
24
24
  this.displaySize = null;
25
+ this.canvasSize = null;
25
26
 
26
27
  this._resetDisplaySizeByMode(this.displayMode);
27
28
 
@@ -59,6 +60,7 @@ class DisplayEngine {
59
60
  this._subscribe('layout:setLayoutName', ({ $screenConfig }) => {
60
61
  this.displayMode = $screenConfig.displayMode;
61
62
  this.displaySize = { ...$screenConfig.displaySize };
63
+ this.canvasSize = { ...$screenConfig.canvasSize };
62
64
  });
63
65
 
64
66
  this._subscribe('freeDrop:setActiveElement', ({ action }) => {
@@ -245,9 +245,12 @@ class GridDropEngine {
245
245
  * @returns {object} - 스타일 객체
246
246
  */
247
247
  getElementStyle(type, cell) {
248
+ const MIN_CELL_PX = 20; // GridLayout.vue의 MIN_ROW_PX와 동일한 값
249
+
248
250
  if (type == 'empty') {
249
251
  return {
250
252
  gridArea: `${cell.row} / ${cell.col} / span 1 / span 1`,
253
+ minHeight: `${MIN_CELL_PX}px`,
251
254
  };
252
255
  } else if (type == 'element') {
253
256
  const { row, col } = cell.position;
@@ -255,6 +258,7 @@ class GridDropEngine {
255
258
 
256
259
  return {
257
260
  gridArea: `${row} / ${col} / span ${rowSpan} / span ${colSpan}`,
261
+ minHeight: `${cell.size?.h ?? MIN_CELL_PX}px`,
258
262
  };
259
263
  }
260
264
  }
@@ -35,6 +35,7 @@ class EngineManager {
35
35
  this._containerWidth = 1920;
36
36
  this._containerHeight = 1080;
37
37
  this._displaySize = { px: 1920, aspectRatio: '16/9' };
38
+ this._canvasSize = null;
38
39
 
39
40
  this._subscriptions = [];
40
41
  this._initialized = false;
@@ -201,7 +202,6 @@ class EngineManager {
201
202
  };
202
203
  }
203
204
 
204
-
205
205
  /** elements 초기화
206
206
  * @param {Array} elements - 새로운 elements 배열
207
207
  */
@@ -340,6 +340,7 @@ class EngineManager {
340
340
  // layoutName 변경에 따른 activeSection 초기화
341
341
  this._subscribe('layout:setLayoutName', ({ $layoutData, $screenConfig }) => {
342
342
  this._displaySize = { ...$screenConfig.displaySize };
343
+ this._canvasSize = { ...$screenConfig.canvasSize };
343
344
 
344
345
  this.state.layoutData = $layoutData;
345
346
  this.setActiveSection('section1', true);
@@ -487,6 +488,73 @@ class EngineManager {
487
488
  this._updateActiveSectionConfig('mode', updates);
488
489
  }
489
490
 
491
+ _calculateGridRatio(axis, newCount) {
492
+ if (!this.activeData) return [];
493
+
494
+ const config = this.activeData?.config;
495
+ if (!config) return [];
496
+
497
+ const oldCount = axis === 'row' ? config.gridRows || 1 : config.gridColumns || 1;
498
+
499
+ const ratioKey = axis == 'row' ? 'gridRowRatio' : 'gridColumnRatio';
500
+ const oldRatio = config[ratioKey];
501
+ const oldWeight = Array.isArray(oldRatio) && oldRatio.length === oldCount ? oldRatio : Array(oldCount).fill(1);
502
+ if (newCount === oldCount) return [...oldWeight];
503
+
504
+ // 기존 대비 증가 - 새로 추가되는 행/열(oldCount+1 ~ newCount) 각각에 대해 점유 여부를 확인하고 weight 계산
505
+ if (oldCount < newCount) {
506
+ // 현재 섹션 데이터
507
+ const sectionData = this.state.elements.filter((el) => el.section === this.state.activeSection);
508
+
509
+ const sectionEl = document.getElementById(`${this.state.activeSection}Layout`); // 예: 'section2Layout'
510
+ if (!sectionEl) {
511
+ console.warn(`[EngineManager] 요소를 찾을 수 없습니다.`);
512
+ return [...oldWeight, ...Array(newCount - oldCount).fill(1)];
513
+ }
514
+
515
+ const sectionRect = sectionEl.getBoundingClientRect();
516
+ const style = getComputedStyle(sectionEl);
517
+ const paddingY = parseFloat(style.paddingTop) + parseFloat(style.paddingBottom);
518
+ const paddingX = parseFloat(style.paddingLeft) + parseFloat(style.paddingRight);
519
+
520
+ const totalPx = axis === 'row' ? sectionRect.height - paddingY : sectionRect.width - paddingX;
521
+
522
+ const newWeights = [];
523
+ for (let newIndex = oldCount + 1; newIndex <= newCount; newIndex++) {
524
+ const occupyingElement =
525
+ axis === 'row'
526
+ ? sectionData.find((el) => el.position.row <= newIndex && newIndex <= el.position.row + el.size.rowSpan - 1)
527
+ : sectionData.find((el) => el.position.col <= newIndex && newIndex <= el.position.col + el.size.colSpan - 1);
528
+
529
+ // 데이터 있으면
530
+ if (occupyingElement) {
531
+ const sizePx = axis === 'row' ? occupyingElement.size.h : occupyingElement.size.w;
532
+ newWeights.push(sizePx / totalPx);
533
+ } else {
534
+ newWeights.push(20 / totalPx);
535
+ }
536
+ }
537
+
538
+ return [...oldWeight, ...newWeights];
539
+ }
540
+
541
+ // 기존 대비 감소 - 마지막 행/열 제거, 남은 항목의 비율은 유지한 채 합계가 newCount가 되도록 재스케일
542
+ if (oldCount > newCount) {
543
+ const keptWeight = oldWeight.slice(0, newCount);
544
+ const keptSum = keptWeight.reduce((sum, w) => sum + w, 0);
545
+
546
+ // 남은 항목이 전부 0이면 비율을 유지할 수 없어 균등값(1)으로 폴백
547
+ if (keptSum === 0) {
548
+ return Array(newCount).fill(1);
549
+ }
550
+
551
+ const scale = newCount / keptSum;
552
+ return keptWeight.map((w) => w * scale);
553
+ }
554
+
555
+ return [];
556
+ }
557
+
490
558
  /** sectionConfig 변경
491
559
  * @param {string} type - config 타입 (column, row, gap, guideLine)
492
560
  * @param {number|boolean} config - 새로운 설정 값
@@ -541,10 +609,12 @@ class EngineManager {
541
609
 
542
610
  if (type === 'column') {
543
611
  // gridColumns 변경되면 gridColumnRatio 초기화
544
- newConfig = { gridColumns: value, gridColumnRatio: [] };
612
+ // newConfig = { gridColumns: value, gridColumnRatio: [] };
613
+ newConfig = { gridColumns: value, gridColumnRatio: this._calculateGridRatio(type, value) };
545
614
  } else if (type === 'row') {
546
615
  // gridRows 변경되면 gridRowRatio 초기화
547
- newConfig = { gridRows: value, gridRowRatio: [] };
616
+ // newConfig = { gridRows: value, gridRowRatio: [] };
617
+ newConfig = { gridRows: value, gridRowRatio: this._calculateGridRatio(type, value) };
548
618
  } else if (type === 'gap') {
549
619
  newConfig = { gridGap: value };
550
620
  }
@@ -6,6 +6,7 @@ function useDisplay(api) {
6
6
  activeDesign: null,
7
7
  displayMode: 'desktop',
8
8
  displaySize: null,
9
+ canvasSize: null,
9
10
  });
10
11
 
11
12
  const _subscriptions = [];
@@ -40,6 +41,7 @@ function useDisplay(api) {
40
41
  _subscribe('layout:setLayoutName', ({ $screenConfig }) => {
41
42
  state.displayMode = $screenConfig.displayMode;
42
43
  state.displaySize = { ...$screenConfig.displaySize };
44
+ state.canvasSize = { ...$screenConfig.canvasSize };
43
45
  });
44
46
 
45
47
  // activeMenu 변경 감지
@@ -61,7 +63,7 @@ function useDisplay(api) {
61
63
  });
62
64
 
63
65
  _subscribe('system:restoredState', ({ domain, state: snapShot }) => {
64
- if (domain === 'all'|| domain === 'draft') {
66
+ if (domain === 'all' || domain === 'draft') {
65
67
  state.displayMode = snapShot.display.displayMode;
66
68
  state.displaySize = { ...snapShot.display.displaySize };
67
69
  }