smartrte-react 0.1.10 → 0.1.14
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 +20 -32
- package/dist/components/ClassicEditor.d.ts +16 -1
- package/dist/components/ClassicEditor.js +659 -264
- package/package.json +12 -13
package/README.md
CHANGED
|
@@ -330,15 +330,13 @@ The editor comes with built-in styles. You can customize the appearance by wrapp
|
|
|
330
330
|
|
|
331
331
|
- Node.js 18+
|
|
332
332
|
- pnpm 9.10.0+
|
|
333
|
-
- Rust (for WASM compilation)
|
|
334
|
-
- wasm-pack
|
|
335
333
|
|
|
336
334
|
### Setting Up Development Environment
|
|
337
335
|
|
|
338
336
|
1. **Clone the repository**
|
|
339
337
|
|
|
340
338
|
```bash
|
|
341
|
-
git clone https://github.com/
|
|
339
|
+
git clone https://github.com/ayush1852017/smart-rte.git
|
|
342
340
|
cd smart-rte
|
|
343
341
|
```
|
|
344
342
|
|
|
@@ -351,13 +349,7 @@ pnpm install
|
|
|
351
349
|
3. **Build the project**
|
|
352
350
|
|
|
353
351
|
```bash
|
|
354
|
-
# Build WASM core
|
|
355
|
-
pnpm build:wasm
|
|
356
|
-
|
|
357
352
|
# Build TypeScript packages
|
|
358
|
-
pnpm build:ts
|
|
359
|
-
|
|
360
|
-
# Or build everything
|
|
361
353
|
pnpm build
|
|
362
354
|
```
|
|
363
355
|
|
|
@@ -376,19 +368,17 @@ The playground will be available at `http://localhost:5173`
|
|
|
376
368
|
```
|
|
377
369
|
smart-rte/
|
|
378
370
|
├── packages/
|
|
379
|
-
│
|
|
380
|
-
│
|
|
381
|
-
│
|
|
382
|
-
│
|
|
383
|
-
│
|
|
384
|
-
│
|
|
385
|
-
│
|
|
386
|
-
│
|
|
387
|
-
|
|
388
|
-
│
|
|
389
|
-
|
|
390
|
-
│ └── smart_rte_core/
|
|
391
|
-
├── apps/ # Example applications
|
|
371
|
+
│ └── react/ # Main React package (smartrte-react)
|
|
372
|
+
│ ├── src/
|
|
373
|
+
│ │ ├── components/
|
|
374
|
+
│ │ │ ├── ClassicEditor.tsx # Main editor component
|
|
375
|
+
│ │ │ └── MediaManager.tsx # Media management component
|
|
376
|
+
│ │ └── index.ts
|
|
377
|
+
│ ├── playground/ # Development playground
|
|
378
|
+
│ └── package.json
|
|
379
|
+
├── dart/ # Flutter/Dart packages
|
|
380
|
+
│ ├── smartrte_flutter/ # Flutter WebView integration
|
|
381
|
+
│ └── example_app/ # Flutter example
|
|
392
382
|
└── package.json
|
|
393
383
|
```
|
|
394
384
|
|
|
@@ -455,7 +445,7 @@ We welcome contributions! Here's how you can help:
|
|
|
455
445
|
|
|
456
446
|
### Reporting Bugs
|
|
457
447
|
|
|
458
|
-
1. Check if the bug has already been reported in [Issues](https://github.com/
|
|
448
|
+
1. Check if the bug has already been reported in [Issues](https://github.com/ayush1852017/smart-rte/issues)
|
|
459
449
|
2. If not, create a new issue with:
|
|
460
450
|
- Clear title and description
|
|
461
451
|
- Steps to reproduce
|
|
@@ -465,7 +455,7 @@ We welcome contributions! Here's how you can help:
|
|
|
465
455
|
|
|
466
456
|
### Suggesting Features
|
|
467
457
|
|
|
468
|
-
1. Check [existing feature requests](https://github.com/
|
|
458
|
+
1. Check [existing feature requests](https://github.com/ayush1852017/smart-rte/issues?q=is%3Aissue+label%3Aenhancement)
|
|
469
459
|
2. Create a new issue with:
|
|
470
460
|
- Clear description of the feature
|
|
471
461
|
- Use cases
|
|
@@ -611,20 +601,20 @@ SOFTWARE.
|
|
|
611
601
|
|
|
612
602
|
- **Smart RTE Team** - Initial work and maintenance
|
|
613
603
|
|
|
614
|
-
See the list of [contributors](https://github.com/
|
|
604
|
+
See the list of [contributors](https://github.com/ayush1852017/smart-rte/contributors) who participated in this project.
|
|
615
605
|
|
|
616
606
|
## 🙏 Acknowledgments
|
|
617
607
|
|
|
618
608
|
- [KaTeX](https://katex.org/) - For mathematical formula rendering
|
|
619
609
|
- [React](https://reactjs.org/) - The UI library
|
|
620
610
|
- [Vite](https://vitejs.dev/) - Build tool
|
|
621
|
-
- All our amazing [contributors](https://github.com/
|
|
611
|
+
- All our amazing [contributors](https://github.com/ayush1852017/smart-rte/contributors)
|
|
622
612
|
|
|
623
613
|
## 📞 Support
|
|
624
614
|
|
|
625
615
|
- **Documentation:** You're reading it! 📖
|
|
626
|
-
- **Issues:** [GitHub Issues](https://github.com/
|
|
627
|
-
- **Discussions:** [GitHub Discussions](https://github.com/
|
|
616
|
+
- **Issues:** [GitHub Issues](https://github.com/ayush1852017/smart-rte/issues)
|
|
617
|
+
- **Discussions:** [GitHub Discussions](https://github.com/ayush1852017/smart-rte/discussions)
|
|
628
618
|
- **Twitter:** [@smartrte](https://twitter.com/smartrte) (if applicable)
|
|
629
619
|
|
|
630
620
|
## 🗺️ Roadmap
|
|
@@ -658,9 +648,7 @@ See the list of [contributors](https://github.com/yourusername/smart-rte/contrib
|
|
|
658
648
|
|
|
659
649
|
## 🔗 Related Packages
|
|
660
650
|
|
|
661
|
-
-
|
|
662
|
-
- **smartrte-flutter** - Flutter/Dart implementation
|
|
663
|
-
- **smart-rte-core** - Rust core library
|
|
651
|
+
- **smartrte-flutter** - Flutter/Dart WebView implementation
|
|
664
652
|
|
|
665
653
|
## 💡 Tips & Best Practices
|
|
666
654
|
|
|
@@ -696,4 +684,4 @@ See the list of [contributors](https://github.com/yourusername/smart-rte/contrib
|
|
|
696
684
|
|
|
697
685
|
**Happy Editing! 🎉**
|
|
698
686
|
|
|
699
|
-
If you find this package useful, please consider giving it a ⭐ on [GitHub](https://github.com/
|
|
687
|
+
If you find this package useful, please consider giving it a ⭐ on [GitHub](https://github.com/ayush1852017/smart-rte)!
|
|
@@ -10,6 +10,21 @@ type ClassicEditorProps = {
|
|
|
10
10
|
media?: boolean;
|
|
11
11
|
formula?: boolean;
|
|
12
12
|
mediaManager?: MediaManagerAdapter;
|
|
13
|
+
/**
|
|
14
|
+
* Optional custom list of fonts to display in the toolbar.
|
|
15
|
+
* If not provided, a default set of web-safe fonts will be used.
|
|
16
|
+
* Example: [{ name: 'Robto', value: 'Roboto, sans-serif' }]
|
|
17
|
+
*/
|
|
18
|
+
fonts?: {
|
|
19
|
+
name: string;
|
|
20
|
+
value: string;
|
|
21
|
+
}[];
|
|
22
|
+
/**
|
|
23
|
+
* The default font family to apply to the editor content.
|
|
24
|
+
* This sets the font-family style of the editable area.
|
|
25
|
+
* Example: "Arial, sans-serif"
|
|
26
|
+
*/
|
|
27
|
+
defaultFont?: string;
|
|
13
28
|
};
|
|
14
|
-
export declare function ClassicEditor({ value, onChange, placeholder, minHeight, maxHeight, readOnly, table, media, formula, mediaManager, }: ClassicEditorProps): import("react/jsx-runtime").JSX.Element;
|
|
29
|
+
export declare function ClassicEditor({ value, onChange, placeholder, minHeight, maxHeight, readOnly, table, media, formula, mediaManager, fonts, defaultFont, }: ClassicEditorProps): import("react/jsx-runtime").JSX.Element;
|
|
15
30
|
export {};
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useRef, useState } from "react";
|
|
3
3
|
import { MediaManager } from "./MediaManager";
|
|
4
|
-
export function ClassicEditor({ value, onChange, placeholder = "Type here…", minHeight = 200, maxHeight = 500, readOnly = false, table = true, media = true, formula = true, mediaManager,
|
|
4
|
+
export function ClassicEditor({ value, onChange, placeholder = "Type here…", minHeight = 200, maxHeight = 500, readOnly = false, table = true, media = true, formula = true, mediaManager, fonts = [
|
|
5
|
+
{ name: "Arial", value: "Arial, Helvetica, sans-serif" },
|
|
6
|
+
{ name: "Georgia", value: "Georgia, serif" },
|
|
7
|
+
{ name: "Impact", value: "Impact, Charcoal, sans-serif" },
|
|
8
|
+
{ name: "Tahoma", value: "Tahoma, Geneva, sans-serif" },
|
|
9
|
+
{ name: "Times New Roman", value: "'Times New Roman', Times, serif" },
|
|
10
|
+
{ name: "Verdana", value: "Verdana, Geneva, sans-serif" },
|
|
11
|
+
{ name: "Courier New", value: "'Courier New', Courier, monospace" },
|
|
12
|
+
], defaultFont, }) {
|
|
5
13
|
const editableRef = useRef(null);
|
|
6
14
|
const lastEmittedRef = useRef("");
|
|
7
15
|
const isComposingRef = useRef(false);
|
|
@@ -11,6 +19,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
11
19
|
const [imageOverlay, setImageOverlay] = useState(null);
|
|
12
20
|
const resizingRef = useRef(null);
|
|
13
21
|
const draggedImageRef = useRef(null);
|
|
22
|
+
const tableResizeRef = useRef(null);
|
|
14
23
|
const [showTableDialog, setShowTableDialog] = useState(false);
|
|
15
24
|
const [tableRows, setTableRows] = useState(3);
|
|
16
25
|
const [tableCols, setTableCols] = useState(3);
|
|
@@ -24,7 +33,8 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
24
33
|
const [showColorPicker, setShowColorPicker] = useState(false);
|
|
25
34
|
const [colorPickerType, setColorPickerType] = useState('text');
|
|
26
35
|
const savedRangeRef = useRef(null);
|
|
27
|
-
const [currentFontSize, setCurrentFontSize] = useState("
|
|
36
|
+
const [currentFontSize, setCurrentFontSize] = useState("");
|
|
37
|
+
const [currentFont, setCurrentFont] = useState("");
|
|
28
38
|
useEffect(() => {
|
|
29
39
|
const el = editableRef.current;
|
|
30
40
|
if (!el)
|
|
@@ -32,6 +42,8 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
32
42
|
// Initialize with provided HTML only when externally controlled value changes
|
|
33
43
|
if (typeof value === "string" && value !== el.innerHTML) {
|
|
34
44
|
el.innerHTML = value || "";
|
|
45
|
+
fixNegativeMargins(el);
|
|
46
|
+
ensureTableWrappers(el);
|
|
35
47
|
}
|
|
36
48
|
// Suppress native context menu inside table cells at capture phase
|
|
37
49
|
const onCtx = (evt) => {
|
|
@@ -151,6 +163,57 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
151
163
|
console.error('Error applying font size:', error);
|
|
152
164
|
}
|
|
153
165
|
};
|
|
166
|
+
const applyFontFamily = (font) => {
|
|
167
|
+
try {
|
|
168
|
+
setCurrentFont(font);
|
|
169
|
+
const editor = editableRef.current;
|
|
170
|
+
if (!editor)
|
|
171
|
+
return;
|
|
172
|
+
editor.focus();
|
|
173
|
+
let range = null;
|
|
174
|
+
const sel = window.getSelection();
|
|
175
|
+
if (sel && sel.rangeCount > 0) {
|
|
176
|
+
const currentRange = sel.getRangeAt(0);
|
|
177
|
+
if (editor.contains(currentRange.commonAncestorContainer)) {
|
|
178
|
+
range = currentRange;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (!range && savedRangeRef.current) {
|
|
182
|
+
range = savedRangeRef.current.cloneRange();
|
|
183
|
+
}
|
|
184
|
+
if (!range)
|
|
185
|
+
return;
|
|
186
|
+
if (range.collapsed) {
|
|
187
|
+
const span = document.createElement('span');
|
|
188
|
+
span.style.fontFamily = font;
|
|
189
|
+
span.textContent = '\u200B';
|
|
190
|
+
range.insertNode(span);
|
|
191
|
+
const newRange = document.createRange();
|
|
192
|
+
newRange.setStart(span.firstChild, 1);
|
|
193
|
+
newRange.collapse(true);
|
|
194
|
+
if (sel) {
|
|
195
|
+
sel.removeAllRanges();
|
|
196
|
+
sel.addRange(newRange);
|
|
197
|
+
}
|
|
198
|
+
handleInput();
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const span = document.createElement('span');
|
|
202
|
+
span.style.fontFamily = font;
|
|
203
|
+
const fragment = range.extractContents();
|
|
204
|
+
span.appendChild(fragment);
|
|
205
|
+
range.insertNode(span);
|
|
206
|
+
if (sel) {
|
|
207
|
+
range.selectNodeContents(span);
|
|
208
|
+
sel.removeAllRanges();
|
|
209
|
+
sel.addRange(range);
|
|
210
|
+
}
|
|
211
|
+
handleInput();
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
console.error('Error applying font family:', error);
|
|
215
|
+
}
|
|
216
|
+
};
|
|
154
217
|
const applyTextColor = (color) => {
|
|
155
218
|
exec("foreColor", color);
|
|
156
219
|
};
|
|
@@ -194,6 +257,124 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
194
257
|
window.removeEventListener("resize", onResize);
|
|
195
258
|
};
|
|
196
259
|
}, []);
|
|
260
|
+
// Table resize event listeners
|
|
261
|
+
useEffect(() => {
|
|
262
|
+
const el = editableRef.current;
|
|
263
|
+
if (!el)
|
|
264
|
+
return;
|
|
265
|
+
addTableResizeHandles();
|
|
266
|
+
const onMouseDown = (e) => {
|
|
267
|
+
if (!table)
|
|
268
|
+
return;
|
|
269
|
+
const target = e.target;
|
|
270
|
+
if (target.tagName === 'TD' || target.tagName === 'TH') {
|
|
271
|
+
const rect = target.getBoundingClientRect();
|
|
272
|
+
const rightEdge = rect.right;
|
|
273
|
+
const clickX = e.clientX;
|
|
274
|
+
if (Math.abs(clickX - rightEdge) < 5) {
|
|
275
|
+
e.preventDefault();
|
|
276
|
+
const tableElem = target.closest('table');
|
|
277
|
+
const colIndex = parseInt(target.getAttribute('data-col-index') || '0', 10);
|
|
278
|
+
if (tableElem) {
|
|
279
|
+
startColumnResize(tableElem, colIndex, e.clientX);
|
|
280
|
+
}
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const bottomEdge = rect.bottom;
|
|
284
|
+
const clickY = e.clientY;
|
|
285
|
+
if (Math.abs(clickY - bottomEdge) < 5) {
|
|
286
|
+
e.preventDefault();
|
|
287
|
+
const tableElem = target.closest('table');
|
|
288
|
+
const row = target.closest('tr');
|
|
289
|
+
if (tableElem && row) {
|
|
290
|
+
const rowIndex = parseInt(row.getAttribute('data-row-index') || '0', 10);
|
|
291
|
+
startRowResize(tableElem, rowIndex, e.clientY);
|
|
292
|
+
}
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
const onMouseMove = (e) => {
|
|
298
|
+
if (tableResizeRef.current) {
|
|
299
|
+
handleTableResizeMove(e);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
if (!table)
|
|
303
|
+
return;
|
|
304
|
+
const target = e.target;
|
|
305
|
+
if (target.tagName === 'TD' || target.tagName === 'TH') {
|
|
306
|
+
const rect = target.getBoundingClientRect();
|
|
307
|
+
const clickX = e.clientX;
|
|
308
|
+
const clickY = e.clientY;
|
|
309
|
+
if (Math.abs(clickX - rect.right) < 5) {
|
|
310
|
+
el.style.cursor = 'col-resize';
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (Math.abs(clickY - rect.bottom) < 5) {
|
|
314
|
+
el.style.cursor = 'row-resize';
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
if (el.style.cursor === 'col-resize' || el.style.cursor === 'row-resize') {
|
|
318
|
+
el.style.cursor = '';
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
const onMouseUp = () => {
|
|
323
|
+
handleTableResizeEnd();
|
|
324
|
+
};
|
|
325
|
+
const onTouchStart = (e) => {
|
|
326
|
+
if (!table)
|
|
327
|
+
return;
|
|
328
|
+
const target = e.target;
|
|
329
|
+
if (target.tagName === 'TD' || target.tagName === 'TH') {
|
|
330
|
+
const rect = target.getBoundingClientRect();
|
|
331
|
+
const touch = e.touches[0];
|
|
332
|
+
const clickX = touch.clientX;
|
|
333
|
+
const clickY = touch.clientY;
|
|
334
|
+
if (Math.abs(clickX - rect.right) < 15) {
|
|
335
|
+
e.preventDefault();
|
|
336
|
+
const tableElem = target.closest('table');
|
|
337
|
+
const colIndex = parseInt(target.getAttribute('data-col-index') || '0', 10);
|
|
338
|
+
if (tableElem) {
|
|
339
|
+
startColumnResize(tableElem, colIndex, clickX);
|
|
340
|
+
}
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
if (Math.abs(clickY - rect.bottom) < 15) {
|
|
344
|
+
e.preventDefault();
|
|
345
|
+
const tableElem = target.closest('table');
|
|
346
|
+
const row = target.closest('tr');
|
|
347
|
+
if (tableElem && row) {
|
|
348
|
+
const rowIndex = parseInt(row.getAttribute('data-row-index') || '0', 10);
|
|
349
|
+
startRowResize(tableElem, rowIndex, clickY);
|
|
350
|
+
}
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
const onTouchMove = (e) => {
|
|
356
|
+
if (tableResizeRef.current) {
|
|
357
|
+
handleTableResizeMove(e);
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
const onTouchEnd = () => {
|
|
361
|
+
handleTableResizeEnd();
|
|
362
|
+
};
|
|
363
|
+
el.addEventListener('mousedown', onMouseDown);
|
|
364
|
+
window.addEventListener('mousemove', onMouseMove);
|
|
365
|
+
window.addEventListener('mouseup', onMouseUp);
|
|
366
|
+
el.addEventListener('touchstart', onTouchStart, { passive: false });
|
|
367
|
+
window.addEventListener('touchmove', onTouchMove, { passive: false });
|
|
368
|
+
window.addEventListener('touchend', onTouchEnd);
|
|
369
|
+
return () => {
|
|
370
|
+
el.removeEventListener('mousedown', onMouseDown);
|
|
371
|
+
window.removeEventListener('mousemove', onMouseMove);
|
|
372
|
+
window.removeEventListener('mouseup', onMouseUp);
|
|
373
|
+
el.removeEventListener('touchstart', onTouchStart);
|
|
374
|
+
window.removeEventListener('touchmove', onTouchMove);
|
|
375
|
+
window.removeEventListener('touchend', onTouchEnd);
|
|
376
|
+
};
|
|
377
|
+
}, [table]);
|
|
197
378
|
const insertImageAtSelection = (src) => {
|
|
198
379
|
try {
|
|
199
380
|
const host = editableRef.current;
|
|
@@ -347,11 +528,61 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
347
528
|
});
|
|
348
529
|
}
|
|
349
530
|
};
|
|
531
|
+
const fixNegativeMargins = (root) => {
|
|
532
|
+
try {
|
|
533
|
+
const nodes = root.querySelectorAll('*');
|
|
534
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
535
|
+
const node = nodes[i];
|
|
536
|
+
if (node.style && node.style.marginLeft && node.style.marginLeft.trim().startsWith('-')) {
|
|
537
|
+
node.style.marginLeft = '0px';
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
catch { }
|
|
542
|
+
};
|
|
543
|
+
const ensureTableWrappers = (root) => {
|
|
544
|
+
try {
|
|
545
|
+
const tables = root.querySelectorAll('table');
|
|
546
|
+
tables.forEach((table) => {
|
|
547
|
+
const parent = table.parentElement;
|
|
548
|
+
if (parent && parent.getAttribute('data-table-wrapper') !== 'true') {
|
|
549
|
+
const wrapper = document.createElement('div');
|
|
550
|
+
wrapper.setAttribute('data-table-wrapper', 'true');
|
|
551
|
+
wrapper.style.overflowX = 'auto';
|
|
552
|
+
wrapper.style.webkitOverflowScrolling = 'touch';
|
|
553
|
+
wrapper.style.width = '100%';
|
|
554
|
+
wrapper.style.maxWidth = '100%';
|
|
555
|
+
wrapper.style.display = 'block';
|
|
556
|
+
// Use insertBefore + appendChild to move element without losing too much state
|
|
557
|
+
// simpler than replaceChild for wrapping
|
|
558
|
+
parent.insertBefore(wrapper, table);
|
|
559
|
+
wrapper.appendChild(table);
|
|
560
|
+
}
|
|
561
|
+
// Always ensure table takes full width
|
|
562
|
+
if (table.style.width !== '100%') {
|
|
563
|
+
table.style.width = '100%';
|
|
564
|
+
}
|
|
565
|
+
// Ensure min-width is set
|
|
566
|
+
if (!table.style.minWidth || table.style.minWidth === '0px') {
|
|
567
|
+
table.style.minWidth = '100%';
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
catch (e) {
|
|
572
|
+
console.error("Error wrapping tables", e);
|
|
573
|
+
}
|
|
574
|
+
};
|
|
350
575
|
const handleInput = () => {
|
|
351
576
|
if (isComposingRef.current)
|
|
352
577
|
return;
|
|
353
578
|
const el = editableRef.current;
|
|
354
|
-
if (!el
|
|
579
|
+
if (!el)
|
|
580
|
+
return;
|
|
581
|
+
// Auto-fix negative margins that might cause visibility issues
|
|
582
|
+
fixNegativeMargins(el);
|
|
583
|
+
// Ensure tables are wrapped for horizontal scrolling
|
|
584
|
+
ensureTableWrappers(el);
|
|
585
|
+
if (!onChange)
|
|
355
586
|
return;
|
|
356
587
|
const html = el.innerHTML;
|
|
357
588
|
if (html !== lastEmittedRef.current) {
|
|
@@ -362,7 +593,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
362
593
|
const buildTableHTML = (rows, cols) => {
|
|
363
594
|
const safeRows = Math.max(1, Math.min(50, Math.floor(rows) || 1));
|
|
364
595
|
const safeCols = Math.max(1, Math.min(20, Math.floor(cols) || 1));
|
|
365
|
-
let html = '<table style="border-collapse:collapse;width:100%;"><tbody>';
|
|
596
|
+
let html = '<div data-table-wrapper="true" style="overflow-x:auto;-webkit-overflow-scrolling:touch;width:100%;max-width:100%;display:block;"><table style="border-collapse:collapse;min-width:100%;"><tbody>';
|
|
366
597
|
for (let r = 0; r < safeRows; r++) {
|
|
367
598
|
html += "<tr>";
|
|
368
599
|
for (let c = 0; c < safeCols; c++) {
|
|
@@ -371,7 +602,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
371
602
|
}
|
|
372
603
|
html += "</tr>";
|
|
373
604
|
}
|
|
374
|
-
html += "</tbody></table>";
|
|
605
|
+
html += "</tbody></table></div>";
|
|
375
606
|
return html;
|
|
376
607
|
};
|
|
377
608
|
const insertTable = () => {
|
|
@@ -400,6 +631,20 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
400
631
|
if (!node || !range)
|
|
401
632
|
return;
|
|
402
633
|
range.insertNode(node);
|
|
634
|
+
// Add resize handles to the new table
|
|
635
|
+
if (node instanceof HTMLTableElement) {
|
|
636
|
+
const tbody = node.querySelector('tbody');
|
|
637
|
+
if (tbody) {
|
|
638
|
+
const rows = Array.from(tbody.querySelectorAll('tr'));
|
|
639
|
+
rows.forEach((row, index) => {
|
|
640
|
+
row.setAttribute('data-row-index', String(index));
|
|
641
|
+
const cells = cellsOfRow(row);
|
|
642
|
+
cells.forEach((cell, cellIndex) => {
|
|
643
|
+
cell.setAttribute('data-col-index', String(cellIndex));
|
|
644
|
+
});
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
}
|
|
403
648
|
// Move caret into first cell
|
|
404
649
|
const firstCell = node.querySelector("td,th");
|
|
405
650
|
if (firstCell)
|
|
@@ -741,14 +986,143 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
741
986
|
applyToggle(fallbackCell);
|
|
742
987
|
}
|
|
743
988
|
};
|
|
744
|
-
|
|
989
|
+
// Table column and row resizing functions
|
|
990
|
+
const getColumnCells = (table, colIndex) => {
|
|
991
|
+
const tbody = table.querySelector('tbody');
|
|
992
|
+
if (!tbody)
|
|
993
|
+
return [];
|
|
994
|
+
const cells = [];
|
|
995
|
+
const rows = Array.from(tbody.querySelectorAll('tr'));
|
|
996
|
+
rows.forEach(row => {
|
|
997
|
+
const rowCells = cellsOfRow(row);
|
|
998
|
+
if (rowCells[colIndex]) {
|
|
999
|
+
cells.push(rowCells[colIndex]);
|
|
1000
|
+
}
|
|
1001
|
+
});
|
|
1002
|
+
return cells;
|
|
1003
|
+
};
|
|
1004
|
+
const startColumnResize = (table, colIndex, clientX) => {
|
|
1005
|
+
const cells = getColumnCells(table, colIndex);
|
|
1006
|
+
if (cells.length === 0)
|
|
1007
|
+
return;
|
|
1008
|
+
const firstCell = cells[0];
|
|
1009
|
+
const currentWidth = firstCell.offsetWidth;
|
|
1010
|
+
// Unlock table width so it can grow
|
|
1011
|
+
table.style.width = "max-content";
|
|
1012
|
+
table.style.minWidth = "100%";
|
|
1013
|
+
tableResizeRef.current = {
|
|
1014
|
+
type: 'column',
|
|
1015
|
+
table,
|
|
1016
|
+
index: colIndex,
|
|
1017
|
+
startPos: clientX,
|
|
1018
|
+
startSize: currentWidth,
|
|
1019
|
+
cells,
|
|
1020
|
+
};
|
|
1021
|
+
document.body.style.cursor = 'col-resize';
|
|
1022
|
+
document.body.style.userSelect = 'none';
|
|
1023
|
+
};
|
|
1024
|
+
const startRowResize = (table, rowIndex, clientY) => {
|
|
1025
|
+
const tbody = table.querySelector('tbody');
|
|
1026
|
+
if (!tbody)
|
|
1027
|
+
return;
|
|
1028
|
+
const rows = Array.from(tbody.querySelectorAll('tr'));
|
|
1029
|
+
const row = rows[rowIndex];
|
|
1030
|
+
if (!row)
|
|
1031
|
+
return;
|
|
1032
|
+
const cells = cellsOfRow(row);
|
|
1033
|
+
const currentHeight = row.offsetHeight;
|
|
1034
|
+
tableResizeRef.current = {
|
|
1035
|
+
type: 'row',
|
|
1036
|
+
table,
|
|
1037
|
+
index: rowIndex,
|
|
1038
|
+
startPos: clientY,
|
|
1039
|
+
startSize: currentHeight,
|
|
1040
|
+
cells,
|
|
1041
|
+
};
|
|
1042
|
+
document.body.style.cursor = 'row-resize';
|
|
1043
|
+
document.body.style.userSelect = 'none';
|
|
1044
|
+
};
|
|
1045
|
+
const handleTableResizeMove = (e) => {
|
|
1046
|
+
const resize = tableResizeRef.current;
|
|
1047
|
+
if (!resize)
|
|
1048
|
+
return;
|
|
1049
|
+
e.preventDefault();
|
|
1050
|
+
const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX;
|
|
1051
|
+
const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY;
|
|
1052
|
+
if (resize.type === 'column') {
|
|
1053
|
+
const delta = clientX - resize.startPos;
|
|
1054
|
+
const newWidth = Math.max(60, resize.startSize + delta);
|
|
1055
|
+
resize.cells.forEach(cell => {
|
|
1056
|
+
cell.style.width = `${newWidth}px`;
|
|
1057
|
+
cell.style.minWidth = `${newWidth}px`;
|
|
1058
|
+
cell.style.maxWidth = `${newWidth}px`;
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
else if (resize.type === 'row') {
|
|
1062
|
+
const delta = clientY - resize.startPos;
|
|
1063
|
+
const newHeight = Math.max(30, resize.startSize + delta);
|
|
1064
|
+
const tbody = resize.table.querySelector('tbody');
|
|
1065
|
+
if (tbody) {
|
|
1066
|
+
const rows = Array.from(tbody.querySelectorAll('tr'));
|
|
1067
|
+
const row = rows[resize.index];
|
|
1068
|
+
if (row) {
|
|
1069
|
+
row.style.height = `${newHeight}px`;
|
|
1070
|
+
resize.cells.forEach(cell => {
|
|
1071
|
+
cell.style.height = `${newHeight}px`;
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
};
|
|
1077
|
+
const handleTableResizeEnd = () => {
|
|
1078
|
+
if (tableResizeRef.current) {
|
|
1079
|
+
tableResizeRef.current = null;
|
|
1080
|
+
document.body.style.cursor = '';
|
|
1081
|
+
document.body.style.userSelect = '';
|
|
1082
|
+
handleInput();
|
|
1083
|
+
}
|
|
1084
|
+
};
|
|
1085
|
+
const addTableResizeHandles = () => {
|
|
1086
|
+
if (!table)
|
|
1087
|
+
return;
|
|
1088
|
+
const el = editableRef.current;
|
|
1089
|
+
if (!el)
|
|
1090
|
+
return;
|
|
1091
|
+
const tables = el.querySelectorAll('table');
|
|
1092
|
+
tables.forEach(tableElem => {
|
|
1093
|
+
const tbody = tableElem.querySelector('tbody');
|
|
1094
|
+
if (!tbody)
|
|
1095
|
+
return;
|
|
1096
|
+
const firstRow = tbody.querySelector('tr');
|
|
1097
|
+
if (firstRow) {
|
|
1098
|
+
const cells = cellsOfRow(firstRow);
|
|
1099
|
+
cells.forEach((cell, index) => {
|
|
1100
|
+
cell.setAttribute('data-col-index', String(index));
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
const rows = Array.from(tbody.querySelectorAll('tr'));
|
|
1104
|
+
rows.forEach((row, index) => {
|
|
1105
|
+
row.setAttribute('data-row-index', String(index));
|
|
1106
|
+
});
|
|
1107
|
+
});
|
|
1108
|
+
};
|
|
1109
|
+
return (_jsxs("div", { style: {
|
|
1110
|
+
border: "1px solid #ddd",
|
|
1111
|
+
borderRadius: 6,
|
|
1112
|
+
width: "100%",
|
|
1113
|
+
maxWidth: "100vw",
|
|
1114
|
+
overflow: "hidden",
|
|
1115
|
+
display: "flex",
|
|
1116
|
+
flexDirection: "column",
|
|
1117
|
+
background: "#fff",
|
|
1118
|
+
boxSizing: "border-box"
|
|
1119
|
+
}, children: [_jsxs("div", { style: {
|
|
745
1120
|
display: "flex",
|
|
746
1121
|
flexWrap: "wrap",
|
|
1122
|
+
maxWidth: "100%",
|
|
747
1123
|
gap: 8,
|
|
748
1124
|
padding: 8,
|
|
749
1125
|
borderBottom: "1px solid #eee",
|
|
750
|
-
background: "#fff",
|
|
751
|
-
color: "#111",
|
|
752
1126
|
position: "sticky",
|
|
753
1127
|
top: 0,
|
|
754
1128
|
zIndex: 1,
|
|
@@ -847,7 +1221,24 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
847
1221
|
borderRadius: 6,
|
|
848
1222
|
background: "#fff",
|
|
849
1223
|
color: "#111",
|
|
850
|
-
}, children: [_jsx("option", { value: "8", children: "8" }), _jsx("option", { value: "9", children: "9" }), _jsx("option", { value: "10", children: "10" }), _jsx("option", { value: "11", children: "11" }), _jsx("option", { value: "12", children: "12" }), _jsx("option", { value: "14", children: "14" }), _jsx("option", { value: "18", children: "18" }), _jsx("option", { value: "24", children: "24" }), _jsx("option", { value: "30", children: "30" }), _jsx("option", { value: "36", children: "36" }), _jsx("option", { value: "48", children: "48" }), _jsx("option", { value: "60", children: "60" }), _jsx("option", { value: "72", children: "72" }), _jsx("option", { value: "96", children: "96" })] }), _jsxs("
|
|
1224
|
+
}, children: [_jsx("option", { value: "", disabled: true, children: "Size" }), _jsx("option", { value: "8", children: "8" }), _jsx("option", { value: "9", children: "9" }), _jsx("option", { value: "10", children: "10" }), _jsx("option", { value: "11", children: "11" }), _jsx("option", { value: "12", children: "12" }), _jsx("option", { value: "14", children: "14" }), _jsx("option", { value: "18", children: "18" }), _jsx("option", { value: "24", children: "24" }), _jsx("option", { value: "30", children: "30" }), _jsx("option", { value: "36", children: "36" }), _jsx("option", { value: "48", children: "48" }), _jsx("option", { value: "60", children: "60" }), _jsx("option", { value: "72", children: "72" }), _jsx("option", { value: "96", children: "96" })] }), _jsxs("select", { value: currentFont, onMouseDown: () => {
|
|
1225
|
+
const sel = window.getSelection();
|
|
1226
|
+
if (sel && sel.rangeCount > 0) {
|
|
1227
|
+
const range = sel.getRangeAt(0);
|
|
1228
|
+
const editor = editableRef.current;
|
|
1229
|
+
if (editor && editor.contains(range.commonAncestorContainer) && !range.collapsed) {
|
|
1230
|
+
savedRangeRef.current = range.cloneRange();
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
}, onChange: (e) => applyFontFamily(e.target.value), title: "Font Family", style: {
|
|
1234
|
+
height: 32,
|
|
1235
|
+
padding: "0 8px",
|
|
1236
|
+
border: "1px solid #e5e7eb",
|
|
1237
|
+
borderRadius: 6,
|
|
1238
|
+
background: "#fff",
|
|
1239
|
+
color: "#111",
|
|
1240
|
+
maxWidth: 100,
|
|
1241
|
+
}, children: [_jsx("option", { value: "", disabled: true, children: "Font" }), fonts.map((f) => (_jsx("option", { value: f.value, children: f.name }, f.value)))] }), _jsx("button", { title: "Text Color", onClick: () => {
|
|
851
1242
|
setColorPickerType('text');
|
|
852
1243
|
setShowColorPicker(true);
|
|
853
1244
|
}, style: {
|
|
@@ -859,16 +1250,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
859
1250
|
background: "#fff",
|
|
860
1251
|
color: "#111",
|
|
861
1252
|
position: "relative",
|
|
862
|
-
}, children:
|
|
863
|
-
position: "absolute",
|
|
864
|
-
bottom: 4,
|
|
865
|
-
left: "50%",
|
|
866
|
-
transform: "translateX(-50%)",
|
|
867
|
-
width: 16,
|
|
868
|
-
height: 3,
|
|
869
|
-
background: "#000",
|
|
870
|
-
borderRadius: 1,
|
|
871
|
-
} })] }), _jsx("button", { title: "Background Color", onClick: () => {
|
|
1253
|
+
}, children: _jsx("span", { style: { fontWeight: 700 }, children: "A" }) }), _jsx("button", { title: "Background Color", onClick: () => {
|
|
872
1254
|
setColorPickerType('background');
|
|
873
1255
|
setShowColorPicker(true);
|
|
874
1256
|
}, style: {
|
|
@@ -879,7 +1261,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
879
1261
|
borderRadius: 6,
|
|
880
1262
|
background: "#fff",
|
|
881
1263
|
color: "#111",
|
|
882
|
-
}, children: _jsx("span", { style: { fontWeight: 700,
|
|
1264
|
+
}, children: _jsx("span", { style: { fontWeight: 700, padding: "2px 4px" }, children: "A" }) }), _jsx("button", { title: "Bulleted list", onClick: () => exec("insertUnorderedList"), style: {
|
|
883
1265
|
height: 32,
|
|
884
1266
|
padding: "0 10px",
|
|
885
1267
|
border: "1px solid #e5e7eb",
|
|
@@ -1082,6 +1464,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
1082
1464
|
padding: 16,
|
|
1083
1465
|
borderRadius: 8,
|
|
1084
1466
|
minWidth: 320,
|
|
1467
|
+
maxWidth: "90vw",
|
|
1085
1468
|
color: "#000",
|
|
1086
1469
|
}, onClick: (e) => e.stopPropagation(), children: [_jsx("div", { style: { fontWeight: 600, marginBottom: 12 }, children: colorPickerType === 'text' ? 'Select Text Color' : 'Select Background Color' }), _jsx("div", { style: {
|
|
1087
1470
|
display: "grid",
|
|
@@ -1235,273 +1618,285 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
1235
1618
|
borderRadius: 4,
|
|
1236
1619
|
background: "#fff",
|
|
1237
1620
|
color: "#000",
|
|
1238
|
-
}, title: sym, children: sym }, i)))] })] }) })), _jsx("div", {
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1621
|
+
}, title: sym, children: sym }, i)))] })] }) })), _jsx("div", { style: {
|
|
1622
|
+
width: "100%",
|
|
1623
|
+
maxWidth: "100%",
|
|
1624
|
+
flex: "1 1 auto",
|
|
1625
|
+
minHeight: typeof minHeight === "number" ? `${minHeight}px` : minHeight,
|
|
1626
|
+
maxHeight: typeof maxHeight === "number" ? `${maxHeight}px` : maxHeight,
|
|
1627
|
+
overflowY: "auto",
|
|
1628
|
+
overflowX: "hidden",
|
|
1629
|
+
boxSizing: "border-box",
|
|
1630
|
+
position: "relative",
|
|
1631
|
+
}, children: _jsx("div", { ref: editableRef, contentEditable: !readOnly, suppressContentEditableWarning: true, onInput: handleInput, onCompositionStart: () => (isComposingRef.current = true), onCompositionEnd: () => {
|
|
1632
|
+
isComposingRef.current = false;
|
|
1633
|
+
handleInput();
|
|
1634
|
+
}, onPaste: (e) => {
|
|
1635
|
+
const items = e.clipboardData?.files;
|
|
1636
|
+
if (media && items && items.length) {
|
|
1637
|
+
const hasImage = Array.from(items).some((f) => f.type.startsWith("image/"));
|
|
1638
|
+
if (hasImage) {
|
|
1639
|
+
e.preventDefault();
|
|
1640
|
+
handleLocalImageFiles(items);
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
}, onDragOver: (e) => {
|
|
1644
|
+
// Allow dragging images within editor and file drops
|
|
1645
|
+
if (draggedImageRef.current ||
|
|
1646
|
+
e.dataTransfer?.types?.includes("Files")) {
|
|
1246
1647
|
e.preventDefault();
|
|
1247
|
-
handleLocalImageFiles(items);
|
|
1248
1648
|
}
|
|
1249
|
-
}
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
}, onDrop: (e) => {
|
|
1257
|
-
// Move existing dragged image inside editor
|
|
1258
|
-
if (draggedImageRef.current) {
|
|
1259
|
-
e.preventDefault();
|
|
1260
|
-
const x = e.clientX;
|
|
1261
|
-
const y = e.clientY;
|
|
1262
|
-
let range = null;
|
|
1263
|
-
// @ts-ignore
|
|
1264
|
-
if (document.caretRangeFromPoint) {
|
|
1649
|
+
}, onDrop: (e) => {
|
|
1650
|
+
// Move existing dragged image inside editor
|
|
1651
|
+
if (draggedImageRef.current) {
|
|
1652
|
+
e.preventDefault();
|
|
1653
|
+
const x = e.clientX;
|
|
1654
|
+
const y = e.clientY;
|
|
1655
|
+
let range = null;
|
|
1265
1656
|
// @ts-ignore
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
const pos = document.caretPositionFromPoint(x, y);
|
|
1270
|
-
if (pos) {
|
|
1271
|
-
range = document.createRange();
|
|
1272
|
-
range.setStart(pos.offsetNode, pos.offset);
|
|
1657
|
+
if (document.caretRangeFromPoint) {
|
|
1658
|
+
// @ts-ignore
|
|
1659
|
+
range = document.caretRangeFromPoint(x, y);
|
|
1273
1660
|
}
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
editableRef.current?.contains(range.commonAncestorContainer)) {
|
|
1280
|
-
// Avoid inserting inside the image itself
|
|
1281
|
-
if (range.startContainer === img || range.endContainer === img)
|
|
1282
|
-
return;
|
|
1283
|
-
// If dropping inside a link, insert right after the link element
|
|
1284
|
-
let container = range.commonAncestorContainer;
|
|
1285
|
-
let linkAncestor = null;
|
|
1286
|
-
let el = container;
|
|
1287
|
-
while (el && el !== editableRef.current) {
|
|
1288
|
-
if (el.tagName === "A") {
|
|
1289
|
-
linkAncestor = el;
|
|
1290
|
-
break;
|
|
1661
|
+
else if (document.caretPositionFromPoint) {
|
|
1662
|
+
const pos = document.caretPositionFromPoint(x, y);
|
|
1663
|
+
if (pos) {
|
|
1664
|
+
range = document.createRange();
|
|
1665
|
+
range.setStart(pos.offsetNode, pos.offset);
|
|
1291
1666
|
}
|
|
1292
|
-
el = el.parentElement;
|
|
1293
|
-
}
|
|
1294
|
-
if (linkAncestor) {
|
|
1295
|
-
linkAncestor.parentElement?.insertBefore(img, linkAncestor.nextSibling);
|
|
1296
1667
|
}
|
|
1297
|
-
|
|
1298
|
-
|
|
1668
|
+
const img = draggedImageRef.current;
|
|
1669
|
+
draggedImageRef.current = null;
|
|
1670
|
+
if (range &&
|
|
1671
|
+
img &&
|
|
1672
|
+
editableRef.current?.contains(range.commonAncestorContainer)) {
|
|
1673
|
+
// Avoid inserting inside the image itself
|
|
1674
|
+
if (range.startContainer === img || range.endContainer === img)
|
|
1675
|
+
return;
|
|
1676
|
+
// If dropping inside a link, insert right after the link element
|
|
1677
|
+
let container = range.commonAncestorContainer;
|
|
1678
|
+
let linkAncestor = null;
|
|
1679
|
+
let el = container;
|
|
1680
|
+
while (el && el !== editableRef.current) {
|
|
1681
|
+
if (el.tagName === "A") {
|
|
1682
|
+
linkAncestor = el;
|
|
1683
|
+
break;
|
|
1684
|
+
}
|
|
1685
|
+
el = el.parentElement;
|
|
1686
|
+
}
|
|
1687
|
+
if (linkAncestor) {
|
|
1688
|
+
linkAncestor.parentElement?.insertBefore(img, linkAncestor.nextSibling);
|
|
1689
|
+
}
|
|
1690
|
+
else {
|
|
1691
|
+
range.insertNode(img);
|
|
1692
|
+
}
|
|
1693
|
+
const r = document.createRange();
|
|
1694
|
+
r.setStartAfter(img);
|
|
1695
|
+
r.collapse(true);
|
|
1696
|
+
safeSelectRange(r);
|
|
1697
|
+
setSelectedImage(img);
|
|
1698
|
+
scheduleImageOverlay();
|
|
1699
|
+
handleInput();
|
|
1299
1700
|
}
|
|
1300
|
-
|
|
1301
|
-
r.setStartAfter(img);
|
|
1302
|
-
r.collapse(true);
|
|
1303
|
-
safeSelectRange(r);
|
|
1304
|
-
setSelectedImage(img);
|
|
1305
|
-
scheduleImageOverlay();
|
|
1306
|
-
handleInput();
|
|
1701
|
+
return;
|
|
1307
1702
|
}
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
const y = e.clientY;
|
|
1315
|
-
let range = null;
|
|
1316
|
-
// @ts-ignore
|
|
1317
|
-
if (document.caretRangeFromPoint) {
|
|
1703
|
+
if (media && e.dataTransfer?.files?.length) {
|
|
1704
|
+
e.preventDefault();
|
|
1705
|
+
// Try to move caret to drop point
|
|
1706
|
+
const x = e.clientX;
|
|
1707
|
+
const y = e.clientY;
|
|
1708
|
+
let range = null;
|
|
1318
1709
|
// @ts-ignore
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
const pos = document.caretPositionFromPoint(x, y);
|
|
1323
|
-
if (pos) {
|
|
1324
|
-
range = document.createRange();
|
|
1325
|
-
range.setStart(pos.offsetNode, pos.offset);
|
|
1710
|
+
if (document.caretRangeFromPoint) {
|
|
1711
|
+
// @ts-ignore
|
|
1712
|
+
range = document.caretRangeFromPoint(x, y);
|
|
1326
1713
|
}
|
|
1714
|
+
else if (document.caretPositionFromPoint) {
|
|
1715
|
+
const pos = document.caretPositionFromPoint(x, y);
|
|
1716
|
+
if (pos) {
|
|
1717
|
+
range = document.createRange();
|
|
1718
|
+
range.setStart(pos.offsetNode, pos.offset);
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
if (range) {
|
|
1722
|
+
const sel = window.getSelection();
|
|
1723
|
+
sel?.removeAllRanges();
|
|
1724
|
+
sel?.addRange(range);
|
|
1725
|
+
}
|
|
1726
|
+
handleLocalImageFiles(e.dataTransfer.files);
|
|
1327
1727
|
}
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1728
|
+
}, onClick: (e) => {
|
|
1729
|
+
const t = e.target;
|
|
1730
|
+
if (t && t.tagName === "IMG") {
|
|
1731
|
+
setSelectedImage(t);
|
|
1732
|
+
scheduleImageOverlay();
|
|
1332
1733
|
}
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
const dt = e.dataTransfer;
|
|
1354
|
-
if (dt && typeof dt.setDragImage === "function") {
|
|
1355
|
-
const ghost = new Image();
|
|
1356
|
-
ghost.src = t.src;
|
|
1357
|
-
ghost.width = Math.min(120, t.width);
|
|
1358
|
-
ghost.height = Math.min(120, t.height);
|
|
1359
|
-
dt.setDragImage(ghost, 10, 10);
|
|
1734
|
+
else {
|
|
1735
|
+
setSelectedImage(null);
|
|
1736
|
+
setImageOverlay(null);
|
|
1737
|
+
}
|
|
1738
|
+
}, onDragStart: (e) => {
|
|
1739
|
+
const t = e.target;
|
|
1740
|
+
if (t && t.tagName === "IMG") {
|
|
1741
|
+
draggedImageRef.current = t;
|
|
1742
|
+
try {
|
|
1743
|
+
e.dataTransfer?.setData("text/plain", "moving-image");
|
|
1744
|
+
e.dataTransfer.effectAllowed = "move";
|
|
1745
|
+
// Provide a subtle drag image
|
|
1746
|
+
const dt = e.dataTransfer;
|
|
1747
|
+
if (dt && typeof dt.setDragImage === "function") {
|
|
1748
|
+
const ghost = new Image();
|
|
1749
|
+
ghost.src = t.src;
|
|
1750
|
+
ghost.width = Math.min(120, t.width);
|
|
1751
|
+
ghost.height = Math.min(120, t.height);
|
|
1752
|
+
dt.setDragImage(ghost, 10, 10);
|
|
1753
|
+
}
|
|
1360
1754
|
}
|
|
1755
|
+
catch { }
|
|
1361
1756
|
}
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1757
|
+
else {
|
|
1758
|
+
draggedImageRef.current = null;
|
|
1759
|
+
}
|
|
1760
|
+
}, onDragEnd: () => {
|
|
1365
1761
|
draggedImageRef.current = null;
|
|
1366
|
-
}
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
el.innerHTML = "<p><br></p>";
|
|
1381
|
-
}
|
|
1382
|
-
}, onKeyDown: (e) => {
|
|
1383
|
-
if (formula &&
|
|
1384
|
-
(e.metaKey || e.ctrlKey) &&
|
|
1385
|
-
String(e.key).toLowerCase() === "m") {
|
|
1386
|
-
e.preventDefault();
|
|
1387
|
-
setShowFormulaDialog(true);
|
|
1388
|
-
return;
|
|
1389
|
-
}
|
|
1390
|
-
// Keep Tab for indentation in lists; otherwise insert 2 spaces
|
|
1391
|
-
if (e.key === "Tab") {
|
|
1392
|
-
e.preventDefault();
|
|
1393
|
-
if (document.queryCommandState("insertUnorderedList") ||
|
|
1394
|
-
document.queryCommandState("insertOrderedList")) {
|
|
1395
|
-
exec(e.shiftKey ? "outdent" : "indent");
|
|
1762
|
+
}, style: {
|
|
1763
|
+
minHeight: "100%",
|
|
1764
|
+
maxWidth: "100%",
|
|
1765
|
+
overflowX: "hidden",
|
|
1766
|
+
padding: "16px",
|
|
1767
|
+
outline: "none",
|
|
1768
|
+
lineHeight: 1.6,
|
|
1769
|
+
boxSizing: "border-box",
|
|
1770
|
+
fontFamily: defaultFont || "inherit",
|
|
1771
|
+
}, "data-placeholder": placeholder, onFocus: (e) => {
|
|
1772
|
+
// Ensure the editor has at least one paragraph to type into
|
|
1773
|
+
const el = e.currentTarget;
|
|
1774
|
+
if (!el.innerHTML || el.innerHTML === "<br>") {
|
|
1775
|
+
el.innerHTML = "<p><br></p>";
|
|
1396
1776
|
}
|
|
1397
|
-
|
|
1398
|
-
|
|
1777
|
+
}, onKeyDown: (e) => {
|
|
1778
|
+
if (formula &&
|
|
1779
|
+
(e.metaKey || e.ctrlKey) &&
|
|
1780
|
+
String(e.key).toLowerCase() === "m") {
|
|
1781
|
+
e.preventDefault();
|
|
1782
|
+
setShowFormulaDialog(true);
|
|
1783
|
+
return;
|
|
1399
1784
|
}
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
cell &&
|
|
1407
|
-
cell.parentElement &&
|
|
1408
|
-
cell.parentElement.parentElement) {
|
|
1409
|
-
const row = cell.parentElement;
|
|
1410
|
-
const tbody = row.parentElement;
|
|
1411
|
-
const cells = Array.from(row.children).filter((c) => c.tagName === "TD" ||
|
|
1412
|
-
c.tagName === "TH");
|
|
1413
|
-
const rows = Array.from(tbody.children);
|
|
1414
|
-
const rIdx = rows.indexOf(row);
|
|
1415
|
-
const cIdx = cells.indexOf(cell);
|
|
1416
|
-
const atStart = (sel?.anchorOffset || 0) === 0;
|
|
1417
|
-
const cellTextLen = (cell.textContent || "").length;
|
|
1418
|
-
const atEnd = (sel?.anchorOffset || 0) >= cellTextLen;
|
|
1419
|
-
let target = null;
|
|
1420
|
-
if (e.key === "ArrowLeft" && atStart && cIdx > 0) {
|
|
1421
|
-
target = row.children[cIdx - 1];
|
|
1422
|
-
}
|
|
1423
|
-
else if (e.key === "ArrowRight" &&
|
|
1424
|
-
atEnd &&
|
|
1425
|
-
cIdx < row.children.length - 1) {
|
|
1426
|
-
target = row.children[cIdx + 1];
|
|
1427
|
-
}
|
|
1428
|
-
else if (e.key === "ArrowUp" && rIdx > 0 && atStart) {
|
|
1429
|
-
target = rows[rIdx - 1].children[cIdx];
|
|
1785
|
+
// Keep Tab for indentation in lists; otherwise insert 2 spaces
|
|
1786
|
+
if (e.key === "Tab") {
|
|
1787
|
+
e.preventDefault();
|
|
1788
|
+
if (document.queryCommandState("insertUnorderedList") ||
|
|
1789
|
+
document.queryCommandState("insertOrderedList")) {
|
|
1790
|
+
exec(e.shiftKey ? "outdent" : "indent");
|
|
1430
1791
|
}
|
|
1431
|
-
else
|
|
1432
|
-
|
|
1433
|
-
atEnd) {
|
|
1434
|
-
target = rows[rIdx + 1].children[cIdx];
|
|
1792
|
+
else {
|
|
1793
|
+
document.execCommand("insertText", false, " ");
|
|
1435
1794
|
}
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1795
|
+
}
|
|
1796
|
+
// Table navigation with arrows inside cells
|
|
1797
|
+
if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.key)) {
|
|
1798
|
+
const sel = window.getSelection();
|
|
1799
|
+
const cell = getClosestCell(sel?.anchorNode || null);
|
|
1800
|
+
if (table &&
|
|
1801
|
+
cell &&
|
|
1802
|
+
cell.parentElement &&
|
|
1803
|
+
cell.parentElement.parentElement) {
|
|
1804
|
+
const row = cell.parentElement;
|
|
1805
|
+
const tbody = row.parentElement;
|
|
1806
|
+
const cells = Array.from(row.children).filter((c) => c.tagName === "TD" ||
|
|
1807
|
+
c.tagName === "TH");
|
|
1808
|
+
const rows = Array.from(tbody.children);
|
|
1809
|
+
const rIdx = rows.indexOf(row);
|
|
1810
|
+
const cIdx = cells.indexOf(cell);
|
|
1811
|
+
const atStart = (sel?.anchorOffset || 0) === 0;
|
|
1812
|
+
const cellTextLen = (cell.textContent || "").length;
|
|
1813
|
+
const atEnd = (sel?.anchorOffset || 0) >= cellTextLen;
|
|
1814
|
+
let target = null;
|
|
1815
|
+
if (e.key === "ArrowLeft" && atStart && cIdx > 0) {
|
|
1816
|
+
target = row.children[cIdx - 1];
|
|
1817
|
+
}
|
|
1818
|
+
else if (e.key === "ArrowRight" &&
|
|
1819
|
+
atEnd &&
|
|
1820
|
+
cIdx < row.children.length - 1) {
|
|
1821
|
+
target = row.children[cIdx + 1];
|
|
1822
|
+
}
|
|
1823
|
+
else if (e.key === "ArrowUp" && rIdx > 0 && atStart) {
|
|
1824
|
+
target = rows[rIdx - 1].children[cIdx];
|
|
1825
|
+
}
|
|
1826
|
+
else if (e.key === "ArrowDown" &&
|
|
1827
|
+
rIdx < rows.length - 1 &&
|
|
1828
|
+
atEnd) {
|
|
1829
|
+
target = rows[rIdx + 1].children[cIdx];
|
|
1830
|
+
}
|
|
1831
|
+
if (target) {
|
|
1832
|
+
e.preventDefault();
|
|
1833
|
+
moveCaretToCell(target, e.key === "ArrowRight" || e.key === "ArrowDown");
|
|
1834
|
+
}
|
|
1439
1835
|
}
|
|
1440
1836
|
}
|
|
1441
|
-
}
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
clearSelectionDecor();
|
|
1446
|
-
return;
|
|
1447
|
-
}
|
|
1448
|
-
const pos = getCellPosition(cell);
|
|
1449
|
-
if (!pos)
|
|
1450
|
-
return;
|
|
1451
|
-
selectingRef.current = { tbody: pos.tbody, start: cell };
|
|
1452
|
-
const onMove = (ev) => {
|
|
1453
|
-
const under = document.elementFromPoint(ev.clientX, ev.clientY);
|
|
1454
|
-
const overCell = getClosestCell(under);
|
|
1455
|
-
const startInfo = selectingRef.current;
|
|
1456
|
-
if (!overCell || !startInfo)
|
|
1837
|
+
}, onMouseDown: (e) => {
|
|
1838
|
+
const cell = getClosestCell(e.target);
|
|
1839
|
+
if (!cell) {
|
|
1840
|
+
clearSelectionDecor();
|
|
1457
1841
|
return;
|
|
1458
|
-
|
|
1459
|
-
const
|
|
1460
|
-
if (!
|
|
1842
|
+
}
|
|
1843
|
+
const pos = getCellPosition(cell);
|
|
1844
|
+
if (!pos)
|
|
1461
1845
|
return;
|
|
1462
|
-
|
|
1463
|
-
const
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
const
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1846
|
+
selectingRef.current = { tbody: pos.tbody, start: cell };
|
|
1847
|
+
const onMove = (ev) => {
|
|
1848
|
+
const under = document.elementFromPoint(ev.clientX, ev.clientY);
|
|
1849
|
+
const overCell = getClosestCell(under);
|
|
1850
|
+
const startInfo = selectingRef.current;
|
|
1851
|
+
if (!overCell || !startInfo)
|
|
1852
|
+
return;
|
|
1853
|
+
const a = getCellPosition(startInfo.start);
|
|
1854
|
+
const b = getCellPosition(overCell);
|
|
1855
|
+
if (!a || !b || a.tbody !== b.tbody)
|
|
1856
|
+
return;
|
|
1857
|
+
const sr = Math.min(a.rIdx, b.rIdx);
|
|
1858
|
+
const sc = Math.min(a.cIdx, b.cIdx);
|
|
1859
|
+
const er = Math.max(a.rIdx, b.rIdx);
|
|
1860
|
+
const ec = Math.max(a.cIdx, b.cIdx);
|
|
1861
|
+
updateSelectionDecor(a.tbody, sr, sc, er, ec);
|
|
1862
|
+
};
|
|
1863
|
+
const onUp = () => {
|
|
1864
|
+
window.removeEventListener("mousemove", onMove);
|
|
1865
|
+
window.removeEventListener("mouseup", onUp);
|
|
1866
|
+
selectingRef.current = null;
|
|
1867
|
+
};
|
|
1868
|
+
window.addEventListener("mousemove", onMove);
|
|
1869
|
+
window.addEventListener("mouseup", onUp);
|
|
1870
|
+
}, onContextMenu: (e) => {
|
|
1871
|
+
const target = e.target;
|
|
1872
|
+
if (target && target.tagName === "IMG") {
|
|
1873
|
+
e.preventDefault();
|
|
1874
|
+
const vw = window.innerWidth;
|
|
1875
|
+
const vh = window.innerHeight;
|
|
1876
|
+
const menuW = 220;
|
|
1877
|
+
const menuH = 200;
|
|
1878
|
+
const x = Math.max(8, Math.min(e.clientX, vw - menuW - 8));
|
|
1879
|
+
const y = Math.max(8, Math.min(e.clientY, vh - menuH - 8));
|
|
1880
|
+
setImageMenu({ x, y, img: target });
|
|
1881
|
+
setTableMenu(null);
|
|
1882
|
+
return;
|
|
1883
|
+
}
|
|
1884
|
+
const cell = getClosestCell(e.target);
|
|
1885
|
+
if (cell) {
|
|
1886
|
+
e.preventDefault();
|
|
1887
|
+
const vw = window.innerWidth;
|
|
1888
|
+
const vh = window.innerHeight;
|
|
1889
|
+
const menuW = 220;
|
|
1890
|
+
const menuH = 300;
|
|
1891
|
+
const x = Math.max(8, Math.min(e.clientX, vw - menuW - 8));
|
|
1892
|
+
const y = Math.max(8, Math.min(e.clientY, vh - menuH - 8));
|
|
1893
|
+
setTableMenu({ x, y, cell });
|
|
1894
|
+
}
|
|
1895
|
+
else {
|
|
1896
|
+
setTableMenu(null);
|
|
1897
|
+
setImageMenu(null);
|
|
1898
|
+
}
|
|
1899
|
+
} }) }), selectedImage && imageOverlay && (_jsxs("div", { style: {
|
|
1505
1900
|
position: "fixed",
|
|
1506
1901
|
left: imageOverlay.left,
|
|
1507
1902
|
top: imageOverlay.top,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "smartrte-react",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
4
4
|
"description": "A powerful, feature-rich Rich Text Editor for React with support for tables, mathematical formulas (LaTeX/KaTeX), and media management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -38,6 +38,16 @@
|
|
|
38
38
|
},
|
|
39
39
|
"author": "Smart RTE Contributors",
|
|
40
40
|
"license": "MIT",
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsc -p tsconfig.json",
|
|
43
|
+
"prepublishOnly": "pnpm run build",
|
|
44
|
+
"dev": "pnpm build",
|
|
45
|
+
"lint": "eslint . || true",
|
|
46
|
+
"test": "vitest run || true",
|
|
47
|
+
"storybook": "storybook dev -p 6006",
|
|
48
|
+
"build-storybook": "storybook build",
|
|
49
|
+
"e2e": "playwright test || true"
|
|
50
|
+
},
|
|
41
51
|
"publishConfig": {
|
|
42
52
|
"access": "public"
|
|
43
53
|
},
|
|
@@ -59,16 +69,5 @@
|
|
|
59
69
|
"@playwright/test": "^1.48.2",
|
|
60
70
|
"react": "18.3.1",
|
|
61
71
|
"react-dom": "18.3.1"
|
|
62
|
-
},
|
|
63
|
-
"scripts": {
|
|
64
|
-
"build": "tsc -p tsconfig.json",
|
|
65
|
-
"build:embed": "vite build",
|
|
66
|
-
"build:all": "pnpm run build && pnpm run build:embed",
|
|
67
|
-
"dev": "pnpm build",
|
|
68
|
-
"lint": "eslint . || true",
|
|
69
|
-
"test": "vitest run || true",
|
|
70
|
-
"storybook": "storybook dev -p 6006",
|
|
71
|
-
"build-storybook": "storybook build",
|
|
72
|
-
"e2e": "playwright test || true"
|
|
73
72
|
}
|
|
74
|
-
}
|
|
73
|
+
}
|