@grandaniel/vue-markdown-editor 1.0.0 → 1.1.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/LICENSE +15 -0
- package/README.md +29 -0
- package/dist/components/MarkdownEditor/Composable/HeadlineTypeMap.d.ts +8 -0
- package/dist/components/MarkdownEditor/Composable/useMarkdownEditor.d.ts +26 -0
- package/dist/components/MarkdownEditor/Composable/useMarkdownProcessor.d.ts +28 -0
- package/dist/components/MarkdownEditor/Composable/useReflectiveState.d.ts +20 -0
- package/dist/components/MarkdownEditor/ContextMenu/MarkdownEditorContextMenu.vue.d.ts +17 -0
- package/dist/components/MarkdownEditor/ContextMenu/MarkdownEditorContextMenuBlockItem.vue.d.ts +17 -0
- package/dist/components/MarkdownEditor/ContextMenu/MarkdownEditorContextMenuInlineItem.vue.d.ts +20 -0
- package/dist/components/MarkdownEditor/ContextMenu/MarkdownEditorImageContextMenu.vue.d.ts +11 -0
- package/dist/components/MarkdownEditor/ContextMenu/MarkdownEditorTextSelectionContextMenu.vue.d.ts +20 -0
- package/dist/components/MarkdownEditor/Factory/MarkdownNodeFactory.d.ts +14 -0
- package/dist/components/MarkdownEditor/MarkdownComponentRegistry.d.ts +15 -0
- package/dist/components/MarkdownEditor/MarkdownEditor.vue.d.ts +42 -0
- package/dist/components/MarkdownEditor/MarkdownEditorModal.vue.d.ts +23 -0
- package/dist/components/MarkdownEditor/MarkdownEditorModule.vue.d.ts +45 -0
- package/dist/components/MarkdownEditor/Modules/MarkdownModuleHeadline1.vue.d.ts +19 -0
- package/dist/components/MarkdownEditor/Modules/MarkdownModuleHeadline2.vue.d.ts +19 -0
- package/dist/components/MarkdownEditor/Modules/MarkdownModuleHeadline3.vue.d.ts +19 -0
- package/dist/components/MarkdownEditor/Modules/MarkdownModuleImage.vue.d.ts +14 -0
- package/dist/components/MarkdownEditor/Modules/MarkdownModuleImageState.d.ts +6 -0
- package/dist/components/MarkdownEditor/Modules/MarkdownModuleList.vue.d.ts +3 -0
- package/dist/components/MarkdownEditor/Modules/MarkdownModuleListState.d.ts +5 -0
- package/dist/components/MarkdownEditor/Modules/MarkdownModuleParagraph.vue.d.ts +19 -0
- package/dist/components/MarkdownEditor/Modules/MarkdownModuleTextState.d.ts +4 -0
- package/dist/components/MarkdownEditor/TipTap/SingleLineExtension.d.ts +4 -0
- package/dist/components/MarkdownEditor/Types/MarkdownAstNode.d.ts +14 -0
- package/dist/components/MarkdownEditor/Types/MarkdownAstNodeType.d.ts +11 -0
- package/dist/components/MarkdownEditor/Types/TextishEmits.d.ts +12 -0
- package/dist/components/MarkdownEditor/index.d.ts +5 -0
- package/dist/components/index.d.ts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/vue-markdown-editor.css +2 -0
- package/dist/vue-markdown-editor.mjs +20524 -0
- package/package.json +53 -5
- package/eslint.config.ts +0 -0
- package/src/components/MarkdownEditor/Composable/HeadlineTypeMap.ts +0 -36
- package/src/components/MarkdownEditor/Composable/useMarkdownEditor.ts +0 -21
- package/src/components/MarkdownEditor/Composable/useMarkdownProcessor.ts +0 -186
- package/src/components/MarkdownEditor/Composable/useReflectiveState.ts +0 -84
- package/src/components/MarkdownEditor/ContextMenu/MarkdownEditorContextMenu.vue +0 -38
- package/src/components/MarkdownEditor/ContextMenu/MarkdownEditorContextMenuBlockItem.vue +0 -43
- package/src/components/MarkdownEditor/ContextMenu/MarkdownEditorContextMenuInlineItem.vue +0 -52
- package/src/components/MarkdownEditor/ContextMenu/MarkdownEditorImageContextMenu.vue +0 -35
- package/src/components/MarkdownEditor/ContextMenu/MarkdownEditorTextSelectionContextMenu.vue +0 -45
- package/src/components/MarkdownEditor/Factory/MarkdownNodeFactory.ts +0 -49
- package/src/components/MarkdownEditor/MarkdownComponentRegistry.ts +0 -48
- package/src/components/MarkdownEditor/MarkdownEditor.vue +0 -333
- package/src/components/MarkdownEditor/MarkdownEditorModal.vue +0 -169
- package/src/components/MarkdownEditor/MarkdownEditorModule.vue +0 -129
- package/src/components/MarkdownEditor/Modules/MarkdownModuleHeadline1.vue +0 -60
- package/src/components/MarkdownEditor/Modules/MarkdownModuleHeadline2.vue +0 -60
- package/src/components/MarkdownEditor/Modules/MarkdownModuleHeadline3.vue +0 -61
- package/src/components/MarkdownEditor/Modules/MarkdownModuleImage.vue +0 -185
- package/src/components/MarkdownEditor/Modules/MarkdownModuleImageState.ts +0 -10
- package/src/components/MarkdownEditor/Modules/MarkdownModuleList.vue +0 -7
- package/src/components/MarkdownEditor/Modules/MarkdownModuleListState.ts +0 -10
- package/src/components/MarkdownEditor/Modules/MarkdownModuleParagraph.vue +0 -54
- package/src/components/MarkdownEditor/Modules/MarkdownModuleTextState.ts +0 -8
- package/src/components/MarkdownEditor/Modules/__tests__/MarkdownModuleHeadline1.test.js +0 -20
- package/src/components/MarkdownEditor/Modules/__tests__/MarkdownModuleHeadline1.test.ts +0 -25
- package/src/components/MarkdownEditor/Modules/__tests__/__snapshots__/MarkdownModuleHeadline1.test.ts.snap +0 -7
- package/src/components/MarkdownEditor/Styles/Mixins.scss +0 -7
- package/src/components/MarkdownEditor/TipTap/SingleLineExtension.ts +0 -21
- package/src/components/MarkdownEditor/Types/MarkdownAstNode.ts +0 -22
- package/src/components/MarkdownEditor/Types/MarkdownAstNodeType.ts +0 -28
- package/src/components/MarkdownEditor/Types/TextishEmits.ts +0 -14
- package/src/components/MarkdownEditor/index.ts +0 -1
- package/src/components/index.ts +0 -1
- package/src/index.ts +0 -1
- package/tsconfig.app.json +0 -11
- package/tsconfig.json +0 -11
- package/tsconfig.node.json +0 -12
- package/vite.config.ts +0 -24
- package/vitest.config.ts +0 -21
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import type { Component } from "vue";
|
|
2
|
-
import MarkdownModuleHeadline1 from "./Modules/MarkdownModuleHeadline1.vue";
|
|
3
|
-
import MarkdownModuleHeadline2 from "./Modules/MarkdownModuleHeadline2.vue";
|
|
4
|
-
import MarkdownModuleHeadline3 from "./Modules/MarkdownModuleHeadline3.vue";
|
|
5
|
-
import MarkdownModuleImage from "./Modules/MarkdownModuleImage.vue";
|
|
6
|
-
import MarkdownModuleImageState from "./Modules/MarkdownModuleImageState";
|
|
7
|
-
import MarkdownModuleParagraph from "./Modules/MarkdownModuleParagraph.vue";
|
|
8
|
-
import MarkdownModuleTextState from "./Modules/MarkdownModuleTextState";
|
|
9
|
-
import type { MarkdownAstNode } from "./Types/MarkdownAstNode";
|
|
10
|
-
import MarkdownNodeType from "./Types/MarkdownAstNodeType";
|
|
11
|
-
|
|
12
|
-
type MarkdownComponentRegistryEntry = {
|
|
13
|
-
component: Component;
|
|
14
|
-
stateType: new (
|
|
15
|
-
object: MarkdownModuleTextState | MarkdownModuleImageState,
|
|
16
|
-
) => MarkdownModuleTextState | MarkdownModuleImageState;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
const registry: Record<MarkdownNodeType, MarkdownComponentRegistryEntry> = {
|
|
20
|
-
[MarkdownNodeType.PARAGRAPH]: {
|
|
21
|
-
component: MarkdownModuleParagraph,
|
|
22
|
-
stateType: MarkdownModuleTextState,
|
|
23
|
-
},
|
|
24
|
-
[MarkdownNodeType.HEADLINE1]: {
|
|
25
|
-
component: MarkdownModuleHeadline1,
|
|
26
|
-
stateType: MarkdownModuleTextState,
|
|
27
|
-
},
|
|
28
|
-
[MarkdownNodeType.HEADLINE2]: {
|
|
29
|
-
component: MarkdownModuleHeadline2,
|
|
30
|
-
stateType: MarkdownModuleTextState,
|
|
31
|
-
},
|
|
32
|
-
[MarkdownNodeType.HEADLINE3]: {
|
|
33
|
-
component: MarkdownModuleHeadline3,
|
|
34
|
-
stateType: MarkdownModuleTextState,
|
|
35
|
-
},
|
|
36
|
-
[MarkdownNodeType.IMAGE]: {
|
|
37
|
-
component: MarkdownModuleImage,
|
|
38
|
-
stateType: MarkdownModuleImageState,
|
|
39
|
-
},
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
export function isTextNodeState(
|
|
43
|
-
state: MarkdownAstNode,
|
|
44
|
-
): state is MarkdownAstNode & { componentState: MarkdownModuleTextState } {
|
|
45
|
-
return state.componentState instanceof MarkdownModuleTextState;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export default registry;
|
|
@@ -1,333 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div
|
|
3
|
-
ref="editorContainerRef"
|
|
4
|
-
class="markdown-editor"
|
|
5
|
-
@click="handleClickBlankArea"
|
|
6
|
-
@mouseup="handleMouseUp"
|
|
7
|
-
>
|
|
8
|
-
<MarkdownEditorModule
|
|
9
|
-
v-for="(node, index) in markdownNodes"
|
|
10
|
-
:key="node.id"
|
|
11
|
-
:node="node"
|
|
12
|
-
:focused="focusedNode === node"
|
|
13
|
-
@click.stop
|
|
14
|
-
@keydown="handleKeyDownOnNode(node, $event)"
|
|
15
|
-
@focus="handleFocusOnNode(node)"
|
|
16
|
-
@update:cursor-position="(pos) => handleUpdateCursorPosition(node, pos)"
|
|
17
|
-
@change-type="handleChangeType"
|
|
18
|
-
>
|
|
19
|
-
<template #focus-controls>
|
|
20
|
-
<span
|
|
21
|
-
class="drag-handle"
|
|
22
|
-
title="Drag to reorder"
|
|
23
|
-
>
|
|
24
|
-
⠿
|
|
25
|
-
</span>
|
|
26
|
-
<button
|
|
27
|
-
type="button"
|
|
28
|
-
tabindex="-1"
|
|
29
|
-
@click="deleteNode(index)"
|
|
30
|
-
>
|
|
31
|
-
🗑️
|
|
32
|
-
</button>
|
|
33
|
-
<button
|
|
34
|
-
type="button"
|
|
35
|
-
tabindex="-1"
|
|
36
|
-
@click="addBlankNode(index)"
|
|
37
|
-
>
|
|
38
|
-
+
|
|
39
|
-
</button>
|
|
40
|
-
</template>
|
|
41
|
-
<template #after-controls>
|
|
42
|
-
<slot name="after-controls" />
|
|
43
|
-
</template>
|
|
44
|
-
</MarkdownEditorModule>
|
|
45
|
-
|
|
46
|
-
<MarkdownEditorTextSelectionContextMenu
|
|
47
|
-
v-if="showContextMenu"
|
|
48
|
-
:x="contextMenuPosition.x"
|
|
49
|
-
:y="contextMenuPosition.y"
|
|
50
|
-
:is-active="contextMenuActiveStates"
|
|
51
|
-
@toggle-bold="toggleBold"
|
|
52
|
-
@toggle-italic="toggleItalic"
|
|
53
|
-
@toggle-underline="toggleUnderline"
|
|
54
|
-
/>
|
|
55
|
-
</div>
|
|
56
|
-
</template>
|
|
57
|
-
|
|
58
|
-
<script lang="ts" setup>
|
|
59
|
-
import { useSortable } from "@vueuse/integrations/useSortable";
|
|
60
|
-
import { nextTick, ref, useTemplateRef, type PropType } from "vue";
|
|
61
|
-
import type { MarkdownEditorInstance } from "./Composable/useMarkdownEditor";
|
|
62
|
-
import MarkdownEditorTextSelectionContextMenu from "./ContextMenu/MarkdownEditorTextSelectionContextMenu.vue";
|
|
63
|
-
import { isTextNodeState as isTextishNode } from "./MarkdownComponentRegistry";
|
|
64
|
-
import MarkdownEditorModule from "./MarkdownEditorModule.vue";
|
|
65
|
-
import type { MarkdownAstNode } from "./Types/MarkdownAstNode";
|
|
66
|
-
import type MarkdownNodeType from "./Types/MarkdownAstNodeType";
|
|
67
|
-
|
|
68
|
-
const props = defineProps({
|
|
69
|
-
editor: {
|
|
70
|
-
type: Object as PropType<MarkdownEditorInstance>,
|
|
71
|
-
required: true,
|
|
72
|
-
},
|
|
73
|
-
focusedNode: {
|
|
74
|
-
type: Object as PropType<MarkdownAstNode | null>,
|
|
75
|
-
required: false,
|
|
76
|
-
default: null,
|
|
77
|
-
},
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
const emit = defineEmits<{
|
|
81
|
-
(e: "update:focused-node", value: MarkdownAstNode | null): void;
|
|
82
|
-
}>();
|
|
83
|
-
|
|
84
|
-
const { markdownNodes, deleteNode, addBlankNode, replaceNodeType, moveNode } = props.editor;
|
|
85
|
-
|
|
86
|
-
const editorContainerRef = useTemplateRef("editorContainerRef");
|
|
87
|
-
useSortable(editorContainerRef, markdownNodes, {
|
|
88
|
-
handle: ".drag-handle",
|
|
89
|
-
animation: 150,
|
|
90
|
-
onUpdate: (e) => {
|
|
91
|
-
moveNode(e.oldIndex!, e.newIndex!);
|
|
92
|
-
},
|
|
93
|
-
});
|
|
94
|
-
const focusedNode = ref<MarkdownAstNode | null>(props.focusedNode);
|
|
95
|
-
|
|
96
|
-
// Context menu state
|
|
97
|
-
const showContextMenu = ref(false);
|
|
98
|
-
const contextMenuPosition = ref({ x: 0, y: 0 });
|
|
99
|
-
const contextMenuActiveStates = ref({
|
|
100
|
-
bold: false,
|
|
101
|
-
italic: false,
|
|
102
|
-
underline: false,
|
|
103
|
-
});
|
|
104
|
-
let selectionDebounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
105
|
-
|
|
106
|
-
function handleUpdateCursorPosition(node: MarkdownAstNode, position: number) {
|
|
107
|
-
node.editingState.cursorPosition = position;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function handleChangeType(node: MarkdownAstNode, newType: MarkdownNodeType) {
|
|
111
|
-
const result = replaceNodeType(node, newType);
|
|
112
|
-
|
|
113
|
-
if (result) {
|
|
114
|
-
if (isTextishNode(result.newNode)) {
|
|
115
|
-
result.newNode.componentState.text = node.componentState.text.slice(node.editingState.cursorPosition);
|
|
116
|
-
}
|
|
117
|
-
focusNodeByIndex(result.index);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function handleKeyDownOnNode(node: MarkdownAstNode, event: KeyboardEvent) {
|
|
122
|
-
const nodeIndex = markdownNodes.value.indexOf(node);
|
|
123
|
-
|
|
124
|
-
if (event.key === "Enter") {
|
|
125
|
-
handleEnter(nodeIndex, event);
|
|
126
|
-
} else if (event.key === "Backspace") {
|
|
127
|
-
handleBackspace(nodeIndex);
|
|
128
|
-
} else if (event.key === "ArrowUp") {
|
|
129
|
-
moveFocusOneUp();
|
|
130
|
-
event.preventDefault();
|
|
131
|
-
} else if (event.key === "ArrowDown") {
|
|
132
|
-
moveFocusOneDown();
|
|
133
|
-
event.preventDefault();
|
|
134
|
-
} else if (event.key === "Delete") {
|
|
135
|
-
handleDelete(nodeIndex);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function handleFocusOnNode(node: MarkdownAstNode) {
|
|
140
|
-
focusedNode.value = node;
|
|
141
|
-
emit("update:focused-node", node);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function handleEnter(nodeIndex: number, event: KeyboardEvent) {
|
|
145
|
-
addBlankNode(nodeIndex);
|
|
146
|
-
moveFocusOneDown();
|
|
147
|
-
event.preventDefault();
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function handleBackspace(index: number) {
|
|
151
|
-
const node = getNodeByIndex(index);
|
|
152
|
-
|
|
153
|
-
if (!isTextishNode(node)) return;
|
|
154
|
-
if (node.componentState.text !== "") return;
|
|
155
|
-
|
|
156
|
-
deleteNode(index);
|
|
157
|
-
|
|
158
|
-
nextTick(() => {
|
|
159
|
-
const newIndex = index > 0 ? index - 1 : 0;
|
|
160
|
-
focusNodeByIndex(newIndex);
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function handleDelete(index: number) {
|
|
165
|
-
const node = getNodeByIndex(index);
|
|
166
|
-
|
|
167
|
-
if (!isTextishNode(node)) return;
|
|
168
|
-
if (node.componentState.text !== "") return;
|
|
169
|
-
|
|
170
|
-
deleteNode(index);
|
|
171
|
-
focusNodeByIndex(index);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function handleClickBlankArea() {
|
|
175
|
-
const lastNode = markdownNodes.value[markdownNodes.value.length - 1];
|
|
176
|
-
if (lastNode && isTextishNode(lastNode) && lastNode.componentState.text === "") {
|
|
177
|
-
focusedNode.value = lastNode;
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const newIndex = markdownNodes.value.length;
|
|
182
|
-
|
|
183
|
-
addBlankNode(newIndex);
|
|
184
|
-
focusNodeByIndex(newIndex);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function focusNodeByIndex(index: number) {
|
|
188
|
-
const node = markdownNodes.value[index];
|
|
189
|
-
if (!node) return;
|
|
190
|
-
nextTick(() => {
|
|
191
|
-
focusedNode.value = node;
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function getNodeByIndex(index: number): MarkdownAstNode {
|
|
196
|
-
const node = markdownNodes.value[index];
|
|
197
|
-
if (!node) throw new Error("Node not found at index " + index);
|
|
198
|
-
return node;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function moveFocusOneUp() {
|
|
202
|
-
if (!focusedNode.value) return;
|
|
203
|
-
|
|
204
|
-
nextTick(() => {
|
|
205
|
-
const currentNode = focusedNode.value;
|
|
206
|
-
if (!currentNode) return;
|
|
207
|
-
|
|
208
|
-
const index = markdownNodes.value.indexOf(currentNode);
|
|
209
|
-
if (index > 0) {
|
|
210
|
-
focusedNode.value = getNodeByIndex(index - 1);
|
|
211
|
-
}
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function moveFocusOneDown() {
|
|
216
|
-
if (!focusedNode.value) return;
|
|
217
|
-
|
|
218
|
-
nextTick(() => {
|
|
219
|
-
const currentNode = focusedNode.value;
|
|
220
|
-
if (!currentNode) return;
|
|
221
|
-
const index = markdownNodes.value.indexOf(currentNode);
|
|
222
|
-
if (index < markdownNodes.value.length - 1) {
|
|
223
|
-
focusedNode.value = getNodeByIndex(index + 1);
|
|
224
|
-
}
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// Context menu handlers
|
|
229
|
-
function handleMouseUp() {
|
|
230
|
-
// Clear existing timer
|
|
231
|
-
if (selectionDebounceTimer) {
|
|
232
|
-
clearTimeout(selectionDebounceTimer);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
const selection = window.getSelection();
|
|
236
|
-
|
|
237
|
-
if (!selection || selection.isCollapsed || selection.toString().trim() === "") {
|
|
238
|
-
showContextMenu.value = false;
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// Debounce: wait 500ms before showing context menu
|
|
243
|
-
selectionDebounceTimer = setTimeout(() => {
|
|
244
|
-
const range = selection.getRangeAt(0);
|
|
245
|
-
const rect = range.getBoundingClientRect();
|
|
246
|
-
|
|
247
|
-
// Position the context menu above the selection
|
|
248
|
-
contextMenuPosition.value = {
|
|
249
|
-
x: rect.left + rect.width / 2 - 75, // Center and offset
|
|
250
|
-
y: rect.top - 50, // Position above selection
|
|
251
|
-
};
|
|
252
|
-
|
|
253
|
-
// Detect active formatting states
|
|
254
|
-
const parentElement = range.commonAncestorContainer.parentElement;
|
|
255
|
-
if (parentElement) {
|
|
256
|
-
contextMenuActiveStates.value = {
|
|
257
|
-
bold: isElementOrParentTagName(parentElement, ["STRONG", "B"]),
|
|
258
|
-
italic: isElementOrParentTagName(parentElement, ["EM", "I"]),
|
|
259
|
-
underline: isElementOrParentTagName(parentElement, ["U"]),
|
|
260
|
-
};
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
showContextMenu.value = true;
|
|
264
|
-
}, 500);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
function isElementOrParentTagName(element: HTMLElement, tagNames: string[]): boolean {
|
|
268
|
-
let current: HTMLElement | null = element;
|
|
269
|
-
while (current) {
|
|
270
|
-
if (tagNames.includes(current.tagName)) {
|
|
271
|
-
return true;
|
|
272
|
-
}
|
|
273
|
-
current = current.parentElement;
|
|
274
|
-
}
|
|
275
|
-
return false;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
function toggleBold() {
|
|
279
|
-
document.execCommand("bold");
|
|
280
|
-
showContextMenu.value = false;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
function toggleItalic() {
|
|
284
|
-
document.execCommand("italic");
|
|
285
|
-
showContextMenu.value = false;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
function toggleUnderline() {
|
|
289
|
-
document.execCommand("underline");
|
|
290
|
-
showContextMenu.value = false;
|
|
291
|
-
}
|
|
292
|
-
</script>
|
|
293
|
-
|
|
294
|
-
<style>
|
|
295
|
-
/* Basic global style */
|
|
296
|
-
p {
|
|
297
|
-
margin: 0.5rem 0;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
strong,
|
|
301
|
-
b {
|
|
302
|
-
font-weight: bold;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
em,
|
|
306
|
-
i {
|
|
307
|
-
font-style: italic;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
code {
|
|
311
|
-
font-family: monospace;
|
|
312
|
-
background: rgba(127, 127, 127, 0.15);
|
|
313
|
-
padding: 0.1em 0.3em;
|
|
314
|
-
border-radius: 3px;
|
|
315
|
-
font-size: 0.9em;
|
|
316
|
-
color: green;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
h1 {
|
|
320
|
-
font-size: 2em;
|
|
321
|
-
margin: 0;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
h2 {
|
|
325
|
-
font-size: 1.5em;
|
|
326
|
-
margin: 0;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
h3 {
|
|
330
|
-
font-size: 1.17em;
|
|
331
|
-
margin: 0;
|
|
332
|
-
}
|
|
333
|
-
</style>
|
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<Teleport to="body">
|
|
3
|
-
<div
|
|
4
|
-
v-if="show"
|
|
5
|
-
class="markdown-editor-modal-overlay"
|
|
6
|
-
@click.self="emit('close')"
|
|
7
|
-
>
|
|
8
|
-
<div class="markdown-editor-modal">
|
|
9
|
-
<div class="markdown-editor-modal-header">
|
|
10
|
-
<h3 class="markdown-editor-modal-title">
|
|
11
|
-
{{ title }}
|
|
12
|
-
</h3>
|
|
13
|
-
<button
|
|
14
|
-
type="button"
|
|
15
|
-
class="markdown-editor-modal-close"
|
|
16
|
-
@click="emit('close')"
|
|
17
|
-
>
|
|
18
|
-
×
|
|
19
|
-
</button>
|
|
20
|
-
</div>
|
|
21
|
-
|
|
22
|
-
<div class="markdown-editor-modal-body">
|
|
23
|
-
<slot />
|
|
24
|
-
</div>
|
|
25
|
-
|
|
26
|
-
<div class="markdown-editor-modal-footer">
|
|
27
|
-
<button
|
|
28
|
-
type="button"
|
|
29
|
-
class="markdown-editor-modal-button markdown-editor-modal-button-secondary"
|
|
30
|
-
@click="emit('close')"
|
|
31
|
-
>
|
|
32
|
-
Cancel
|
|
33
|
-
</button>
|
|
34
|
-
<button
|
|
35
|
-
type="button"
|
|
36
|
-
class="markdown-editor-modal-button markdown-editor-modal-button-primary"
|
|
37
|
-
@click="emit('confirm')"
|
|
38
|
-
>
|
|
39
|
-
Save
|
|
40
|
-
</button>
|
|
41
|
-
</div>
|
|
42
|
-
</div>
|
|
43
|
-
</div>
|
|
44
|
-
</Teleport>
|
|
45
|
-
</template>
|
|
46
|
-
|
|
47
|
-
<script setup lang="ts">
|
|
48
|
-
defineProps<{
|
|
49
|
-
show: boolean;
|
|
50
|
-
title: string;
|
|
51
|
-
}>();
|
|
52
|
-
|
|
53
|
-
const emit = defineEmits<{
|
|
54
|
-
close: [];
|
|
55
|
-
confirm: [];
|
|
56
|
-
}>();
|
|
57
|
-
</script>
|
|
58
|
-
|
|
59
|
-
<style lang="scss" scoped>
|
|
60
|
-
.markdown-editor-modal-overlay {
|
|
61
|
-
position: fixed;
|
|
62
|
-
top: 0;
|
|
63
|
-
left: 0;
|
|
64
|
-
right: 0;
|
|
65
|
-
bottom: 0;
|
|
66
|
-
background: rgba(0, 0, 0, 0.5);
|
|
67
|
-
display: flex;
|
|
68
|
-
align-items: center;
|
|
69
|
-
justify-content: center;
|
|
70
|
-
z-index: 9999;
|
|
71
|
-
padding: 1rem;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
.markdown-editor-modal {
|
|
75
|
-
background: #ffffff;
|
|
76
|
-
border-radius: 0.75rem;
|
|
77
|
-
box-shadow:
|
|
78
|
-
0 20px 25px -5px rgba(0, 0, 0, 0.1),
|
|
79
|
-
0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
|
80
|
-
max-width: 32rem;
|
|
81
|
-
width: 100%;
|
|
82
|
-
max-height: 90vh;
|
|
83
|
-
display: flex;
|
|
84
|
-
flex-direction: column;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
.markdown-editor-modal-header {
|
|
88
|
-
display: flex;
|
|
89
|
-
align-items: center;
|
|
90
|
-
justify-content: space-between;
|
|
91
|
-
padding: 1.25rem 1.5rem;
|
|
92
|
-
border-bottom: 1px solid #e5e7eb;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
.markdown-editor-modal-title {
|
|
96
|
-
font-size: 1.125rem;
|
|
97
|
-
font-weight: 600;
|
|
98
|
-
color: #111827;
|
|
99
|
-
margin: 0;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
.markdown-editor-modal-close {
|
|
103
|
-
background: transparent;
|
|
104
|
-
border: none;
|
|
105
|
-
font-size: 1.75rem;
|
|
106
|
-
line-height: 1;
|
|
107
|
-
color: #6b7280;
|
|
108
|
-
cursor: pointer;
|
|
109
|
-
padding: 0;
|
|
110
|
-
width: 2rem;
|
|
111
|
-
height: 2rem;
|
|
112
|
-
display: flex;
|
|
113
|
-
align-items: center;
|
|
114
|
-
justify-content: center;
|
|
115
|
-
border-radius: 0.375rem;
|
|
116
|
-
transition: all 0.15s ease;
|
|
117
|
-
|
|
118
|
-
&:hover {
|
|
119
|
-
background: #f3f4f6;
|
|
120
|
-
color: #111827;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
.markdown-editor-modal-body {
|
|
125
|
-
padding: 1.5rem;
|
|
126
|
-
overflow-y: auto;
|
|
127
|
-
flex: 1;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
.markdown-editor-modal-footer {
|
|
131
|
-
display: flex;
|
|
132
|
-
gap: 0.75rem;
|
|
133
|
-
justify-content: flex-end;
|
|
134
|
-
padding: 1.25rem 1.5rem;
|
|
135
|
-
border-top: 1px solid #e5e7eb;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
.markdown-editor-modal-button {
|
|
139
|
-
padding: 0.625rem 1.25rem;
|
|
140
|
-
font-size: 0.875rem;
|
|
141
|
-
font-weight: 500;
|
|
142
|
-
border-radius: 0.5rem;
|
|
143
|
-
border: none;
|
|
144
|
-
cursor: pointer;
|
|
145
|
-
transition: all 0.15s ease;
|
|
146
|
-
|
|
147
|
-
&:active {
|
|
148
|
-
transform: scale(0.98);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
.markdown-editor-modal-button-secondary {
|
|
153
|
-
background: #f3f4f6;
|
|
154
|
-
color: #374151;
|
|
155
|
-
|
|
156
|
-
&:hover {
|
|
157
|
-
background: #e5e7eb;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
.markdown-editor-modal-button-primary {
|
|
162
|
-
background: #3b82f6;
|
|
163
|
-
color: #ffffff;
|
|
164
|
-
|
|
165
|
-
&:hover {
|
|
166
|
-
background: #2563eb;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
</style>
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div
|
|
3
|
-
ref="markdownEditorModuleRef"
|
|
4
|
-
class="markdown-editor-module"
|
|
5
|
-
>
|
|
6
|
-
<div class="markdown-editor-module-controls">
|
|
7
|
-
<template v-if="focused || mouseOver">
|
|
8
|
-
<slot name="focus-controls" />
|
|
9
|
-
</template>
|
|
10
|
-
</div>
|
|
11
|
-
<div
|
|
12
|
-
ref="contentRef"
|
|
13
|
-
class="markdown-editor-module-content"
|
|
14
|
-
:class="{
|
|
15
|
-
'markdown-editor-module-content-focused': props.focused,
|
|
16
|
-
}"
|
|
17
|
-
>
|
|
18
|
-
<component
|
|
19
|
-
:is="ComponentRegistry[props.node.type].component"
|
|
20
|
-
v-if="ComponentRegistry[props.node.type]"
|
|
21
|
-
ref="moduleComponentRef"
|
|
22
|
-
:model-value="props.node.componentState"
|
|
23
|
-
@update:model-value="emit('update:node', $event)"
|
|
24
|
-
@change-type="(newType: MarkdownAstNodeType) => emit('change-type', props.node, newType)"
|
|
25
|
-
@update:cursor-position="emit('update:cursor-position', $event)"
|
|
26
|
-
@click="emit('focus')"
|
|
27
|
-
/>
|
|
28
|
-
</div>
|
|
29
|
-
<slot name="after-controls" />
|
|
30
|
-
</div>
|
|
31
|
-
</template>
|
|
32
|
-
|
|
33
|
-
<script lang="ts" setup>
|
|
34
|
-
import { useFocusWithin, useMouseInElement } from "@vueuse/core";
|
|
35
|
-
import { computed, onMounted, ref, useTemplateRef, watch, type PropType } from "vue";
|
|
36
|
-
import type { MarkdownAstNode } from "./Types/MarkdownAstNode";
|
|
37
|
-
|
|
38
|
-
import ComponentRegistry from "./MarkdownComponentRegistry";
|
|
39
|
-
import type MarkdownAstNodeType from "./Types/MarkdownAstNodeType";
|
|
40
|
-
|
|
41
|
-
const props = defineProps({
|
|
42
|
-
node: {
|
|
43
|
-
type: Object as PropType<MarkdownAstNode>,
|
|
44
|
-
required: true,
|
|
45
|
-
},
|
|
46
|
-
focused: {
|
|
47
|
-
type: Boolean,
|
|
48
|
-
required: true,
|
|
49
|
-
},
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
onMounted(() => {
|
|
53
|
-
if (moduleComponentRef.value && props.focused) {
|
|
54
|
-
moduleComponentRef.value.focus();
|
|
55
|
-
}
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
const emit = defineEmits<{
|
|
59
|
-
"focus": [];
|
|
60
|
-
"update:cursor-position": [number];
|
|
61
|
-
"update:node": [Record<string, unknown>];
|
|
62
|
-
"change-type": [MarkdownAstNode, number];
|
|
63
|
-
}>();
|
|
64
|
-
|
|
65
|
-
const markdownEditorModuleRef = useTemplateRef("markdownEditorModuleRef");
|
|
66
|
-
const contentRef = useTemplateRef("contentRef");
|
|
67
|
-
const moduleComponentRef = ref<{ focus: () => void } | null>(null);
|
|
68
|
-
const innerElementFocus = useFocusWithin(contentRef);
|
|
69
|
-
const mouseInTarget = useMouseInElement(markdownEditorModuleRef);
|
|
70
|
-
const mouseOver = computed(() => !mouseInTarget.isOutside.value);
|
|
71
|
-
|
|
72
|
-
// Handle external focus changes
|
|
73
|
-
watch(
|
|
74
|
-
() => props.focused,
|
|
75
|
-
(nowFocused) => {
|
|
76
|
-
if (nowFocused && moduleComponentRef.value) {
|
|
77
|
-
moduleComponentRef.value.focus();
|
|
78
|
-
}
|
|
79
|
-
},
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
// Focus detection root
|
|
83
|
-
watch(innerElementFocus.focused, (isFocused) => {
|
|
84
|
-
if (isFocused) {
|
|
85
|
-
emit("focus");
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
</script>
|
|
89
|
-
|
|
90
|
-
<style lang="scss" scoped>
|
|
91
|
-
.markdown-editor-module {
|
|
92
|
-
display: flex;
|
|
93
|
-
flex-direction: row;
|
|
94
|
-
gap: 1rem;
|
|
95
|
-
|
|
96
|
-
:deep(.tiptap) {
|
|
97
|
-
outline: none;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
.markdown-editor-module-content {
|
|
101
|
-
width: 100%;
|
|
102
|
-
outline: none;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
.drag-handle {
|
|
106
|
-
cursor: grab;
|
|
107
|
-
padding: 0 0.25rem;
|
|
108
|
-
user-select: none;
|
|
109
|
-
font-size: 1rem;
|
|
110
|
-
opacity: 0.5;
|
|
111
|
-
|
|
112
|
-
&:active {
|
|
113
|
-
cursor: grabbing;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
.markdown-editor-module-controls {
|
|
118
|
-
width: 6rem;
|
|
119
|
-
display: flex;
|
|
120
|
-
flex-direction: row;
|
|
121
|
-
gap: 0.25rem;
|
|
122
|
-
align-items: center;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
.markdown-editor-module-content-focused {
|
|
126
|
-
background-color: rgba(158, 149, 149, 0.05);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
</style>
|