@topconsultnpm/sdkui-react-beta 6.13.68 → 6.13.69
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.
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
3
3
|
import HtmlEditor, { Toolbar, Item } from 'devextreme-react/html-editor';
|
|
4
|
-
import
|
|
4
|
+
import ReactDOM from 'react-dom';
|
|
5
5
|
import { SDKUI_Localizator } from '../../helper';
|
|
6
|
+
import TMVilViewer from '../base/TMVilViewer';
|
|
6
7
|
const TMHtmlEditor = (props) => {
|
|
7
8
|
const { width = "100%", height = "100%", initialMarkup = "", mentionsConfig, onValueChanged, validationItems = [], isEditorEnabled: isEditorEnabledProp = false, toolbarMode = 'compact', autoFocus = false } = props;
|
|
8
9
|
// Ref for HtmlEditor instance
|
|
@@ -11,6 +12,84 @@ const TMHtmlEditor = (props) => {
|
|
|
11
12
|
const [isEditorEnabled, setIsEditorEnabled] = useState(false);
|
|
12
13
|
// State to hold HTML markup
|
|
13
14
|
const [markup, setMarkup] = useState(initialMarkup);
|
|
15
|
+
// State to track the position (x, y coordinates) where the custom context menu should be displayed.
|
|
16
|
+
const [contextMenuPos, setContextMenuPos] = useState(null);
|
|
17
|
+
// State to indicate whether a paste operation is currently in progress
|
|
18
|
+
const [isPasting, setIsPasting] = useState(false);
|
|
19
|
+
// Function to read text from the clipboard, format it, and paste into the Quill editor
|
|
20
|
+
const pasteFromClipboard = async () => {
|
|
21
|
+
try {
|
|
22
|
+
// Set flag to indicate paste operation is in progress
|
|
23
|
+
setIsPasting(true);
|
|
24
|
+
// Read plain text from the clipboard asynchronously
|
|
25
|
+
let text = await navigator.clipboard.readText();
|
|
26
|
+
// Removes potentially dangerous HTML tags (script, iframe, embed, etc.) from the pasted content
|
|
27
|
+
text = text.replace(/<(script|iframe|embed|object|link|style|img|video|audio|svg|form|input|button|textarea|select|pre|function)[^>]*>/gi, '');
|
|
28
|
+
// Replace line breaks with HTML paragraph tags to keep formatting
|
|
29
|
+
text = text.replace(/\r\n/g, '</p><p>').replace(/^/, '<p>').replace(/$/, '</p>');
|
|
30
|
+
// Access the Quill editor instance from the HtmlEditor component reference
|
|
31
|
+
const quill = editorRef.current?.instance()?.getQuillInstance();
|
|
32
|
+
if (quill) {
|
|
33
|
+
// Get the current cursor position or selection range in the editor
|
|
34
|
+
const range = quill.getSelection(true);
|
|
35
|
+
if (range) {
|
|
36
|
+
if (range.length > 0) {
|
|
37
|
+
// If there is a text selection, delete the selected text from the editor
|
|
38
|
+
quill.deleteText(range.index, range.length);
|
|
39
|
+
// Use a short delay before pasting the new content, this ensures the deletion is fully processed before inserting HTML
|
|
40
|
+
setTimeout(() => {
|
|
41
|
+
// Paste the sanitized HTML content at the cursor's original position
|
|
42
|
+
quill.clipboard.dangerouslyPasteHTML(range.index, text);
|
|
43
|
+
}, 100);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
// Paste the formatted HTML content at the current cursor position
|
|
47
|
+
setTimeout(() => quill.clipboard.dangerouslyPasteHTML(range.index, text), 100);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
// Log any errors that might occur during clipboard access or pasting
|
|
54
|
+
console.warn('Clipboard paste failed:', err);
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
// Reset paste state after a short delay to restore UI state
|
|
58
|
+
setTimeout(() => setIsPasting(false), 500);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
const handleKeyDown = (e) => {
|
|
63
|
+
// Check if the pressed keys correspond to paste (Ctrl or Cmd + V)
|
|
64
|
+
const isPaste = (e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'v';
|
|
65
|
+
if (isPaste) {
|
|
66
|
+
// Prevent the default paste action to handle custom pasting
|
|
67
|
+
e.preventDefault();
|
|
68
|
+
// Call the custom paste function to handle clipboard text
|
|
69
|
+
pasteFromClipboard();
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
// Add event listener on component mount
|
|
73
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
74
|
+
return () => {
|
|
75
|
+
// Cleanup event listener on component unmount
|
|
76
|
+
window.removeEventListener('keydown', handleKeyDown);
|
|
77
|
+
};
|
|
78
|
+
}, []);
|
|
79
|
+
const handleContextMenu = (e) => {
|
|
80
|
+
e.preventDefault();
|
|
81
|
+
setContextMenuPos({ x: e.clientX, y: e.clientY });
|
|
82
|
+
};
|
|
83
|
+
const handleClickOutside = () => {
|
|
84
|
+
if (contextMenuPos)
|
|
85
|
+
setContextMenuPos(null);
|
|
86
|
+
};
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
document.addEventListener('click', handleClickOutside);
|
|
89
|
+
return () => {
|
|
90
|
+
document.removeEventListener('click', handleClickOutside);
|
|
91
|
+
};
|
|
92
|
+
}, [contextMenuPos]);
|
|
14
93
|
useEffect(() => {
|
|
15
94
|
if (markup === initialMarkup)
|
|
16
95
|
return;
|
|
@@ -38,10 +117,28 @@ const TMHtmlEditor = (props) => {
|
|
|
38
117
|
}, [autoFocus]);
|
|
39
118
|
// Memoized check for validation errors
|
|
40
119
|
const hasValidationErrors = useMemo(() => {
|
|
41
|
-
return validationItems && validationItems.length > 0;
|
|
120
|
+
return !isPasting && validationItems && validationItems.length > 0;
|
|
42
121
|
}, [validationItems]);
|
|
43
|
-
|
|
122
|
+
// Handler function triggered by the context menu's "Paste" action
|
|
123
|
+
const handlePaste = async () => {
|
|
124
|
+
// Use the same clipboard reading and pasting logic as in keyboard shortcut
|
|
125
|
+
await pasteFromClipboard();
|
|
126
|
+
// Close the custom context menu after pasting
|
|
127
|
+
setContextMenuPos(null);
|
|
128
|
+
};
|
|
129
|
+
return (_jsxs("div", { style: { height: hasValidationErrors ? `calc(${height} - 30px)` : "100%", width: width }, onContextMenu: handleContextMenu, children: [_jsx("div", { className: "custom-mentions-wrapper", style: { borderWidth: '1px', borderStyle: 'solid', borderColor: hasValidationErrors ? "red" : "#e0e0e0 #e0e0e0 #616161", width: "100%", height: "100%" }, children: _jsx(HtmlEditor, { ref: editorRef, placeholder: SDKUI_Localizator.TypeAMessage + "...", width: "100%", height: "100%", value: markup, onValueChange: onValueChangeCallback, mentions: mentionsConfig, style: { overflow: 'hidden', outline: "none", fontSize: '1rem' }, children: isEditorEnabled && (toolbarMode === 'compact' ?
|
|
44
130
|
_jsxs(Toolbar, { multiline: false, children: [_jsx(Item, { name: "undo" }), _jsx(Item, { name: "redo" }), _jsx(Item, { name: "separator" }), _jsx(Item, { name: "bold" }), _jsx(Item, { name: "italic" }), _jsx(Item, { name: "strike" }), _jsx(Item, { name: "underline" }), _jsx(Item, { name: "separator" }), _jsx(Item, { name: "orderedList" }), _jsx(Item, { name: "bulletList" })] }) :
|
|
45
|
-
_jsxs(Toolbar, { children: [_jsx(Item, { name: "undo" }), _jsx(Item, { name: "redo" }), _jsx(Item, { name: "separator" }), _jsx(Item, { name: "separator" }), _jsx(Item, { name: "bold" }), _jsx(Item, { name: "italic" }), _jsx(Item, { name: "strike" }), _jsx(Item, { name: "underline" }), _jsx(Item, { name: "separator" }), _jsx(Item, { name: "alignLeft" }), _jsx(Item, { name: "alignCenter" }), _jsx(Item, { name: "alignRight" }), _jsx(Item, { name: "alignJustify" }), _jsx(Item, { name: "separator" }), _jsx(Item, { name: "color" }), _jsx(Item, { name: "background" }), _jsx(Item, { name: "separator" }), _jsx(Item, { name: "link" }), _jsx(Item, { name: "image" }), _jsx(Item, { name: "separator" })] })) }) }), _jsx("div", { style: { width: "100%", height:
|
|
131
|
+
_jsxs(Toolbar, { children: [_jsx(Item, { name: "undo" }), _jsx(Item, { name: "redo" }), _jsx(Item, { name: "separator" }), _jsx(Item, { name: "separator" }), _jsx(Item, { name: "bold" }), _jsx(Item, { name: "italic" }), _jsx(Item, { name: "strike" }), _jsx(Item, { name: "underline" }), _jsx(Item, { name: "separator" }), _jsx(Item, { name: "alignLeft" }), _jsx(Item, { name: "alignCenter" }), _jsx(Item, { name: "alignRight" }), _jsx(Item, { name: "alignJustify" }), _jsx(Item, { name: "separator" }), _jsx(Item, { name: "color" }), _jsx(Item, { name: "background" }), _jsx(Item, { name: "separator" }), _jsx(Item, { name: "link" }), _jsx(Item, { name: "image" }), _jsx(Item, { name: "separator" })] })) }) }), !isPasting && validationItems.length > 0 && (_jsx("div", { style: { width: "100%", height: "30px" }, children: _jsx(TMVilViewer, { vil: validationItems }) })), contextMenuPos &&
|
|
132
|
+
ReactDOM.createPortal(_jsx("div", { style: {
|
|
133
|
+
position: 'fixed',
|
|
134
|
+
top: contextMenuPos.y,
|
|
135
|
+
left: contextMenuPos.x,
|
|
136
|
+
background: 'white',
|
|
137
|
+
border: '1px solid #ccc',
|
|
138
|
+
boxShadow: '0 2px 6px rgba(0,0,0,0.2)',
|
|
139
|
+
zIndex: 9999,
|
|
140
|
+
padding: '5px 10px',
|
|
141
|
+
cursor: 'pointer'
|
|
142
|
+
}, onClick: handlePaste, children: "Paste" }), document.body)] }));
|
|
46
143
|
};
|
|
47
144
|
export default TMHtmlEditor;
|