flikkui 0.1.0-beta.8 → 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.
Files changed (36) hide show
  1. package/dist/components/charts/AreaChart/AreaChart.js +80 -38
  2. package/dist/components/charts/AreaChart/AreaChart.types.d.ts +5 -1
  3. package/dist/components/charts/BarChart/BarChart.js +51 -77
  4. package/dist/components/charts/BarChart/BarChart.types.d.ts +1 -0
  5. package/dist/components/charts/LineChart/LineChart.js +6 -6
  6. package/dist/components/charts/types/chart.types.d.ts +1 -1
  7. package/dist/components/charts/utils/chart-validation.js +13 -4
  8. package/dist/components/charts/utils/color-utils.js +12 -12
  9. package/dist/components/core/Avatar/Avatar.theme.js +1 -1
  10. package/dist/components/core/Badge/Badge.theme.js +12 -12
  11. package/dist/components/core/Breadcrumbs/Breadcrumbs.theme.js +4 -4
  12. package/dist/components/core/Card/Card.js +2 -2
  13. package/dist/components/core/Card/Card.theme.js +1 -1
  14. package/dist/components/core/Divider/Divider.theme.js +1 -1
  15. package/dist/components/core/Drawer/Drawer.theme.js +4 -4
  16. package/dist/components/forms/RichTextEditor/RichTextEditor.d.ts +4 -0
  17. package/dist/components/forms/RichTextEditor/RichTextEditor.js +960 -0
  18. package/dist/components/forms/RichTextEditor/RichTextEditor.theme.d.ts +10 -0
  19. package/dist/components/forms/RichTextEditor/RichTextEditor.theme.js +37 -0
  20. package/dist/components/forms/RichTextEditor/RichTextEditor.types.d.ts +122 -0
  21. package/dist/components/forms/RichTextEditor/index.d.ts +3 -0
  22. package/dist/components/forms/index.d.ts +1 -0
  23. package/dist/components/navigation/NavItem/NavItem.theme.d.ts +1 -1
  24. package/dist/components/navigation/NavItem/NavItem.theme.js +15 -15
  25. package/dist/index.js +2 -0
  26. package/dist/node_modules/@heroicons/react/24/outline/esm/ArrowUturnLeftIcon.js +28 -0
  27. package/dist/node_modules/@heroicons/react/24/outline/esm/ArrowUturnRightIcon.js +28 -0
  28. package/dist/node_modules/@heroicons/react/24/outline/esm/Bars3BottomLeftIcon.js +28 -0
  29. package/dist/node_modules/@heroicons/react/24/outline/esm/Bars3BottomRightIcon.js +28 -0
  30. package/dist/node_modules/@heroicons/react/24/outline/esm/BoldIcon.js +27 -0
  31. package/dist/node_modules/@heroicons/react/24/outline/esm/ItalicIcon.js +28 -0
  32. package/dist/node_modules/@heroicons/react/24/outline/esm/UnderlineIcon.js +28 -0
  33. package/dist/node_modules/@heroicons/react/24/solid/esm/PaintBrushIcon.js +26 -0
  34. package/dist/styles.css +1 -1
  35. package/package.json +1 -1
  36. package/src/shadcn-compat.css +0 -80
@@ -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 };