polarvo-layout 1.0.81 → 1.0.83
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,5 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div
|
|
3
|
+
ref="canvasRoot"
|
|
3
4
|
:id="preview ? 'previewLayout' : 'baseLayout'"
|
|
4
5
|
class="grid h-full p-[10px] bg-transparent rounded-lg"
|
|
5
6
|
:class="[layoutType + '-layout', `grid-cols-${gridNumber?.column}`, { 'mb-4': layoutType === 'three-column-split' }]"
|
|
@@ -36,7 +37,7 @@
|
|
|
36
37
|
</template>
|
|
37
38
|
|
|
38
39
|
<script setup>
|
|
39
|
-
import { toRefs, computed } from 'vue';
|
|
40
|
+
import { toRefs, computed, onMounted, onUnmounted, ref, inject } from 'vue';
|
|
40
41
|
|
|
41
42
|
import PolarLayout from './PolarLayout.vue';
|
|
42
43
|
import FreeLayout from './FreeLayout.vue';
|
|
@@ -66,8 +67,11 @@ const layoutType = computed(() => {
|
|
|
66
67
|
.toLowerCase()
|
|
67
68
|
.substring(1);
|
|
68
69
|
});
|
|
69
|
-
|
|
70
70
|
const { layoutName, layoutData, activeSection, gapSize, gridNumber, gridRatio } = toRefs(props.polarvo.layout.state);
|
|
71
|
+
const { activeDesign } = toRefs(props.polarvo.display.state);
|
|
72
|
+
const selectedElement = inject('selectedElement');
|
|
73
|
+
const activeEditMode = inject('activeEditMode');
|
|
74
|
+
const activeLeaf = inject('activeLeaf');
|
|
71
75
|
|
|
72
76
|
const getBaseStyle = computed(() => {
|
|
73
77
|
return {
|
|
@@ -95,6 +99,73 @@ const getSectionClass = (key, section) => ({
|
|
|
95
99
|
'section-active': activeSection.value === key,
|
|
96
100
|
disabled: activeSection.value !== key && !props.preview,
|
|
97
101
|
});
|
|
102
|
+
|
|
103
|
+
const canvasRoot = ref(null);
|
|
104
|
+
|
|
105
|
+
function isEligible(e) {
|
|
106
|
+
// 디자인 설정 패널이 열려있지 않으면 무시
|
|
107
|
+
if (activeDesign.value !== 'design') return null;
|
|
108
|
+
// 활성화된 요소가 없거나 activeMode인 경우 무시
|
|
109
|
+
if (!selectedElement.value?.id || activeEditMode.value) return null;
|
|
110
|
+
|
|
111
|
+
// 현재 마우스 위치가 활성화된 요소 내부에 있는지 확인F
|
|
112
|
+
const activeElRoot = document.getElementById(selectedElement.value.id);
|
|
113
|
+
if (!activeElRoot?.contains(e.target)) return null;
|
|
114
|
+
|
|
115
|
+
return activeElRoot;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let hoveredEl = null;
|
|
119
|
+
|
|
120
|
+
function setHovered(el) {
|
|
121
|
+
if (hoveredEl === el) return;
|
|
122
|
+
hoveredEl?.removeAttribute('data-hovering');
|
|
123
|
+
hoveredEl = el;
|
|
124
|
+
hoveredEl?.setAttribute('data-hovering', 'true');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function onHover(e) {
|
|
128
|
+
const activeElRoot = isEligible(e);
|
|
129
|
+
if (!activeElRoot) {
|
|
130
|
+
setHovered(null);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const leafEl = e.target.closest('[data-leaf]');
|
|
135
|
+
const ownerEl = e.target.closest('[data-style-owner]');
|
|
136
|
+
if (!leafEl || !ownerEl) {
|
|
137
|
+
setHovered(null);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
setHovered(leafEl);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function onClick(e) {
|
|
145
|
+
const activeElRoot = isEligible(e);
|
|
146
|
+
if (!activeElRoot) return;
|
|
147
|
+
|
|
148
|
+
const leafEl = e.target.closest('[data-leaf]');
|
|
149
|
+
const ownerEl = e.target.closest('[data-style-owner]');
|
|
150
|
+
if (!leafEl || !ownerEl) return;
|
|
151
|
+
|
|
152
|
+
// leaf 내부의 실제 버튼/링크/폼 제출 등 기본 동작 막기
|
|
153
|
+
e.preventDefault();
|
|
154
|
+
e.stopPropagation();
|
|
155
|
+
|
|
156
|
+
const currentType = activeElRoot.id.split('_')?.[0]
|
|
157
|
+
if (ownerEl.dataset.styleOwner == currentType) return;
|
|
158
|
+
activeLeaf.value = { owner: ownerEl.dataset.styleOwner, leaf: leafEl.dataset.leaf, elementId: activeElRoot.id };
|
|
159
|
+
}
|
|
160
|
+
onMounted(() => {
|
|
161
|
+
canvasRoot.value.addEventListener('mouseover', onHover);
|
|
162
|
+
canvasRoot.value.addEventListener('click', onClick, true);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
onUnmounted(() => {
|
|
166
|
+
canvasRoot.value?.removeEventListener('mouseover', onHover);
|
|
167
|
+
canvasRoot.value?.removeEventListener('click', onClick, true);
|
|
168
|
+
});
|
|
98
169
|
</script>
|
|
99
170
|
|
|
100
171
|
<style scoped>
|
|
@@ -118,4 +189,19 @@ const getSectionClass = (key, section) => ({
|
|
|
118
189
|
pointer-events: none;
|
|
119
190
|
opacity: 0.6;
|
|
120
191
|
}
|
|
192
|
+
|
|
193
|
+
:deep(.pointer-events-auto [data-leaf]) {
|
|
194
|
+
pointer-events: auto;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
:deep([data-leaf][data-hovering]) {
|
|
198
|
+
border-radius: 2px;
|
|
199
|
+
box-shadow:
|
|
200
|
+
0 0 0 1px #fff,
|
|
201
|
+
0 0 0 3px #6366f1;
|
|
202
|
+
background-color: rgb(99 102 241 / 0.06);
|
|
203
|
+
transition:
|
|
204
|
+
box-shadow 120ms ease,
|
|
205
|
+
background-color 120ms ease;
|
|
206
|
+
}
|
|
121
207
|
</style>
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div
|
|
3
3
|
:id="item.id"
|
|
4
|
-
class="absolute user-select-none"
|
|
4
|
+
class="absolute user-select-none p-1"
|
|
5
5
|
:style="itemStyle"
|
|
6
|
-
:class="{
|
|
6
|
+
:class="{
|
|
7
|
+
'z-50': item.id === activeId,
|
|
8
|
+
'rounded-sm bg-white shadow-sm outline outline-1 outline-blue-500 outline-offset-[-1px]': item.id === activeId,
|
|
9
|
+
'bg-white': item.type === 'chart',
|
|
10
|
+
}"
|
|
7
11
|
@mousedown.stop="activeEditMode ? null : handleMouseDown($event, item.id)"
|
|
8
12
|
>
|
|
9
13
|
<!-- 크기-->
|
|
10
|
-
<div v-if="item.id === activeId" class="absolute bg-blue-
|
|
14
|
+
<div v-if="item.id === activeId" class="absolute right-0 z-20 bg-blue-500 px-2 py-1 text-sm text-white shadow-sm mr-1">
|
|
11
15
|
가로: {{ Math.round(size.w) }} 세로: {{ Math.round(size.h) }}
|
|
12
16
|
</div>
|
|
13
17
|
|
|
@@ -16,22 +20,30 @@
|
|
|
16
20
|
<div
|
|
17
21
|
v-for="handle in handles"
|
|
18
22
|
:key="handle.name"
|
|
19
|
-
:class="`handle absolute
|
|
23
|
+
:class="`handle absolute z-20 pointer-events-auto rounded-[2px] border border-blue-500 bg-white shadow-sm transition-transform duration-100 hover:scale-125 hover:bg-blue-500 ${handle.name}`"
|
|
20
24
|
@mousedown="startResize($event, handle.name)"
|
|
21
25
|
></div>
|
|
22
|
-
<div class="flex flex-row m-1 gap-
|
|
26
|
+
<div class="flex flex-row m-1 gap-2">
|
|
23
27
|
<!-- 잠금 토글-->
|
|
24
|
-
<div class="z-20 pointer-events-auto cursor-pointer" @click="toggleLock()">
|
|
28
|
+
<div class="z-20 pointer-events-auto cursor-pointer ring-1 ring-slate-200 rounded-md" @click="toggleLock()">
|
|
25
29
|
<LockIcon v-if="item.isLocked" />
|
|
26
30
|
<UnlockIcon v-else />
|
|
27
31
|
</div>
|
|
28
32
|
<!-- 편집 토글-->
|
|
29
33
|
<div class="z-20 pointer-events-auto cursor-pointer">
|
|
30
34
|
<div v-if="item.type === 'inputForm'" @click="toggleEditMode">
|
|
31
|
-
<div
|
|
35
|
+
<div
|
|
36
|
+
v-if="activeEditMode"
|
|
37
|
+
class="rounded-sm bg-red-100 px-2 py-1 text-sm text-red-400 shadow-sm ring-1 ring-red-200 hover:opacity-80 animate-pulse"
|
|
38
|
+
>
|
|
32
39
|
편집모드 on
|
|
33
40
|
</div>
|
|
34
|
-
<div
|
|
41
|
+
<div
|
|
42
|
+
v-else
|
|
43
|
+
class="rounded-sm bg-gray-100 px-2 py-1 text-sm text-gray-600 shadow-sm ring-1 ring-slate-200 transition-colors hover:bg-gray-200"
|
|
44
|
+
>
|
|
45
|
+
편집모드 off
|
|
46
|
+
</div>
|
|
35
47
|
</div>
|
|
36
48
|
</div>
|
|
37
49
|
</div>
|
|
@@ -71,7 +71,8 @@ const activeEditMode = inject('activeEditMode');
|
|
|
71
71
|
|
|
72
72
|
const isHovable = (item) => {
|
|
73
73
|
if (!item || props.preview) return true;
|
|
74
|
-
return activeElement.value?.id === item.id && ['chart'].includes(item.type);
|
|
74
|
+
// return activeElement.value?.id === item.id && ['chart'].includes(item.type);
|
|
75
|
+
return activeElement.value?.id === item.id;
|
|
75
76
|
};
|
|
76
77
|
|
|
77
78
|
let _isElementSwitching = false;
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div
|
|
3
3
|
:id="item.id ?? `${item.row}-${item.col}`"
|
|
4
|
+
class="p-1"
|
|
4
5
|
:class="
|
|
5
6
|
item.id
|
|
6
|
-
? [
|
|
7
|
-
|
|
7
|
+
? [
|
|
8
|
+
'relative bg-gray-200 user-select-none',
|
|
9
|
+
{ 'z-50 rounded-sm bg-white shadow-sm outline outline-2 outline-blue-500 outline-offset-[-2px]': item.id === activeId },
|
|
10
|
+
]
|
|
11
|
+
: 'flex items-center justify-center rounded-sm border border-slate-200 bg-slate-50'
|
|
8
12
|
"
|
|
9
13
|
:style="itemStyle"
|
|
10
14
|
@mousedown.stop="activeEditMode ? null : handleMouseDown($event, item)"
|
|
@@ -14,22 +18,30 @@
|
|
|
14
18
|
<!-- 리사이즈 핸들 -->
|
|
15
19
|
<div v-if="item.id === activeId" class="absolute inset-0 pointer-events-none">
|
|
16
20
|
<div
|
|
17
|
-
class="absolute
|
|
21
|
+
class="absolute bottom-0 right-0 z-20 h-2 w-2 cursor-se-resize rounded-[2px] border border-blue-500 bg-white pointer-events-auto shadow-sm transition-transform duration-100 hover:scale-125 hover:bg-blue-500"
|
|
18
22
|
@mousedown="startResize($event, item.id)"
|
|
19
23
|
></div>
|
|
20
|
-
<div class="flex gap-
|
|
24
|
+
<div class="flex gap-2 m-1">
|
|
21
25
|
<!-- 잠금 토글-->
|
|
22
|
-
<div class="z-20 pointer-events-auto cursor-pointer" @click="toggleLock()">
|
|
26
|
+
<div class="z-20 pointer-events-auto cursor-pointer ring-1 ring-slate-200 rounded-md" @click="toggleLock()">
|
|
23
27
|
<LockIcon v-if="item.isLocked" />
|
|
24
28
|
<UnlockIcon v-else />
|
|
25
29
|
</div>
|
|
26
30
|
<!-- 편집 토글-->
|
|
27
31
|
<div class="z-20 pointer-events-auto cursor-pointer">
|
|
28
32
|
<div v-if="item.type === 'inputForm'" @click="toggleEditMode">
|
|
29
|
-
<div
|
|
33
|
+
<div
|
|
34
|
+
v-if="activeEditMode"
|
|
35
|
+
class="rounded-sm bg-red-100 px-2 py-1 text-sm text-red-400 shadow-sm ring-1 ring-red-200 hover:opacity-80 animate-pulse"
|
|
36
|
+
>
|
|
30
37
|
편집모드 on
|
|
31
38
|
</div>
|
|
32
|
-
<div
|
|
39
|
+
<div
|
|
40
|
+
v-else
|
|
41
|
+
class="rounded-sm bg-gray-100 px-2 py-1 text-sm text-gray-600 shadow-sm ring-1 ring-slate-200 transition-colors hover:bg-gray-200"
|
|
42
|
+
>
|
|
43
|
+
편집모드 off
|
|
44
|
+
</div>
|
|
33
45
|
</div>
|
|
34
46
|
</div>
|
|
35
47
|
</div>
|
|
@@ -51,7 +63,7 @@
|
|
|
51
63
|
|
|
52
64
|
<template v-else>
|
|
53
65
|
<div
|
|
54
|
-
class="
|
|
66
|
+
class="flex h-8 w-8 cursor-pointer items-center justify-center rounded-full border border-slate-200 bg-white text-sm text-slate-400 shadow-sm transition-colors hover:border-blue-300 hover:bg-blue-50 hover:text-blue-500"
|
|
55
67
|
>
|
|
56
68
|
+
|
|
57
69
|
</div>
|
|
@@ -110,7 +110,8 @@ const activeEditMode = inject('activeEditMode');
|
|
|
110
110
|
|
|
111
111
|
const isHovable = (item) => {
|
|
112
112
|
if (!item || props.preview) return true;
|
|
113
|
-
return activeElement.value?.id === item.id && ['chart'].includes(item.type);
|
|
113
|
+
// return activeElement.value?.id === item.id && ['chart'].includes(item.type);
|
|
114
|
+
return activeElement.value?.id === item.id;
|
|
114
115
|
};
|
|
115
116
|
|
|
116
117
|
import { omit, cloneDeep, debounce } from 'lodash-es';
|