@grandaniel/vue-markdown-editor 1.1.3-dev.dd484fc → 1.1.3-dev.ed12560
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 +363 -5
- package/dist/components/MarkdownEditor/Composable/parseMarkdown.d.ts +2 -0
- package/dist/components/MarkdownEditor/Composable/serializeMarkdown.d.ts +2 -0
- package/dist/components/MarkdownEditor/ContextMenu/MarkdownEditorContextMenu.vue.d.ts +3 -1
- package/dist/components/MarkdownEditor/ContextMenu/MarkdownEditorFileContextMenu.vue.d.ts +19 -0
- package/dist/components/MarkdownEditor/ContextMenu/MarkdownEditorImageContextMenu.vue.d.ts +4 -0
- package/dist/components/MarkdownEditor/Factory/MarkdownNodeFactory.d.ts +2 -0
- package/dist/components/MarkdownEditor/MarkdownComponentRegistry.d.ts +2 -1
- package/dist/components/MarkdownEditor/MarkdownEditor.vue.d.ts +8 -0
- package/dist/components/MarkdownEditor/MarkdownEditorModule.vue.d.ts +2 -2
- package/dist/components/MarkdownEditor/Modules/MarkdownModuleFile.vue.d.ts +16 -0
- package/dist/components/MarkdownEditor/Modules/MarkdownModuleFileState.d.ts +8 -0
- package/dist/components/MarkdownEditor/Modules/MarkdownModuleHeadline1.vue.d.ts +2 -2
- package/dist/components/MarkdownEditor/Modules/MarkdownModuleHeadline2.vue.d.ts +2 -2
- package/dist/components/MarkdownEditor/Modules/MarkdownModuleHeadline3.vue.d.ts +2 -2
- package/dist/components/MarkdownEditor/Modules/MarkdownModuleParagraph.vue.d.ts +2 -2
- package/dist/components/MarkdownEditor/Types/MarkdownAstNode.d.ts +2 -0
- package/dist/components/MarkdownEditor/Types/MarkdownAstNodeType.d.ts +2 -1
- package/dist/components/MarkdownEditor/index.d.ts +2 -1
- package/dist/components/MarkdownRenderer/MarkdownRenderComponentRegistry.d.ts +7 -0
- package/dist/components/MarkdownRenderer/MarkdownRenderer.vue.d.ts +13 -0
- package/dist/components/MarkdownRenderer/RenderModules/MarkdownModuleFileRender.vue.d.ts +7 -0
- package/dist/components/MarkdownRenderer/RenderModules/MarkdownModuleHeadline1Render.vue.d.ts +7 -0
- package/dist/components/MarkdownRenderer/RenderModules/MarkdownModuleHeadline2Render.vue.d.ts +7 -0
- package/dist/components/MarkdownRenderer/RenderModules/MarkdownModuleHeadline3Render.vue.d.ts +7 -0
- package/dist/components/MarkdownRenderer/RenderModules/MarkdownModuleImageRender.vue.d.ts +7 -0
- package/dist/components/MarkdownRenderer/RenderModules/MarkdownModuleListRender.vue.d.ts +7 -0
- package/dist/components/MarkdownRenderer/RenderModules/MarkdownModuleParagraphRender.vue.d.ts +7 -0
- package/dist/components/MarkdownRenderer/index.d.ts +2 -0
- package/dist/components/index.d.ts +1 -0
- package/dist/vue-markdown-editor.css +1 -1
- package/dist/vue-markdown-editor.mjs +4711 -4237
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -1,6 +1,41 @@
|
|
|
1
|
+
<picture>
|
|
2
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/img/editor-1.png">
|
|
3
|
+
<img alt="vue-markdown-editor — A block-based, Notion-like markdown editor for Vue 3" src="docs/img/editor-1.png">
|
|
4
|
+
</picture>
|
|
5
|
+
|
|
1
6
|
# @grandaniel/vue-markdown-editor
|
|
2
7
|
|
|
3
|
-
Vue 3
|
|
8
|
+
> A block-based, Notion-like Markdown editor for Vue 3. UI-first. Powered by [TipTap](https://tiptap.dev/).
|
|
9
|
+
|
|
10
|
+
**@grandaniel/vue-markdown-editor** is a rich block-editing experience where every Markdown element — headlines, paragraphs, lists, images — is its own independently editable, draggable block. Built for content-first workflows: write, reorder, and format with keyboard shortcuts and an always-visible drag handle.
|
|
11
|
+
|
|
12
|
+
Images are first-class citizens: paste an image to auto-upload, then edit its **src**, **alt text**, and **caption** in a dedicated modal. The editor serializes back to clean Markdown automatically.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Table of Contents
|
|
17
|
+
|
|
18
|
+
- [Who uses it](#who-uses-it)
|
|
19
|
+
- [Installation](#installation)
|
|
20
|
+
- [Quick Start](#quick-start)
|
|
21
|
+
- [Keyboard Shortcuts](#keyboard-shortcuts)
|
|
22
|
+
- [Image Upload](#image-upload)
|
|
23
|
+
- [API Reference](#api-reference)
|
|
24
|
+
- [Custom Styling](#custom-styling)
|
|
25
|
+
- [Supply Chain Security](#supply-chain-security)
|
|
26
|
+
- [Contributing](#contributing)
|
|
27
|
+
- [License](#license)
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Who uses it
|
|
32
|
+
|
|
33
|
+
| Project | How |
|
|
34
|
+
|---|---|
|
|
35
|
+
| **[heartbeat.systems](https://heartbeat.systems)** | Admin utility — content editors manage help articles, release notes, and in-app documentation through the block editor. |
|
|
36
|
+
| **[markdownstud.io](https://markdownstud.io)** | AI writing assistant — the editor serves as the primary composition surface where users draft, review, and polish AI-generated content. |
|
|
37
|
+
|
|
38
|
+
---
|
|
4
39
|
|
|
5
40
|
## Installation
|
|
6
41
|
|
|
@@ -8,16 +43,34 @@ Vue 3 markdown editor component library.
|
|
|
8
43
|
npm install @grandaniel/vue-markdown-editor
|
|
9
44
|
```
|
|
10
45
|
|
|
46
|
+
> **Peer dependency:** Vue `^3.5.0`
|
|
47
|
+
> **Node:** `>=22`
|
|
48
|
+
|
|
49
|
+
Import the component **and** the stylesheet:
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
import { MarkdownEditor, useMarkdownEditor } from "@grandaniel/vue-markdown-editor";
|
|
53
|
+
import "@grandaniel/vue-markdown-editor/style.css";
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
11
58
|
## Quick Start
|
|
12
59
|
|
|
60
|
+
The editor is driven by a composable: `useMarkdownEditor()` creates the reactive state, and `<MarkdownEditor>` renders it.
|
|
61
|
+
|
|
13
62
|
```vue
|
|
14
63
|
<script setup lang="ts">
|
|
15
64
|
import { ref } from "vue";
|
|
16
|
-
import {
|
|
17
|
-
|
|
65
|
+
import {
|
|
66
|
+
MarkdownEditor,
|
|
67
|
+
useMarkdownEditor,
|
|
68
|
+
type MarkdownAstNode,
|
|
69
|
+
} from "@grandaniel/vue-markdown-editor";
|
|
70
|
+
import "@grandaniel/vue-markdown-editor/style.css";
|
|
18
71
|
|
|
19
|
-
const editor = useMarkdownEditor("# Hello
|
|
20
|
-
const focusedNode = ref(null);
|
|
72
|
+
const editor = useMarkdownEditor("# Hello, world!\n\nStart writing here…");
|
|
73
|
+
const focusedNode = ref<MarkdownAstNode | null>(null);
|
|
21
74
|
</script>
|
|
22
75
|
|
|
23
76
|
<template>
|
|
@@ -27,3 +80,308 @@ const focusedNode = ref(null);
|
|
|
27
80
|
/>
|
|
28
81
|
</template>
|
|
29
82
|
```
|
|
83
|
+
|
|
84
|
+
### Reading the output
|
|
85
|
+
|
|
86
|
+
The composable keeps the raw Markdown in sync automatically. Read it at any time:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
console.log(editor.markdownContent.value);
|
|
90
|
+
// "# Hello, world!\n\nStart writing here…"
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
You can also **programmatically set** the content:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
editor.markdownContent.value = "## New heading\n\nFresh content.";
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Keyboard Shortcuts
|
|
102
|
+
|
|
103
|
+
| Key | Action |
|
|
104
|
+
|---|---|
|
|
105
|
+
| <kbd>↑</kbd> / <kbd>↓</kbd> | Move focus between blocks |
|
|
106
|
+
| <kbd>Enter</kbd> | Split current block → insert new paragraph below |
|
|
107
|
+
| <kbd>Backspace</kbd> (empty block) | Delete the block, focus moves up |
|
|
108
|
+
| <kbd>Delete</kbd> (empty block) | Delete the block, focus stays at same index |
|
|
109
|
+
| Click blank area | Append a new empty paragraph at the bottom |
|
|
110
|
+
|
|
111
|
+
### Auto type‑detection
|
|
112
|
+
|
|
113
|
+
Type `# `, `## `, or `### ` at the start of a paragraph and the block auto‑converts to the matching heading level.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Image Upload
|
|
118
|
+
|
|
119
|
+
Images are first-class blocks with **src**, **alt text**, and **caption** fields. Right‑click any image → **Edit Attributes** to open the editing modal.
|
|
120
|
+
|
|
121
|
+
### Paste‑to‑upload
|
|
122
|
+
|
|
123
|
+
Pass an `imageUploadFunction` prop — any pasted image (`Ctrl+V`) from the clipboard will be sent through your upload handler and inserted as a new image block:
|
|
124
|
+
|
|
125
|
+
```vue
|
|
126
|
+
<script setup lang="ts">
|
|
127
|
+
async function uploadImage(file: File): Promise<string> {
|
|
128
|
+
const formData = new FormData();
|
|
129
|
+
formData.append("image", file);
|
|
130
|
+
|
|
131
|
+
const res = await fetch("/api/upload", { method: "POST", body: formData });
|
|
132
|
+
const { url } = await res.json();
|
|
133
|
+
return url;
|
|
134
|
+
}
|
|
135
|
+
</script>
|
|
136
|
+
|
|
137
|
+
<template>
|
|
138
|
+
<MarkdownEditor
|
|
139
|
+
:editor="editor"
|
|
140
|
+
:image-upload-function="uploadImage"
|
|
141
|
+
/>
|
|
142
|
+
</template>
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The serialized Markdown uses a custom block syntax for images:
|
|
146
|
+
|
|
147
|
+
```markdown
|
|
148
|
+
"""MarkdownModuleImage
|
|
149
|
+
src: https://example.com/photo.jpg
|
|
150
|
+
alt: A scenic mountain view
|
|
151
|
+
caption: Photo taken during the 2026 summit
|
|
152
|
+
"""
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
> **Note:** `imageUploadFunction` is optional. Without it, pasted images from the clipboard are ignored.
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## API Reference
|
|
160
|
+
|
|
161
|
+
### `useMarkdownEditor(initialContent?: string)`
|
|
162
|
+
|
|
163
|
+
Returns a reactive editor instance:
|
|
164
|
+
|
|
165
|
+
| Member | Type | Description |
|
|
166
|
+
|---|---|---|
|
|
167
|
+
| `markdownContent` | `Ref<string>` | Reactive raw Markdown. Read to serialize, write to load content. |
|
|
168
|
+
| `markdownNodes` | `Ref<MarkdownAstNode[]>` | Reactive array of AST nodes. |
|
|
169
|
+
| `deleteNode(index)` | `(index: number) => void` | Remove the node at `index`. |
|
|
170
|
+
| `addBlankNode(index?)` | `(index?: number) => number` | Insert an empty paragraph at `index` (or end). Returns the new index. |
|
|
171
|
+
| `addNodeWithType(index, type, content?)` | `(index: number, type: MarkdownNodeType, content?: string) => number` | Insert a typed node. Returns the new index. |
|
|
172
|
+
| `replaceNodeType(node, newType)` | `(node: MarkdownAstNode, type: MarkdownNodeType) => { newNode, index } \| null` | Convert between block types (e.g. paragraph → heading). |
|
|
173
|
+
| `moveNode(from, to)` | `(fromIndex: number, toIndex: number) => void` | Programmatically reorder a block. |
|
|
174
|
+
|
|
175
|
+
### `MarkdownEditor` props
|
|
176
|
+
|
|
177
|
+
| Prop | Type | Required | Description |
|
|
178
|
+
|---|---|---|---|
|
|
179
|
+
| `editor` | `MarkdownEditorInstance` | ✓ | Instance from `useMarkdownEditor()`. |
|
|
180
|
+
| `focusedNode` | `MarkdownAstNode \| null` | — | For `v-model:focused-node` tracking. |
|
|
181
|
+
| `imageUploadFunction` | `(file: File) => Promise<string>` | — | Async callback for paste‑to‑upload. |
|
|
182
|
+
|
|
183
|
+
### `MarkdownEditor` emits
|
|
184
|
+
|
|
185
|
+
| Event | Payload | Description |
|
|
186
|
+
|---|---|---|
|
|
187
|
+
| `update:focused-node` | `MarkdownAstNode \| null` | Fires when focus moves to a new block. |
|
|
188
|
+
|
|
189
|
+
### `MarkdownEditor` slots
|
|
190
|
+
|
|
191
|
+
| Slot | Description |
|
|
192
|
+
|---|---|
|
|
193
|
+
| `after-controls` | Injected inside every block, after the drag‑handle / add / delete controls. |
|
|
194
|
+
|
|
195
|
+
### Exported types & utilities
|
|
196
|
+
|
|
197
|
+
| Export | Kind |
|
|
198
|
+
|---|---|
|
|
199
|
+
| `MarkdownEditorInstance` | Type — return type of `useMarkdownEditor()`. |
|
|
200
|
+
| `MarkdownAstNode` | Class — AST node with `id`, `type`, `componentState`, `editingState`. |
|
|
201
|
+
| `MarkdownAstNodeType` | Enum — `PARAGRAPH`, `HEADLINE1`, `HEADLINE2`, `HEADLINE3`, `IMAGE`, `LIST`. |
|
|
202
|
+
| `ImageNode` | Type alias — `MarkdownAstNode<MarkdownModuleImageState>`. |
|
|
203
|
+
| `TextNode` | Type alias — `MarkdownAstNode<MarkdownModuleTextState>`. |
|
|
204
|
+
| `TextishNodeType` | Type — union of `PARAGRAPH \| HEADLINE1 \| HEADLINE2 \| HEADLINE3 \| LIST`. |
|
|
205
|
+
| `isTextNodeState(node)` | Type guard for text‑based nodes. |
|
|
206
|
+
| `isTextNodeType(type)` | Type guard for text‑based node types. |
|
|
207
|
+
| `MarkdownRenderer` | Component — renders Markdown to plain HTML. SSR‑safe. |
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## MarkdownRenderer
|
|
212
|
+
|
|
213
|
+
`<MarkdownRenderer>` converts a Markdown string to plain HTML. It uses the same `remark-parse` pipeline as the editor, so custom module blocks (like `"""MarkdownModuleImage"""`) render as rich `<figure>` elements automatically.
|
|
214
|
+
|
|
215
|
+
The renderer is **SSR‑safe** — no browser APIs, no TipTap. It works in `nuxt generate`, `vite-ssg`, and any server‑side rendering context.
|
|
216
|
+
|
|
217
|
+
```vue
|
|
218
|
+
<script setup lang="ts">
|
|
219
|
+
import { MarkdownRenderer } from "@grandaniel/vue-markdown-editor";
|
|
220
|
+
|
|
221
|
+
const markdown = `# Hello World
|
|
222
|
+
|
|
223
|
+
This is a **paragraph** with *inline* formatting.
|
|
224
|
+
|
|
225
|
+
- List item 1
|
|
226
|
+
- List item 2
|
|
227
|
+
|
|
228
|
+
"""MarkdownModuleImage
|
|
229
|
+
src: https://example.com/photo.jpg
|
|
230
|
+
alt: A scenic view
|
|
231
|
+
caption: Photo caption
|
|
232
|
+
"""
|
|
233
|
+
`;
|
|
234
|
+
</script>
|
|
235
|
+
|
|
236
|
+
<template>
|
|
237
|
+
<MarkdownRenderer :markdown="markdown" />
|
|
238
|
+
</template>
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Live preview alongside the editor
|
|
242
|
+
|
|
243
|
+
Bind the editor's reactive `markdownContent` to the renderer:
|
|
244
|
+
|
|
245
|
+
```vue
|
|
246
|
+
<script setup lang="ts">
|
|
247
|
+
import { MarkdownEditor, MarkdownRenderer, useMarkdownEditor } from "@grandaniel/vue-markdown-editor";
|
|
248
|
+
|
|
249
|
+
const editor = useMarkdownEditor("# Start writing…");
|
|
250
|
+
</script>
|
|
251
|
+
|
|
252
|
+
<template>
|
|
253
|
+
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 2rem;">
|
|
254
|
+
<MarkdownEditor :editor="editor" />
|
|
255
|
+
<MarkdownRenderer :markdown="editor.markdownContent.value" />
|
|
256
|
+
</div>
|
|
257
|
+
</template>
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Props
|
|
261
|
+
|
|
262
|
+
| Prop | Type | Required | Description |
|
|
263
|
+
|---|---|---|---|
|
|
264
|
+
| `markdown` | `string` | ✓ | Raw Markdown string to render as HTML. |
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## Custom Styling
|
|
269
|
+
|
|
270
|
+
All components use scoped SCSS. To override styles, use **global CSS** with higher specificity, or Vue's `:deep()` combinator from a parent component.
|
|
271
|
+
|
|
272
|
+
### CSS class reference
|
|
273
|
+
|
|
274
|
+
| Class | Applies to |
|
|
275
|
+
|---|---|
|
|
276
|
+
| `.markdown-editor` | Root editor container |
|
|
277
|
+
| `.markdown-editor-module` | Individual block wrapper — `.is-focused` when active |
|
|
278
|
+
| `.markdown-editor-module-controls` | Left control bar (drag handle + add/delete buttons) |
|
|
279
|
+
| `.markdown-editor-module-content` | Content area inside a block |
|
|
280
|
+
| `.markdown-editor-module-content-focused` | Content area when the block is focused |
|
|
281
|
+
| `.markdown-editor-focus-controls` | Row containing drag‑handle, delete, and add buttons |
|
|
282
|
+
| `.drag-handle` | SortableJS drag handle (⠿) |
|
|
283
|
+
| `.focus-control-btn` | Delete / Add buttons in the control bar |
|
|
284
|
+
| `.markdown-editor-context-menu` | Floating block context menu (`z-index: 1000`) |
|
|
285
|
+
| `.markdown-editor-context-menu-block-item` | Full‑width context menu button |
|
|
286
|
+
| `.markdown-editor-context-menu-inline-item` | Inline toolbar button (`.is-active` when toggled) |
|
|
287
|
+
| `.markdown-editor-modal-overlay` | Modal backdrop (`z-index: 9999`) |
|
|
288
|
+
| `.markdown-editor-modal` | Modal container |
|
|
289
|
+
| `.markdown-editor-modal-header` | Modal title bar |
|
|
290
|
+
| `.markdown-editor-modal-title` | Modal heading text |
|
|
291
|
+
| `.markdown-editor-modal-close` | Close (✕) button |
|
|
292
|
+
| `.markdown-editor-modal-body` | Modal content area |
|
|
293
|
+
| `.markdown-editor-modal-footer` | Modal action bar |
|
|
294
|
+
| `.markdown-editor-modal-button` | Base modal button |
|
|
295
|
+
| `.markdown-editor-modal-button-primary` | Primary (Save) button — blue |
|
|
296
|
+
| `.markdown-editor-modal-button-secondary` | Secondary (Cancel) button — gray |
|
|
297
|
+
| `.markdown-module-image` | Image block wrapper |
|
|
298
|
+
| `.markdown-module-image-form` | Image edit form inside the modal |
|
|
299
|
+
| `.markdown-module-image-form-field` | Form field group (label + input) |
|
|
300
|
+
|
|
301
|
+
### Styling TipTap content
|
|
302
|
+
|
|
303
|
+
Each text‑based block hosts its own TinyMCE‑style TipTap editor. Target `.tiptap` inside a block's content area:
|
|
304
|
+
|
|
305
|
+
```css
|
|
306
|
+
/* Make all TipTap editors use your font */
|
|
307
|
+
.markdown-editor-module-content .tiptap {
|
|
308
|
+
font-family: "Georgia", serif;
|
|
309
|
+
font-size: 1.1rem;
|
|
310
|
+
line-height: 1.8;
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### Do's
|
|
315
|
+
|
|
316
|
+
- ✅ Import the stylesheet: `import "@grandaniel/vue-markdown-editor/style.css"`
|
|
317
|
+
- ✅ Use **global** (unscoped) CSS or `:deep()` from a parent to override styles
|
|
318
|
+
- ✅ Target `.tiptap` inside `.markdown-editor-module-content` for editor typography
|
|
319
|
+
- ✅ Use `z-index` values above `1000` / `9999` for anything that must layer **above** context menus and modals
|
|
320
|
+
|
|
321
|
+
### Don'ts
|
|
322
|
+
|
|
323
|
+
- ❌ Don't rely on CSS custom properties — the editor uses hard‑coded Tailwind‑scale colors (grays and blues)
|
|
324
|
+
- ❌ Don't override `z-index` on `.markdown-editor-context-menu` or `.markdown-editor-modal-overlay` — it will break layering
|
|
325
|
+
- ❌ Don't use `display: contents` on `.markdown-editor-module` — it interferes with SortableJS drag logic
|
|
326
|
+
- ❌ Don't set `outline: none` on `.markdown-editor-module-content` globally — the focus ring is intentional for keyboard navigation
|
|
327
|
+
|
|
328
|
+
---
|
|
329
|
+
|
|
330
|
+
## Supply Chain Security
|
|
331
|
+
|
|
332
|
+
We take package integrity seriously.
|
|
333
|
+
|
|
334
|
+
| Measure | Status |
|
|
335
|
+
|---|---|
|
|
336
|
+
| **npm package provenance** | ✅ Enabled — every publish includes [provenance attestations](https://docs.npmjs.com/generating-provenance-statements) via GitHub Actions and Sigstore. |
|
|
337
|
+
| **CI/CD** | ✅ GitHub Actions runs `npm ci` → `npm test` → `npm run build` → publish on every push to `dev` and `main`. |
|
|
338
|
+
| **Prerelease tags** | ✅ Non‑main branches publish with a `dev` dist‑tag (e.g. `1.1.3-dev.abc1234`). |
|
|
339
|
+
| **Dependabot** | 🔜 Planned — automated dependency update PRs will be enabled via `.github/dependabot.yml`. |
|
|
340
|
+
|
|
341
|
+
To verify provenance locally:
|
|
342
|
+
|
|
343
|
+
```bash
|
|
344
|
+
npm audit signatures
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
---
|
|
348
|
+
|
|
349
|
+
## Contributing
|
|
350
|
+
|
|
351
|
+
We welcome contributions! Please follow the guidelines below.
|
|
352
|
+
|
|
353
|
+
### PR Policy
|
|
354
|
+
|
|
355
|
+
1. **Fork** the repository and create a feature branch off `dev`.
|
|
356
|
+
2. **Keep changes focused** — one feature or fix per PR.
|
|
357
|
+
3. **Add tests** for any new functionality. The project uses [Vitest](https://vitest.dev/) + [`@vue/test-utils`](https://test-utils.vuejs.org/).
|
|
358
|
+
4. **Run the full check** before pushing:
|
|
359
|
+
|
|
360
|
+
```bash
|
|
361
|
+
npm ci
|
|
362
|
+
npm run test
|
|
363
|
+
npm run type-check
|
|
364
|
+
npm run build
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
5. **Open a PR** against the `dev` branch with a clear description of what changed and why.
|
|
368
|
+
|
|
369
|
+
### Dev setup
|
|
370
|
+
|
|
371
|
+
```bash
|
|
372
|
+
# Clone and install
|
|
373
|
+
git clone https://github.com/danielgran/vue-markdown-editor.git
|
|
374
|
+
cd vue-markdown-editor
|
|
375
|
+
npm ci
|
|
376
|
+
|
|
377
|
+
# Start the dev server
|
|
378
|
+
npm run dev
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
The dev server launches at `http://localhost:4010`. The entry point is `dev/App.vue` — a full showcase that demonstrates every editor feature: block types, drag & drop, image upload, context menus, keyboard navigation, and Markdown output serialization. Use it as a playground while developing.
|
|
382
|
+
|
|
383
|
+
---
|
|
384
|
+
|
|
385
|
+
## License
|
|
386
|
+
|
|
387
|
+
[ISC](LICENSE) © 2026 [danielgran](https://github.com/danielgran)
|
|
@@ -7,7 +7,9 @@ declare var __VLS_7: {};
|
|
|
7
7
|
type __VLS_Slots = {} & {
|
|
8
8
|
default?: (props: typeof __VLS_7) => any;
|
|
9
9
|
};
|
|
10
|
-
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
|
|
10
|
+
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
|
|
11
|
+
rootEl: import("vue").Ref<HTMLElement | null, HTMLElement | null>;
|
|
12
|
+
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
11
13
|
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
12
14
|
declare const _default: typeof __VLS_export;
|
|
13
15
|
export default _default;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
x: number;
|
|
3
|
+
y: number;
|
|
4
|
+
};
|
|
5
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
6
|
+
click: () => any;
|
|
7
|
+
editAttributes: () => any;
|
|
8
|
+
download: () => any;
|
|
9
|
+
retry: () => any;
|
|
10
|
+
close: () => any;
|
|
11
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
12
|
+
onClick?: (() => any) | undefined;
|
|
13
|
+
onEditAttributes?: (() => any) | undefined;
|
|
14
|
+
onDownload?: (() => any) | undefined;
|
|
15
|
+
onRetry?: (() => any) | undefined;
|
|
16
|
+
onClose?: (() => any) | undefined;
|
|
17
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
18
|
+
declare const _default: typeof __VLS_export;
|
|
19
|
+
export default _default;
|
|
@@ -3,9 +3,13 @@ type __VLS_Props = {
|
|
|
3
3
|
y: number;
|
|
4
4
|
};
|
|
5
5
|
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
6
|
+
click: () => any;
|
|
6
7
|
editAttributes: () => any;
|
|
8
|
+
close: () => any;
|
|
7
9
|
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
10
|
+
onClick?: (() => any) | undefined;
|
|
8
11
|
onEditAttributes?: (() => any) | undefined;
|
|
12
|
+
onClose?: (() => any) | undefined;
|
|
9
13
|
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
10
14
|
declare const _default: typeof __VLS_export;
|
|
11
15
|
export default _default;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type MarkdownModuleFileState from "../Modules/MarkdownModuleFileState";
|
|
1
2
|
import type MarkdownModuleImageState from "../Modules/MarkdownModuleImageState";
|
|
2
3
|
import type MarkdownModuleListState from "../Modules/MarkdownModuleListState";
|
|
3
4
|
import MarkdownModuleTextState from "../Modules/MarkdownModuleTextState";
|
|
@@ -9,6 +10,7 @@ declare class MarkdownNodeFactory {
|
|
|
9
10
|
createImageNode(src: string, alt: string, caption?: string): MarkdownAstNode<MarkdownModuleImageState>;
|
|
10
11
|
createListNode(items: string[]): MarkdownAstNode<MarkdownModuleListState>;
|
|
11
12
|
createBlankParagraph(): MarkdownAstNode<MarkdownModuleTextState>;
|
|
13
|
+
createFileNode(url: string, fileName: string, fileSize: number, mimeType: string, uploadError?: string): MarkdownAstNode<MarkdownModuleFileState>;
|
|
12
14
|
}
|
|
13
15
|
declare const _default: MarkdownNodeFactory;
|
|
14
16
|
export default _default;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Component } from "vue";
|
|
2
|
+
import MarkdownModuleFileState from "./Modules/MarkdownModuleFileState";
|
|
2
3
|
import MarkdownModuleImageState from "./Modules/MarkdownModuleImageState";
|
|
3
4
|
import MarkdownModuleListState from "./Modules/MarkdownModuleListState";
|
|
4
5
|
import MarkdownModuleTextState from "./Modules/MarkdownModuleTextState";
|
|
@@ -6,7 +7,7 @@ import type { MarkdownAstNode } from "./Types/MarkdownAstNode";
|
|
|
6
7
|
import MarkdownNodeType from "./Types/MarkdownAstNodeType";
|
|
7
8
|
type MarkdownComponentRegistryEntry = {
|
|
8
9
|
component: Component;
|
|
9
|
-
stateType: typeof MarkdownModuleTextState | typeof MarkdownModuleImageState | typeof MarkdownModuleListState;
|
|
10
|
+
stateType: typeof MarkdownModuleTextState | typeof MarkdownModuleImageState | typeof MarkdownModuleListState | typeof MarkdownModuleFileState;
|
|
10
11
|
};
|
|
11
12
|
declare const registry: Record<MarkdownNodeType, MarkdownComponentRegistryEntry>;
|
|
12
13
|
export declare function isTextNodeState(state: MarkdownAstNode): state is MarkdownAstNode & {
|
|
@@ -19,6 +19,10 @@ declare const __VLS_base: import("vue").DefineComponent<import("vue").ExtractPro
|
|
|
19
19
|
type: PropType<(file: File) => Promise<string>>;
|
|
20
20
|
required: false;
|
|
21
21
|
};
|
|
22
|
+
fileUploadFunction: {
|
|
23
|
+
type: PropType<(file: File) => Promise<string>>;
|
|
24
|
+
required: false;
|
|
25
|
+
};
|
|
22
26
|
}>, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
23
27
|
"update:focused-node": (value: MarkdownAstNode<object> | null) => any;
|
|
24
28
|
}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
@@ -35,6 +39,10 @@ declare const __VLS_base: import("vue").DefineComponent<import("vue").ExtractPro
|
|
|
35
39
|
type: PropType<(file: File) => Promise<string>>;
|
|
36
40
|
required: false;
|
|
37
41
|
};
|
|
42
|
+
fileUploadFunction: {
|
|
43
|
+
type: PropType<(file: File) => Promise<string>>;
|
|
44
|
+
required: false;
|
|
45
|
+
};
|
|
38
46
|
}>> & Readonly<{
|
|
39
47
|
"onUpdate:focused-node"?: ((value: MarkdownAstNode<object> | null) => any) | undefined;
|
|
40
48
|
}>, {
|
|
@@ -16,9 +16,9 @@ declare const __VLS_base: import("vue").DefineComponent<import("vue").ExtractPro
|
|
|
16
16
|
required: true;
|
|
17
17
|
};
|
|
18
18
|
}>, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
19
|
+
focus: () => any;
|
|
19
20
|
"update:cursor-position": (args_0: number) => any;
|
|
20
21
|
"change-type": (args_0: MarkdownAstNode<object>, args_1: number) => any;
|
|
21
|
-
focus: () => any;
|
|
22
22
|
"update:node": (args_0: Record<string, unknown>) => any;
|
|
23
23
|
}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
24
24
|
node: {
|
|
@@ -30,9 +30,9 @@ declare const __VLS_base: import("vue").DefineComponent<import("vue").ExtractPro
|
|
|
30
30
|
required: true;
|
|
31
31
|
};
|
|
32
32
|
}>> & Readonly<{
|
|
33
|
+
onFocus?: (() => any) | undefined;
|
|
33
34
|
"onUpdate:cursor-position"?: ((args_0: number) => any) | undefined;
|
|
34
35
|
"onChange-type"?: ((args_0: MarkdownAstNode<object>, args_1: number) => any) | undefined;
|
|
35
|
-
onFocus?: (() => any) | undefined;
|
|
36
36
|
"onUpdate:node"?: ((args_0: Record<string, unknown>) => any) | undefined;
|
|
37
37
|
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
38
38
|
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type MarkdownModuleFileState from "./MarkdownModuleFileState";
|
|
2
|
+
declare function focus(): void;
|
|
3
|
+
type __VLS_ModelProps = {
|
|
4
|
+
modelValue: MarkdownModuleFileState;
|
|
5
|
+
};
|
|
6
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {
|
|
7
|
+
focus: typeof focus;
|
|
8
|
+
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
9
|
+
"retry-upload": (fileState: MarkdownModuleFileState) => any;
|
|
10
|
+
"update:modelValue": (value: MarkdownModuleFileState) => any;
|
|
11
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_ModelProps> & Readonly<{
|
|
12
|
+
"onRetry-upload"?: ((fileState: MarkdownModuleFileState) => any) | undefined;
|
|
13
|
+
"onUpdate:modelValue"?: ((value: MarkdownModuleFileState) => any) | undefined;
|
|
14
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
15
|
+
declare const _default: typeof __VLS_export;
|
|
16
|
+
export default _default;
|
|
@@ -6,15 +6,15 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {
|
|
|
6
6
|
editor: import("vue").ShallowRef<import("@tiptap/vue-3").Editor | undefined, import("@tiptap/vue-3").Editor | undefined>;
|
|
7
7
|
focus: () => void;
|
|
8
8
|
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
9
|
+
"update:modelValue": (value: MarkdownModuleTextState) => any;
|
|
9
10
|
"update:model-value": (componentState: MarkdownModuleTextState) => any;
|
|
10
11
|
"update:cursor-position": (cursorPosition: number) => any;
|
|
11
12
|
"change-type": (newType: import("..").MarkdownAstNodeType) => any;
|
|
12
|
-
"update:modelValue": (value: MarkdownModuleTextState) => any;
|
|
13
13
|
}, string, import("vue").PublicProps, Readonly<__VLS_ModelProps> & Readonly<{
|
|
14
|
+
"onUpdate:modelValue"?: ((value: MarkdownModuleTextState) => any) | undefined;
|
|
14
15
|
"onUpdate:model-value"?: ((componentState: MarkdownModuleTextState) => any) | undefined;
|
|
15
16
|
"onUpdate:cursor-position"?: ((cursorPosition: number) => any) | undefined;
|
|
16
17
|
"onChange-type"?: ((newType: import("..").MarkdownAstNodeType) => any) | undefined;
|
|
17
|
-
"onUpdate:modelValue"?: ((value: MarkdownModuleTextState) => any) | undefined;
|
|
18
18
|
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
19
19
|
declare const _default: typeof __VLS_export;
|
|
20
20
|
export default _default;
|
|
@@ -6,15 +6,15 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {
|
|
|
6
6
|
editor: import("vue").ShallowRef<import("@tiptap/vue-3").Editor | undefined, import("@tiptap/vue-3").Editor | undefined>;
|
|
7
7
|
focus: () => void;
|
|
8
8
|
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
9
|
+
"update:modelValue": (value: MarkdownModuleTextState) => any;
|
|
9
10
|
"update:model-value": (componentState: MarkdownModuleTextState) => any;
|
|
10
11
|
"update:cursor-position": (cursorPosition: number) => any;
|
|
11
12
|
"change-type": (newType: import("..").MarkdownAstNodeType) => any;
|
|
12
|
-
"update:modelValue": (value: MarkdownModuleTextState) => any;
|
|
13
13
|
}, string, import("vue").PublicProps, Readonly<__VLS_ModelProps> & Readonly<{
|
|
14
|
+
"onUpdate:modelValue"?: ((value: MarkdownModuleTextState) => any) | undefined;
|
|
14
15
|
"onUpdate:model-value"?: ((componentState: MarkdownModuleTextState) => any) | undefined;
|
|
15
16
|
"onUpdate:cursor-position"?: ((cursorPosition: number) => any) | undefined;
|
|
16
17
|
"onChange-type"?: ((newType: import("..").MarkdownAstNodeType) => any) | undefined;
|
|
17
|
-
"onUpdate:modelValue"?: ((value: MarkdownModuleTextState) => any) | undefined;
|
|
18
18
|
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
19
19
|
declare const _default: typeof __VLS_export;
|
|
20
20
|
export default _default;
|
|
@@ -6,15 +6,15 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {
|
|
|
6
6
|
editor: import("vue").ShallowRef<import("@tiptap/vue-3").Editor | undefined, import("@tiptap/vue-3").Editor | undefined>;
|
|
7
7
|
focus: () => void;
|
|
8
8
|
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
9
|
+
"update:modelValue": (value: MarkdownModuleTextState) => any;
|
|
9
10
|
"update:model-value": (componentState: MarkdownModuleTextState) => any;
|
|
10
11
|
"update:cursor-position": (cursorPosition: number) => any;
|
|
11
12
|
"change-type": (newType: import("..").MarkdownAstNodeType) => any;
|
|
12
|
-
"update:modelValue": (value: MarkdownModuleTextState) => any;
|
|
13
13
|
}, string, import("vue").PublicProps, Readonly<__VLS_ModelProps> & Readonly<{
|
|
14
|
+
"onUpdate:modelValue"?: ((value: MarkdownModuleTextState) => any) | undefined;
|
|
14
15
|
"onUpdate:model-value"?: ((componentState: MarkdownModuleTextState) => any) | undefined;
|
|
15
16
|
"onUpdate:cursor-position"?: ((cursorPosition: number) => any) | undefined;
|
|
16
17
|
"onChange-type"?: ((newType: import("..").MarkdownAstNodeType) => any) | undefined;
|
|
17
|
-
"onUpdate:modelValue"?: ((value: MarkdownModuleTextState) => any) | undefined;
|
|
18
18
|
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
19
19
|
declare const _default: typeof __VLS_export;
|
|
20
20
|
export default _default;
|
|
@@ -6,15 +6,15 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {
|
|
|
6
6
|
editor: import("vue").ShallowRef<import("@tiptap/vue-3").Editor | undefined, import("@tiptap/vue-3").Editor | undefined>;
|
|
7
7
|
focus: () => void;
|
|
8
8
|
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
9
|
+
"update:modelValue": (value: MarkdownModuleTextState) => any;
|
|
9
10
|
"update:model-value": (componentState: MarkdownModuleTextState) => any;
|
|
10
11
|
"update:cursor-position": (cursorPosition: number) => any;
|
|
11
12
|
"change-type": (newType: import("..").MarkdownAstNodeType) => any;
|
|
12
|
-
"update:modelValue": (value: MarkdownModuleTextState) => any;
|
|
13
13
|
}, string, import("vue").PublicProps, Readonly<__VLS_ModelProps> & Readonly<{
|
|
14
|
+
"onUpdate:modelValue"?: ((value: MarkdownModuleTextState) => any) | undefined;
|
|
14
15
|
"onUpdate:model-value"?: ((componentState: MarkdownModuleTextState) => any) | undefined;
|
|
15
16
|
"onUpdate:cursor-position"?: ((cursorPosition: number) => any) | undefined;
|
|
16
17
|
"onChange-type"?: ((newType: import("..").MarkdownAstNodeType) => any) | undefined;
|
|
17
|
-
"onUpdate:modelValue"?: ((value: MarkdownModuleTextState) => any) | undefined;
|
|
18
18
|
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
19
19
|
declare const _default: typeof __VLS_export;
|
|
20
20
|
export default _default;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type MarkdownModuleFileState from "../Modules/MarkdownModuleFileState";
|
|
1
2
|
import type MarkdownModuleImageState from "../Modules/MarkdownModuleImageState";
|
|
2
3
|
import type MarkdownModuleTextState from "../Modules/MarkdownModuleTextState";
|
|
3
4
|
import type MarkdownNodeType from "./MarkdownAstNodeType";
|
|
@@ -12,3 +13,4 @@ export declare class MarkdownAstNode<TState extends object = object> {
|
|
|
12
13
|
}
|
|
13
14
|
export type TextNode = MarkdownAstNode<MarkdownModuleTextState>;
|
|
14
15
|
export type ImageNode = MarkdownAstNode<MarkdownModuleImageState>;
|
|
16
|
+
export type FileNode = MarkdownAstNode<MarkdownModuleFileState>;
|
|
@@ -4,7 +4,8 @@ declare enum MarkdownNodeType {
|
|
|
4
4
|
HEADLINE2 = 2,
|
|
5
5
|
HEADLINE3 = 3,
|
|
6
6
|
IMAGE = 4,
|
|
7
|
-
LIST = 5
|
|
7
|
+
LIST = 5,
|
|
8
|
+
FILE = 6
|
|
8
9
|
}
|
|
9
10
|
export type TextishNodeType = MarkdownNodeType.PARAGRAPH | MarkdownNodeType.LIST | MarkdownNodeType.HEADLINE1 | MarkdownNodeType.HEADLINE2 | MarkdownNodeType.HEADLINE3;
|
|
10
11
|
export declare function isTextNodeType(type: MarkdownNodeType): type is TextishNodeType;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { default as MarkdownEditor } from './MarkdownEditor.vue';
|
|
2
2
|
export { useMarkdownEditor, type MarkdownEditorInstance } from "./Composable/useMarkdownEditor";
|
|
3
3
|
export { isTextNodeState } from "./MarkdownComponentRegistry";
|
|
4
|
-
export { MarkdownAstNode, type ImageNode, type TextNode } from "./Types/MarkdownAstNode";
|
|
4
|
+
export { MarkdownAstNode, type FileNode, type ImageNode, type TextNode } from "./Types/MarkdownAstNode";
|
|
5
5
|
export { default as MarkdownAstNodeType, isTextNodeType, type TextishNodeType } from "./Types/MarkdownAstNodeType";
|
|
6
|
+
export { default as MarkdownModuleFileState } from "./Modules/MarkdownModuleFileState";
|