kamotive_ui 2.3.26 → 4.5.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,44 +1,16 @@
1
1
  import React, { useCallback, useEffect, useRef, useState } from 'react';
2
- import { createRoot } from 'react-dom/client';
3
- import { init, exec } from 'pell';
4
- import { IconAttachToString, IconBoldToString, IconBulletlistToString, IconHeader2ToString, IconItalicToString, IconStrikethroughToString, IconUnderlineToString, IconSubmit, IconClose, IconRedoToString, IconUndoToString, } from '../../Icons';
5
- import { Typography } from '../Typography/Typography';
6
2
  import classNames from 'classnames';
7
- import { AttachedFilesPreview } from '../AttachedFilesPreview/AttachedFilesPreview';
8
3
  import styles from './TextEditor.module.css';
4
+ import { IconClose, IconSubmit, IconRedoToString, IconUndoToString, IconBoldToString, IconItalicToString, IconAttachToString, IconHeader2ToString, IconUnderlineToString, IconBulletlistToString, IconStrikethroughToString, } from '../../Icons';
5
+ import { Typography } from '../Typography/Typography';
9
6
  import { IconButton } from '../IconButton/IconButton';
7
+ import { AttachedFilesPreview } from '../AttachedFilesPreview/AttachedFilesPreview';
8
+ import { Extension } from '@tiptap/core';
9
+ import StarterKit from '@tiptap/starter-kit';
10
+ import Underline from '@tiptap/extension-underline';
11
+ import { EditorContent, useEditor, useEditorState } from '@tiptap/react';
10
12
  const ACCEPTED_FILE_TYPES = 'image/*,audio/*,video/*,.doc,.docx,.html,.htm,.odt,.pdf,.xls,.xlsx,.ods,.ppt,.pptx,.txt,.zip,.djvu';
11
13
  const MAX_FILE_SIZE = 2147483648; // 2 ГБ
12
- const getSafeSelection = () => {
13
- try {
14
- const selection = window.getSelection();
15
- if (!selection || selection.rangeCount === 0) {
16
- return null;
17
- }
18
- return selection;
19
- }
20
- catch (error) {
21
- return null;
22
- }
23
- };
24
- const getElementFromRange = (range) => {
25
- let node = range.startContainer;
26
- if (node.nodeType === Node.ELEMENT_NODE) {
27
- const element = node;
28
- if (element.childNodes.length > 0) {
29
- const childIndex = range.startOffset > 0 ? range.startOffset - 1 : 0;
30
- let lastChild = element.childNodes[childIndex];
31
- while (lastChild && lastChild.hasChildNodes()) {
32
- lastChild = lastChild.lastChild;
33
- }
34
- return lastChild.nodeType === Node.ELEMENT_NODE
35
- ? lastChild
36
- : lastChild.parentElement;
37
- }
38
- return element;
39
- }
40
- return node.parentElement;
41
- };
42
14
  export const formatFileSize = (bytes, lng) => {
43
15
  if (!bytes || bytes === 0) {
44
16
  return lng === 'ru' || (lng === null || lng === void 0 ? void 0 : lng.includes('ru')) ? '0 Байт' : '0 Bytes';
@@ -80,250 +52,142 @@ const parseFileSize = (sizeStr) => {
80
52
  const unit = match[2];
81
53
  return value * (units[unit] || 0);
82
54
  };
55
+ const Hotkeys = Extension.create({
56
+ name: 'customHotkeys',
57
+ addKeyboardShortcuts() {
58
+ return {
59
+ 'Mod-b': () => this.editor.chain().focus().toggleBold().run(),
60
+ 'Mod-i': () => this.editor.chain().focus().toggleItalic().run(),
61
+ 'Mod-u': () => this.editor.chain().focus().toggleUnderline().run(),
62
+ 'Mod-z': () => this.editor.chain().focus().undo().run(),
63
+ 'Mod-Shift-z': () => this.editor.chain().focus().redo().run(),
64
+ 'Mod-y': () => this.editor.chain().focus().redo().run(),
65
+ };
66
+ },
67
+ });
83
68
  export const TextEditor = ({ defaultValue, attachedFiles, label, onSubmit, onCancel, onDelete, error, helperText, isEditMode, canAttachFiles = true, maxFileCount = 5, maxFileSize = '1Гб', required, className, lng = 'en', }) => {
84
- const editorRef = useRef(null);
85
- const pellRef = useRef(null);
69
+ var _a;
86
70
  const uploaderRef = useRef(null);
87
- const submitButtonRef = useRef(null);
88
- const cancelButtonRef = useRef(null);
89
- const buttonRefs = useRef({});
90
- const redoContentRef = useRef('');
91
- const [editor, setEditor] = useState(null);
92
71
  const [editorHtml, setEditorHtml] = useState(defaultValue || '');
93
72
  const [temporaryFiles, setTemporaryFiles] = useState(attachedFiles !== null && attachedFiles !== void 0 ? attachedFiles : []);
94
73
  const tempFilesRef = useRef(attachedFiles !== null && attachedFiles !== void 0 ? attachedFiles : []);
95
74
  useEffect(() => {
96
75
  tempFilesRef.current = temporaryFiles;
97
76
  }, [temporaryFiles]);
98
- const [activeStates, setActiveStates] = useState({
99
- bold: false,
100
- italic: false,
101
- underline: false,
102
- strikethrough: false,
103
- heading2: false,
104
- olist: false,
77
+ const editor = useEditor({
78
+ extensions: [
79
+ StarterKit.configure({
80
+ history: false,
81
+ }),
82
+ Underline,
83
+ Hotkeys,
84
+ ],
85
+ content: defaultValue || '',
86
+ editorProps: {
87
+ attributes: {
88
+ class: styles.pellContent,
89
+ style: 'overflow: visible; height: auto; outline: none;',
90
+ },
91
+ },
92
+ onUpdate: ({ editor }) => {
93
+ const cleanHtml = editor.getHTML().replace(/\u200B/g, '');
94
+ setEditorHtml(cleanHtml);
95
+ },
105
96
  });
106
- const checkFormatting = useCallback((element, tagNames) => {
107
- let current = element;
108
- while (current && current !== (editor === null || editor === void 0 ? void 0 : editor.content)) {
109
- if (tagNames.includes(current.tagName)) {
110
- return true;
111
- }
112
- current = current.parentElement;
113
- }
114
- return false;
115
- }, [editor]);
116
- const hasStyle = useCallback((element, property, values) => {
117
- let current = element;
118
- while (current && current !== (editor === null || editor === void 0 ? void 0 : editor.content)) {
119
- const computedStyle = window.getComputedStyle(current);
120
- const styleValue = computedStyle.getPropertyValue(property);
121
- if (values.some((value) => styleValue.includes(value))) {
122
- return true;
123
- }
124
- current = current.parentElement;
125
- }
126
- return false;
127
- }, [editor]);
128
- const isFormatActive = useCallback((element, tagNames, styleProperty, styleValues) => {
129
- return (checkFormatting(element, tagNames) ||
130
- (styleProperty && styleValues ? hasStyle(element, styleProperty, styleValues) : false));
131
- }, [checkFormatting, hasStyle]);
132
- const setCursorToEnd = () => {
133
- var _a;
134
- try {
135
- const content = (_a = pellRef.current) === null || _a === void 0 ? void 0 : _a.content;
136
- if (!content)
137
- return;
138
- content.focus();
139
- const selection = window.getSelection();
140
- if (!selection)
141
- return;
142
- const range = document.createRange();
143
- range.selectNodeContents(content);
144
- range.collapse(false);
145
- selection.removeAllRanges();
146
- selection.addRange(range);
147
- content.scrollTop = content.scrollHeight;
148
- }
149
- catch (error) {
150
- console.warn('Error setting cursor to end:', error);
151
- }
152
- };
153
- const getSafeRange = () => {
154
- try {
155
- const selection = getSafeSelection();
156
- if (!selection || selection.rangeCount === 0) {
157
- setCursorToEnd();
158
- const newSelection = getSafeSelection();
159
- if (!newSelection || newSelection.rangeCount === 0) {
160
- return null;
161
- }
162
- return newSelection.getRangeAt(0);
163
- }
164
- return selection.getRangeAt(0);
165
- }
166
- catch (error) {
167
- setCursorToEnd();
168
- return null;
169
- }
170
- };
171
- const updateButtonStates = useCallback((states) => {
172
- Object.entries(states).forEach(([command, isActive]) => {
173
- const button = buttonRefs.current[command];
174
- if (button) {
175
- button.classList.toggle(styles.pellButtonSelected, isActive);
176
- }
177
- });
178
- }, []);
179
- const updateActiveStates = useCallback(() => {
180
- var _a;
181
- const contentElement = (_a = pellRef.current) === null || _a === void 0 ? void 0 : _a.content;
182
- if (!contentElement)
183
- return;
184
- const selection = window.getSelection();
185
- if (!(selection === null || selection === void 0 ? void 0 : selection.rangeCount)) {
186
- const defaultStates = {
187
- bold: false,
188
- italic: false,
189
- underline: false,
190
- strikethrough: false,
191
- heading2: false,
192
- olist: false,
193
- };
194
- setActiveStates(defaultStates);
195
- updateButtonStates(defaultStates);
196
- return;
197
- }
198
- if (!contentElement.contains(selection.anchorNode)) {
199
- return;
200
- }
201
- const range = selection.getRangeAt(0);
202
- const element = getElementFromRange(range);
203
- if (!element)
204
- return;
205
- const newStates = {
206
- bold: isFormatActive(element, ['B', 'STRONG'], 'font-weight', ['bold', '700', '800', '900']),
207
- italic: isFormatActive(element, ['I', 'EM'], 'font-style', ['italic']),
208
- underline: isFormatActive(element, ['U'], 'text-decoration', ['underline']),
209
- strikethrough: isFormatActive(element, ['S', 'STRIKE', 'DEL'], 'text-decoration', ['line-through']),
210
- heading2: checkFormatting(element, ['H2']),
211
- olist: checkFormatting(element, ['OL']) || !!element.closest('ol'),
212
- };
213
- setActiveStates(newStates);
214
- updateButtonStates(newStates);
215
- }, [isFormatActive, checkFormatting, updateButtonStates]);
216
- const toggleHeading2 = () => {
217
- var _a;
218
- const selection = window.getSelection();
219
- if (!selection || selection.rangeCount === 0)
220
- return;
221
- const range = getSafeRange();
222
- if (!range)
97
+ const editorState = useEditorState({
98
+ editor,
99
+ selector: ({ editor }) => ({
100
+ bold: editor.isActive('bold'),
101
+ italic: editor.isActive('italic'),
102
+ underline: editor.isActive('underline'),
103
+ strikethrough: editor.isActive('strike'),
104
+ heading2: editor.isActive('heading', { level: 2 }),
105
+ olist: editor.isActive('orderedList'),
106
+ }),
107
+ });
108
+ const applyAction = (action) => {
109
+ if (!editor)
223
110
  return;
224
- let h2Element = null;
225
- if (range.startContainer.nodeType === Node.TEXT_NODE) {
226
- const parentElement = range.startContainer.parentElement;
227
- h2Element = (parentElement === null || parentElement === void 0 ? void 0 : parentElement.closest('h2')) || null;
228
- }
229
- else {
230
- const container = range.startContainer;
231
- if (container.querySelector) {
232
- h2Element = container.querySelector('h2');
233
- }
234
- if (!h2Element && container.tagName === 'H2') {
235
- h2Element = container;
236
- }
237
- if (!h2Element && container.nodeType === Node.ELEMENT_NODE) {
238
- h2Element = container.closest('h2');
239
- }
240
- }
241
- if (h2Element) {
242
- const div = document.createElement('div');
243
- div.innerHTML = h2Element.innerHTML;
244
- const rangeOffset = range.startOffset;
245
- const textNode = range.startContainer;
246
- (_a = h2Element.parentNode) === null || _a === void 0 ? void 0 : _a.replaceChild(div, h2Element);
247
- try {
248
- const newRange = document.createRange();
249
- if (textNode.nodeType === Node.TEXT_NODE && div.contains(textNode)) {
250
- newRange.setStart(textNode, rangeOffset);
251
- newRange.setEnd(textNode, rangeOffset);
252
- }
253
- else {
254
- newRange.setStart(div, 0);
255
- newRange.setEnd(div, 0);
256
- }
257
- selection.removeAllRanges();
258
- selection.addRange(newRange);
259
- }
260
- catch (e) {
261
- const newRange = document.createRange();
262
- newRange.selectNodeContents(div);
263
- newRange.collapse(false);
264
- selection.removeAllRanges();
265
- selection.addRange(newRange);
266
- }
267
- }
268
- else {
269
- exec('formatBlock', 'h2');
270
- }
111
+ action(editor);
112
+ editor.commands.focus();
271
113
  };
272
- const toggleBulletList = () => {
273
- var _a;
274
- const selection = window.getSelection();
275
- if (!selection || selection.rangeCount === 0)
276
- return;
277
- const range = getSafeRange();
278
- if (!range)
279
- return;
280
- const container = range.commonAncestorContainer;
281
- const currentElement = container.nodeType === Node.TEXT_NODE ? container.parentElement : container;
282
- if (!currentElement)
283
- return;
284
- const isInList = !!currentElement.closest('ol');
285
- exec('insertOrderedList');
286
- if (isInList) {
287
- const contentElement = (_a = editorRef.current) === null || _a === void 0 ? void 0 : _a.querySelector(`.${styles.pellContent}`);
288
- if (!contentElement)
289
- return;
290
- const spans = contentElement.querySelectorAll('span');
291
- spans.forEach((span) => {
292
- var _a;
293
- const computedStyle = window.getComputedStyle(span);
294
- const fontSize = computedStyle.fontSize;
295
- const fontWeight = computedStyle.fontWeight;
296
- if (fontSize && (parseFloat(fontSize) > 16 || fontWeight === 'bold' || fontWeight === '700')) {
297
- const div = document.createElement('div');
298
- div.innerHTML = span.innerHTML;
299
- (_a = span.parentNode) === null || _a === void 0 ? void 0 : _a.replaceChild(div, span);
300
- }
301
- else {
302
- const parent = span.parentNode;
303
- if (parent) {
304
- while (span.firstChild) {
305
- parent.insertBefore(span.firstChild, span);
306
- }
307
- parent.removeChild(span);
308
- }
309
- }
310
- });
311
- const allElements = contentElement.querySelectorAll('*');
312
- allElements.forEach((element) => {
313
- const htmlElement = element;
314
- const computedStyle = window.getComputedStyle(element);
315
- const fontSize = computedStyle.fontSize;
316
- if (!['H1', 'H2', 'H3', 'H4', 'H5', 'H6'].includes(element.tagName)) {
317
- if (fontSize && parseFloat(fontSize) > 18) {
318
- htmlElement.style.fontSize = '';
319
- htmlElement.style.fontWeight = '';
320
- htmlElement.style.fontFamily = '';
321
- }
322
- }
323
- });
324
- updateActiveStates();
325
- }
114
+ const commands = {
115
+ bold: () => applyAction((e) => e.chain().toggleBold().run()),
116
+ italic: () => applyAction((e) => e.chain().toggleItalic().run()),
117
+ underline: () => applyAction((e) => e.chain().toggleUnderline().run()),
118
+ strikethrough: () => applyAction((e) => e.chain().toggleStrike().run()),
119
+ heading2: () => applyAction((e) => e.chain().toggleHeading({ level: 2 }).run()),
120
+ olist: () => applyAction((e) => e.chain().toggleOrderedList().run()),
121
+ undo: () => applyAction((e) => e.chain().undo().run()),
122
+ redo: () => applyAction((e) => e.chain().redo().run()),
123
+ image: () => { var _a; return (_a = uploaderRef.current) === null || _a === void 0 ? void 0 : _a.click(); },
326
124
  };
125
+ const toolbarButtons = [
126
+ {
127
+ name: 'bold',
128
+ icon: IconBoldToString('', '', '1.5'),
129
+ action: commands.bold,
130
+ active: 'bold',
131
+ title: lng === 'ru' ? 'Жирный (Ctrl+B)' : 'Bold (Ctrl+B)',
132
+ },
133
+ {
134
+ name: 'italic',
135
+ icon: IconItalicToString('', '', '1.5'),
136
+ action: commands.italic,
137
+ active: 'italic',
138
+ title: lng === 'ru' ? 'Курсив (Ctrl+I)' : 'Italic (Ctrl+I)',
139
+ },
140
+ {
141
+ name: 'underline',
142
+ icon: IconUnderlineToString('', '', '1.5'),
143
+ action: commands.underline,
144
+ active: 'underline',
145
+ title: lng === 'ru' ? 'Подчеркнутый (Ctrl+U)' : 'Underline (Ctrl+U)',
146
+ },
147
+ {
148
+ name: 'strikethrough',
149
+ icon: IconStrikethroughToString('', '', '1.5'),
150
+ action: commands.strikethrough,
151
+ active: 'strikethrough',
152
+ title: lng === 'ru' ? 'Зачеркнутый' : 'Strike-through',
153
+ },
154
+ {
155
+ name: 'heading2',
156
+ icon: IconHeader2ToString('', '', '1.5'),
157
+ action: commands.heading2,
158
+ active: 'heading2',
159
+ title: lng === 'ru' ? 'Заголовок' : 'Heading 2',
160
+ },
161
+ {
162
+ name: 'olist',
163
+ icon: IconBulletlistToString(),
164
+ action: commands.olist,
165
+ active: 'olist',
166
+ title: lng === 'ru' ? 'Список' : 'Bullet List',
167
+ },
168
+ {
169
+ name: 'undo',
170
+ icon: IconUndoToString('', '', '1.5'),
171
+ action: commands.undo,
172
+ title: lng === 'ru' ? 'Возврат последнего действия' : 'Return last action',
173
+ },
174
+ {
175
+ name: 'redo',
176
+ icon: IconRedoToString('', '', '1.5'),
177
+ action: commands.redo,
178
+ title: lng === 'ru' ? 'Отмена последнего действия' : 'Cancel last action',
179
+ },
180
+ ];
181
+ const normalize = (html) => html.replace(/&nbsp;|\s+/g, ' ').replace(/>\s+</g, '><').trim();
182
+ const normalizedEditor = normalize(editorHtml || '');
183
+ const normalizedDefault = normalize(defaultValue || '');
184
+ const isTextEmpty = normalizedEditor.replace(/<[^>]*>/g, '').trim().length === 0;
185
+ const hasNoNewFiles = temporaryFiles.filter(f => f.file).length === 0 &&
186
+ temporaryFiles.length === ((_a = attachedFiles === null || attachedFiles === void 0 ? void 0 : attachedFiles.length) !== null && _a !== void 0 ? _a : 0);
187
+ const hasErrorsInFiles = temporaryFiles.some(f => !!f.error);
188
+ const hasNoTextChanges = normalizedEditor === normalizedDefault;
189
+ const isSubmitDisabled = (hasNoTextChanges && hasNoNewFiles) || isTextEmpty || hasErrorsInFiles;
190
+ const isCancelDisabled = !isEditMode && (hasNoTextChanges && hasNoNewFiles);
327
191
  // Функция обработки загрузки файлов
328
192
  const handleUploadFiles = (event) => {
329
193
  const files = event.target.files;
@@ -376,314 +240,21 @@ export const TextEditor = ({ defaultValue, attachedFiles, label, onSubmit, onCan
376
240
  onDelete === null || onDelete === void 0 ? void 0 : onDelete(id);
377
241
  }
378
242
  };
379
- const getEditorActions = useCallback(() => {
380
- const baseActions = [
381
- {
382
- name: 'bold',
383
- icon: IconBoldToString('', '', '1.5'),
384
- title: lng === 'ru' ? 'Жирный (Ctrl+B)' : 'Bold (Ctrl+B)',
385
- result: () => { },
386
- },
387
- {
388
- name: 'italic',
389
- icon: IconItalicToString('', '', '1.5'),
390
- title: lng === 'ru' ? 'Курсив (Ctrl+I)' : 'Italic (Ctrl+I)',
391
- result: () => { },
392
- },
393
- {
394
- name: 'underline',
395
- icon: IconUnderlineToString('', '', '1.5'),
396
- title: lng === 'ru' ? 'Подчеркнутый (Ctrl+U)' : 'Underline (Ctrl+U)',
397
- result: () => { },
398
- },
399
- {
400
- name: 'strikethrough',
401
- icon: IconStrikethroughToString('', '', '1.5'),
402
- title: lng === 'ru' ? 'Зачеркнутый' : 'Strike-through',
403
- result: () => { },
404
- },
405
- {
406
- name: 'heading2',
407
- icon: IconHeader2ToString('', '', '1.5'),
408
- title: lng === 'ru' ? 'Заголовок' : 'Heading 2',
409
- result: () => { },
410
- },
411
- {
412
- name: 'olist',
413
- icon: IconBulletlistToString(),
414
- title: lng === 'ru' ? 'Список' : 'Bullet List',
415
- result: () => { },
416
- },
417
- {
418
- name: 'undo',
419
- icon: IconUndoToString('', '', '1.5'),
420
- title: lng === 'ru' ? 'Возврат последнего действия' : 'Return last action',
421
- result: () => { },
422
- },
423
- {
424
- name: 'redo',
425
- icon: IconRedoToString('', '', '1.5'),
426
- title: lng === 'ru' ? 'Отмена последнего действия' : 'Cancel last action',
427
- result: () => { },
428
- },
429
- ];
430
- if (canAttachFiles) {
431
- baseActions.push({
432
- name: 'image',
433
- icon: IconAttachToString('', '', '1.5'),
434
- title: lng === 'ru' ? 'Прикрепить файл' : 'Upload file',
435
- result: () => { },
436
- });
437
- }
438
- return baseActions;
439
- }, [canAttachFiles, lng]);
440
- const getEditorClasses = useCallback(() => ({
441
- actionbar: styles.pellActionbar,
442
- button: styles.pellButton,
443
- content: styles.pellContent,
444
- selected: styles.pellButtonSelected,
445
- }), []);
446
- const initializePellEditor = () => {
447
- return init({
448
- element: editorRef.current,
449
- onChange: handleEditorChange,
450
- defaultParagraphSeparator: 'div',
451
- actions: getEditorActions(),
452
- classes: getEditorClasses(),
453
- });
454
- };
455
243
  const handleSubmit = useCallback(() => {
456
- const currentPell = pellRef.current;
457
- if (!(currentPell === null || currentPell === void 0 ? void 0 : currentPell.content)) {
244
+ if (!editor)
458
245
  return;
459
- }
460
- if (onSubmit && currentPell.content.innerHTML) {
461
- const filesToSend = convertAttacmentsToFile(tempFilesRef.current.filter(file => !Boolean(file.error) && file.file));
462
- onSubmit(currentPell.content.innerHTML, filesToSend);
463
- currentPell.content.innerHTML = '';
464
- setTemporaryFiles([]);
465
- setEditorHtml('');
466
- }
467
- }, [onSubmit]);
246
+ const filesToSend = convertAttacmentsToFile(tempFilesRef.current.filter(file => !Boolean(file.error) && file.file));
247
+ onSubmit === null || onSubmit === void 0 ? void 0 : onSubmit(editor.getHTML(), filesToSend);
248
+ editor.commands.clearContent();
249
+ setTemporaryFiles([]);
250
+ }, [editor, onSubmit]);
468
251
  const handleCancel = useCallback(() => {
469
- const currentPell = pellRef.current;
470
- if (!(currentPell === null || currentPell === void 0 ? void 0 : currentPell.content)) {
471
- return;
472
- }
473
- if (currentPell.content.innerHTML || tempFilesRef.current.length) {
474
- currentPell.content.innerHTML = defaultValue || '';
475
- setEditorHtml(defaultValue || '');
476
- setTemporaryFiles(attachedFiles ? attachedFiles.map(file => (Object.assign({}, file))) : []);
477
- if (onCancel) {
478
- onCancel === null || onCancel === void 0 ? void 0 : onCancel();
479
- }
480
- }
481
- }, [defaultValue, onCancel]);
482
- const hadleRedo = useCallback(() => {
483
- const currentPell = pellRef.current;
484
- const contentToRestore = redoContentRef.current;
485
- if (!(currentPell === null || currentPell === void 0 ? void 0 : currentPell.content) || !contentToRestore) {
252
+ if (!editor)
486
253
  return;
487
- }
488
- currentPell.content.innerHTML = contentToRestore;
489
- setEditorHtml(contentToRestore);
490
- currentPell.content.focus();
491
- setTimeout(setCursorToEnd, 0);
492
- }, []);
493
- const handleUndo = useCallback(() => {
494
- const currentPell = pellRef.current;
495
- if (!(currentPell === null || currentPell === void 0 ? void 0 : currentPell.content)) {
496
- return;
497
- }
498
- redoContentRef.current = currentPell.content.innerHTML;
499
- currentPell.content.innerHTML = defaultValue || '';
500
- setEditorHtml(defaultValue || '');
501
- setTimeout(setCursorToEnd, 0);
502
- }, [defaultValue]);
503
- const setupToolbar = (pellEditor) => {
504
- if (!editorRef.current)
505
- return;
506
- const actionbar = editorRef.current.querySelector(`.${styles.pellActionbar}`);
507
- const content = editorRef.current.querySelector(`.${styles.pellContent}`);
508
- if (actionbar && content) {
509
- // 1. Контейнер для кнопок форматирования
510
- const buttonsContainer = document.createElement('div');
511
- buttonsContainer.className = styles.buttonsContainer;
512
- while (actionbar.firstChild) {
513
- buttonsContainer.appendChild(actionbar.firstChild);
514
- }
515
- // 2. Контейнер для Отменить/Добавить
516
- const actionsWrapper = document.createElement('div');
517
- actionsWrapper.className = styles.actionsWrapper || 'actions-container';
518
- actionsWrapper.style.display = 'flex';
519
- actionsWrapper.style.alignItems = 'center';
520
- actionsWrapper.style.gap = '8px';
521
- actionsWrapper.style.marginLeft = 'auto';
522
- actionbar.appendChild(buttonsContainer);
523
- actionbar.appendChild(actionsWrapper);
524
- const root = createRoot(actionsWrapper);
525
- root.render(React.createElement("div", { style: { display: 'flex', alignItems: 'center', gap: '8px' } },
526
- React.createElement(IconButton, { ref: cancelButtonRef, title: lng === 'ru' ? 'Отменить' : 'Cancel', icon: React.createElement(IconClose, null), onClick: handleCancel, style: {
527
- width: '25px',
528
- height: '25px',
529
- padding: '5px',
530
- backgroundColor: 'white',
531
- }, color: "var(--blue-main)" }),
532
- React.createElement(IconButton, { ref: submitButtonRef, title: lng === 'ru' ? 'Отправить' : 'Submit', icon: React.createElement(IconSubmit, { width: '10', height: '10', htmlColor: 'blue', strokeWidth: '1' }), onClick: handleSubmit, style: {
533
- width: '25px',
534
- height: '25px',
535
- padding: '5px',
536
- backgroundColor: 'var(--blue-main)',
537
- }, color: "white" })));
538
- }
539
- const buttons = editorRef.current.querySelectorAll(`.${styles.pellButton}`);
540
- const commands = ['bold', 'italic', 'underline', 'strikethrough', 'heading2', 'olist', 'undo', 'redo'];
541
- if (canAttachFiles) {
542
- commands.push('image');
543
- }
544
- buttons.forEach((button, index) => {
545
- const command = commands[index];
546
- if (command) {
547
- const htmlButton = button;
548
- buttonRefs.current[command] = htmlButton;
549
- htmlButton.setAttribute('data-command', command);
550
- htmlButton.onclick = null;
551
- htmlButton.addEventListener('mousedown', (e) => {
552
- e.preventDefault();
553
- e.stopPropagation();
554
- if (command === 'heading2') {
555
- toggleHeading2();
556
- }
557
- else if (command === 'olist') {
558
- toggleBulletList();
559
- }
560
- else if (command === 'undo') {
561
- handleUndo();
562
- }
563
- else if (command === 'redo') {
564
- hadleRedo();
565
- }
566
- else if (command === 'image') {
567
- handleAttachFiles();
568
- }
569
- else {
570
- document.execCommand(command, false, undefined);
571
- }
572
- pellEditor.content.focus();
573
- updateActiveStates();
574
- });
575
- htmlButton.addEventListener('click', (e) => {
576
- e.preventDefault();
577
- e.stopPropagation();
578
- });
579
- }
580
- });
581
- };
582
- // Функция для открытия диалога выбора файлов
583
- const handleAttachFiles = () => {
584
- var _a;
585
- (_a = uploaderRef.current) === null || _a === void 0 ? void 0 : _a.click();
586
- };
587
- const handleEditorChange = useCallback((html) => {
588
- setEditorHtml(html);
589
- redoContentRef.current = html;
590
- updateActiveStates();
591
- }, [updateActiveStates]);
592
- useEffect(() => {
593
- var _a;
594
- if (!submitButtonRef.current)
595
- return;
596
- const normalizeHtml = (html) => {
597
- if (!html)
598
- return '';
599
- return html
600
- .replace(/&nbsp;/g, ' ') // неразрывные пробелы
601
- .replace(/\s+/g, ' ') // лишние пробелы и переносы
602
- .replace(/>\s+</g, '><') // пробелы между тегами
603
- .trim();
604
- };
605
- const normalizedEditor = normalizeHtml(editorHtml);
606
- const normalizedDefault = normalizeHtml(defaultValue);
607
- const contentOnly = normalizedEditor
608
- .replace(/<[^>]*>/g, '') // все теги
609
- .replace(/\s/g, '') // все пробелов
610
- .trim();
611
- const isTextEmpty = contentOnly.length === 0;
612
- const hasNoNewFiles = temporaryFiles.filter(file => file.file).length === 0 &&
613
- temporaryFiles.length === ((_a = attachedFiles === null || attachedFiles === void 0 ? void 0 : attachedFiles.length) !== null && _a !== void 0 ? _a : 0);
614
- const hasErrorsInFiles = temporaryFiles.some(file => Boolean(file.error));
615
- const hasNoTextChanges = normalizedEditor === normalizedDefault;
616
- const hasNoChanges = hasNoTextChanges && hasNoNewFiles;
617
- if (submitButtonRef.current) {
618
- submitButtonRef.current.disabled = hasNoChanges || isTextEmpty || hasErrorsInFiles;
619
- submitButtonRef.current.style.opacity = hasNoChanges || isTextEmpty || hasErrorsInFiles ? '0.5' : '1';
620
- submitButtonRef.current.style.cursor = hasNoChanges || isTextEmpty || hasErrorsInFiles ? 'default' : 'pointer';
621
- }
622
- if (cancelButtonRef.current) {
623
- cancelButtonRef.current.disabled = !isEditMode && hasNoChanges;
624
- cancelButtonRef.current.style.opacity = isEditMode ? '1' : hasNoChanges ? '0.5' : '1';
625
- cancelButtonRef.current.style.cursor = isEditMode ? 'pointer' : hasNoChanges ? 'default' : 'pointer';
626
- }
627
- }, [editorHtml, defaultValue, temporaryFiles, submitButtonRef.current, cancelButtonRef.current, isEditMode]);
628
- const handleKeyDown = (e) => {
629
- if (e.ctrlKey || e.metaKey) {
630
- switch (e.key) {
631
- case 'b':
632
- e.preventDefault();
633
- document.execCommand('bold', false, undefined);
634
- updateActiveStates();
635
- break;
636
- case 'i':
637
- e.preventDefault();
638
- document.execCommand('italic', false, undefined);
639
- updateActiveStates();
640
- break;
641
- case 'u':
642
- e.preventDefault();
643
- document.execCommand('underline', false, undefined);
644
- updateActiveStates();
645
- break;
646
- }
647
- }
648
- };
649
- useEffect(() => {
650
- if (editorRef.current) {
651
- editorRef.current.innerHTML = '';
652
- const pellEditor = initializePellEditor();
653
- pellRef.current = pellEditor;
654
- pellEditor.content.innerHTML = defaultValue || '';
655
- setEditor(pellEditor);
656
- setupToolbar(pellEditor);
657
- const pellEditorContent = pellEditor.content;
658
- const handleInput = () => updateActiveStates();
659
- const handleKeyUp = () => setTimeout(updateActiveStates, 10);
660
- const handleMouseUp = () => setTimeout(updateActiveStates, 10);
661
- const handleFocus = () => setTimeout(updateActiveStates, 10);
662
- pellEditorContent.addEventListener('input', handleInput);
663
- pellEditorContent.addEventListener('keyup', handleKeyUp);
664
- pellEditorContent.addEventListener('mouseup', handleMouseUp);
665
- pellEditorContent.addEventListener('focus', handleFocus);
666
- setTimeout(setCursorToEnd, 0);
667
- return () => {
668
- pellEditorContent.removeEventListener('input', handleInput);
669
- pellEditorContent.removeEventListener('keyup', handleKeyUp);
670
- pellEditorContent.removeEventListener('mouseup', handleMouseUp);
671
- pellEditorContent.removeEventListener('focus', handleFocus);
672
- if (editorRef.current) {
673
- editorRef.current.innerHTML = '';
674
- }
675
- };
676
- }
677
- document.addEventListener('keydown', handleKeyDown);
678
- return () => {
679
- document.removeEventListener('keydown', handleKeyDown);
680
- };
681
- }, []);
682
- useEffect(() => {
683
- if (editor) {
684
- setTimeout(updateActiveStates, 100);
685
- }
686
- }, [editor]);
254
+ editor.commands.setContent(defaultValue || '');
255
+ setTemporaryFiles(attachedFiles !== null && attachedFiles !== void 0 ? attachedFiles : []);
256
+ onCancel === null || onCancel === void 0 ? void 0 : onCancel();
257
+ }, [editor, defaultValue, attachedFiles, onCancel]);
687
258
  const wrapperClassess = classNames(styles['wrapper--input'], {
688
259
  [styles['wrapper--input-label']]: label && !required,
689
260
  [styles['wrapper--input-helperText']]: error,
@@ -698,7 +269,35 @@ export const TextEditor = ({ defaultValue, attachedFiles, label, onSubmit, onCan
698
269
  label && (React.createElement(Typography, { variant: "Caption", className: labelClasses }, label)),
699
270
  React.createElement("div", { className: inputClassess, title: '' },
700
271
  temporaryFiles.length > 0 && (React.createElement(AttachedFilesPreview, { files: temporaryFiles, allowDelete: true, onDelete: removeAttachedFile, className: styles.attachedFilesContainer, lng: lng, maxFileCount: maxFileCount })),
701
- React.createElement("div", { className: styles.editorContainer, ref: editorRef }),
272
+ React.createElement("div", { className: styles.editorContainer },
273
+ React.createElement("div", { className: styles.pellActionbar },
274
+ React.createElement("div", { className: styles.buttonsContainer },
275
+ toolbarButtons.map((btn) => {
276
+ const isActive = btn.active ? editorState[btn.active] : false;
277
+ return (React.createElement("button", { key: btn.name, type: "button", className: `${styles.pellButton} ${btn.active && isActive ? styles.pellButtonSelected : ''}`, onMouseDown: (e) => {
278
+ e.preventDefault();
279
+ btn.action();
280
+ }, dangerouslySetInnerHTML: { __html: btn.icon }, title: btn.title }));
281
+ }),
282
+ canAttachFiles && (React.createElement("button", { type: "button", className: styles.pellButton, onMouseDown: (e) => { e.preventDefault(); commands.image(); }, dangerouslySetInnerHTML: { __html: IconAttachToString('', '', '1.5') }, title: lng === 'ru' ? 'Прикрепить файл' : 'Upload file' }))),
283
+ React.createElement("div", { style: { display: 'flex', alignItems: 'center', gap: '8px' } },
284
+ React.createElement(IconButton, { disabled: isCancelDisabled, title: lng === 'ru' ? 'Отменить' : 'Cancel', icon: React.createElement(IconClose, null), onClick: handleCancel, style: {
285
+ width: '25px',
286
+ height: '25px',
287
+ padding: '5px',
288
+ backgroundColor: 'white',
289
+ opacity: isCancelDisabled ? 0.5 : 1,
290
+ cursor: isCancelDisabled ? 'default' : 'pointer',
291
+ }, color: "var(--blue-main)" }),
292
+ React.createElement(IconButton, { title: lng === 'ru' ? 'Отправить' : 'Submit', icon: React.createElement(IconSubmit, { width: '10', height: '10', htmlColor: 'blue', strokeWidth: '1' }), onClick: handleSubmit, disabled: isSubmitDisabled, style: {
293
+ width: '25px',
294
+ height: '25px',
295
+ padding: '5px',
296
+ backgroundColor: 'var(--blue-main)',
297
+ opacity: isSubmitDisabled ? 0.5 : 1,
298
+ cursor: isSubmitDisabled ? 'default' : 'pointer'
299
+ }, color: "white" }))),
300
+ React.createElement("div", { className: styles.pellContent, onClick: () => editor === null || editor === void 0 ? void 0 : editor.chain().focus().run() }, editor && React.createElement(EditorContent, { editor: editor }))),
702
301
  canAttachFiles && (React.createElement("input", { ref: uploaderRef, type: "file", style: { display: 'none' }, multiple: true, onChange: handleUploadFiles, accept: ACCEPTED_FILE_TYPES }))),
703
- (error && helperText) && (React.createElement(Typography, { variant: "Caption", className: classNames(styles.helperText) }, helperText))));
302
+ error && helperText && (React.createElement(Typography, { variant: "Caption", className: styles.helperText }, helperText))));
704
303
  };