@noseberry/nbd-editor 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +538 -0
- package/dist/nbd-editor.cjs.js +2 -0
- package/dist/nbd-editor.cjs.js.map +1 -0
- package/dist/nbd-editor.css +1 -0
- package/dist/nbd-editor.esm.js +2 -0
- package/dist/nbd-editor.esm.js.map +1 -0
- package/dist/nbd-editor.umd.js +2 -0
- package/dist/nbd-editor.umd.js.map +1 -0
- package/package.json +60 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Noseberry Private Limited
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
# @noseberry/nbd-editor
|
|
2
|
+
|
|
3
|
+
A framework-agnostic, Gutenberg-style block editor by **Noseberry Private Limited**. Works with vanilla JavaScript, React, and Next.js out of the box.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
- [Installation](#installation)
|
|
10
|
+
- [Quick Start (Vanilla JS)](#quick-start-vanilla-js)
|
|
11
|
+
- [React Integration](#react-integration)
|
|
12
|
+
- [Without Ref](#without-ref)
|
|
13
|
+
- [With Ref](#with-ref)
|
|
14
|
+
- [Next.js Integration](#nextjs-integration)
|
|
15
|
+
- [App Router (Next 13+)](#app-router-next-13)
|
|
16
|
+
- [Pages Router](#pages-router)
|
|
17
|
+
- [Configuration Options](#configuration-options)
|
|
18
|
+
- [Public API](#public-api)
|
|
19
|
+
- [Data Contract](#data-contract)
|
|
20
|
+
- [Block Types](#block-types)
|
|
21
|
+
- [Media Handling](#media-handling)
|
|
22
|
+
- [Keyboard Shortcuts](#keyboard-shortcuts)
|
|
23
|
+
- [Events](#events)
|
|
24
|
+
- [Build from Source](#build-from-source)
|
|
25
|
+
- [License](#license)
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
This is a **private** repository. You need GitHub access to install it.
|
|
32
|
+
|
|
33
|
+
**SSH (recommended):**
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm install git+ssh://git@github.com/Noseberry-Private-Limited/NBD-editor.git
|
|
37
|
+
# or
|
|
38
|
+
yarn add git+ssh://git@github.com/Noseberry-Private-Limited/NBD-editor.git
|
|
39
|
+
# or
|
|
40
|
+
pnpm add git+ssh://git@github.com/Noseberry-Private-Limited/NBD-editor.git
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
> Requires an SSH key linked to your GitHub account.
|
|
44
|
+
|
|
45
|
+
**HTTPS with personal access token:**
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm install git+https://<YOUR_GITHUB_TOKEN>@github.com/Noseberry-Private-Limited/NBD-editor.git
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Pin a specific branch or tag:**
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npm install git+ssh://git@github.com/Noseberry-Private-Limited/NBD-editor.git#main
|
|
55
|
+
npm install git+ssh://git@github.com/Noseberry-Private-Limited/NBD-editor.git#v1.0.0
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Important — always import the CSS:**
|
|
59
|
+
|
|
60
|
+
```js
|
|
61
|
+
import '@noseberry/nbd-editor/style.css';
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Without this import, no editor styles will be applied. This is required for all setups (vanilla JS, React, Next.js).
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Quick Start (Vanilla JS)
|
|
69
|
+
|
|
70
|
+
```js
|
|
71
|
+
import { NBDEditor } from '@noseberry/nbd-editor';
|
|
72
|
+
import '@noseberry/nbd-editor/style.css';
|
|
73
|
+
|
|
74
|
+
const editor = new NBDEditor('#editor', {
|
|
75
|
+
siteName: 'My Blog',
|
|
76
|
+
authorName: 'Harshit',
|
|
77
|
+
content: '<p>Start writing here...</p>',
|
|
78
|
+
maxFileSize: 200 * 1024 * 1024,
|
|
79
|
+
onChooseMediaSource: (type) => 'upload',
|
|
80
|
+
onChange: (data) => console.log('Content changed:', data),
|
|
81
|
+
onSave: (data) => console.log('Saved:', data),
|
|
82
|
+
onPublish: (data) => console.log('Published:', data),
|
|
83
|
+
uploadHandler: async (file) => {
|
|
84
|
+
const form = new FormData();
|
|
85
|
+
form.append('file', file);
|
|
86
|
+
const res = await fetch('/api/upload', { method: 'POST', body: form });
|
|
87
|
+
return res.json(); // must return { url, id? }
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
You can pass either a CSS selector string (`'#editor'`) or a DOM element directly as the first argument.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## React Integration
|
|
97
|
+
|
|
98
|
+
### Without Ref
|
|
99
|
+
|
|
100
|
+
The simplest approach — just render the editor and listen for changes via callbacks. No imperative access needed.
|
|
101
|
+
|
|
102
|
+
```jsx
|
|
103
|
+
import { ReactNBDEditor } from '@noseberry/nbd-editor';
|
|
104
|
+
import '@noseberry/nbd-editor/style.css';
|
|
105
|
+
|
|
106
|
+
export default function EditorPage() {
|
|
107
|
+
const handleChange = (data) => {
|
|
108
|
+
console.log('Editor content:', data.content);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const handleSave = (data) => {
|
|
112
|
+
fetch('/api/posts', {
|
|
113
|
+
method: 'POST',
|
|
114
|
+
headers: { 'Content-Type': 'application/json' },
|
|
115
|
+
body: JSON.stringify(data),
|
|
116
|
+
});
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<ReactNBDEditor
|
|
121
|
+
content="<p>Hello World</p>"
|
|
122
|
+
siteName="My App"
|
|
123
|
+
authorName="Harshit"
|
|
124
|
+
maxFileSize={200 * 1024 * 1024}
|
|
125
|
+
onChooseMediaSource={(type) => 'upload'}
|
|
126
|
+
onChange={handleChange}
|
|
127
|
+
onSave={handleSave}
|
|
128
|
+
uploadHandler={async (file) => {
|
|
129
|
+
const form = new FormData();
|
|
130
|
+
form.append('file', file);
|
|
131
|
+
const res = await fetch('/api/upload', { method: 'POST', body: form });
|
|
132
|
+
return res.json();
|
|
133
|
+
}}
|
|
134
|
+
/>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### With Ref
|
|
140
|
+
|
|
141
|
+
Use a ref when you need imperative control — save programmatically, get data on demand, undo/redo, etc.
|
|
142
|
+
|
|
143
|
+
```jsx
|
|
144
|
+
import React, { useRef, useCallback } from 'react';
|
|
145
|
+
import { ReactNBDEditor } from '@noseberry/nbd-editor';
|
|
146
|
+
import '@noseberry/nbd-editor/style.css';
|
|
147
|
+
|
|
148
|
+
export default function EditorPage() {
|
|
149
|
+
const editorRef = useRef(null);
|
|
150
|
+
|
|
151
|
+
const handleSaveClick = useCallback(() => {
|
|
152
|
+
editorRef.current?.save();
|
|
153
|
+
}, []);
|
|
154
|
+
|
|
155
|
+
const handleGetData = useCallback(() => {
|
|
156
|
+
const data = editorRef.current?.getData();
|
|
157
|
+
console.log('Editor data:', data);
|
|
158
|
+
// data => { content: '<p>...</p>' }
|
|
159
|
+
}, []);
|
|
160
|
+
|
|
161
|
+
const handleGetHTML = useCallback(() => {
|
|
162
|
+
const html = editorRef.current?.toHTML();
|
|
163
|
+
console.log('Raw HTML:', html);
|
|
164
|
+
}, []);
|
|
165
|
+
|
|
166
|
+
const handleUndo = useCallback(() => {
|
|
167
|
+
editorRef.current?.undo();
|
|
168
|
+
}, []);
|
|
169
|
+
|
|
170
|
+
const handleRedo = useCallback(() => {
|
|
171
|
+
editorRef.current?.redo();
|
|
172
|
+
}, []);
|
|
173
|
+
|
|
174
|
+
const handleSetContent = useCallback(() => {
|
|
175
|
+
editorRef.current?.setData({ content: '<h2>Replaced content</h2><p>New paragraph</p>' });
|
|
176
|
+
}, []);
|
|
177
|
+
|
|
178
|
+
return (
|
|
179
|
+
<div>
|
|
180
|
+
<div style={{ marginBottom: 16, display: 'flex', gap: 8 }}>
|
|
181
|
+
<button onClick={handleSaveClick}>Save</button>
|
|
182
|
+
<button onClick={handleGetData}>Get Data</button>
|
|
183
|
+
<button onClick={handleGetHTML}>Get HTML</button>
|
|
184
|
+
<button onClick={handleUndo}>Undo</button>
|
|
185
|
+
<button onClick={handleRedo}>Redo</button>
|
|
186
|
+
<button onClick={handleSetContent}>Replace Content</button>
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
<ReactNBDEditor
|
|
190
|
+
ref={editorRef}
|
|
191
|
+
content="<p>Edit me...</p>"
|
|
192
|
+
siteName="My App"
|
|
193
|
+
authorName="Harshit"
|
|
194
|
+
maxFileSize={200 * 1024 * 1024}
|
|
195
|
+
onChooseMediaSource={(type) => 'upload'}
|
|
196
|
+
onSave={(data) => console.log('Saved:', data)}
|
|
197
|
+
/>
|
|
198
|
+
</div>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
**Ref methods available:**
|
|
204
|
+
|
|
205
|
+
| Method | Description |
|
|
206
|
+
|---|---|
|
|
207
|
+
| `getInstance()` | Returns the underlying `NBDEditor` instance |
|
|
208
|
+
| `getData()` | Returns `{ content: '<p>...</p>' }` |
|
|
209
|
+
| `setData(data)` | Sets content — accepts `{ content: '...' }` |
|
|
210
|
+
| `toHTML()` | Returns the raw HTML string |
|
|
211
|
+
| `save()` | Triggers save (fires `onSave` callback) |
|
|
212
|
+
| `publish()` | Triggers publish (fires `onPublish` callback) |
|
|
213
|
+
| `undo(opts?)` | Undo last action |
|
|
214
|
+
| `redo(opts?)` | Redo last undone action |
|
|
215
|
+
| `destroy()` | Destroys the editor instance and cleans up |
|
|
216
|
+
|
|
217
|
+
### Important: Avoid Re-controlling Content
|
|
218
|
+
|
|
219
|
+
The `content` prop is treated as an **initial/external value**. Do NOT bind it to state that updates on every keystroke:
|
|
220
|
+
|
|
221
|
+
```jsx
|
|
222
|
+
// BAD — causes infinite re-render loop
|
|
223
|
+
const [content, setContent] = useState('<p>Hello</p>');
|
|
224
|
+
<ReactNBDEditor content={content} onChange={(d) => setContent(d.content)} />
|
|
225
|
+
|
|
226
|
+
// GOOD — use content only for initial value or external resets
|
|
227
|
+
const [initialContent] = useState('<p>Hello</p>');
|
|
228
|
+
<ReactNBDEditor content={initialContent} onChange={(d) => console.log(d)} />
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
If you need to update the editor's content programmatically from the parent, use the ref:
|
|
232
|
+
|
|
233
|
+
```jsx
|
|
234
|
+
editorRef.current?.setData({ content: '<p>New content from server</p>' });
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## Next.js Integration
|
|
240
|
+
|
|
241
|
+
The editor uses browser APIs (`document`, `window`, `contenteditable`), so it must be rendered **client-side only**.
|
|
242
|
+
|
|
243
|
+
### App Router (Next 13+)
|
|
244
|
+
|
|
245
|
+
Create a client component wrapper:
|
|
246
|
+
|
|
247
|
+
```jsx
|
|
248
|
+
// components/Editor.jsx
|
|
249
|
+
'use client';
|
|
250
|
+
|
|
251
|
+
import React, { useRef } from 'react';
|
|
252
|
+
import { ReactNBDEditor } from '@noseberry/nbd-editor';
|
|
253
|
+
import '@noseberry/nbd-editor/style.css';
|
|
254
|
+
|
|
255
|
+
export default function Editor({ initialContent }) {
|
|
256
|
+
const editorRef = useRef(null);
|
|
257
|
+
|
|
258
|
+
return (
|
|
259
|
+
<ReactNBDEditor
|
|
260
|
+
ref={editorRef}
|
|
261
|
+
content={initialContent}
|
|
262
|
+
siteName="My Next.js App"
|
|
263
|
+
authorName="Harshit"
|
|
264
|
+
maxFileSize={200 * 1024 * 1024}
|
|
265
|
+
onChooseMediaSource={(type) => 'upload'}
|
|
266
|
+
onSave={(data) => {
|
|
267
|
+
console.log('Saved:', data);
|
|
268
|
+
}}
|
|
269
|
+
uploadHandler={async (file) => {
|
|
270
|
+
const form = new FormData();
|
|
271
|
+
form.append('file', file);
|
|
272
|
+
const res = await fetch('/api/upload', { method: 'POST', body: form });
|
|
273
|
+
return res.json();
|
|
274
|
+
}}
|
|
275
|
+
/>
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
Use it in a page:
|
|
281
|
+
|
|
282
|
+
```jsx
|
|
283
|
+
// app/editor/page.jsx
|
|
284
|
+
import Editor from '@/components/Editor';
|
|
285
|
+
|
|
286
|
+
export default function EditorPage() {
|
|
287
|
+
return (
|
|
288
|
+
<main>
|
|
289
|
+
<h1>Post Editor</h1>
|
|
290
|
+
<Editor initialContent="<p>Write your post...</p>" />
|
|
291
|
+
</main>
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
> **Note:** `onSave`, `onChange`, and `uploadHandler` are functions — they cannot be passed from a Server Component. Define them inside the `'use client'` wrapper component (as shown above), or make the page itself a Client Component.
|
|
297
|
+
|
|
298
|
+
**Alternative — dynamic import (skip SSR entirely):**
|
|
299
|
+
|
|
300
|
+
```jsx
|
|
301
|
+
// app/editor/page.jsx
|
|
302
|
+
'use client';
|
|
303
|
+
|
|
304
|
+
import dynamic from 'next/dynamic';
|
|
305
|
+
|
|
306
|
+
const Editor = dynamic(() => import('@/components/Editor'), {
|
|
307
|
+
ssr: false,
|
|
308
|
+
loading: () => <p>Loading editor...</p>,
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
export default function EditorPage() {
|
|
312
|
+
return (
|
|
313
|
+
<main>
|
|
314
|
+
<h1>Post Editor</h1>
|
|
315
|
+
<Editor initialContent="<p>Write your post...</p>" />
|
|
316
|
+
</main>
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### Pages Router
|
|
322
|
+
|
|
323
|
+
```jsx
|
|
324
|
+
// pages/editor.jsx
|
|
325
|
+
import dynamic from 'next/dynamic';
|
|
326
|
+
|
|
327
|
+
const Editor = dynamic(() => import('../components/Editor'), {
|
|
328
|
+
ssr: false,
|
|
329
|
+
loading: () => <p>Loading editor...</p>,
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
export default function EditorPage() {
|
|
333
|
+
return (
|
|
334
|
+
<main>
|
|
335
|
+
<h1>Post Editor</h1>
|
|
336
|
+
<Editor initialContent="<p>Write your post...</p>" />
|
|
337
|
+
</main>
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
> **Tip:** Using `dynamic` with `ssr: false` ensures the editor is never rendered on the server, preventing hydration mismatches.
|
|
343
|
+
|
|
344
|
+
---
|
|
345
|
+
|
|
346
|
+
## Configuration Options
|
|
347
|
+
|
|
348
|
+
Pass these as the second argument to `new NBDEditor(selector, options)` or as props to `<ReactNBDEditor />`.
|
|
349
|
+
|
|
350
|
+
| Option | Type | Default | Description |
|
|
351
|
+
|---|---|---|---|
|
|
352
|
+
| `siteName` | `string` | `'My Site'` | Displayed in the editor header |
|
|
353
|
+
| `authorName` | `string` | `'Admin'` | Displayed as the author name |
|
|
354
|
+
| `content` | `string` | `null` | Initial HTML content to load |
|
|
355
|
+
| `placeholder` | `string` | `'Start writing...'` | Placeholder text for empty blocks |
|
|
356
|
+
| `showStatusBar` | `boolean` | `true` | Show/hide the bottom status bar |
|
|
357
|
+
| `showSettingsPanel` | `boolean` | `false` | Show/hide the right settings panel |
|
|
358
|
+
| `showHeaderActions` | `boolean` | `false` | Show/hide header action buttons |
|
|
359
|
+
| `autoSaveInterval` | `number` | `30000` | Auto-save interval in ms (0 to disable) |
|
|
360
|
+
| `maxFileSize` | `number` | `10485760` | Max upload file size in bytes (default 10 MB) |
|
|
361
|
+
| `uploadHandler` | `async (file) => { url, id? }` | `null` | Custom file upload function |
|
|
362
|
+
| `onChooseMediaSource` | `(type) => 'url' \| 'upload'` | `null` | Controls video/audio source chooser |
|
|
363
|
+
| `onChange` | `(data) => void` | `null` | Called on every content change |
|
|
364
|
+
| `onSave` | `(data) => void` | `null` | Called when save is triggered |
|
|
365
|
+
| `onPublish` | `(data) => void` | `null` | Called when publish is triggered |
|
|
366
|
+
|
|
367
|
+
### React-only props
|
|
368
|
+
|
|
369
|
+
| Prop | Type | Description |
|
|
370
|
+
|---|---|---|
|
|
371
|
+
| `className` | `string` | CSS class for the wrapper `div` |
|
|
372
|
+
| `style` | `object` | Inline styles for the wrapper `div` |
|
|
373
|
+
| `onReady` | `(editor) => void` | Called once the editor instance is initialized |
|
|
374
|
+
|
|
375
|
+
---
|
|
376
|
+
|
|
377
|
+
## Public API
|
|
378
|
+
|
|
379
|
+
These methods are available on the `NBDEditor` instance (vanilla JS) or via the ref (React).
|
|
380
|
+
|
|
381
|
+
```js
|
|
382
|
+
// Vanilla JS
|
|
383
|
+
const editor = new NBDEditor('#editor', { ... });
|
|
384
|
+
|
|
385
|
+
// React — via ref
|
|
386
|
+
const instance = editorRef.current?.getInstance();
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
| Method | Returns | Description |
|
|
390
|
+
|---|---|---|
|
|
391
|
+
| `getData()` | `{ content: string }` | Get the full editor content |
|
|
392
|
+
| `setData(data)` | `void` | Set content. Accepts `{ content: '...' }` or a plain string |
|
|
393
|
+
| `toHTML()` | `string` | Export all blocks as a merged HTML string |
|
|
394
|
+
| `save()` | `void` | Trigger save — fires `onSave` callback and `'save'` event |
|
|
395
|
+
| `publish()` | `void` | Trigger publish — fires `onPublish` callback and `'publish'` event |
|
|
396
|
+
| `undo(opts?)` | `boolean` | Undo the last action |
|
|
397
|
+
| `redo(opts?)` | `boolean` | Redo the last undone action |
|
|
398
|
+
| `destroy()` | `void` | Tear down the editor and clean up listeners |
|
|
399
|
+
| `getTitle()` | `string` | Get the current title value |
|
|
400
|
+
| `setTitle(title)` | `void` | Set the title |
|
|
401
|
+
| `selectBlock(id)` | `void` | Select a block by its ID |
|
|
402
|
+
| `focusBlock(id)` | `void` | Select and focus a block |
|
|
403
|
+
| `insertBlockAtSelection(type, data?)` | `block` | Insert a new block after the selected block |
|
|
404
|
+
| `insertBlockAtEnd(type, data?)` | `block` | Append a new block at the end |
|
|
405
|
+
| `toggleBlockHtmlMode(blockId?)` | `void` | Toggle a block between visual and raw HTML mode |
|
|
406
|
+
| `openBlockHtmlEditor(blockId?)` | `void` | Open the HTML editor modal for a block |
|
|
407
|
+
|
|
408
|
+
### Event Emitter
|
|
409
|
+
|
|
410
|
+
The editor extends `EventEmitter`. Listen for events:
|
|
411
|
+
|
|
412
|
+
```js
|
|
413
|
+
editor.on('change', (data) => { /* content changed */ });
|
|
414
|
+
editor.on('save', (data) => { /* save triggered */ });
|
|
415
|
+
editor.on('publish', (data) => { /* publish triggered */ });
|
|
416
|
+
editor.on('autosave', (data) => { /* auto-save tick */ });
|
|
417
|
+
editor.on('block:select', (blockId) => { /* block selected */ });
|
|
418
|
+
editor.on('input', (block) => { /* block input */ });
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
---
|
|
422
|
+
|
|
423
|
+
## Data Contract
|
|
424
|
+
|
|
425
|
+
The editor uses a simple content-only data format:
|
|
426
|
+
|
|
427
|
+
```js
|
|
428
|
+
// Getting data
|
|
429
|
+
const data = editor.getData();
|
|
430
|
+
// => { content: '<h2>Title</h2><p>Body text...</p>' }
|
|
431
|
+
|
|
432
|
+
// Setting data
|
|
433
|
+
editor.setData({ content: '<p>New content</p>' });
|
|
434
|
+
|
|
435
|
+
// Getting raw HTML
|
|
436
|
+
const html = editor.toHTML();
|
|
437
|
+
// => '<h2>Title</h2><p>Body text...</p>'
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
Store the `content` string in your database. Pass it back to the editor to restore.
|
|
441
|
+
|
|
442
|
+
---
|
|
443
|
+
|
|
444
|
+
## Block Types
|
|
445
|
+
|
|
446
|
+
The editor supports these block types out of the box:
|
|
447
|
+
|
|
448
|
+
**Text blocks:** Paragraph, Heading 2, Heading 3, Heading 4, List (bulleted), Ordered List, Quote, Pullquote, Code, Custom HTML
|
|
449
|
+
|
|
450
|
+
**Media blocks:** Image, Gallery, Video, Audio, File, Embed
|
|
451
|
+
|
|
452
|
+
**Design blocks:** Separator, Spacer, Columns, Table, Button
|
|
453
|
+
|
|
454
|
+
Users can insert blocks via the inserter panel (`+` button) or the slash command menu (type `/` in an empty block).
|
|
455
|
+
|
|
456
|
+
---
|
|
457
|
+
|
|
458
|
+
## Media Handling
|
|
459
|
+
|
|
460
|
+
For video and audio, users see a modal chooser with two options: **Upload file** or **Use URL**.
|
|
461
|
+
|
|
462
|
+
Control the default behavior with `onChooseMediaSource`:
|
|
463
|
+
|
|
464
|
+
```js
|
|
465
|
+
new NBDEditor('#editor', {
|
|
466
|
+
onChooseMediaSource: (type) => {
|
|
467
|
+
// type is 'video' or 'audio'
|
|
468
|
+
return 'upload'; // or 'url'
|
|
469
|
+
},
|
|
470
|
+
maxFileSize: 500 * 1024 * 1024, // 500 MB
|
|
471
|
+
uploadHandler: async (file) => {
|
|
472
|
+
const form = new FormData();
|
|
473
|
+
form.append('file', file);
|
|
474
|
+
const res = await fetch('/api/upload', { method: 'POST', body: form });
|
|
475
|
+
return res.json(); // { url: 'https://...', id: '...' }
|
|
476
|
+
},
|
|
477
|
+
});
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
Files exceeding `maxFileSize` are blocked from uploading. Users can also drag and drop files directly onto the editor canvas.
|
|
481
|
+
|
|
482
|
+
---
|
|
483
|
+
|
|
484
|
+
## Keyboard Shortcuts
|
|
485
|
+
|
|
486
|
+
| Shortcut | Action |
|
|
487
|
+
|---|---|
|
|
488
|
+
| `Ctrl/Cmd + S` | Save |
|
|
489
|
+
| `Ctrl/Cmd + Z` | Undo |
|
|
490
|
+
| `Ctrl/Cmd + Shift + Z` / `Ctrl/Cmd + Y` | Redo |
|
|
491
|
+
| `Ctrl/Cmd + B` | Bold |
|
|
492
|
+
| `Ctrl/Cmd + I` | Italic |
|
|
493
|
+
| `Ctrl/Cmd + U` | Underline |
|
|
494
|
+
| `Ctrl/Cmd + K` | Insert link |
|
|
495
|
+
| `Ctrl/Cmd + A` | Select all (within current block) |
|
|
496
|
+
| `Arrow Up` (at block start) | Move to previous block |
|
|
497
|
+
| `Arrow Down` (at block end) | Move to next block |
|
|
498
|
+
| `Tab` (in table) | Move to next cell / create new row |
|
|
499
|
+
| `Shift + Tab` (in table) | Move to previous cell |
|
|
500
|
+
| `Tab` (in code block) | Insert 2 spaces |
|
|
501
|
+
| `/` (in empty block) | Open slash command menu |
|
|
502
|
+
|
|
503
|
+
---
|
|
504
|
+
|
|
505
|
+
## Events
|
|
506
|
+
|
|
507
|
+
```js
|
|
508
|
+
editor.on('change', (data) => { }); // Any content change
|
|
509
|
+
editor.on('save', (data) => { }); // Save triggered (Ctrl+S or API)
|
|
510
|
+
editor.on('publish', (data) => { }); // Publish triggered
|
|
511
|
+
editor.on('autosave', (data) => { }); // Auto-save interval tick
|
|
512
|
+
editor.on('block:select', (id) => { }); // Block selected
|
|
513
|
+
editor.on('input', (block) => { }); // Block content input
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
---
|
|
517
|
+
|
|
518
|
+
## Build from Source
|
|
519
|
+
|
|
520
|
+
```bash
|
|
521
|
+
git clone git@github.com:Noseberry-Private-Limited/NBD-editor.git
|
|
522
|
+
cd NBD-editor
|
|
523
|
+
npm install
|
|
524
|
+
npm run build
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
Output files in `dist/`:
|
|
528
|
+
|
|
529
|
+
- `nbd-editor.esm.js` — ES module
|
|
530
|
+
- `nbd-editor.cjs.js` — CommonJS
|
|
531
|
+
- `nbd-editor.umd.js` — UMD (for `<script>` tags)
|
|
532
|
+
- `nbd-editor.css` — Styles
|
|
533
|
+
|
|
534
|
+
---
|
|
535
|
+
|
|
536
|
+
## License
|
|
537
|
+
|
|
538
|
+
MIT — Noseberry Private Limited
|