@meenainwal/rich-text-editor 1.1.1 โ†’ 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,7 +5,17 @@
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
+ ---
9
19
 
10
20
  ## ๐ŸŽฎ Live React Preview
11
21
  Wanna see it in action? Try the **Interactive React Demo** on StackBlitz:
@@ -14,8 +24,10 @@ Wanna see it in action? Try the **Interactive React Demo** on StackBlitz:
14
24
  ## โœจ Premium Features & Why Choose This Editor?
15
25
 
16
26
  ### ๐Ÿ‘ Key Pros & Capabilities
17
- - **Microscopic Footprint**: Only **~11kB** (Initial gzipped JS) + **2.3kB** (CSS). Total initial load is **~13kB gzipped**.
18
- - **Performance Optimized**: Heavy components like the Emoji Picker (~19kB) are **lazy-loaded** only when clicked, ensuring your app stays fast.
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.
19
31
  - **Framework Agnostic**: Native support for **React**, **Next.js**, **Vue**, **Angular**, and **Svelte**.
20
32
  - **Auto-Formatting Magic**: Intelligently parses pasted HTML strings into clean, formatted rich text.
21
33
  - **Professional UI/UX**: Modern aesthetics curated with a polished Slate & Indigo color palette.
@@ -23,6 +35,7 @@ Wanna see it in action? Try the **Interactive React Demo** on StackBlitz:
23
35
  - **Emoji Picker**: Integrated searchable emoji library for expressive content.
24
36
  - **Dark Mode**: Sophisticated dark theme for premium developer experiences.
25
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.
26
39
 
27
40
  ---
28
41
 
@@ -39,25 +52,31 @@ We are currently building a dedicated official website to provide the best possi
39
52
 
40
53
  ---
41
54
 
42
- ### ๐Ÿ“ฆ Bundle Size Breakdown
43
- Transparency matters. Here's a breakdown of what your users download:
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.
44
61
 
45
- | Asset | Minified | Gzipped (Download) |
46
- | :--- | :--- | :--- |
47
- | **Core Bundle (JS)** | ~45 kB | **~11 kB** |
48
- | **Styles (CSS)** | ~9 kB | **~2 kB** |
49
- | **Total Initial Load** | **~54 kB** | **~13 kB** |
50
- | Emoji Picker (Lazy-loaded) | +19 kB | +3 kB |
62
+ ---
51
63
 
52
64
  > [!TIP]
53
65
  > The editor is optimized for performance. Features like the **Emoji Picker** are only loaded when needed, keeping your initial page load lightning fast.
54
66
 
55
67
  ### ๐Ÿ‘Ž Cons (Current Limitations)
56
- - Base64 image storage can increase the raw output string size for very large images (Backend S3 uploading adapter coming soon).
57
68
  - Markdown shortcut typing (e.g., typing `#` for H1) is not natively supported yet.
58
69
 
59
70
  ---
60
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
+
61
80
  ## ๐Ÿ“ฆ Installation
62
81
 
63
82
  ```bash
@@ -69,11 +88,11 @@ npm install @meenainwal/rich-text-editor
69
88
  ### Basic Usage (Vanilla JS)
70
89
 
71
90
  ```javascript
72
- import { TestEditor } from '@meenainwal/rich-text-editor';
91
+ import { InkFlowEditor } from '@meenainwal/rich-text-editor';
73
92
  import '@meenainwal/rich-text-editor/style'; // Simple style import
74
93
 
75
94
  const container = document.getElementById('editor');
76
- const editor = new TestEditor(container, {
95
+ const editor = new InkFlowEditor(container, {
77
96
  placeholder: 'Type something beautiful...',
78
97
  autofocus: true,
79
98
  showStatus: true,
@@ -86,16 +105,16 @@ In React **Strict Mode**, components mount twice in development. Always use the
86
105
 
87
106
  ```tsx
88
107
  import { useEffect, useRef } from 'react';
89
- import { TestEditor } from '@meenainwal/rich-text-editor';
108
+ import { InkFlowEditor } from '@meenainwal/rich-text-editor';
90
109
  import '@meenainwal/rich-text-editor/style';
91
110
 
92
111
  export default function App() {
93
112
  const containerRef = useRef<HTMLDivElement>(null);
94
- const editorRef = useRef<TestEditor | null>(null);
113
+ const editorRef = useRef<InkFlowEditor | null>(null);
95
114
 
96
115
  useEffect(() => {
97
116
  if (containerRef.current && !editorRef.current) {
98
- editorRef.current = new TestEditor(containerRef.current, {
117
+ editorRef.current = new InkFlowEditor(containerRef.current, {
99
118
  placeholder: 'Start writing...',
100
119
  });
101
120
  }
@@ -118,7 +137,7 @@ For Next.js, ensure the editor is only initialized on the client side using `use
118
137
  ```tsx
119
138
  "use client";
120
139
  import { useEffect, useRef } from 'react';
121
- import { TestEditor } from '@meenainwal/rich-text-editor';
140
+ import { InkFlowEditor } from '@meenainwal/rich-text-editor';
122
141
  import '@meenainwal/rich-text-editor/style';
123
142
 
124
143
  export default function MyEditor() {
@@ -127,7 +146,7 @@ export default function MyEditor() {
127
146
  useEffect(() => {
128
147
  if (!containerRef.current) return;
129
148
 
130
- const editor = new TestEditor(containerRef.current, {
149
+ const editor = new InkFlowEditor(containerRef.current, {
131
150
  onSave: (html) => console.log(html)
132
151
  });
133
152
 
@@ -151,6 +170,9 @@ export default function MyEditor() {
151
170
  | `toolbarItems` | `string[]` | `all` | Array of tool IDs to display (e.g., `['bold', 'table']`). |
152
171
  | `onSave` | `function` | `undefined` | Callback triggered when content is saved. |
153
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). |
154
176
 
155
177
  ## ๐Ÿ›  API Methods
156
178
 
@@ -160,25 +182,34 @@ export default function MyEditor() {
160
182
  - `focus()`: Forces focus onto the editor.
161
183
  - `setDarkMode(boolean)`: Dynamically toggle dark mode.
162
184
  - `insertTable(rows, cols)`: Programmatically insert a table.
185
+ - `insertImage(url, id, isLoading)`: Programmatically insert an image with optional loading state.
163
186
 
164
187
  ## ๐Ÿ’ก Troubleshooting: Duplicate Editors?
165
188
  If you see multiple toolbars or editors, it's likely because:
166
189
  1. **React Strict Mode**: Ensure you call `editor.destroy()` in the `useEffect` cleanup.
167
190
  2. **Missing Cleanup**: The editor injects elements into the DOM; if you don't destroy it when the component unmounts, those elements remain.
168
191
 
169
- ## ๐Ÿ“ Patch Notes (v1.1.1)
170
-
171
- ### ๐Ÿ› Bug Fixes
172
- - **Fixed Missing `destroy()` Export**: Resolved TypeScript error where the `destroy()` method was missing from the generated type declarations.
173
- - **Image Resizer Cleanup**: Fixed a memory leak where window-level event listeners for image resizing were not being removed on editor destruction.
174
- - **Double Initializing Failsafe**: Added an internal check to clear the container before initialization, preventing duplicate editors in React Strict Mode.
175
- - **Lazy-Loaded Emojis**: Moved the Emoji List to a separate chunk (~19kB) that only loads when the picker is opened, reducing the initial bundle size.
192
+ ---
176
193
 
177
- ### โœจ Improvements
178
- - **Optimized Initial Load**: Critical path JS is now only **~11kB gzipped**.
179
- - **Robust Documentation**: Added detailed React and Next.js implementation guides with proper lifecycle cleanup examples.
180
- - **Troubleshooting Guide**: New section for common pitfalls like duplicate editors and SSR issues.
181
- - **Live Demo**: Added StackBlitz interactive demo for instant preview.
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.
182
213
 
183
214
  ---
184
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
+ };
@@ -31,7 +31,18 @@ export interface EditorOptions {
31
31
  autoSaveInterval?: number;
32
32
  autoSave?: boolean;
33
33
  showStatus?: boolean;
34
+ showLoader?: boolean;
34
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;
35
46
  }
36
47
  export declare class CoreEditor {
37
48
  protected container: HTMLElement;
@@ -44,7 +55,10 @@ export declare class CoreEditor {
44
55
  private historyTimeout;
45
56
  private pendingStyles;
46
57
  private observer;
58
+ private floatingToolbar;
47
59
  private eventListeners;
60
+ private loaderElement;
61
+ private isUndoingRedoing;
48
62
  constructor(container: HTMLElement, options?: EditorOptions);
49
63
  /**
50
64
  * Applies custom theme variables to the editor container.
@@ -66,13 +80,19 @@ export declare class CoreEditor {
66
80
  */
67
81
  private wrapImage;
68
82
  protected setupInputHandlers(): void;
83
+ /**
84
+ * Immediately records a history state if one is pending.
85
+ */
86
+ private flushHistoryRecord;
69
87
  private handleInput;
70
88
  private scheduleHistoryRecord;
71
89
  private scheduleAutoSave;
72
90
  save(): void;
73
91
  undo(): void;
74
92
  redo(): void;
75
- private triggerChange;
93
+ protected triggerChange(): void;
94
+ private createLoader;
95
+ private hideLoader;
76
96
  protected createEditableElement(): HTMLElement;
77
97
  /**
78
98
  * Focuses the editor.
@@ -82,6 +102,10 @@ export declare class CoreEditor {
82
102
  * Executes a command on the current selection.
83
103
  */
84
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;
85
109
  /**
86
110
  * Inserts a table at the current selection.
87
111
  */
@@ -108,12 +132,21 @@ export declare class CoreEditor {
108
132
  * Recursively removes a style property from all elements in a fragment.
109
133
  */
110
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
+ */
111
140
  /**
112
141
  * Applies an inline style to the selection.
113
142
  * This is used for properties like font-size (px) and font-family
114
143
  * where execCommand is outdated or limited.
115
144
  */
116
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;
117
150
  /**
118
151
  * Creates a link at the current selection.
119
152
  * Ensures the link opens in a new tab with proper security attributes.
@@ -122,11 +155,25 @@ export declare class CoreEditor {
122
155
  /**
123
156
  * Inserts an image at the current selection.
124
157
  */
125
- insertImage(url: string): void;
158
+ insertImage(url: string, id?: string, isLoading?: boolean): HTMLElement | null;
126
159
  /**
127
- * Returns the clean HTML content of the editor.
160
+ * Returns the clean and optimized HTML content of the editor.
128
161
  */
129
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;
130
177
  /**
131
178
  * Sets the HTML content of the editor.
132
179
  */
@@ -139,4 +186,8 @@ export declare class CoreEditor {
139
186
  * Returns the editor options.
140
187
  */
141
188
  getOptions(): EditorOptions;
189
+ /**
190
+ * Internal helper to handle multiple files.
191
+ */
192
+ handleFiles(files: File[]): Promise<void>;
142
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
  */
@@ -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,6 +1,6 @@
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;