dragon-editor 2.0.0-beta.2.1.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -0
- package/dist/module.json +1 -1
- package/dist/runtime/core/components/editor/OlBlock.vue +89 -62
- package/dist/runtime/core/components/editor/TextBlock.vue +7 -1
- package/dist/runtime/core/components/editor/UlBlock.vue +162 -0
- package/dist/runtime/core/style/common.css +17 -0
- package/dist/runtime/core/style/viewer.css +14 -0
- package/dist/runtime/core/utils/convertor.d.ts +2 -0
- package/dist/runtime/core/utils/convertor.mjs +39 -0
- package/dist/runtime/core/utils/index.d.ts +1 -0
- package/dist/runtime/core/utils/index.mjs +4 -1
- package/dist/runtime/core/utils/keyboard.mjs +22 -14
- package/dist/runtime/shared/components/DragonEditor.vue +7 -3
- package/dist/runtime/shared/components/DragonEditorComment.vue +60 -27
- package/dist/runtime/shared/components/DragonEditorViewer.vue +2 -17
- package/package.json +57 -57
package/README.md
CHANGED
|
@@ -66,6 +66,13 @@ editor.value.addImageBlock({
|
|
|
66
66
|
});
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
+
2. 저장
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
editor.value.updateBlockData();
|
|
73
|
+
// do somthing
|
|
74
|
+
```
|
|
75
|
+
|
|
69
76
|
### 코멘트
|
|
70
77
|
|
|
71
78
|
```vue
|
|
@@ -104,6 +111,19 @@ editor.value.setStyles("decorationBold");
|
|
|
104
111
|
- `decorationUnderline` : 밑줄
|
|
105
112
|
- `decorationStrikethrough` : 취소선
|
|
106
113
|
|
|
114
|
+
2. 저장
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
editor.value.updateBlockData();
|
|
118
|
+
// do somthing
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
3. 포커스
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
editor.value.focus();
|
|
125
|
+
```
|
|
126
|
+
|
|
107
127
|
### 뷰어
|
|
108
128
|
|
|
109
129
|
```vue
|
package/dist/module.json
CHANGED
|
@@ -1,21 +1,28 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<ol class="d-ol-block"
|
|
3
|
-
<li class="d-li-item" contenteditable ref="$
|
|
2
|
+
<ol class="d-ol-block" @keydown="textKeyboardEvent" ref="$ol" :key="updateCount">
|
|
3
|
+
<li class="d-li-item" v-for="(row, i) in data.childList" :key="i" :class="row.classList" contenteditable ref="$item" v-html="row.content"></li>
|
|
4
4
|
</ol>
|
|
5
5
|
</template>
|
|
6
6
|
|
|
7
7
|
<script setup lang="ts">
|
|
8
8
|
// @ts-ignore
|
|
9
9
|
import { ref, unref } from "#imports";
|
|
10
|
-
import { cursorSelection, listBlock, styleFunctionArgument } from "../../../../types";
|
|
11
|
-
import { getArrangementCursorData, setCursor, pasteText, styleSettings, keyboardEvent } from "../../utils";
|
|
10
|
+
import { cursorSelection, liItem, listBlock, styleFunctionArgument } from "../../../../types";
|
|
11
|
+
import { getArrangementCursorData, setCursor, pasteText, styleSettings, keyboardEvent, getCursor, findEditableElement } from "../../utils";
|
|
12
12
|
|
|
13
|
-
const
|
|
13
|
+
const updateCount = ref<number>(0);
|
|
14
|
+
const $ol = ref();
|
|
15
|
+
const $item = ref();
|
|
16
|
+
const itemIdx = ref<number>(0);
|
|
14
17
|
const data = ref<listBlock>({
|
|
15
18
|
type: "",
|
|
16
19
|
id: "",
|
|
17
|
-
|
|
18
|
-
|
|
20
|
+
childList: [
|
|
21
|
+
{
|
|
22
|
+
classList: [],
|
|
23
|
+
content: "",
|
|
24
|
+
},
|
|
25
|
+
],
|
|
19
26
|
});
|
|
20
27
|
const props = defineProps<{ modelValue: listBlock; cursorData: cursorSelection }>();
|
|
21
28
|
const emit = defineEmits<{
|
|
@@ -25,13 +32,12 @@ const emit = defineEmits<{
|
|
|
25
32
|
}>();
|
|
26
33
|
data.value = unref(props.modelValue) as listBlock;
|
|
27
34
|
|
|
28
|
-
if(data.value.childList.length === 0){
|
|
29
|
-
|
|
35
|
+
if (data.value.childList.length === 0) {
|
|
30
36
|
}
|
|
31
37
|
|
|
32
38
|
// 키보드 이벤트 할당
|
|
33
39
|
function textKeyboardEvent(e: KeyboardEvent) {
|
|
34
|
-
keyboardEvent("
|
|
40
|
+
keyboardEvent("list", e, emit, updateBlockData);
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
/**
|
|
@@ -40,64 +46,85 @@ function textKeyboardEvent(e: KeyboardEvent) {
|
|
|
40
46
|
|
|
41
47
|
// 데이터 정규화 및 검수
|
|
42
48
|
function updateBlockData() {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
49
|
+
const $block = $ol.value;
|
|
50
|
+
const $childList = $block.querySelectorAll("li");
|
|
51
|
+
const childData: liItem[] = [];
|
|
52
|
+
const cursorData = getCursor();
|
|
53
|
+
|
|
54
|
+
$childList.forEach((row) => {
|
|
55
|
+
row.childNodes.forEach((child: ChildNode) => {
|
|
56
|
+
const $child = child as HTMLElement;
|
|
57
|
+
|
|
58
|
+
if (child.constructor.name !== "Text") {
|
|
59
|
+
// 텍스트가 아닐경우
|
|
60
|
+
if (child.constructor.name !== "HTMLBRElement") {
|
|
61
|
+
// br 태그 유지
|
|
62
|
+
if (child.textContent === "") {
|
|
63
|
+
// 빈 태그 삭제
|
|
64
|
+
child.remove();
|
|
65
|
+
} else if ($child.classList.length === 0) {
|
|
66
|
+
// 클레스 없는 엘리먼트 처리
|
|
67
|
+
$child.insertAdjacentHTML("afterend", $child.innerHTML);
|
|
68
|
+
child.remove();
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
$child.removeAttribute("class");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
childData.push({
|
|
77
|
+
classList: [...row.classList].splice(1),
|
|
78
|
+
content: row.innerHTML,
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
data.value.childList = childData;
|
|
83
|
+
emit("update:modelValue", data.value);
|
|
84
|
+
updateCount.value += 1;
|
|
85
|
+
|
|
86
|
+
if (cursorData.startNode) {
|
|
87
|
+
const editableNode = findEditableElement(cursorData.startNode);
|
|
88
|
+
let childIdx = -1;
|
|
89
|
+
|
|
90
|
+
$childList.forEach((row, count) => {
|
|
91
|
+
if (row === editableNode) {
|
|
92
|
+
childIdx = count;
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (childIdx > -1) {
|
|
97
|
+
// 기본 로직
|
|
98
|
+
itemIdx.value = childIdx;
|
|
99
|
+
setTimeout(() => {
|
|
100
|
+
const afterChildList = $ol.value.querySelectorAll("li");
|
|
101
|
+
setCursor(afterChildList[childIdx], cursorData.startOffset as number);
|
|
102
|
+
}, 100);
|
|
103
|
+
} else {
|
|
104
|
+
// 중간 엔터
|
|
105
|
+
$childList.forEach((row, count) => {
|
|
106
|
+
if (row === (cursorData.startNode as Node).parentNode) {
|
|
107
|
+
childIdx = count;
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
setTimeout(() => {
|
|
112
|
+
const afterChildList = $ol.value.querySelectorAll("li");
|
|
113
|
+
setCursor(afterChildList[childIdx], cursorData.startOffset as number);
|
|
114
|
+
}, 100);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
85
117
|
}
|
|
86
118
|
|
|
87
119
|
// 포커스
|
|
88
|
-
function focus(
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
// } else {
|
|
92
|
-
// const childCount = $block.value.childNodes.length;
|
|
93
|
-
// const targetChild = $block.value.childNodes[childCount - 1];
|
|
94
|
-
// setCursor(targetChild, 0);
|
|
95
|
-
// }
|
|
120
|
+
function focus() {
|
|
121
|
+
const childList = $ol.value.querySelectorAll(".d-li-item");
|
|
122
|
+
setCursor(childList[itemIdx.value], 0);
|
|
96
123
|
}
|
|
97
124
|
|
|
98
125
|
// 블럭 위치 주기
|
|
99
126
|
function getBoundingClientRect() {
|
|
100
|
-
|
|
127
|
+
return $ol.value.parentNode.getBoundingClientRect();
|
|
101
128
|
}
|
|
102
129
|
|
|
103
130
|
// 타입 전달
|
|
@@ -115,7 +142,7 @@ function setStyles({ type, url }: styleFunctionArgument) {
|
|
|
115
142
|
data.value = styleSettings({
|
|
116
143
|
kind: type,
|
|
117
144
|
blockData: data.value,
|
|
118
|
-
$target:
|
|
145
|
+
$target: $item[itemIdx.value],
|
|
119
146
|
url: url,
|
|
120
147
|
cursorData: props.cursorData,
|
|
121
148
|
});
|
|
@@ -117,7 +117,13 @@ function updateBlockData() {
|
|
|
117
117
|
// 포커스
|
|
118
118
|
function focus(type: string = "first") {
|
|
119
119
|
if (type === "first") {
|
|
120
|
-
|
|
120
|
+
if ($block.value.childNodes.length > 0) {
|
|
121
|
+
setTimeout(() => {
|
|
122
|
+
setCursor($block.value.childNodes[0], 0);
|
|
123
|
+
}, 100);
|
|
124
|
+
} else {
|
|
125
|
+
setCursor($block.value, 0);
|
|
126
|
+
}
|
|
121
127
|
} else {
|
|
122
128
|
const childCount = $block.value.childNodes.length;
|
|
123
129
|
const targetChild = $block.value.childNodes[childCount - 1];
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<ul class="d-ul-block" @keydown="textKeyboardEvent" ref="$ul" :key="updateCount">
|
|
3
|
+
<li class="d-li-item" v-for="(row, i) in data.childList" :key="i" :class="row.classList" contenteditable ref="$item" v-html="row.content"></li>
|
|
4
|
+
</ul>
|
|
5
|
+
</template>
|
|
6
|
+
|
|
7
|
+
<script setup lang="ts">
|
|
8
|
+
// @ts-ignore
|
|
9
|
+
import { ref, unref } from "#imports";
|
|
10
|
+
import { cursorSelection, liItem, listBlock, styleFunctionArgument } from "../../../../types";
|
|
11
|
+
import { getArrangementCursorData, setCursor, pasteText, styleSettings, keyboardEvent, getCursor, findEditableElement } from "../../utils";
|
|
12
|
+
|
|
13
|
+
const updateCount = ref<number>(0);
|
|
14
|
+
const $ul = ref();
|
|
15
|
+
const $item = ref();
|
|
16
|
+
const itemIdx = ref<number>(0);
|
|
17
|
+
const data = ref<listBlock>({
|
|
18
|
+
type: "",
|
|
19
|
+
id: "",
|
|
20
|
+
childList: [
|
|
21
|
+
{
|
|
22
|
+
classList: [],
|
|
23
|
+
content: "",
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
});
|
|
27
|
+
const props = defineProps<{ modelValue: listBlock; cursorData: cursorSelection }>();
|
|
28
|
+
const emit = defineEmits<{
|
|
29
|
+
(e: "update:modelValue", modelValue: listBlock): void;
|
|
30
|
+
(e: "addBlock", {}: { name: string; value: object }): void;
|
|
31
|
+
(e: "deleteBlockLocal", index?: number): void;
|
|
32
|
+
}>();
|
|
33
|
+
data.value = unref(props.modelValue) as listBlock;
|
|
34
|
+
|
|
35
|
+
if (data.value.childList.length === 0) {
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 키보드 이벤트 할당
|
|
39
|
+
function textKeyboardEvent(e: KeyboardEvent) {
|
|
40
|
+
keyboardEvent("list", e, emit, updateBlockData);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 외부용 함수
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
// 데이터 정규화 및 검수
|
|
48
|
+
function updateBlockData() {
|
|
49
|
+
const $block = $ul.value;
|
|
50
|
+
const $childList = $block.querySelectorAll("li");
|
|
51
|
+
const childData: liItem[] = [];
|
|
52
|
+
const cursorData = getCursor();
|
|
53
|
+
|
|
54
|
+
$childList.forEach((row) => {
|
|
55
|
+
row.childNodes.forEach((child: ChildNode) => {
|
|
56
|
+
const $child = child as HTMLElement;
|
|
57
|
+
|
|
58
|
+
if (child.constructor.name !== "Text") {
|
|
59
|
+
// 텍스트가 아닐경우
|
|
60
|
+
if (child.constructor.name !== "HTMLBRElement") {
|
|
61
|
+
// br 태그 유지
|
|
62
|
+
if (child.textContent === "") {
|
|
63
|
+
// 빈 태그 삭제
|
|
64
|
+
child.remove();
|
|
65
|
+
} else if ($child.classList.length === 0) {
|
|
66
|
+
// 클레스 없는 엘리먼트 처리
|
|
67
|
+
$child.insertAdjacentHTML("afterend", $child.innerHTML);
|
|
68
|
+
child.remove();
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
$child.removeAttribute("class");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
childData.push({
|
|
77
|
+
classList: [...row.classList].splice(1),
|
|
78
|
+
content: row.innerHTML,
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
data.value.childList = childData;
|
|
83
|
+
emit("update:modelValue", data.value);
|
|
84
|
+
updateCount.value += 1;
|
|
85
|
+
|
|
86
|
+
if (cursorData.startNode) {
|
|
87
|
+
const editableNode = findEditableElement(cursorData.startNode);
|
|
88
|
+
let childIdx = -1;
|
|
89
|
+
|
|
90
|
+
$childList.forEach((row, count) => {
|
|
91
|
+
if (row === editableNode) {
|
|
92
|
+
childIdx = count;
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (childIdx > -1) {
|
|
97
|
+
// 기본 로직
|
|
98
|
+
itemIdx.value = childIdx;
|
|
99
|
+
setTimeout(() => {
|
|
100
|
+
const afterChildList = $ul.value.querySelectorAll("li");
|
|
101
|
+
setCursor(afterChildList[childIdx], cursorData.startOffset as number);
|
|
102
|
+
}, 100);
|
|
103
|
+
} else {
|
|
104
|
+
// 중간 엔터
|
|
105
|
+
$childList.forEach((row, count) => {
|
|
106
|
+
if (row === (cursorData.startNode as Node).parentNode) {
|
|
107
|
+
childIdx = count;
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
setTimeout(() => {
|
|
112
|
+
const afterChildList = $ul.value.querySelectorAll("li");
|
|
113
|
+
setCursor(afterChildList[childIdx], cursorData.startOffset as number);
|
|
114
|
+
}, 100);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 포커스
|
|
120
|
+
function focus() {
|
|
121
|
+
const childList = $ul.value.querySelectorAll(".d-li-item");
|
|
122
|
+
setCursor(childList[itemIdx.value], 0);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 블럭 위치 주기
|
|
126
|
+
function getBoundingClientRect() {
|
|
127
|
+
return $ul.value.parentNode.getBoundingClientRect();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 타입 전달
|
|
131
|
+
function getType() {
|
|
132
|
+
return data.value.type;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 붙여넣기 이벤트
|
|
136
|
+
function pasteEvent(text: string) {
|
|
137
|
+
pasteText("text", text);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 텍스트 스타일 지정
|
|
141
|
+
function setStyles({ type, url }: styleFunctionArgument) {
|
|
142
|
+
data.value = styleSettings({
|
|
143
|
+
kind: type,
|
|
144
|
+
blockData: data.value,
|
|
145
|
+
$target: $item[itemIdx.value],
|
|
146
|
+
url: url,
|
|
147
|
+
cursorData: props.cursorData,
|
|
148
|
+
});
|
|
149
|
+
setTimeout(() => {
|
|
150
|
+
updateBlockData();
|
|
151
|
+
}, 250);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
defineExpose({
|
|
155
|
+
updateBlockData,
|
|
156
|
+
focus,
|
|
157
|
+
getType,
|
|
158
|
+
pasteEvent,
|
|
159
|
+
setStyles,
|
|
160
|
+
getBoundingClientRect,
|
|
161
|
+
});
|
|
162
|
+
</script>
|
|
@@ -168,6 +168,23 @@
|
|
|
168
168
|
content: "Write text";
|
|
169
169
|
color: #ccc;
|
|
170
170
|
}
|
|
171
|
+
.dragon-editor .d-ul-block {
|
|
172
|
+
padding-left: 30px;
|
|
173
|
+
cursor: text;
|
|
174
|
+
list-style: disc;
|
|
175
|
+
}
|
|
176
|
+
.dragon-editor .d-ul-block .d-li-item {
|
|
177
|
+
list-style: inherit;
|
|
178
|
+
outline: 0;
|
|
179
|
+
}
|
|
180
|
+
.dragon-editor .d-ul-block .d-li-item:empty {
|
|
181
|
+
min-height: 1.6em;
|
|
182
|
+
}
|
|
183
|
+
.dragon-editor .d-ul-block .d-li-item:empty::after {
|
|
184
|
+
display: inline;
|
|
185
|
+
content: "Write text";
|
|
186
|
+
color: #ccc;
|
|
187
|
+
}
|
|
171
188
|
.dragon-editor .d-image-block {
|
|
172
189
|
display: flex;
|
|
173
190
|
flex-direction: column;
|
|
@@ -189,3 +189,17 @@
|
|
|
189
189
|
color: #ccc;
|
|
190
190
|
font-size: 1rem;
|
|
191
191
|
}
|
|
192
|
+
.dragon-editor-viewer .d-ol-block {
|
|
193
|
+
padding-left: 30px;
|
|
194
|
+
list-style: decimal;
|
|
195
|
+
}
|
|
196
|
+
.dragon-editor-viewer .d-ol-block .d-li-item {
|
|
197
|
+
list-style: inherit;
|
|
198
|
+
}
|
|
199
|
+
.dragon-editor-viewer .d-ul-block {
|
|
200
|
+
padding-left: 30px;
|
|
201
|
+
list-style: disc;
|
|
202
|
+
}
|
|
203
|
+
.dragon-editor-viewer .d-ul-block .d-li-item {
|
|
204
|
+
list-style: inherit;
|
|
205
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export function convertToHTML(data) {
|
|
2
|
+
let htmlStructure = "";
|
|
3
|
+
data.forEach((row) => {
|
|
4
|
+
switch (row.type) {
|
|
5
|
+
case "text":
|
|
6
|
+
htmlStructure += `<p class="d-text-block ${row.classList.join(" ")}">${row.content}</p>`;
|
|
7
|
+
break;
|
|
8
|
+
case "image":
|
|
9
|
+
htmlStructure += `
|
|
10
|
+
<div class="d-image-block ${row.classList.join(" ")}">
|
|
11
|
+
<div class="d-image-area">
|
|
12
|
+
<img class="d-img" src="${row.src}" width="${row.width}" height="${row.height}" alt="${row.caption}" loading="lazy">
|
|
13
|
+
</div>
|
|
14
|
+
`;
|
|
15
|
+
if (row.caption) {
|
|
16
|
+
htmlStructure += `<p class="d-caption">${row.caption}</p>`;
|
|
17
|
+
}
|
|
18
|
+
htmlStructure += `</div>`;
|
|
19
|
+
break;
|
|
20
|
+
case "ol":
|
|
21
|
+
htmlStructure += `<ol class="d-ol-block ${row.classList.join(" ")}">`;
|
|
22
|
+
row.childList.forEach((child) => {
|
|
23
|
+
htmlStructure += `<li class="d-li-item ${child.classList.join(" ")}">${child.content}</li>`;
|
|
24
|
+
});
|
|
25
|
+
htmlStructure += `</ol>`;
|
|
26
|
+
break;
|
|
27
|
+
case "ul":
|
|
28
|
+
htmlStructure += `<ul class="d-ul-block ${row.classList.join(" ")}">`;
|
|
29
|
+
row.childList.forEach((child) => {
|
|
30
|
+
htmlStructure += `<li class="d-li-item ${child.classList.join(" ")}">${child.content}</li>`;
|
|
31
|
+
});
|
|
32
|
+
htmlStructure += `</ul>`;
|
|
33
|
+
break;
|
|
34
|
+
default:
|
|
35
|
+
htmlStructure += row.innerHTML;
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
return htmlStructure;
|
|
39
|
+
}
|
|
@@ -46,7 +46,6 @@ function createImageBlock(data) {
|
|
|
46
46
|
src: data.src,
|
|
47
47
|
width: data.width,
|
|
48
48
|
height: data.height,
|
|
49
|
-
webp: data.webp,
|
|
50
49
|
caption: data.caption
|
|
51
50
|
};
|
|
52
51
|
}
|
|
@@ -66,6 +65,9 @@ function createlistBlock(type = "ul") {
|
|
|
66
65
|
export function createBlock(name, value) {
|
|
67
66
|
let blockData;
|
|
68
67
|
switch (name) {
|
|
68
|
+
case "ul":
|
|
69
|
+
blockData = createlistBlock();
|
|
70
|
+
break;
|
|
69
71
|
case "ol":
|
|
70
72
|
blockData = createlistBlock("ol");
|
|
71
73
|
break;
|
|
@@ -81,3 +83,4 @@ export * from "./keyboard.mjs";
|
|
|
81
83
|
export * from "./cursor.mjs";
|
|
82
84
|
export * from "./style.mjs";
|
|
83
85
|
export * from "./element.mjs";
|
|
86
|
+
export * from "./convertor.mjs";
|
|
@@ -2,15 +2,15 @@ import { getCursor, setCursor } from "./cursor.mjs";
|
|
|
2
2
|
import { findEditableElement, findChildNumber, findLiElement } from "./element.mjs";
|
|
3
3
|
import { getTagName } from "./style.mjs";
|
|
4
4
|
let enterCount = 0;
|
|
5
|
-
function enterEvent(type, event, action) {
|
|
5
|
+
function enterEvent(type, event, action, update) {
|
|
6
6
|
if (event.code === "Enter") {
|
|
7
7
|
event.preventDefault();
|
|
8
8
|
const useShift = event.shiftKey;
|
|
9
9
|
switch (type) {
|
|
10
|
-
case "
|
|
10
|
+
case "list":
|
|
11
11
|
if (useShift === false) {
|
|
12
12
|
if (enterCount == 0) {
|
|
13
|
-
listEnterEvent(
|
|
13
|
+
listEnterEvent(action, update);
|
|
14
14
|
}
|
|
15
15
|
} else {
|
|
16
16
|
addBrEvent();
|
|
@@ -27,7 +27,7 @@ function enterEvent(type, event, action) {
|
|
|
27
27
|
default:
|
|
28
28
|
if (useShift === false) {
|
|
29
29
|
if (enterCount == 0) {
|
|
30
|
-
textEnterEvent(
|
|
30
|
+
textEnterEvent(action);
|
|
31
31
|
}
|
|
32
32
|
} else {
|
|
33
33
|
addBrEvent();
|
|
@@ -39,7 +39,7 @@ function enterEvent(type, event, action) {
|
|
|
39
39
|
}, 150);
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
|
-
function listEnterEvent(
|
|
42
|
+
function listEnterEvent(action, update) {
|
|
43
43
|
const cursorData = getCursor();
|
|
44
44
|
if (cursorData.startNode) {
|
|
45
45
|
const editableElement = findEditableElement(cursorData.startNode);
|
|
@@ -74,9 +74,16 @@ function listEnterEvent(event, action) {
|
|
|
74
74
|
endOffset = cursorData.endOffset;
|
|
75
75
|
}
|
|
76
76
|
if (editableElement.childNodes.length === 0 || endChildIdx === editableElement.childNodes.length - 1 && editableElement.childNodes[endChildIdx].textContent.length === endOffset) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
if (editableElement.childNodes.length === 0) {
|
|
78
|
+
editableElement.remove();
|
|
79
|
+
action("addBlock", {
|
|
80
|
+
name: "text"
|
|
81
|
+
});
|
|
82
|
+
} else {
|
|
83
|
+
editableElement.insertAdjacentHTML("afterend", `<li class="d-li-item" contenteditable></li>`);
|
|
84
|
+
setCursor(editableElement.nextSibling, 0);
|
|
85
|
+
update();
|
|
86
|
+
}
|
|
80
87
|
} else {
|
|
81
88
|
editableElement.childNodes.forEach((child, count) => {
|
|
82
89
|
const text = child.textContent;
|
|
@@ -125,14 +132,15 @@ function listEnterEvent(event, action) {
|
|
|
125
132
|
}
|
|
126
133
|
});
|
|
127
134
|
editableElement.innerHTML = preHTMLStructor;
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
135
|
+
editableElement.insertAdjacentHTML("afterend", `<li class="d-li-item ${editableElementClassList.join(" ")}">${nextHTMLStructor}</li>`);
|
|
136
|
+
setTimeout(() => {
|
|
137
|
+
setCursor(editableElement.nextSibling.childNodes[0], 0);
|
|
138
|
+
update();
|
|
139
|
+
}, 100);
|
|
132
140
|
}
|
|
133
141
|
}
|
|
134
142
|
}
|
|
135
|
-
function textEnterEvent(
|
|
143
|
+
function textEnterEvent(action) {
|
|
136
144
|
const cursorData = getCursor();
|
|
137
145
|
if (cursorData.startNode) {
|
|
138
146
|
const editableElement = findEditableElement(cursorData.startNode);
|
|
@@ -245,7 +253,7 @@ function backspaceEvent(type, event, action, update) {
|
|
|
245
253
|
}
|
|
246
254
|
}
|
|
247
255
|
export function keyboardEvent(type, event, action, update) {
|
|
248
|
-
enterEvent(type, event, action);
|
|
256
|
+
enterEvent(type, event, action, update);
|
|
249
257
|
backspaceEvent(type, event, action, update);
|
|
250
258
|
}
|
|
251
259
|
export function getClipboardData(data) {
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
<template v-else>
|
|
59
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
60
|
|
|
61
|
-
<div class="d-child-list" :class="{ '--active': item.active }">
|
|
61
|
+
<div v-if="item.target.indexOf(content[activeIdx].type) > -1" class="d-child-list" :class="{ '--active': item.active }">
|
|
62
62
|
<button class="d-child-btn" v-for="(child, k) in item.childList" :key="k" @click="child.action(count, j)">{{ child.name }}</button>
|
|
63
63
|
</div>
|
|
64
64
|
</template>
|
|
@@ -84,6 +84,7 @@ import SvgIcon from "../../core/components/SvgIcon.vue";
|
|
|
84
84
|
import textBlock from "../../core/components/editor/TextBlock.vue";
|
|
85
85
|
import imageBlock from "../../core/components/editor/ImageBlock.vue";
|
|
86
86
|
import olBlock from "../../core/components/editor/OlBlock.vue";
|
|
87
|
+
import ulBlock from "../../core/components/editor/UlBlock.vue";
|
|
87
88
|
|
|
88
89
|
// 기본 정보
|
|
89
90
|
const props = defineProps<{
|
|
@@ -92,7 +93,7 @@ const props = defineProps<{
|
|
|
92
93
|
}>();
|
|
93
94
|
const modelValue = ref<editorContentType>([]);
|
|
94
95
|
const option = ref<editorOptions>({
|
|
95
|
-
blockMenu: ["text"],
|
|
96
|
+
blockMenu: ["text", "ol", "ul"],
|
|
96
97
|
// blockMenu: ["text", "ol", "ul", "table", "quotation"], // TODO : 다른 블럭 만들기
|
|
97
98
|
});
|
|
98
99
|
|
|
@@ -557,6 +558,9 @@ function setComponentKind(kind: string) {
|
|
|
557
558
|
let componentData: any;
|
|
558
559
|
|
|
559
560
|
switch (kind) {
|
|
561
|
+
case "ul":
|
|
562
|
+
componentData = ulBlock;
|
|
563
|
+
break;
|
|
560
564
|
case "ol":
|
|
561
565
|
componentData = olBlock;
|
|
562
566
|
break;
|
|
@@ -675,7 +679,7 @@ function addImageBlock({ src, width, height, webp, caption }: { src: string; wid
|
|
|
675
679
|
// 함수 내보내기
|
|
676
680
|
defineExpose({
|
|
677
681
|
addImageBlock,
|
|
678
|
-
dataUpdateAction
|
|
682
|
+
dataUpdateAction,
|
|
679
683
|
});
|
|
680
684
|
|
|
681
685
|
/**
|
|
@@ -12,7 +12,6 @@ import { commentBlock, cursorSelection } from "../../../types/index";
|
|
|
12
12
|
|
|
13
13
|
const $block = ref();
|
|
14
14
|
const data = ref<commentBlock>({
|
|
15
|
-
type: "comment",
|
|
16
15
|
classList: [],
|
|
17
16
|
content: "",
|
|
18
17
|
});
|
|
@@ -54,34 +53,68 @@ function updateBlockData() {
|
|
|
54
53
|
|
|
55
54
|
// 커서위치 재지정
|
|
56
55
|
if ($block.value.innerHTML.length > 0) {
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
56
|
+
const preCursorData = getCursor();
|
|
57
|
+
|
|
58
|
+
if (preCursorData.startNode !== null) {
|
|
59
|
+
const cursorData = getArrangementCursorData(blockCursorData.value);
|
|
60
|
+
|
|
61
|
+
data.value.content = $block.value.innerHTML;
|
|
62
|
+
emit("update:modelValue", data.value);
|
|
63
|
+
|
|
64
|
+
setTimeout(() => {
|
|
65
|
+
if ($block.value) {
|
|
66
|
+
setCursor($block.value.childNodes[cursorData.childCount], cursorData.length);
|
|
67
|
+
|
|
68
|
+
// 구조 검수
|
|
69
|
+
$block.value.childNodes.forEach((child: ChildNode) => {
|
|
70
|
+
const $child = child as HTMLElement;
|
|
71
|
+
|
|
72
|
+
if (child.constructor.name !== "Text") {
|
|
73
|
+
// 텍스트가 아닐경우
|
|
74
|
+
if (child.constructor.name !== "HTMLBRElement") {
|
|
75
|
+
// br 태그 유지
|
|
76
|
+
if (child.textContent === "") {
|
|
77
|
+
// 빈 태그 삭제
|
|
78
|
+
child.remove();
|
|
79
|
+
} else if ($child.classList.length === 0) {
|
|
80
|
+
// 클레스 없는 엘리먼트 처리
|
|
81
|
+
$child.insertAdjacentHTML("afterend", $child.innerHTML);
|
|
82
|
+
child.remove();
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
$child.removeAttribute("class");
|
|
86
|
+
}
|
|
78
87
|
}
|
|
79
|
-
}
|
|
80
|
-
(child as HTMLElement).removeAttribute("class");
|
|
81
|
-
}
|
|
88
|
+
});
|
|
82
89
|
}
|
|
83
|
-
});
|
|
84
|
-
}
|
|
90
|
+
}, 100);
|
|
91
|
+
} else {
|
|
92
|
+
if ($block.value) {
|
|
93
|
+
$block.value.childNodes.forEach((child: ChildNode) => {
|
|
94
|
+
const $child = child as HTMLElement;
|
|
95
|
+
|
|
96
|
+
if (child.constructor.name !== "Text") {
|
|
97
|
+
// 텍스트가 아닐경우
|
|
98
|
+
if (child.constructor.name !== "HTMLBRElement") {
|
|
99
|
+
// br 태그 유지
|
|
100
|
+
if (child.textContent === "") {
|
|
101
|
+
// 빈 태그 삭제
|
|
102
|
+
child.remove();
|
|
103
|
+
} else if ($child.classList.length === 0) {
|
|
104
|
+
// 클레스 없는 엘리먼트 처리
|
|
105
|
+
$child.insertAdjacentHTML("afterend", $child.innerHTML);
|
|
106
|
+
child.remove();
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
$child.removeAttribute("class");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
data.value.content = $block.value.innerHTML;
|
|
115
|
+
emit("update:modelValue", data.value);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
85
118
|
} else {
|
|
86
119
|
emit("update:modelValue", data.value);
|
|
87
120
|
}
|
|
@@ -1,25 +1,10 @@
|
|
|
1
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>
|
|
2
|
+
<div class="dragon-editor-viewer" v-html="convertToHTML(props.content)"></div>
|
|
19
3
|
</template>
|
|
20
4
|
|
|
21
5
|
<script setup lang="ts">
|
|
22
6
|
import { editorContentType } from "../../../types";
|
|
7
|
+
import { convertToHTML } from "../../core/utils";
|
|
23
8
|
|
|
24
9
|
const props = defineProps<{
|
|
25
10
|
content: editorContentType;
|
package/package.json
CHANGED
|
@@ -1,58 +1,58 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
2
|
+
"name": "dragon-editor",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "WYSIWYG editor on Nuxt.js",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/lovefields/dragonEditor.git"
|
|
8
|
+
},
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"type": "module",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/types.d.ts",
|
|
14
|
+
"import": "./dist/module.mjs",
|
|
15
|
+
"require": "./dist/module.cjs"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"main": "./dist/module.cjs",
|
|
19
|
+
"types": "./dist/types.d.ts",
|
|
20
|
+
"files": [
|
|
21
|
+
"dist"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"prepack": "nuxt-module-build",
|
|
25
|
+
"dev": "nuxi dev playground",
|
|
26
|
+
"dev:build": "nuxi build playground",
|
|
27
|
+
"dev:prepare": "nuxt-module-build --stub && nuxi prepare playground",
|
|
28
|
+
"release": "npm run lint && npm run test && npm run prepack && changelogen --release && npm publish && git push --follow-tags",
|
|
29
|
+
"lint": "eslint .",
|
|
30
|
+
"test": "vitest run",
|
|
31
|
+
"test:watch": "vitest watch"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@nuxt/kit": "^3.5.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@nuxt/eslint-config": "^0.1.1",
|
|
38
|
+
"@nuxt/module-builder": "^0.3.1",
|
|
39
|
+
"@nuxt/schema": "^3.5.0",
|
|
40
|
+
"@nuxt/test-utils": "^3.5.0",
|
|
41
|
+
"@types/node": "^18",
|
|
42
|
+
"changelogen": "^0.5.3",
|
|
43
|
+
"eslint": "^8.40.0",
|
|
44
|
+
"nuxt": "^3.5.0",
|
|
45
|
+
"vitest": "^0.31.0"
|
|
46
|
+
},
|
|
47
|
+
"keywords": [
|
|
48
|
+
"editor",
|
|
49
|
+
"wysiwyg-editor",
|
|
50
|
+
"korean",
|
|
51
|
+
"language"
|
|
52
|
+
],
|
|
53
|
+
"author": "Lovefields <lovefield@dico.me> (https://dico.me)",
|
|
54
|
+
"bugs": {
|
|
55
|
+
"url": "https://github.com/lovefields/dragonEditor/issues"
|
|
56
|
+
},
|
|
57
|
+
"homepage": "https://github.com/lovefields/dragonEditor#readme"
|
|
58
|
+
}
|