flikkui 0.1.0-beta.6 → 0.1.0-beta.9
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/dist/components/charts/AreaChart/AreaChart.js +80 -38
- package/dist/components/charts/AreaChart/AreaChart.types.d.ts +5 -1
- package/dist/components/charts/BarChart/BarChart.js +51 -77
- package/dist/components/charts/BarChart/BarChart.types.d.ts +1 -0
- package/dist/components/charts/LineChart/LineChart.js +6 -6
- package/dist/components/charts/types/chart.types.d.ts +1 -1
- package/dist/components/charts/utils/chart-validation.js +13 -4
- package/dist/components/charts/utils/color-utils.js +12 -12
- package/dist/components/core/Avatar/Avatar.theme.js +1 -1
- package/dist/components/core/Badge/Badge.theme.js +12 -12
- package/dist/components/core/Breadcrumbs/Breadcrumbs.theme.js +4 -4
- package/dist/components/core/Card/Card.js +2 -2
- package/dist/components/core/Card/Card.theme.js +1 -1
- package/dist/components/core/Divider/Divider.theme.js +1 -1
- package/dist/components/core/Drawer/Drawer.theme.js +4 -4
- package/dist/components/forms/RichTextEditor/RichTextEditor.d.ts +4 -0
- package/dist/components/forms/RichTextEditor/RichTextEditor.js +960 -0
- package/dist/components/forms/RichTextEditor/RichTextEditor.theme.d.ts +10 -0
- package/dist/components/forms/RichTextEditor/RichTextEditor.theme.js +37 -0
- package/dist/components/forms/RichTextEditor/RichTextEditor.types.d.ts +122 -0
- package/dist/components/forms/RichTextEditor/index.d.ts +3 -0
- package/dist/components/forms/index.d.ts +1 -0
- package/dist/components/navigation/NavItem/NavItem.theme.d.ts +1 -1
- package/dist/components/navigation/NavItem/NavItem.theme.js +15 -15
- package/dist/index.js +2 -0
- package/dist/node_modules/@heroicons/react/24/outline/esm/ArrowUturnLeftIcon.js +28 -0
- package/dist/node_modules/@heroicons/react/24/outline/esm/ArrowUturnRightIcon.js +28 -0
- package/dist/node_modules/@heroicons/react/24/outline/esm/Bars3BottomLeftIcon.js +28 -0
- package/dist/node_modules/@heroicons/react/24/outline/esm/Bars3BottomRightIcon.js +28 -0
- package/dist/node_modules/@heroicons/react/24/outline/esm/BoldIcon.js +27 -0
- package/dist/node_modules/@heroicons/react/24/outline/esm/ItalicIcon.js +28 -0
- package/dist/node_modules/@heroicons/react/24/outline/esm/UnderlineIcon.js +28 -0
- package/dist/node_modules/@heroicons/react/24/solid/esm/PaintBrushIcon.js +26 -0
- package/dist/styles.css +2 -0
- package/package.json +9 -4
- package/tailwind.preset.cjs +102 -0
|
@@ -0,0 +1,960 @@
|
|
|
1
|
+
import { __rest, __assign } from '../../../node_modules/tslib/tslib.es6.js';
|
|
2
|
+
import React__default, { useState, useRef, useCallback, useEffect } from 'react';
|
|
3
|
+
import { FormLabel } from '../FormLabel/FormLabel.js';
|
|
4
|
+
import { richTextEditorTheme, colorOptions } from './RichTextEditor.theme.js';
|
|
5
|
+
import { cn } from '../../../utils/cn.js';
|
|
6
|
+
import Button from '../../core/Button/Button.js';
|
|
7
|
+
import ForwardRef from '../../../node_modules/@heroicons/react/24/outline/esm/ArrowUturnRightIcon.js';
|
|
8
|
+
import ForwardRef$1 from '../../../node_modules/@heroicons/react/24/outline/esm/ArrowUturnLeftIcon.js';
|
|
9
|
+
import ForwardRef$2 from '../../../node_modules/@heroicons/react/24/outline/esm/Bars3BottomRightIcon.js';
|
|
10
|
+
import ForwardRef$3 from '../../../node_modules/@heroicons/react/24/outline/esm/Bars3Icon.js';
|
|
11
|
+
import ForwardRef$4 from '../../../node_modules/@heroicons/react/24/outline/esm/Bars3BottomLeftIcon.js';
|
|
12
|
+
import ForwardRef$5 from '../../../node_modules/@heroicons/react/24/solid/esm/PaintBrushIcon.js';
|
|
13
|
+
import ForwardRef$6 from '../../../node_modules/@heroicons/react/24/outline/esm/UnderlineIcon.js';
|
|
14
|
+
import ForwardRef$7 from '../../../node_modules/@heroicons/react/24/outline/esm/ItalicIcon.js';
|
|
15
|
+
import ForwardRef$8 from '../../../node_modules/@heroicons/react/24/outline/esm/BoldIcon.js';
|
|
16
|
+
|
|
17
|
+
var RichTextEditor = React__default.forwardRef(function (_a, ref) {
|
|
18
|
+
var _b = _a.size, size = _b === void 0 ? "md" : _b, _c = _a.state, state = _c === void 0 ? "default" : _c, _d = _a.className, className = _d === void 0 ? "" : _d, label = _a.label, _e = _a.labelClassName, labelClassName = _e === void 0 ? "" : _e, helperText = _a.helperText, _f = _a.helperTextClassName, helperTextClassName = _f === void 0 ? "" : _f, _g = _a.value, value = _g === void 0 ? "" : _g, onChange = _a.onChange, _h = _a.placeholder, placeholder = _h === void 0 ? "Start typing..." : _h, id = _a.id, required = _a.required, _j = _a.minHeight, minHeight = _j === void 0 ? 120 : _j, maxHeight = _a.maxHeight, _k = _a.toolbar, toolbar = _k === void 0 ? {
|
|
19
|
+
show: true,
|
|
20
|
+
position: "top",
|
|
21
|
+
tools: ["bold", "italic", "underline", "separator", "textColor", "separator", "alignLeft", "alignCenter", "alignRight", "separator", "undo", "redo"]
|
|
22
|
+
} : _k; _a.theme; var props = __rest(_a, ["size", "state", "className", "label", "labelClassName", "helperText", "helperTextClassName", "value", "onChange", "placeholder", "id", "required", "minHeight", "maxHeight", "toolbar", "theme"]);
|
|
23
|
+
var _m = useState(false), isFocused = _m[0], setIsFocused = _m[1];
|
|
24
|
+
var _o = useState(false), showToolbar = _o[0], setShowToolbar = _o[1];
|
|
25
|
+
var _p = useState({ top: 0, left: 0 }), toolbarPosition = _p[0], setToolbarPosition = _p[1];
|
|
26
|
+
var _q = useState({
|
|
27
|
+
bold: false,
|
|
28
|
+
italic: false,
|
|
29
|
+
underline: false,
|
|
30
|
+
textAlign: 'left',
|
|
31
|
+
fontSize: '14px',
|
|
32
|
+
textColor: '#000000',
|
|
33
|
+
}), formattingState = _q[0], setFormattingState = _q[1];
|
|
34
|
+
var _r = useState(false), showColorPicker = _r[0], setShowColorPicker = _r[1];
|
|
35
|
+
var _s = useState(['']), history = _s[0], setHistory = _s[1];
|
|
36
|
+
var _t = useState(0), historyIndex = _t[0], setHistoryIndex = _t[1];
|
|
37
|
+
var _u = useState(null), savedSelection = _u[0], setSavedSelection = _u[1];
|
|
38
|
+
var editorRef = useRef(null);
|
|
39
|
+
var toolbarRef = useRef(null);
|
|
40
|
+
var isInteractingRef = useRef(false);
|
|
41
|
+
var savedSelectionRangeRef = useRef(null);
|
|
42
|
+
var editorId = id || "rich-text-editor-".concat(Math.random().toString(36).substring(2, 9));
|
|
43
|
+
// Derived state flags
|
|
44
|
+
var isDisabled = state === "disabled";
|
|
45
|
+
var isInvalid = state === "invalid";
|
|
46
|
+
// Get data attributes for state handling
|
|
47
|
+
var getDataState = function () {
|
|
48
|
+
if (isDisabled)
|
|
49
|
+
return "disabled";
|
|
50
|
+
if (isInvalid)
|
|
51
|
+
return "invalid";
|
|
52
|
+
if (isFocused)
|
|
53
|
+
return "focus";
|
|
54
|
+
return "default";
|
|
55
|
+
};
|
|
56
|
+
// Save current selection position with simplified approach
|
|
57
|
+
var saveSelection = function () {
|
|
58
|
+
try {
|
|
59
|
+
var selection = window.getSelection();
|
|
60
|
+
if (!selection || !editorRef.current || selection.rangeCount === 0 || selection.isCollapsed) {
|
|
61
|
+
setSavedSelection(null);
|
|
62
|
+
savedSelectionRangeRef.current = null;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
var range = selection.getRangeAt(0);
|
|
66
|
+
// Only save if the selection is within our editor
|
|
67
|
+
if (!editorRef.current.contains(range.commonAncestorContainer)) {
|
|
68
|
+
setSavedSelection(null);
|
|
69
|
+
savedSelectionRangeRef.current = null;
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
// Simple approach: save the range directly instead of converting to offsets
|
|
73
|
+
// This avoids complex text offset calculations that can fail
|
|
74
|
+
var clonedRange = range.cloneRange();
|
|
75
|
+
savedSelection = clonedRange; // We'll store the actual range
|
|
76
|
+
// For compatibility with existing code, still calculate offsets as backup
|
|
77
|
+
var preCaretRange = range.cloneRange();
|
|
78
|
+
preCaretRange.selectNodeContents(editorRef.current);
|
|
79
|
+
preCaretRange.setEnd(range.startContainer, range.startOffset);
|
|
80
|
+
var start = preCaretRange.toString().length;
|
|
81
|
+
preCaretRange.setEnd(range.endContainer, range.endOffset);
|
|
82
|
+
var end = preCaretRange.toString().length;
|
|
83
|
+
// Store both offset-based selection and range for compatibility
|
|
84
|
+
setSavedSelection({ start: start, end: end });
|
|
85
|
+
// Store the actual range separately on the ref to avoid type issues
|
|
86
|
+
savedSelectionRangeRef.current = clonedRange;
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
console.warn('Failed to save selection:', error);
|
|
90
|
+
setSavedSelection(null);
|
|
91
|
+
savedSelectionRangeRef.current = null;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
// Restore saved selection with simplified approach
|
|
95
|
+
var restoreSelection = function () {
|
|
96
|
+
var _a;
|
|
97
|
+
if (!savedSelection || !editorRef.current)
|
|
98
|
+
return;
|
|
99
|
+
try {
|
|
100
|
+
var selection = window.getSelection();
|
|
101
|
+
if (!selection)
|
|
102
|
+
return;
|
|
103
|
+
// First try to restore from the saved range if available
|
|
104
|
+
if (savedSelectionRangeRef.current) {
|
|
105
|
+
var savedRange = savedSelectionRangeRef.current;
|
|
106
|
+
// Verify the range is still valid
|
|
107
|
+
try {
|
|
108
|
+
if (editorRef.current.contains(savedRange.commonAncestorContainer)) {
|
|
109
|
+
selection.removeAllRanges();
|
|
110
|
+
selection.addRange(savedRange.cloneRange());
|
|
111
|
+
editorRef.current.focus();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch (rangeError) {
|
|
116
|
+
console.warn('Saved range invalid, falling back to offset method:', rangeError);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// Fallback to offset-based restoration
|
|
120
|
+
if (typeof savedSelection.start === 'number' && typeof savedSelection.end === 'number') {
|
|
121
|
+
var range = document.createRange();
|
|
122
|
+
var walker = document.createTreeWalker(editorRef.current, NodeFilter.SHOW_TEXT);
|
|
123
|
+
var currentPos = 0;
|
|
124
|
+
var startNode = null;
|
|
125
|
+
var endNode = null;
|
|
126
|
+
var startOffset = 0;
|
|
127
|
+
var endOffset = 0;
|
|
128
|
+
var node = void 0;
|
|
129
|
+
while (node = walker.nextNode()) {
|
|
130
|
+
var nodeLength = ((_a = node.textContent) === null || _a === void 0 ? void 0 : _a.length) || 0;
|
|
131
|
+
if (startNode === null && currentPos + nodeLength >= savedSelection.start) {
|
|
132
|
+
startNode = node;
|
|
133
|
+
startOffset = Math.min(savedSelection.start - currentPos, nodeLength);
|
|
134
|
+
}
|
|
135
|
+
if (endNode === null && currentPos + nodeLength >= savedSelection.end) {
|
|
136
|
+
endNode = node;
|
|
137
|
+
endOffset = Math.min(savedSelection.end - currentPos, nodeLength);
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
currentPos += nodeLength;
|
|
141
|
+
}
|
|
142
|
+
if (startNode && endNode) {
|
|
143
|
+
range.setStart(startNode, Math.max(0, startOffset));
|
|
144
|
+
range.setEnd(endNode, Math.max(0, endOffset));
|
|
145
|
+
selection.removeAllRanges();
|
|
146
|
+
selection.addRange(range);
|
|
147
|
+
editorRef.current.focus();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
console.warn('Failed to restore selection:', error);
|
|
153
|
+
// Simple fallback: don't restore selection, let user reselect
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
// Modern text formatting using Selection and Range APIs
|
|
157
|
+
var applyFormatting = useCallback(function (command, value) {
|
|
158
|
+
if (isDisabled || !editorRef.current)
|
|
159
|
+
return;
|
|
160
|
+
var selection = window.getSelection();
|
|
161
|
+
if (!selection || selection.rangeCount === 0)
|
|
162
|
+
return;
|
|
163
|
+
var range = selection.getRangeAt(0);
|
|
164
|
+
try {
|
|
165
|
+
switch (command) {
|
|
166
|
+
case 'bold':
|
|
167
|
+
toggleInlineStyle(range, 'font-weight', 'bold');
|
|
168
|
+
break;
|
|
169
|
+
case 'italic':
|
|
170
|
+
toggleInlineStyle(range, 'font-style', 'italic');
|
|
171
|
+
break;
|
|
172
|
+
case 'underline':
|
|
173
|
+
toggleInlineStyle(range, 'text-decoration', 'underline');
|
|
174
|
+
break;
|
|
175
|
+
case 'textColor':
|
|
176
|
+
if (value)
|
|
177
|
+
applyInlineStyle(range, 'color', value);
|
|
178
|
+
break;
|
|
179
|
+
case 'fontSize':
|
|
180
|
+
if (value)
|
|
181
|
+
applyInlineStyle(range, 'font-size', value);
|
|
182
|
+
break;
|
|
183
|
+
case 'alignLeft':
|
|
184
|
+
case 'alignCenter':
|
|
185
|
+
case 'alignRight':
|
|
186
|
+
case 'alignJustify':
|
|
187
|
+
applyAlignment(command.replace('align', '').toLowerCase());
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
updateFormattingState();
|
|
191
|
+
handleContentChange();
|
|
192
|
+
saveToHistory();
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
console.warn('Formatting command failed:', error);
|
|
196
|
+
}
|
|
197
|
+
}, [isDisabled]);
|
|
198
|
+
// Modern inline style application with span reuse
|
|
199
|
+
var applyInlineStyle = function (range, styleName, styleValue) {
|
|
200
|
+
var _a;
|
|
201
|
+
// Check if we can reuse an existing span
|
|
202
|
+
if (range.collapsed) {
|
|
203
|
+
// For collapsed range (cursor position), look for existing span to modify
|
|
204
|
+
var element = range.commonAncestorContainer;
|
|
205
|
+
if (element && element.nodeType === Node.TEXT_NODE) {
|
|
206
|
+
element = element.parentElement;
|
|
207
|
+
}
|
|
208
|
+
while (element && element !== editorRef.current) {
|
|
209
|
+
if (element.nodeType === Node.ELEMENT_NODE && element.tagName === 'SPAN') {
|
|
210
|
+
var span_1 = element;
|
|
211
|
+
span_1.style.setProperty(styleName, styleValue);
|
|
212
|
+
return; // Style applied to existing span
|
|
213
|
+
}
|
|
214
|
+
element = element.parentElement;
|
|
215
|
+
}
|
|
216
|
+
return; // For cursor position without existing span, do nothing
|
|
217
|
+
}
|
|
218
|
+
// For text selections, check if we can extend existing spans
|
|
219
|
+
var contents = range.extractContents();
|
|
220
|
+
// Check if the extracted contents are already in a single span
|
|
221
|
+
if (contents.childNodes.length === 1 &&
|
|
222
|
+
((_a = contents.firstChild) === null || _a === void 0 ? void 0 : _a.nodeType) === Node.ELEMENT_NODE &&
|
|
223
|
+
contents.firstChild.tagName === 'SPAN') {
|
|
224
|
+
var existingSpan = contents.firstChild;
|
|
225
|
+
existingSpan.style.setProperty(styleName, styleValue);
|
|
226
|
+
range.insertNode(contents);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
// Create new span only if we can't reuse
|
|
230
|
+
var span = document.createElement('span');
|
|
231
|
+
span.style.setProperty(styleName, styleValue);
|
|
232
|
+
try {
|
|
233
|
+
span.appendChild(contents);
|
|
234
|
+
range.insertNode(span);
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
console.warn('Failed to apply inline style:', error);
|
|
238
|
+
// Fallback: insert contents back without styling
|
|
239
|
+
range.insertNode(contents);
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
// Check if a style is currently applied to the selection (only explicit formatting)
|
|
243
|
+
var isStyleApplied = function (styleName, styleValue) {
|
|
244
|
+
var selection = window.getSelection();
|
|
245
|
+
if (!selection || selection.rangeCount === 0)
|
|
246
|
+
return false;
|
|
247
|
+
var range = selection.getRangeAt(0);
|
|
248
|
+
// For collapsed selections (cursor), check if we're inside a formatted element
|
|
249
|
+
if (range.collapsed) {
|
|
250
|
+
var element = range.commonAncestorContainer;
|
|
251
|
+
if (element && element.nodeType === Node.TEXT_NODE) {
|
|
252
|
+
element = element.parentElement;
|
|
253
|
+
}
|
|
254
|
+
// Only check for explicitly applied formatting (spans with inline styles or semantic tags)
|
|
255
|
+
while (element && element !== editorRef.current) {
|
|
256
|
+
if (element.nodeType === Node.ELEMENT_NODE) {
|
|
257
|
+
var htmlElement = element;
|
|
258
|
+
// Check for explicit inline styles first
|
|
259
|
+
if (htmlElement.style) {
|
|
260
|
+
var inlineValue = htmlElement.style.getPropertyValue(styleName);
|
|
261
|
+
if (inlineValue) {
|
|
262
|
+
if (styleName === 'font-weight') {
|
|
263
|
+
return inlineValue === 'bold' || inlineValue === '700' || parseInt(inlineValue) >= 600;
|
|
264
|
+
}
|
|
265
|
+
else if (styleName === 'font-style') {
|
|
266
|
+
return inlineValue === 'italic';
|
|
267
|
+
}
|
|
268
|
+
else if (styleName === 'text-decoration') {
|
|
269
|
+
return inlineValue.includes('underline');
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
return inlineValue === styleValue;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
// Check for semantic HTML elements
|
|
277
|
+
var tagName = htmlElement.tagName.toLowerCase();
|
|
278
|
+
if (styleName === 'font-weight' && (tagName === 'strong' || tagName === 'b')) {
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
if (styleName === 'font-style' && (tagName === 'em' || tagName === 'i')) {
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
if (styleName === 'text-decoration' && tagName === 'u') {
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
element = element && element.nodeType === Node.ELEMENT_NODE ? element.parentElement : null;
|
|
289
|
+
}
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
// For text selections, check if all selected text has the formatting
|
|
293
|
+
var textNodes = [];
|
|
294
|
+
var walker = document.createTreeWalker(range.commonAncestorContainer, NodeFilter.SHOW_TEXT, {
|
|
295
|
+
acceptNode: function (node) {
|
|
296
|
+
return range.intersectsNode(node) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
var node;
|
|
300
|
+
while (node = walker.nextNode()) {
|
|
301
|
+
textNodes.push(node);
|
|
302
|
+
}
|
|
303
|
+
if (textNodes.length === 0)
|
|
304
|
+
return false;
|
|
305
|
+
// All text nodes must be formatted for the style to be considered "applied"
|
|
306
|
+
return textNodes.every(function (textNode) {
|
|
307
|
+
var element = textNode.parentElement;
|
|
308
|
+
while (element && element !== editorRef.current) {
|
|
309
|
+
// Check inline styles
|
|
310
|
+
if (element.style) {
|
|
311
|
+
var inlineValue = element.style.getPropertyValue(styleName);
|
|
312
|
+
if (inlineValue) {
|
|
313
|
+
if (styleName === 'font-weight') {
|
|
314
|
+
return inlineValue === 'bold' || inlineValue === '700' || parseInt(inlineValue) >= 600;
|
|
315
|
+
}
|
|
316
|
+
else if (styleName === 'font-style') {
|
|
317
|
+
return inlineValue === 'italic';
|
|
318
|
+
}
|
|
319
|
+
else if (styleName === 'text-decoration') {
|
|
320
|
+
return inlineValue.includes('underline');
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
return inlineValue === styleValue;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
// Check semantic elements
|
|
328
|
+
var tagName = element.tagName.toLowerCase();
|
|
329
|
+
if (styleName === 'font-weight' && (tagName === 'strong' || tagName === 'b')) {
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
if (styleName === 'font-style' && (tagName === 'em' || tagName === 'i')) {
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
if (styleName === 'text-decoration' && tagName === 'u') {
|
|
336
|
+
return true;
|
|
337
|
+
}
|
|
338
|
+
element = element.parentElement;
|
|
339
|
+
}
|
|
340
|
+
return false;
|
|
341
|
+
});
|
|
342
|
+
};
|
|
343
|
+
// Remove inline style from selection with improved cleanup
|
|
344
|
+
var removeInlineStyle = function (range, styleName) {
|
|
345
|
+
if (range.collapsed) {
|
|
346
|
+
// For cursor position, find and modify the parent span
|
|
347
|
+
var element = range.commonAncestorContainer;
|
|
348
|
+
if (element && element.nodeType === Node.TEXT_NODE) {
|
|
349
|
+
element = element.parentElement;
|
|
350
|
+
}
|
|
351
|
+
while (element && element !== editorRef.current) {
|
|
352
|
+
if (element.nodeType === Node.ELEMENT_NODE && element.tagName === 'SPAN') {
|
|
353
|
+
var span = element;
|
|
354
|
+
span.style.removeProperty(styleName);
|
|
355
|
+
// If span has no more styles and no other attributes, unwrap it
|
|
356
|
+
if (!span.style.cssText && !span.hasAttributes()) {
|
|
357
|
+
var parent_1 = span.parentNode;
|
|
358
|
+
var fragment = document.createDocumentFragment();
|
|
359
|
+
while (span.firstChild) {
|
|
360
|
+
fragment.appendChild(span.firstChild);
|
|
361
|
+
}
|
|
362
|
+
parent_1 === null || parent_1 === void 0 ? void 0 : parent_1.replaceChild(fragment, span);
|
|
363
|
+
}
|
|
364
|
+
break;
|
|
365
|
+
}
|
|
366
|
+
element = element.parentElement;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
else {
|
|
370
|
+
// For selected text, extract and clean up spans
|
|
371
|
+
var contents_1 = range.extractContents();
|
|
372
|
+
// Find all spans in the extracted contents
|
|
373
|
+
var spans = contents_1.querySelectorAll('span');
|
|
374
|
+
spans.forEach(function (span) {
|
|
375
|
+
span.style.removeProperty(styleName);
|
|
376
|
+
// If span has no more styles and no other attributes, unwrap it
|
|
377
|
+
if (!span.style.cssText && !span.hasAttributes()) {
|
|
378
|
+
var parent_2 = span.parentNode;
|
|
379
|
+
var fragment = document.createDocumentFragment();
|
|
380
|
+
while (span.firstChild) {
|
|
381
|
+
fragment.appendChild(span.firstChild);
|
|
382
|
+
}
|
|
383
|
+
if (parent_2) {
|
|
384
|
+
parent_2.replaceChild(fragment, span);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
// Also handle direct span children
|
|
389
|
+
var directSpans = Array.from(contents_1.childNodes).filter(function (node) {
|
|
390
|
+
return node.nodeType === Node.ELEMENT_NODE && node.tagName === 'SPAN';
|
|
391
|
+
});
|
|
392
|
+
directSpans.forEach(function (span) {
|
|
393
|
+
span.style.removeProperty(styleName);
|
|
394
|
+
if (!span.style.cssText && !span.hasAttributes()) {
|
|
395
|
+
var fragment = document.createDocumentFragment();
|
|
396
|
+
while (span.firstChild) {
|
|
397
|
+
fragment.appendChild(span.firstChild);
|
|
398
|
+
}
|
|
399
|
+
contents_1.replaceChild(fragment, span);
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
range.insertNode(contents_1);
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
// Toggle inline style (for bold, italic, underline)
|
|
406
|
+
var toggleInlineStyle = function (range, styleName, styleValue) {
|
|
407
|
+
if (isStyleApplied(styleName, styleValue)) {
|
|
408
|
+
// Remove the style
|
|
409
|
+
removeInlineStyle(range, styleName);
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
// Apply the style
|
|
413
|
+
applyInlineStyle(range, styleName, styleValue);
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
// Apply text alignment to block-level elements
|
|
417
|
+
var applyAlignment = function (alignment) {
|
|
418
|
+
var _a;
|
|
419
|
+
if (!editorRef.current)
|
|
420
|
+
return;
|
|
421
|
+
var selection = window.getSelection();
|
|
422
|
+
if (!selection || selection.rangeCount === 0)
|
|
423
|
+
return;
|
|
424
|
+
var range = selection.getRangeAt(0);
|
|
425
|
+
var blockElement = null;
|
|
426
|
+
// Find existing block element containing the selection
|
|
427
|
+
var element = range.commonAncestorContainer;
|
|
428
|
+
// If we're in a text node, get its parent
|
|
429
|
+
if (element && element.nodeType === Node.TEXT_NODE) {
|
|
430
|
+
element = element.parentElement;
|
|
431
|
+
}
|
|
432
|
+
// Walk up the tree to find a block element
|
|
433
|
+
while (element && element !== editorRef.current) {
|
|
434
|
+
if (element.nodeType === Node.ELEMENT_NODE) {
|
|
435
|
+
var tagName = element.tagName.toLowerCase();
|
|
436
|
+
if (['p', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'li'].includes(tagName)) {
|
|
437
|
+
blockElement = element;
|
|
438
|
+
break;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
element = element.parentElement;
|
|
442
|
+
}
|
|
443
|
+
if (blockElement) {
|
|
444
|
+
// Apply alignment to existing block element
|
|
445
|
+
blockElement.style.textAlign = alignment;
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
// No block element found, need to create one or apply to editor
|
|
449
|
+
if (range.collapsed) {
|
|
450
|
+
// For cursor position, apply to nearest parent or create paragraph
|
|
451
|
+
var parentElement = range.commonAncestorContainer;
|
|
452
|
+
if (parentElement.nodeType === Node.TEXT_NODE) {
|
|
453
|
+
parentElement = parentElement.parentElement;
|
|
454
|
+
}
|
|
455
|
+
if (parentElement === editorRef.current) {
|
|
456
|
+
// We're directly in the editor, create a paragraph
|
|
457
|
+
var p = document.createElement('p');
|
|
458
|
+
p.style.textAlign = alignment;
|
|
459
|
+
// If there's content at cursor, wrap it
|
|
460
|
+
if ((_a = editorRef.current.textContent) === null || _a === void 0 ? void 0 : _a.trim()) {
|
|
461
|
+
// Move cursor content to paragraph
|
|
462
|
+
var restOfContent = range.extractContents();
|
|
463
|
+
p.appendChild(restOfContent);
|
|
464
|
+
}
|
|
465
|
+
range.insertNode(p);
|
|
466
|
+
// Place cursor inside the paragraph
|
|
467
|
+
var newRange = document.createRange();
|
|
468
|
+
newRange.setStart(p, p.childNodes.length);
|
|
469
|
+
newRange.collapse(true);
|
|
470
|
+
selection.removeAllRanges();
|
|
471
|
+
selection.addRange(newRange);
|
|
472
|
+
}
|
|
473
|
+
else {
|
|
474
|
+
// Apply to the parent element if it's reasonable
|
|
475
|
+
parentElement.style.textAlign = alignment;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
else {
|
|
479
|
+
// For text selection, wrap selected content in paragraph
|
|
480
|
+
var p = document.createElement('p');
|
|
481
|
+
p.style.textAlign = alignment;
|
|
482
|
+
try {
|
|
483
|
+
var contents = range.extractContents();
|
|
484
|
+
p.appendChild(contents);
|
|
485
|
+
range.insertNode(p);
|
|
486
|
+
// Restore selection within the new paragraph
|
|
487
|
+
var newRange = document.createRange();
|
|
488
|
+
newRange.selectNodeContents(p);
|
|
489
|
+
selection.removeAllRanges();
|
|
490
|
+
selection.addRange(newRange);
|
|
491
|
+
}
|
|
492
|
+
catch (error) {
|
|
493
|
+
console.warn('Failed to wrap selection in paragraph:', error);
|
|
494
|
+
// Fallback: apply alignment to editor itself
|
|
495
|
+
editorRef.current.style.textAlign = alignment;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
// Update formatting state based on current selection
|
|
501
|
+
var updateFormattingState = useCallback(function () {
|
|
502
|
+
if (!editorRef.current || isDisabled)
|
|
503
|
+
return;
|
|
504
|
+
var selection = window.getSelection();
|
|
505
|
+
if (!selection || selection.rangeCount === 0) {
|
|
506
|
+
// No selection, reset to default state
|
|
507
|
+
setFormattingState({
|
|
508
|
+
bold: false,
|
|
509
|
+
italic: false,
|
|
510
|
+
underline: false,
|
|
511
|
+
textAlign: 'left',
|
|
512
|
+
fontSize: '14px',
|
|
513
|
+
textColor: '#000000',
|
|
514
|
+
});
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
// Use the same logic as isStyleApplied for consistency
|
|
518
|
+
setFormattingState({
|
|
519
|
+
bold: isStyleApplied('font-weight', 'bold'),
|
|
520
|
+
italic: isStyleApplied('font-style', 'italic'),
|
|
521
|
+
underline: isStyleApplied('text-decoration', 'underline'),
|
|
522
|
+
textAlign: getCurrentAlignment(),
|
|
523
|
+
fontSize: '14px', // We'll keep this simple for now
|
|
524
|
+
textColor: getCurrentColor(),
|
|
525
|
+
});
|
|
526
|
+
}, [isDisabled]);
|
|
527
|
+
// Get current text alignment (only explicit alignment)
|
|
528
|
+
var getCurrentAlignment = function () {
|
|
529
|
+
var selection = window.getSelection();
|
|
530
|
+
if (!selection || selection.rangeCount === 0)
|
|
531
|
+
return 'left';
|
|
532
|
+
var range = selection.getRangeAt(0);
|
|
533
|
+
var element = range.commonAncestorContainer;
|
|
534
|
+
// If we're in a text node, get its parent
|
|
535
|
+
if (element && element.nodeType === Node.TEXT_NODE) {
|
|
536
|
+
element = element.parentElement;
|
|
537
|
+
}
|
|
538
|
+
// Find the block element with explicit text-align style
|
|
539
|
+
while (element && element !== editorRef.current) {
|
|
540
|
+
if (element.nodeType === Node.ELEMENT_NODE) {
|
|
541
|
+
var htmlElement = element;
|
|
542
|
+
var tagName = htmlElement.tagName.toLowerCase();
|
|
543
|
+
if (['p', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
|
|
544
|
+
// Check for explicit inline style first
|
|
545
|
+
var inlineAlign = htmlElement.style.textAlign;
|
|
546
|
+
if (inlineAlign) {
|
|
547
|
+
return inlineAlign;
|
|
548
|
+
}
|
|
549
|
+
// If no inline style, this block has no explicit alignment
|
|
550
|
+
break;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
element = element.parentElement;
|
|
554
|
+
}
|
|
555
|
+
return 'left'; // Default alignment
|
|
556
|
+
};
|
|
557
|
+
// Get current text color
|
|
558
|
+
var getCurrentColor = function () {
|
|
559
|
+
var selection = window.getSelection();
|
|
560
|
+
if (!selection || selection.rangeCount === 0)
|
|
561
|
+
return '#000000';
|
|
562
|
+
var range = selection.getRangeAt(0);
|
|
563
|
+
var element = range.commonAncestorContainer;
|
|
564
|
+
if (element && element.nodeType === Node.TEXT_NODE) {
|
|
565
|
+
element = element.parentElement;
|
|
566
|
+
}
|
|
567
|
+
while (element && element !== editorRef.current) {
|
|
568
|
+
var computedStyle = window.getComputedStyle(element);
|
|
569
|
+
var color = computedStyle.color;
|
|
570
|
+
if (color && color !== 'inherit') {
|
|
571
|
+
return color;
|
|
572
|
+
}
|
|
573
|
+
element = element ? element.parentElement : null;
|
|
574
|
+
}
|
|
575
|
+
return '#000000';
|
|
576
|
+
};
|
|
577
|
+
// Handle text selection for floating toolbar with improved UX
|
|
578
|
+
var handleSelection = useCallback(function () {
|
|
579
|
+
// If interacting with toolbar (mousedown on a toolbar control), do not change toolbar visibility or saved selection
|
|
580
|
+
if (isInteractingRef.current)
|
|
581
|
+
return;
|
|
582
|
+
if (!editorRef.current || isDisabled)
|
|
583
|
+
return;
|
|
584
|
+
var selection = window.getSelection();
|
|
585
|
+
if (!selection || selection.rangeCount === 0) {
|
|
586
|
+
// Update formatting state even when no selection
|
|
587
|
+
updateFormattingState();
|
|
588
|
+
if (toolbar.position === 'floating') {
|
|
589
|
+
// Only hide toolbar if we don't have a saved selection
|
|
590
|
+
if (!savedSelection) {
|
|
591
|
+
setShowToolbar(false);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
var range = selection.getRangeAt(0);
|
|
597
|
+
// Ensure selection is within our editor
|
|
598
|
+
if (!editorRef.current.contains(range.commonAncestorContainer)) {
|
|
599
|
+
updateFormattingState();
|
|
600
|
+
if (toolbar.position === 'floating') {
|
|
601
|
+
setShowToolbar(false);
|
|
602
|
+
setSavedSelection(null);
|
|
603
|
+
}
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
// Always update formatting state when there's a valid selection
|
|
607
|
+
updateFormattingState();
|
|
608
|
+
if (toolbar.position === 'floating') {
|
|
609
|
+
// If selection is collapsed (just cursor), hide toolbar unless it's from clicking in toolbar
|
|
610
|
+
if (selection.isCollapsed) {
|
|
611
|
+
// Check if we're clicking in the toolbar area - if so, keep it visible
|
|
612
|
+
if (savedSelection && showToolbar) {
|
|
613
|
+
return; // Keep toolbar visible
|
|
614
|
+
}
|
|
615
|
+
setShowToolbar(false);
|
|
616
|
+
setSavedSelection(null);
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
var rect = range.getBoundingClientRect();
|
|
620
|
+
var editorRect = editorRef.current.getBoundingClientRect();
|
|
621
|
+
if (rect.width > 0 && rect.height > 0) {
|
|
622
|
+
// Save the selection before showing toolbar
|
|
623
|
+
saveSelection();
|
|
624
|
+
// Position toolbar above the selection, centered
|
|
625
|
+
var toolbarWidth = 200; // Approximate toolbar width
|
|
626
|
+
var left = Math.max(10, Math.min(rect.left - editorRect.left + (rect.width / 2) - (toolbarWidth / 2), editorRect.width - toolbarWidth - 10));
|
|
627
|
+
setToolbarPosition({
|
|
628
|
+
top: rect.top - editorRect.top - 50,
|
|
629
|
+
left: left,
|
|
630
|
+
});
|
|
631
|
+
setShowToolbar(true);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}, [isDisabled, toolbar.position, updateFormattingState, savedSelection, showToolbar]);
|
|
635
|
+
// Handle content change
|
|
636
|
+
var handleContentChange = useCallback(function () {
|
|
637
|
+
if (!editorRef.current || !onChange)
|
|
638
|
+
return;
|
|
639
|
+
var content = editorRef.current.innerHTML;
|
|
640
|
+
onChange(content);
|
|
641
|
+
}, [onChange]);
|
|
642
|
+
// Save content to history for undo/redo
|
|
643
|
+
var saveToHistory = useCallback(function () {
|
|
644
|
+
if (!editorRef.current)
|
|
645
|
+
return;
|
|
646
|
+
var content = editorRef.current.innerHTML;
|
|
647
|
+
setHistory(function (prev) {
|
|
648
|
+
var newHistory = prev.slice(0, historyIndex + 1);
|
|
649
|
+
newHistory.push(content);
|
|
650
|
+
return newHistory.slice(-50); // Keep last 50 changes
|
|
651
|
+
});
|
|
652
|
+
setHistoryIndex(function (prev) { return Math.min(prev + 1, 49); });
|
|
653
|
+
}, [historyIndex]);
|
|
654
|
+
// Undo functionality
|
|
655
|
+
var handleUndo = useCallback(function (event) {
|
|
656
|
+
if (event) {
|
|
657
|
+
event.preventDefault();
|
|
658
|
+
event.stopPropagation();
|
|
659
|
+
}
|
|
660
|
+
if (historyIndex > 0 && editorRef.current) {
|
|
661
|
+
var newIndex = historyIndex - 1;
|
|
662
|
+
setHistoryIndex(newIndex);
|
|
663
|
+
editorRef.current.innerHTML = history[newIndex];
|
|
664
|
+
handleContentChange();
|
|
665
|
+
}
|
|
666
|
+
}, [historyIndex, history, handleContentChange]);
|
|
667
|
+
// Redo functionality
|
|
668
|
+
var handleRedo = useCallback(function (event) {
|
|
669
|
+
if (event) {
|
|
670
|
+
event.preventDefault();
|
|
671
|
+
event.stopPropagation();
|
|
672
|
+
}
|
|
673
|
+
if (historyIndex < history.length - 1 && editorRef.current) {
|
|
674
|
+
var newIndex = historyIndex + 1;
|
|
675
|
+
setHistoryIndex(newIndex);
|
|
676
|
+
editorRef.current.innerHTML = history[newIndex];
|
|
677
|
+
handleContentChange();
|
|
678
|
+
}
|
|
679
|
+
}, [historyIndex, history, handleContentChange]);
|
|
680
|
+
// Toolbar button handlers with selection preservation for floating toolbar
|
|
681
|
+
var handleBold = function (event) {
|
|
682
|
+
if (event) {
|
|
683
|
+
event.preventDefault();
|
|
684
|
+
event.stopPropagation();
|
|
685
|
+
}
|
|
686
|
+
if (toolbar.position === 'floating') {
|
|
687
|
+
restoreSelection();
|
|
688
|
+
}
|
|
689
|
+
applyFormatting('bold');
|
|
690
|
+
};
|
|
691
|
+
var handleItalic = function (event) {
|
|
692
|
+
if (event) {
|
|
693
|
+
event.preventDefault();
|
|
694
|
+
event.stopPropagation();
|
|
695
|
+
}
|
|
696
|
+
if (toolbar.position === 'floating') {
|
|
697
|
+
restoreSelection();
|
|
698
|
+
}
|
|
699
|
+
applyFormatting('italic');
|
|
700
|
+
};
|
|
701
|
+
var handleUnderline = function (event) {
|
|
702
|
+
if (event) {
|
|
703
|
+
event.preventDefault();
|
|
704
|
+
event.stopPropagation();
|
|
705
|
+
}
|
|
706
|
+
if (toolbar.position === 'floating') {
|
|
707
|
+
restoreSelection();
|
|
708
|
+
}
|
|
709
|
+
applyFormatting('underline');
|
|
710
|
+
};
|
|
711
|
+
var handleTextColor = function (color, event) {
|
|
712
|
+
if (event) {
|
|
713
|
+
event.preventDefault();
|
|
714
|
+
event.stopPropagation();
|
|
715
|
+
}
|
|
716
|
+
if (toolbar.position === 'floating') {
|
|
717
|
+
restoreSelection();
|
|
718
|
+
}
|
|
719
|
+
applyFormatting('textColor', color);
|
|
720
|
+
setShowColorPicker(false);
|
|
721
|
+
};
|
|
722
|
+
var handleAlign = function (alignment, event) {
|
|
723
|
+
if (event) {
|
|
724
|
+
event.preventDefault();
|
|
725
|
+
event.stopPropagation();
|
|
726
|
+
}
|
|
727
|
+
if (toolbar.position === 'floating') {
|
|
728
|
+
restoreSelection();
|
|
729
|
+
}
|
|
730
|
+
applyFormatting("align".concat(alignment.charAt(0).toUpperCase() + alignment.slice(1)));
|
|
731
|
+
};
|
|
732
|
+
// Set initial content
|
|
733
|
+
useEffect(function () {
|
|
734
|
+
if (editorRef.current && value !== editorRef.current.innerHTML) {
|
|
735
|
+
editorRef.current.innerHTML = value;
|
|
736
|
+
setHistory([value]);
|
|
737
|
+
setHistoryIndex(0);
|
|
738
|
+
}
|
|
739
|
+
}, [value]);
|
|
740
|
+
// Add event listeners
|
|
741
|
+
useEffect(function () {
|
|
742
|
+
var editor = editorRef.current;
|
|
743
|
+
if (!editor)
|
|
744
|
+
return;
|
|
745
|
+
var selectionTimeout;
|
|
746
|
+
var handleSelectionChange = function () {
|
|
747
|
+
// Debounce selection changes to prevent excessive updates
|
|
748
|
+
clearTimeout(selectionTimeout);
|
|
749
|
+
selectionTimeout = setTimeout(handleSelection, 10);
|
|
750
|
+
};
|
|
751
|
+
var handleInput = function () {
|
|
752
|
+
handleContentChange();
|
|
753
|
+
saveToHistory();
|
|
754
|
+
};
|
|
755
|
+
document.addEventListener('selectionchange', handleSelectionChange);
|
|
756
|
+
editor.addEventListener('input', handleInput);
|
|
757
|
+
// Remove duplicate updateFormattingState calls since handleSelection now handles them
|
|
758
|
+
// editor.addEventListener('keyup', updateFormattingState);
|
|
759
|
+
// editor.addEventListener('mouseup', updateFormattingState);
|
|
760
|
+
// Toolbar interaction handlers
|
|
761
|
+
var handleToolbarMouseDown = function () {
|
|
762
|
+
// Mark that we are interacting with the toolbar (prevents selectionchange from clearing selection)
|
|
763
|
+
isInteractingRef.current = true;
|
|
764
|
+
};
|
|
765
|
+
var handleToolbarMouseUp = function () {
|
|
766
|
+
// Release the interaction lock shortly after mouseup to allow click handlers to run
|
|
767
|
+
setTimeout(function () {
|
|
768
|
+
isInteractingRef.current = false;
|
|
769
|
+
}, 0);
|
|
770
|
+
};
|
|
771
|
+
// Attach to toolbar container if present
|
|
772
|
+
if (toolbarRef.current) {
|
|
773
|
+
toolbarRef.current.addEventListener('mousedown', handleToolbarMouseDown);
|
|
774
|
+
toolbarRef.current.addEventListener('mouseup', handleToolbarMouseUp);
|
|
775
|
+
}
|
|
776
|
+
return function () {
|
|
777
|
+
clearTimeout(selectionTimeout);
|
|
778
|
+
document.removeEventListener('selectionchange', handleSelectionChange);
|
|
779
|
+
editor.removeEventListener('input', handleInput);
|
|
780
|
+
if (toolbarRef.current) {
|
|
781
|
+
toolbarRef.current.removeEventListener('mousedown', handleToolbarMouseDown);
|
|
782
|
+
toolbarRef.current.removeEventListener('mouseup', handleToolbarMouseUp);
|
|
783
|
+
}
|
|
784
|
+
};
|
|
785
|
+
}, [handleSelection, handleContentChange, saveToHistory]);
|
|
786
|
+
// Handle clicks outside editor and toolbar with improved behavior
|
|
787
|
+
useEffect(function () {
|
|
788
|
+
var handleClickOutside = function (event) {
|
|
789
|
+
var _a, _b;
|
|
790
|
+
var target = event.target;
|
|
791
|
+
var isInEditor = (_a = editorRef.current) === null || _a === void 0 ? void 0 : _a.contains(target);
|
|
792
|
+
var isInToolbar = (_b = toolbarRef.current) === null || _b === void 0 ? void 0 : _b.contains(target);
|
|
793
|
+
if (!isInEditor && !isInToolbar) {
|
|
794
|
+
// Clicked outside both editor and toolbar
|
|
795
|
+
if (toolbar.position === 'floating') {
|
|
796
|
+
// Delay hiding to allow for double-clicks and other interactions
|
|
797
|
+
setTimeout(function () {
|
|
798
|
+
setShowToolbar(false);
|
|
799
|
+
setSavedSelection(null);
|
|
800
|
+
}, 100);
|
|
801
|
+
}
|
|
802
|
+
setShowColorPicker(false);
|
|
803
|
+
}
|
|
804
|
+
else if (isInToolbar) {
|
|
805
|
+
// Clicked in toolbar - close color picker unless clicking on color-related elements
|
|
806
|
+
var isColorRelated = target === null || target === void 0 ? void 0 : target.closest('[data-tool="textColor"]');
|
|
807
|
+
if (!isColorRelated) {
|
|
808
|
+
setShowColorPicker(false);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
else if (isInEditor) {
|
|
812
|
+
// Clicked in editor - close color picker but keep toolbar if we have a selection
|
|
813
|
+
setShowColorPicker(false);
|
|
814
|
+
}
|
|
815
|
+
};
|
|
816
|
+
// Use mousedown instead of click for faster response
|
|
817
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
818
|
+
return function () { return document.removeEventListener('mousedown', handleClickOutside); };
|
|
819
|
+
}, [toolbar.position]);
|
|
820
|
+
// Render toolbar tool
|
|
821
|
+
var renderToolbarTool = function (tool, index) {
|
|
822
|
+
var isActive = formattingState[tool];
|
|
823
|
+
switch (tool) {
|
|
824
|
+
case 'bold':
|
|
825
|
+
return (React__default.createElement(Button, { key: index, size: "sm", variant: "ghost", iconOnly: true, className: cn(richTextEditorTheme.toolbarButtonStyle), "data-state": isActive ? 'active' : 'inactive', onMouseDown: function (e) {
|
|
826
|
+
// Prevent selection from collapsing and restore before click
|
|
827
|
+
e.preventDefault();
|
|
828
|
+
restoreSelection();
|
|
829
|
+
}, onClick: function (e) { return handleBold(e); }, disabled: isDisabled, "aria-label": "Bold", "aria-pressed": isActive ? "true" : "false" },
|
|
830
|
+
React__default.createElement(ForwardRef$8, { className: "w-4 h-4" })));
|
|
831
|
+
case 'italic':
|
|
832
|
+
return (React__default.createElement(Button, { key: index, size: "sm", variant: "ghost", iconOnly: true, className: cn(richTextEditorTheme.toolbarButtonStyle), "data-state": isActive ? 'active' : 'inactive', onMouseDown: function (e) {
|
|
833
|
+
e.preventDefault();
|
|
834
|
+
restoreSelection();
|
|
835
|
+
}, onClick: function (e) { return handleItalic(e); }, disabled: isDisabled, "aria-label": "Italic", "aria-pressed": isActive ? "true" : "false" },
|
|
836
|
+
React__default.createElement(ForwardRef$7, { className: "w-4 h-4" })));
|
|
837
|
+
case 'underline':
|
|
838
|
+
return (React__default.createElement(Button, { key: index, size: "sm", variant: "ghost", iconOnly: true, className: cn(richTextEditorTheme.toolbarButtonStyle), "data-state": isActive ? 'active' : 'inactive', onMouseDown: function (e) {
|
|
839
|
+
e.preventDefault();
|
|
840
|
+
restoreSelection();
|
|
841
|
+
}, onClick: function (e) { return handleUnderline(e); }, disabled: isDisabled, "aria-label": "Underline", "aria-pressed": isActive ? "true" : "false" },
|
|
842
|
+
React__default.createElement(ForwardRef$6, { className: "w-4 h-4" })));
|
|
843
|
+
case 'textColor':
|
|
844
|
+
return (React__default.createElement("div", { key: index, className: "relative", "data-tool": "textColor" },
|
|
845
|
+
React__default.createElement(Button, { size: "sm", variant: "ghost", iconOnly: true, className: cn(richTextEditorTheme.toolbarButtonStyle), onClick: function (e) {
|
|
846
|
+
e.preventDefault();
|
|
847
|
+
e.stopPropagation();
|
|
848
|
+
// For floating toolbar, restore selection when opening color picker
|
|
849
|
+
if (toolbar.position === 'floating' && !showColorPicker) {
|
|
850
|
+
restoreSelection();
|
|
851
|
+
}
|
|
852
|
+
setShowColorPicker(!showColorPicker);
|
|
853
|
+
}, disabled: isDisabled, "aria-label": "Text Color", "aria-expanded": showColorPicker },
|
|
854
|
+
React__default.createElement(ForwardRef$5, { className: "w-4 h-4" })),
|
|
855
|
+
showColorPicker && (React__default.createElement("div", { className: cn(richTextEditorTheme.dropdownStyle, richTextEditorTheme.colorPickerStyle) }, colorOptions.map(function (color) { return (React__default.createElement("button", { key: color.value, className: richTextEditorTheme.colorSwatchStyle, style: { backgroundColor: color.value }, onClick: function (e) {
|
|
856
|
+
e.preventDefault();
|
|
857
|
+
e.stopPropagation();
|
|
858
|
+
handleTextColor(color.value, e);
|
|
859
|
+
}, onMouseDown: function (e) {
|
|
860
|
+
// Prevent the mousedown from clearing the selection
|
|
861
|
+
e.preventDefault();
|
|
862
|
+
}, "aria-label": color.label, title: color.label })); })))));
|
|
863
|
+
case 'alignLeft':
|
|
864
|
+
return (React__default.createElement(Button, { key: index, size: "sm", variant: "ghost", iconOnly: true, className: cn(richTextEditorTheme.toolbarButtonStyle), "data-state": formattingState.textAlign === 'left' ? 'active' : 'inactive', onMouseDown: function (e) {
|
|
865
|
+
e.preventDefault();
|
|
866
|
+
restoreSelection();
|
|
867
|
+
}, onClick: function (e) { return handleAlign('left', e); }, disabled: isDisabled, "aria-label": "Align Left", "aria-pressed": formattingState.textAlign === 'left' ? "true" : "false" },
|
|
868
|
+
React__default.createElement(ForwardRef$4, { className: "w-4 h-4" })));
|
|
869
|
+
case 'alignCenter':
|
|
870
|
+
return (React__default.createElement(Button, { key: index, size: "sm", variant: "ghost", iconOnly: true, className: cn(richTextEditorTheme.toolbarButtonStyle), "data-state": formattingState.textAlign === 'center' ? 'active' : 'inactive', onMouseDown: function (e) {
|
|
871
|
+
e.preventDefault();
|
|
872
|
+
restoreSelection();
|
|
873
|
+
}, onClick: function (e) { return handleAlign('center', e); }, disabled: isDisabled, "aria-label": "Align Center", "aria-pressed": formattingState.textAlign === 'center' ? "true" : "false" },
|
|
874
|
+
React__default.createElement(ForwardRef$3, { className: "w-4 h-4" })));
|
|
875
|
+
case 'alignRight':
|
|
876
|
+
return (React__default.createElement(Button, { key: index, size: "sm", variant: "ghost", iconOnly: true, className: cn(richTextEditorTheme.toolbarButtonStyle), "data-state": formattingState.textAlign === 'right' ? 'active' : 'inactive', onMouseDown: function (e) {
|
|
877
|
+
e.preventDefault();
|
|
878
|
+
restoreSelection();
|
|
879
|
+
}, onClick: function (e) { return handleAlign('right', e); }, disabled: isDisabled, "aria-label": "Align Right", "aria-pressed": formattingState.textAlign === 'right' ? "true" : "false" },
|
|
880
|
+
React__default.createElement(ForwardRef$2, { className: "w-4 h-4" })));
|
|
881
|
+
case 'undo':
|
|
882
|
+
return (React__default.createElement(Button, { key: index, size: "sm", variant: "ghost", iconOnly: true, className: cn(richTextEditorTheme.toolbarButtonStyle), onClick: function (e) { return handleUndo(e); }, disabled: isDisabled || historyIndex <= 0, "aria-label": "Undo" },
|
|
883
|
+
React__default.createElement(ForwardRef$1, { className: "w-4 h-4" })));
|
|
884
|
+
case 'redo':
|
|
885
|
+
return (React__default.createElement(Button, { key: index, size: "sm", variant: "ghost", iconOnly: true, className: cn(richTextEditorTheme.toolbarButtonStyle), onClick: function (e) { return handleRedo(e); }, disabled: isDisabled || historyIndex >= history.length - 1, "aria-label": "Redo" },
|
|
886
|
+
React__default.createElement(ForwardRef, { className: "w-4 h-4" })));
|
|
887
|
+
case 'separator':
|
|
888
|
+
return React__default.createElement("div", { key: index, className: richTextEditorTheme.separatorStyle });
|
|
889
|
+
default:
|
|
890
|
+
return null;
|
|
891
|
+
}
|
|
892
|
+
};
|
|
893
|
+
// Render toolbar
|
|
894
|
+
var renderToolbar = function () {
|
|
895
|
+
if (!toolbar.show || !toolbar.tools)
|
|
896
|
+
return null;
|
|
897
|
+
var toolbarClass = cn(richTextEditorTheme.toolbarStyle, toolbar.className);
|
|
898
|
+
var toolbarContent = (React__default.createElement("div", { ref: toolbarRef, className: toolbarClass }, toolbar.tools.map(function (tool, index) { return renderToolbarTool(tool, index); })));
|
|
899
|
+
if (toolbar.position === 'floating') {
|
|
900
|
+
return showToolbar ? (React__default.createElement("div", { className: "bg-white/70 backdrop-blur-sm shadow-2xl rounded-xl border border-[var(--color-border)] overflow-hidden", style: {
|
|
901
|
+
position: 'absolute',
|
|
902
|
+
top: toolbarPosition.top,
|
|
903
|
+
left: toolbarPosition.left,
|
|
904
|
+
zIndex: 50,
|
|
905
|
+
} }, toolbarContent)) : null;
|
|
906
|
+
}
|
|
907
|
+
return toolbarContent;
|
|
908
|
+
};
|
|
909
|
+
return (React__default.createElement("div", __assign({ className: cn(richTextEditorTheme.baseStyle, richTextEditorTheme.sizes[size], className) }, props),
|
|
910
|
+
label && (React__default.createElement("div", { className: "mb-1" },
|
|
911
|
+
React__default.createElement(FormLabel, { htmlFor: editorId, className: labelClassName, state: state, required: required }, label))),
|
|
912
|
+
React__default.createElement("div", { className: "relative" },
|
|
913
|
+
React__default.createElement("div", { className: cn(richTextEditorTheme.containerStyle), "data-state": getDataState() },
|
|
914
|
+
toolbar.position === 'top' && renderToolbar(),
|
|
915
|
+
React__default.createElement("div", { ref: function (el) {
|
|
916
|
+
if (el) {
|
|
917
|
+
editorRef.current = el;
|
|
918
|
+
if (typeof ref === 'function') {
|
|
919
|
+
ref(el);
|
|
920
|
+
}
|
|
921
|
+
else if (ref) {
|
|
922
|
+
ref.current = el;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}, id: editorId, contentEditable: !isDisabled, className: cn(richTextEditorTheme.editorStyle), "data-state": getDataState(), style: {
|
|
926
|
+
minHeight: "".concat(minHeight, "px"),
|
|
927
|
+
maxHeight: maxHeight ? "".concat(maxHeight, "px") : undefined,
|
|
928
|
+
}, "data-placeholder": placeholder, onFocus: function () {
|
|
929
|
+
setIsFocused(true);
|
|
930
|
+
updateFormattingState();
|
|
931
|
+
}, onBlur: function (e) {
|
|
932
|
+
var _a;
|
|
933
|
+
setIsFocused(false);
|
|
934
|
+
// Only hide floating toolbar if focus is not moving to the toolbar
|
|
935
|
+
var relatedTarget = e.relatedTarget;
|
|
936
|
+
var isMovingToToolbar = relatedTarget && ((_a = toolbarRef.current) === null || _a === void 0 ? void 0 : _a.contains(relatedTarget));
|
|
937
|
+
if (toolbar.position === 'floating' && !isMovingToToolbar) {
|
|
938
|
+
// Delay hiding to allow toolbar interactions
|
|
939
|
+
setTimeout(function () {
|
|
940
|
+
var _a;
|
|
941
|
+
// Check if toolbar is still focused
|
|
942
|
+
var activeElement = document.activeElement;
|
|
943
|
+
var isToolbarActive = activeElement && ((_a = toolbarRef.current) === null || _a === void 0 ? void 0 : _a.contains(activeElement));
|
|
944
|
+
if (!isToolbarActive) {
|
|
945
|
+
setShowToolbar(false);
|
|
946
|
+
setSavedSelection(null);
|
|
947
|
+
}
|
|
948
|
+
}, 150);
|
|
949
|
+
}
|
|
950
|
+
else if (toolbar.position === 'top') {
|
|
951
|
+
setShowToolbar(false);
|
|
952
|
+
}
|
|
953
|
+
setShowColorPicker(false);
|
|
954
|
+
}, "aria-invalid": isInvalid, "aria-required": required, role: "textbox", "aria-multiline": "true", "aria-label": label || "Rich text editor", suppressContentEditableWarning: true })),
|
|
955
|
+
toolbar.position === 'floating' && renderToolbar()),
|
|
956
|
+
helperText && (React__default.createElement("div", { className: cn("mt-1 text-sm text-text-muted", helperTextClassName) }, helperText))));
|
|
957
|
+
});
|
|
958
|
+
RichTextEditor.displayName = "RichTextEditor";
|
|
959
|
+
|
|
960
|
+
export { RichTextEditor, RichTextEditor as default };
|