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

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