@grandaniel/vue-markdown-editor 1.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/eslint.config.ts +0 -0
- package/package.json +43 -0
- package/src/components/MarkdownEditor/Composable/HeadlineTypeMap.ts +36 -0
- package/src/components/MarkdownEditor/Composable/useMarkdownEditor.ts +21 -0
- package/src/components/MarkdownEditor/Composable/useMarkdownProcessor.ts +186 -0
- package/src/components/MarkdownEditor/Composable/useReflectiveState.ts +84 -0
- package/src/components/MarkdownEditor/ContextMenu/MarkdownEditorContextMenu.vue +38 -0
- package/src/components/MarkdownEditor/ContextMenu/MarkdownEditorContextMenuBlockItem.vue +43 -0
- package/src/components/MarkdownEditor/ContextMenu/MarkdownEditorContextMenuInlineItem.vue +52 -0
- package/src/components/MarkdownEditor/ContextMenu/MarkdownEditorImageContextMenu.vue +35 -0
- package/src/components/MarkdownEditor/ContextMenu/MarkdownEditorTextSelectionContextMenu.vue +45 -0
- package/src/components/MarkdownEditor/Factory/MarkdownNodeFactory.ts +49 -0
- package/src/components/MarkdownEditor/MarkdownComponentRegistry.ts +48 -0
- package/src/components/MarkdownEditor/MarkdownEditor.vue +333 -0
- package/src/components/MarkdownEditor/MarkdownEditorModal.vue +169 -0
- package/src/components/MarkdownEditor/MarkdownEditorModule.vue +129 -0
- package/src/components/MarkdownEditor/Modules/MarkdownModuleHeadline1.vue +60 -0
- package/src/components/MarkdownEditor/Modules/MarkdownModuleHeadline2.vue +60 -0
- package/src/components/MarkdownEditor/Modules/MarkdownModuleHeadline3.vue +61 -0
- package/src/components/MarkdownEditor/Modules/MarkdownModuleImage.vue +185 -0
- package/src/components/MarkdownEditor/Modules/MarkdownModuleImageState.ts +10 -0
- package/src/components/MarkdownEditor/Modules/MarkdownModuleList.vue +7 -0
- package/src/components/MarkdownEditor/Modules/MarkdownModuleListState.ts +10 -0
- package/src/components/MarkdownEditor/Modules/MarkdownModuleParagraph.vue +54 -0
- package/src/components/MarkdownEditor/Modules/MarkdownModuleTextState.ts +8 -0
- package/src/components/MarkdownEditor/Modules/__tests__/MarkdownModuleHeadline1.test.js +20 -0
- package/src/components/MarkdownEditor/Modules/__tests__/MarkdownModuleHeadline1.test.ts +25 -0
- package/src/components/MarkdownEditor/Modules/__tests__/__snapshots__/MarkdownModuleHeadline1.test.ts.snap +7 -0
- package/src/components/MarkdownEditor/Styles/Mixins.scss +7 -0
- package/src/components/MarkdownEditor/TipTap/SingleLineExtension.ts +21 -0
- package/src/components/MarkdownEditor/Types/MarkdownAstNode.ts +22 -0
- package/src/components/MarkdownEditor/Types/MarkdownAstNodeType.ts +28 -0
- package/src/components/MarkdownEditor/Types/TextishEmits.ts +14 -0
- package/src/components/MarkdownEditor/index.ts +1 -0
- package/src/components/index.ts +1 -0
- package/src/index.ts +1 -0
- package/tsconfig.app.json +11 -0
- package/tsconfig.json +11 -0
- package/tsconfig.node.json +12 -0
- package/vite.config.ts +24 -0
- package/vitest.config.ts +21 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<EditorContent
|
|
3
|
+
ref="editorRef"
|
|
4
|
+
:editor="editor"
|
|
5
|
+
/>
|
|
6
|
+
</template>
|
|
7
|
+
|
|
8
|
+
<script lang="ts" setup>
|
|
9
|
+
import StarterKit from "@tiptap/starter-kit";
|
|
10
|
+
import { EditorContent, useEditor } from "@tiptap/vue-3";
|
|
11
|
+
import { ref } from "vue";
|
|
12
|
+
import useReflectiveState from "../Composable/useReflectiveState";
|
|
13
|
+
import { HeadingDocument, PreventNewline } from "../TipTap/SingleLineExtension";
|
|
14
|
+
import type { TextishEmits } from "../Types/TextishEmits";
|
|
15
|
+
import type MarkdownModuleTextState from "./MarkdownModuleTextState";
|
|
16
|
+
|
|
17
|
+
const modelValue = defineModel<MarkdownModuleTextState>({
|
|
18
|
+
required: true,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const editorRef = ref<InstanceType<typeof EditorContent>>();
|
|
22
|
+
|
|
23
|
+
const emit = defineEmits<TextishEmits>();
|
|
24
|
+
|
|
25
|
+
const state = useReflectiveState({
|
|
26
|
+
modelRef: modelValue,
|
|
27
|
+
emit,
|
|
28
|
+
editorRef,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const editor = useEditor({
|
|
32
|
+
extensions: [
|
|
33
|
+
HeadingDocument,
|
|
34
|
+
StarterKit.configure({
|
|
35
|
+
document: false,
|
|
36
|
+
paragraph: false,
|
|
37
|
+
heading: {
|
|
38
|
+
levels: [2],
|
|
39
|
+
},
|
|
40
|
+
codeBlock: false,
|
|
41
|
+
blockquote: false,
|
|
42
|
+
horizontalRule: false,
|
|
43
|
+
hardBreak: false,
|
|
44
|
+
bulletList: false,
|
|
45
|
+
orderedList: false,
|
|
46
|
+
listItem: false,
|
|
47
|
+
}),
|
|
48
|
+
PreventNewline,
|
|
49
|
+
],
|
|
50
|
+
content: `<h2>${state.editorContent.value}</h2>`,
|
|
51
|
+
onUpdate: (event) => state.handleTipTapUpdateEvent(event),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
defineExpose(state.expose);
|
|
55
|
+
</script>
|
|
56
|
+
|
|
57
|
+
<style lang="scss" scoped>
|
|
58
|
+
@use "../Styles/Mixins.scss" as *;
|
|
59
|
+
@include reset-contenteditable;
|
|
60
|
+
</style>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<EditorContent
|
|
3
|
+
ref="editorRef"
|
|
4
|
+
:editor="editor"
|
|
5
|
+
/>
|
|
6
|
+
</template>
|
|
7
|
+
|
|
8
|
+
<script lang="ts" setup>
|
|
9
|
+
import StarterKit from "@tiptap/starter-kit";
|
|
10
|
+
import { EditorContent, useEditor } from "@tiptap/vue-3";
|
|
11
|
+
import { ref } from "vue";
|
|
12
|
+
import useReflectiveState from "../Composable/useReflectiveState";
|
|
13
|
+
import { HeadingDocument, PreventNewline } from "../TipTap/SingleLineExtension";
|
|
14
|
+
import type { TextishEmits } from "../Types/TextishEmits";
|
|
15
|
+
import type MarkdownModuleTextState from "./MarkdownModuleTextState";
|
|
16
|
+
|
|
17
|
+
const modelValue = defineModel<MarkdownModuleTextState>({
|
|
18
|
+
required: true,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const editorRef = ref<InstanceType<typeof EditorContent>>();
|
|
22
|
+
|
|
23
|
+
const emit = defineEmits<TextishEmits>();
|
|
24
|
+
|
|
25
|
+
const state = useReflectiveState({
|
|
26
|
+
modelRef: modelValue,
|
|
27
|
+
emit,
|
|
28
|
+
editorRef,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const editor = useEditor({
|
|
32
|
+
extensions: [
|
|
33
|
+
HeadingDocument,
|
|
34
|
+
StarterKit.configure({
|
|
35
|
+
|
|
36
|
+
document: false,
|
|
37
|
+
paragraph: false,
|
|
38
|
+
heading: {
|
|
39
|
+
levels: [3],
|
|
40
|
+
},
|
|
41
|
+
codeBlock: false,
|
|
42
|
+
blockquote: false,
|
|
43
|
+
horizontalRule: false,
|
|
44
|
+
hardBreak: false,
|
|
45
|
+
bulletList: false,
|
|
46
|
+
orderedList: false,
|
|
47
|
+
listItem: false,
|
|
48
|
+
}),
|
|
49
|
+
PreventNewline,
|
|
50
|
+
],
|
|
51
|
+
content: `<h3>${state.editorContent.value}</h3>`,
|
|
52
|
+
onUpdate: (event) => state.handleTipTapUpdateEvent(event),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
defineExpose(state.expose);
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
<style lang="scss" scoped>
|
|
59
|
+
@use "../Styles/Mixins.scss" as *;
|
|
60
|
+
@include reset-contenteditable;
|
|
61
|
+
</style>
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
ref="divRef"
|
|
4
|
+
class="markdown-module-image"
|
|
5
|
+
@click="handleClick"
|
|
6
|
+
>
|
|
7
|
+
<img
|
|
8
|
+
:src="modelValue.src"
|
|
9
|
+
:alt="modelValue.alt"
|
|
10
|
+
>
|
|
11
|
+
<span v-if="modelValue.alt">{{ modelValue.caption }}</span>
|
|
12
|
+
|
|
13
|
+
<MarkdownEditorImageContextMenu
|
|
14
|
+
v-if="showContextMenu"
|
|
15
|
+
:x="contextMenuPosition.x"
|
|
16
|
+
:y="contextMenuPosition.y"
|
|
17
|
+
@edit-attributes="openAttributesModal"
|
|
18
|
+
/>
|
|
19
|
+
|
|
20
|
+
<MarkdownEditorModal
|
|
21
|
+
:show="showModal"
|
|
22
|
+
title="Edit Image Attributes"
|
|
23
|
+
@close="closeModal"
|
|
24
|
+
@confirm="saveAttributes"
|
|
25
|
+
>
|
|
26
|
+
<div class="markdown-module-image-form">
|
|
27
|
+
<div class="markdown-module-image-form-field">
|
|
28
|
+
<label for="image-src">Image Source (URL)</label>
|
|
29
|
+
<input
|
|
30
|
+
id="image-src"
|
|
31
|
+
v-model="editForm.src"
|
|
32
|
+
type="text"
|
|
33
|
+
placeholder="https://example.com/image.jpg"
|
|
34
|
+
>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<div class="markdown-module-image-form-field">
|
|
38
|
+
<label for="image-alt">Alt Text</label>
|
|
39
|
+
<input
|
|
40
|
+
|
|
41
|
+
id="image-alt"
|
|
42
|
+
v-model="editForm.alt"
|
|
43
|
+
type="text"
|
|
44
|
+
placeholder="Description of the image"
|
|
45
|
+
>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div class="markdown-module-image-form-field">
|
|
49
|
+
<label for="image-caption">Caption</label>
|
|
50
|
+
<input
|
|
51
|
+
id="image-caption"
|
|
52
|
+
v-model="editForm.caption"
|
|
53
|
+
type="text"
|
|
54
|
+
placeholder="Optional caption"
|
|
55
|
+
>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</MarkdownEditorModal>
|
|
59
|
+
</div>
|
|
60
|
+
</template>
|
|
61
|
+
|
|
62
|
+
<script lang="ts" setup>
|
|
63
|
+
import { ref } from "vue";
|
|
64
|
+
import MarkdownEditorModal from "../MarkdownEditorModal.vue";
|
|
65
|
+
import type MarkdownModuleImageState from "./MarkdownModuleImageState";
|
|
66
|
+
|
|
67
|
+
const divRef = ref<HTMLDivElement>();
|
|
68
|
+
|
|
69
|
+
const modelValue = defineModel<MarkdownModuleImageState>({
|
|
70
|
+
required: true,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Context menu state
|
|
74
|
+
const showContextMenu = ref(false);
|
|
75
|
+
const contextMenuPosition = ref({ x: 0, y: 0 });
|
|
76
|
+
|
|
77
|
+
// Modal state
|
|
78
|
+
const showModal = ref(false);
|
|
79
|
+
const editForm = ref({
|
|
80
|
+
src: "",
|
|
81
|
+
alt: "",
|
|
82
|
+
caption: "",
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
function handleClick(event: MouseEvent) {
|
|
86
|
+
const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();
|
|
87
|
+
contextMenuPosition.value = {
|
|
88
|
+
x: event.clientX,
|
|
89
|
+
y: rect.bottom + 5,
|
|
90
|
+
};
|
|
91
|
+
showContextMenu.value = true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function openAttributesModal() {
|
|
95
|
+
showContextMenu.value = false;
|
|
96
|
+
editForm.value = {
|
|
97
|
+
src: modelValue.value.src || "",
|
|
98
|
+
alt: modelValue.value.alt || "",
|
|
99
|
+
caption: modelValue.value.caption || "",
|
|
100
|
+
};
|
|
101
|
+
showModal.value = true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function closeModal() {
|
|
105
|
+
showModal.value = false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function saveAttributes() {
|
|
109
|
+
modelValue.value.src = editForm.value.src;
|
|
110
|
+
modelValue.value.alt = editForm.value.alt;
|
|
111
|
+
modelValue.value.caption = editForm.value.caption;
|
|
112
|
+
showModal.value = false;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function focus() {
|
|
116
|
+
if (divRef.value) {
|
|
117
|
+
divRef.value.focus();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
defineExpose({ focus });
|
|
122
|
+
</script>
|
|
123
|
+
|
|
124
|
+
<style lang="scss" scoped>
|
|
125
|
+
.markdown-module-image {
|
|
126
|
+
cursor: pointer;
|
|
127
|
+
position: relative;
|
|
128
|
+
text-align: center;
|
|
129
|
+
|
|
130
|
+
img {
|
|
131
|
+
max-width: 100%;
|
|
132
|
+
height: auto;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
span {
|
|
136
|
+
display: block;
|
|
137
|
+
margin-top: 0.25rem;
|
|
138
|
+
font-size: 0.875rem;
|
|
139
|
+
color: #6b7280;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.markdown-module-image-form {
|
|
144
|
+
display: flex;
|
|
145
|
+
flex-direction: column;
|
|
146
|
+
gap: 1.25rem;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.markdown-module-image-form-field {
|
|
150
|
+
display: flex;
|
|
151
|
+
flex-direction: column;
|
|
152
|
+
gap: 0.5rem;
|
|
153
|
+
|
|
154
|
+
label {
|
|
155
|
+
font-size: 0.875rem;
|
|
156
|
+
font-weight: 500;
|
|
157
|
+
color: #374151;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
input {
|
|
161
|
+
padding: 0.625rem 0.875rem;
|
|
162
|
+
font-size: 0.875rem;
|
|
163
|
+
border: 1px solid #d1d5db;
|
|
164
|
+
border-radius: 0.5rem;
|
|
165
|
+
background: #ffffff;
|
|
166
|
+
color: #111827;
|
|
167
|
+
transition: all 0.15s ease;
|
|
168
|
+
|
|
169
|
+
&:focus {
|
|
170
|
+
outline: none;
|
|
171
|
+
border-color: #3b82f6;
|
|
172
|
+
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
&::placeholder {
|
|
176
|
+
color: #9ca3af;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
</style>
|
|
181
|
+
|
|
182
|
+
<style lang="scss" scoped>
|
|
183
|
+
@use "../Styles/Mixins.scss" as *;
|
|
184
|
+
@include reset-contenteditable;
|
|
185
|
+
</style>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<EditorContent
|
|
3
|
+
ref="editorRef"
|
|
4
|
+
:editor="editor"
|
|
5
|
+
/>
|
|
6
|
+
</template>
|
|
7
|
+
|
|
8
|
+
<script lang="ts" setup>
|
|
9
|
+
import { Placeholder } from "@tiptap/extensions";
|
|
10
|
+
import StarterKit from "@tiptap/starter-kit";
|
|
11
|
+
import { EditorContent, useEditor } from "@tiptap/vue-3";
|
|
12
|
+
import { ref } from "vue";
|
|
13
|
+
import useReflectiveState from "../Composable/useReflectiveState";
|
|
14
|
+
import { PreventNewline } from "../TipTap/SingleLineExtension";
|
|
15
|
+
import type { TextishEmits } from "../Types/TextishEmits";
|
|
16
|
+
import type MarkdownModuleTextState from "./MarkdownModuleTextState";
|
|
17
|
+
|
|
18
|
+
const modelValue = defineModel<MarkdownModuleTextState>({
|
|
19
|
+
required: true,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const editorRef = ref<InstanceType<typeof EditorContent>>();
|
|
23
|
+
|
|
24
|
+
const emit = defineEmits<TextishEmits>();
|
|
25
|
+
|
|
26
|
+
const state = useReflectiveState({
|
|
27
|
+
modelRef: modelValue,
|
|
28
|
+
emit,
|
|
29
|
+
editorRef,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const editor = useEditor({
|
|
33
|
+
extensions: [
|
|
34
|
+
Placeholder.configure({
|
|
35
|
+
placeholder: "Type something...",
|
|
36
|
+
}),
|
|
37
|
+
StarterKit.configure({
|
|
38
|
+
heading: false,
|
|
39
|
+
codeBlock: false,
|
|
40
|
+
blockquote: false,
|
|
41
|
+
horizontalRule: false,
|
|
42
|
+
hardBreak: false,
|
|
43
|
+
}),
|
|
44
|
+
PreventNewline,
|
|
45
|
+
],
|
|
46
|
+
content: state.editorContent.value,
|
|
47
|
+
onUpdate: (event) => state.handleTipTapUpdateEvent(event),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
defineExpose(state.expose);
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
<style lang="scss" scoped>
|
|
54
|
+
</style>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { shallowMount } from "@vue/test-utils";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import MarkdownModuleHeadline1 from "../MarkdownModuleHeadline1.vue";
|
|
4
|
+
import MarkdownModuleTextState from "../MarkdownModuleTextState";
|
|
5
|
+
describe("MarkdownModuleHeadline1", () => {
|
|
6
|
+
function getComponent() {
|
|
7
|
+
return shallowMount(MarkdownModuleHeadline1, {
|
|
8
|
+
props: {
|
|
9
|
+
modelValue: new MarkdownModuleTextState({ text: "" }),
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
it("renders correctly with default props", async () => {
|
|
14
|
+
// Arrange
|
|
15
|
+
const wrapper = getComponent();
|
|
16
|
+
// Act
|
|
17
|
+
// Assert
|
|
18
|
+
expect(wrapper.element).toMatchSnapshot();
|
|
19
|
+
});
|
|
20
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { shallowMount } from "@vue/test-utils";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import MarkdownModuleHeadline1 from "../MarkdownModuleHeadline1.vue";
|
|
4
|
+
import MarkdownModuleTextState from "../MarkdownModuleTextState";
|
|
5
|
+
|
|
6
|
+
describe("MarkdownModuleHeadline1", () => {
|
|
7
|
+
function getComponent() {
|
|
8
|
+
return shallowMount(MarkdownModuleHeadline1, {
|
|
9
|
+
props: {
|
|
10
|
+
modelValue: new MarkdownModuleTextState({ text: "" }),
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
it("renders correctly with default props", async () => {
|
|
16
|
+
// Arrange
|
|
17
|
+
const wrapper = getComponent();
|
|
18
|
+
|
|
19
|
+
// Act
|
|
20
|
+
|
|
21
|
+
// Assert
|
|
22
|
+
expect(wrapper.element).toMatchSnapshot();
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Extension } from "@tiptap/core";
|
|
2
|
+
import { Document } from "@tiptap/extension-document";
|
|
3
|
+
|
|
4
|
+
export const SingleLineDocument = Document.extend({
|
|
5
|
+
content: "block",
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
export const HeadingDocument = Document.extend({
|
|
9
|
+
content: "heading+",
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const PreventNewline = Extension.create({
|
|
13
|
+
name: "preventNewline",
|
|
14
|
+
addKeyboardShortcuts() {
|
|
15
|
+
return {
|
|
16
|
+
"Enter": () => true,
|
|
17
|
+
"Shift-Enter": () => true,
|
|
18
|
+
};
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type MarkdownModuleImageState from "../Modules/MarkdownModuleImageState";
|
|
2
|
+
import type MarkdownModuleTextState from "../Modules/MarkdownModuleTextState";
|
|
3
|
+
import type MarkdownNodeType from "./MarkdownAstNodeType";
|
|
4
|
+
|
|
5
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
6
|
+
export class MarkdownAstNode<TState extends object = Record<string, any>> {
|
|
7
|
+
id: symbol;
|
|
8
|
+
type: MarkdownNodeType;
|
|
9
|
+
componentState: TState;
|
|
10
|
+
|
|
11
|
+
editingState: {
|
|
12
|
+
cursorPosition: number;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
constructor(value: Omit<MarkdownAstNode<TState>, "id">) {
|
|
16
|
+
Object.assign(this, value);
|
|
17
|
+
this.id = Symbol();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type TextNode = MarkdownAstNode<MarkdownModuleTextState>;
|
|
22
|
+
export type ImageNode = MarkdownAstNode<MarkdownModuleImageState>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
enum MarkdownNodeType {
|
|
2
|
+
PARAGRAPH,
|
|
3
|
+
HEADLINE1,
|
|
4
|
+
HEADLINE2,
|
|
5
|
+
HEADLINE3,
|
|
6
|
+
IMAGE,
|
|
7
|
+
LIST,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type TextishNodeType
|
|
11
|
+
= | MarkdownNodeType.PARAGRAPH
|
|
12
|
+
| MarkdownNodeType.LIST
|
|
13
|
+
| MarkdownNodeType.HEADLINE1
|
|
14
|
+
| MarkdownNodeType.HEADLINE2
|
|
15
|
+
| MarkdownNodeType.HEADLINE3;
|
|
16
|
+
|
|
17
|
+
export function isTextNodeType(type: MarkdownNodeType): type is TextishNodeType {
|
|
18
|
+
return (
|
|
19
|
+
type === MarkdownNodeType.PARAGRAPH
|
|
20
|
+
|| type === MarkdownNodeType.LIST
|
|
21
|
+
|| type === MarkdownNodeType.HEADLINE1
|
|
22
|
+
|| type === MarkdownNodeType.HEADLINE2
|
|
23
|
+
|| type === MarkdownNodeType.HEADLINE3
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default MarkdownNodeType;
|
|
28
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type MarkdownModuleTextState from "../Modules/MarkdownModuleTextState";
|
|
2
|
+
import type MarkdownAstNodeType from "./MarkdownAstNodeType";
|
|
3
|
+
|
|
4
|
+
export interface TextishEmits {
|
|
5
|
+
"update:model-value": [componentState: MarkdownModuleTextState];
|
|
6
|
+
"update:cursor-position": [cursorPosition: number];
|
|
7
|
+
"change-type": [newType: MarkdownAstNodeType];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface TextishEmitFunction {
|
|
11
|
+
(event: "update:model-value", value: MarkdownModuleTextState): void;
|
|
12
|
+
(event: "update:cursor-position", value: number): void;
|
|
13
|
+
(event: "change-type", value: MarkdownAstNodeType): void;
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as MarkdownEditor } from './MarkdownEditor.vue';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './MarkdownEditor/';
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './components/';
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "@tsconfig/node22/tsconfig.json",
|
|
3
|
+
"include": ["vite.config.ts", "eslint.config.ts"],
|
|
4
|
+
"compilerOptions": {
|
|
5
|
+
"noEmit": true,
|
|
6
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
|
7
|
+
|
|
8
|
+
"module": "ESNext",
|
|
9
|
+
"moduleResolution": "Bundler",
|
|
10
|
+
"types": ["node"]
|
|
11
|
+
}
|
|
12
|
+
}
|
package/vite.config.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
import vue from '@vitejs/plugin-vue';
|
|
3
|
+
import { fileURLToPath, URL } from 'node:url';
|
|
4
|
+
import { resolve } from 'node:path';
|
|
5
|
+
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
plugins: [vue()],
|
|
8
|
+
resolve: {
|
|
9
|
+
alias: {
|
|
10
|
+
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
build: {
|
|
14
|
+
emptyOutDir: false,
|
|
15
|
+
lib: {
|
|
16
|
+
formats: ['es'],
|
|
17
|
+
entry: resolve(import.meta.dirname, 'src', 'index.ts'),
|
|
18
|
+
fileName: 'vue-markdown-editor',
|
|
19
|
+
},
|
|
20
|
+
rollupOptions: {
|
|
21
|
+
external: ['vue', 'date-fns', '@date-fns/tz', '@floating-ui/vue', '@vueuse/core'],
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
});
|
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url';
|
|
2
|
+
import { mergeConfig, defineConfig, configDefaults } from 'vitest/config';
|
|
3
|
+
import viteConfig from './vite.config';
|
|
4
|
+
import { resolve } from 'node:path';
|
|
5
|
+
|
|
6
|
+
export default mergeConfig(
|
|
7
|
+
viteConfig,
|
|
8
|
+
defineConfig({
|
|
9
|
+
test: {
|
|
10
|
+
environment: 'happy-dom',
|
|
11
|
+
include: ['src/**/*.test.ts'],
|
|
12
|
+
root: fileURLToPath(new URL('./', import.meta.url)),
|
|
13
|
+
coverage: {
|
|
14
|
+
reporter: ['lcov'],
|
|
15
|
+
reportsDirectory: resolve(__dirname, 'coverage'),
|
|
16
|
+
include: ['src/**/*.ts', 'src/**/*.vue'],
|
|
17
|
+
exclude: ['src/types'],
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
}),
|
|
21
|
+
);
|