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.
- package/README.md +72 -135
- 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 +174 -0
- package/dist/runtime/core/components/editor/TextBlock.vue +76 -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 +260 -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.mjs +8 -4
- package/dist/runtime/core/utils/index.d.ts +2 -3
- package/dist/runtime/core/utils/index.mjs +47 -6
- package/dist/runtime/core/utils/keyboard.d.ts +1 -1
- package/dist/runtime/core/utils/keyboard.mjs +264 -40
- package/dist/runtime/core/utils/style.d.ts +6 -2
- package/dist/runtime/core/utils/style.mjs +125 -30
- package/dist/runtime/shared/components/DragonEditor.vue +356 -157
- package/dist/runtime/shared/components/DragonEditorComment.vue +33 -11
- package/dist/runtime/shared/components/DragonEditorViewer.vue +28 -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,239 @@
|
|
|
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" :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"
|
|
23
|
-
|
|
24
|
-
|
|
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-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
>
|
|
70
|
-
<SvgIcon kind="
|
|
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
|
-
</
|
|
48
|
+
</template>
|
|
49
|
+
</div>
|
|
79
50
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
127
|
-
const props =
|
|
128
|
-
modelValue:
|
|
129
|
-
option
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
//
|
|
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
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
//
|
|
167
|
-
blockMenu.value = setEditorMenu(
|
|
223
|
+
// 블럭 추가 메뉴 설정
|
|
224
|
+
blockMenu.value = setEditorMenu(option.value.blockMenu as string[], unref(option.value.customBlockMenu) as userCustomMenu[]);
|
|
168
225
|
|
|
169
226
|
// 유저 커스텀 스타일 메뉴
|
|
170
|
-
if (
|
|
171
|
-
customStyleMenu.value = unref(
|
|
227
|
+
if (option.value.customStyleMenu) {
|
|
228
|
+
customStyleMenu.value = unref(option.value.customStyleMenu);
|
|
172
229
|
}
|
|
173
230
|
|
|
174
|
-
//
|
|
175
|
-
if (
|
|
176
|
-
if (
|
|
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 =
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
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
|
-
|
|
280
|
-
|
|
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(() => {
|
|
288
|
-
|
|
289
|
-
|
|
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 !== "
|
|
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 "
|
|
314
|
-
|
|
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 =
|
|
329
|
-
const bottom =
|
|
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
|
-
|
|
336
|
-
|
|
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
|
-
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
|