dragon-editor 2.0.0-beta.1.3 → 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 (45) 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 +9 -1
  5. package/dist/runtime/core/components/SvgIcon.vue +175 -0
  6. package/dist/runtime/core/components/editor/ImageBlock.vue +174 -0
  7. package/dist/runtime/core/components/editor/TextBlock.vue +137 -0
  8. package/dist/runtime/core/components/icon/Accept.vue +5 -0
  9. package/dist/runtime/core/components/icon/AlignCenter.vue +6 -0
  10. package/dist/runtime/core/components/icon/AlignLeft.vue +6 -0
  11. package/dist/runtime/core/components/icon/AlignRight.vue +6 -0
  12. package/dist/runtime/core/components/icon/ArrowDown.vue +3 -0
  13. package/dist/runtime/core/components/icon/ArrowUp.vue +3 -0
  14. package/dist/runtime/core/components/icon/Cancel.vue +5 -0
  15. package/dist/runtime/core/components/icon/CodeBlock.vue +6 -0
  16. package/dist/runtime/core/components/icon/DecorationBold.vue +6 -0
  17. package/dist/runtime/core/components/icon/DecorationItalic.vue +6 -0
  18. package/dist/runtime/core/components/icon/DecorationStrikethrough.vue +6 -0
  19. package/dist/runtime/core/components/icon/DecorationUnderline.vue +6 -0
  20. package/dist/runtime/core/components/icon/Delete.vue +3 -0
  21. package/dist/runtime/core/components/icon/ImageBlock.vue +5 -0
  22. package/dist/runtime/core/components/icon/LinkPath.vue +6 -0
  23. package/dist/runtime/core/components/icon/OlBlock.vue +6 -0
  24. package/dist/runtime/core/components/icon/QuotationBlock.vue +6 -0
  25. package/dist/runtime/core/components/icon/TableBlock.vue +8 -0
  26. package/dist/runtime/core/components/icon/TextBlock.vue +5 -0
  27. package/dist/runtime/core/components/icon/UlBlock.vue +6 -0
  28. package/dist/runtime/core/style/common.css +419 -0
  29. package/dist/runtime/core/style/viewer.css +191 -0
  30. package/dist/runtime/core/utils/cursor.d.ts +4 -0
  31. package/dist/runtime/core/utils/cursor.mjs +84 -0
  32. package/dist/runtime/core/utils/element.d.ts +2 -0
  33. package/dist/runtime/core/utils/element.mjs +29 -0
  34. package/dist/runtime/core/utils/index.d.ts +6 -0
  35. package/dist/runtime/core/utils/index.mjs +67 -0
  36. package/dist/runtime/core/utils/keyboard.d.ts +6 -0
  37. package/dist/runtime/core/utils/keyboard.mjs +322 -0
  38. package/dist/runtime/core/utils/style.d.ts +6 -0
  39. package/dist/runtime/core/utils/style.mjs +359 -0
  40. package/dist/runtime/shared/components/DragonEditor.vue +560 -0
  41. package/dist/runtime/{components → shared/components}/DragonEditorComment.vue +33 -11
  42. package/dist/runtime/shared/components/DragonEditorViewer.vue +29 -0
  43. package/package.json +1 -1
  44. package/dist/runtime/components/DragonEditor.vue +0 -361
  45. package/dist/runtime/components/DragonEditorViewer.vue +0 -3
@@ -0,0 +1,560 @@
1
+ <template>
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` }">
4
+ <div class="d-add-block">
5
+ <button class="d-btn-menu-pop" @click="toggleBlockAddMenu"></button>
6
+
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>
11
+ <p class="d-name">{{ row.name }}</p>
12
+ </button>
13
+ </div>
14
+ </div>
15
+
16
+ <div class="d-control-block">
17
+ <button class="d-btn-block-pop" @click="toggleBlockControlMenu"></button>
18
+
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>
29
+ </div>
30
+ </div>
31
+ </div>
32
+
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" />
41
+ </button>
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" />
47
+ </button>
48
+ </template>
49
+ </div>
50
+
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>
58
+ </div>
59
+ <div v-if="customStyleMenu.length > 0" class="d-column">
60
+ <!-- customStyleMenu-->
61
+ </div>
62
+ </div>
63
+
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" />
66
+ </div>
67
+ </div>
68
+ </template>
69
+
70
+ <script setup lang="ts">
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";
74
+
75
+ // components
76
+ import SvgIcon from "../../core/components/SvgIcon.vue";
77
+ import textBlock from "../../core/components/editor/TextBlock.vue";
78
+ import imageBlock from "../../core/components/editor/ImageBlock.vue";
79
+
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 : 다른 블럭 만들기
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
+
99
+ const emit = defineEmits<{
100
+ (e: "update:modelValue", modelValue: editorContentType): void;
101
+ }>();
102
+
103
+ // 내부 데이터
104
+ const $wrap = ref();
105
+ const $child = ref();
106
+ const activeMenu = ref<boolean>(false);
107
+ const activeLinkBox = ref<boolean>(false);
108
+ const activeBlockAddMenu = ref<boolean>(false);
109
+ const activeBlockColtrolMenu = ref<boolean>(false);
110
+ const leftMenuPosition = ref<number>(0);
111
+ const styleMenuPosition = ref<number>(0);
112
+ const linkBoxPosition = ref({
113
+ top: 0,
114
+ left: 0,
115
+ });
116
+ const iconList = ["textBlock", "imageBlock", "ulBlock", "olBlock", "quotationBlock", "tableBlock"];
117
+ const blockMenu = ref<editorMenu[]>([]);
118
+ const customStyleMenu = ref<userStyleMenu[]>([]);
119
+ const content = ref<editorContentType>([]);
120
+ const activeIdx = ref<number>(0);
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,
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);
222
+
223
+ // 블럭 추가 메뉴 설정
224
+ blockMenu.value = setEditorMenu(option.value.blockMenu as string[], unref(option.value.customBlockMenu) as userCustomMenu[]);
225
+
226
+ // 유저 커스텀 스타일 메뉴
227
+ if (option.value.customStyleMenu) {
228
+ customStyleMenu.value = unref(option.value.customStyleMenu);
229
+ }
230
+
231
+ // 컨텐츠 데이터 설정
232
+ if (modelValue.value && Array.isArray(modelValue.value)) {
233
+ if (modelValue.value.length == 0) {
234
+ addBlockLocal({ name: "text", time: true });
235
+ } else {
236
+ content.value = modelValue.value;
237
+ }
238
+ } else {
239
+ throw new Error("[DragonEditor]ERROR : You must set 'v-model' attribute and 'v-mode' type is must be Array.");
240
+ }
241
+
242
+ // local logic
243
+ function checkAlignActive(className: string) {
244
+ const data = content.value[activeIdx.value];
245
+ let value = false;
246
+
247
+ switch (data.type) {
248
+ case "text":
249
+ value = data.classList.indexOf(className) > -1;
250
+ break;
251
+ }
252
+
253
+ return value;
254
+ }
255
+
256
+ function checkDecoActive() {
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");
266
+ }
267
+
268
+ function hasClassNameCheckLogic(className: string) {
269
+ const cursorData = getCursor();
270
+ let value = false;
271
+
272
+ if (cursorData.type === "Caret") {
273
+ const type = (cursorData.startNode as Node).constructor.name;
274
+ let $target = cursorData.startNode as HTMLElement;
275
+
276
+ if (type === "Text") {
277
+ $target = (cursorData.startNode as HTMLElement).parentNode as HTMLElement;
278
+ }
279
+
280
+ if ($target) {
281
+ const classList = [...$target.classList];
282
+
283
+ if (classList.indexOf(className) > -1) {
284
+ value = true;
285
+ }
286
+ }
287
+
288
+ if (className === "d-deco-link") {
289
+ if (value) {
290
+ linkValue.value = $target.getAttribute("href");
291
+ } else {
292
+ linkValue.value = "";
293
+ }
294
+ }
295
+ }
296
+
297
+ return value;
298
+ }
299
+
300
+ function setEditorMenu(vanillaData: string[], customData?: userCustomMenu[]) {
301
+ const dataList: editorMenu[] = [];
302
+
303
+ vanillaData.forEach((name) => {
304
+ dataList.push({
305
+ name: name,
306
+ hasIcon: true,
307
+ icon: `${name}Block`,
308
+ action: () => {
309
+ addBlockLocal({ name: name });
310
+ },
311
+ });
312
+ });
313
+
314
+ if (customData) {
315
+ customData.forEach((row) => {
316
+ dataList.push({
317
+ name: row.name,
318
+ hasIcon: iconList.indexOf(row.icon) > -1,
319
+ icon: row.icon,
320
+ action: row.action,
321
+ });
322
+ });
323
+ }
324
+
325
+ return dataList;
326
+ }
327
+
328
+ /**
329
+ * 내부용 이벤트 함수
330
+ */
331
+ // 관련 메뉴 열기
332
+ function activeMenuEvent(count: number, e?: MouseEvent) {
333
+ let $target: HTMLElement;
334
+
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);
346
+ checkDecoActive();
347
+ activeMenu.value = true;
348
+ }
349
+
350
+ // 관련 메뉴 닫기
351
+ function deactiveMenuEvent(e?: (MouseEvent | KeyboardEvent)) {
352
+ activeMenu.value = false;
353
+ activeBlockAddMenu.value = false;
354
+ activeBlockColtrolMenu.value = false;
355
+
356
+ if (e && e.type === "mouseleave") {
357
+ activeLinkBox.value = false;
358
+ }
359
+ }
360
+
361
+ function dataUpdateAction() {
362
+ $child.value.forEach((row: any) => {
363
+ row.updateBlockData();
364
+ });
365
+
366
+ emit("update:modelValue", content.value);
367
+ }
368
+
369
+ // 블럭 추가 로직
370
+ function addBlockLocal({ name, value, time = false }: { name: string; value?: object; time?: boolean }) {
371
+ const block = createBlock(name, value);
372
+
373
+ content.value.splice(activeIdx.value + 1, 0, block);
374
+
375
+ if (time === false) {
376
+ activeIdx.value += 1;
377
+
378
+ setTimeout(() => {
379
+ activeBlockAddMenu.value = false;
380
+ $child.value[activeIdx.value].focus();
381
+ dataUpdateAction();
382
+ activeMenuEvent(activeIdx.value);
383
+ }, 100);
384
+ }
385
+ }
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
+ // 붙여넣기 이벤트
413
+ function pasteEvent(e: ClipboardEvent) {
414
+ e.preventDefault();
415
+ const data = getClipboardData(e.clipboardData as DataTransfer);
416
+
417
+ if (data.type === "text") {
418
+ const targetComponent = $child.value[activeIdx.value];
419
+ const componentType = targetComponent.getType();
420
+
421
+ if (componentType !== "other") {
422
+ targetComponent.pasteEvent(data.value);
423
+ }
424
+ }
425
+ }
426
+
427
+ // 블럭 종류 정의
428
+ function setComponentKind(kind: string) {
429
+ let componentData: any;
430
+ switch (kind) {
431
+ case "image":
432
+ componentData = imageBlock;
433
+ break;
434
+ case "text":
435
+ componentData = textBlock;
436
+ }
437
+
438
+ return componentData;
439
+ }
440
+
441
+ function setMenuPosition($target: HTMLElement) {
442
+ const parentNode = $wrap.value.parentNode;
443
+ const bodyRect = document.body.getBoundingClientRect();
444
+ const wrapRect = $wrap.value.getBoundingClientRect();
445
+ const targetRect = $target.getBoundingClientRect();
446
+ const parentNodeScrollY = parentNode.scrollTop;
447
+ const wrapTop = wrapRect.top - bodyRect.top;
448
+ const targetTop = targetRect.top - bodyRect.top;
449
+ const targetBottom = targetRect.bottom - bodyRect.top;
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
+ }
467
+
468
+ styleMenuPosition.value = bottom;
469
+ leftMenuPosition.value = top;
470
+ }
471
+
472
+ // 블럭 스타일 이벤트
473
+ function setBlockDecoEvent(type: string, url?: string) {
474
+ $child.value[activeIdx.value].setStyles({
475
+ type: type,
476
+ url: url,
477
+ });
478
+ setTimeout(() => {
479
+ checkDecoActive();
480
+ }, 100);
481
+ }
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;
507
+
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
+ */
528
+ // function checkStyleActive(className: string) {
529
+ // return hasClassNameCheckLogic(className);
530
+ // }
531
+
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
+ });
543
+ }
544
+
545
+ // 함수 내보내기
546
+ defineExpose({
547
+ addImageBlock,
548
+ });
549
+
550
+ /**
551
+ * 초기 데이터 확인용 로직
552
+ */
553
+ onMounted(() => {
554
+ dataUpdateAction();
555
+ });
556
+ </script>
557
+
558
+ <style>
559
+ @import "../../core/style/common.css";
560
+ </style>
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div class="dragon-editor">
2
+ <div class="dragon-editor --comment">
3
3
  <p class="d-text-block" :class="data.classList" contenteditable v-html="data.content" @keydown="textKeyboardEvent"
4
4
  @paste="pasteEvent" ref="$block"></p>
5
5
  </div>
@@ -17,7 +17,7 @@ import {
17
17
  getCursor,
18
18
  findEditableElement
19
19
  } from "../../core/utils/index";
20
- import { commentBlock } from "../../types/index";
20
+ import { commentBlock, cursorSelection } from "../../../types/index";
21
21
 
22
22
  const $block = ref();
23
23
  const data = ref<commentBlock>({
@@ -29,6 +29,13 @@ const props = defineProps<{ modelValue: commentBlock }>();
29
29
  const emit = defineEmits<{
30
30
  (e: "update:modelValue", modelValue: commentBlock): void;
31
31
  }>();
32
+ const blockCursorData = ref<cursorSelection>({
33
+ type: "",
34
+ startNode: null,
35
+ startOffset: null,
36
+ endNode: null,
37
+ endOffset: null,
38
+ });
32
39
 
33
40
  data.value = unref(props.modelValue) as commentBlock;
34
41
 
@@ -58,7 +65,7 @@ function updateBlockData() {
58
65
 
59
66
  // 커서위치 재지정
60
67
  if ($block.value.innerHTML.length > 0) {
61
- const cursorData = getArrangementCursorData();
68
+ const cursorData = getArrangementCursorData(blockCursorData.value);
62
69
 
63
70
  data.value.content = $block.value.innerHTML;
64
71
  emit("update:modelValue", data.value);
@@ -69,13 +76,19 @@ function updateBlockData() {
69
76
  cursorData.length
70
77
  );
71
78
 
72
- // 태그 삭제
79
+ // 구조 검수
73
80
  $block.value.childNodes.forEach((child: ChildNode) => {
74
- if (
75
- child.constructor.name !== "Text" &&
76
- child.textContent === ""
77
- ) {
78
- child.remove();
81
+ if (child.constructor.name !== "Text") { // 텍스트가 아닐경우
82
+ if (child.constructor.name !== "HTMLBRElement") { // br 태그 유지
83
+ if (child.textContent === "") { // 빈 태그 삭제
84
+ child.remove();
85
+ } else if ((child as HTMLElement).classList.length === 0) { // 클레스 없는 엘리먼트 처리
86
+ (child as HTMLElement).insertAdjacentHTML("afterend", child.textContent as string);
87
+ child.remove();
88
+ }
89
+ } else {
90
+ (child as HTMLElement).removeAttribute("class");
91
+ }
79
92
  }
80
93
  });
81
94
  }, 100);
@@ -86,10 +99,19 @@ function updateBlockData() {
86
99
 
87
100
  function focus() {
88
101
  setCursor($block.value, 0);
102
+ blockCursorData.value = getCursor();
89
103
  }
90
104
 
91
- function setStyles(kind: string) {
92
- data.value = styleSettings(kind, data.value, $block.value);
105
+ function setStyles(kind: string, url?: string) {
106
+ data.value = styleSettings(
107
+ {
108
+ kind: kind,
109
+ blockData: data.value,
110
+ $target: $block.value,
111
+ url: url,
112
+ cursorData: blockCursorData.value
113
+ }
114
+ );
93
115
  setTimeout(() => {
94
116
  updateBlockData();
95
117
  }, 250);
@@ -0,0 +1,29 @@
1
+ <template>
2
+ <div class="dragon-editor-viewer">
3
+ <template v-for="(row, count) in props.content">
4
+ <p class="d-text-block" v-if="row.type === 'text'" :class="row.classList" v-html="row.content"></p>
5
+
6
+ <template v-if="row.type === 'image'">
7
+ <template v-if="row.webp"> </template>
8
+ <template v-else>
9
+ <div class="d-image-block" :class="row.classList">
10
+ <div class="d-image-area">
11
+ <img class="d-img" :src="row.src" :width="row.width" :height="row.height" :alt="row.caption" loading="lazy">
12
+ </div>
13
+ <p class="d-caption" v-if="row.caption" v-html="row.caption"></p>
14
+ </div>
15
+ </template>
16
+ </template>
17
+ </template>
18
+ </div>
19
+ </template>
20
+
21
+ <script setup lang="ts">
22
+ const props = defineProps<{
23
+ content: any[];
24
+ }>();
25
+ </script>
26
+
27
+ <style>
28
+ @import "../../core/style/viewer.css";
29
+ </style>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dragon-editor",
3
- "version": "2.0.0-beta.1.3",
3
+ "version": "2.0.0-beta.2",
4
4
  "description": "WYSIWYG editor on Nuxt.js",
5
5
  "repository": {
6
6
  "type": "git",