@meenainwal/rich-text-editor 1.1.0 โ†’ 1.2.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/ReadME.md CHANGED
@@ -5,13 +5,29 @@
5
5
 
6
6
  A premium, ultra-lightweight, and framework-agnostic **WYSIWYG rich text editor** built entirely with Vanilla TypeScript. Featuring a sophisticated **Slate & Indigo** design system, it provides a flawless writing experience for React, Next.js, and modern web applications.
7
7
 
8
- ![Editor Preview](./images/editor-preview.png)
8
+ ### ๐Ÿ’ก What is WYSIWYG?
9
+ **WYSIWYG** stands for **"What You See Is What You Get"**.
10
+ Unlike markdown or code editors, what you see while typing in InkFlowโ€”the bold text, centered headings, and interactive tablesโ€”is exactly how it will appear when published. It bridges the gap between editing and the final result, making rich-text creation accessible and predictable.
11
+
12
+ ## ๐Ÿš€ Recent Performance & Security Breakthrough (v1.1.2)
13
+ We recently completed an aggressive optimization and security hardening pass:
14
+ - **79% Size Reduction:** Packed weight dropped from **132kB to 28kB**.
15
+ - **9.8/10 Security Score:** Internal audit confirmed world-class XSS protection.
16
+ - **Pure ESM Architecture:** Zero legacy CommonJS bloat for modern bundlers.
17
+
18
+ ---
19
+
20
+ ## ๐ŸŽฎ Live React Preview
21
+ Wanna see it in action? Try the **Interactive React Demo** on StackBlitz:
22
+ [**Run Demo on StackBlitz**](https://stackblitz.com/edit/vitejs-vite-e8u5yntq?embed=1&view=preview)
9
23
 
10
24
  ## โœจ Premium Features & Why Choose This Editor?
11
25
 
12
26
  ### ๐Ÿ‘ Key Pros & Capabilities
13
- - **Zero Dependencies**: Pure Vanilla JS/TypeScript. No bloated third-party libraries.
14
- - **Microscopic Footprint**: Only **~25kB** gzipped, making it one of the most lightweight editors available.
27
+ - **Microscopic Footprint**: Only **~28kB** packed weight. Total initial load is incredibly light.
28
+ - **Secure By Design**: Rated **9.8/10** in security audits with forced XSS sanitization.
29
+ - **Pure ESM Build**: Optimized for modern bundlers (Vite, Webpack 5, etc.) with zero CJS bloat.
30
+ - **Performance Optimized**: Heavy components like the Emoji Picker are **dynamic-imported** only when clicked.
15
31
  - **Framework Agnostic**: Native support for **React**, **Next.js**, **Vue**, **Angular**, and **Svelte**.
16
32
  - **Auto-Formatting Magic**: Intelligently parses pasted HTML strings into clean, formatted rich text.
17
33
  - **Professional UI/UX**: Modern aesthetics curated with a polished Slate & Indigo color palette.
@@ -19,13 +35,48 @@ A premium, ultra-lightweight, and framework-agnostic **WYSIWYG rich text editor*
19
35
  - **Emoji Picker**: Integrated searchable emoji library for expressive content.
20
36
  - **Dark Mode**: Sophisticated dark theme for premium developer experiences.
21
37
  - **Customizable Toolbar**: Granular control over tool visibility and layout.
38
+ - **Smart Image Management**: Built-in client-side compression (WebP), loading states, custom upload adapters, live resizing, and native captions.
39
+
40
+ ---
41
+
42
+ ## ๐ŸŒ Documentation Website (Coming Soon!)
43
+ We are currently building a dedicated official website to provide the best possible developer experience.
44
+
45
+ **What to expect:**
46
+ - **Interactive Playground**: Test all features live in your browser.
47
+ - **Deep-Dive Guides**: Detailed integration steps for React, Next.js, Vue, and more.
48
+ - **Full API Reference**: Comprehensive documentation for every method and option.
49
+ - **Custom Theme Builder**: Visually design your editor's look and feel.
50
+
51
+ ๐Ÿš€ **Stay tuned for the official launch!**
52
+
53
+ ---
54
+
55
+ ## ๐Ÿ›ก Security & XSS Protection
56
+ InkFlow takes security seriously. It features a hard-coded strict whitelist in `DOMPurify` to ensure:
57
+ - **Malicious Scripts:** Automatically stripped from pastes and API inputs.
58
+ - **URI Blocking:** Blocks `javascript:`, `data:`, and `vbscript:` schemes.
59
+ - **Link Hardening:** Every link is forced to have `rel="noopener noreferrer"`.
60
+ - **Normalization:** Every structural cleanup is followed by a final sanitization pass.
61
+
62
+ ---
63
+
64
+ > [!TIP]
65
+ > The editor is optimized for performance. Features like the **Emoji Picker** are only loaded when needed, keeping your initial page load lightning fast.
22
66
 
23
67
  ### ๐Ÿ‘Ž Cons (Current Limitations)
24
- - Base64 image storage can increase the raw output string size for very large images (Backend S3 uploading adapter coming soon).
25
68
  - Markdown shortcut typing (e.g., typing `#` for H1) is not natively supported yet.
26
69
 
27
70
  ---
28
71
 
72
+ ## ๐Ÿ“š Technical Guides
73
+ For deep-dive documentation, check out our local guides:
74
+ - [**Usage Guide**](./USAGE_GUIDE.md): Configuration, API methods, and feature customization.
75
+ - [**Technical Integration Guide**](./INTEGRATION_GUIDE.md): Step-by-step setup and advanced patterns.
76
+ - [**Security Report**](./SECURITY_REPORT.md): Full breakdown of our XSS protection and hardening.
77
+
78
+ ---
79
+
29
80
  ## ๐Ÿ“ฆ Installation
30
81
 
31
82
  ```bash
@@ -37,11 +88,11 @@ npm install @meenainwal/rich-text-editor
37
88
  ### Basic Usage (Vanilla JS)
38
89
 
39
90
  ```javascript
40
- import { TestEditor } from '@meenainwal/rich-text-editor';
91
+ import { InkFlowEditor } from '@meenainwal/rich-text-editor';
41
92
  import '@meenainwal/rich-text-editor/style'; // Simple style import
42
93
 
43
94
  const container = document.getElementById('editor');
44
- const editor = new TestEditor(container, {
95
+ const editor = new InkFlowEditor(container, {
45
96
  placeholder: 'Type something beautiful...',
46
97
  autofocus: true,
47
98
  showStatus: true,
@@ -49,29 +100,63 @@ const editor = new TestEditor(container, {
49
100
  });
50
101
  ```
51
102
 
52
- ### In React / Next.js (SSR Safe)
103
+ ### In React (Preventing Duplicates)
104
+ In React **Strict Mode**, components mount twice in development. Always use the cleanup function to destroy the editor instance.
53
105
 
54
106
  ```tsx
55
- "use client";
56
107
  import { useEffect, useRef } from 'react';
57
- import { TestEditor } from '@meenainwal/rich-text-editor';
108
+ import { InkFlowEditor } from '@meenainwal/rich-text-editor';
58
109
  import '@meenainwal/rich-text-editor/style';
59
110
 
60
- export default function Editor() {
111
+ export default function App() {
61
112
  const containerRef = useRef<HTMLDivElement>(null);
113
+ const editorRef = useRef<InkFlowEditor | null>(null);
62
114
 
63
115
  useEffect(() => {
64
- if (containerRef.current) {
65
- new TestEditor(containerRef.current, {
66
- onSave: (html) => console.log("Saved:", html)
116
+ if (containerRef.current && !editorRef.current) {
117
+ editorRef.current = new InkFlowEditor(containerRef.current, {
118
+ placeholder: 'Start writing...',
67
119
  });
68
120
  }
121
+
122
+ return () => {
123
+ if (editorRef.current) {
124
+ editorRef.current.destroy();
125
+ editorRef.current = null;
126
+ }
127
+ };
69
128
  }, []);
70
129
 
71
130
  return <div ref={containerRef} />;
72
131
  }
73
132
  ```
74
133
 
134
+ ### In Next.js (Safe Implementation)
135
+ For Next.js, ensure the editor is only initialized on the client side using `useEffect`.
136
+
137
+ ```tsx
138
+ "use client";
139
+ import { useEffect, useRef } from 'react';
140
+ import { InkFlowEditor } from '@meenainwal/rich-text-editor';
141
+ import '@meenainwal/rich-text-editor/style';
142
+
143
+ export default function MyEditor() {
144
+ const containerRef = useRef<HTMLDivElement>(null);
145
+
146
+ useEffect(() => {
147
+ if (!containerRef.current) return;
148
+
149
+ const editor = new InkFlowEditor(containerRef.current, {
150
+ onSave: (html) => console.log(html)
151
+ });
152
+
153
+ return () => editor.destroy(); // Crucial for HMR and Strict Mode
154
+ }, []);
155
+
156
+ return <div ref={containerRef} className="editor-shell" />;
157
+ }
158
+ ```
159
+
75
160
  ---
76
161
 
77
162
  ## โš™๏ธ Configuration Options
@@ -85,14 +170,46 @@ export default function Editor() {
85
170
  | `toolbarItems` | `string[]` | `all` | Array of tool IDs to display (e.g., `['bold', 'table']`). |
86
171
  | `onSave` | `function` | `undefined` | Callback triggered when content is saved. |
87
172
  | `autoSaveInterval` | `number` | `1000` | Delay in ms before auto-save triggers after typing. |
173
+ | `imageEndpoints` | `object` | `undefined` | Custom upload endpoint configuration: `{ upload: string }`. |
174
+ | `cloudinaryFallback` | `object` | `undefined` | Cloudinary settings: `{ cloudName: string, uploadPreset: string }`. |
175
+ | `maxImageSizeMB` | `number` | `5` | Maximum image size in MB (enforced pre and post compression). |
88
176
 
89
177
  ## ๐Ÿ›  API Methods
90
178
 
179
+ - `destroy()`: **Crucial** - Cleans up DOM, event listeners, and memory leaks.
91
180
  - `getHTML()`: Returns the content as a sanitized HTML string.
92
181
  - `setHTML(html)`: Programmatically sets the editor content.
93
182
  - `focus()`: Forces focus onto the editor.
94
183
  - `setDarkMode(boolean)`: Dynamically toggle dark mode.
95
184
  - `insertTable(rows, cols)`: Programmatically insert a table.
185
+ - `insertImage(url, id, isLoading)`: Programmatically insert an image with optional loading state.
186
+
187
+ ## ๐Ÿ’ก Troubleshooting: Duplicate Editors?
188
+ If you see multiple toolbars or editors, it's likely because:
189
+ 1. **React Strict Mode**: Ensure you call `editor.destroy()` in the `useEffect` cleanup.
190
+ 2. **Missing Cleanup**: The editor injects elements into the DOM; if you don't destroy it when the component unmounts, those elements remain.
191
+
192
+ ---
193
+
194
+ ## ๐Ÿ“ Patch Notes
195
+
196
+ ### v1.2.0 (Premium UI & Image Power-Up)
197
+ - **Lucide Icon Upgrade**: Replaced all 27 toolbar icons with high-quality, professional Lucide-styled SVGs.
198
+ - **Initialization Loader**: Added a sophisticated shimmering glassmorphism loader for a smoother startup experience.
199
+ - **Advanced Image Pipeline**: Added drag-and-drop support with automatic client-side **WebP compression**.
200
+ - **Interactive UX**: Added loading state previews, 4-corner resizing handles, and native `<figcaption>` support.
201
+ - **Glassmorphism Design**: Enhanced modals and loaders with modern backdrop-blur effects.
202
+
203
+ ### v1.1.2 (Security & Performance)
204
+ - **Aggressive Size Optimization**: Reduced packed size to **28kB** by moving to ESM-only and pruning datasets.
205
+ - **Hardened Sanitization**: Centralized all HTML processing through a unified security layer (Rating 9.8/10).
206
+ - **Safe UI Rendering**: Eliminated `innerHTML` usage in all UI components for zero-trust text rendering.
207
+ - **CJS Build Deprecation**: Removed CommonJS versions to optimize for modern ESM-based environments.
208
+
209
+ ### v1.1.1 (Quick Fixes)
210
+ - Fixed missing `destroy()` export.
211
+ - Resolved memory leaks in image resizer.
212
+ - Prevented duplicate editors in React Strict Mode.
96
213
 
97
214
  ---
98
215
 
@@ -0,0 +1,25 @@
1
+ const e = [
2
+ { emoji: "๐Ÿ˜€", name: "grinning", category: "Smileys" },
3
+ { emoji: "๐Ÿ˜ƒ", name: "smiley", category: "Smileys" },
4
+ { emoji: "๐Ÿ˜„", name: "smile", category: "Smileys" },
5
+ { emoji: "๐Ÿ˜", name: "grin", category: "Smileys" },
6
+ { emoji: "๐Ÿ˜†", name: "laughing", category: "Smileys" },
7
+ { emoji: "๐Ÿ˜…", name: "sweat smile", category: "Smileys" },
8
+ { emoji: "๐Ÿ˜‚", name: "joy", category: "Smileys" },
9
+ { emoji: "๐Ÿ™‚", name: "slight smile", category: "Smileys" },
10
+ { emoji: "๐Ÿ˜‰", name: "wink", category: "Smileys" },
11
+ { emoji: "๐Ÿ˜Š", name: "blush", category: "Smileys" },
12
+ { emoji: "๐Ÿ˜", name: "heart eyes", category: "Smileys" },
13
+ { emoji: "๐Ÿ˜˜", name: "kissing heart", category: "Smileys" },
14
+ { emoji: "๐Ÿ‘", name: "thumbs up", category: "Hands" },
15
+ { emoji: "๐Ÿ‘Ž", name: "thumbs down", category: "Hands" },
16
+ { emoji: "โค๏ธ", name: "heart", category: "Symbols" },
17
+ { emoji: "โœจ", name: "sparkles", category: "Symbols" },
18
+ { emoji: "๐Ÿ”ฅ", name: "fire", category: "Symbols" },
19
+ { emoji: "โœ…", name: "check", category: "Symbols" },
20
+ { emoji: "๐ŸŽ‰", name: "party", category: "Activities" },
21
+ { emoji: "๐Ÿš€", name: "rocket", category: "Travel" }
22
+ ];
23
+ export {
24
+ e as EMOJI_LIST
25
+ };
@@ -29,8 +29,20 @@ export interface EditorOptions {
29
29
  onSaving?: () => void;
30
30
  onChange?: (html: string) => void;
31
31
  autoSaveInterval?: number;
32
+ autoSave?: boolean;
32
33
  showStatus?: boolean;
34
+ showLoader?: boolean;
33
35
  toolbarItems?: string[];
36
+ imageEndpoints?: {
37
+ upload: string;
38
+ delete: string;
39
+ };
40
+ cloudinaryFallback?: {
41
+ cloudName: string;
42
+ uploadPreset: string;
43
+ };
44
+ maxImageSizeMB?: number;
45
+ onImageDelete?: (imageId?: string, imageUrl?: string) => void;
34
46
  }
35
47
  export declare class CoreEditor {
36
48
  protected container: HTMLElement;
@@ -42,6 +54,11 @@ export declare class CoreEditor {
42
54
  private saveTimeout;
43
55
  private historyTimeout;
44
56
  private pendingStyles;
57
+ private observer;
58
+ private floatingToolbar;
59
+ private eventListeners;
60
+ private loaderElement;
61
+ private isUndoingRedoing;
45
62
  constructor(container: HTMLElement, options?: EditorOptions);
46
63
  /**
47
64
  * Applies custom theme variables to the editor container.
@@ -51,19 +68,31 @@ export declare class CoreEditor {
51
68
  * Toggles dark mode on the editor.
52
69
  */
53
70
  setDarkMode(enabled: boolean): void;
71
+ /**
72
+ * Destroys the editor instance and cleans up.
73
+ */
74
+ destroy(): void;
75
+ protected checkPlaceholder(): void;
76
+ private addEventListener;
54
77
  protected setupImageObserver(): void;
55
78
  /**
56
79
  * Wraps a raw <img> element in the interactive container
57
80
  */
58
81
  private wrapImage;
59
82
  protected setupInputHandlers(): void;
83
+ /**
84
+ * Immediately records a history state if one is pending.
85
+ */
86
+ private flushHistoryRecord;
60
87
  private handleInput;
61
88
  private scheduleHistoryRecord;
62
89
  private scheduleAutoSave;
63
90
  save(): void;
64
91
  undo(): void;
65
92
  redo(): void;
66
- private triggerChange;
93
+ protected triggerChange(): void;
94
+ private createLoader;
95
+ private hideLoader;
67
96
  protected createEditableElement(): HTMLElement;
68
97
  /**
69
98
  * Focuses the editor.
@@ -73,6 +102,10 @@ export declare class CoreEditor {
73
102
  * Executes a command on the current selection.
74
103
  */
75
104
  execute(command: string, value?: string | null): void;
105
+ /**
106
+ * Special handler for links to open them in a new tab when clicked.
107
+ */
108
+ private setupLinkClickHandlers;
76
109
  /**
77
110
  * Inserts a table at the current selection.
78
111
  */
@@ -99,12 +132,21 @@ export declare class CoreEditor {
99
132
  * Recursively removes a style property from all elements in a fragment.
100
133
  */
101
134
  private clearStyleRecursive;
135
+ /**
136
+ * Applies an inline style to the selection.
137
+ * This is used for properties like font-size (px) and font-family
138
+ * where execCommand is outdated or limited.
139
+ */
102
140
  /**
103
141
  * Applies an inline style to the selection.
104
142
  * This is used for properties like font-size (px) and font-family
105
143
  * where execCommand is outdated or limited.
106
144
  */
107
145
  setStyle(property: string, value: string, range?: Range): Range | null;
146
+ /**
147
+ * Applies a style to the block-level containers within the range.
148
+ */
149
+ private setBlockStyle;
108
150
  /**
109
151
  * Creates a link at the current selection.
110
152
  * Ensures the link opens in a new tab with proper security attributes.
@@ -113,11 +155,25 @@ export declare class CoreEditor {
113
155
  /**
114
156
  * Inserts an image at the current selection.
115
157
  */
116
- insertImage(url: string): void;
158
+ insertImage(url: string, id?: string, isLoading?: boolean): HTMLElement | null;
117
159
  /**
118
- * Returns the clean HTML content of the editor.
160
+ * Returns the clean and optimized HTML content of the editor.
119
161
  */
120
162
  getHTML(): string;
163
+ /**
164
+ * Normalizes the editor's content in-place.
165
+ */
166
+ normalize(): void;
167
+ private normalizationContainer;
168
+ /**
169
+ * Internal helper to strictly sanitize HTML strings.
170
+ */
171
+ private sanitize;
172
+ /**
173
+ * Optimizes HTML by fixing invalid nesting and removing redundant tags.
174
+ */
175
+ private normalizeHTML;
176
+ private handlePaste;
121
177
  /**
122
178
  * Sets the HTML content of the editor.
123
179
  */
@@ -130,4 +186,8 @@ export declare class CoreEditor {
130
186
  * Returns the editor options.
131
187
  */
132
188
  getOptions(): EditorOptions;
189
+ /**
190
+ * Internal helper to handle multiple files.
191
+ */
192
+ handleFiles(files: File[]): Promise<void>;
133
193
  }
@@ -1,5 +1,11 @@
1
1
  export interface HistoryState {
2
2
  html: string;
3
+ selection: {
4
+ startPath: number[];
5
+ startOffset: number;
6
+ endPath: number[];
7
+ endOffset: number;
8
+ } | null;
3
9
  }
4
10
  export declare class HistoryManager {
5
11
  private stack;
@@ -10,15 +16,15 @@ export declare class HistoryManager {
10
16
  * Records a new state in the history stack.
11
17
  * Clears any "redo" states if we record a new action.
12
18
  */
13
- record(html: string): void;
19
+ record(html: string, selection: HistoryState['selection'] | null): void;
14
20
  /**
15
21
  * Returns the previous state if available.
16
22
  */
17
- undo(): string | null;
23
+ undo(): HistoryState | null;
18
24
  /**
19
25
  * Returns the next state if available.
20
26
  */
21
- redo(): string | null;
27
+ redo(): HistoryState | null;
22
28
  /**
23
29
  * Checks if undo is possible.
24
30
  */
@@ -9,8 +9,17 @@ export declare class ImageManager {
9
9
  private startHeight;
10
10
  private currentHandle;
11
11
  private aspectRatio;
12
+ private boundMouseDown;
13
+ private boundMouseMove;
14
+ private boundMouseUp;
15
+ private boundKeyDown;
12
16
  constructor(editor: CoreEditor);
13
17
  private setupListeners;
18
+ private handleMouseDown;
19
+ private handleMouseMove;
20
+ private handleMouseUp;
21
+ private handleKeyDown;
22
+ destroy(): void;
14
23
  private selectImage;
15
24
  private deselectImage;
16
25
  private startResize;
@@ -12,6 +12,27 @@ export declare class SelectionManager {
12
12
  * Returns the first range of the current selection.
13
13
  */
14
14
  getRange(): Range | null;
15
+ /**
16
+ * Serializes the current selection into a path-based format relative to a root element.
17
+ * This allows restoring selection even if the DOM nodes are replaced but the structure is similar.
18
+ */
19
+ getSelectionPath(root: HTMLElement): {
20
+ startPath: number[];
21
+ startOffset: number;
22
+ endPath: number[];
23
+ endOffset: number;
24
+ } | null;
25
+ /**
26
+ * Restores selection from a path-based serialization.
27
+ */
28
+ restoreSelectionPath(root: HTMLElement, path: {
29
+ startPath: number[];
30
+ startOffset: number;
31
+ endPath: number[];
32
+ endOffset: number;
33
+ } | null): void;
34
+ private getNodePath;
35
+ private getNodeByPath;
15
36
  /**
16
37
  * Saves the current selection range.
17
38
  */
@@ -0,0 +1,15 @@
1
+ import { EditorOptions } from '../Editor';
2
+ export interface UploadResult {
3
+ imageUrl: string;
4
+ imageId?: string;
5
+ }
6
+ export declare class ImageUploader {
7
+ /**
8
+ * Compresses an image file using HTML5 Canvas.
9
+ */
10
+ static compressImage(file: File, maxSizeMB: number): Promise<File | Blob>;
11
+ /**
12
+ * Uploads a file based on editor configuration.
13
+ */
14
+ static uploadFile(file: File | Blob, options: EditorOptions): Promise<UploadResult | null>;
15
+ }
package/dist/index.d.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import { CoreEditor, EditorOptions } from './core/Editor';
2
2
  import { Toolbar } from './ui/Toolbar';
3
- export declare class TestEditor extends CoreEditor {
3
+ export declare class InkFlowEditor extends CoreEditor {
4
4
  private toolbar;
5
5
  constructor(container: HTMLElement, options?: EditorOptions);
6
6
  getToolbar(): Toolbar;
7
+ destroy(): void;
7
8
  }
8
9
  export { CoreEditor, type EditorOptions } from './core/Editor';
9
10
  export { SelectionManager } from './core/SelectionManager';