polarvo-layout 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +70 -0
  2. package/package.json +41 -0
  3. package/src/components/FastMenu/DesignFastMenu.vue +83 -0
  4. package/src/components/FastMenu/DisplayFastMenu.vue +65 -0
  5. package/src/components/FastMenu/HistoryFastMenu.vue +12 -0
  6. package/src/components/FastMenu/LayoutFastMenu.vue +98 -0
  7. package/src/components/IconBar/IconBar.vue +220 -0
  8. package/src/components/Layout/BaseLayout.vue +119 -0
  9. package/src/components/Layout/CanvasContainer.vue +45 -0
  10. package/src/components/Layout/FreeLayout.vue +246 -0
  11. package/src/components/Layout/GridLayout.vue +146 -0
  12. package/src/components/Layout/PolarLayout.vue +5 -0
  13. package/src/components/MiniMenu/LayoutMiniMenu.vue +62 -0
  14. package/src/components/SideBar/ElementSideBar.vue +76 -0
  15. package/src/components/SideBar/LayoutSettingSideBar.vue +90 -0
  16. package/src/components/SideBar/LayoutSideBar.vue +89 -0
  17. package/src/configs/index.js +23 -0
  18. package/src/configs/resources/displayResource.js +27 -0
  19. package/src/configs/resources/dropResource.js +23 -0
  20. package/src/configs/resources/elementResource.js +54 -0
  21. package/src/configs/resources/index.js +10 -0
  22. package/src/configs/resources/layoutResource.js +82 -0
  23. package/src/core/engines/DisplayEngine.js +171 -0
  24. package/src/core/engines/FreeDropEngine.js +701 -0
  25. package/src/core/engines/GridDropEngine.js +461 -0
  26. package/src/core/engines/HistoryEngine.js +69 -0
  27. package/src/core/engines/LayoutEngine.js +240 -0
  28. package/src/core/managers/ApiManager.js +21 -0
  29. package/src/core/managers/EngineManager.js +648 -0
  30. package/src/core/managers/EventBus.js +277 -0
  31. package/src/icons/action/LockIcon.vue +52 -0
  32. package/src/icons/action/RedoIcon.vue +16 -0
  33. package/src/icons/action/UndoIcon.vue +16 -0
  34. package/src/icons/action/UnlockIcon.vue +52 -0
  35. package/src/icons/display/DesktopIcon.vue +18 -0
  36. package/src/icons/display/MobileIcon.vue +16 -0
  37. package/src/icons/display/TabletIcon.vue +16 -0
  38. package/src/icons/history/RedoIcon.vue +16 -0
  39. package/src/icons/history/UndoIcon.vue +16 -0
  40. package/src/icons/layout/LeftRightSplitIcon.vue +6 -0
  41. package/src/icons/layout/MainBottomSplitIcon.vue +7 -0
  42. package/src/icons/layout/MainTopSplitIcon.vue +7 -0
  43. package/src/icons/layout/NoSplitIcon.vue +5 -0
  44. package/src/icons/layout/ThreeColumnSplitIcon.vue +7 -0
  45. package/src/icons/layout/ThreeRowSplitIcon.vue +7 -0
  46. package/src/icons/layout/TopBottomSplitIcon.vue +6 -0
  47. package/src/icons/menu/AddIcon.vue +23 -0
  48. package/src/icons/menu/LayoutIcon.vue +17 -0
  49. package/src/icons/menu/ThemeIcon.vue +21 -0
  50. package/src/index.js +32 -0
  51. package/src/library/DisplayLibrary.js +122 -0
  52. package/src/library/FreeDropLibrary.js +152 -0
  53. package/src/library/GridDropLibrary.js +151 -0
  54. package/src/library/HistoryLibrary.js +61 -0
  55. package/src/library/LayoutLibrary.js +199 -0
  56. package/src/library/index.js +50 -0
  57. package/src/styles.scss +5 -0
  58. package/src/utils/directives/click-outside.js +14 -0
  59. package/src/utils/directives/index.js +1 -0
  60. package/src/utils/index.js +1 -0
@@ -0,0 +1,119 @@
1
+ <template>
2
+ <div
3
+ :id="preview ? 'previewLayout' : 'baseLayout'"
4
+ class="grid h-full p-2 bg-transparent rounded-lg"
5
+ :class="[
6
+ layoutType + '-layout',
7
+ `grid-cols-${gridNumber.column}`,
8
+ 'mb-4' ? layoutType === 'three-column-split' : '',
9
+ ]"
10
+ :style="getBaseStyle"
11
+ >
12
+ <template v-for="(section, key) in layoutData" :key="key">
13
+ <PolarLayout
14
+ :id="preview ? `${key}Preview` : `${key}Layout`"
15
+ :style="getSectionStyle(section)"
16
+ :class="[
17
+ getSectionClass(key, section),
18
+ activeSection === key && preview ? 'text-blue-400 bg-blue-50' : 'text-gray-400',
19
+ ]"
20
+ @click.stop="emit('click:section', key)"
21
+ >
22
+ <div v-if="preview" class="absolute text-sm top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
23
+ {{ key }}
24
+ </div>
25
+ <template v-if="section.mode === 'grid'">
26
+ <GridLayout :polavo="polavo" :preview="preview" :sectionKey="key" />
27
+ </template>
28
+
29
+ <template v-else>
30
+ <FreeLayout
31
+ :polavo="polavo"
32
+ :preview="preview"
33
+ :sectionKey="key"
34
+ :isActive="activeSection === key"
35
+ />
36
+ </template>
37
+ </PolarLayout>
38
+ </template>
39
+ </div>
40
+ </template>
41
+
42
+ <script setup>
43
+ import { toRefs, reactive, computed } from 'vue';
44
+
45
+ import PolarLayout from './PolarLayout.vue';
46
+ import FreeLayout from './FreeLayout.vue';
47
+ import GridLayout from './GridLayout.vue';
48
+
49
+ // 우클릭 방지
50
+ window.oncontextmenu = () => {
51
+ return false;
52
+ };
53
+
54
+ const emit = defineEmits(['click:section']);
55
+ const props = defineProps({
56
+ polavo: {
57
+ type: Object,
58
+ required: true,
59
+ },
60
+ preview: {
61
+ type: Boolean,
62
+ default: false,
63
+ },
64
+ });
65
+
66
+ const layoutType = computed(() => {
67
+ if (layoutName.value.eng == null) return 'no-split';
68
+ return layoutName.value.eng
69
+ .replace(/([A-Z])/g, '-$1')
70
+ .toLowerCase()
71
+ .substring(1);
72
+ });
73
+
74
+ const getBaseStyle = computed(() => {
75
+ return {
76
+ gridTemplateColumns: gridRatio.value.column.map((ratio) => `${ratio}%`).join(' '),
77
+ gridTemplateRows: gridRatio.value.row.map((ratio) => `${ratio}%`).join(' '),
78
+ marginRight: layoutType.value === 'no-split' ? '0' : layoutType.value === 'three-column-split' ? '14px' : '8px',
79
+ gap: `${gapSize.value}px`,
80
+ };
81
+ });
82
+
83
+ const getSectionStyle = (section) => ({
84
+ '--grid-columns': section.config?.gridColumns || 3,
85
+ '--grid-gap': `${section.config?.gridGap || 5}px`,
86
+ position: 'relative',
87
+ });
88
+
89
+ const getSectionClass = (key, section) => ({
90
+ [key]: true,
91
+ [section.mode]: true,
92
+ 'section-active': activeSection.value === key,
93
+ disabled: activeSection.value != key && !props.preview,
94
+ });
95
+
96
+ const { layoutName, layoutData, activeSection, gapSize, gridNumber, gridRatio } = toRefs(props.polavo.layout.state);
97
+ </script>
98
+
99
+ <style scoped>
100
+ .main-bottom-split-layout :deep(.section1),
101
+ .main-top-split-layout :deep(.section3) {
102
+ grid-column: span 2 / span 2;
103
+ }
104
+
105
+ [id^='section'] {
106
+ border: 1px solid transparent;
107
+ border-color: gray;
108
+ }
109
+
110
+ [id^='section'].section-active {
111
+ border-width: 1px;
112
+ border-color: #3b82f6; /* blue-500 */
113
+ }
114
+
115
+ [id^='section'].disabled {
116
+ pointer-events: none;
117
+ opacity: 0.6;
118
+ }
119
+ </style>
@@ -0,0 +1,45 @@
1
+ <template>
2
+ <div class="w-full h-full p-4">
3
+ <div id="canvas-container" class="border rounded-md w-full h-full bg-white" :style="containerStyle">
4
+ <BaseLayout ref="baseLayoutRef" :polavo="polavo" />
5
+ </div>
6
+ </div>
7
+ </template>
8
+
9
+ <script setup>
10
+ import { toRefs, inject, computed, provide } from 'vue';
11
+ import BaseLayout from './BaseLayout.vue';
12
+
13
+ const props = defineProps({
14
+ polavo: {
15
+ type: Object,
16
+ required: true,
17
+ },
18
+ });
19
+
20
+ const { displaySize } = toRefs(props.polavo.display.state);
21
+ const gridSize = computed(() => displaySize.value.gridSize || 16);
22
+
23
+ const containerStyle = computed(() => {
24
+ return {
25
+ aspectRatio: displaySize.value.aspectRatio,
26
+ width: `${displaySize.value.px}px`,
27
+ margin: `0 auto`,
28
+ transform: `scale(${displaySize.value.percent / 100})`,
29
+ transformOrigin: 'top center',
30
+ backgroundImage: `
31
+ linear-gradient(rgba(0,0,0,0.05) 1px, transparent 1px),
32
+ linear-gradient(90deg, rgba(0,0,0,0.05) 1px, transparent 1px)
33
+ `,
34
+ backgroundSize: `${gridSize.value}px ${gridSize.value}px`,
35
+ };
36
+ });
37
+
38
+ const { elements } = toRefs(props.polavo.layout.state);
39
+
40
+ // 전체 elements
41
+ provide('elements', elements)
42
+
43
+ </script>
44
+
45
+ <style scoped lang="scss"></style>
@@ -0,0 +1,246 @@
1
+ <template>
2
+ <div>
3
+ <div
4
+ v-for="(item, index) in elements"
5
+ :key="index"
6
+ :id="item.id"
7
+ class="absolute user-select-none"
8
+ :style="getElementStyle(item)"
9
+ :class="{ 'z-50': item.id === activeId, 'bg-white border-2 border-blue-400': item.id === activeId && !preview }"
10
+ @mousedown.stop="startDrag($event, item.id)"
11
+ >
12
+ <!-- 리사이즈 핸들 -->
13
+ <div v-if="item.id === activeId && !preview" class="absolute inset-0 pointer-events-none">
14
+ <div
15
+ v-for="handle in handles"
16
+ :key="handle.name"
17
+ :class="`handle absolute bg-blue-400 z-10 pointer-events-auto hover:bg-blue-700 ${handle.name}`"
18
+ @mousedown="startResize($event, handle.name)"
19
+ ></div>
20
+ <div class="absolute z-10 pointer-events-auto cursor-pointer" @click="toggleLock()">
21
+ <LockIcon v-if="item.isLocked" />
22
+ <UnlockIcon v-else />
23
+ </div>
24
+ </div>
25
+ <component
26
+ v-if="!preview"
27
+ :is="dynamicComponent(item.type)"
28
+ :id="`${sectionKey}-${index}`"
29
+ :ref="(el) => setComponentRef(el, item.id)"
30
+ v-bind="item"
31
+ class="hover:opacity-80 pointer-events-none"
32
+ />
33
+ </div>
34
+
35
+ <!-- 가이드라인 -->
36
+ <div v-if="isActive && !preview">
37
+ <div
38
+ v-if="guides.x !== null"
39
+ class="guide absolute z-10 pointer-events-none vertical"
40
+ :style="{ left: `${guides.x}px` }"
41
+ ></div>
42
+ <div
43
+ v-if="guides.x !== null"
44
+ class="guide absolute z-10 pointer-events-none vertical"
45
+ :style="{ left: `${guides.x + guides.w}px` }"
46
+ ></div>
47
+ <div
48
+ v-if="guides.y !== null"
49
+ class="guide absolute z-10 pointer-events-none horizontal"
50
+ :style="{ top: `${guides.y}px` }"
51
+ ></div>
52
+ <div
53
+ v-if="guides.y !== null"
54
+ class="guide absolute z-10 pointer-events-none horizontal"
55
+ :style="{ top: `${guides.y + guides.h}px` }"
56
+ ></div>
57
+ </div>
58
+ </div>
59
+ </template>
60
+
61
+ <script setup>
62
+ import LockIcon from '../../icons/action/LockIcon.vue';
63
+ import UnlockIcon from '../../icons/action/UnlockIcon.vue';
64
+
65
+ import {
66
+ toRefs,
67
+ defineAsyncComponent,
68
+ markRaw,
69
+ getCurrentInstance,
70
+ onMounted,
71
+ onUnmounted,
72
+ inject,
73
+ watch,
74
+ toRaw,
75
+ computed,
76
+ } from 'vue';
77
+
78
+ const props = defineProps({
79
+ polavo: {
80
+ type: Object,
81
+ required: true,
82
+ },
83
+ preview: {
84
+ type: Boolean,
85
+ default: false,
86
+ },
87
+ sectionKey: {
88
+ type: String,
89
+ required: true,
90
+ },
91
+ isActive: {
92
+ type: Boolean,
93
+ default: false,
94
+ },
95
+ });
96
+
97
+ const { setActiveDesign } = props.polavo.display;
98
+ const { updateActiveElement } = props.polavo.layout;
99
+ const { elements, activeId, handles, guides, activeElement } = toRefs(props.polavo.freeDrop.state);
100
+ const { getElementStyle, toggleLock, startDrag, startResize } = props.polavo.freeDrop;
101
+
102
+ function dynamicComponent(elName) {
103
+ const modules = import.meta.glob('@/components/elements/*.vue');
104
+ const matched = Object.keys(modules).find((path) => {
105
+ return path.includes(elName);
106
+ });
107
+
108
+ if (!matched) {
109
+ return markRaw(defineAsyncComponent(async () => ({ template: '<span style="display:none"></span>' })));
110
+ }
111
+
112
+ return markRaw(defineAsyncComponent(modules[matched]));
113
+ }
114
+
115
+ function setComponentRef(el, elementId) {
116
+ if (el) {
117
+ props.polavo.components.register('elements', `${props.sectionKey}-${elementId}`, el);
118
+ } else {
119
+ props.polavo.components.unregister('elements', `${props.sectionKey}-${elementId}`);
120
+ }
121
+ }
122
+
123
+ const selectedElement = inject('selectedElement');
124
+
125
+ watch(
126
+ () => activeElement.value?.id,
127
+ (newId, oldId) => {
128
+ if (newId === oldId) return;
129
+ if (!newId) {
130
+ setActiveDesign(null)
131
+ }
132
+ selectedElement.value = structuredClone(toRaw(activeElement.value));
133
+ },
134
+ { immediate: true }
135
+ );
136
+
137
+ import { omit } from 'lodash-es';
138
+ // 특정 키 값을 제외하고 변경 감지
139
+ const selectedElementForWatch = computed(() => omit(selectedElement.value, ['position', 'size', 'id']));
140
+
141
+ watch(
142
+ () => selectedElementForWatch.value,
143
+ (newElement) => {
144
+ updateActiveElement(selectedElement.value?.id, newElement)
145
+ },
146
+ { deep: true }
147
+ );
148
+
149
+ onMounted(() => {
150
+ props.polavo.components.register('sections', props.sectionKey, getCurrentInstance());
151
+ });
152
+
153
+ onUnmounted(() => {
154
+ props.polavo.components.unregister('sections', props.sectionKey);
155
+ });
156
+ </script>
157
+
158
+ <style lang="scss" scoped>
159
+ .handle {
160
+ // 모서리
161
+ &.nw {
162
+ top: -4px;
163
+ left: -4px;
164
+ width: 8px;
165
+ height: 8px;
166
+ cursor: nw-resize;
167
+ }
168
+
169
+ &.ne {
170
+ top: -4px;
171
+ right: -4px;
172
+ width: 8px;
173
+ height: 8px;
174
+ cursor: ne-resize;
175
+ }
176
+
177
+ &.sw {
178
+ bottom: -4px;
179
+ left: -4px;
180
+ width: 8px;
181
+ height: 8px;
182
+ cursor: sw-resize;
183
+ }
184
+
185
+ &.se {
186
+ bottom: -4px;
187
+ right: -4px;
188
+ width: 8px;
189
+ height: 8px;
190
+ cursor: se-resize;
191
+ }
192
+
193
+ // 가장자리
194
+ &.n {
195
+ top: -4px;
196
+ left: 50%;
197
+ transform: translateX(-50%);
198
+ width: 8px;
199
+ height: 8px;
200
+ cursor: n-resize;
201
+ }
202
+
203
+ &.s {
204
+ bottom: -4px;
205
+ left: 50%;
206
+ transform: translateX(-50%);
207
+ width: 8px;
208
+ height: 8px;
209
+ cursor: s-resize;
210
+ }
211
+
212
+ &.w {
213
+ top: 50%;
214
+ left: -4px;
215
+ transform: translateY(-50%);
216
+ width: 8px;
217
+ height: 8px;
218
+ cursor: w-resize;
219
+ }
220
+
221
+ &.e {
222
+ top: 50%;
223
+ right: -4px;
224
+ transform: translateY(-50%);
225
+ width: 8px;
226
+ height: 8px;
227
+ cursor: e-resize;
228
+ }
229
+ }
230
+
231
+ .guide {
232
+ &.vertical {
233
+ top: 0;
234
+ bottom: 0;
235
+ width: 1px;
236
+ border-left: 1px dashed #60a5fa; // border-blue-400
237
+ }
238
+
239
+ &.horizontal {
240
+ left: 0;
241
+ right: 0;
242
+ height: 1px;
243
+ border-top: 1px dashed #60a5fa; // border-blue-400
244
+ }
245
+ }
246
+ </style>
@@ -0,0 +1,146 @@
1
+ <template>
2
+ <div
3
+ v-for="(item, index) in elements"
4
+ :key="index"
5
+ :id="item.id"
6
+ class="relative bg-gray-200"
7
+ :class="{
8
+ 'z-50': activeElement?.id === item.id,
9
+ 'bg-white border-2 border-blue-400': activeElement?.id === item.id && !preview,
10
+ }"
11
+ :style="getElementStyle('element', item)"
12
+ @mousedown.stop="startDrag($event, item.id)"
13
+ >
14
+ <!-- 리사이즈 핸들 -->
15
+ <div v-if="activeElement?.id === item.id && !preview">
16
+ <div
17
+ class="absolute w-4 h-4 bottom-0 right-0 bg-blue-400 z-10 cursor-se-resize"
18
+ @mousedown="startResize($event, item.id)"
19
+ ></div>
20
+ <div class="absolute z-10 pointer-events-auto cursor-pointer" @click="toggleLock()">
21
+ <LockIcon v-if="item.isLocked" />
22
+ <UnlockIcon v-else />
23
+ </div>
24
+ </div>
25
+
26
+ <component
27
+ v-if="!preview"
28
+ :is="dynamicComponent(item.type)"
29
+ :id="`${sectionKey}-${index}`"
30
+ :ref="(el) => setComponentRef(el, item.id)"
31
+ v-bind="item"
32
+ class="hover:opacity-80 pointer-events-none"
33
+ />
34
+ </div>
35
+
36
+ <!-- 빈 그리드 설정 -->
37
+ <div
38
+ v-for="(cell, index) in emptyData[sectionKey]"
39
+ :key="index"
40
+ class="flex items-center justify-center bg-gray-100 border border-gray-300"
41
+ :style="getElementStyle('empty', cell)"
42
+ @mouseenter="detectHoverCell(cell)"
43
+ >
44
+ <div
45
+ v-show="!preview"
46
+ class="text-sm w-8 h-8 rounded-full bg-gray-300 flex items-center justify-center text-gray-500 hover:bg-blue-200 hover:text-white transition-colors cursor-pointer"
47
+ @click="setActiveCell('new', cell)"
48
+ >
49
+ +
50
+ </div>
51
+ </div>
52
+ </template>
53
+
54
+ <script setup>
55
+ import LockIcon from '../../icons/action/LockIcon.vue';
56
+ import UnlockIcon from '../../icons/action/UnlockIcon.vue';
57
+
58
+ import {
59
+ toRefs,
60
+ defineAsyncComponent,
61
+ markRaw,
62
+ getCurrentInstance,
63
+ onMounted,
64
+ onUnmounted,
65
+ inject,
66
+ watch,
67
+ toRaw,
68
+ computed,
69
+ } from 'vue';
70
+ const emit = defineEmits(['click:element']);
71
+ const props = defineProps({
72
+ polavo: {
73
+ type: Object,
74
+ required: true,
75
+ },
76
+ preview: {
77
+ type: Boolean,
78
+ default: false,
79
+ },
80
+ sectionKey: {
81
+ type: String,
82
+ required: true,
83
+ },
84
+ });
85
+
86
+ const { setActiveDesign } = props.polavo.display;
87
+ const { updateActiveElement } = props.polavo.layout;
88
+ const { elements, emptyData, activeCell, activeElement } = toRefs(props.polavo.gridDrop.state);
89
+ const { getElementStyle, setActiveCell, toggleLock, startDrag, detectHoverCell, startResize } = props.polavo.gridDrop;
90
+
91
+ function dynamicComponent(elName) {
92
+ const modules = import.meta.glob('@/components/elements/*.vue');
93
+ const matched = Object.keys(modules).find((path) => {
94
+ return path.includes(elName);
95
+ });
96
+
97
+ if (!matched) {
98
+ return markRaw(defineAsyncComponent(async () => ({ template: '<span style="display:none"></span>' })));
99
+ }
100
+
101
+ return markRaw(defineAsyncComponent(modules[matched]));
102
+ }
103
+
104
+ function setComponentRef(el, elementId) {
105
+ if (el) {
106
+ props.polavo.components.register('elements', `${props.sectionKey}-${elementId}`, el);
107
+ } else {
108
+ props.polavo.components.unregister('elements', `${props.sectionKey}-${elementId}`);
109
+ }
110
+ }
111
+
112
+ const selectedElement = inject('selectedElement');
113
+
114
+
115
+ watch(
116
+ () => activeElement.value?.id,
117
+ (newId, oldId) => {
118
+ if (newId === oldId) return;
119
+ if (!newId) {
120
+ setActiveDesign(null)
121
+ }
122
+ selectedElement.value = structuredClone(toRaw(activeElement.value));
123
+ },
124
+ { immediate: true }
125
+ );
126
+
127
+ import { omit } from 'lodash-es';
128
+ // 특정 키 값을 제외하고 변경 감지
129
+ const selectedElementForWatch = computed(() => omit(selectedElement.value, ['position', 'size', 'id']));
130
+
131
+ watch(
132
+ () => selectedElementForWatch.value,
133
+ (newElement) => {
134
+ updateActiveElement(selectedElement.value?.id, newElement)
135
+ },
136
+ { deep: true }
137
+ );
138
+
139
+ onMounted(() => {
140
+ props.polavo.components.register('sections', props.sectionKey, getCurrentInstance());
141
+ });
142
+
143
+ onUnmounted(() => {
144
+ props.polavo.components.unregister('sections', props.sectionKey);
145
+ });
146
+ </script>
@@ -0,0 +1,5 @@
1
+ <template>
2
+ <div id="polarLayout" class="w-full h-full rounded-md">
3
+ <slot></slot>
4
+ </div>
5
+ </template>
@@ -0,0 +1,62 @@
1
+ <template>
2
+ <div id="mini-menu">
3
+ <div class="text-sm text-gray-600 font-semibold mb-1">레이아웃 형태</div>
4
+ <div class="text-sm text-gray-400 mx-3 my-2">단일화면</div>
5
+ <div class="grid gap-1 grid-cols-1 mx-1 mb-3">
6
+ <div
7
+ class="item flex flex-col items-center gap-1 p-2 border border-gray-200 rounded-md bg-white cursor-pointer hover:border-gray-400"
8
+ :class="{ active: layoutName.eng == 'NoSplit' }"
9
+ @click="setLayoutName('NoSplit')"
10
+ >
11
+ <component class="text-gray-600" :is="layoutSource.noSplit.icon"></component>
12
+ <span class="text-xs">{{ layoutSource.noSplit.name }}</span>
13
+ </div>
14
+ </div>
15
+ <div class="text-sm text-gray-400 mx-3 my-2">분할화면</div>
16
+ <div class="grid gap-1 grid-cols-2 mx-1 mb-3">
17
+ <div
18
+ v-for="(layout, key) in layoutSource.split"
19
+ :key="key"
20
+ class="item flex flex-col items-center gap-1 p-2 border border-gray-200 rounded-md bg-white cursor-pointer hover:border-gray-400"
21
+ :class="{ active: layoutName.eng == key }"
22
+ @click="setLayoutName(key)"
23
+ >
24
+ <component class="text-gray-600" :is="layout.icon"></component>
25
+ <span class="text-xs">{{ layout.name }}</span>
26
+ </div>
27
+ </div>
28
+ </div>
29
+ </template>
30
+
31
+ <script setup>
32
+ import { toRefs, watch } from 'vue';
33
+
34
+ const props = defineProps({
35
+ polavo: {
36
+ type: Object,
37
+ required: true,
38
+ },
39
+ });
40
+ const { layoutName, layoutSource } = toRefs(props.polavo.layout.state);
41
+ const { setLayoutName } = props.polavo.layout;
42
+
43
+ const emits = defineEmits(['click:layout']);
44
+ watch(
45
+ () => layoutName.value,
46
+ () => {
47
+ emits('click:layout');
48
+ },
49
+ { deep: true }
50
+ );
51
+ </script>
52
+
53
+ <style scoped>
54
+ .item.active {
55
+ background-color: #eff6ff; /* bg-blue-100 */
56
+ border: 1px solid #3b82f6; /* border-blue-500 */
57
+ color: #3b82f6; /* text-blue-500 */
58
+ }
59
+ .item.active svg {
60
+ color: #3b82f6; /* text-blue-500 */
61
+ }
62
+ </style>
@@ -0,0 +1,76 @@
1
+ <template>
2
+ <div class="gap-4 space-y-4">
3
+ <div
4
+ v-for="(item, index) in allElements"
5
+ :key="index"
6
+ class="flex flex-col items-center justify-center border border-gray-200 rounded-lg p-2 hover:bg-gray-50 transition-colors cursor-pointer active:scale-95"
7
+ @mousedown.stop="addElementToFree($event, item.elName)"
8
+ @click="addElementToGrid(item.elName)"
9
+ >
10
+ <img :src="item.imgurl" alt="" class="w-24 h-auto rounded" />
11
+ <h4 class="text-sm">{{ item.elName }}</h4>
12
+ </div>
13
+ </div>
14
+ </template>
15
+
16
+ <script setup>
17
+ import { ref, onBeforeMount } from 'vue';
18
+
19
+ const props = defineProps({
20
+ polavo: {
21
+ type: Object,
22
+ required: true,
23
+ },
24
+ urlBase: {
25
+ type: String,
26
+ required: false,
27
+ default: '',
28
+ },
29
+ });
30
+ const { addElement: addElementToFree } = props.polavo.freeDrop;
31
+ const { addElement: addElementToGrid } = props.polavo.gridDrop;
32
+
33
+ const allElements = ref([]);
34
+
35
+ /** 프로젝트 폴더에서 요소 컴포넌트 주소를 맞춰줘야 정상 작동합니다.
36
+ * (현재 /src/components/elements/ 내부에 모든 요소 컴포넌트가 있어야 함) */
37
+ const modules = import.meta.glob('@/components/elements/*.vue');
38
+ async function getElements() {
39
+ // 특정 컴포넌트는 제외
40
+ const excludedElements = [
41
+ 'button',
42
+ 'labelInput',
43
+ 'labelSelect',
44
+ 'dataList',
45
+ 'loginForm',
46
+ 'multiCheckRadio',
47
+ 'labelInputRows',
48
+ 'refLabel',
49
+ ];
50
+
51
+ const elementsData = Object.keys(modules)
52
+ .filter((path) => {
53
+ const elName = path.split('/').pop().replace('.vue', '');
54
+ return !excludedElements.includes(elName);
55
+ })
56
+ .map((path) => {
57
+ const elName = path.split('/').pop().replace('.vue', '');
58
+
59
+ // 확장자 조건 설정
60
+ const ext = elName === 'dataList' ? 'gif' : 'png';
61
+ let result = {
62
+ elName,
63
+ imgurl: new URL(`../../assets/el_img/${elName}.${ext}`, props.urlBase),
64
+ // component: markRaw(defineAsyncComponent(modules[path]))
65
+ };
66
+
67
+ return result;
68
+ });
69
+
70
+ allElements.value = elementsData;
71
+ }
72
+
73
+ onBeforeMount(() => {
74
+ getElements();
75
+ });
76
+ </script>