neuphlo-editor 1.1.0 → 1.1.1
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 +453 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,20 +1,458 @@
|
|
|
1
|
-
Neuphlo Editor
|
|
2
|
-
===============
|
|
1
|
+
# Neuphlo Editor
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
A lightweight, feature-rich React wrapper around [Tiptap](https://tiptap.dev) with sensible defaults, built-in styling, and powerful image upload capabilities.
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/neuphlo-editor)
|
|
6
|
+
[](https://github.com/Neuphlo/neuphlo-editor/blob/main/LICENSE)
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
import 'neuphlo-editor/styles.css';
|
|
8
|
+
## Features
|
|
11
9
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
-
|
|
15
|
-
|
|
16
|
-
-
|
|
10
|
+
- 🎨 **Beautiful defaults** - Pre-styled editor with clean, modern design
|
|
11
|
+
- 🖼️ **Image upload** - Integrated image support with custom upload handlers
|
|
12
|
+
- 📐 **Image controls** - Resize and align images with intuitive controls
|
|
13
|
+
- ⌨️ **Slash commands** - Quick formatting with `/` commands
|
|
14
|
+
- 💬 **Bubble menus** - Context-aware formatting menus
|
|
15
|
+
- 🎯 **TypeScript** - Full TypeScript support with type definitions
|
|
16
|
+
- 🎨 **Syntax highlighting** - Code blocks with highlight.js support
|
|
17
|
+
- 🔗 **Link management** - Easy link insertion and editing
|
|
18
|
+
- 📦 **Lightweight** - Minimal bundle size with tree-shaking support
|
|
17
19
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install neuphlo-editor
|
|
24
|
+
# or
|
|
25
|
+
pnpm add neuphlo-editor
|
|
26
|
+
# or
|
|
27
|
+
yarn add neuphlo-editor
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Peer Dependencies
|
|
31
|
+
|
|
32
|
+
Make sure you have the required peer dependencies installed:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm install react react-dom @tiptap/react @tiptap/pm
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Quick Start
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
import { Editor } from 'neuphlo-editor'
|
|
42
|
+
import 'neuphlo-editor/styles.css'
|
|
43
|
+
|
|
44
|
+
function MyApp() {
|
|
45
|
+
return <Editor content="<p>Start editing...</p>" />
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Image Upload
|
|
50
|
+
|
|
51
|
+
Neuphlo Editor makes it easy to add image upload functionality with your own backend:
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
import { Editor } from 'neuphlo-editor'
|
|
55
|
+
import 'neuphlo-editor/styles.css'
|
|
56
|
+
|
|
57
|
+
function MyApp() {
|
|
58
|
+
const handleImageUpload = async (file: File): Promise<string> => {
|
|
59
|
+
// Upload to your backend (S3, Cloudinary, custom API, etc.)
|
|
60
|
+
const formData = new FormData()
|
|
61
|
+
formData.append('file', file)
|
|
62
|
+
|
|
63
|
+
const response = await fetch('/api/upload', {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
body: formData,
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
const { url } = await response.json()
|
|
69
|
+
return url // Return the permanent image URL
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return <Editor uploadImage={handleImageUpload} />
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Image Upload Methods
|
|
77
|
+
|
|
78
|
+
Users can insert images in multiple ways:
|
|
79
|
+
|
|
80
|
+
1. **Slash command** - Type `/image` and press Enter
|
|
81
|
+
2. **Image menu** - Click the upload button in the image bubble menu
|
|
82
|
+
3. **Drag & drop** - Drag image files directly into the editor
|
|
83
|
+
4. **Paste** - Copy and paste images from clipboard
|
|
84
|
+
|
|
85
|
+
### Image Controls
|
|
86
|
+
|
|
87
|
+
When an image is selected, a bubble menu appears with controls to:
|
|
88
|
+
|
|
89
|
+
- **Resize** - Adjust size from 25% to 100% with a slider
|
|
90
|
+
- **Align** - Position left, center, or right
|
|
91
|
+
- **Replace** - Upload a new image
|
|
92
|
+
- **Delete** - Remove the image
|
|
93
|
+
|
|
94
|
+
## Upload Handler Examples
|
|
95
|
+
|
|
96
|
+
### AWS S3
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
const handleImageUpload = async (file: File): Promise<string> => {
|
|
100
|
+
// Get presigned URL from your API
|
|
101
|
+
const response = await fetch('/api/presigned-url', {
|
|
102
|
+
method: 'POST',
|
|
103
|
+
headers: { 'Content-Type': 'application/json' },
|
|
104
|
+
body: JSON.stringify({
|
|
105
|
+
fileName: file.name,
|
|
106
|
+
fileType: file.type,
|
|
107
|
+
}),
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
const { uploadUrl, imageUrl } = await response.json()
|
|
111
|
+
|
|
112
|
+
// Upload to S3
|
|
113
|
+
await fetch(uploadUrl, {
|
|
114
|
+
method: 'PUT',
|
|
115
|
+
body: file,
|
|
116
|
+
headers: { 'Content-Type': file.type },
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
return imageUrl
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Cloudinary
|
|
124
|
+
|
|
125
|
+
```tsx
|
|
126
|
+
const handleImageUpload = async (file: File): Promise<string> => {
|
|
127
|
+
const formData = new FormData()
|
|
128
|
+
formData.append('file', file)
|
|
129
|
+
formData.append('upload_preset', 'your_upload_preset')
|
|
130
|
+
|
|
131
|
+
const response = await fetch(
|
|
132
|
+
`https://api.cloudinary.com/v1_1/your_cloud_name/image/upload`,
|
|
133
|
+
{
|
|
134
|
+
method: 'POST',
|
|
135
|
+
body: formData,
|
|
136
|
+
}
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
const data = await response.json()
|
|
140
|
+
return data.secure_url
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Custom Backend
|
|
145
|
+
|
|
146
|
+
```tsx
|
|
147
|
+
const handleImageUpload = async (file: File): Promise<string> => {
|
|
148
|
+
const formData = new FormData()
|
|
149
|
+
formData.append('file', file)
|
|
150
|
+
|
|
151
|
+
const response = await fetch('/api/upload', {
|
|
152
|
+
method: 'POST',
|
|
153
|
+
body: formData,
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
if (!response.ok) {
|
|
157
|
+
throw new Error('Upload failed')
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const { url } = await response.json()
|
|
161
|
+
return url
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## API Reference
|
|
166
|
+
|
|
167
|
+
### Editor Props
|
|
168
|
+
|
|
169
|
+
| Prop | Type | Default | Description |
|
|
170
|
+
|------|------|---------|-------------|
|
|
171
|
+
| `content` | `string` | `""` | Initial HTML content |
|
|
172
|
+
| `className` | `string` | `undefined` | CSS class for the editor wrapper |
|
|
173
|
+
| `editable` | `boolean` | `true` | Whether the editor is editable |
|
|
174
|
+
| `immediatelyRender` | `boolean` | `false` | Render immediately on mount |
|
|
175
|
+
| `showTextMenu` | `boolean` | `true` | Show text formatting bubble menu |
|
|
176
|
+
| `showImageMenu` | `boolean` | `true` | Show image controls bubble menu |
|
|
177
|
+
| `showSlashMenu` | `boolean` | `true` | Show slash command menu |
|
|
178
|
+
| `extensions` | `Extension[]` | `[]` | Additional Tiptap extensions |
|
|
179
|
+
| `uploadImage` | `(file: File) => Promise<string>` | `undefined` | Image upload handler |
|
|
180
|
+
| `onUpdate` | `({ editor }) => void` | `undefined` | Called when content changes |
|
|
181
|
+
| `onCreate` | `({ editor }) => void` | `undefined` | Called when editor is created |
|
|
182
|
+
|
|
183
|
+
### Example with Props
|
|
184
|
+
|
|
185
|
+
```tsx
|
|
186
|
+
<Editor
|
|
187
|
+
content="<p>Hello world!</p>"
|
|
188
|
+
className="my-editor"
|
|
189
|
+
editable={true}
|
|
190
|
+
showTextMenu={true}
|
|
191
|
+
showImageMenu={true}
|
|
192
|
+
showSlashMenu={true}
|
|
193
|
+
uploadImage={handleImageUpload}
|
|
194
|
+
onUpdate={({ editor }) => {
|
|
195
|
+
const html = editor.getHTML()
|
|
196
|
+
console.log('Content updated:', html)
|
|
197
|
+
}}
|
|
198
|
+
onCreate={({ editor }) => {
|
|
199
|
+
console.log('Editor created:', editor)
|
|
200
|
+
}}
|
|
201
|
+
/>
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Styling
|
|
205
|
+
|
|
206
|
+
### Basic Styling
|
|
207
|
+
|
|
208
|
+
Import the default styles in your app:
|
|
209
|
+
|
|
210
|
+
```tsx
|
|
211
|
+
import 'neuphlo-editor/styles.css'
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Syntax Highlighting (Optional)
|
|
215
|
+
|
|
216
|
+
For code block syntax highlighting, import the highlight.js theme:
|
|
217
|
+
|
|
218
|
+
```tsx
|
|
219
|
+
import 'neuphlo-editor/styles.css'
|
|
220
|
+
import 'neuphlo-editor/highlight.css'
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Or use any other highlight.js theme:
|
|
224
|
+
|
|
225
|
+
```tsx
|
|
226
|
+
import 'neuphlo-editor/styles.css'
|
|
227
|
+
import 'highlight.js/styles/github-dark.css'
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Custom Styling
|
|
231
|
+
|
|
232
|
+
All styles are namespaced with `.nph-` prefix to avoid conflicts. You can override them:
|
|
233
|
+
|
|
234
|
+
```css
|
|
235
|
+
.nph-editor {
|
|
236
|
+
min-height: 400px;
|
|
237
|
+
border: 1px solid #e5e7eb;
|
|
238
|
+
border-radius: 0.5rem;
|
|
239
|
+
padding: 1rem;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.nph-editor img {
|
|
243
|
+
max-width: 100%;
|
|
244
|
+
border-radius: 0.5rem;
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## Available Features
|
|
249
|
+
|
|
250
|
+
### Text Formatting
|
|
251
|
+
|
|
252
|
+
- **Bold** (`Ctrl+B` / `Cmd+B`)
|
|
253
|
+
- **Italic** (`Ctrl+I` / `Cmd+I`)
|
|
254
|
+
- **Strike** (`Ctrl+Shift+X` / `Cmd+Shift+X`)
|
|
255
|
+
- **Code** (inline code)
|
|
256
|
+
|
|
257
|
+
### Blocks
|
|
258
|
+
|
|
259
|
+
- Headings (H1-H6)
|
|
260
|
+
- Bullet lists
|
|
261
|
+
- Ordered lists
|
|
262
|
+
- Blockquotes
|
|
263
|
+
- Code blocks with syntax highlighting
|
|
264
|
+
|
|
265
|
+
### Slash Commands
|
|
266
|
+
|
|
267
|
+
Type `/` to open the command menu:
|
|
268
|
+
|
|
269
|
+
- `/bold` - Bold text
|
|
270
|
+
- `/italic` - Italic text
|
|
271
|
+
- `/strike` - Strikethrough
|
|
272
|
+
- `/h1` - Heading 1
|
|
273
|
+
- `/h2` - Heading 2
|
|
274
|
+
- `/h3` - Heading 3
|
|
275
|
+
- `/h4` - Heading 4
|
|
276
|
+
- `/list` - Bullet list
|
|
277
|
+
- `/ol` - Ordered list
|
|
278
|
+
- `/quote` - Blockquote
|
|
279
|
+
- `/code` - Inline code
|
|
280
|
+
- `/codeblock` - Code block
|
|
281
|
+
- `/image` - Insert image
|
|
282
|
+
|
|
283
|
+
### Links
|
|
284
|
+
|
|
285
|
+
- Insert/edit links via the bubble menu
|
|
286
|
+
- Open links in new tab
|
|
287
|
+
- Remove links
|
|
288
|
+
|
|
289
|
+
### Images
|
|
290
|
+
|
|
291
|
+
- Upload via slash command (`/image`)
|
|
292
|
+
- Drag and drop images
|
|
293
|
+
- Paste images from clipboard
|
|
294
|
+
- Resize images (25%-100%)
|
|
295
|
+
- Align images (left, center, right)
|
|
296
|
+
- Replace images
|
|
297
|
+
- Delete images
|
|
298
|
+
|
|
299
|
+
## Advanced Usage
|
|
300
|
+
|
|
301
|
+
### Custom Extensions
|
|
302
|
+
|
|
303
|
+
Add your own Tiptap extensions:
|
|
304
|
+
|
|
305
|
+
```tsx
|
|
306
|
+
import { Editor } from 'neuphlo-editor'
|
|
307
|
+
import { Underline } from '@tiptap/extension-underline'
|
|
308
|
+
|
|
309
|
+
<Editor
|
|
310
|
+
extensions={[Underline]}
|
|
311
|
+
content="<p>Now you can <u>underline</u> text!</p>"
|
|
312
|
+
/>
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### Accessing the Editor Instance
|
|
316
|
+
|
|
317
|
+
```tsx
|
|
318
|
+
import { useRef } from 'react'
|
|
319
|
+
import { Editor } from 'neuphlo-editor'
|
|
320
|
+
|
|
321
|
+
function MyApp() {
|
|
322
|
+
const editorRef = useRef(null)
|
|
323
|
+
|
|
324
|
+
const handleCreate = ({ editor }) => {
|
|
325
|
+
editorRef.current = editor
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const getContent = () => {
|
|
329
|
+
if (editorRef.current) {
|
|
330
|
+
const html = editorRef.current.getHTML()
|
|
331
|
+
const json = editorRef.current.getJSON()
|
|
332
|
+
console.log('HTML:', html)
|
|
333
|
+
console.log('JSON:', json)
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return (
|
|
338
|
+
<>
|
|
339
|
+
<Editor onCreate={handleCreate} />
|
|
340
|
+
<button onClick={getContent}>Get Content</button>
|
|
341
|
+
</>
|
|
342
|
+
)
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### Controlled Content
|
|
347
|
+
|
|
348
|
+
```tsx
|
|
349
|
+
import { useState } from 'react'
|
|
350
|
+
import { Editor } from 'neuphlo-editor'
|
|
351
|
+
|
|
352
|
+
function MyApp() {
|
|
353
|
+
const [content, setContent] = useState('<p>Initial content</p>')
|
|
354
|
+
|
|
355
|
+
return (
|
|
356
|
+
<Editor
|
|
357
|
+
content={content}
|
|
358
|
+
onUpdate={({ editor }) => {
|
|
359
|
+
setContent(editor.getHTML())
|
|
360
|
+
}}
|
|
361
|
+
/>
|
|
362
|
+
)
|
|
363
|
+
}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
## Error Handling
|
|
367
|
+
|
|
368
|
+
Handle upload errors gracefully:
|
|
369
|
+
|
|
370
|
+
```tsx
|
|
371
|
+
const handleImageUpload = async (file: File): Promise<string> => {
|
|
372
|
+
try {
|
|
373
|
+
// Validate file size (e.g., max 5MB)
|
|
374
|
+
if (file.size > 5 * 1024 * 1024) {
|
|
375
|
+
throw new Error('File size must be less than 5MB')
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Validate file type
|
|
379
|
+
if (!file.type.startsWith('image/')) {
|
|
380
|
+
throw new Error('File must be an image')
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Upload logic
|
|
384
|
+
const formData = new FormData()
|
|
385
|
+
formData.append('file', file)
|
|
386
|
+
|
|
387
|
+
const response = await fetch('/api/upload', {
|
|
388
|
+
method: 'POST',
|
|
389
|
+
body: formData,
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
if (!response.ok) {
|
|
393
|
+
throw new Error('Upload failed')
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const { url } = await response.json()
|
|
397
|
+
return url
|
|
398
|
+
} catch (error) {
|
|
399
|
+
console.error('Image upload error:', error)
|
|
400
|
+
// You might want to show a toast notification here
|
|
401
|
+
throw error
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
## TypeScript
|
|
407
|
+
|
|
408
|
+
The package is fully typed with TypeScript:
|
|
409
|
+
|
|
410
|
+
```tsx
|
|
411
|
+
import type { Editor as TiptapEditor } from '@tiptap/core'
|
|
412
|
+
import { Editor } from 'neuphlo-editor'
|
|
413
|
+
|
|
414
|
+
type UploadImageFn = (file: File) => Promise<string>
|
|
415
|
+
|
|
416
|
+
const handleImageUpload: UploadImageFn = async (file) => {
|
|
417
|
+
// Your upload logic
|
|
418
|
+
return 'https://example.com/image.jpg'
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function MyApp() {
|
|
422
|
+
const handleCreate = ({ editor }: { editor: TiptapEditor }) => {
|
|
423
|
+
console.log('Editor created:', editor)
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return (
|
|
427
|
+
<Editor
|
|
428
|
+
uploadImage={handleImageUpload}
|
|
429
|
+
onCreate={handleCreate}
|
|
430
|
+
/>
|
|
431
|
+
)
|
|
432
|
+
}
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
## Browser Support
|
|
436
|
+
|
|
437
|
+
Neuphlo Editor works in all modern browsers that support:
|
|
438
|
+
- FileReader API
|
|
439
|
+
- Drag and Drop API
|
|
440
|
+
- Clipboard API
|
|
441
|
+
|
|
442
|
+
Supported browsers:
|
|
443
|
+
- Chrome 60+
|
|
444
|
+
- Firefox 55+
|
|
445
|
+
- Safari 11+
|
|
446
|
+
- Edge 79+
|
|
447
|
+
|
|
448
|
+
## License
|
|
449
|
+
|
|
450
|
+
MIT
|
|
451
|
+
|
|
452
|
+
## Contributing
|
|
453
|
+
|
|
454
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
455
|
+
|
|
456
|
+
## Support
|
|
457
|
+
|
|
458
|
+
If you encounter any issues or have questions, please file an issue on the [GitHub repository](https://github.com/Neuphlo/neuphlo-editor/issues).
|