tiptap-notion-editor 0.6.0 → 0.6.2
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 +100 -74
- package/lib/components/node/image-upload/types.d.ts +1 -1
- package/lib/index.cjs +3 -3
- package/lib/index.cjs.map +1 -1
- package/lib/index.js +1032 -1029
- package/lib/index.js.map +1 -1
- package/lib/styles/editor.css +18 -2
- package/lib/styles/image.css +5 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
# tiptap-notion-editor
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A Notion-style rich text editor built on Tiptap 3.
|
|
4
4
|
|
|
5
|
-
Slash
|
|
6
|
-
|
|
5
|
+
Slash commands, bubble menus, block drag-and-drop, tables with handles, image
|
|
6
|
+
upload, math (KaTeX), emoji, `@mention` and a table of contents in the margin.
|
|
7
7
|
|
|
8
|
-
##
|
|
8
|
+
## Install
|
|
9
9
|
|
|
10
10
|
```bash
|
|
11
11
|
pnpm add tiptap-notion-editor
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
Peer
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
Peer dependencies must already exist in the host app — that is deliberate, not an
|
|
15
|
+
oversight: bundling `@tiptap/*` would create a second ProseMirror instance, and
|
|
16
|
+
schemas from two different instances do not recognise each other.
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
19
|
pnpm add @tiptap/core @tiptap/pm @tiptap/react react react-dom
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
##
|
|
22
|
+
## Usage
|
|
23
23
|
|
|
24
24
|
```tsx
|
|
25
25
|
import { NotionEditor } from "tiptap-notion-editor";
|
|
@@ -46,79 +46,93 @@ export function PageEditor({ page }) {
|
|
|
46
46
|
|
|
47
47
|
## Tailwind
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
app,
|
|
49
|
+
The components use Tailwind utility classes, and those classes are compiled by
|
|
50
|
+
the **host app** — they are not baked into `style.css`. Add this line to the
|
|
51
|
+
app's CSS entry file, right after `@import "tailwindcss"`:
|
|
52
52
|
|
|
53
53
|
```css
|
|
54
54
|
@source "../node_modules/tiptap-notion-editor/lib/*.js";
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
Without it the editor still runs, but with almost no styling.
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
`--color-primary`…).
|
|
61
|
-
|
|
59
|
+
The editor also reads the app's theme variables (`--color-background`,
|
|
60
|
+
`--color-border`, `--color-primary`…). Any variable you leave undefined falls
|
|
61
|
+
back to the default in `lib/styles/tokens.css`.
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
### Content width
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
The content column stretches to fill whatever space it is given, reserving 96px
|
|
66
|
+
on each side for the drag handles and the table of contents. To pin it to a
|
|
67
|
+
fixed reading width instead, override the grid track in your own CSS:
|
|
68
|
+
|
|
69
|
+
```css
|
|
70
|
+
.notion-like-editor-layout {
|
|
71
|
+
--content-width: minmax(auto, 708px);
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Language
|
|
76
|
+
|
|
77
|
+
The default is **Vietnamese**. Change it with the `locale` prop:
|
|
66
78
|
|
|
67
79
|
```tsx
|
|
68
80
|
<NotionEditor locale="en" />
|
|
69
81
|
```
|
|
70
82
|
|
|
71
|
-
|
|
72
|
-
|
|
83
|
+
Both the `vi` and `en` dictionaries are exported, so the host app can override
|
|
84
|
+
individual strings without forking the package:
|
|
73
85
|
|
|
74
86
|
```tsx
|
|
75
87
|
import { NotionEditor, getMessages, type Messages } from "tiptap-notion-editor";
|
|
76
88
|
|
|
77
|
-
const t: Messages = getMessages("
|
|
89
|
+
const t: Messages = getMessages("en");
|
|
78
90
|
const custom: Messages = {
|
|
79
91
|
...t,
|
|
80
|
-
slash: { ...t.slash, groups: { ...t.slash.groups, insert: "
|
|
92
|
+
slash: { ...t.slash, groups: { ...t.slash.groups, insert: "Add" } },
|
|
81
93
|
};
|
|
82
94
|
```
|
|
83
95
|
|
|
84
|
-
|
|
85
|
-
|
|
96
|
+
The `Messages` type is derived from the Vietnamese dictionary, so a missing key
|
|
97
|
+
in another translation is a compile error rather than an empty string at runtime.
|
|
86
98
|
|
|
87
99
|
## Props
|
|
88
100
|
|
|
89
|
-
| Prop |
|
|
101
|
+
| Prop | Type | Default | Meaning |
|
|
90
102
|
| --- | --- | --- | --- |
|
|
91
|
-
| `content` | `JSONContent \| string` | — |
|
|
92
|
-
| `onChange` | `(json, editor) => void` | — |
|
|
93
|
-
| `onReady` | `(editor) => void` | — |
|
|
94
|
-
| `editable` | `boolean` | `true` | `false`
|
|
95
|
-
| `uploadImage` | `(file, onProgress?, signal?) => Promise<string>` | — |
|
|
96
|
-
| `maxImageSize` | `number` | `1048576` |
|
|
97
|
-
| `uploadAttachment` | `(file, onProgress?, signal?) => Promise<string \| { src?, fileId? }>` | — |
|
|
98
|
-
| `maxAttachmentSize` | `number` | `26214400` |
|
|
99
|
-
| `onOpenAttachment` | `(attachment) => void` | — |
|
|
100
|
-
| `mentionSource` | `(query) => MentionItem[] \| Promise<MentionItem[]>` | — |
|
|
101
|
-
| `locale` | `"vi" \| "en"` | `"vi"` |
|
|
102
|
-
| `placeholder` | `string \| (({ editor, node }) => string)` | — |
|
|
103
|
-
| `showToc` | `boolean` | `true` |
|
|
104
|
-
| `tocVariant` | `"content" \| "line"` | `"line"` |
|
|
105
|
-
| `onReplaceImage` | `() => void` | — |
|
|
106
|
-
| `extensions` | `AnyExtension[]` | `[]` |
|
|
107
|
-
| `className` / `editorClassName` | `string` | — |
|
|
108
|
-
| `ref` | `Ref<NotionEditorHandle>` | — |
|
|
109
|
-
|
|
110
|
-
###
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
`onOpenAttachment`
|
|
103
|
+
| `content` | `JSONContent \| string` | — | Initial content. **Uncontrolled** — see the note below. |
|
|
104
|
+
| `onChange` | `(json, editor) => void` | — | Called every time the document changes. |
|
|
105
|
+
| `onReady` | `(editor) => void` | — | Called once, when the editor has finished initialising. |
|
|
106
|
+
| `editable` | `boolean` | `true` | `false` for read-only; can be changed at runtime. |
|
|
107
|
+
| `uploadImage` | `(file, onProgress?, signal?) => Promise<string>` | — | Uploads an image to your server, returns its URL. |
|
|
108
|
+
| `maxImageSize` | `number` | `1048576` | Image size limit, in bytes. |
|
|
109
|
+
| `uploadAttachment` | `(file, onProgress?, signal?) => Promise<string \| { src?, fileId? }>` | — | Uploads an attachment. Return `src` if your storage hands out stable URLs, or `fileId` if it is private and only issues short-lived ones. |
|
|
110
|
+
| `maxAttachmentSize` | `number` | `26214400` | Attachment size limit, in bytes. |
|
|
111
|
+
| `onOpenAttachment` | `(attachment) => void` | — | Opens an attachment that only has a `fileId`. Attachments with a `src` use a real `<a>` tag and never reach this. |
|
|
112
|
+
| `mentionSource` | `(query) => MentionItem[] \| Promise<MentionItem[]>` | — | Leave it out to disable `@mention` entirely. |
|
|
113
|
+
| `locale` | `"vi" \| "en"` | `"vi"` | Language of the editor's own labels. |
|
|
114
|
+
| `placeholder` | `string \| (({ editor, node }) => string)` | — | Placeholder for an empty document. Empty headings always show `Heading N`; other empty nodes show nothing. Pass a function to decide entirely on your own. |
|
|
115
|
+
| `showToc` | `boolean` | `true` | Table of contents in the right margin. |
|
|
116
|
+
| `tocVariant` | `"content" \| "line"` | `"line"` | How the table of contents is rendered. |
|
|
117
|
+
| `onReplaceImage` | `() => void` | — | Handles the "Replace" button in the image bubble menu. |
|
|
118
|
+
| `extensions` | `AnyExtension[]` | `[]` | Extra Tiptap extensions. |
|
|
119
|
+
| `className` / `editorClassName` | `string` | — | Classes for the outer frame / the editable area. |
|
|
120
|
+
| `ref` | `Ref<NotionEditorHandle>` | — | Access to `editor`, `getJSON`, `getHTML`, `setContent`, `focus`. |
|
|
121
|
+
|
|
122
|
+
### Attachments
|
|
123
|
+
|
|
124
|
+
The package knows nothing about your storage. It calls `uploadAttachment`, keeps
|
|
125
|
+
whatever that function returns, and when the user clicks to download it either
|
|
126
|
+
uses `src` directly or calls `onOpenAttachment` so the host app can fetch the
|
|
127
|
+
content itself.
|
|
115
128
|
|
|
116
129
|
```tsx
|
|
117
130
|
<NotionEditor
|
|
118
131
|
uploadAttachment={async (file) => {
|
|
119
132
|
const { fileId } = await uploadToPrivateStorage(file);
|
|
120
|
-
//
|
|
121
|
-
//
|
|
133
|
+
// Private storage only issues short-lived URLs; embedding one in the
|
|
134
|
+
// content guarantees it breaks a few minutes later — so store the id and
|
|
135
|
+
// exchange it for a URL when it is actually needed.
|
|
122
136
|
return { fileId };
|
|
123
137
|
}}
|
|
124
138
|
onOpenAttachment={async ({ fileId, name }) => {
|
|
@@ -128,23 +142,25 @@ trả về, và khi người dùng bấm tải thì hoặc dùng `src` trực ti
|
|
|
128
142
|
/>
|
|
129
143
|
```
|
|
130
144
|
|
|
131
|
-
`collectFileUrls(doc)`
|
|
132
|
-
|
|
145
|
+
`collectFileUrls(doc)` returns every file URL a document references, base64
|
|
146
|
+
images excluded — useful when the host app needs to clean up storage after
|
|
147
|
+
deleting a page.
|
|
133
148
|
|
|
134
|
-
### `content`
|
|
149
|
+
### `content` is not a controlled prop
|
|
135
150
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
node,
|
|
139
|
-
|
|
151
|
+
Changing `content` does **not** reload the document. To switch to a different
|
|
152
|
+
document, remount with `key` as shown above — that is cheaper than diffing the
|
|
153
|
+
node tree, and it avoids the cursor jumping back to the top every time a parent
|
|
154
|
+
re-renders mid-typing. For a deliberate overwrite, use
|
|
155
|
+
`ref.current.setContent(...)`.
|
|
140
156
|
|
|
141
|
-
###
|
|
157
|
+
### Image upload
|
|
142
158
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
159
|
+
Without `uploadImage`, the upload block reports an error when the user picks a
|
|
160
|
+
file, and dragged-in images fall back to inline base64. Base64 bloats a document
|
|
161
|
+
very quickly, so do not let that fallback reach the content you persist.
|
|
146
162
|
|
|
147
|
-
##
|
|
163
|
+
## Development
|
|
148
164
|
|
|
149
165
|
```bash
|
|
150
166
|
pnpm install
|
|
@@ -152,21 +168,31 @@ pnpm build # -> lib/
|
|
|
152
168
|
pnpm type-check
|
|
153
169
|
```
|
|
154
170
|
|
|
155
|
-
###
|
|
171
|
+
### Trying it in a host app before publishing
|
|
156
172
|
|
|
157
|
-
|
|
173
|
+
Use a tarball, **not** `link:` or `pnpm link`:
|
|
158
174
|
|
|
159
175
|
```bash
|
|
160
|
-
pnpm build && pnpm pack
|
|
161
|
-
pnpm add file:../notion-editor/
|
|
176
|
+
pnpm build && pnpm pack # in this repo
|
|
177
|
+
pnpm add file:../notion-editor/tiptap-notion-editor-0.6.0.tgz # in the host app
|
|
162
178
|
```
|
|
163
179
|
|
|
164
|
-
`link:`
|
|
165
|
-
`
|
|
166
|
-
|
|
167
|
-
`RangeError: Adding different instances of a keyed plugin`
|
|
168
|
-
view. `resolve.dedupe`
|
|
169
|
-
|
|
180
|
+
`link:` points outside the app's `node_modules` tree, so the package still loads
|
|
181
|
+
`react` and `prosemirror-*` from its own. Two ProseMirror copies keep two
|
|
182
|
+
separate `PluginKey` counters, both produce the key `plugin$`, and ProseMirror
|
|
183
|
+
throws `RangeError: Adding different instances of a keyed plugin` the moment the
|
|
184
|
+
editor view is created. Vite's `resolve.dedupe` cannot save you here, because
|
|
185
|
+
Vitest treats a package outside the root directory as external and lets Node
|
|
186
|
+
resolve it.
|
|
170
187
|
|
|
171
|
-
|
|
172
|
-
resolve
|
|
188
|
+
Installing from a tarball puts the package in the app's own store, and peer
|
|
189
|
+
dependencies resolve to a single copy — exactly like installing from npm.
|
|
190
|
+
|
|
191
|
+
### Publishing
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
pnpm type-check
|
|
195
|
+
npm version minor # creates the version commit and tag
|
|
196
|
+
npm publish # prepublishOnly runs the build
|
|
197
|
+
git push --follow-tags
|
|
198
|
+
```
|
|
@@ -48,7 +48,7 @@ export interface UploadOptions {
|
|
|
48
48
|
* @param {AbortSignal} signal - Signal that can be used to abort the upload
|
|
49
49
|
* @returns {Promise<string>} Promise resolving to the URL of the uploaded file
|
|
50
50
|
*/
|
|
51
|
-
upload
|
|
51
|
+
upload?: (file: File, onProgress: (event: {
|
|
52
52
|
progress: number;
|
|
53
53
|
}) => void, signal: AbortSignal) => Promise<string>;
|
|
54
54
|
/**
|