polarvo-layout 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polarvo-layout",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "type": "module",
5
5
  "author": "unigence <unigencelab@gmail.com>",
6
6
  "repository": {
@@ -24,7 +24,7 @@
24
24
  min="600"
25
25
  max="2400"
26
26
  step="50"
27
- :value="displaySize.px"
27
+ :value="displaySize?.px"
28
28
  class="border-b-2 pr-1 border-white focus:outline-none focus:border-b-2 focus:border-blue-500"
29
29
  @change="setDisplaySize('px', $event.target.value)"
30
30
  />
@@ -36,7 +36,7 @@
36
36
  min="0"
37
37
  max="100"
38
38
  step="10"
39
- :value="displaySize.percent"
39
+ :value="displaySize?.percent"
40
40
  class="border-b-2 pr-1 border-white focus:outline-none focus:border-b-2 focus:border-blue-500"
41
41
  @change="setDisplaySize('percent', $event.target.value)"
42
42
  />
@@ -46,7 +46,7 @@
46
46
  </template>
47
47
 
48
48
  <script setup>
49
- import { toRefs } from 'vue';
49
+ import { toRefs, onMounted } from 'vue';
50
50
 
51
51
  const props = defineProps({
52
52
  polarvo: {
@@ -56,6 +56,7 @@ const props = defineProps({
56
56
  });
57
57
  const { displayMode, displaySize } = toRefs(props.polarvo.display.state);
58
58
  const { setDisplayMode, setDisplaySize } = props.polarvo.display;
59
+
59
60
  </script>
60
61
 
61
62
  <style scoped>
@@ -1,12 +1,27 @@
1
1
  <template>
2
2
  <!-- editor - title right -->
3
3
  <div id="fast-menu" class="flex gap-2 text-gray-400">
4
- <UndoIcon class="size-5 cursor-pointer hover:opacity-70 active:text-blue-600" />
5
- <RedoIcon class="size-5 cursor-pointer hover:opacity-70 active:text-blue-600" />
4
+ <button @click="undo" :class="undoable ? 'cursor-pointer hover:opacity-70 ' : 'cursor-not-allowed opacity-50'" :disabled="!undoable">
5
+ <UndoIcon class="size-5 active:text-blue-600" />
6
+ </button>
7
+ <button @click="redo" :class="redoable ? 'cursor-pointer hover:opacity-70' : 'cursor-not-allowed opacity-50'" :disabled="!redoable">
8
+ <RedoIcon class="size-5 active:text-blue-600" />
9
+ </button>
6
10
  </div>
7
11
  </template>
8
12
 
9
13
  <script setup>
10
- import UndoIcon from '../../icons//history/UndoIcon.vue';
14
+ import { toRefs } from 'vue';
15
+ import UndoIcon from '../../icons/history/UndoIcon.vue';
11
16
  import RedoIcon from '../../icons/history/RedoIcon.vue';
17
+
18
+ const props = defineProps({
19
+ polarvo: {
20
+ type: Object,
21
+ required: true,
22
+ },
23
+ });
24
+
25
+ const { undoable, redoable } = toRefs(props.polarvo.history.state);
26
+ const { undo, redo } = props.polarvo.history;
12
27
  </script>
@@ -2,7 +2,7 @@
2
2
  <div
3
3
  :id="preview ? 'previewLayout' : 'baseLayout'"
4
4
  class="grid h-full p-2 bg-transparent rounded-lg"
5
- :class="[layoutType + '-layout', `grid-cols-${gridNumber.column}`, 'mb-4' ? layoutType === 'three-column-split' : '']"
5
+ :class="[layoutType + '-layout', `grid-cols-${gridNumber?.column}`, 'mb-4' ? layoutType === 'three-column-split' : '']"
6
6
  :style="getBaseStyle"
7
7
  >
8
8
  <template v-for="(section, key) in layoutData" :key="key">
@@ -34,7 +34,7 @@
34
34
  </template>
35
35
 
36
36
  <script setup>
37
- import { toRefs, computed } from 'vue';
37
+ import { toRefs, computed, onUnmounted } from 'vue';
38
38
 
39
39
  import PolarLayout from './PolarLayout.vue';
40
40
  import FreeLayout from './FreeLayout.vue';
@@ -67,8 +67,8 @@ const layoutType = computed(() => {
67
67
 
68
68
  const getBaseStyle = computed(() => {
69
69
  return {
70
- gridTemplateColumns: gridRatio.value.column.map((ratio) => `${ratio}%`).join(' '),
71
- gridTemplateRows: gridRatio.value.row.map((ratio) => `${ratio}%`).join(' '),
70
+ gridTemplateColumns: gridRatio.value?.column.map((ratio) => `${ratio}%`).join(' '),
71
+ gridTemplateRows: gridRatio.value?.row.map((ratio) => `${ratio}%`).join(' '),
72
72
  marginRight: layoutType.value === 'no-split' ? '0' : layoutType.value === 'three-column-split' ? '14px' : '8px',
73
73
  gap: `${gapSize.value}px`,
74
74
  };
@@ -88,6 +88,8 @@ const getSectionClass = (key, section) => ({
88
88
  });
89
89
 
90
90
  const { layoutName, layoutData, activeSection, gapSize, gridNumber, gridRatio } = toRefs(props.polarvo.layout.state);
91
+
92
+
91
93
  </script>
92
94
 
93
95
  <style scoped>
@@ -7,7 +7,7 @@
7
7
  </template>
8
8
 
9
9
  <script setup>
10
- import { toRefs, inject, computed, provide } from 'vue';
10
+ import { toRefs, computed, provide } from 'vue';
11
11
  import BaseLayout from './BaseLayout.vue';
12
12
 
13
13
  const props = defineProps({
@@ -18,14 +18,14 @@ const props = defineProps({
18
18
  });
19
19
 
20
20
  const { displaySize } = toRefs(props.polarvo.display.state);
21
- const gridSize = computed(() => displaySize.value.gridSize || 16);
21
+ const gridSize = computed(() => displaySize.value?.gridSize || 16);
22
22
 
23
23
  const containerStyle = computed(() => {
24
24
  return {
25
- aspectRatio: displaySize.value.aspectRatio,
26
- width: `${displaySize.value.px}px`,
25
+ aspectRatio: displaySize.value?.aspectRatio,
26
+ width: `${displaySize.value?.px}px`,
27
27
  margin: `0 auto`,
28
- transform: `scale(${displaySize.value.percent / 100})`,
28
+ transform: `scale(${displaySize.value?.percent / 100})`,
29
29
  transformOrigin: 'top center',
30
30
  backgroundImage: `
31
31
  linear-gradient(rgba(0,0,0,0.05) 1px, transparent 1px),
@@ -38,8 +38,7 @@ const containerStyle = computed(() => {
38
38
  const { elements } = toRefs(props.polarvo.layout.state);
39
39
 
40
40
  // 전체 elements
41
- provide('elements', elements)
42
-
41
+ provide('elements', elements);
43
42
  </script>
44
43
 
45
44
  <style scoped lang="scss"></style>
@@ -30,7 +30,7 @@
30
30
  class="hover:opacity-80 pointer-events-none"
31
31
  />
32
32
  </div>
33
-
33
+
34
34
  <!-- 가이드라인 -->
35
35
  <div v-if="isActive">
36
36
  <div v-if="guides.x !== null" class="guide absolute z-10 pointer-events-none vertical" :style="{ left: `${guides.x}px` }"></div>
@@ -109,6 +109,7 @@ function setComponentRef(el, elementId) {
109
109
 
110
110
  const selectedElement = inject('selectedElement');
111
111
 
112
+ import { omit, cloneDeep } from 'lodash-es';
112
113
  watch(
113
114
  () => activeElement.value?.id,
114
115
  (newId, oldId) => {
@@ -116,12 +117,11 @@ watch(
116
117
  if (!newId) {
117
118
  setActiveDesign(null);
118
119
  }
119
- selectedElement.value = structuredClone(toRaw(activeElement.value));
120
+ // selectedElement.value = structuredClone(toRaw(activeElement.value));
121
+ selectedElement.value = cloneDeep(activeElement.value);
120
122
  },
121
- { immediate: true }
122
123
  );
123
124
 
124
- import { omit } from 'lodash-es';
125
125
  // 특정 키 값을 제외하고 변경 감지
126
126
  const selectedElementForWatch = computed(() => omit(selectedElement.value, ['position', 'size', 'id']));
127
127
 
@@ -1,51 +1,51 @@
1
1
  <template>
2
- <div
3
- v-for="(item, index) in filteredElements"
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 class="absolute w-4 h-4 bottom-0 right-0 bg-blue-400 z-10 cursor-se-resize" @mousedown="startResize($event, item.id)"></div>
17
- <div class="absolute z-10 pointer-events-auto cursor-pointer" @click="toggleLock()">
18
- <LockIcon v-if="item.isLocked" />
19
- <UnlockIcon v-else />
2
+ <template v-if="!preview">
3
+ <div
4
+ v-for="(item, index) in filteredElements"
5
+ :key="index"
6
+ :id="item.id"
7
+ class="relative bg-gray-200"
8
+ :class="{
9
+ 'z-50': activeElement?.id === item.id,
10
+ 'bg-white border-2 border-blue-400': activeElement?.id === item.id,
11
+ }"
12
+ :style="getElementStyle('element', item)"
13
+ @mousedown.stop="startDrag($event, item.id)"
14
+ >
15
+ <!-- 리사이즈 핸들 -->
16
+ <div v-if="activeElement?.id === item.id">
17
+ <div class="absolute w-4 h-4 bottom-0 right-0 bg-blue-400 z-10 cursor-se-resize" @mousedown="startResize($event, item.id)"></div>
18
+ <div class="absolute z-10 pointer-events-auto cursor-pointer" @click="toggleLock()">
19
+ <LockIcon v-if="item.isLocked" />
20
+ <UnlockIcon v-else />
21
+ </div>
20
22
  </div>
21
- </div>
22
23
 
23
- <component
24
- v-if="!preview"
25
- :is="dynamicComponent(item.type)"
26
- :id="`${sectionKey}-${index}`"
27
- :ref="(el) => setComponentRef(el, item.id)"
28
- v-bind="item"
29
- class="hover:opacity-80 pointer-events-none"
30
- />
31
- </div>
24
+ <component
25
+ :is="dynamicComponent(item.type)"
26
+ :id="`${sectionKey}-${index}`"
27
+ :ref="(el) => setComponentRef(el, item.id)"
28
+ v-bind="item"
29
+ class="hover:opacity-80 pointer-events-none"
30
+ />
31
+ </div>
32
32
 
33
- <!-- 빈 그리드 설정 -->
34
- <div
35
- v-for="(cell, index) in emptyData[sectionKey]"
36
- :key="index"
37
- class="flex items-center justify-center bg-gray-100 border border-gray-300"
38
- :style="getElementStyle('empty', cell)"
39
- @mouseenter="detectHoverCell(cell)"
40
- >
33
+ <!-- 빈 그리드 설정 -->
41
34
  <div
42
- v-show="!preview"
43
- 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"
44
- @click="setActiveCell('new', cell)"
35
+ v-for="(cell, index) in emptyData[sectionKey]"
36
+ :key="index"
37
+ class="flex items-center justify-center bg-gray-100 border border-gray-300"
38
+ :style="getElementStyle('empty', cell)"
39
+ @mouseenter="detectHoverCell(cell)"
45
40
  >
46
- +
41
+ <div
42
+ 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"
43
+ @click="setActiveElement({ position: cell }, 'click')"
44
+ >
45
+ +
46
+ </div>
47
47
  </div>
48
- </div>
48
+ </template>
49
49
  </template>
50
50
 
51
51
  <script setup>
@@ -70,13 +70,13 @@ const props = defineProps({
70
70
  elementIds: {
71
71
  type: Array,
72
72
  required: true,
73
- }
73
+ },
74
74
  });
75
75
 
76
76
  const { setActiveDesign } = props.polarvo.display;
77
77
  const { updateActiveElement } = props.polarvo.layout;
78
78
  const { elements, emptyData, activeCell, activeElement } = toRefs(props.polarvo.gridDrop.state);
79
- const { getElementStyle, setActiveCell, toggleLock, startDrag, detectHoverCell, startResize } = props.polarvo.gridDrop;
79
+ const { getElementStyle, setActiveElement, toggleLock, startDrag, detectHoverCell, startResize } = props.polarvo.gridDrop;
80
80
  const filteredElements = computed(() => {
81
81
  return elements.value.filter((el) => props.elementIds?.includes(el.id) && el.section === props.sectionKey);
82
82
  });
@@ -104,6 +104,7 @@ function setComponentRef(el, elementId) {
104
104
 
105
105
  const selectedElement = inject('selectedElement');
106
106
 
107
+ import { omit, cloneDeep } from 'lodash-es';
107
108
  watch(
108
109
  () => activeElement.value?.id,
109
110
  (newId, oldId) => {
@@ -111,12 +112,12 @@ watch(
111
112
  if (!newId) {
112
113
  setActiveDesign(null);
113
114
  }
114
- selectedElement.value = structuredClone(toRaw(activeElement.value));
115
+ // selectedElement.value = structuredClone(toRaw(activeElement.value));
116
+ selectedElement.value = cloneDeep(activeElement.value);
115
117
  },
116
118
  { immediate: true }
117
119
  );
118
120
 
119
- import { omit } from 'lodash-es';
120
121
  // 특정 키 값을 제외하고 변경 감지
121
122
  const selectedElementForWatch = computed(() => omit(selectedElement.value, ['position', 'size', 'id']));
122
123
 
@@ -49,8 +49,8 @@
49
49
  id="gap-size"
50
50
  min="0"
51
51
  max="12"
52
- :value="gapSize"
53
- @change="setGapSize($event.target.value)"
52
+ :value="activeData?.config?.gridGap"
53
+ @change="setSectionConfig('gap', $event.target.value)"
54
54
  />
55
55
  </div>
56
56
  </div>
@@ -81,8 +81,8 @@ const props = defineProps({
81
81
  required: true,
82
82
  },
83
83
  });
84
- const { activeData, gapSize, gridNumber, gridRatio } = toRefs(props.polarvo.layout.state);
85
- const { setGapSize, setRatio, setSectionMode, setSectionConfig } = props.polarvo.layout;
84
+ const { activeData } = toRefs(props.polarvo.layout.state);
85
+ const { setSectionMode, setSectionConfig } = props.polarvo.layout;
86
86
 
87
87
  const sectionMode = computed(() => activeData.value?.mode);
88
88
  </script>
@@ -27,8 +27,8 @@
27
27
  </div>
28
28
 
29
29
  <div class="px-4 py-0">
30
- <div class="flex flex-row items-center gap-2 mb-2" v-if="gridNumber.column > 1">
31
- <template v-for="column in gridNumber.column" :key="column">
30
+ <div class="flex flex-row items-center gap-2 mb-2" v-if="gridNumber?.column > 1">
31
+ <template v-for="column in gridNumber?.column" :key="column">
32
32
  <label class="text-sm w-12 text-gray-500" :for="`col-ratio-${column}`" v-if="column === 1">열비율</label>
33
33
  <input
34
34
  class="text-sm w-12 border-b-2 border-white focus:outline-none focus:border-b-2 focus:border-blue-500"
@@ -40,8 +40,8 @@
40
40
  </template>
41
41
  </div>
42
42
 
43
- <div class="flex flex-row items-center gap-2 mb-2" v-if="gridNumber.row > 1">
44
- <template v-for="row in gridNumber.row" :key="row">
43
+ <div class="flex flex-row items-center gap-2 mb-2" v-if="gridNumber?.row > 1">
44
+ <template v-for="row in gridNumber?.row" :key="row">
45
45
  <label class="text-sm w-12 text-gray-500" :for="`row-ratio-${row}`" v-if="row === 1">행비율</label>
46
46
  <input
47
47
  class="text-sm w-12 border-b-2 border-white focus:outline-none focus:border-b-2 focus:border-blue-500"
@@ -3,13 +3,8 @@ import { resources } from './resources/index.js';
3
3
  const createDefaultConfig = (overrides = {}) => {
4
4
  return {
5
5
  initialState: {
6
- gridSize: 16,
7
- gridNumber: { column: 1, row: 1 },
8
6
  elements: [], // 요소들
9
- // layoutData: null, // 레이아웃 데이터 - 캐시용
10
7
  activeSection: null,
11
- // activeData: null,
12
- activeId: null,
13
8
  },
14
9
  resource: {
15
10
  display: resources.display,
@@ -5,8 +5,8 @@ class DisplayEngine {
5
5
 
6
6
  this.activeMenu = null; // 현재 활성화된 메뉴
7
7
  this.activeDesign = null; // 현재 활성화된 디자인 메뉴
8
- this.displayMode = 'desktop'; // 현재 디스플레이 모드
9
- this.displaySize = { px: 1200, percent: 100, gridSize: 16, aspectRatio: '16/9' }; // 현재 디스플레이 사이즈
8
+ this.displayMode = 'desktop'; // 현재 디스플레이 모드 (초기값 = 'desktop')
9
+ this.displaySize = null;
10
10
 
11
11
  this._subscriptions = []; // 구독 해제 함수 저장용
12
12
  this._setupSubscriptions();
@@ -27,29 +27,65 @@ class DisplayEngine {
27
27
  /** 구독 설정 */
28
28
  _setupSubscriptions() {
29
29
  // 초기화 완료 이벤트 구독 - from EngineManager
30
- this._subscribe('system:engineInitialized', (data) => {
31
- this._setDisplaySizeByMode(this.displayMode);
32
- });
30
+ this._subscribe('system:engineInitialized', () => {
31
+ this._resetDisplaySizeByMode(this.displayMode);
33
32
 
34
- this._subscribe('freeDrop:closeActiveMenu', () => {
35
- this.setActiveMenu(null);
33
+ this.eventBus.emit('display:engineInitialized', {
34
+ displaySize: this.displaySize,
35
+ timestamp: Date.now(),
36
+ });
36
37
  });
37
38
 
38
- this._subscribe('freeDrop:closeDesignMenu', () => {
39
+ this._subscribe('system:setElements', () => {
40
+ this.setActiveMenu(null);
39
41
  this.setActiveDesign(null);
40
42
  });
41
43
 
42
- this._subscribe('system:closeActiveMenu', () => {
44
+ this._subscribe('freeDrop:startDrag', () => {
43
45
  this.setActiveMenu(null);
46
+ this.setActiveDesign(null);
44
47
  });
45
48
 
46
- this._subscribe('gridDrop:updateActiveCell', ({ type, activeCell }) => {
47
- if (type === 'new' && activeCell != null) {
49
+ this._subscribe('gridDrop:setActiveElement', ({ action }) => {
50
+ if (action === 'click') {
48
51
  this.setActiveMenu('element');
49
52
  } else {
50
- this.setActiveMenu(activeCell);
53
+ this.setActiveMenu(null);
51
54
  }
55
+
56
+ this.setActiveDesign(null);
52
57
  });
58
+
59
+ // this._subscribe('freeDrop:closeActiveMenu', () => {
60
+ // this.setActiveMenu(null);
61
+ // });
62
+
63
+ // this._subscribe('freeDrop:closeDesignMenu', () => {
64
+ // this.setActiveDesign(null);
65
+ // });
66
+
67
+ // this._subscribe('system:closeActiveMenu', () => {
68
+ // this.setActiveMenu(null);
69
+ // });
70
+
71
+ // this._subscribe('gridDrop:updateActiveCell', ({ type, activeCell }) => {
72
+ // if (type === 'new' && activeCell != null) {
73
+ // this.setActiveMenu('element');
74
+ // } else {
75
+ // this.setActiveMenu(activeCell);
76
+ // }
77
+ // });
78
+
79
+ // this._subscribe('system:enginesReset', () => {
80
+ // this.reset();
81
+ // })
82
+ }
83
+
84
+ reset() {
85
+ this.activeMenu = null;
86
+ this.activeDesign = null;
87
+ this.displayMode = 'desktop';
88
+ this.displaySize = { px: 1200, percent: 100, gridSize: 16, aspectRatio: '16/9' }; // 현재 디스플레이 사이즈
53
89
  }
54
90
 
55
91
  /** 삭제 및 정리 */
@@ -67,6 +103,27 @@ class DisplayEngine {
67
103
  console.log('[DisplayEngine] 삭제 완료');
68
104
  }
69
105
 
106
+ getState() {
107
+ return {
108
+ displayMode: this.displayMode,
109
+ displaySize: { ...this.displaySize },
110
+ activeMenu: this.activeMenu,
111
+ activeDesign: this.activeDesign,
112
+ };
113
+ }
114
+
115
+ setState(state) {
116
+ this.displayMode = state.displayMode;
117
+ this.displaySize = { ...state.displaySize };
118
+ this.activeMenu = state.activeMenu;
119
+ this.activeDesign = state.activeDesign;
120
+
121
+ // this.eventBus.emit('display:restoredState', {
122
+ // prev: state,
123
+ // timestamp: Date.now(),
124
+ // });
125
+ }
126
+
70
127
  /** ---------------------------------- 공통 메소드 ---------------------------------- **/
71
128
 
72
129
  /** 아이콘바 메뉴 변경
@@ -105,19 +162,29 @@ class DisplayEngine {
105
162
  }
106
163
 
107
164
  if (this.displayMode === mode) return;
165
+
166
+ const prevDisplayMode = this.displayMode;
167
+ const prevDisplaySize = { ...this.displaySize };
168
+
108
169
  this.displayMode = mode;
170
+ this._resetDisplaySizeByMode(mode);
109
171
 
172
+ // displayMode 변경 시, displaySize도 초기화 됨
110
173
  this.eventBus.emit('display:updateDisplayMode', {
111
- displayMode: mode,
174
+ displayMode: this.displayMode,
175
+ displaySize: this.displaySize,
176
+ prev: {
177
+ displayMode: prevDisplayMode,
178
+ displaySize: prevDisplaySize,
179
+ },
112
180
  timestamp: Date.now(),
113
181
  });
114
- this._setDisplaySizeByMode(mode);
115
182
  }
116
183
 
117
184
  /** 사이즈 설정
118
185
  * @param {string} mode - 디스플레이 모드
119
186
  */
120
- _setDisplaySizeByMode(mode) {
187
+ _resetDisplaySizeByMode(mode) {
121
188
  if (this.resource[mode]) {
122
189
  this.displaySize = {
123
190
  px: this.resource[mode]?.defaultWidth || 1200,
@@ -125,11 +192,6 @@ class DisplayEngine {
125
192
  gridSize: this.resource[mode]?.gridSize || 16,
126
193
  aspectRatio: this.resource[mode]?.aspectRatio || '16/9',
127
194
  };
128
-
129
- this.eventBus.emit('display:updateDisplaySizeByMode', {
130
- displaySize: this.displaySize,
131
- timestamp: Date.now(),
132
- });
133
195
  }
134
196
  }
135
197
 
@@ -159,10 +221,14 @@ class DisplayEngine {
159
221
  alert(`설정 가능한 최대 ${typeLimit.name}는 ${typeLimit.max}${typeLimit.unit}입니다.`);
160
222
  }
161
223
 
224
+ const prevDisplaySize = { ...this.displaySize };
162
225
  this.displaySize[type] = Math.min(Math.max(0, numSize), typeLimit.max);
163
226
 
164
227
  this.eventBus.emit('display:updateDisplaySize', {
165
228
  displaySize: this.displaySize,
229
+ prev: {
230
+ displaySize: prevDisplaySize,
231
+ },
166
232
  timestamp: Date.now(),
167
233
  });
168
234
  }