polarvo-layout 1.0.89 → 1.0.90
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 +1 -1
- package/src/components/Layout/BaseLayout.vue +26 -16
- package/src/components/Layout/CanvasContainer.vue +96 -32
- package/src/components/Layout/GridLayout.vue +65 -29
- package/src/core/engines/GridDropEngine.js +17 -7
- package/src/core/engines/LayoutEngine.js +49 -0
- package/src/core/managers/EngineManager.js +26 -3
- package/src/library/LayoutLibrary.js +21 -0
- package/src/utils/grid-track.js +73 -29
package/package.json
CHANGED
|
@@ -49,7 +49,7 @@ import { toRefs, computed, onMounted, onUnmounted, ref, inject, unref } from 'vu
|
|
|
49
49
|
import PolarLayout from './PolarLayout.vue';
|
|
50
50
|
import FreeLayout from './FreeLayout.vue';
|
|
51
51
|
import GridLayout from './GridLayout.vue';
|
|
52
|
-
import { getGridRowMinPx, toGridTrackSizes, toGridRowSizes, isInLastLayoutRow } from '../../utils/grid-track.js';
|
|
52
|
+
import { getGridRowMinPx, getRowExtraPx, toGridTrackSizes, toGridRowSizes, toSectionRowSizes, isInLastLayoutRow } from '../../utils/grid-track.js';
|
|
53
53
|
|
|
54
54
|
// 우클릭 방지
|
|
55
55
|
// window.oncontextmenu = () => {
|
|
@@ -88,18 +88,26 @@ const extraHeight = computed(() => (props.preview ? 0 : (unref(canvasExtraHeight
|
|
|
88
88
|
// 확장 높이는 마지막 행 섹션만 가져감
|
|
89
89
|
const getSectionExtraHeight = (section) => (isInLastLayoutRow(section, gridRatio.value?.row?.length) ? extraHeight.value : 0);
|
|
90
90
|
|
|
91
|
+
// 레이아웃 행 크기
|
|
92
|
+
// - 미리보기: 부모 높이를 벗어나지 않도록 min 0 + 비율(fr)만 사용
|
|
93
|
+
// - 편집 화면: 확장 높이가 있으면 마지막 행만 가져감 (min은 기존 fr과 동일하게 auto - 섹션 콘텐츠 최소 높이 유지), 없으면 기존 fr
|
|
94
|
+
const getBaseRows = () => {
|
|
95
|
+
const rowRatio = gridRatio.value?.row;
|
|
96
|
+
if (!rowRatio) return undefined;
|
|
97
|
+
if (props.preview) return rowRatio.map((ratio) => `minmax(0, ${ratio}fr)`).join(' ');
|
|
98
|
+
if (!(extraHeight.value > 0)) return rowRatio.map((ratio) => `${ratio}fr`).join(' ');
|
|
99
|
+
return toGridRowSizes(
|
|
100
|
+
rowRatio,
|
|
101
|
+
rowRatio.map(() => 'auto'),
|
|
102
|
+
gapSize.value ?? 0,
|
|
103
|
+
extraHeight.value,
|
|
104
|
+
);
|
|
105
|
+
};
|
|
106
|
+
|
|
91
107
|
const getBaseStyle = computed(() => {
|
|
92
108
|
return {
|
|
93
109
|
gridTemplateColumns: gridRatio.value?.column.map((ratio) => `${ratio}fr`).join(' '),
|
|
94
|
-
|
|
95
|
-
gridTemplateRows: gridRatio.value?.row
|
|
96
|
-
? toGridRowSizes(
|
|
97
|
-
gridRatio.value.row,
|
|
98
|
-
gridRatio.value.row.map(() => 'auto'),
|
|
99
|
-
gapSize.value ?? 0,
|
|
100
|
-
extraHeight.value,
|
|
101
|
-
)
|
|
102
|
-
: undefined,
|
|
110
|
+
gridTemplateRows: getBaseRows(),
|
|
103
111
|
marginRight: layoutType.value === 'no-split' ? '0' : layoutType.value === 'three-column-split' ? '14px' : '8px',
|
|
104
112
|
gap: `${gapSize.value}px`,
|
|
105
113
|
};
|
|
@@ -109,17 +117,19 @@ const getSectionStyle = (key, section) => {
|
|
|
109
117
|
const gridRows = section.config?.gridRows ?? 3;
|
|
110
118
|
const rowRatio = section.config?.gridRowRatio?.length ? section.config.gridRowRatio : Array(gridRows).fill(1);
|
|
111
119
|
const sectionElements = gridElements.value.filter((el) => el.section === key && section.elementIds?.includes(el.id));
|
|
120
|
+
const gap = section.config?.gridGap ?? 5;
|
|
121
|
+
const extraHeight = getSectionExtraHeight(section);
|
|
122
|
+
// 미리보기는 요소 최소 높이(편집 화면 px) 미적용 - 부모 높이 초과 방지
|
|
123
|
+
const minPx = props.preview
|
|
124
|
+
? []
|
|
125
|
+
: getGridRowMinPx(sectionElements, gridRows, rowRatio, gap, getRowExtraPx(section.config?.gridRowExtra, gridRows, extraHeight));
|
|
112
126
|
|
|
113
127
|
return {
|
|
114
128
|
'--grid-columns': section.config?.gridColumns ?? 3,
|
|
115
129
|
'--grid-rows': gridRows,
|
|
116
130
|
'--grid-gap': `${section.config?.gridGap ?? 5}px`,
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
getGridRowMinPx(sectionElements, gridRows),
|
|
120
|
-
section.config?.gridGap ?? 5,
|
|
121
|
-
getSectionExtraHeight(section),
|
|
122
|
-
),
|
|
131
|
+
// 비율(gridRowRatio)은 확장 높이 제외 기준 - 확장 높이는 마지막 행 + 위로 넘어간 몫(gridRowExtra)으로 편집 화면에만 붙음
|
|
132
|
+
'--grid-row-sizes': toSectionRowSizes(rowRatio, minPx, gap, extraHeight, section.config?.gridRowExtra),
|
|
123
133
|
'--grid-column-sizes': section.config?.gridColumnRatio?.length ? toGridTrackSizes(section.config.gridColumnRatio) : undefined, // 비어있으면 아예 안 넣어서 CSS 기본값으로 폴백
|
|
124
134
|
position: 'relative',
|
|
125
135
|
};
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
<script setup>
|
|
27
27
|
import { toRefs, ref, computed, watch, provide, onMounted } from 'vue';
|
|
28
28
|
import BaseLayout from './BaseLayout.vue';
|
|
29
|
-
import { isInLastLayoutRow,
|
|
29
|
+
import { GRID_MIN_ROW_PX, isInLastLayoutRow, getRowExtraPx, satisfiesGridRowMin } from '../../utils/grid-track.js';
|
|
30
30
|
|
|
31
31
|
const props = defineProps({
|
|
32
32
|
polarvo: {
|
|
@@ -44,10 +44,14 @@ const DEFAULT_DISPLAY_SIZE = {
|
|
|
44
44
|
|
|
45
45
|
// height 관련 여유 공간 추가하여 계산함 (50, 200)
|
|
46
46
|
const { displaySize, canvasSize } = toRefs(props.polarvo.display.state);
|
|
47
|
-
const { elements,
|
|
47
|
+
const { elements, layoutData, gridRatio, canvasExtraHeight } = toRefs(props.polarvo.layout.state);
|
|
48
|
+
const { setCanvasExtraHeight } = props.polarvo.layout;
|
|
48
49
|
const { elements: gridElements } = toRefs(props.polarvo.gridDrop.state);
|
|
49
50
|
const { elements: freeElements } = toRefs(props.polarvo.freeDrop.state);
|
|
50
51
|
|
|
52
|
+
// 화면에 그리드 섹션이 하나라도 있는지 - 높이 계산 기준 (선택된 섹션 모드와 무관하게 화면 단위로 고정)
|
|
53
|
+
const hasGridSection = computed(() => Object.values(layoutData.value ?? {}).some((section) => section?.mode === 'grid'));
|
|
54
|
+
|
|
51
55
|
const defaultHeight = computed(() => {
|
|
52
56
|
const freeElementsBottom =
|
|
53
57
|
elements.value?.reduce((max, el) => {
|
|
@@ -62,8 +66,8 @@ const defaultHeight = computed(() => {
|
|
|
62
66
|
// 저장된 canvasSize가 없으면 free 요소 기준 계산값만 사용 (grid 섹션은 기본값 750으로 취급)
|
|
63
67
|
if (!canvasSize.value) return freeElementsBottom;
|
|
64
68
|
|
|
65
|
-
//
|
|
66
|
-
if (
|
|
69
|
+
// 그리드 섹션이 있는 화면: 라이브로 정확히 계산할 방법이 없어 저장된 값을 그대로 신뢰
|
|
70
|
+
if (hasGridSection.value) {
|
|
67
71
|
return canvasSize.value.defaultHeight ?? freeElementsBottom;
|
|
68
72
|
}
|
|
69
73
|
|
|
@@ -76,7 +80,7 @@ const defaultHeight = computed(() => {
|
|
|
76
80
|
function restoreAdditionalHeight() {
|
|
77
81
|
if (!canvasSize.value) return 0;
|
|
78
82
|
|
|
79
|
-
if (
|
|
83
|
+
if (hasGridSection.value) {
|
|
80
84
|
return canvasSize.value.additionalHeight ?? 0;
|
|
81
85
|
}
|
|
82
86
|
|
|
@@ -84,7 +88,13 @@ function restoreAdditionalHeight() {
|
|
|
84
88
|
return Math.max(0, savedTotal - defaultHeight.value);
|
|
85
89
|
}
|
|
86
90
|
|
|
87
|
-
|
|
91
|
+
// 확장 높이는 LayoutEngine 상태(canvasExtraHeight)로 관리 - 섹션 행 비율과 함께 히스토리(undo/redo)에 기록됨
|
|
92
|
+
// 직접 대입은 히스토리 없이 동기화만 함 (화면 전환 시 복원 등)
|
|
93
|
+
const additionalHeight = computed({
|
|
94
|
+
get: () => canvasExtraHeight.value ?? 0,
|
|
95
|
+
set: (value) => setCanvasExtraHeight(value),
|
|
96
|
+
});
|
|
97
|
+
additionalHeight.value = restoreAdditionalHeight();
|
|
88
98
|
|
|
89
99
|
// canvasSize(저장된 캔버스 크기)가 바뀌면(=화면 전환) 그 값으로 복원, 없으면 0
|
|
90
100
|
watch(
|
|
@@ -141,55 +151,109 @@ defineExpose({ defaultHeight, additionalHeight, displayHeight });
|
|
|
141
151
|
provide('canvasExtraHeight', additionalHeight);
|
|
142
152
|
|
|
143
153
|
const RESIZE_STEP = 200;
|
|
144
|
-
const MIN_ROW_PX = 20; // GridLayout.vue의 MIN_ROW_PX와 동일한 값
|
|
145
154
|
|
|
146
|
-
|
|
147
|
-
|
|
155
|
+
// 확장 높이가 delta만큼 줄었을 때 행별 확장 높이 몫 - 마지막 행 몫부터 줄이고, 모자라면 아래쪽 행부터 줄임
|
|
156
|
+
function reduceExtraShares(shares, delta) {
|
|
157
|
+
const reduced = [...shares];
|
|
158
|
+
for (let i = reduced.length - 1; i >= 0 && delta > 0; i--) {
|
|
159
|
+
const take = Math.min(reduced[i], delta);
|
|
160
|
+
reduced[i] -= take;
|
|
161
|
+
delta -= take;
|
|
162
|
+
}
|
|
163
|
+
return reduced;
|
|
148
164
|
}
|
|
149
165
|
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
166
|
+
// 마지막 행 그리드 섹션의 현재 행 px / 행별 확장 높이 몫 / 요소 (DOM 기준)
|
|
167
|
+
function getGridSectionInfo(key, section, sectionEl) {
|
|
168
|
+
const gridRows = section.config?.gridRows || 1;
|
|
169
|
+
const trackPx = getComputedStyle(sectionEl)
|
|
170
|
+
.gridTemplateRows.split(' ')
|
|
171
|
+
.slice(0, gridRows)
|
|
172
|
+
.map((v) => parseFloat(v) || 0);
|
|
154
173
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
174
|
+
// GridLayout.filteredElements와 동일 (범위 밖 요소 제외)
|
|
175
|
+
const elements = (gridElements.value ?? []).filter(
|
|
176
|
+
(el) => el.section === key && section.elementIds?.includes(el.id) && el.position.row >= 1 && el.position.row + el.size.rowSpan - 1 <= gridRows,
|
|
177
|
+
);
|
|
159
178
|
|
|
160
|
-
|
|
161
|
-
|
|
179
|
+
return {
|
|
180
|
+
trackPx,
|
|
181
|
+
shares: getRowExtraPx(section.config?.gridRowExtra, gridRows, additionalHeight.value),
|
|
182
|
+
elements,
|
|
183
|
+
gap: section.config?.gridGap ?? 5,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// 그리드 섹션이 행/요소 최소 크기를 유지하면서 줄어들 수 있는 최대 px (limit 이하)
|
|
188
|
+
function getGridShrinkable({ trackPx, shares, elements, gap }, limit) {
|
|
189
|
+
const canShrinkBy = (delta) => {
|
|
190
|
+
const next = reduceExtraShares(shares, delta);
|
|
191
|
+
const nextPx = trackPx.map((v, i) => v - (shares[i] - next[i]));
|
|
192
|
+
// 줄어드는 행만 검사 (기존에 이미 최소 크기를 못 맞춘 다른 행 때문에 막히지 않도록)
|
|
193
|
+
const shrunkRows = next.map((v, i) => (v < shares[i] ? i : -1)).filter((i) => i >= 0);
|
|
194
|
+
return satisfiesGridRowMin(elements, nextPx, gap, GRID_MIN_ROW_PX, shrunkRows);
|
|
195
|
+
};
|
|
162
196
|
|
|
197
|
+
for (let delta = limit; delta > 0; delta--) {
|
|
198
|
+
if (canShrinkBy(delta)) return delta;
|
|
199
|
+
}
|
|
200
|
+
return 0;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// 확장 높이를 받는 마지막 행 섹션 목록 (DOM이 있는 것만)
|
|
204
|
+
function getExtraSections() {
|
|
205
|
+
return Object.entries(layoutData.value ?? {})
|
|
206
|
+
.filter(([, section]) => isInLastLayoutRow(section, gridRatio.value?.row?.length))
|
|
207
|
+
.map(([key, section]) => ({ key, section, sectionEl: document.getElementById(`${key}Layout`) }))
|
|
208
|
+
.filter(({ sectionEl }) => sectionEl);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// 확장 높이를 받는 마지막 행 섹션들이 요소 최소 크기를 유지하면서 줄어들 수 있는 높이(px, limit 이하)
|
|
212
|
+
function getShrinkableHeight(limit = additionalHeight.value) {
|
|
213
|
+
let slack = Math.max(0, Math.floor(limit));
|
|
214
|
+
|
|
215
|
+
getExtraSections().forEach(({ key, section, sectionEl }) => {
|
|
163
216
|
if (section.mode === 'grid') {
|
|
164
|
-
|
|
165
|
-
const trackPx = getComputedStyle(sectionEl)
|
|
166
|
-
.gridTemplateRows.split(' ')
|
|
167
|
-
.map((v) => parseFloat(v));
|
|
168
|
-
const lastTrackPx = trackPx[gridRows - 1] ?? 0;
|
|
169
|
-
const required = getGridLastRowRequiredPx(sectionElements, gridRows, trackPx, section.config?.gridGap ?? 5, MIN_ROW_PX);
|
|
170
|
-
slack = Math.min(slack, lastTrackPx - required);
|
|
217
|
+
slack = Math.min(slack, getGridShrinkable(getGridSectionInfo(key, section, sectionEl), slack));
|
|
171
218
|
} else {
|
|
219
|
+
const sectionElements = (freeElements.value ?? []).filter((el) => el.section === key && section.elementIds?.includes(el.id));
|
|
172
220
|
const bottom = sectionElements.reduce((max, el) => Math.max(max, (el.position?.y || 0) + (el.size?.h || 0)), 0);
|
|
173
|
-
slack = Math.min(slack, sectionEl.clientHeight - bottom);
|
|
221
|
+
slack = Math.min(slack, Math.floor(sectionEl.clientHeight - bottom));
|
|
174
222
|
}
|
|
175
223
|
});
|
|
176
224
|
|
|
177
|
-
return Math.max(0,
|
|
225
|
+
return Math.max(0, slack);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// 확장: 늘어난 높이는 마지막 행이 전부 가져감 (비율/위로 넘어간 확장분은 그대로)
|
|
229
|
+
function expandContainer() {
|
|
230
|
+
setCanvasExtraHeight(additionalHeight.value + RESIZE_STEP, {}, true);
|
|
178
231
|
}
|
|
179
232
|
|
|
180
233
|
// 요소 크기/배치·레이아웃·확장 높이가 바뀔 때마다 DOM 반영 후 다시 계산
|
|
181
234
|
const shrinkableHeight = ref(0);
|
|
182
235
|
const updateShrinkableHeight = () => {
|
|
183
|
-
shrinkableHeight.value = getShrinkableHeight();
|
|
236
|
+
shrinkableHeight.value = getShrinkableHeight(Math.min(RESIZE_STEP, additionalHeight.value));
|
|
184
237
|
};
|
|
185
238
|
watch([displayHeight, gridElements, freeElements, layoutData, gridRatio], updateShrinkableHeight, { deep: true, flush: 'post' });
|
|
186
239
|
onMounted(updateShrinkableHeight);
|
|
187
240
|
|
|
188
|
-
//
|
|
241
|
+
// 축소: 마지막 행 몫부터 줄이고, 모자라면 위로 넘어간 확장분(gridRowExtra)도 줄임 - 확장 높이와 함께 히스토리에 한 번 기록
|
|
242
|
+
// displayHeight(=defaultHeight + additionalHeight)는 항상 defaultHeight 이상, 요소 최소 크기 이상으로 유지
|
|
189
243
|
const canShrink = computed(() => shrinkableHeight.value > 0);
|
|
190
244
|
function shrinkContainer() {
|
|
191
|
-
const
|
|
192
|
-
|
|
245
|
+
const delta = getShrinkableHeight(Math.min(RESIZE_STEP, additionalHeight.value));
|
|
246
|
+
if (!delta) return;
|
|
247
|
+
|
|
248
|
+
// 위로 넘어간 확장분(gridRowExtra)이 줄어드는 섹션만 함께 저장
|
|
249
|
+
const rowExtras = {};
|
|
250
|
+
getExtraSections().forEach(({ key, section, sectionEl }) => {
|
|
251
|
+
if (section.mode !== 'grid' || !section.config?.gridRowExtra?.length) return;
|
|
252
|
+
const moved = reduceExtraShares(getGridSectionInfo(key, section, sectionEl).shares, delta).slice(0, -1);
|
|
253
|
+
if (moved.some((v, i) => v !== section.config.gridRowExtra[i])) rowExtras[key] = moved;
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
setCanvasExtraHeight(additionalHeight.value - delta, rowExtras, true);
|
|
193
257
|
}
|
|
194
258
|
</script>
|
|
195
259
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<template v-if="!preview">
|
|
3
3
|
<GridItem
|
|
4
|
-
v-for="
|
|
5
|
-
:key="item.id || `empty-${
|
|
4
|
+
v-for="item in mergedData"
|
|
5
|
+
:key="item.id || `empty-${item.row}-${item.col}`"
|
|
6
6
|
:item="item"
|
|
7
7
|
:polarvo="props.polarvo"
|
|
8
8
|
:sectionKey="props.sectionKey"
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
|
|
75
75
|
<script setup>
|
|
76
76
|
import GridItem from './GridItem.vue';
|
|
77
|
-
import { getGridRowMinPx, toGridTrackSizes,
|
|
77
|
+
import { GRID_MIN_ROW_PX, getGridRowMinPx, toGridTrackSizes, toSectionRowSizes, getRowExtraPx } from '../../utils/grid-track.js';
|
|
78
78
|
|
|
79
79
|
import { ref, toRefs, getCurrentInstance, onMounted, onUnmounted, inject, watch, computed } from 'vue';
|
|
80
80
|
|
|
@@ -99,7 +99,7 @@ const props = defineProps({
|
|
|
99
99
|
type: Array,
|
|
100
100
|
required: true,
|
|
101
101
|
},
|
|
102
|
-
// 캔버스 확장 높이 -
|
|
102
|
+
// 캔버스 확장 높이 - 이 섹션이 추가로 가져가는 px (BaseLayout에서 마지막 행 섹션에만 전달, 미리보기는 0)
|
|
103
103
|
extraHeight: {
|
|
104
104
|
type: Number,
|
|
105
105
|
default: 0,
|
|
@@ -127,7 +127,7 @@ const gridRows = ref(1); // 동적으로 계산
|
|
|
127
127
|
const gridGap = computed(() => props.sectionData?.config?.gridGap ?? 5); // BaseLayout.vue --grid-gap과 동일한 소스/기본값
|
|
128
128
|
|
|
129
129
|
// row, col Resize
|
|
130
|
-
const MIN_ROW_PX =
|
|
130
|
+
const MIN_ROW_PX = GRID_MIN_ROW_PX; // 행 최소 높이(px)
|
|
131
131
|
const MIN_COL_PX = 20; // 열 최소 너비(px)
|
|
132
132
|
|
|
133
133
|
const DRAG_CONFIG = {
|
|
@@ -142,8 +142,19 @@ const DRAG_CONFIG = {
|
|
|
142
142
|
posKey: 'row',
|
|
143
143
|
spanKey: 'rowSpan',
|
|
144
144
|
getMinPx: (el) => el.size?.h ?? MIN_ROW_PX, // GridDropEngine.getElementStyle의 minHeight와 동일
|
|
145
|
-
|
|
146
|
-
|
|
145
|
+
// 행별 확장 높이 몫 - 비율에는 섞지 않고 편집 화면에만 붙임
|
|
146
|
+
getExtraPx: () => getRowExtraPx(props.sectionData?.config?.gridRowExtra, gridRows.value, props.extraHeight),
|
|
147
|
+
// BaseLayout과 동일한 형식
|
|
148
|
+
toSizes: (ratio, extras) =>
|
|
149
|
+
toSectionRowSizes(
|
|
150
|
+
ratio,
|
|
151
|
+
getGridRowMinPx(filteredElements.value, ratio.length, ratio, gridGap.value, extras),
|
|
152
|
+
gridGap.value,
|
|
153
|
+
props.extraHeight,
|
|
154
|
+
extras.slice(0, -1),
|
|
155
|
+
),
|
|
156
|
+
// 확장 높이를 받는 섹션만 gridRowExtra를 함께 저장 (비율 + 확장분 한 번에 히스토리 기록)
|
|
157
|
+
toConfig: (ratio, extras) => (props.extraHeight > 0 ? { ratio, extra: extras.slice(0, -1).map((v) => Math.max(0, v)) } : ratio),
|
|
147
158
|
},
|
|
148
159
|
col: {
|
|
149
160
|
ref: gridColumns,
|
|
@@ -156,8 +167,9 @@ const DRAG_CONFIG = {
|
|
|
156
167
|
posKey: 'col',
|
|
157
168
|
spanKey: 'colSpan',
|
|
158
169
|
getMinPx: () => MIN_COL_PX,
|
|
159
|
-
|
|
170
|
+
getExtraPx: () => Array(gridColumns.value).fill(0),
|
|
160
171
|
toSizes: (ratio) => toGridTrackSizes(ratio), // BaseLayout과 동일한 형식
|
|
172
|
+
toConfig: (ratio) => ratio,
|
|
161
173
|
},
|
|
162
174
|
};
|
|
163
175
|
|
|
@@ -173,7 +185,7 @@ function handleMouseDown(event, axis, index) {
|
|
|
173
185
|
event.preventDefault();
|
|
174
186
|
event.stopPropagation();
|
|
175
187
|
|
|
176
|
-
const { ref, minPx, ratioKey, gridTemplateKey, clientKey, positionKey, cssVar, posKey, spanKey, getMinPx,
|
|
188
|
+
const { ref, minPx, ratioKey, gridTemplateKey, clientKey, positionKey, cssVar, posKey, spanKey, getMinPx, getExtraPx, toSizes, toConfig } =
|
|
177
189
|
DRAG_CONFIG[axis];
|
|
178
190
|
|
|
179
191
|
dragState.value.axis = axis;
|
|
@@ -185,6 +197,7 @@ function handleMouseDown(event, axis, index) {
|
|
|
185
197
|
|
|
186
198
|
let dragSnapShot = null; // 드래그 시작 시 스냅샷
|
|
187
199
|
let liveRatio = null; // 미리보기 중 계산된 최종 fr 배열 (stopDrag에서 커밋)
|
|
200
|
+
let liveExtras = null; // 미리보기 중 계산된 트랙별 확장 높이 px (stopDrag에서 커밋)
|
|
188
201
|
let pendingClient = null;
|
|
189
202
|
let rafId = null;
|
|
190
203
|
|
|
@@ -230,27 +243,33 @@ function handleMouseDown(event, axis, index) {
|
|
|
230
243
|
|
|
231
244
|
const trackPx = getComputedStyle(container)
|
|
232
245
|
[gridTemplateKey].split(' ')
|
|
246
|
+
.slice(0, ref.value)
|
|
233
247
|
.map((v) => parseFloat(v));
|
|
234
248
|
|
|
235
249
|
const prevIdx = index - 1; // 드래그 행 위쪽 행 인덱스
|
|
236
250
|
const nextIdx = index; // 드래그 행 인덱스
|
|
237
251
|
|
|
238
|
-
//
|
|
239
|
-
|
|
240
|
-
const
|
|
241
|
-
ratioPx[ratioPx.length - 1] -= extraPx();
|
|
242
|
-
const frPerPx = (baselineRatio[prevIdx] + baselineRatio[nextIdx]) / (ratioPx[prevIdx] + ratioPx[nextIdx]);
|
|
252
|
+
// 트랙 px = 비율 몫(basePx) + 확장 높이 몫(extraPx)
|
|
253
|
+
const baselineExtras = getExtraPx();
|
|
254
|
+
const basePx = trackPx.map((v, i) => v - baselineExtras[i]);
|
|
243
255
|
|
|
244
|
-
//
|
|
256
|
+
// 1px당 Fr 계산 - 드래그로 바뀌는 두 트랙의 비율 몫 기준 (확장 높이는 비율과 무관)
|
|
257
|
+
const frPerPx = (baselineRatio[prevIdx] + baselineRatio[nextIdx]) / (basePx[prevIdx] + basePx[nextIdx]);
|
|
258
|
+
|
|
259
|
+
// 요소 최소 크기 때문에 두 트랙이 가져야 하는 최소 px (확장 높이 포함한 트랙 전체 기준)
|
|
245
260
|
const { prevMinPx, nextMinPx } = _getElementMinPx(trackPx, prevIdx, nextIdx);
|
|
246
261
|
|
|
247
262
|
dragSnapShot = {
|
|
248
263
|
container,
|
|
249
264
|
baselineRatio,
|
|
265
|
+
baselineExtras,
|
|
250
266
|
frPerPx,
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
267
|
+
// 두 트랙이 각각 줄어들 수 있는 최대 px
|
|
268
|
+
prevShrinkPx: Math.max(0, trackPx[prevIdx] - Math.max(minPx, prevMinPx)),
|
|
269
|
+
nextShrinkPx: Math.max(0, trackPx[nextIdx] - Math.max(minPx, nextMinPx)),
|
|
270
|
+
// 비율 몫이 줄어들 수 있는 최대 px (미리보기에서도 행 최소 크기 유지)
|
|
271
|
+
prevBaseShrinkPx: Math.max(0, basePx[prevIdx] - minPx),
|
|
272
|
+
nextBaseShrinkPx: Math.max(0, basePx[nextIdx] - minPx),
|
|
254
273
|
};
|
|
255
274
|
|
|
256
275
|
document.addEventListener('mousemove', _startDragMove);
|
|
@@ -294,26 +313,42 @@ function handleMouseDown(event, axis, index) {
|
|
|
294
313
|
rafId = requestAnimationFrame(() => {
|
|
295
314
|
rafId = null;
|
|
296
315
|
|
|
297
|
-
const { baselineRatio, frPerPx,
|
|
316
|
+
const { baselineRatio, baselineExtras, frPerPx, prevShrinkPx, nextShrinkPx, prevBaseShrinkPx, nextBaseShrinkPx, container } =
|
|
317
|
+
dragSnapShot;
|
|
298
318
|
const prevIdx = index - 1; // 드래그 행 위쪽 행 인덱스
|
|
299
319
|
const nextIdx = index; // 드래그 행 인덱스
|
|
300
320
|
|
|
301
|
-
// 순수 이동량
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
//
|
|
306
|
-
|
|
321
|
+
// 순수 이동량(px) - 양수 = 아래로 이동(위쪽 행 커짐), 음수 = 위로 이동(아래쪽 행 커짐)
|
|
322
|
+
// 두 트랙 모두 요소/행 최소 크기 이하로 작아지지 않도록 제한
|
|
323
|
+
const movePx = Math.min(Math.max(pendingClient - startPos.value[positionKey], -prevShrinkPx), nextShrinkPx);
|
|
324
|
+
|
|
325
|
+
// 줄어드는 트랙에서 옮길 px를 비율 몫 / 확장 높이 몫으로 나눔 - 확장 높이는 아래 행에 우선 배정
|
|
326
|
+
// 아래쪽 행이 줄어들 때: 비율 몫 먼저 → 최소치에 닿으면 확장 높이 몫을 위 행으로 넘김
|
|
327
|
+
// 위쪽 행이 줄어들 때: 넘겨받은 확장 높이 몫 먼저 아래 행으로 돌려줌 → 그다음 비율 몫
|
|
328
|
+
let ratioPx = 0; // 위쪽 행으로 옮겨지는 비율 몫 px (음수 = 아래쪽 행으로)
|
|
329
|
+
let extraPx = 0; // 위쪽 행으로 옮겨지는 확장 높이 몫 px (음수 = 아래쪽 행으로)
|
|
330
|
+
if (movePx >= 0) {
|
|
331
|
+
ratioPx = Math.min(movePx, nextBaseShrinkPx);
|
|
332
|
+
extraPx = Math.min(movePx - ratioPx, baselineExtras[nextIdx]);
|
|
333
|
+
} else {
|
|
334
|
+
extraPx = -Math.min(-movePx, baselineExtras[prevIdx]);
|
|
335
|
+
ratioPx = -Math.min(-movePx + extraPx, prevBaseShrinkPx);
|
|
336
|
+
}
|
|
307
337
|
|
|
308
338
|
// 미리보기용
|
|
309
339
|
const preview = [...baselineRatio];
|
|
310
|
-
preview[prevIdx] +=
|
|
311
|
-
preview[nextIdx] -=
|
|
340
|
+
preview[prevIdx] += ratioPx * frPerPx;
|
|
341
|
+
preview[nextIdx] -= ratioPx * frPerPx;
|
|
342
|
+
|
|
343
|
+
const extras = [...baselineExtras];
|
|
344
|
+
extras[prevIdx] += extraPx;
|
|
345
|
+
extras[nextIdx] -= extraPx;
|
|
312
346
|
|
|
313
347
|
liveRatio = preview;
|
|
348
|
+
liveExtras = extras;
|
|
314
349
|
|
|
315
350
|
// 화면에만 즉시 반영
|
|
316
|
-
container.style.setProperty(cssVar, toSizes(preview));
|
|
351
|
+
container.style.setProperty(cssVar, toSizes(preview, extras));
|
|
317
352
|
});
|
|
318
353
|
}
|
|
319
354
|
function _stopDrag() {
|
|
@@ -322,12 +357,13 @@ function handleMouseDown(event, axis, index) {
|
|
|
322
357
|
dragState.value.activeIdx = null;
|
|
323
358
|
|
|
324
359
|
if (liveRatio) {
|
|
325
|
-
setSectionConfig(ratioKey, liveRatio);
|
|
360
|
+
setSectionConfig(ratioKey, toConfig(liveRatio, liveExtras));
|
|
326
361
|
}
|
|
327
362
|
|
|
328
363
|
startPos.value = { x: 0, y: 0 };
|
|
329
364
|
dragSnapShot = null;
|
|
330
365
|
liveRatio = null;
|
|
366
|
+
liveExtras = null;
|
|
331
367
|
pendingClient = null;
|
|
332
368
|
rafId = null;
|
|
333
369
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { cloneDeep } from 'lodash-es';
|
|
2
|
+
import { GRID_MIN_ROW_PX } from '../../utils/grid-track.js';
|
|
2
3
|
|
|
3
4
|
// event 발행 시 'gridDrop'으로 시작하는 이름 사용
|
|
4
5
|
class GridDropEngine {
|
|
@@ -245,12 +246,10 @@ class GridDropEngine {
|
|
|
245
246
|
* @returns {object} - 스타일 객체
|
|
246
247
|
*/
|
|
247
248
|
getElementStyle(type, cell) {
|
|
248
|
-
const MIN_CELL_PX = 20; // GridLayout.vue의 MIN_ROW_PX와 동일한 값
|
|
249
|
-
|
|
250
249
|
if (type == 'empty') {
|
|
251
250
|
return {
|
|
252
251
|
gridArea: `${cell.row} / ${cell.col} / span 1 / span 1`,
|
|
253
|
-
minHeight: `${
|
|
252
|
+
minHeight: `${GRID_MIN_ROW_PX}px`,
|
|
254
253
|
};
|
|
255
254
|
} else if (type == 'element') {
|
|
256
255
|
const { row, col } = cell.position;
|
|
@@ -258,7 +257,7 @@ class GridDropEngine {
|
|
|
258
257
|
|
|
259
258
|
return {
|
|
260
259
|
gridArea: `${row} / ${col} / span ${rowSpan} / span ${colSpan}`,
|
|
261
|
-
minHeight: `${cell.size?.h ??
|
|
260
|
+
minHeight: `${cell.size?.h ?? GRID_MIN_ROW_PX}px`,
|
|
262
261
|
};
|
|
263
262
|
}
|
|
264
263
|
}
|
|
@@ -477,10 +476,21 @@ class GridDropEngine {
|
|
|
477
476
|
console.error('[GridDropEngine] 활성화된 배치요소를 찾을 수 없습니다.');
|
|
478
477
|
return false;
|
|
479
478
|
}
|
|
480
|
-
if (col === this._activeElement.position.col && row === this._activeElement.position.row) return;
|
|
481
479
|
|
|
482
|
-
|
|
483
|
-
this._activeElement.
|
|
480
|
+
// 요소 크기만큼 그리드 범위 안으로 제한 (범위를 벗어나면 GridLayout.filteredElements에서 제외되어 사라짐)
|
|
481
|
+
const { colSpan, rowSpan } = this._activeElement.size;
|
|
482
|
+
const { gridColumns, gridRows } = this._activeConfig ?? {};
|
|
483
|
+
const nextCol = Math.max(1, Math.min(col, (gridColumns ?? col) - colSpan + 1));
|
|
484
|
+
const nextRow = Math.max(1, Math.min(row, (gridRows ?? row) - rowSpan + 1));
|
|
485
|
+
|
|
486
|
+
if (nextCol === this._activeElement.position.col && nextRow === this._activeElement.position.row) return;
|
|
487
|
+
|
|
488
|
+
// 다른 요소와 겹치면 이동하지 않음
|
|
489
|
+
const nextPosition = { ...this._activeElement.position, col: nextCol, row: nextRow };
|
|
490
|
+
if (this._detectElementCollision(nextPosition, this._activeElement.size)) return;
|
|
491
|
+
|
|
492
|
+
this._activeElement.position.col = nextCol;
|
|
493
|
+
this._activeElement.position.row = nextRow;
|
|
484
494
|
|
|
485
495
|
this.eventBus.emit('gridDrop:requestUpdateLocalBounds', {
|
|
486
496
|
elementId: this._activeElement.id,
|
|
@@ -12,6 +12,7 @@ class LayoutEngine {
|
|
|
12
12
|
this.gridNumber = { column: 1, row: 1 }; // 레이아웃 그리드 수
|
|
13
13
|
this.gridRatio = { column: [100], row: [100] }; // 레이아웃 그리드 비율
|
|
14
14
|
this.gapSize = 8; // 레이아웃 갭 간격
|
|
15
|
+
this.canvasExtraHeight = 0; // 캔버스 확장 높이(px) - 마지막 행에 배정, 히스토리에 기록
|
|
15
16
|
|
|
16
17
|
this._subscriptions = []; // 구독 해제 함수 저장용
|
|
17
18
|
this._setupSubscriptions();
|
|
@@ -139,6 +140,7 @@ class LayoutEngine {
|
|
|
139
140
|
gridNumber: this.gridNumber,
|
|
140
141
|
gridRatio: this.gridRatio,
|
|
141
142
|
gapSize: this.gapSize,
|
|
143
|
+
canvasExtraHeight: this.canvasExtraHeight,
|
|
142
144
|
});
|
|
143
145
|
}
|
|
144
146
|
|
|
@@ -154,6 +156,10 @@ class LayoutEngine {
|
|
|
154
156
|
row: [...state.gridRatio?.row],
|
|
155
157
|
};
|
|
156
158
|
this.gapSize = state.gapSize;
|
|
159
|
+
// 이전 버전 스냅샷에는 값이 없으므로 있을 때만 복원
|
|
160
|
+
if (typeof state.canvasExtraHeight === 'number') {
|
|
161
|
+
this.canvasExtraHeight = state.canvasExtraHeight;
|
|
162
|
+
}
|
|
157
163
|
}
|
|
158
164
|
|
|
159
165
|
/** ---------------------------------- 공통 메소드 ---------------------------------- **/
|
|
@@ -278,6 +284,49 @@ class LayoutEngine {
|
|
|
278
284
|
return { result: true, message: '갭 사이즈가 성공적으로 변경되었습니다.' };
|
|
279
285
|
}
|
|
280
286
|
|
|
287
|
+
/** 캔버스 확장 높이 변경 (+ 확장분을 받는 섹션들의 gridRowExtra 일괄 변경)
|
|
288
|
+
* gridRowExtra: 편집 화면 전용 - 마지막 행에서 위 행으로 넘어간 확장 높이(px), 길이 = gridRows - 1
|
|
289
|
+
* (미리보기용 비율 gridRowRatio에는 확장 높이가 섞이지 않음)
|
|
290
|
+
* @param {number} height - 새로운 확장 높이(px)
|
|
291
|
+
* @param {object} rowExtras - { [sectionKey]: gridRowExtra[] } - 함께 바꿀 섹션 값 (축소로 넘어간 확장분이 줄어드는 경우)
|
|
292
|
+
* @param {boolean} record - 히스토리 기록 여부 (확장/축소 버튼 = true, 화면 전환 시 복원 = false)
|
|
293
|
+
*/
|
|
294
|
+
setCanvasExtraHeight(height, rowExtras = {}, record = false) {
|
|
295
|
+
const value = Number(height);
|
|
296
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
297
|
+
return { result: false, message: '잘못된 캔버스 확장 높이입니다.' };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const changedSections = {};
|
|
301
|
+
Object.entries(rowExtras ?? {}).forEach(([key, extra]) => {
|
|
302
|
+
const section = this.layoutData?.[key];
|
|
303
|
+
const gridRows = section?.config?.gridRows || 1;
|
|
304
|
+
if (!section || !Array.isArray(extra) || extra.length !== gridRows - 1 || extra.some((v) => typeof v !== 'number' || !(v >= 0))) return;
|
|
305
|
+
|
|
306
|
+
changedSections[key] = { ...section, config: { ...section.config, gridRowExtra: [...extra] } };
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
const sectionKeys = Object.keys(changedSections);
|
|
310
|
+
if (value === this.canvasExtraHeight && !sectionKeys.length) {
|
|
311
|
+
return { result: true, message: '변경 사항이 없습니다.' };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (sectionKeys.length) {
|
|
315
|
+
this.layoutData = { ...this.layoutData, ...changedSections };
|
|
316
|
+
}
|
|
317
|
+
this.canvasExtraHeight = value;
|
|
318
|
+
|
|
319
|
+
this.eventBus.emit('layout:updateCanvasExtraHeight', {
|
|
320
|
+
$canvasExtraHeight: this.canvasExtraHeight,
|
|
321
|
+
$layoutData: this.layoutData,
|
|
322
|
+
$sections: sectionKeys,
|
|
323
|
+
$record: record,
|
|
324
|
+
timestamp: Date.now(),
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
return { result: true, message: '캔버스 확장 높이가 성공적으로 변경되었습니다.' };
|
|
328
|
+
}
|
|
329
|
+
|
|
281
330
|
/** 섹션 비율 변경
|
|
282
331
|
* @param {string} type - 'column' | 'row'
|
|
283
332
|
* @param {number} index - 변경할 비율의 인덱스
|
|
@@ -374,6 +374,17 @@ class EngineManager {
|
|
|
374
374
|
});
|
|
375
375
|
});
|
|
376
376
|
|
|
377
|
+
// 캔버스 확장/축소 - 확장 높이와 섹션 gridRowExtra를 한 번에 기록
|
|
378
|
+
this._subscribe('layout:updateCanvasExtraHeight', ({ $layoutData, $record }) => {
|
|
379
|
+
this.state.layoutData = $layoutData;
|
|
380
|
+
|
|
381
|
+
if (!$record) return;
|
|
382
|
+
this.eventBus.emit('system:historyCheckpoint', {
|
|
383
|
+
domain: 'layout',
|
|
384
|
+
state: this.getEngine('layout')?.getState() || null,
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
|
|
377
388
|
this._subscribe('layout:updateGapSize', () => {
|
|
378
389
|
this.eventBus.emit('system:historyCheckpoint', {
|
|
379
390
|
domain: 'layout',
|
|
@@ -481,6 +492,7 @@ class EngineManager {
|
|
|
481
492
|
gridRows: this.activeData.config?.gridRows || 2,
|
|
482
493
|
gridGap: this.activeData.config?.gridGap || 4,
|
|
483
494
|
gridRowRatio: this.activeData.config?.gridRowRatio || [],
|
|
495
|
+
gridRowExtra: this.activeData.config?.gridRowExtra || [],
|
|
484
496
|
gridColumnRatio: this.activeData.config?.gridColumnRatio || [],
|
|
485
497
|
showGuideLine: this.activeData.config?.showGuideLine || false,
|
|
486
498
|
};
|
|
@@ -579,13 +591,21 @@ class EngineManager {
|
|
|
579
591
|
if (type === 'guideLine') {
|
|
580
592
|
newConfig = { showGuideLine: value };
|
|
581
593
|
} else if (type === 'gridRowRatio') {
|
|
594
|
+
// 배열(비율만) 또는 { ratio, extra } (비율 + 위 행으로 넘어간 캔버스 확장분 px - 편집 화면 전용) - 한 번에 히스토리 기록
|
|
595
|
+
const ratio = Array.isArray(value) ? value : value?.ratio;
|
|
596
|
+
const extra = Array.isArray(value) ? undefined : value?.extra;
|
|
597
|
+
|
|
582
598
|
// gridRowRatio 배열은 gridRows 길이와 일치해야 함
|
|
583
599
|
const gridRows = this.activeData.config?.gridRows || 1;
|
|
584
|
-
if (!Array.isArray(
|
|
600
|
+
if (!Array.isArray(ratio) || ratio.length !== gridRows || ratio.some((v) => typeof v !== 'number' || !(v > 0))) {
|
|
585
601
|
return { result: false, message: '잘못된 행 비율 값입니다.' };
|
|
586
602
|
}
|
|
603
|
+
// gridRowExtra 배열은 gridRows - 1 길이와 일치해야 함
|
|
604
|
+
if (extra !== undefined && (!Array.isArray(extra) || extra.length !== gridRows - 1 || extra.some((v) => typeof v !== 'number' || !(v >= 0)))) {
|
|
605
|
+
return { result: false, message: '잘못된 행 확장 높이 값입니다.' };
|
|
606
|
+
}
|
|
587
607
|
|
|
588
|
-
newConfig = { gridRowRatio:
|
|
608
|
+
newConfig = extra === undefined ? { gridRowRatio: ratio } : { gridRowRatio: ratio, gridRowExtra: extra };
|
|
589
609
|
} else if (type === 'gridColumnRatio') {
|
|
590
610
|
// gridColumnRatio 배열은 gridColumns 길이와 일치해야 함
|
|
591
611
|
const gridColumns = this.activeData.config?.gridColumns || 1;
|
|
@@ -614,7 +634,8 @@ class EngineManager {
|
|
|
614
634
|
} else if (type === 'row') {
|
|
615
635
|
// gridRows 변경되면 gridRowRatio 초기화
|
|
616
636
|
// newConfig = { gridRows: value, gridRowRatio: [] };
|
|
617
|
-
|
|
637
|
+
// 행 개수가 바뀌면 위 행으로 넘어간 확장분도 초기화 (확장분 전체가 다시 마지막 행으로)
|
|
638
|
+
newConfig = { gridRows: value, gridRowRatio: this._calculateGridRatio(type, value), gridRowExtra: [] };
|
|
618
639
|
} else if (type === 'gap') {
|
|
619
640
|
newConfig = { gridGap: value };
|
|
620
641
|
}
|
|
@@ -1073,6 +1094,7 @@ class EngineManager {
|
|
|
1073
1094
|
gapSize: layoutEngine.gapSize,
|
|
1074
1095
|
gridNumber: cloneDeep(layoutEngine.gridNumber),
|
|
1075
1096
|
gridRatio: cloneDeep(layoutEngine.gridRatio),
|
|
1097
|
+
canvasExtraHeight: layoutEngine.canvasExtraHeight,
|
|
1076
1098
|
}),
|
|
1077
1099
|
|
|
1078
1100
|
getFreeDropState: () => ({
|
|
@@ -1110,6 +1132,7 @@ class EngineManager {
|
|
|
1110
1132
|
setLayoutName: (name, userData = null, setting = false) => layoutEngine.setLayoutName(name, userData, setting),
|
|
1111
1133
|
setGapSize: (size) => layoutEngine.setGapSize(size),
|
|
1112
1134
|
setRatio: (type, index, ratio) => layoutEngine.setRatio(type, index, ratio),
|
|
1135
|
+
setCanvasExtraHeight: (height, rowExtras, record) => layoutEngine.setCanvasExtraHeight(height, rowExtras, record),
|
|
1113
1136
|
// setUserLayoutData: (userData) => layoutEngine.setUserLayoutData(userData),
|
|
1114
1137
|
setActiveSection: (name) => this.setActiveSection(name),
|
|
1115
1138
|
updateActiveElement: (id, changes, emitEvent) => this.updateActiveElement(id, changes, emitEvent),
|
|
@@ -17,6 +17,7 @@ function useLayout(api) {
|
|
|
17
17
|
gapSize: null,
|
|
18
18
|
gridNumber: {},
|
|
19
19
|
gridRatio: {},
|
|
20
|
+
canvasExtraHeight: 0,
|
|
20
21
|
});
|
|
21
22
|
|
|
22
23
|
const _subscriptions = [];
|
|
@@ -82,6 +83,15 @@ function useLayout(api) {
|
|
|
82
83
|
state.gridRatio[$type] = [...$gridRatio[$type]];
|
|
83
84
|
});
|
|
84
85
|
|
|
86
|
+
// 캔버스 확장 높이 변경 감지 (축소 시 섹션 gridRowExtra가 함께 바뀜)
|
|
87
|
+
_subscribe('layout:updateCanvasExtraHeight', ({ $canvasExtraHeight, $layoutData, $sections }) => {
|
|
88
|
+
state.canvasExtraHeight = $canvasExtraHeight;
|
|
89
|
+
$sections.forEach((key) => {
|
|
90
|
+
state.layoutData[key] = { ...$layoutData[key] };
|
|
91
|
+
if (state.activeSection === key) state.activeConfig = $layoutData[key]?.config || null;
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
85
95
|
// activeSection 변경 감지 (activeData가 함께 초기화됨)
|
|
86
96
|
_subscribe('system:updateActiveSection', ({ $section, $mode, $config }) => {
|
|
87
97
|
state.activeSection = $section;
|
|
@@ -132,6 +142,7 @@ function useLayout(api) {
|
|
|
132
142
|
row: [...snapShot.layout.gridRatio.row],
|
|
133
143
|
};
|
|
134
144
|
state.gapSize = snapShot.layout.gapSize;
|
|
145
|
+
if (typeof snapShot.layout.canvasExtraHeight === 'number') state.canvasExtraHeight = snapShot.layout.canvasExtraHeight;
|
|
135
146
|
}
|
|
136
147
|
|
|
137
148
|
if (domain === 'manager') {
|
|
@@ -148,6 +159,7 @@ function useLayout(api) {
|
|
|
148
159
|
row: [...snapShot.gridRatio.row],
|
|
149
160
|
};
|
|
150
161
|
state.gapSize = snapShot.gapSize;
|
|
162
|
+
if (typeof snapShot.canvasExtraHeight === 'number') state.canvasExtraHeight = snapShot.canvasExtraHeight;
|
|
151
163
|
}
|
|
152
164
|
});
|
|
153
165
|
};
|
|
@@ -213,6 +225,14 @@ function useLayout(api) {
|
|
|
213
225
|
}
|
|
214
226
|
};
|
|
215
227
|
|
|
228
|
+
const setCanvasExtraHeight = (height, rowExtras = {}, record = false) => {
|
|
229
|
+
try {
|
|
230
|
+
return api.layout.setCanvasExtraHeight(height, rowExtras, record);
|
|
231
|
+
} catch (error) {
|
|
232
|
+
console.error('[useLayout] setCanvasExtraHeight 실행 중 오류 발생:', error);
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
|
|
216
236
|
const setActiveSection = (name) => {
|
|
217
237
|
try {
|
|
218
238
|
api.layout.setActiveSection(name);
|
|
@@ -261,6 +281,7 @@ function useLayout(api) {
|
|
|
261
281
|
setLayoutName,
|
|
262
282
|
setGapSize,
|
|
263
283
|
setRatio,
|
|
284
|
+
setCanvasExtraHeight,
|
|
264
285
|
setActiveSection,
|
|
265
286
|
setElements,
|
|
266
287
|
updateActiveElement,
|
package/src/utils/grid-track.js
CHANGED
|
@@ -1,11 +1,28 @@
|
|
|
1
|
-
// 그리드
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
// 그리드 행/셀 최소 높이(px) - 빈 셀 minHeight(GridDropEngine.getElementStyle), 행 크기 조절, 캔버스 축소 제한 공통
|
|
2
|
+
export const GRID_MIN_ROW_PX = 20;
|
|
3
|
+
|
|
4
|
+
// 그리드 행 트랙별 최소 높이(px) - 기본 GRID_MIN_ROW_PX, 요소가 걸친 행들은 합계가 요소 높이(size.h) 이상이 되도록
|
|
5
|
+
// 여러 행에 걸친 요소: 행별 확장 높이 몫(extraPx)을 먼저 채우고, 모자란 높이는 행 비율(ratio)대로 나눔
|
|
6
|
+
// → 행 크기가 요소를 만족하는 동안에는 각 행 실제 크기보다 작아서 레이아웃에 영향이 없고, 섹션이 작아질 때만 요소 높이를 지킴
|
|
7
|
+
// @param {Array} ratio - 행 비율 (생략 시 균등), extraPx - 행별 확장 높이 몫 (getRowExtraPx 결과, 생략 시 0)
|
|
8
|
+
export function getGridRowMinPx(elements, gridRows, ratio = [], gap = 0, extraPx = []) {
|
|
9
|
+
const mins = Array(gridRows).fill(GRID_MIN_ROW_PX);
|
|
10
|
+
const weight = (i) => (ratio[i] > 0 ? ratio[i] : 1);
|
|
11
|
+
const extra = (i) => extraPx[i] ?? 0;
|
|
12
|
+
|
|
5
13
|
elements.forEach((el) => {
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
14
|
+
const start = el.position.row - 1;
|
|
15
|
+
const end = start + el.size.rowSpan - 1;
|
|
16
|
+
if (start < 0 || end >= gridRows) return;
|
|
17
|
+
|
|
18
|
+
const rows = Array.from({ length: end - start + 1 }, (_, k) => start + k);
|
|
19
|
+
const totalWeight = rows.reduce((sum, i) => sum + weight(i), 0);
|
|
20
|
+
const totalExtra = rows.reduce((sum, i) => sum + extra(i), 0);
|
|
21
|
+
const lackPx = Math.max(0, (el.size.h ?? 0) - gap * (rows.length - 1) - totalExtra);
|
|
22
|
+
|
|
23
|
+
rows.forEach((i) => {
|
|
24
|
+
mins[i] = Math.max(mins[i], extra(i) + (lackPx * weight(i)) / totalWeight);
|
|
25
|
+
});
|
|
9
26
|
});
|
|
10
27
|
return mins;
|
|
11
28
|
}
|
|
@@ -17,36 +34,63 @@ export function toGridTrackSizes(ratio, minPx = []) {
|
|
|
17
34
|
}
|
|
18
35
|
|
|
19
36
|
// fr 비율 → grid-template-rows 값 (마지막 행만 추가 높이를 가져가도록)
|
|
20
|
-
// 마지막 행을 제외한 행은 (컨테이너 높이 - 추가 높이 - gap 합) 기준
|
|
21
|
-
//
|
|
22
|
-
|
|
37
|
+
// 마지막 행을 제외한 행은 (컨테이너 높이 - 추가 높이 - gap 합) 기준 비율 + 위 행으로 넘어간 추가 높이(rowExtraPx)로 고정하고,
|
|
38
|
+
// 남는 높이는 마지막 행(1fr)이 전부 가져감
|
|
39
|
+
// → 캔버스 확장(CanvasContainer.additionalHeight) 시 전체 행에 나눠지지 않고 마지막 행만 늘어나며, 비율(ratio)에는 추가 높이가 섞이지 않음
|
|
40
|
+
export function toGridRowSizes(ratio, minPx = [], gap = 0, extraPx = 0, rowExtraPx = []) {
|
|
23
41
|
const sum = ratio.reduce((acc, r) => acc + r, 0) || 1;
|
|
24
42
|
const fixedPx = extraPx + gap * (ratio.length - 1);
|
|
25
43
|
const toMin = (i) => (typeof minPx[i] === 'string' ? minPx[i] : `${minPx[i] ?? 0}px`);
|
|
44
|
+
const toSize = (r, i) => {
|
|
45
|
+
const base = `(100% - ${fixedPx}px) * ${r / sum}`;
|
|
46
|
+
return rowExtraPx[i] > 0 ? `calc(${base} + ${rowExtraPx[i]}px)` : `calc(${base})`;
|
|
47
|
+
};
|
|
26
48
|
|
|
27
|
-
return ratio
|
|
28
|
-
.map((r, i) => (i === ratio.length - 1 ? `minmax(${toMin(i)}, 1fr)` : `minmax(${toMin(i)}, calc((100% - ${fixedPx}px) * ${r / sum}))`))
|
|
29
|
-
.join(' ');
|
|
49
|
+
return ratio.map((r, i) => (i === ratio.length - 1 ? `minmax(${toMin(i)}, 1fr)` : `minmax(${toMin(i)}, ${toSize(r, i)})`)).join(' ');
|
|
30
50
|
}
|
|
31
51
|
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
52
|
+
// 편집 화면 전용 - 행별 확장 높이 몫(px), 길이 = 행 개수
|
|
53
|
+
// 위 행들은 마지막 행에서 넘어간 몫(gridRowExtra), 마지막 행은 나머지 전부
|
|
54
|
+
// 확장 높이가 없으면(미리보기/마지막 행이 아닌 섹션) 전부 0, gridRowExtra 합계가 확장 높이를 넘으면 비례 축소
|
|
55
|
+
export function getRowExtraPx(rowExtra, gridRows, extraPx) {
|
|
56
|
+
const shares = Array(gridRows).fill(0);
|
|
57
|
+
if (!(extraPx > 0)) return shares;
|
|
58
|
+
|
|
59
|
+
const moved = Array.isArray(rowExtra) && rowExtra.length === gridRows - 1 ? rowExtra.map((v) => (v > 0 ? v : 0)) : [];
|
|
60
|
+
const sum = moved.reduce((acc, v) => acc + v, 0);
|
|
61
|
+
const scale = sum > extraPx ? extraPx / sum : 1;
|
|
62
|
+
moved.forEach((v, i) => (shares[i] = v * scale));
|
|
63
|
+
shares[gridRows - 1] = extraPx - sum * scale;
|
|
64
|
+
return shares;
|
|
36
65
|
}
|
|
37
66
|
|
|
38
|
-
// 그리드 섹션
|
|
39
|
-
//
|
|
40
|
-
export function
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
67
|
+
// 그리드 섹션 grid-template-rows 값 - BaseLayout/GridLayout 공용
|
|
68
|
+
// 확장 높이를 받지 않는 섹션은 일반 fr 비율, 받는 섹션은 비율 + 확장 높이 몫
|
|
69
|
+
export function toSectionRowSizes(ratio, minPx = [], gap = 0, extraPx = 0, rowExtra = []) {
|
|
70
|
+
if (!(extraPx > 0)) return toGridTrackSizes(ratio, minPx);
|
|
71
|
+
return toGridRowSizes(ratio, minPx, gap, extraPx, getRowExtraPx(rowExtra, ratio.length, extraPx));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 행 px(trackPx)가 행 최소 높이와 요소 최소 높이(GridDropEngine.getElementStyle의 minHeight)를 모두 만족하는지
|
|
75
|
+
// rows(0-based 행 인덱스 배열)를 넘기면 해당 행과 그 행에 걸친 요소만 검사 (바뀌지 않은 행의 기존 상태는 무시)
|
|
76
|
+
export function satisfiesGridRowMin(elements, trackPx, gap, minRowPx, rows = null) {
|
|
77
|
+
const EPS = 0.5;
|
|
78
|
+
const checkRows = rows ?? trackPx.map((_, i) => i);
|
|
79
|
+
if (checkRows.some((i) => trackPx[i] < minRowPx - EPS)) return false;
|
|
80
|
+
|
|
81
|
+
return elements.every((el) => {
|
|
82
|
+
const start = el.position.row - 1;
|
|
83
|
+
const end = start + el.size.rowSpan - 1;
|
|
84
|
+
if (!checkRows.some((i) => start <= i && i <= end)) return true;
|
|
46
85
|
|
|
47
|
-
let
|
|
48
|
-
for (let t =
|
|
49
|
-
|
|
86
|
+
let px = gap * (end - start);
|
|
87
|
+
for (let t = start; t <= end; t++) px += trackPx[t] ?? 0;
|
|
88
|
+
return px >= (el.size.h ?? minRowPx) - EPS;
|
|
50
89
|
});
|
|
51
|
-
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 섹션이 BaseLayout의 마지막 행에 걸쳐 있는지 (layoutPosition.row는 0-based) - 캔버스 추가 높이를 받는 섹션
|
|
93
|
+
export function isInLastLayoutRow(section, layoutRowCount) {
|
|
94
|
+
const { row = 0, rowSpan = 1 } = section?.layoutPosition ?? {};
|
|
95
|
+
return row + rowSpan >= (layoutRowCount || 1);
|
|
52
96
|
}
|