dragon-editor 2.0.0-beta.1 → 2.0.0-beta.1.3

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/dist/module.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "dragon-editor",
3
3
  "configKey": "dragon-editor",
4
- "version": "2.0.0-beta.1"
4
+ "version": "2.0.0-beta.1.3"
5
5
  }
@@ -0,0 +1,361 @@
1
+ <template>
2
+ <div class="dragon-editor" @paste="pasteEvent" ref="$wrap" @mouseleave="deactiveMenuEvent"
3
+ @keydown="deactiveMenuEvent"
4
+ >
5
+ <div class="d-left-menu" :class="{'--active' : activeMenu}" :style="{top:`${leftMenuPosition}px`}">
6
+ <div class="d-add-block">
7
+ <button class="d-btn-menu-pop"></button>
8
+
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>
13
+ <p class="d-name">{{ row.name }}</p>
14
+ </button>
15
+ </div>
16
+ </div>
17
+
18
+ <div class="d-control-block">
19
+ <button class="d-btn-block-pop"></button>
20
+
21
+ <div class="d-block-list">
22
+ <button class="d-btn-block"></button>
23
+ <button class="d-btn-block"></button>
24
+ <button class="d-btn-block"></button>
25
+ </div>
26
+ </div>
27
+ </div>
28
+
29
+ <div class="d-style-menu" :class="{'--active' : activeMenu}" :style="{top:`${styleMenuPosition}px`}">
30
+ <div class="d-column">
31
+ <button class="d-btn" :class="{'--active' : checkAlignActive('d-align-left')}"
32
+ @click="setBlockEvent('alignLeft')"
33
+ >
34
+ <SvgIcon kind="alignLeft"/>
35
+ </button>
36
+ <button class="d-btn" :class="{'--active' : checkAlignActive('d-align-center')}"
37
+ @click="setBlockEvent('alignCenter')"
38
+ >
39
+ <SvgIcon kind="alignCenter"/>
40
+ </button>
41
+ <button class="d-btn" :class="{'--active' : checkAlignActive('d-align-right')}"
42
+ @click="setBlockEvent('alignRight')"
43
+ >
44
+ <SvgIcon kind="alignRight"/>
45
+ </button>
46
+ </div>
47
+
48
+ <div class="d-column">
49
+ <button class="d-btn" :class="{'--active' : activeStyle.bold}"
50
+ @click="setBlockEvent('decorationBold')"
51
+ >
52
+ <SvgIcon kind="decorationBold"/>
53
+ </button>
54
+
55
+ <button class="d-btn" :class="{'--active' : activeStyle.italic}"
56
+ @click="setBlockEvent('decorationItalic')"
57
+ >
58
+ <SvgIcon kind="decorationItalic"/>
59
+ </button>
60
+
61
+ <button class="d-btn" :class="{'--active' : activeStyle.underline}"
62
+ @click="setBlockEvent('decorationUnderline')"
63
+ >
64
+ <SvgIcon kind="decorationUnderline"/>
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"/>
77
+ </button>
78
+ </div>
79
+
80
+ <div class="d-column">
81
+ <button class="d-btn">
82
+ <SvgIcon kind="codeBlock"/>
83
+ </button>
84
+ </div>
85
+
86
+ <div v-if="customStyleMenu.length > 0" class="d-column">
87
+ <!-- customStyleMenu-->
88
+ </div>
89
+ </div>
90
+
91
+ <div
92
+ class="d-row-block"
93
+ v-for="(row,count) in content"
94
+ :key="count"
95
+ @click="activeIdx = count"
96
+ @mouseenter="activeMenuEvent"
97
+ @mousemove="activeMenuEvent"
98
+ @mouseup="activeMenuEvent"
99
+ >
100
+ <component
101
+ ref="$child"
102
+ v-model="content[count]"
103
+ :is="setComponentKind(row.type)"
104
+ @addBlock="addBlockLocal"
105
+ />
106
+ </div>
107
+ </div>
108
+ </template>
109
+
110
+ <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";
121
+
122
+ // components
123
+ import SvgIcon from "../../core/components/SvgIcon.vue";
124
+ import textBlock from "../../core/components/editor/TextBlock.vue";
125
+
126
+ // props
127
+ const props = withDefaults(defineProps<{ modelValue: editorContentType, option?: editorOptions }>(), {
128
+ modelValue: () => [],
129
+ option: () => {
130
+ return {
131
+ blockMenu: ["text", "ol", "ul", "table", "quotation"]
132
+ }
133
+ },
134
+ });
135
+ const emit = defineEmits<{
136
+ (e: "update:modelValue", modelValue: editorContentType): void;
137
+ }>();
138
+
139
+ // Editor data
140
+ const $wrap = ref();
141
+ const $child = ref();
142
+ const activeMenu = ref<boolean>(false);
143
+ const leftMenuPosition = ref<number>(0);
144
+ const styleMenuPosition = ref<number>(0);
145
+ const iconList = ["textBlock", "imageBlock", "ulBlock", "olBlock", "quotationBlock", "tableBlock"];
146
+ const blockMenu = ref<editorMenu[]>([]);
147
+ const customStyleMenu = ref<userStyleMenu[]>([]);
148
+ const content = ref<editorContentType>([]);
149
+ const activeIdx = ref<number>(0);
150
+ const activeStyle = ref<styleActiveType>({
151
+ bold: false,
152
+ italic: false,
153
+ underline: false,
154
+ through: false,
155
+ link: false,
156
+ code: false,
157
+ });
158
+ // const activeItemId = ref<string>("");
159
+ // const selectItems = ref<string[]>([]);
160
+
161
+ // initial logic
162
+ onMounted(() => {
163
+ dataUpdateAction();
164
+ });
165
+
166
+ // block menu setting
167
+ blockMenu.value = setEditorMenu(props.option.blockMenu as string[], unref(props.option.customBlockMenu) as userCustomMenu[]);
168
+
169
+ // 유저 커스텀 스타일 메뉴
170
+ if (props.option.customStyleMenu) {
171
+ customStyleMenu.value = unref(props.option.customStyleMenu);
172
+ }
173
+
174
+ // content data setting
175
+ if (props.modelValue && Array.isArray(props.modelValue)) {
176
+ if (props.modelValue.length == 0) {
177
+ addBlockLocal("text", true);
178
+ } else {
179
+ content.value = unref(props.modelValue) as editorContentType;
180
+ }
181
+ } else {
182
+ throw new Error("[DragonEditor]ERROR : You must set 'v-model' attribute and 'v-mode' type is must be Array.");
183
+ }
184
+
185
+ // local logic
186
+ function checkAlignActive(className: string) {
187
+ const data = content.value[activeIdx.value];
188
+ let value = false;
189
+
190
+ switch (data.type) {
191
+ case "text":
192
+ value = data.classList.indexOf(className) > -1;
193
+ break;
194
+ }
195
+
196
+ return value;
197
+ }
198
+
199
+ function checkDecoActive() {
200
+ activeStyle.value.bold = hasClassNameCheckLogic("d-deco-bold");
201
+ activeStyle.value.italic = hasClassNameCheckLogic("d-deco-italic");
202
+ activeStyle.value.underline = hasClassNameCheckLogic("d-deco-underline");
203
+ activeStyle.value.through = hasClassNameCheckLogic("d-deco-through");
204
+ }
205
+
206
+ function hasClassNameCheckLogic(className: string) {
207
+ const cursorData = getCursor();
208
+ let value = false;
209
+
210
+ if (cursorData.type === "Caret") {
211
+ const type = (cursorData.startNode as Node).constructor.name;
212
+ let $target = cursorData.startNode as HTMLElement;
213
+
214
+ if (type === "Text") {
215
+ $target = (cursorData.startNode as HTMLElement).parentNode as HTMLElement;
216
+ }
217
+
218
+ if ($target) {
219
+ const classList = [...$target.classList];
220
+
221
+ if (classList.indexOf(className) > -1) {
222
+ value = true;
223
+ }
224
+ }
225
+ }
226
+
227
+ return value;
228
+ }
229
+
230
+
231
+ function setEditorMenu(vanillaData: string[], customData?: userCustomMenu[]) {
232
+ const dataList: editorMenu[] = [];
233
+
234
+ vanillaData.forEach((name) => {
235
+ dataList.push({
236
+ name: name,
237
+ hasIcon: true,
238
+ icon: `${name}Block`,
239
+ action: () => {
240
+ addBlockLocal(name);
241
+ }
242
+ });
243
+ });
244
+
245
+ if (customData) {
246
+ customData.forEach((row) => {
247
+ dataList.push({
248
+ name: row.name,
249
+ hasIcon: iconList.indexOf(row.icon) > -1,
250
+ icon: row.icon,
251
+ action: row.action
252
+ });
253
+ });
254
+ }
255
+
256
+ return dataList;
257
+ }
258
+
259
+
260
+ // event function
261
+ function activeMenuEvent(e: MouseEvent) {
262
+ setMenuPosition(e.currentTarget as HTMLElement);
263
+ checkDecoActive();
264
+ activeMenu.value = true;
265
+ }
266
+
267
+ function deactiveMenuEvent() {
268
+ activeMenu.value = false;
269
+ }
270
+
271
+ function dataUpdateAction() {
272
+ $child.value.forEach((row: any) => {
273
+ row.updateBlockData();
274
+ });
275
+
276
+ emit("update:modelValue", content.value);
277
+ }
278
+
279
+ function addBlockLocal(name: string, time: boolean = false) {
280
+ const block = createBlock(name);
281
+
282
+ content.value.splice(activeIdx.value + 1, 0, block);
283
+
284
+ if (time === false) {
285
+ activeIdx.value += 1;
286
+
287
+ setTimeout(() => { // waiting data set
288
+ if (name !== "image") {
289
+ $child.value[activeIdx.value].focus();
290
+ }
291
+
292
+ dataUpdateAction();
293
+ }, 100);
294
+ }
295
+ }
296
+
297
+ function pasteEvent(e: ClipboardEvent) {
298
+ e.preventDefault();
299
+ const data = getClipboardData(e.clipboardData as DataTransfer);
300
+
301
+ if (data.type === "text") {
302
+ const targetComponent = $child.value[activeIdx.value];
303
+ const componentType = targetComponent.getType();
304
+
305
+ if (componentType !== "image" && componentType !== "other") {
306
+ targetComponent.pasteEvent(data.value);
307
+ }
308
+ }
309
+ }
310
+
311
+ function setComponentKind(kind: string) {
312
+ switch (kind) {
313
+ case "text":
314
+ return textBlock;
315
+ break;
316
+ }
317
+ }
318
+
319
+ function setMenuPosition($target: HTMLElement) {
320
+ const parentNode = $wrap.value.parentNode;
321
+ const bodyRect = document.body.getBoundingClientRect();
322
+ const wrapRect = $wrap.value.getBoundingClientRect();
323
+ const targetRect = $target.getBoundingClientRect();
324
+ const parentNodeScrollY = parentNode.scrollTop;
325
+ const wrapTop = wrapRect.top - bodyRect.top;
326
+ const targetTop = targetRect.top - bodyRect.top;
327
+ const targetBottom = targetRect.bottom - bodyRect.top;
328
+ const top = ((targetTop - (wrapTop + 10)) - parentNodeScrollY) + 13;
329
+ const bottom = ((targetBottom - (wrapTop + 10)) - parentNodeScrollY) + 10;
330
+
331
+ styleMenuPosition.value = bottom;
332
+ leftMenuPosition.value = top;
333
+ }
334
+
335
+ function setBlockEvent(type: string) {
336
+ $child.value[activeIdx.value].setStyles(type);
337
+ setTimeout(() => {
338
+ checkDecoActive();
339
+ }, 100);
340
+ }
341
+
342
+
343
+ // export function
344
+ // function checkStyleActive(className: string) {
345
+ // return hasClassNameCheckLogic(className);
346
+ // }
347
+
348
+ function addImageBlock() {
349
+ console.log("local image added event!");
350
+ console.log($child);
351
+ // contentData.value = value;
352
+ }
353
+
354
+ defineExpose({
355
+ addImageBlock
356
+ });
357
+ </script>
358
+
359
+ <style>
360
+ @import "../../core/style/common.css";
361
+ </style>
@@ -0,0 +1,129 @@
1
+ <template>
2
+ <div class="dragon-editor">
3
+ <p class="d-text-block" :class="data.classList" contenteditable v-html="data.content" @keydown="textKeyboardEvent"
4
+ @paste="pasteEvent" ref="$block"></p>
5
+ </div>
6
+ </template>
7
+
8
+ <script setup lang="ts">
9
+ import { ref, unref } from "#imports";
10
+ import {
11
+ keyboardEvent,
12
+ setCursor,
13
+ pasteText,
14
+ styleSettings,
15
+ getArrangementCursorData,
16
+ getClipboardData,
17
+ getCursor,
18
+ findEditableElement
19
+ } from "../../core/utils/index";
20
+ import { commentBlock } from "../../types/index";
21
+
22
+ const $block = ref();
23
+ const data = ref<commentBlock>({
24
+ type: "comment",
25
+ classList: [],
26
+ content: "",
27
+ });
28
+ const props = defineProps<{ modelValue: commentBlock }>();
29
+ const emit = defineEmits<{
30
+ (e: "update:modelValue", modelValue: commentBlock): void;
31
+ }>();
32
+
33
+ data.value = unref(props.modelValue) as commentBlock;
34
+
35
+ function textKeyboardEvent(e: KeyboardEvent) {
36
+ keyboardEvent("comment", e, emit);
37
+ }
38
+
39
+ function pasteEvent(e: ClipboardEvent) {
40
+ e.preventDefault();
41
+ const data = getClipboardData(e.clipboardData as DataTransfer);
42
+
43
+ if (data.type === "text") {
44
+ pasteText("text", data.value as string);
45
+ }
46
+ }
47
+
48
+ // export event
49
+ function updateBlockData() {
50
+ // 데이터 정규화 및 검수
51
+ const blockClassList = [...$block.value.classList];
52
+ blockClassList.splice(0, 1);
53
+ const pushList = blockClassList.filter(
54
+ (item) => data.value.classList.indexOf(item) === -1
55
+ );
56
+
57
+ data.value.classList = data.value.classList.concat(pushList);
58
+
59
+ // 커서위치 재지정
60
+ if ($block.value.innerHTML.length > 0) {
61
+ const cursorData = getArrangementCursorData();
62
+
63
+ data.value.content = $block.value.innerHTML;
64
+ emit("update:modelValue", data.value);
65
+
66
+ setTimeout(() => {
67
+ setCursor(
68
+ $block.value.childNodes[cursorData.childCount],
69
+ cursorData.length
70
+ );
71
+
72
+ // 빈 태그 삭제
73
+ $block.value.childNodes.forEach((child: ChildNode) => {
74
+ if (
75
+ child.constructor.name !== "Text" &&
76
+ child.textContent === ""
77
+ ) {
78
+ child.remove();
79
+ }
80
+ });
81
+ }, 100);
82
+ } else {
83
+ emit("update:modelValue", data.value);
84
+ }
85
+ }
86
+
87
+ function focus() {
88
+ setCursor($block.value, 0);
89
+ }
90
+
91
+ function setStyles(kind: string) {
92
+ data.value = styleSettings(kind, data.value, $block.value);
93
+ setTimeout(() => {
94
+ updateBlockData();
95
+ }, 250);
96
+ }
97
+
98
+ function getCursorClassList(className: string) {
99
+ const cursorData = getCursor();
100
+ let value: string[] = [];
101
+
102
+ if (cursorData.type === "Caret") {
103
+ const type = (cursorData.startNode as Node).constructor.name;
104
+ const editableElement = findEditableElement(cursorData.startNode as Node);
105
+ let $target = cursorData.startNode as HTMLElement;
106
+
107
+ if (type === "Text") {
108
+ $target = (cursorData.startNode as HTMLElement).parentNode as HTMLElement;
109
+ }
110
+
111
+ if ($target !== editableElement) {
112
+ value = [...$target.classList];
113
+ }
114
+ }
115
+
116
+ return value;
117
+ }
118
+
119
+ defineExpose({
120
+ updateBlockData,
121
+ focus,
122
+ setStyles,
123
+ getCursorClassList
124
+ });
125
+ </script>
126
+
127
+ <style>
128
+ @import "../../core/style/common.css";
129
+ </style>
@@ -0,0 +1,3 @@
1
+ <template>
2
+ <div class="editor-viewer"></div>
3
+ </template>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dragon-editor",
3
- "version": "2.0.0-beta.1",
3
+ "version": "2.0.0-beta.1.3",
4
4
  "description": "WYSIWYG editor on Nuxt.js",
5
5
  "repository": {
6
6
  "type": "git",