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.
- package/dist/components/Button/Button.d.ts +1 -1
- package/dist/components/Button/Button.js +6 -5
- package/dist/components/Button/Button.module.css +2 -1
- package/dist/components/Dropdown/Dropdown.d.ts +1 -1
- package/dist/components/Dropdown/Dropdown.js +98 -30
- package/dist/components/Dropdown/Dropdown.module.css +12 -4
- package/dist/components/FileLoader/FileLoader.js +105 -106
- package/dist/components/TextEditor/TextEditor.js +172 -573
- package/dist/components/TextEditor/TextEditor.module.css +8 -7
- package/dist/types/index.d.ts +24 -13
- package/package.json +5 -1
|
@@ -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
|
-
|
|
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
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
225
|
-
|
|
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
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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(/ |\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
|
-
|
|
457
|
-
if (!(currentPell === null || currentPell === void 0 ? void 0 : currentPell.content)) {
|
|
244
|
+
if (!editor)
|
|
458
245
|
return;
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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
|
-
|
|
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
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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(/ /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
|
|
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
|
-
|
|
302
|
+
error && helperText && (React.createElement(Typography, { variant: "Caption", className: styles.helperText }, helperText))));
|
|
704
303
|
};
|