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.
- package/README.md +94 -147
- package/README_en.md +14 -62
- package/dist/module.json +1 -1
- package/dist/module.mjs +8 -0
- package/dist/runtime/core/components/SvgIcon.vue +30 -21
- package/dist/runtime/core/components/editor/ImageBlock.vue +175 -0
- package/dist/runtime/core/components/editor/OlBlock.vue +135 -0
- package/dist/runtime/core/components/editor/TextBlock.vue +77 -31
- package/dist/runtime/core/components/icon/Accept.vue +5 -0
- package/dist/runtime/core/components/icon/ArrowDown.vue +3 -0
- package/dist/runtime/core/components/icon/ArrowUp.vue +3 -0
- package/dist/runtime/core/components/icon/Cancel.vue +5 -0
- package/dist/runtime/core/components/icon/Delete.vue +3 -0
- package/dist/runtime/core/style/common.css +320 -31
- package/dist/runtime/core/style/viewer.css +191 -0
- package/dist/runtime/core/utils/cursor.d.ts +1 -1
- package/dist/runtime/core/utils/cursor.mjs +16 -4
- package/dist/runtime/core/utils/element.d.ts +2 -1
- package/dist/runtime/core/utils/element.mjs +19 -4
- package/dist/runtime/core/utils/index.d.ts +2 -3
- package/dist/runtime/core/utils/index.mjs +62 -5
- package/dist/runtime/core/utils/keyboard.d.ts +1 -1
- package/dist/runtime/core/utils/keyboard.mjs +500 -41
- package/dist/runtime/core/utils/style.d.ts +6 -2
- package/dist/runtime/core/utils/style.mjs +140 -30
- package/dist/runtime/shared/components/DragonEditor.vue +488 -159
- package/dist/runtime/shared/components/DragonEditorComment.vue +42 -32
- package/dist/runtime/shared/components/DragonEditorViewer.vue +30 -2
- package/package.json +1 -1
- package/dist/runtime/core/style/main.d.ts +0 -1
- 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
|
-
|
|
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"
|
|
23
|
-
<button class="d-btn-block"
|
|
24
|
-
<button class="d-btn-block"
|
|
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
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
>
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
</
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
<button class="d-btn">
|
|
82
|
-
<SvgIcon kind="
|
|
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
|
-
</
|
|
46
|
+
</template>
|
|
47
|
+
</div>
|
|
85
48
|
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
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
|
-
|
|
112
|
-
import {
|
|
113
|
-
import {
|
|
114
|
-
|
|
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
|
-
//
|
|
127
|
-
const props =
|
|
128
|
-
modelValue:
|
|
129
|
-
option
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
//
|
|
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
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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 (
|
|
171
|
-
customStyleMenu.value = unref(
|
|
305
|
+
if (option.value.customStyleMenu) {
|
|
306
|
+
customStyleMenu.value = unref(option.value.customStyleMenu);
|
|
172
307
|
}
|
|
173
308
|
|
|
174
|
-
//
|
|
175
|
-
if (
|
|
176
|
-
if (
|
|
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 =
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
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
|
-
|
|
280
|
-
|
|
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(() => {
|
|
288
|
-
|
|
289
|
-
|
|
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
|
|
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 "
|
|
314
|
-
|
|
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 =
|
|
329
|
-
const bottom =
|
|
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
|
-
|
|
336
|
-
|
|
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
|
-
|
|
655
|
+
/**
|
|
656
|
+
* 외부용 함수
|
|
657
|
+
*/
|
|
344
658
|
// function checkStyleActive(className: string) {
|
|
345
659
|
// return hasClassNameCheckLogic(className);
|
|
346
660
|
// }
|
|
347
661
|
|
|
348
|
-
function addImageBlock() {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
|