@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 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