@grandaniel/vue-markdown-editor 1.1.3 → 1.2.0-dev.3b1fd30
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 +588 -6
- package/dist/components/MarkdownEditor/Composable/activeEditorStore.d.ts +7 -0
- package/dist/components/MarkdownEditor/Composable/parseMarkdown.d.ts +2 -0
- package/dist/components/MarkdownEditor/Composable/serializeMarkdown.d.ts +2 -0
- package/dist/components/MarkdownEditor/Composable/useMarkdownEditor.d.ts +1 -0
- package/dist/components/MarkdownEditor/Composable/useMarkdownProcessor.d.ts +1 -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 +6 -2
- 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 +16 -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 +3 -2
- package/dist/components/MarkdownEditor/Modules/MarkdownModuleHeadline2.vue.d.ts +3 -2
- package/dist/components/MarkdownEditor/Modules/MarkdownModuleHeadline3.vue.d.ts +3 -2
- package/dist/components/MarkdownEditor/Modules/MarkdownModuleList.vue.d.ts +1 -0
- package/dist/components/MarkdownEditor/Modules/MarkdownModuleParagraph.vue.d.ts +3 -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/Composable/useMarkdownRenderer.d.ts +1189 -0
- package/dist/components/MarkdownRenderer/MarkdownRenderer.vue.d.ts +1170 -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/defaultRenderComponentRegistry.d.ts +10 -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 +4770 -4174
- package/package.json +8 -7
package/README.md
CHANGED
|
@@ -1,6 +1,51 @@
|
|
|
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 **and** SSR-safe renderer for Vue 3. UI-first. Powered by [TipTap](https://tiptap.dev/).
|
|
9
|
+
|
|
10
|
+
**@grandaniel/vue-markdown-editor** provides two complementary packages:
|
|
11
|
+
|
|
12
|
+
- **Editor** — 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.
|
|
13
|
+
|
|
14
|
+
- **Renderer** — A lightweight, SSR-safe component that renders Markdown strings to semantic HTML. Uses the same `remark-parse` pipeline as the editor, so custom module blocks render identically. Fully customizable: override any default render component with your own Vue components.
|
|
15
|
+
|
|
16
|
+
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.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Table of Contents
|
|
21
|
+
|
|
22
|
+
- [Who uses it](#who-uses-it)
|
|
23
|
+
- [Installation](#installation)
|
|
24
|
+
- [Editor](#editor)
|
|
25
|
+
- [Quick Start](#editor-quick-start)
|
|
26
|
+
- [Keyboard Shortcuts](#keyboard-shortcuts)
|
|
27
|
+
- [Image Upload](#image-upload)
|
|
28
|
+
- [API Reference](#editor-api-reference)
|
|
29
|
+
- [Renderer](#renderer)
|
|
30
|
+
- [Quick Start](#renderer-quick-start)
|
|
31
|
+
- [Custom Render Components](#custom-render-components)
|
|
32
|
+
- [API Reference](#renderer-api-reference)
|
|
33
|
+
- [Exported Types & Utilities](#exported-types--utilities)
|
|
34
|
+
- [Custom Styling](#custom-styling)
|
|
35
|
+
- [Supply Chain Security](#supply-chain-security)
|
|
36
|
+
- [Contributing](#contributing)
|
|
37
|
+
- [License](#license)
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Who uses it
|
|
42
|
+
|
|
43
|
+
| Project | How |
|
|
44
|
+
|---|---|
|
|
45
|
+
| **[heartbeat.systems](https://heartbeat.systems)** | Admin utility — content editors manage help articles, release notes, and in-app documentation through the block editor. |
|
|
46
|
+
| **[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. |
|
|
47
|
+
|
|
48
|
+
---
|
|
4
49
|
|
|
5
50
|
## Installation
|
|
6
51
|
|
|
@@ -8,16 +53,36 @@ Vue 3 markdown editor component library.
|
|
|
8
53
|
npm install @grandaniel/vue-markdown-editor
|
|
9
54
|
```
|
|
10
55
|
|
|
11
|
-
|
|
56
|
+
> **Peer dependency:** Vue `^3.5.0`
|
|
57
|
+
> **Node:** `>=22`
|
|
58
|
+
|
|
59
|
+
Import the components **and** the stylesheet:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import { MarkdownEditor, MarkdownRenderer, useMarkdownEditor, useMarkdownRenderer } from "@grandaniel/vue-markdown-editor";
|
|
63
|
+
import "@grandaniel/vue-markdown-editor/style.css";
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Editor
|
|
69
|
+
|
|
70
|
+
The editor is driven by a composable: `useMarkdownEditor()` creates the reactive state, and `<MarkdownEditor>` renders it.
|
|
71
|
+
|
|
72
|
+
### Editor Quick Start
|
|
12
73
|
|
|
13
74
|
```vue
|
|
14
75
|
<script setup lang="ts">
|
|
15
76
|
import { ref } from "vue";
|
|
16
|
-
import {
|
|
17
|
-
|
|
77
|
+
import {
|
|
78
|
+
MarkdownEditor,
|
|
79
|
+
useMarkdownEditor,
|
|
80
|
+
type MarkdownAstNode,
|
|
81
|
+
} from "@grandaniel/vue-markdown-editor";
|
|
82
|
+
import "@grandaniel/vue-markdown-editor/style.css";
|
|
18
83
|
|
|
19
|
-
const editor = useMarkdownEditor("# Hello
|
|
20
|
-
const focusedNode = ref(null);
|
|
84
|
+
const editor = useMarkdownEditor("# Hello, world!\n\nStart writing here…");
|
|
85
|
+
const focusedNode = ref<MarkdownAstNode | null>(null);
|
|
21
86
|
</script>
|
|
22
87
|
|
|
23
88
|
<template>
|
|
@@ -27,3 +92,520 @@ const focusedNode = ref(null);
|
|
|
27
92
|
/>
|
|
28
93
|
</template>
|
|
29
94
|
```
|
|
95
|
+
|
|
96
|
+
#### Reading the output
|
|
97
|
+
|
|
98
|
+
The composable keeps the raw Markdown in sync automatically. Read it at any time:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
console.log(editor.markdownContent.value);
|
|
102
|
+
// "# Hello, world!\n\nStart writing here…"
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
You can also **programmatically set** the content:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
editor.markdownContent.value = "## New heading\n\nFresh content.";
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
### Keyboard Shortcuts
|
|
114
|
+
|
|
115
|
+
| Key | Action |
|
|
116
|
+
|---|---|
|
|
117
|
+
| <kbd>↑</kbd> / <kbd>↓</kbd> | Move focus between blocks |
|
|
118
|
+
| <kbd>Enter</kbd> | Split current block → insert new paragraph below |
|
|
119
|
+
| <kbd>Backspace</kbd> (empty block) | Delete the block, focus moves up |
|
|
120
|
+
| <kbd>Delete</kbd> (empty block) | Delete the block, focus stays at same index |
|
|
121
|
+
| Click blank area | Append a new empty paragraph at the bottom |
|
|
122
|
+
|
|
123
|
+
#### Auto type‑detection
|
|
124
|
+
|
|
125
|
+
Type `# `, `## `, or `### ` at the start of a paragraph and the block auto‑converts to the matching heading level.
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
### Image Upload
|
|
130
|
+
|
|
131
|
+
Images are first-class blocks with **src**, **alt text**, and **caption** fields. Right‑click any image → **Edit Attributes** to open the editing modal.
|
|
132
|
+
|
|
133
|
+
#### Paste‑to‑upload
|
|
134
|
+
|
|
135
|
+
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:
|
|
136
|
+
|
|
137
|
+
```vue
|
|
138
|
+
<script setup lang="ts">
|
|
139
|
+
async function uploadImage(file: File): Promise<string> {
|
|
140
|
+
const formData = new FormData();
|
|
141
|
+
formData.append("image", file);
|
|
142
|
+
|
|
143
|
+
const res = await fetch("/api/upload", { method: "POST", body: formData });
|
|
144
|
+
const { url } = await res.json();
|
|
145
|
+
return url;
|
|
146
|
+
}
|
|
147
|
+
</script>
|
|
148
|
+
|
|
149
|
+
<template>
|
|
150
|
+
<MarkdownEditor
|
|
151
|
+
:editor="editor"
|
|
152
|
+
:image-upload-function="uploadImage"
|
|
153
|
+
/>
|
|
154
|
+
</template>
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
The serialized Markdown uses a custom block syntax for images:
|
|
158
|
+
|
|
159
|
+
```markdown
|
|
160
|
+
"""MarkdownModuleImage
|
|
161
|
+
src: https://example.com/photo.jpg
|
|
162
|
+
alt: A scenic mountain view
|
|
163
|
+
caption: Photo taken during the 2026 summit
|
|
164
|
+
"""
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
> **Note:** `imageUploadFunction` is optional. Without it, pasted images from the clipboard are ignored.
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
### Editor API Reference
|
|
172
|
+
|
|
173
|
+
#### `useMarkdownEditor(initialContent?: string)`
|
|
174
|
+
|
|
175
|
+
Returns a reactive editor instance:
|
|
176
|
+
|
|
177
|
+
| Member | Type | Description |
|
|
178
|
+
|---|---|---|
|
|
179
|
+
| `markdownContent` | `Ref<string>` | Reactive raw Markdown. Read to serialize, write to load content. |
|
|
180
|
+
| `markdownNodes` | `Ref<MarkdownAstNode[]>` | Reactive array of AST nodes. |
|
|
181
|
+
| `deleteNode(index)` | `(index: number) => void` | Remove the node at `index`. |
|
|
182
|
+
| `addBlankNode(index?)` | `(index?: number) => number` | Insert an empty paragraph at `index` (or end). Returns the new index. |
|
|
183
|
+
| `addNodeWithType(index, type, content?)` | `(index: number, type: MarkdownNodeType, content?: string) => number` | Insert a typed node. Returns the new index. |
|
|
184
|
+
| `replaceNodeType(node, newType)` | `(node: MarkdownAstNode, type: MarkdownNodeType) => { newNode, index } \| null` | Convert between block types (e.g. paragraph → heading). |
|
|
185
|
+
| `moveNode(from, to)` | `(fromIndex: number, toIndex: number) => void` | Programmatically reorder a block. |
|
|
186
|
+
|
|
187
|
+
#### `MarkdownEditor` props
|
|
188
|
+
|
|
189
|
+
| Prop | Type | Required | Description |
|
|
190
|
+
|---|---|---|---|
|
|
191
|
+
| `editor` | `MarkdownEditorInstance` | ✓ | Instance from `useMarkdownEditor()`. |
|
|
192
|
+
| `focusedNode` | `MarkdownAstNode \| null` | — | For `v-model:focused-node` tracking. |
|
|
193
|
+
| `imageUploadFunction` | `(file: File) => Promise<string>` | — | Async callback for paste‑to‑upload. |
|
|
194
|
+
|
|
195
|
+
#### `MarkdownEditor` emits
|
|
196
|
+
|
|
197
|
+
| Event | Payload | Description |
|
|
198
|
+
|---|---|---|
|
|
199
|
+
| `update:focused-node` | `MarkdownAstNode \| null` | Fires when focus moves to a new block. |
|
|
200
|
+
|
|
201
|
+
#### `MarkdownEditor` slots
|
|
202
|
+
|
|
203
|
+
| Slot | Description |
|
|
204
|
+
|---|---|
|
|
205
|
+
| `after-controls` | Injected inside every block, after the drag‑handle / add / delete controls. |
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Renderer
|
|
210
|
+
|
|
211
|
+
`<MarkdownRenderer>` converts a Markdown string to semantic HTML. It uses the same `remark-parse` pipeline as the editor, so custom module blocks (like `"""MarkdownModuleImage"""`) render as rich `<figure>` elements automatically.
|
|
212
|
+
|
|
213
|
+
The renderer is **SSR‑safe** — no browser APIs, no TipTap. It works in `nuxt generate`, `vite-ssg`, and any server‑side rendering context.
|
|
214
|
+
|
|
215
|
+
### Renderer Quick Start
|
|
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
|
+
---
|
|
261
|
+
|
|
262
|
+
### Custom Render Components
|
|
263
|
+
|
|
264
|
+
You can override any default render component with your own Vue component. Use `useMarkdownRenderer()` to create a renderer instance, then call `overrideComponent()` to swap in your custom component for a specific node type.
|
|
265
|
+
|
|
266
|
+
Each override is **strictly typed** — the replacement component must accept a `state` prop matching the correct state class for that node type (e.g. `MarkdownModuleImageState` for images, `MarkdownModuleTextState` for paragraphs and headings).
|
|
267
|
+
|
|
268
|
+
#### Overriding the image render component
|
|
269
|
+
|
|
270
|
+
```vue
|
|
271
|
+
<script setup lang="ts">
|
|
272
|
+
import {
|
|
273
|
+
MarkdownRenderer,
|
|
274
|
+
useMarkdownRenderer,
|
|
275
|
+
MarkdownAstNodeType,
|
|
276
|
+
type MarkdownModuleImageState,
|
|
277
|
+
} from "@grandaniel/vue-markdown-editor";
|
|
278
|
+
import CustomImageRender from "./CustomImageRender.vue";
|
|
279
|
+
|
|
280
|
+
// Create a renderer instance and override the image component
|
|
281
|
+
const renderer = useMarkdownRenderer();
|
|
282
|
+
renderer.overrideComponent(MarkdownAstNodeType.IMAGE, CustomImageRender);
|
|
283
|
+
|
|
284
|
+
const markdown = `"""MarkdownModuleImage
|
|
285
|
+
src: https://example.com/photo.jpg
|
|
286
|
+
alt: A scenic view
|
|
287
|
+
caption: My custom caption
|
|
288
|
+
"""
|
|
289
|
+
`;
|
|
290
|
+
</script>
|
|
291
|
+
|
|
292
|
+
<template>
|
|
293
|
+
<MarkdownRenderer
|
|
294
|
+
:markdown="markdown"
|
|
295
|
+
:renderer="renderer"
|
|
296
|
+
/>
|
|
297
|
+
</template>
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Your custom component receives the state as a typed prop:
|
|
301
|
+
|
|
302
|
+
```vue
|
|
303
|
+
<!-- CustomImageRender.vue -->
|
|
304
|
+
<script setup lang="ts">
|
|
305
|
+
import { type MarkdownModuleImageState } from "@grandaniel/vue-markdown-editor";
|
|
306
|
+
|
|
307
|
+
defineProps<{ state: MarkdownModuleImageState }>();
|
|
308
|
+
</script>
|
|
309
|
+
|
|
310
|
+
<template>
|
|
311
|
+
<div class="my-custom-image-wrapper">
|
|
312
|
+
<img :src="state.src" :alt="state.alt" />
|
|
313
|
+
<span class="my-caption">{{ state.caption }}</span>
|
|
314
|
+
</div>
|
|
315
|
+
</template>
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
#### Available node types and their state classes
|
|
319
|
+
|
|
320
|
+
| Node Type | State Class | Prop Interface |
|
|
321
|
+
|---|---|---|
|
|
322
|
+
| `MarkdownAstNodeType.PARAGRAPH` | `MarkdownModuleTextState` | `{ text: string }` |
|
|
323
|
+
| `MarkdownAstNodeType.HEADLINE1` | `MarkdownModuleTextState` | `{ text: string }` |
|
|
324
|
+
| `MarkdownAstNodeType.HEADLINE2` | `MarkdownModuleTextState` | `{ text: string }` |
|
|
325
|
+
| `MarkdownAstNodeType.HEADLINE3` | `MarkdownModuleTextState` | `{ text: string }` |
|
|
326
|
+
| `MarkdownAstNodeType.LIST` | `MarkdownModuleListState` | `{ items: MarkdownModuleTextState[] }` |
|
|
327
|
+
| `MarkdownAstNodeType.IMAGE` | `MarkdownModuleImageState` | `{ src: string; alt: string; caption: string }` |
|
|
328
|
+
| `MarkdownAstNodeType.FILE` | `MarkdownModuleFileState` | `{ url: string; fileName: string; fileSize: number; mimeType: string; uploadError: string }` |
|
|
329
|
+
|
|
330
|
+
---
|
|
331
|
+
|
|
332
|
+
### Renderer API Reference
|
|
333
|
+
|
|
334
|
+
#### `useMarkdownRenderer()`
|
|
335
|
+
|
|
336
|
+
Returns a reactive renderer instance with a customizable component registry:
|
|
337
|
+
|
|
338
|
+
| Member | Type | Description |
|
|
339
|
+
|---|---|---|
|
|
340
|
+
| `componentRegistry` | `Reactive<{ [K in MarkdownNodeType]: RenderComponent<RenderStateMap[K]> }>` | Reactive map of node types to their current render components. |
|
|
341
|
+
| `overrideComponent(type, component)` | `<K extends MarkdownNodeType>(type: K, component: RenderComponent<RenderStateMap[K]>) => void` | Replace the render component for a given node type. Strictly typed — the component must accept the correct `state` prop. |
|
|
342
|
+
|
|
343
|
+
#### `MarkdownRenderer` props
|
|
344
|
+
|
|
345
|
+
| Prop | Type | Required | Description |
|
|
346
|
+
|---|---|---|---|
|
|
347
|
+
| `markdown` | `string` | ✓ | Raw Markdown string to render as HTML. |
|
|
348
|
+
| `renderer` | `MarkdownRendererInstance` | — | Instance from `useMarkdownRenderer()`. Optional — when omitted, the built-in default render components are used. Provide this to supply custom render components via `overrideComponent()`. |
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
352
|
+
## Exported Types & Utilities
|
|
353
|
+
|
|
354
|
+
| Export | Kind |
|
|
355
|
+
|---|---|
|
|
356
|
+
| `MarkdownEditorInstance` | Type — return type of `useMarkdownEditor()`. |
|
|
357
|
+
| `MarkdownRendererInstance` | Type — return type of `useMarkdownRenderer()`. |
|
|
358
|
+
| `MarkdownAstNode` | Class — AST node with `id`, `type`, `componentState`, `editingState`. |
|
|
359
|
+
| `MarkdownAstNodeType` | Enum — `PARAGRAPH`, `HEADLINE1`, `HEADLINE2`, `HEADLINE3`, `IMAGE`, `LIST`, `FILE`. |
|
|
360
|
+
| `ImageNode` | Type alias — `MarkdownAstNode<MarkdownModuleImageState>`. |
|
|
361
|
+
| `TextNode` | Type alias — `MarkdownAstNode<MarkdownModuleTextState>`. |
|
|
362
|
+
| `FileNode` | Type alias — `MarkdownAstNode<MarkdownModuleFileState>`. |
|
|
363
|
+
| `TextishNodeType` | Type — union of `PARAGRAPH \| HEADLINE1 \| HEADLINE2 \| HEADLINE3 \| LIST`. |
|
|
364
|
+
| `isTextNodeState(node)` | Type guard for text‑based nodes. |
|
|
365
|
+
| `isTextNodeType(type)` | Type guard for text‑based node types. |
|
|
366
|
+
| `RenderComponent<TState>` | Type — a Vue component that accepts `{ state: TState }` as props. |
|
|
367
|
+
| `RenderStateMap` | Interface — maps each `MarkdownNodeType` to its state class for strict typing. |
|
|
368
|
+
| `MarkdownModuleImageState` | Class — state for image nodes (`src`, `alt`, `caption`). |
|
|
369
|
+
| `MarkdownModuleFileState` | Class — state for file nodes (`url`, `fileName`, `fileSize`, `mimeType`, `uploadError`). |
|
|
370
|
+
|
|
371
|
+
---
|
|
372
|
+
|
|
373
|
+
## Custom Styling
|
|
374
|
+
|
|
375
|
+
All components use scoped SCSS. To override styles, use **global CSS** with higher specificity, or Vue's `:deep()` combinator from a parent component.
|
|
376
|
+
|
|
377
|
+
### CSS class reference
|
|
378
|
+
|
|
379
|
+
| Class | Applies to |
|
|
380
|
+
|---|---|
|
|
381
|
+
| `.markdown-editor` | Root editor container |
|
|
382
|
+
| `.markdown-editor-module` | Individual block wrapper — `.is-focused` when active |
|
|
383
|
+
| `.markdown-editor-module-controls` | Left control bar (drag handle + add/delete buttons) |
|
|
384
|
+
| `.markdown-editor-module-content` | Content area inside a block |
|
|
385
|
+
| `.markdown-editor-module-content-focused` | Content area when the block is focused |
|
|
386
|
+
| `.markdown-editor-focus-controls` | Row containing drag‑handle, delete, and add buttons |
|
|
387
|
+
| `.drag-handle` | SortableJS drag handle (⠿) |
|
|
388
|
+
| `.focus-control-btn` | Delete / Add buttons in the control bar |
|
|
389
|
+
| `.markdown-editor-context-menu` | Floating block context menu (`z-index: 1000`) |
|
|
390
|
+
| `.markdown-editor-context-menu-block-item` | Full‑width context menu button |
|
|
391
|
+
| `.markdown-editor-context-menu-inline-item` | Inline toolbar button (`.is-active` when toggled) |
|
|
392
|
+
| `.markdown-editor-modal-overlay` | Modal backdrop (`z-index: 9999`) |
|
|
393
|
+
| `.markdown-editor-modal` | Modal container |
|
|
394
|
+
| `.markdown-editor-modal-header` | Modal title bar |
|
|
395
|
+
| `.markdown-editor-modal-title` | Modal heading text |
|
|
396
|
+
| `.markdown-editor-modal-close` | Close (✕) button |
|
|
397
|
+
| `.markdown-editor-modal-body` | Modal content area |
|
|
398
|
+
| `.markdown-editor-modal-footer` | Modal action bar |
|
|
399
|
+
| `.markdown-editor-modal-button` | Base modal button |
|
|
400
|
+
| `.markdown-editor-modal-button-primary` | Primary (Save) button — blue |
|
|
401
|
+
| `.markdown-editor-modal-button-secondary` | Secondary (Cancel) button — gray |
|
|
402
|
+
| `.markdown-module-image` | Image block wrapper |
|
|
403
|
+
| `.markdown-module-image-form` | Image edit form inside the modal |
|
|
404
|
+
| `.markdown-module-image-form-field` | Form field group (label + input) |
|
|
405
|
+
|
|
406
|
+
### Styling TipTap content
|
|
407
|
+
|
|
408
|
+
Each text‑based block hosts its own TinyMCE‑style TipTap editor. Target `.tiptap` inside a block's content area:
|
|
409
|
+
|
|
410
|
+
```css
|
|
411
|
+
/* Make all TipTap editors use your font */
|
|
412
|
+
.markdown-editor-module-content .tiptap {
|
|
413
|
+
font-family: "Georgia", serif;
|
|
414
|
+
font-size: 1.1rem;
|
|
415
|
+
line-height: 1.8;
|
|
416
|
+
}
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
### Do's
|
|
420
|
+
|
|
421
|
+
- ✅ Import the stylesheet: `import "@grandaniel/vue-markdown-editor/style.css"`
|
|
422
|
+
- ✅ Use **global** (unscoped) CSS or `:deep()` from a parent to override styles
|
|
423
|
+
- ✅ Target `.tiptap` inside `.markdown-editor-module-content` for editor typography
|
|
424
|
+
- ✅ Use `z-index` values above `1000` / `9999` for anything that must layer **above** context menus and modals
|
|
425
|
+
|
|
426
|
+
### Don'ts
|
|
427
|
+
|
|
428
|
+
- ❌ Don't rely on CSS custom properties — the editor uses hard‑coded Tailwind‑scale colors (grays and blues)
|
|
429
|
+
- ❌ Don't override `z-index` on `.markdown-editor-context-menu` or `.markdown-editor-modal-overlay` — it will break layering
|
|
430
|
+
- ❌ Don't use `display: contents` on `.markdown-editor-module` — it interferes with SortableJS drag logic
|
|
431
|
+
- ❌ Don't set `outline: none` on `.markdown-editor-module-content` globally — the focus ring is intentional for keyboard navigation
|
|
432
|
+
|
|
433
|
+
---
|
|
434
|
+
|
|
435
|
+
## Supply Chain Security
|
|
436
|
+
|
|
437
|
+
We take package integrity seriously.
|
|
438
|
+
|
|
439
|
+
| Measure | Status |
|
|
440
|
+
|---|---|
|
|
441
|
+
| **npm package provenance** | ✅ Enabled — every publish includes [provenance attestations](https://docs.npmjs.com/generating-provenance-statements) via GitHub Actions and Sigstore. |
|
|
442
|
+
| **CI/CD** | ✅ GitHub Actions runs `npm ci` → `npm test` → `npm run build` → publish on every push to `dev` and `main`. |
|
|
443
|
+
| **Prerelease tags** | ✅ Non‑main branches publish with a `dev` dist‑tag (e.g. `1.1.3-dev.abc1234`). |
|
|
444
|
+
| **Dependabot** | 🔜 Planned — automated dependency update PRs will be enabled via `.github/dependabot.yml`. |
|
|
445
|
+
|
|
446
|
+
To verify provenance locally:
|
|
447
|
+
|
|
448
|
+
```bash
|
|
449
|
+
npm audit signatures
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
---
|
|
453
|
+
|
|
454
|
+
## Contributing
|
|
455
|
+
|
|
456
|
+
We welcome contributions! Please follow the guidelines below.
|
|
457
|
+
|
|
458
|
+
### PR Policy
|
|
459
|
+
|
|
460
|
+
1. **Fork** the repository and create a feature branch off `dev`.
|
|
461
|
+
2. **Keep changes focused** — one feature or fix per PR.
|
|
462
|
+
3. **Add tests** for any new functionality. The project uses [Vitest](https://vitest.dev/) + [`@vue/test-utils`](https://test-utils.vuejs.org/).
|
|
463
|
+
4. **Run the full check** before pushing:
|
|
464
|
+
|
|
465
|
+
```bash
|
|
466
|
+
npm ci
|
|
467
|
+
npm run test
|
|
468
|
+
npm run type-check
|
|
469
|
+
npm run build
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
5. **Open a PR** against the `dev` branch with a clear description of what changed and why.
|
|
473
|
+
|
|
474
|
+
### Dev setup
|
|
475
|
+
|
|
476
|
+
```bash
|
|
477
|
+
# Clone and install
|
|
478
|
+
git clone https://github.com/danielgran/vue-markdown-editor.git
|
|
479
|
+
cd vue-markdown-editor
|
|
480
|
+
npm ci
|
|
481
|
+
|
|
482
|
+
# Start the dev server
|
|
483
|
+
npm run dev
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
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.
|
|
487
|
+
|
|
488
|
+
---
|
|
489
|
+
|
|
490
|
+
## License
|
|
491
|
+
|
|
492
|
+
[ISC](LICENSE) © 2026 [danielgran](https://github.com/danielgran)
|
|
493
|
+
|
|
494
|
+
All components use scoped SCSS. To override styles, use **global CSS** with higher specificity, or Vue's `:deep()` combinator from a parent component.
|
|
495
|
+
|
|
496
|
+
### CSS class reference
|
|
497
|
+
|
|
498
|
+
| Class | Applies to |
|
|
499
|
+
|---|---|
|
|
500
|
+
| `.markdown-editor` | Root editor container |
|
|
501
|
+
| `.markdown-editor-module` | Individual block wrapper — `.is-focused` when active |
|
|
502
|
+
| `.markdown-editor-module-controls` | Left control bar (drag handle + add/delete buttons) |
|
|
503
|
+
| `.markdown-editor-module-content` | Content area inside a block |
|
|
504
|
+
| `.markdown-editor-module-content-focused` | Content area when the block is focused |
|
|
505
|
+
| `.markdown-editor-focus-controls` | Row containing drag‑handle, delete, and add buttons |
|
|
506
|
+
| `.drag-handle` | SortableJS drag handle (⠿) |
|
|
507
|
+
| `.focus-control-btn` | Delete / Add buttons in the control bar |
|
|
508
|
+
| `.markdown-editor-context-menu` | Floating block context menu (`z-index: 1000`) |
|
|
509
|
+
| `.markdown-editor-context-menu-block-item` | Full‑width context menu button |
|
|
510
|
+
| `.markdown-editor-context-menu-inline-item` | Inline toolbar button (`.is-active` when toggled) |
|
|
511
|
+
| `.markdown-editor-modal-overlay` | Modal backdrop (`z-index: 9999`) |
|
|
512
|
+
| `.markdown-editor-modal` | Modal container |
|
|
513
|
+
| `.markdown-editor-modal-header` | Modal title bar |
|
|
514
|
+
| `.markdown-editor-modal-title` | Modal heading text |
|
|
515
|
+
| `.markdown-editor-modal-close` | Close (✕) button |
|
|
516
|
+
| `.markdown-editor-modal-body` | Modal content area |
|
|
517
|
+
| `.markdown-editor-modal-footer` | Modal action bar |
|
|
518
|
+
| `.markdown-editor-modal-button` | Base modal button |
|
|
519
|
+
| `.markdown-editor-modal-button-primary` | Primary (Save) button — blue |
|
|
520
|
+
| `.markdown-editor-modal-button-secondary` | Secondary (Cancel) button — gray |
|
|
521
|
+
| `.markdown-module-image` | Image block wrapper |
|
|
522
|
+
| `.markdown-module-image-form` | Image edit form inside the modal |
|
|
523
|
+
| `.markdown-module-image-form-field` | Form field group (label + input) |
|
|
524
|
+
|
|
525
|
+
### Styling TipTap content
|
|
526
|
+
|
|
527
|
+
Each text‑based block hosts its own TinyMCE‑style TipTap editor. Target `.tiptap` inside a block's content area:
|
|
528
|
+
|
|
529
|
+
```css
|
|
530
|
+
/* Make all TipTap editors use your font */
|
|
531
|
+
.markdown-editor-module-content .tiptap {
|
|
532
|
+
font-family: "Georgia", serif;
|
|
533
|
+
font-size: 1.1rem;
|
|
534
|
+
line-height: 1.8;
|
|
535
|
+
}
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
### Do's
|
|
539
|
+
|
|
540
|
+
- ✅ Import the stylesheet: `import "@grandaniel/vue-markdown-editor/style.css"`
|
|
541
|
+
- ✅ Use **global** (unscoped) CSS or `:deep()` from a parent to override styles
|
|
542
|
+
- ✅ Target `.tiptap` inside `.markdown-editor-module-content` for editor typography
|
|
543
|
+
- ✅ Use `z-index` values above `1000` / `9999` for anything that must layer **above** context menus and modals
|
|
544
|
+
|
|
545
|
+
### Don'ts
|
|
546
|
+
|
|
547
|
+
- ❌ Don't rely on CSS custom properties — the editor uses hard‑coded Tailwind‑scale colors (grays and blues)
|
|
548
|
+
- ❌ Don't override `z-index` on `.markdown-editor-context-menu` or `.markdown-editor-modal-overlay` — it will break layering
|
|
549
|
+
- ❌ Don't use `display: contents` on `.markdown-editor-module` — it interferes with SortableJS drag logic
|
|
550
|
+
- ❌ Don't set `outline: none` on `.markdown-editor-module-content` globally — the focus ring is intentional for keyboard navigation
|
|
551
|
+
|
|
552
|
+
---
|
|
553
|
+
|
|
554
|
+
## Supply Chain Security
|
|
555
|
+
|
|
556
|
+
We take package integrity seriously.
|
|
557
|
+
|
|
558
|
+
| Measure | Status |
|
|
559
|
+
|---|---|
|
|
560
|
+
| **npm package provenance** | ✅ Enabled — every publish includes [provenance attestations](https://docs.npmjs.com/generating-provenance-statements) via GitHub Actions and Sigstore. |
|
|
561
|
+
| **CI/CD** | ✅ GitHub Actions runs `npm ci` → `npm test` → `npm run build` → publish on every push to `dev` and `main`. |
|
|
562
|
+
| **Prerelease tags** | ✅ Non‑main branches publish with a `dev` dist‑tag (e.g. `1.1.3-dev.abc1234`). |
|
|
563
|
+
| **Dependabot** | 🔜 Planned — automated dependency update PRs will be enabled via `.github/dependabot.yml`. |
|
|
564
|
+
|
|
565
|
+
To verify provenance locally:
|
|
566
|
+
|
|
567
|
+
```bash
|
|
568
|
+
npm audit signatures
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
---
|
|
572
|
+
|
|
573
|
+
## Contributing
|
|
574
|
+
|
|
575
|
+
We welcome contributions! Please follow the guidelines below.
|
|
576
|
+
|
|
577
|
+
### PR Policy
|
|
578
|
+
|
|
579
|
+
1. **Fork** the repository and create a feature branch off `dev`.
|
|
580
|
+
2. **Keep changes focused** — one feature or fix per PR.
|
|
581
|
+
3. **Add tests** for any new functionality. The project uses [Vitest](https://vitest.dev/) + [`@vue/test-utils`](https://test-utils.vuejs.org/).
|
|
582
|
+
4. **Run the full check** before pushing:
|
|
583
|
+
|
|
584
|
+
```bash
|
|
585
|
+
npm ci
|
|
586
|
+
npm run test
|
|
587
|
+
npm run type-check
|
|
588
|
+
npm run build
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
5. **Open a PR** against the `dev` branch with a clear description of what changed and why.
|
|
592
|
+
|
|
593
|
+
### Dev setup
|
|
594
|
+
|
|
595
|
+
```bash
|
|
596
|
+
# Clone and install
|
|
597
|
+
git clone https://github.com/danielgran/vue-markdown-editor.git
|
|
598
|
+
cd vue-markdown-editor
|
|
599
|
+
npm ci
|
|
600
|
+
|
|
601
|
+
# Start the dev server
|
|
602
|
+
npm run dev
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
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.
|
|
606
|
+
|
|
607
|
+
---
|
|
608
|
+
|
|
609
|
+
## License
|
|
610
|
+
|
|
611
|
+
[ISC](LICENSE) © 2026 [danielgran](https://github.com/danielgran)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type ShallowRef } from "vue";
|
|
2
|
+
import type { Editor } from "@tiptap/vue-3";
|
|
3
|
+
/**
|
|
4
|
+
* Singleton reactive reference to the currently focused TipTap editor.
|
|
5
|
+
* Set by each TipTap module on focus, read by the text selection context menu.
|
|
6
|
+
*/
|
|
7
|
+
export declare const activeEditor: ShallowRef<Editor | null>;
|
|
@@ -17,6 +17,7 @@ export declare function useMarkdownEditor(initialContent?: string): {
|
|
|
17
17
|
}[]>;
|
|
18
18
|
deleteNode: (nodeIndex: number) => void;
|
|
19
19
|
addBlankNode: (nodeIndex: number) => number;
|
|
20
|
+
addNodeWithType: (nodeIndex: number, type: import("..").MarkdownAstNodeType, content?: string) => number;
|
|
20
21
|
replaceNodeType: (node: import("..").MarkdownAstNode, newType: import("..").MarkdownAstNodeType) => {
|
|
21
22
|
newNode: import("..").MarkdownAstNode;
|
|
22
23
|
index: number;
|
|
@@ -19,6 +19,7 @@ declare function useMarkdownProcessor(modelValue: ModelRef<string | undefined>):
|
|
|
19
19
|
}[]>;
|
|
20
20
|
deleteNode: (nodeIndex: number) => void;
|
|
21
21
|
addBlankNode: (nodeIndex: number) => number;
|
|
22
|
+
addNodeWithType: (nodeIndex: number, type: MarkdownNodeType, content?: string) => number;
|
|
22
23
|
replaceNodeType: (node: MarkdownAstNode, newType: MarkdownNodeType) => {
|
|
23
24
|
newNode: MarkdownAstNode;
|
|
24
25
|
index: number;
|
|
@@ -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;
|