dragon-editor 2.0.0-beta.1.4 → 2.0.0-beta.2.1

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 (31) hide show
  1. package/README.md +94 -147
  2. package/README_en.md +14 -62
  3. package/dist/module.json +1 -1
  4. package/dist/module.mjs +8 -0
  5. package/dist/runtime/core/components/SvgIcon.vue +30 -21
  6. package/dist/runtime/core/components/editor/ImageBlock.vue +175 -0
  7. package/dist/runtime/core/components/editor/OlBlock.vue +135 -0
  8. package/dist/runtime/core/components/editor/TextBlock.vue +77 -31
  9. package/dist/runtime/core/components/icon/Accept.vue +5 -0
  10. package/dist/runtime/core/components/icon/ArrowDown.vue +3 -0
  11. package/dist/runtime/core/components/icon/ArrowUp.vue +3 -0
  12. package/dist/runtime/core/components/icon/Cancel.vue +5 -0
  13. package/dist/runtime/core/components/icon/Delete.vue +3 -0
  14. package/dist/runtime/core/style/common.css +320 -31
  15. package/dist/runtime/core/style/viewer.css +191 -0
  16. package/dist/runtime/core/utils/cursor.d.ts +1 -1
  17. package/dist/runtime/core/utils/cursor.mjs +16 -4
  18. package/dist/runtime/core/utils/element.d.ts +2 -1
  19. package/dist/runtime/core/utils/element.mjs +19 -4
  20. package/dist/runtime/core/utils/index.d.ts +2 -3
  21. package/dist/runtime/core/utils/index.mjs +62 -5
  22. package/dist/runtime/core/utils/keyboard.d.ts +1 -1
  23. package/dist/runtime/core/utils/keyboard.mjs +500 -41
  24. package/dist/runtime/core/utils/style.d.ts +6 -2
  25. package/dist/runtime/core/utils/style.mjs +140 -30
  26. package/dist/runtime/shared/components/DragonEditor.vue +488 -159
  27. package/dist/runtime/shared/components/DragonEditorComment.vue +42 -32
  28. package/dist/runtime/shared/components/DragonEditorViewer.vue +30 -2
  29. package/package.json +1 -1
  30. package/dist/runtime/core/style/main.d.ts +0 -1
  31. package/dist/runtime/core/style/main.mjs +0 -24
@@ -1,182 +1,317 @@
1
1
  <template>
2
- <div class="dragon-editor" @paste="pasteEvent" ref="$wrap" @mouseleave="deactiveMenuEvent"
3
- @keydown="deactiveMenuEvent"
4
- >
5
- <div class="d-left-menu" :class="{'--active' : activeMenu}" :style="{top:`${leftMenuPosition}px`}">
2
+ <div class="dragon-editor" @paste="pasteEvent" ref="$wrap" @mouseleave="deactiveMenuEvent" @keydown="deactiveMenuEvent">
3
+ <div class="d-left-menu" :class="{ '--active': activeMenu }" :style="{ top: `${leftMenuPosition}px` }">
6
4
  <div class="d-add-block">
7
- <button class="d-btn-menu-pop"></button>
5
+ <button class="d-btn-menu-pop" @click="toggleBlockAddMenu"></button>
8
6
 
9
- <div class="d-block-list">
10
- <button v-for="(row,count) in blockMenu" :key="count" class="d-btn-block" @click="row.action">
11
- <SvgIcon v-if="row.hasIcon" :kind="row.icon"/>
12
- <div v-else class="icon" v-html="row.icon"></div>
7
+ <div class="d-block-list" :class="{ '--active': activeBlockAddMenu }">
8
+ <button v-for="(row, count) in blockMenu" :key="count" class="d-btn-block" @click="row.action">
9
+ <SvgIcon v-if="row.hasIcon" :kind="row.icon" />
10
+ <div v-else class="d-icon" v-html="row.icon"></div>
13
11
  <p class="d-name">{{ row.name }}</p>
14
12
  </button>
15
13
  </div>
16
14
  </div>
17
15
 
18
16
  <div class="d-control-block">
19
- <button class="d-btn-block-pop"></button>
17
+ <button class="d-btn-block-pop" @click="toggleBlockControlMenu"></button>
20
18
 
21
- <div class="d-block-list">
22
- <button class="d-btn-block"></button>
23
- <button class="d-btn-block"></button>
24
- <button class="d-btn-block"></button>
19
+ <div class="d-block-list" :class="{ '--active': activeBlockColtrolMenu }">
20
+ <button class="d-btn-block" @click="moveBlock('up')"> <SvgIcon kind="arrowUp" />Up </button>
21
+ <button class="d-btn-block" @click="moveBlock('down')"> <SvgIcon kind="arrowDown" />Down </button>
22
+ <button class="d-btn-block"> <SvgIcon kind="delete" />Delete </button>
25
23
  </div>
26
24
  </div>
27
25
  </div>
28
26
 
29
- <div class="d-style-menu" :class="{'--active' : activeMenu}" :style="{top:`${styleMenuPosition}px`}">
30
- <div class="d-column">
31
- <button class="d-btn" :class="{'--active' : checkAlignActive('d-align-left')}"
32
- @click="setBlockEvent('alignLeft')"
33
- >
34
- <SvgIcon kind="alignLeft"/>
35
- </button>
36
- <button class="d-btn" :class="{'--active' : checkAlignActive('d-align-center')}"
37
- @click="setBlockEvent('alignCenter')"
38
- >
39
- <SvgIcon kind="alignCenter"/>
40
- </button>
41
- <button class="d-btn" :class="{'--active' : checkAlignActive('d-align-right')}"
42
- @click="setBlockEvent('alignRight')"
43
- >
44
- <SvgIcon kind="alignRight"/>
45
- </button>
46
- </div>
47
-
48
- <div class="d-column">
49
- <button class="d-btn" :class="{'--active' : activeStyle.bold}"
50
- @click="setBlockEvent('decorationBold')"
51
- >
52
- <SvgIcon kind="decorationBold"/>
53
- </button>
54
-
55
- <button class="d-btn" :class="{'--active' : activeStyle.italic}"
56
- @click="setBlockEvent('decorationItalic')"
57
- >
58
- <SvgIcon kind="decorationItalic"/>
59
- </button>
60
-
61
- <button class="d-btn" :class="{'--active' : activeStyle.underline}"
62
- @click="setBlockEvent('decorationUnderline')"
63
- >
64
- <SvgIcon kind="decorationUnderline"/>
65
- </button>
66
-
67
- <button class="d-btn" :class="{'--active' : activeStyle.through}"
68
- @click="setBlockEvent('decorationStrikethrough')"
69
- >
70
- <SvgIcon kind="decorationStrikethrough"/>
71
- </button>
72
- </div>
73
-
74
- <div class="d-column">
75
- <button class="d-btn">
76
- <SvgIcon kind="link"/>
27
+ <div
28
+ class="d-link-box"
29
+ :class="{ '--active': activeLinkBox }"
30
+ :style="{
31
+ top: `${linkBoxPosition.top}px`,
32
+ left: `${linkBoxPosition.left}px`,
33
+ }"
34
+ >
35
+ <template v-if="styleButtonList[2][0].active">
36
+ <p class="d-input">{{ linkValue }}</p>
37
+ <button class="d-btn-link" @click="decoLinkControl">
38
+ <SvgIcon kind="cancel" />
77
39
  </button>
78
- </div>
79
-
80
- <div class="d-column">
81
- <button class="d-btn">
82
- <SvgIcon kind="codeBlock"/>
40
+ </template>
41
+ <template v-else>
42
+ <input type="url" class="d-input" v-model="linkValue" />
43
+ <button class="d-btn-link" @click="decoLinkControl">
44
+ <SvgIcon kind="accept" />
83
45
  </button>
84
- </div>
46
+ </template>
47
+ </div>
85
48
 
86
- <div v-if="customStyleMenu.length > 0" class="d-column">
87
- <!-- customStyleMenu-->
49
+ <div class="d-style-menu" :class="{ '--active': activeMenu }" :style="{ top: `${styleMenuPosition}px` }">
50
+ <div v-for="(column, count) in styleButtonList" :key="count" class="d-column">
51
+ <template v-for="(item, j) in column">
52
+ <template v-if="item.type === 'single'">
53
+ <button v-if="item.target.indexOf(content[activeIdx].type) > -1" class="d-btn" :class="{ '--active': item.active }" @click="item.action">
54
+ <SvgIcon :kind="item.icon" />
55
+ </button>
56
+ </template>
57
+
58
+ <template v-else>
59
+ <button v-if="item.target.indexOf(content[activeIdx].type) > -1" class="d-btn --hug" @click="item.action(count, j)">{{ `${item.name} : ${item.value}` }}</button>
60
+
61
+ <div class="d-child-list" :class="{ '--active': item.active }">
62
+ <button class="d-child-btn" v-for="(child, k) in item.childList" :key="k" @click="child.action(count, j)">{{ child.name }}</button>
63
+ </div>
64
+ </template>
65
+ </template>
88
66
  </div>
67
+ <div v-if="customStyleMenu.length > 0" class="d-column"></div>
89
68
  </div>
90
69
 
91
- <div
92
- class="d-row-block"
93
- v-for="(row,count) in content"
94
- :key="count"
95
- @click="activeIdx = count"
96
- @mouseenter="activeMenuEvent"
97
- @mousemove="activeMenuEvent"
98
- @mouseup="activeMenuEvent"
99
- >
100
- <component
101
- ref="$child"
102
- v-model="content[count]"
103
- :is="setComponentKind(row.type)"
104
- @addBlock="addBlockLocal"
105
- />
70
+ <div class="d-row-block" v-for="(row, count) in content" :key="`${row.id}-wrap`" @click="activeIdx = count" @mouseenter="activeMenuEvent(count, $event)" @mousemove="activeMenuEvent(count, $event)" @mouseup="activeMenuEvent(count, $event)">
71
+ <component ref="$child" v-model="content[count]" :key="row.id" :is="setComponentKind(row.type)" :cursorData="cursorData" @addBlock="addBlockLocal" @deleteBlockLocal="deleteBlockLocal" />
106
72
  </div>
107
73
  </div>
108
74
  </template>
109
75
 
110
76
  <script setup lang="ts">
111
- import {ref, unref, onMounted} from "#imports";
112
- import {createBlock, getClipboardData, getCursor} from "../../core/utils";
113
- import {
114
- editorOptions,
115
- editorMenu,
116
- editorContentType,
117
- userCustomMenu,
118
- styleActiveType,
119
- userStyleMenu
120
- } from "../../../types/index";
77
+ // @ts-ignore
78
+ import { ref, unref, onMounted } from "#imports";
79
+ import { createBlock, getClipboardData, getCursor } from "../../core/utils";
80
+ import type { editorOptions, editorMenu, editorContentType, userCustomMenu, userStyleMenu, cursorSelection } from "../../../types/index";
121
81
 
122
82
  // components
123
83
  import SvgIcon from "../../core/components/SvgIcon.vue";
124
84
  import textBlock from "../../core/components/editor/TextBlock.vue";
85
+ import imageBlock from "../../core/components/editor/ImageBlock.vue";
86
+ import olBlock from "../../core/components/editor/OlBlock.vue";
125
87
 
126
- // props
127
- const props = withDefaults(defineProps<{ modelValue: editorContentType, option?: editorOptions }>(), {
128
- modelValue: () => [],
129
- option: () => {
130
- return {
131
- blockMenu: ["text", "ol", "ul", "table", "quotation"]
132
- }
133
- },
88
+ // 기본 정보
89
+ const props = defineProps<{
90
+ modelValue: editorContentType;
91
+ option?: editorOptions;
92
+ }>();
93
+ const modelValue = ref<editorContentType>([]);
94
+ const option = ref<editorOptions>({
95
+ blockMenu: ["text"],
96
+ // blockMenu: ["text", "ol", "ul", "table", "quotation"], // TODO : 다른 블럭 만들기
134
97
  });
98
+
99
+ if (props.modelValue) {
100
+ modelValue.value = props.modelValue;
101
+ }
102
+
103
+ if (props.option) {
104
+ option.value = Object.assign(option.value, props.option);
105
+ }
106
+
135
107
  const emit = defineEmits<{
136
108
  (e: "update:modelValue", modelValue: editorContentType): void;
137
109
  }>();
138
110
 
139
- // Editor data
111
+ // 내부 데이터
140
112
  const $wrap = ref();
141
113
  const $child = ref();
142
114
  const activeMenu = ref<boolean>(false);
115
+ const activeLinkBox = ref<boolean>(false);
116
+ const activeBlockAddMenu = ref<boolean>(false);
117
+ const activeBlockColtrolMenu = ref<boolean>(false);
143
118
  const leftMenuPosition = ref<number>(0);
144
119
  const styleMenuPosition = ref<number>(0);
120
+ const linkBoxPosition = ref({
121
+ top: 0,
122
+ left: 0,
123
+ });
145
124
  const iconList = ["textBlock", "imageBlock", "ulBlock", "olBlock", "quotationBlock", "tableBlock"];
146
125
  const blockMenu = ref<editorMenu[]>([]);
147
126
  const customStyleMenu = ref<userStyleMenu[]>([]);
148
127
  const content = ref<editorContentType>([]);
149
128
  const activeIdx = ref<number>(0);
150
- const activeStyle = ref<styleActiveType>({
151
- bold: false,
152
- italic: false,
153
- underline: false,
154
- through: false,
155
- link: false,
156
- code: false,
129
+ const focusIdx = ref<number>(0);
130
+ const linkValue = ref<string>("");
131
+ const cursorData = ref<cursorSelection>({
132
+ type: "",
133
+ startNode: null,
134
+ startOffset: null,
135
+ endNode: null,
136
+ endOffset: null,
157
137
  });
158
- // const activeItemId = ref<string>("");
159
- // const selectItems = ref<string[]>([]);
160
-
161
- // initial logic
162
- onMounted(() => {
163
- dataUpdateAction();
164
- });
165
-
166
- // block menu setting
167
- blockMenu.value = setEditorMenu(props.option.blockMenu as string[], unref(props.option.customBlockMenu) as userCustomMenu[]);
138
+ const styleButtonList = ref([
139
+ [
140
+ {
141
+ type: "single",
142
+ name: "Align Left",
143
+ icon: "alignLeft",
144
+ active: false,
145
+ target: ["text", "image", "table", "ul", "ol"],
146
+ action: () => {
147
+ setBlockDecoEvent("alignLeft");
148
+ },
149
+ },
150
+ {
151
+ type: "single",
152
+ name: "Align Center",
153
+ icon: "alignCenter",
154
+ target: ["text", "image", "table", "ul", "ol"],
155
+ active: false,
156
+ action: () => {
157
+ setBlockDecoEvent("alignCenter");
158
+ },
159
+ },
160
+ {
161
+ type: "single",
162
+ name: "Align right",
163
+ icon: "alignRight",
164
+ target: ["text", "image", "table", "ul", "ol"],
165
+ active: false,
166
+ action: () => {
167
+ setBlockDecoEvent("alignRight");
168
+ },
169
+ },
170
+ ],
171
+ [
172
+ {
173
+ type: "single",
174
+ name: "Decoration Bold",
175
+ icon: "decorationBold",
176
+ target: ["text", "table", "ul", "ol"],
177
+ active: false,
178
+ action: () => {
179
+ setBlockDecoEvent("decorationBold");
180
+ },
181
+ },
182
+ {
183
+ type: "single",
184
+ name: "Decoration Italic",
185
+ icon: "decorationItalic",
186
+ target: ["text", "table", "ul", "ol"],
187
+ active: false,
188
+ action: () => {
189
+ setBlockDecoEvent("decorationItalic");
190
+ },
191
+ },
192
+ {
193
+ type: "single",
194
+ name: "Decoration Underline",
195
+ icon: "decorationUnderline",
196
+ target: ["text", "table", "ul", "ol"],
197
+ active: false,
198
+ action: () => {
199
+ setBlockDecoEvent("decorationUnderline");
200
+ },
201
+ },
202
+ {
203
+ type: "single",
204
+ name: "Decoration Strikethrough",
205
+ icon: "decorationStrikethrough",
206
+ target: ["text", "table", "ul", "ol"],
207
+ active: false,
208
+ action: () => {
209
+ setBlockDecoEvent("decorationStrikethrough");
210
+ },
211
+ },
212
+ ],
213
+ [
214
+ {
215
+ type: "single",
216
+ name: "Link",
217
+ icon: "link",
218
+ active: false,
219
+ target: ["text", "table", "ul", "ol"],
220
+ action: () => {
221
+ activeLinkBox.value = !activeLinkBox.value;
222
+ },
223
+ },
224
+ ],
225
+ [
226
+ {
227
+ type: "single",
228
+ name: "Decoration Code",
229
+ icon: "codeBlock",
230
+ target: ["text", "table", "ul", "ol"],
231
+ active: false,
232
+ action: () => {
233
+ setBlockDecoEvent("decorationCode");
234
+ },
235
+ },
236
+ ],
237
+ [
238
+ {
239
+ type: "list",
240
+ name: "Font Size",
241
+ value: "default",
242
+ target: ["text"],
243
+ active: false,
244
+ action: (count, j) => {
245
+ styleButtonList.value[count][j].active = !styleButtonList.value[count][j].active;
246
+ },
247
+ childList: [
248
+ {
249
+ name: "default",
250
+ action: (count, j) => {
251
+ setBlockDecoEvent("heading-4");
252
+ styleButtonList.value[count][j].value = "default";
253
+ styleButtonList.value[count][j].active = false;
254
+ },
255
+ },
256
+ {
257
+ name: "h1",
258
+ action: (count, j) => {
259
+ setBlockDecoEvent("heading-1");
260
+ styleButtonList.value[count][j].value = "h1";
261
+ styleButtonList.value[count][j].active = false;
262
+ },
263
+ },
264
+ {
265
+ name: "h2",
266
+ action: (count, j) => {
267
+ setBlockDecoEvent("heading-2");
268
+ styleButtonList.value[count][j].value = "h2";
269
+ styleButtonList.value[count][j].active = false;
270
+ },
271
+ },
272
+ {
273
+ name: "h3",
274
+ action: (count, j) => {
275
+ setBlockDecoEvent("heading-3");
276
+ styleButtonList.value[count][j].value = "h3";
277
+ styleButtonList.value[count][j].active = false;
278
+ },
279
+ },
280
+ {
281
+ name: "h5",
282
+ action: (count, j) => {
283
+ setBlockDecoEvent("heading-5");
284
+ styleButtonList.value[count][j].value = "h5";
285
+ styleButtonList.value[count][j].active = false;
286
+ },
287
+ },
288
+ {
289
+ name: "h6",
290
+ action: (count, j) => {
291
+ setBlockDecoEvent("heading-6");
292
+ styleButtonList.value[count][j].value = "h6";
293
+ styleButtonList.value[count][j].active = false;
294
+ },
295
+ },
296
+ ],
297
+ },
298
+ ],
299
+ ]);
300
+
301
+ // 블럭 추가 메뉴 설정
302
+ blockMenu.value = setEditorMenu(option.value.blockMenu as string[], unref(option.value.customBlockMenu) as userCustomMenu[]);
168
303
 
169
304
  // 유저 커스텀 스타일 메뉴
170
- if (props.option.customStyleMenu) {
171
- customStyleMenu.value = unref(props.option.customStyleMenu);
305
+ if (option.value.customStyleMenu) {
306
+ customStyleMenu.value = unref(option.value.customStyleMenu);
172
307
  }
173
308
 
174
- // content data setting
175
- if (props.modelValue && Array.isArray(props.modelValue)) {
176
- if (props.modelValue.length == 0) {
177
- addBlockLocal("text", true);
309
+ // 컨텐츠 데이터 설정
310
+ if (modelValue.value && Array.isArray(modelValue.value)) {
311
+ if (modelValue.value.length == 0) {
312
+ addBlockLocal({ name: "text", time: true });
178
313
  } else {
179
- content.value = unref(props.modelValue) as editorContentType;
314
+ content.value = modelValue.value;
180
315
  }
181
316
  } else {
182
317
  throw new Error("[DragonEditor]ERROR : You must set 'v-model' attribute and 'v-mode' type is must be Array.");
@@ -196,13 +331,21 @@ function checkAlignActive(className: string) {
196
331
  return value;
197
332
  }
198
333
 
334
+ // 스타일 메뉴 엑티브 상황
199
335
  function checkDecoActive() {
200
- activeStyle.value.bold = hasClassNameCheckLogic("d-deco-bold");
201
- activeStyle.value.italic = hasClassNameCheckLogic("d-deco-italic");
202
- activeStyle.value.underline = hasClassNameCheckLogic("d-deco-underline");
203
- activeStyle.value.through = hasClassNameCheckLogic("d-deco-through");
336
+ styleButtonList.value[0][0].active = checkAlignActive("d-align-left");
337
+ styleButtonList.value[0][1].active = checkAlignActive("d-align-center");
338
+ styleButtonList.value[0][2].active = checkAlignActive("d-align-right");
339
+ styleButtonList.value[1][0].active = hasClassNameCheckLogic("d-deco-bold");
340
+ styleButtonList.value[1][1].active = hasClassNameCheckLogic("d-deco-italic");
341
+ styleButtonList.value[1][2].active = hasClassNameCheckLogic("d-deco-underline");
342
+ styleButtonList.value[1][3].active = hasClassNameCheckLogic("d-deco-through");
343
+ styleButtonList.value[2][0].active = hasClassNameCheckLogic("d-deco-link");
344
+ styleButtonList.value[3][0].active = hasClassNameCheckLogic("d-deco-code");
345
+ styleButtonList.value[4][0].value = checkHeadingClass();
204
346
  }
205
347
 
348
+ // 텍스트 스타일 확인용 함수
206
349
  function hasClassNameCheckLogic(className: string) {
207
350
  const cursorData = getCursor();
208
351
  let value = false;
@@ -222,11 +365,45 @@ function hasClassNameCheckLogic(className: string) {
222
365
  value = true;
223
366
  }
224
367
  }
368
+
369
+ if (className === "d-deco-link") {
370
+ if (value) {
371
+ linkValue.value = $target.getAttribute("href");
372
+ } else {
373
+ linkValue.value = "";
374
+ }
375
+ }
225
376
  }
226
377
 
227
378
  return value;
228
379
  }
229
380
 
381
+ // 글자크기 클레스 확인
382
+ function checkHeadingClass(): string {
383
+ let value: string = "";
384
+
385
+ switch (true) {
386
+ case hasClassNameCheckLogic("d-h1"):
387
+ value = "h1";
388
+ break;
389
+ case hasClassNameCheckLogic("d-h2"):
390
+ value = "h2";
391
+ break;
392
+ case hasClassNameCheckLogic("d-h3"):
393
+ value = "h3";
394
+ break;
395
+ case hasClassNameCheckLogic("d-h5"):
396
+ value = "h5";
397
+ break;
398
+ case hasClassNameCheckLogic("d-h6"):
399
+ value = "h6";
400
+ break;
401
+ default:
402
+ value = "default";
403
+ }
404
+
405
+ return value;
406
+ }
230
407
 
231
408
  function setEditorMenu(vanillaData: string[], customData?: userCustomMenu[]) {
232
409
  const dataList: editorMenu[] = [];
@@ -237,8 +414,8 @@ function setEditorMenu(vanillaData: string[], customData?: userCustomMenu[]) {
237
414
  hasIcon: true,
238
415
  icon: `${name}Block`,
239
416
  action: () => {
240
- addBlockLocal(name);
241
- }
417
+ addBlockLocal({ name: name });
418
+ },
242
419
  });
243
420
  });
244
421
 
@@ -248,7 +425,7 @@ function setEditorMenu(vanillaData: string[], customData?: userCustomMenu[]) {
248
425
  name: row.name,
249
426
  hasIcon: iconList.indexOf(row.icon) > -1,
250
427
  icon: row.icon,
251
- action: row.action
428
+ action: row.action,
252
429
  });
253
430
  });
254
431
  }
@@ -256,16 +433,55 @@ function setEditorMenu(vanillaData: string[], customData?: userCustomMenu[]) {
256
433
  return dataList;
257
434
  }
258
435
 
436
+ /**
437
+ * 내부용 이벤트 함수
438
+ */
439
+ // 관련 메뉴 열기
440
+ function activeMenuEvent(count: number, e?: MouseEvent) {
441
+ let $target: HTMLElement;
442
+
443
+ focusIdx.value = count;
444
+
445
+ cursorData.value = getCursor();
446
+
447
+ if (e) {
448
+ $target = e.currentTarget as HTMLElement;
449
+
450
+ if (e.type === "mouseup") {
451
+ const wrap = $target.parentElement as HTMLElement;
452
+ const child = wrap.querySelectorAll(".d-row-block");
453
+ let idx: number = -1;
454
+
455
+ [...child].filter((item, count) => {
456
+ if (item === $target) {
457
+ idx = count;
458
+ }
459
+ });
259
460
 
260
- // event function
261
- function activeMenuEvent(e: MouseEvent) {
262
- setMenuPosition(e.currentTarget as HTMLElement);
461
+ if (idx > -1) {
462
+ activeIdx.value = idx;
463
+ }
464
+ }
465
+ } else {
466
+ $target = $child.value[activeIdx.value];
467
+ }
468
+
469
+ setMenuPosition($target);
263
470
  checkDecoActive();
264
471
  activeMenu.value = true;
472
+ styleButtonList.value[4][0].active = false;
265
473
  }
266
474
 
267
- function deactiveMenuEvent() {
475
+ // 관련 메뉴 닫기
476
+ function deactiveMenuEvent(e?: MouseEvent | KeyboardEvent) {
268
477
  activeMenu.value = false;
478
+ activeBlockAddMenu.value = false;
479
+ activeBlockColtrolMenu.value = false;
480
+ styleButtonList.value[4][0].active = false;
481
+
482
+ if (e && e.type === "mouseleave") {
483
+ activeLinkBox.value = false;
484
+ }
269
485
  }
270
486
 
271
487
  function dataUpdateAction() {
@@ -276,24 +492,50 @@ function dataUpdateAction() {
276
492
  emit("update:modelValue", content.value);
277
493
  }
278
494
 
279
- function addBlockLocal(name: string, time: boolean = false) {
280
- const block = createBlock(name);
495
+ // 블럭 추가 로직
496
+ function addBlockLocal({ name, value, time = false }: { name: string; value?: object; time?: boolean }) {
497
+ const block = createBlock(name, value);
281
498
 
282
499
  content.value.splice(activeIdx.value + 1, 0, block);
283
500
 
284
501
  if (time === false) {
285
502
  activeIdx.value += 1;
286
503
 
287
- setTimeout(() => { // waiting data set
288
- if (name !== "image") {
289
- $child.value[activeIdx.value].focus();
290
- }
291
-
504
+ setTimeout(() => {
505
+ activeBlockAddMenu.value = false;
506
+ $child.value[activeIdx.value].focus();
292
507
  dataUpdateAction();
508
+ activeMenuEvent(activeIdx.value);
293
509
  }, 100);
294
510
  }
295
511
  }
296
512
 
513
+ // 블럭 삭제 이벤트
514
+ function deleteBlockLocal(index?: number) {
515
+ if (content.value.length > 1) {
516
+ if (index === undefined) {
517
+ index = activeIdx.value as number;
518
+ }
519
+
520
+ if (index - 1 !== -1) {
521
+ const $targetData = content.value[index - 1];
522
+ const $thisData = content.value[index];
523
+
524
+ if ($targetData.type === "text") {
525
+ activeIdx.value -= 1;
526
+ content.value[index - 1].content += `<span class="${$thisData.classList.join(" ")}">${$thisData.content}</span>`;
527
+ content.value.splice(index, 1);
528
+
529
+ setTimeout(() => {
530
+ dataUpdateAction();
531
+ $child.value[activeIdx.value].focus("last");
532
+ }, 150);
533
+ }
534
+ }
535
+ }
536
+ }
537
+
538
+ // 붙여넣기 이벤트
297
539
  function pasteEvent(e: ClipboardEvent) {
298
540
  e.preventDefault();
299
541
  const data = getClipboardData(e.clipboardData as DataTransfer);
@@ -302,18 +544,30 @@ function pasteEvent(e: ClipboardEvent) {
302
544
  const targetComponent = $child.value[activeIdx.value];
303
545
  const componentType = targetComponent.getType();
304
546
 
305
- if (componentType !== "image" && componentType !== "other") {
547
+ if (componentType === "other") {
548
+ // TODO : add block
549
+ } else {
306
550
  targetComponent.pasteEvent(data.value);
307
551
  }
308
552
  }
309
553
  }
310
554
 
555
+ // 블럭 종류 정의
311
556
  function setComponentKind(kind: string) {
557
+ let componentData: any;
558
+
312
559
  switch (kind) {
313
- case "text":
314
- return textBlock;
560
+ case "ol":
561
+ componentData = olBlock;
315
562
  break;
563
+ case "image":
564
+ componentData = imageBlock;
565
+ break;
566
+ case "text":
567
+ componentData = textBlock;
316
568
  }
569
+
570
+ return componentData;
317
571
  }
318
572
 
319
573
  function setMenuPosition($target: HTMLElement) {
@@ -325,34 +579,109 @@ function setMenuPosition($target: HTMLElement) {
325
579
  const wrapTop = wrapRect.top - bodyRect.top;
326
580
  const targetTop = targetRect.top - bodyRect.top;
327
581
  const targetBottom = targetRect.bottom - bodyRect.top;
328
- const top = ((targetTop - (wrapTop + 10)) - parentNodeScrollY) + 13;
329
- const bottom = ((targetBottom - (wrapTop + 10)) - parentNodeScrollY) + 10;
582
+ const top = targetTop - (wrapTop + 10) - parentNodeScrollY + 13;
583
+ const bottom = targetBottom - (wrapTop + 10) - parentNodeScrollY + 10;
584
+ let startNode = cursorData.value.startNode;
585
+
586
+ if (startNode !== null) {
587
+ if (startNode.constructor.name === "Text") {
588
+ startNode = startNode.parentNode;
589
+ }
590
+
591
+ const startNodeRect = startNode.getBoundingClientRect();
592
+ const wrapleft = startNodeRect.left - bodyRect.left;
593
+
594
+ linkBoxPosition.value = {
595
+ top: top - 32,
596
+ left: wrapleft,
597
+ };
598
+ }
330
599
 
331
600
  styleMenuPosition.value = bottom;
332
601
  leftMenuPosition.value = top;
333
602
  }
334
603
 
335
- function setBlockEvent(type: string) {
336
- $child.value[activeIdx.value].setStyles(type);
604
+ // 블럭 스타일 이벤트
605
+ function setBlockDecoEvent(type: string, url?: string) {
606
+ $child.value[activeIdx.value].setStyles({
607
+ type: type,
608
+ url: url,
609
+ });
337
610
  setTimeout(() => {
338
611
  checkDecoActive();
339
612
  }, 100);
340
613
  }
341
614
 
615
+ // 링크 스타일컨트롤
616
+ function decoLinkControl() {
617
+ setBlockDecoEvent("decorationLink", linkValue.value);
618
+ activeLinkBox.value = false;
619
+ }
620
+
621
+ // 블럭 위치 조정
622
+ async function moveBlock(type: string) {
623
+ let targetIdx = 0;
624
+ dataUpdateAction();
625
+
626
+ if (type === "up") {
627
+ targetIdx = activeIdx.value - 1;
628
+ } else {
629
+ targetIdx = activeIdx.value + 1;
630
+ }
631
+
632
+ if (targetIdx >= 0 && targetIdx < content.value.length) {
633
+ const targetData = content.value[targetIdx];
634
+ const thisData = content.value[activeIdx.value];
635
+
636
+ content.value.splice(targetIdx, 1, thisData);
637
+ content.value.splice(activeIdx.value, 1, targetData);
638
+ activeIdx.value = targetIdx;
639
+ deactiveMenuEvent();
640
+ }
641
+ }
642
+
643
+ // 블럭 추가 메뉴 열기
644
+ function toggleBlockAddMenu() {
645
+ activeIdx.value = focusIdx.value;
646
+ activeBlockAddMenu.value = !activeBlockAddMenu.value;
647
+ }
648
+
649
+ // 블럭 컨트롤 메뉴 열기
650
+ function toggleBlockControlMenu() {
651
+ activeIdx.value = focusIdx.value;
652
+ activeBlockColtrolMenu.value = !activeBlockColtrolMenu.value;
653
+ }
342
654
 
343
- // export function
655
+ /**
656
+ * 외부용 함수
657
+ */
344
658
  // function checkStyleActive(className: string) {
345
659
  // return hasClassNameCheckLogic(className);
346
660
  // }
347
661
 
348
- function addImageBlock() {
349
- console.log("local image added event!");
350
- console.log($child);
351
- // contentData.value = value;
662
+ function addImageBlock({ src, width, height, webp, caption }: { src: string; width: number; height: number; webp: boolean; caption?: string }) {
663
+ addBlockLocal({
664
+ name: "image",
665
+ value: {
666
+ src: src,
667
+ width: width,
668
+ height: height,
669
+ webp: webp,
670
+ caption: caption,
671
+ },
672
+ });
352
673
  }
353
674
 
675
+ // 함수 내보내기
354
676
  defineExpose({
355
- addImageBlock
677
+ addImageBlock,
678
+ });
679
+
680
+ /**
681
+ * 초기 데이터 확인용 로직
682
+ */
683
+ onMounted(() => {
684
+ dataUpdateAction();
356
685
  });
357
686
  </script>
358
687