polarvo-layout 1.0.88 → 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 +56 -14
- package/src/components/Layout/CanvasContainer.vue +122 -10
- package/src/components/Layout/GridLayout.vue +110 -23
- package/src/components/Layout/PolarLayout.vue +2 -2
- 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 +96 -0
package/package.json
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
<PolarLayout
|
|
12
12
|
:id="preview ? `${key}Preview` : `${key}Layout`"
|
|
13
13
|
:mode="section.mode"
|
|
14
|
-
:style="getSectionStyle(section)"
|
|
14
|
+
:style="getSectionStyle(key, section)"
|
|
15
15
|
:class="[getSectionClass(key, section), activeSection === key && preview ? 'text-blue-400 bg-blue-50' : 'text-gray-400']"
|
|
16
16
|
@click.stop="emit('click:section', key)"
|
|
17
17
|
>
|
|
@@ -19,7 +19,14 @@
|
|
|
19
19
|
{{ key }}
|
|
20
20
|
</div>
|
|
21
21
|
<template v-if="section.mode === 'grid'">
|
|
22
|
-
<GridLayout
|
|
22
|
+
<GridLayout
|
|
23
|
+
:polarvo="polarvo"
|
|
24
|
+
:preview="preview"
|
|
25
|
+
:sectionKey="key"
|
|
26
|
+
:sectionData="section"
|
|
27
|
+
:elementIds="section.elementIds"
|
|
28
|
+
:extraHeight="getSectionExtraHeight(section)"
|
|
29
|
+
/>
|
|
23
30
|
</template>
|
|
24
31
|
|
|
25
32
|
<template v-else>
|
|
@@ -37,11 +44,12 @@
|
|
|
37
44
|
</template>
|
|
38
45
|
|
|
39
46
|
<script setup>
|
|
40
|
-
import { toRefs, computed, onMounted, onUnmounted, ref, inject } from 'vue';
|
|
47
|
+
import { toRefs, computed, onMounted, onUnmounted, ref, inject, unref } from 'vue';
|
|
41
48
|
|
|
42
49
|
import PolarLayout from './PolarLayout.vue';
|
|
43
50
|
import FreeLayout from './FreeLayout.vue';
|
|
44
51
|
import GridLayout from './GridLayout.vue';
|
|
52
|
+
import { getGridRowMinPx, getRowExtraPx, toGridTrackSizes, toGridRowSizes, toSectionRowSizes, isInLastLayoutRow } from '../../utils/grid-track.js';
|
|
45
53
|
|
|
46
54
|
// 우클릭 방지
|
|
47
55
|
// window.oncontextmenu = () => {
|
|
@@ -69,29 +77,63 @@ const layoutType = computed(() => {
|
|
|
69
77
|
});
|
|
70
78
|
const { layoutName, layoutData, activeSection, gapSize, gridNumber, gridRatio } = toRefs(props.polarvo.layout.state);
|
|
71
79
|
const { activeDesign } = toRefs(props.polarvo.display.state);
|
|
80
|
+
const { elements: gridElements } = toRefs(props.polarvo.gridDrop.state);
|
|
72
81
|
const selectedElement = inject('selectedElement');
|
|
73
82
|
const activeEditMode = inject('activeEditMode');
|
|
74
83
|
const activeLeaf = inject('activeLeaf');
|
|
84
|
+
const canvasExtraHeight = inject('canvasExtraHeight', 0); // CanvasContainer 확장 높이 (미리보기는 0)
|
|
85
|
+
|
|
86
|
+
const extraHeight = computed(() => (props.preview ? 0 : (unref(canvasExtraHeight) ?? 0)));
|
|
87
|
+
|
|
88
|
+
// 확장 높이는 마지막 행 섹션만 가져감
|
|
89
|
+
const getSectionExtraHeight = (section) => (isInLastLayoutRow(section, gridRatio.value?.row?.length) ? extraHeight.value : 0);
|
|
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
|
+
};
|
|
75
106
|
|
|
76
107
|
const getBaseStyle = computed(() => {
|
|
77
108
|
return {
|
|
78
109
|
gridTemplateColumns: gridRatio.value?.column.map((ratio) => `${ratio}fr`).join(' '),
|
|
79
|
-
gridTemplateRows:
|
|
110
|
+
gridTemplateRows: getBaseRows(),
|
|
80
111
|
marginRight: layoutType.value === 'no-split' ? '0' : layoutType.value === 'three-column-split' ? '14px' : '8px',
|
|
81
112
|
gap: `${gapSize.value}px`,
|
|
82
113
|
};
|
|
83
114
|
});
|
|
84
115
|
|
|
85
|
-
const getSectionStyle = (section) =>
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
116
|
+
const getSectionStyle = (key, section) => {
|
|
117
|
+
const gridRows = section.config?.gridRows ?? 3;
|
|
118
|
+
const rowRatio = section.config?.gridRowRatio?.length ? section.config.gridRowRatio : Array(gridRows).fill(1);
|
|
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));
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
'--grid-columns': section.config?.gridColumns ?? 3,
|
|
129
|
+
'--grid-rows': gridRows,
|
|
130
|
+
'--grid-gap': `${section.config?.gridGap ?? 5}px`,
|
|
131
|
+
// 비율(gridRowRatio)은 확장 높이 제외 기준 - 확장 높이는 마지막 행 + 위로 넘어간 몫(gridRowExtra)으로 편집 화면에만 붙음
|
|
132
|
+
'--grid-row-sizes': toSectionRowSizes(rowRatio, minPx, gap, extraHeight, section.config?.gridRowExtra),
|
|
133
|
+
'--grid-column-sizes': section.config?.gridColumnRatio?.length ? toGridTrackSizes(section.config.gridColumnRatio) : undefined, // 비어있으면 아예 안 넣어서 CSS 기본값으로 폴백
|
|
134
|
+
position: 'relative',
|
|
135
|
+
};
|
|
136
|
+
};
|
|
95
137
|
|
|
96
138
|
const getSectionClass = (key, section) => ({
|
|
97
139
|
[key]: true,
|
|
@@ -24,8 +24,9 @@
|
|
|
24
24
|
</template>
|
|
25
25
|
|
|
26
26
|
<script setup>
|
|
27
|
-
import { toRefs, ref, computed, watch } from 'vue';
|
|
27
|
+
import { toRefs, ref, computed, watch, provide, onMounted } from 'vue';
|
|
28
28
|
import BaseLayout from './BaseLayout.vue';
|
|
29
|
+
import { GRID_MIN_ROW_PX, isInLastLayoutRow, getRowExtraPx, satisfiesGridRowMin } from '../../utils/grid-track.js';
|
|
29
30
|
|
|
30
31
|
const props = defineProps({
|
|
31
32
|
polarvo: {
|
|
@@ -43,7 +44,13 @@ const DEFAULT_DISPLAY_SIZE = {
|
|
|
43
44
|
|
|
44
45
|
// height 관련 여유 공간 추가하여 계산함 (50, 200)
|
|
45
46
|
const { displaySize, canvasSize } = toRefs(props.polarvo.display.state);
|
|
46
|
-
const { elements,
|
|
47
|
+
const { elements, layoutData, gridRatio, canvasExtraHeight } = toRefs(props.polarvo.layout.state);
|
|
48
|
+
const { setCanvasExtraHeight } = props.polarvo.layout;
|
|
49
|
+
const { elements: gridElements } = toRefs(props.polarvo.gridDrop.state);
|
|
50
|
+
const { elements: freeElements } = toRefs(props.polarvo.freeDrop.state);
|
|
51
|
+
|
|
52
|
+
// 화면에 그리드 섹션이 하나라도 있는지 - 높이 계산 기준 (선택된 섹션 모드와 무관하게 화면 단위로 고정)
|
|
53
|
+
const hasGridSection = computed(() => Object.values(layoutData.value ?? {}).some((section) => section?.mode === 'grid'));
|
|
47
54
|
|
|
48
55
|
const defaultHeight = computed(() => {
|
|
49
56
|
const freeElementsBottom =
|
|
@@ -59,8 +66,8 @@ const defaultHeight = computed(() => {
|
|
|
59
66
|
// 저장된 canvasSize가 없으면 free 요소 기준 계산값만 사용 (grid 섹션은 기본값 750으로 취급)
|
|
60
67
|
if (!canvasSize.value) return freeElementsBottom;
|
|
61
68
|
|
|
62
|
-
//
|
|
63
|
-
if (
|
|
69
|
+
// 그리드 섹션이 있는 화면: 라이브로 정확히 계산할 방법이 없어 저장된 값을 그대로 신뢰
|
|
70
|
+
if (hasGridSection.value) {
|
|
64
71
|
return canvasSize.value.defaultHeight ?? freeElementsBottom;
|
|
65
72
|
}
|
|
66
73
|
|
|
@@ -73,7 +80,7 @@ const defaultHeight = computed(() => {
|
|
|
73
80
|
function restoreAdditionalHeight() {
|
|
74
81
|
if (!canvasSize.value) return 0;
|
|
75
82
|
|
|
76
|
-
if (
|
|
83
|
+
if (hasGridSection.value) {
|
|
77
84
|
return canvasSize.value.additionalHeight ?? 0;
|
|
78
85
|
}
|
|
79
86
|
|
|
@@ -81,7 +88,13 @@ function restoreAdditionalHeight() {
|
|
|
81
88
|
return Math.max(0, savedTotal - defaultHeight.value);
|
|
82
89
|
}
|
|
83
90
|
|
|
84
|
-
|
|
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();
|
|
85
98
|
|
|
86
99
|
// canvasSize(저장된 캔버스 크기)가 바뀌면(=화면 전환) 그 값으로 복원, 없으면 0
|
|
87
100
|
watch(
|
|
@@ -134,14 +147,113 @@ const containerStyle = computed(() => {
|
|
|
134
147
|
|
|
135
148
|
defineExpose({ defaultHeight, additionalHeight, displayHeight });
|
|
136
149
|
|
|
150
|
+
// 확장 높이는 BaseLayout/GridLayout에서 마지막 행에만 붙임
|
|
151
|
+
provide('canvasExtraHeight', additionalHeight);
|
|
152
|
+
|
|
153
|
+
const RESIZE_STEP = 200;
|
|
154
|
+
|
|
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;
|
|
164
|
+
}
|
|
165
|
+
|
|
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);
|
|
173
|
+
|
|
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
|
+
);
|
|
178
|
+
|
|
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
|
+
};
|
|
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 }) => {
|
|
216
|
+
if (section.mode === 'grid') {
|
|
217
|
+
slack = Math.min(slack, getGridShrinkable(getGridSectionInfo(key, section, sectionEl), slack));
|
|
218
|
+
} else {
|
|
219
|
+
const sectionElements = (freeElements.value ?? []).filter((el) => el.section === key && section.elementIds?.includes(el.id));
|
|
220
|
+
const bottom = sectionElements.reduce((max, el) => Math.max(max, (el.position?.y || 0) + (el.size?.h || 0)), 0);
|
|
221
|
+
slack = Math.min(slack, Math.floor(sectionEl.clientHeight - bottom));
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
return Math.max(0, slack);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// 확장: 늘어난 높이는 마지막 행이 전부 가져감 (비율/위로 넘어간 확장분은 그대로)
|
|
137
229
|
function expandContainer() {
|
|
138
|
-
additionalHeight.value
|
|
230
|
+
setCanvasExtraHeight(additionalHeight.value + RESIZE_STEP, {}, true);
|
|
139
231
|
}
|
|
140
232
|
|
|
141
|
-
//
|
|
142
|
-
const
|
|
233
|
+
// 요소 크기/배치·레이아웃·확장 높이가 바뀔 때마다 DOM 반영 후 다시 계산
|
|
234
|
+
const shrinkableHeight = ref(0);
|
|
235
|
+
const updateShrinkableHeight = () => {
|
|
236
|
+
shrinkableHeight.value = getShrinkableHeight(Math.min(RESIZE_STEP, additionalHeight.value));
|
|
237
|
+
};
|
|
238
|
+
watch([displayHeight, gridElements, freeElements, layoutData, gridRatio], updateShrinkableHeight, { deep: true, flush: 'post' });
|
|
239
|
+
onMounted(updateShrinkableHeight);
|
|
240
|
+
|
|
241
|
+
// 축소: 마지막 행 몫부터 줄이고, 모자라면 위로 넘어간 확장분(gridRowExtra)도 줄임 - 확장 높이와 함께 히스토리에 한 번 기록
|
|
242
|
+
// displayHeight(=defaultHeight + additionalHeight)는 항상 defaultHeight 이상, 요소 최소 크기 이상으로 유지
|
|
243
|
+
const canShrink = computed(() => shrinkableHeight.value > 0);
|
|
143
244
|
function shrinkContainer() {
|
|
144
|
-
|
|
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);
|
|
145
257
|
}
|
|
146
258
|
</script>
|
|
147
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,6 +74,7 @@
|
|
|
74
74
|
|
|
75
75
|
<script setup>
|
|
76
76
|
import GridItem from './GridItem.vue';
|
|
77
|
+
import { GRID_MIN_ROW_PX, getGridRowMinPx, toGridTrackSizes, toSectionRowSizes, getRowExtraPx } from '../../utils/grid-track.js';
|
|
77
78
|
|
|
78
79
|
import { ref, toRefs, getCurrentInstance, onMounted, onUnmounted, inject, watch, computed } from 'vue';
|
|
79
80
|
|
|
@@ -98,6 +99,11 @@ const props = defineProps({
|
|
|
98
99
|
type: Array,
|
|
99
100
|
required: true,
|
|
100
101
|
},
|
|
102
|
+
// 캔버스 확장 높이 - 이 섹션이 추가로 가져가는 px (BaseLayout에서 마지막 행 섹션에만 전달, 미리보기는 0)
|
|
103
|
+
extraHeight: {
|
|
104
|
+
type: Number,
|
|
105
|
+
default: 0,
|
|
106
|
+
},
|
|
101
107
|
});
|
|
102
108
|
|
|
103
109
|
const { setActiveDesign, setActiveMenu } = props.polarvo.display;
|
|
@@ -121,7 +127,7 @@ const gridRows = ref(1); // 동적으로 계산
|
|
|
121
127
|
const gridGap = computed(() => props.sectionData?.config?.gridGap ?? 5); // BaseLayout.vue --grid-gap과 동일한 소스/기본값
|
|
122
128
|
|
|
123
129
|
// row, col Resize
|
|
124
|
-
const MIN_ROW_PX =
|
|
130
|
+
const MIN_ROW_PX = GRID_MIN_ROW_PX; // 행 최소 높이(px)
|
|
125
131
|
const MIN_COL_PX = 20; // 열 최소 너비(px)
|
|
126
132
|
|
|
127
133
|
const DRAG_CONFIG = {
|
|
@@ -133,6 +139,22 @@ const DRAG_CONFIG = {
|
|
|
133
139
|
clientKey: 'clientY',
|
|
134
140
|
positionKey: 'y',
|
|
135
141
|
cssVar: '--grid-row-sizes',
|
|
142
|
+
posKey: 'row',
|
|
143
|
+
spanKey: 'rowSpan',
|
|
144
|
+
getMinPx: (el) => el.size?.h ?? MIN_ROW_PX, // GridDropEngine.getElementStyle의 minHeight와 동일
|
|
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),
|
|
136
158
|
},
|
|
137
159
|
col: {
|
|
138
160
|
ref: gridColumns,
|
|
@@ -142,6 +164,12 @@ const DRAG_CONFIG = {
|
|
|
142
164
|
clientKey: 'clientX',
|
|
143
165
|
positionKey: 'x',
|
|
144
166
|
cssVar: '--grid-column-sizes',
|
|
167
|
+
posKey: 'col',
|
|
168
|
+
spanKey: 'colSpan',
|
|
169
|
+
getMinPx: () => MIN_COL_PX,
|
|
170
|
+
getExtraPx: () => Array(gridColumns.value).fill(0),
|
|
171
|
+
toSizes: (ratio) => toGridTrackSizes(ratio), // BaseLayout과 동일한 형식
|
|
172
|
+
toConfig: (ratio) => ratio,
|
|
145
173
|
},
|
|
146
174
|
};
|
|
147
175
|
|
|
@@ -157,7 +185,8 @@ function handleMouseDown(event, axis, index) {
|
|
|
157
185
|
event.preventDefault();
|
|
158
186
|
event.stopPropagation();
|
|
159
187
|
|
|
160
|
-
const { ref, minPx, ratioKey, gridTemplateKey, clientKey, positionKey, cssVar } =
|
|
188
|
+
const { ref, minPx, ratioKey, gridTemplateKey, clientKey, positionKey, cssVar, posKey, spanKey, getMinPx, getExtraPx, toSizes, toConfig } =
|
|
189
|
+
DRAG_CONFIG[axis];
|
|
161
190
|
|
|
162
191
|
dragState.value.axis = axis;
|
|
163
192
|
dragState.value.activeIdx = index;
|
|
@@ -168,6 +197,7 @@ function handleMouseDown(event, axis, index) {
|
|
|
168
197
|
|
|
169
198
|
let dragSnapShot = null; // 드래그 시작 시 스냅샷
|
|
170
199
|
let liveRatio = null; // 미리보기 중 계산된 최종 fr 배열 (stopDrag에서 커밋)
|
|
200
|
+
let liveExtras = null; // 미리보기 중 계산된 트랙별 확장 높이 px (stopDrag에서 커밋)
|
|
171
201
|
let pendingClient = null;
|
|
172
202
|
let rafId = null;
|
|
173
203
|
|
|
@@ -207,32 +237,72 @@ function handleMouseDown(event, axis, index) {
|
|
|
207
237
|
|
|
208
238
|
const configured = props.sectionData?.config?.[ratioKey];
|
|
209
239
|
const baselineRatio =
|
|
210
|
-
|
|
240
|
+
Array.isArray(configured) && configured.length === ref.value && configured.every((v) => typeof v === 'number' && v > 0)
|
|
211
241
|
? [...configured]
|
|
212
242
|
: Array(ref.value).fill(1);
|
|
213
243
|
|
|
214
244
|
const trackPx = getComputedStyle(container)
|
|
215
245
|
[gridTemplateKey].split(' ')
|
|
246
|
+
.slice(0, ref.value)
|
|
216
247
|
.map((v) => parseFloat(v));
|
|
217
248
|
|
|
218
|
-
//
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
const
|
|
249
|
+
const prevIdx = index - 1; // 드래그 행 위쪽 행 인덱스
|
|
250
|
+
const nextIdx = index; // 드래그 행 인덱스
|
|
251
|
+
|
|
252
|
+
// 트랙 px = 비율 몫(basePx) + 확장 높이 몫(extraPx)
|
|
253
|
+
const baselineExtras = getExtraPx();
|
|
254
|
+
const basePx = trackPx.map((v, i) => v - baselineExtras[i]);
|
|
255
|
+
|
|
256
|
+
// 1px당 Fr 계산 - 드래그로 바뀌는 두 트랙의 비율 몫 기준 (확장 높이는 비율과 무관)
|
|
257
|
+
const frPerPx = (baselineRatio[prevIdx] + baselineRatio[nextIdx]) / (basePx[prevIdx] + basePx[nextIdx]);
|
|
258
|
+
|
|
259
|
+
// 요소 최소 크기 때문에 두 트랙이 가져야 하는 최소 px (확장 높이 포함한 트랙 전체 기준)
|
|
260
|
+
const { prevMinPx, nextMinPx } = _getElementMinPx(trackPx, prevIdx, nextIdx);
|
|
224
261
|
|
|
225
262
|
dragSnapShot = {
|
|
226
263
|
container,
|
|
227
264
|
baselineRatio,
|
|
265
|
+
baselineExtras,
|
|
228
266
|
frPerPx,
|
|
229
|
-
|
|
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),
|
|
230
273
|
};
|
|
231
274
|
|
|
232
275
|
document.addEventListener('mousemove', _startDragMove);
|
|
233
276
|
document.addEventListener('mouseup', _stopDrag);
|
|
234
277
|
}
|
|
235
278
|
|
|
279
|
+
// 드래그로 바뀌는 트랙(prev/next) 중 하나에만 걸친 요소 기준으로, 각 트랙이 가져야 하는 최소 px 계산
|
|
280
|
+
// (두 트랙 모두에 걸친 요소는 합이 그대로라 제약 없음)
|
|
281
|
+
function _getElementMinPx(trackPx, prevIdx, nextIdx) {
|
|
282
|
+
const gap = gridGap.value;
|
|
283
|
+
let prevMinPx = 0;
|
|
284
|
+
let nextMinPx = 0;
|
|
285
|
+
|
|
286
|
+
filteredElements.value.forEach((el) => {
|
|
287
|
+
const start = el.position[posKey] - 1; // 0-based 트랙 인덱스
|
|
288
|
+
const end = start + el.size[spanKey] - 1;
|
|
289
|
+
const coversPrev = start <= prevIdx && prevIdx <= end;
|
|
290
|
+
const coversNext = start <= nextIdx && nextIdx <= end;
|
|
291
|
+
if (coversPrev === coversNext) return;
|
|
292
|
+
|
|
293
|
+
const targetIdx = coversPrev ? prevIdx : nextIdx;
|
|
294
|
+
// 요소가 걸친 나머지 트랙 + gap 만큼은 이미 확보되어 있음
|
|
295
|
+
let otherPx = gap * (end - start);
|
|
296
|
+
for (let t = start; t <= end; t++) if (t !== targetIdx) otherPx += trackPx[t];
|
|
297
|
+
|
|
298
|
+
const required = getMinPx(el) - otherPx;
|
|
299
|
+
if (coversPrev) prevMinPx = Math.max(prevMinPx, required);
|
|
300
|
+
else nextMinPx = Math.max(nextMinPx, required);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
return { prevMinPx, nextMinPx };
|
|
304
|
+
}
|
|
305
|
+
|
|
236
306
|
function _startDragMove(e) {
|
|
237
307
|
if (!dragSnapShot) return;
|
|
238
308
|
pendingClient = e[clientKey];
|
|
@@ -243,26 +313,42 @@ function handleMouseDown(event, axis, index) {
|
|
|
243
313
|
rafId = requestAnimationFrame(() => {
|
|
244
314
|
rafId = null;
|
|
245
315
|
|
|
246
|
-
const { baselineRatio, frPerPx,
|
|
316
|
+
const { baselineRatio, baselineExtras, frPerPx, prevShrinkPx, nextShrinkPx, prevBaseShrinkPx, nextBaseShrinkPx, container } =
|
|
317
|
+
dragSnapShot;
|
|
247
318
|
const prevIdx = index - 1; // 드래그 행 위쪽 행 인덱스
|
|
248
319
|
const nextIdx = index; // 드래그 행 인덱스
|
|
249
320
|
|
|
250
|
-
// 순수 이동량
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
//
|
|
255
|
-
|
|
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
|
+
}
|
|
256
337
|
|
|
257
338
|
// 미리보기용
|
|
258
339
|
const preview = [...baselineRatio];
|
|
259
|
-
preview[prevIdx] +=
|
|
260
|
-
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;
|
|
261
346
|
|
|
262
347
|
liveRatio = preview;
|
|
348
|
+
liveExtras = extras;
|
|
263
349
|
|
|
264
350
|
// 화면에만 즉시 반영
|
|
265
|
-
container.style.setProperty(cssVar, preview
|
|
351
|
+
container.style.setProperty(cssVar, toSizes(preview, extras));
|
|
266
352
|
});
|
|
267
353
|
}
|
|
268
354
|
function _stopDrag() {
|
|
@@ -271,12 +357,13 @@ function handleMouseDown(event, axis, index) {
|
|
|
271
357
|
dragState.value.activeIdx = null;
|
|
272
358
|
|
|
273
359
|
if (liveRatio) {
|
|
274
|
-
setSectionConfig(ratioKey, liveRatio);
|
|
360
|
+
setSectionConfig(ratioKey, toConfig(liveRatio, liveExtras));
|
|
275
361
|
}
|
|
276
362
|
|
|
277
363
|
startPos.value = { x: 0, y: 0 };
|
|
278
364
|
dragSnapShot = null;
|
|
279
365
|
liveRatio = null;
|
|
366
|
+
liveExtras = null;
|
|
280
367
|
pendingClient = null;
|
|
281
368
|
rafId = null;
|
|
282
369
|
|
|
@@ -17,9 +17,9 @@ const props = defineProps({
|
|
|
17
17
|
<style scoped>
|
|
18
18
|
.polar-grid {
|
|
19
19
|
display: grid;
|
|
20
|
-
grid-template-columns: var(--grid-column-sizes, repeat(var(--grid-columns, 3), 1fr));
|
|
20
|
+
grid-template-columns: var(--grid-column-sizes, repeat(var(--grid-columns, 3), minmax(0, 1fr)));
|
|
21
21
|
/* grid-template-rows: repeat(var(--grid-rows, 3), 1fr); */
|
|
22
|
-
grid-template-rows: var(--grid-row-sizes, repeat(var(--grid-rows, 3), 1fr));
|
|
22
|
+
grid-template-rows: var(--grid-row-sizes, repeat(var(--grid-rows, 3), minmax(0, 1fr)));
|
|
23
23
|
gap: var(--grid-gap, 5px);
|
|
24
24
|
width: 100%;
|
|
25
25
|
height: 100%;
|
|
@@ -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,
|
|
@@ -0,0 +1,96 @@
|
|
|
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
|
+
|
|
13
|
+
elements.forEach((el) => {
|
|
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
|
+
});
|
|
26
|
+
});
|
|
27
|
+
return mins;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// fr 비율 → grid-template 값
|
|
31
|
+
// min을 auto(콘텐츠 크기) 대신 명시값으로 고정 - 콘텐츠 크기가 트랙을 고정하면 남은 공간이 다른 트랙으로 재분배됨
|
|
32
|
+
export function toGridTrackSizes(ratio, minPx = []) {
|
|
33
|
+
return ratio.map((r, i) => `minmax(${minPx[i] ?? 0}px, ${r}fr)`).join(' ');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// fr 비율 → grid-template-rows 값 (마지막 행만 추가 높이를 가져가도록)
|
|
37
|
+
// 마지막 행을 제외한 행은 (컨테이너 높이 - 추가 높이 - gap 합) 기준 비율 + 위 행으로 넘어간 추가 높이(rowExtraPx)로 고정하고,
|
|
38
|
+
// 남는 높이는 마지막 행(1fr)이 전부 가져감
|
|
39
|
+
// → 캔버스 확장(CanvasContainer.additionalHeight) 시 전체 행에 나눠지지 않고 마지막 행만 늘어나며, 비율(ratio)에는 추가 높이가 섞이지 않음
|
|
40
|
+
export function toGridRowSizes(ratio, minPx = [], gap = 0, extraPx = 0, rowExtraPx = []) {
|
|
41
|
+
const sum = ratio.reduce((acc, r) => acc + r, 0) || 1;
|
|
42
|
+
const fixedPx = extraPx + gap * (ratio.length - 1);
|
|
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
|
+
};
|
|
48
|
+
|
|
49
|
+
return ratio.map((r, i) => (i === ratio.length - 1 ? `minmax(${toMin(i)}, 1fr)` : `minmax(${toMin(i)}, ${toSize(r, i)})`)).join(' ');
|
|
50
|
+
}
|
|
51
|
+
|
|
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;
|
|
65
|
+
}
|
|
66
|
+
|
|
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;
|
|
85
|
+
|
|
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;
|
|
89
|
+
});
|
|
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);
|
|
96
|
+
}
|