Image.jsx 29 KB
Newer Older
yakoff94's avatar
yakoff94 committed
1
import { NodeViewWrapper, ReactNodeViewRenderer } from "@tiptap/react";
2
import React, { useEffect, useRef, useState, Fragment } from "react";
yakoff94's avatar
yakoff94 committed
3
import TipTapImage from "@tiptap/extension-image";
Яков's avatar
update    
Яков committed
4
import { Button, Modal, Input, Typography } from 'antd';
Яков's avatar
update    
Яков committed
5
import {FontSizeOutlined} from "@ant-design/icons";
Яков's avatar
update    
Яков committed
6
const { TextArea } = Input;
Яков's avatar
update    
Яков committed
7
const {Text} = Typography;
yakoff94's avatar
yakoff94 committed
8
9
10

const MIN_WIDTH = 60;
const BORDER_COLOR = '#0096fd';
Яков's avatar
Яков committed
11
const ALIGN_OPTIONS = ['left', 'center', 'right', 'text'];
yakoff94's avatar
yakoff94 committed
12

Яков's avatar
fix    
Яков committed
13
const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos, selected }) => {
yakoff94's avatar
yakoff94 committed
14
    const imgRef = useRef(null);
15
16
17
    const wrapperRef = useRef(null);
    const [showAlignMenu, setShowAlignMenu] = useState(false);
    const isInitialized = useRef(false);
Яков's avatar
update    
Яков committed
18
    const [isResizing, setIsResizing] = useState(false);
Яков's avatar
update    
Яков committed
19
20
    const [altModalVisible, setAltModalVisible] = useState(false);
    const [tempAlt, setTempAlt] = useState(node.attrs.alt || '');
Яков's avatar
update    
Яков committed
21
    const [tempFrontAlt, setTempFrontAlt] = useState(node.attrs.frontAlt || '');
Яков's avatar
update    
Яков committed
22

Яков's avatar
update    
Яков committed
23

Яков's avatar
fix    
Яков committed
24
25
    // Добавляем прозрачный нулевой пробел после изображения
    useEffect(() => {
Яков's avatar
Яков committed
26
        if (!editor || !getPos || editor.isDestroyed) return
Яков's avatar
fix    
Яков committed
27

Яков's avatar
Яков committed
28
        let pos
Яков's avatar
update    
Яков committed
29
        try {
Яков's avatar
Яков committed
30
31
32
            pos = getPos()
        } catch {
            return
Яков's avatar
update    
Яков committed
33
        }
Яков's avatar
Яков committed
34
        if (typeof pos !== 'number') return
Яков's avatar
update    
Яков committed
35

Яков's avatar
Яков committed
36
37
38
        const { doc } = editor.state
        const node = doc.nodeAt(pos)
        if (!node || node.type.name !== 'image') return
Яков's avatar
fix    
Яков committed
39

Яков's avatar
Яков committed
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
        const next = doc.nodeAt(pos + node.nodeSize)
        if (next?.isText && next.text === '\u200B') return

        requestAnimationFrame(() => {
            if (editor.isDestroyed) return

            try {
                const p = getPos()
                const n = editor.state.doc.nodeAt(p)
                if (!n || n.type.name !== 'image') return

                editor.commands.insertContentAt(p + n.nodeSize, '\u200B')
            } catch {}
        })
    }, [])
Яков's avatar
fix    
Яков committed
55

Яков's avatar
update    
Яков committed
56

Яков's avatar
fix    
Яков committed
57
58
    // Получаем текущую ширину редактора и доступное пространство
    const getEditorDimensions = () => {
Яков's avatar
Яков committed
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
        const editorContent = editor?.options?.element?.closest('.atma-editor-content');
        if (!editorContent) return { width: Infinity, availableSpace: Infinity };

        const fullEditorWidth = editorContent.clientWidth;
        const editorStyles = window.getComputedStyle(editorContent);
        const paddingLeft = parseFloat(editorStyles.paddingLeft) || 0;
        const paddingRight = parseFloat(editorStyles.paddingRight) || 0;
        const availableEditorWidth = fullEditorWidth - paddingLeft - paddingRight;

        let container;

        // при center — всегда редактор
        if (node.attrs.align === 'center') {
            container = editorContent;
        } else {
            // при других выравниваниях — ближайший блок
            container = imgRef.current?.closest('li, blockquote, td, p, div') || editorContent;
Яков's avatar
fix    
Яков committed
76
77
        }

Яков's avatar
Яков committed
78
79
80
81
82
83
84
85
86
        const containerStyles = window.getComputedStyle(container);
        const containerPaddingLeft = parseFloat(containerStyles.paddingLeft) || 0;
        const containerPaddingRight = parseFloat(containerStyles.paddingRight) || 0;
        const containerWidth = container.clientWidth - containerPaddingLeft - containerPaddingRight;

        return {
            width: containerWidth,            // текущая ширина контейнера
            availableSpace: availableEditorWidth // фиксированная доступная ширина
        };
Яков's avatar
fix    
Яков committed
87
88
    };

Яков's avatar
Яков committed
89

Яков's avatar
fix    
Яков committed
90
    // Безопасное обновление атрибутов с учетом выравнивания и границ
Яков's avatar
Яков committed
91
92
93
94
95
96
97
98
99
100
    const clamp = (v, min, max) => Math.min(max, Math.max(min, v))

    const safeUpdateAttributes = (patch) => {
        if (!editor || editor.isDestroyed) return

        let pos
        try {
            pos = getPos()
        } catch {
            return
Яков's avatar
fix    
Яков committed
101
        }
Яков's avatar
Яков committed
102
        if (typeof pos !== 'number') return
Яков's avatar
fix    
Яков committed
103

Яков's avatar
Яков committed
104
105
106
107
108
109
110
111
112
113
114
115
        const currentNode = editor.state.doc.nodeAt(pos)
        if (!currentNode || currentNode.type.name !== 'image') return

        const base = currentNode.attrs || {}

        // 1) сохраняем то, что не хотим потерять
        const keep = {
            align: base.align,
            border: base.border,
            borderColor: base.borderColor,
            borderWidth: base.borderWidth,
            borderRadius: base.borderRadius,
Яков's avatar
fix    
Яков committed
116
117
        }

Яков's avatar
Яков committed
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
        // 2) кандидат на апдейт
        let next = { ...keep, ...base, ...patch }

        // 3) нормализуем размеры (границы)
        const minW = 80
        const maxW = 1600
        const minH = 40
        const maxH = 2000

        if (next.width != null) next.width = clamp(Number(next.width) || 0, minW, maxW)
        if (next.height != null) next.height = clamp(Number(next.height) || 0, minH, maxH)

        // 4) нормализуем align (чтобы не улетало в мусор)
        const allowedAlign = new Set(['left', 'center', 'right', 'full'])
        if (next.align && !allowedAlign.has(next.align)) next.align = base.align || 'center'

        updateAttributes(next)
    }
    //
    // const safeUpdateAttributes = (newAttrs) => {
    //     const { width: editorWidth, availableSpace } = getEditorDimensions();
    //     let { width, height, align } = { ...node.attrs, ...newAttrs };
    //     const newAlign = newAttrs.align || align;
    //
    //     // При изменении выравнивания проверяем доступное пространство
    //     if (newAlign && newAlign !== align) {
    //         const maxWidth = availableSpace;
    //         if (width > maxWidth) {
    //             const ratio = maxWidth / width;
    //             width = maxWidth;
    //             height = Math.round(height * ratio);
    //         }
    //     } else {
    //         // Для обычного обновления размеров
    //         const maxWidth = availableSpace;
    //         if (width > maxWidth) {
    //             const ratio = maxWidth / width;
    //             width = maxWidth;
    //             height = Math.round(height * ratio);
    //         }
    //     }
    //
    //     // Проверяем минимальный размер
    //     if (width < MIN_WIDTH) {
    //         const ratio = MIN_WIDTH / width;
    //         width = MIN_WIDTH;
    //         height = Math.round(height * ratio);
    //     }
    //
    //     updateAttributes({ width, height, ...newAttrs });
    // };
169

Яков's avatar
fix    
Яков committed
170
    // Инициализация изображения
171
    useEffect(() => {
Яков's avatar
fix    
Яков committed
172
        if (!node.attrs['data-node-id']) {
Яков's avatar
fix    
Яков committed
173
            safeUpdateAttributes({
Яков's avatar
fix    
Яков committed
174
175
176
                'data-node-id': `img-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
            });
        }
Яков's avatar
fix    
Яков committed
177
    }, [node.attrs['data-node-id']]);
Яков's avatar
fix    
Яков committed
178

Яков's avatar
fix    
Яков committed
179
    // Обработка кликов вне изображения
Яков's avatar
fix    
Яков committed
180
181
182
    useEffect(() => {
        const handleClickOutside = (event) => {
            if (wrapperRef.current && !wrapperRef.current.contains(event.target) && selected) {
Яков's avatar
update    
Яков committed
183
184
185
186
187
188
189
190
191
                try {
                    const pos = getPos?.()
                    if (typeof pos === 'number') {
                        editor.commands.setNodeSelection(pos)
                    }
                } catch (e) {
                    console.warn('getPos() failed:', e)
                }
                // editor.commands.setNodeSelection(getPos());
Яков's avatar
fix    
Яков committed
192
193
            }
        };
Яков's avatar
fix    
Яков committed
194
195
196
        document.addEventListener('mousedown', handleClickOutside);
        return () => document.removeEventListener('mousedown', handleClickOutside);
    }, [selected, editor, getPos]);
yakoff94's avatar
yakoff94 committed
197

Яков's avatar
fix    
Яков committed
198
    // Загрузка и инициализация изображения
yakoff94's avatar
yakoff94 committed
199
    useEffect(() => {
Яков's avatar
fix    
Яков committed
200
201
202
203
        if (!imgRef.current || isInitialized.current) return;

        const initImageSize = () => {
            try {
Яков's avatar
fix    
Яков committed
204
205
206
207
208
                // Если размеры уже заданы в атрибутах - используем их сразу
                if (node.attrs.width && node.attrs.height) {
                    isInitialized.current = true;
                    return;
                }
Яков's avatar
fix    
Яков committed
209

Яков's avatar
fix    
Яков committed
210
                const { width: editorWidth } = getEditorDimensions();
Яков's avatar
fix    
Яков committed
211
212
                const naturalWidth = imgRef.current.naturalWidth;
                const naturalHeight = imgRef.current.naturalHeight;
Яков's avatar
update    
Яков committed
213

Яков's avatar
fix    
Яков committed
214
215
                if (naturalWidth <= 0 || naturalHeight <= 0) {
                    console.warn('Image has invalid natural dimensions, retrying...');
Яков's avatar
fix    
Яков committed
216
                    setTimeout(initImageSize, 100);
Яков's avatar
fix    
Яков committed
217
218
219
220
221
222
223
224
225
226
227
228
                    return;
                }

                let initialWidth = naturalWidth;
                let initialHeight = naturalHeight;

                if (initialWidth > editorWidth) {
                    const ratio = editorWidth / initialWidth;
                    initialWidth = editorWidth;
                    initialHeight = Math.round(initialHeight * ratio);
                }

Яков's avatar
fix    
Яков committed
229
                safeUpdateAttributes({
Яков's avatar
fix    
Яков committed
230
231
232
                    width: initialWidth,
                    height: initialHeight,
                    'data-node-id': node.attrs['data-node-id'] || `img-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
Яков's avatar
fix    
Яков committed
233
234
                });
                isInitialized.current = true;
Яков's avatar
fix    
Яков committed
235
236
237
238
239
            } catch (error) {
                console.warn('Error initializing image size:', error);
            }
        };

Яков's avatar
fix    
Яков committed
240
        const handleLoad = () => {
Яков's avatar
fix    
Яков committed
241
242
243
244
245
            // Если размеры уже заданы в атрибутах, пропускаем инициализацию
            if (node.attrs.width && node.attrs.height) {
                isInitialized.current = true;
                return;
            }
Яков's avatar
fix    
Яков committed
246
247
248
            setTimeout(initImageSize, 50);
        };

Яков's avatar
fix    
Яков committed
249
        if (imgRef.current.complete) {
Яков's avatar
fix    
Яков committed
250
            handleLoad();
Яков's avatar
fix    
Яков committed
251
        } else {
Яков's avatar
fix    
Яков committed
252
            imgRef.current.addEventListener('load', handleLoad);
253
        }
Яков's avatar
fix    
Яков committed
254
255

        return () => {
Яков's avatar
fix    
Яков committed
256
257
258
            if (imgRef.current) {
                imgRef.current.removeEventListener('load', handleLoad);
            }
Яков's avatar
fix    
Яков committed
259
        };
Яков's avatar
fix    
Яков committed
260
    }, [node.attrs.width, node.attrs.height, node.attrs['data-node-id']]);
261

Яков's avatar
fix    
Яков committed
262
    // Обработка ресайза изображения
263
264
265
266
    const handleResizeStart = (direction) => (e) => {
        e.preventDefault();
        e.stopPropagation();

Яков's avatar
update    
Яков committed
267
        setIsResizing(true);
Яков's avatar
update    
Яков committed
268
269
270
271
272
273
274
275
276
        try {
            const pos = getPos?.()
            if (typeof pos === 'number') {
                editor.commands.setNodeSelection(pos)
            }
        } catch (e) {
            console.warn('getPos() failed:', e)
        }
        // editor.commands.setNodeSelection(getPos());
Яков's avatar
update    
Яков committed
277

Яков's avatar
fix    
Яков committed
278
279
280
281
282
        const startWidth = node.attrs.width || imgRef.current.naturalWidth;
        const startHeight = node.attrs.height || imgRef.current.naturalHeight;
        const aspectRatio = startWidth / startHeight;
        const startX = e.clientX;
        const startY = e.clientY;
Яков's avatar
Яков committed
283
        const { width: initialEditorWidth, availableSpace: initialAvailableSpace } = getEditorDimensions();
284
285

        const onMouseMove = (e) => {
Яков's avatar
Яков committed
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
            requestAnimationFrame(() => {
                const maxWidth = node.attrs.align === 'center' ? initialEditorWidth : initialAvailableSpace;

                const deltaX = e.clientX - startX;
                const deltaY = e.clientY - startY;

                let newWidth, newHeight;

                if (node.attrs.align === 'center') {
                    if (direction.includes('n') || direction.includes('s')) {
                        const scale = direction.includes('s') ? 1 : -1;
                        newHeight = Math.max(startHeight + deltaY * scale, MIN_WIDTH);
                        newWidth = Math.min(Math.round(newHeight * aspectRatio), maxWidth);
                        newHeight = Math.round(newWidth / aspectRatio);
                    } else {
                        const scale = direction.includes('e') ? 1 : -1;
                        newWidth = Math.min(
                            Math.max(startWidth + deltaX * scale, MIN_WIDTH),
                            maxWidth
                        );
                        newHeight = Math.round(newWidth / aspectRatio);
                    }
308
                } else {
Яков's avatar
Яков committed
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
                    if (direction.includes('e') || direction.includes('w')) {
                        const scale = direction.includes('e') ? 1 : -1;
                        newWidth = Math.min(
                            Math.max(startWidth + deltaX * scale, MIN_WIDTH),
                            maxWidth
                        );
                        newHeight = Math.round(newWidth / aspectRatio);
                    } else {
                        const scale = direction.includes('s') ? 1 : -1;
                        newHeight = Math.max(startHeight + deltaY * scale, MIN_WIDTH);
                        newWidth = Math.min(
                            Math.round(newHeight * aspectRatio),
                            maxWidth
                        );
                        newHeight = Math.round(newWidth / aspectRatio);
                    }
325
326
                }

Яков's avatar
Яков committed
327
328
                safeUpdateAttributes({ width: newWidth, height: newHeight });
            });
yakoff94's avatar
yakoff94 committed
329
330
        };

Яков's avatar
Яков committed
331

332
333
334
        const onMouseUp = () => {
            window.removeEventListener('mousemove', onMouseMove);
            window.removeEventListener('mouseup', onMouseUp);
Яков's avatar
update    
Яков committed
335
            setIsResizing(false);
Яков's avatar
update    
Яков committed
336
337
338
339
340
341
342
343
344
            try {
                const pos = getPos?.()
                if (typeof pos === 'number') {
                    editor.commands.setNodeSelection(pos)
                }
            } catch (e) {
                console.warn('getPos() failed:', e)
            }
            // editor.commands.setNodeSelection(getPos());
Яков's avatar
fix    
Яков committed
345
            editor.commands.focus();
yakoff94's avatar
yakoff94 committed
346
347
        };

348
349
350
        window.addEventListener('mousemove', onMouseMove);
        window.addEventListener('mouseup', onMouseUp);
    };
yakoff94's avatar
yakoff94 committed
351

Яков's avatar
fix    
Яков committed
352
    // Изменение выравнивания с автоматическим масштабированием
353
    const handleAlign = (align) => {
Яков's avatar
Яков committed
354
355
356
357
        safeUpdateAttributes({ align }); // первый вызов
        setTimeout(() => {
            safeUpdateAttributes({ align }); // повторный вызов с обновлёнными размерами
        }, 50);
358
        setShowAlignMenu(false);
Яков's avatar
fix    
Яков committed
359
        editor.commands.focus();
360
361
    };

Яков's avatar
fix    
Яков committed
362
    // Стили для обертки изображения
Яков's avatar
update    
Яков committed
363
364
    const getWrapperStyle = () => {
        const baseStyle = {
365
            display: 'inline-block',
Яков's avatar
update    
Яков committed
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
            lineHeight: 0,
            position: 'relative',
            outline: (selected || isResizing) ? `1px dashed ${BORDER_COLOR}` : 'none',
            verticalAlign: 'top',
            margin: '0.5rem 0',
        };

        if (node.attrs.align === 'center') {
            return {
                ...baseStyle,
                display: 'block',
                marginLeft: 'auto',
                marginRight: 'auto',
                width: node.attrs.width ? `${node.attrs.width}px` : 'fit-content',
                maxWidth: '100%',
                textAlign: 'center'
            };
        }

        return {
            ...baseStyle,
            ...(node.attrs.align === 'left' && {
                float: 'left',
                marginRight: '1rem',
                width: node.attrs.width ? `${node.attrs.width}px` : 'auto',
            }),
            ...(node.attrs.align === 'right' && {
                float: 'right',
                marginLeft: '1rem',
                width: node.attrs.width ? `${node.attrs.width}px` : 'auto',
            }),
            ...(node.attrs.align === 'text' && {
                display: 'inline-block',
                float: 'none',
                margin: '0 0.2rem',
                verticalAlign: 'middle',
                width: node.attrs.width ? `${node.attrs.width}px` : 'auto',
            }),
        };
    };
yakoff94's avatar
yakoff94 committed
406

Яков's avatar
fix    
Яков committed
407
    // Стили для самого изображения
408
409
    const getImageStyle = () => ({
        width: node.attrs.width ? `${node.attrs.width}px` : 'auto',
Яков's avatar
fix    
Яков committed
410
        height: 'auto',
411
412
413
414
        maxWidth: '100%',
        display: 'block',
        cursor: 'default',
        userSelect: 'none',
Яков's avatar
Яков committed
415
        margin: node.attrs.align === 'center' ? '0 auto' : '0',
Яков's avatar
update    
Яков committed
416
        verticalAlign: node.attrs.align === 'text' ? 'middle' : 'top',
Яков's avatar
fix    
Яков committed
417
        objectFit: 'contain'
418
419
    });

Яков's avatar
update    
Яков committed
420
    console.log(node.attrs.frontAlt);
yakoff94's avatar
yakoff94 committed
421
422
    return (
        <NodeViewWrapper
423
            as="div"
424
425
426
427
            style={getWrapperStyle()}
            ref={wrapperRef}
            onClick={(e) => {
                e.stopPropagation();
Яков's avatar
update    
Яков committed
428
429
430
431
432
433
434
435
436
                try {
                    const pos = getPos?.()
                    if (typeof pos === 'number') {
                        editor.commands.setNodeSelection(pos)
                    }
                } catch (e) {
                    console.warn('getPos() failed:', e)
                }
                // editor.commands.setNodeSelection(getPos());
yakoff94's avatar
yakoff94 committed
437
            }}
438
439
            contentEditable={false}
            data-image-wrapper
yakoff94's avatar
yakoff94 committed
440
441
        >
            <img
442
443
                {...node.attrs}
                ref={imgRef}
Яков's avatar
update    
Яков committed
444
                draggable={true}
445
                style={getImageStyle()}
Яков's avatar
Яков committed
446
447
448
449
450
451
452
453
454
455
456
457
458
459
                // onLoad={() => {
                //     if (imgRef.current && !isInitialized.current && !node.attrs.width && !node.attrs.height) {
                //         const { width: editorWidth } = getEditorDimensions();
                //         const naturalWidth = imgRef.current.naturalWidth;
                //         const naturalHeight = imgRef.current.naturalHeight;
                //
                //         safeUpdateAttributes({
                //             width: naturalWidth,
                //             height: naturalHeight,
                //             'data-node-id': node.attrs['data-node-id'] || Math.random().toString(36).substr(2, 9)
                //         });
                //         isInitialized.current = true;
                //     }
                // }}
yakoff94's avatar
yakoff94 committed
460
            />
Яков's avatar
update    
Яков committed
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
            {
                node.attrs.frontAlt?.length > 0 &&
                <div
                    style={{
                        backgroundColor: '#FDE674',
                        borderRadius: '35px',
                        padding: '5px 25px',
                        textAlign: 'center',
                        color: '#000000D9',
                        position: 'absolute',
                        left: '50%',
                        transform: 'translateX(-50%)',
                        fontSize: '12px',
                        lineHeight: '16px',
                        letterSpacing: '2%',
                        fontWeight: '500',
                        bottom: '10px',
                        whiteSpace: 'pre-line'
                    }}
                >{node.attrs.frontAlt}</div>
            }
Яков's avatar
update    
Яков committed
482
483
484
            <Button
                size="default"
                shape={'circle'}
Яков's avatar
update    
Яков committed
485
                type={node.attrs.alt?.length > 0 || node.attrs.frontAlt?.length ? 'primary' : 'default'}
Яков's avatar
update    
Яков committed
486
487
488
                onClick={(e) => {
                    e.stopPropagation();
                    setTempAlt(node.attrs.alt || '');
Яков's avatar
update    
Яков committed
489
                    setTempFrontAlt(node.attrs.frontAlt || '');
Яков's avatar
update    
Яков committed
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
                    setAltModalVisible(true);
                }}
                style={{
                    position: 'absolute',
                    top: 4,
                    right: '30px',
                    zIndex: 15,
                }}
            >
                <FontSizeOutlined />
            </Button>
            {selected && (
                <Button
                    type="text"
                    danger
                    size="small"
                    onClick={(e) => {
                        e.stopPropagation()

                        const pos = getPos?.()
                        if (typeof pos === 'number') {
                            editor.view.dispatch(
                                editor.view.state.tr.delete(pos, pos + node.nodeSize)
                            )
                        }
                    }}
                    style={{
                        position: 'absolute',
                        top: 4,
                        right: 4,
                        zIndex: 30,
                        backgroundColor: 'white',
                        border: '1px solid #d9d9d9',
                        borderRadius: '50%',
                        width: 20,
                        height: 20,
                        fontSize: 12,
                        lineHeight: 1,
                        padding: '0px 0px 2px 0px',
                        cursor: 'pointer'
                    }}
                >
                    ×
                </Button>
            )}
Яков's avatar
update    
Яков committed
535
            {(selected || isResizing) && (
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
                <Fragment>
                    {['nw', 'ne', 'sw', 'se'].map(dir => (
                        <div
                            key={dir}
                            onMouseDown={handleResizeStart(dir)}
                            style={{
                                position: 'absolute',
                                width: 12,
                                height: 12,
                                backgroundColor: BORDER_COLOR,
                                border: '1px solid white',
                                [dir[0] === 'n' ? 'top' : 'bottom']: -6,
                                [dir[1] === 'w' ? 'left' : 'right']: node.attrs.align === 'center' ? '50%' : -6,
                                transform: node.attrs.align === 'center' ?
                                    `translateX(${dir[1] === 'w' ? '-100%' : '0%'})` : 'none',
                                cursor: `${dir}-resize`,
                                zIndex: 10
                            }}
                        />
yakoff94's avatar
yakoff94 committed
555
                    ))}
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571

                    {showAlignMenu && (
                        <div style={{
                            position: 'absolute',
                            top: -40,
                            left: '50%',
                            transform: 'translateX(-50%)',
                            backgroundColor: 'white',
                            boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
                            borderRadius: 4,
                            padding: 4,
                            zIndex: 20,
                            display: 'flex'
                        }}>
                            {ALIGN_OPTIONS.map(align => (
                                <button
Яков's avatar
update    
Яков committed
572
                                    type="button"
573
574
575
576
                                    key={align}
                                    onClick={() => handleAlign(align)}
                                    style={{
                                        margin: '0 2px',
577
                                        padding: '10px 8px',
578
579
580
581
582
583
584
585
586
587
588
589
590
                                        background: node.attrs.align === align ? '#e6f7ff' : 'transparent',
                                        border: '1px solid #d9d9d9',
                                        borderRadius: 2,
                                        cursor: 'pointer'
                                    }}
                                >
                                    {align}
                                </button>
                            ))}
                        </div>
                    )}

                    <button
Яков's avatar
update    
Яков committed
591
                        type="button"
592
593
594
595
596
597
598
                        onClick={(e) => {
                            e.stopPropagation();
                            setShowAlignMenu(!showAlignMenu);
                        }}
                        style={{
                            position: 'absolute',
                            top: -30,
Яков's avatar
update    
Яков committed
599
                            left: 'calc(50% - 6px)',
600
601
602
603
                            transform: 'translateX(-50%)',
                            backgroundColor: 'white',
                            border: `1px solid ${BORDER_COLOR}`,
                            borderRadius: 4,
604
                            padding: '8px 8px',
605
606
607
608
609
610
611
612
                            cursor: 'pointer',
                            fontSize: 12,
                            zIndex: 10
                        }}
                    >
                        Align
                    </button>
                </Fragment>
yakoff94's avatar
yakoff94 committed
613
            )}
Яков's avatar
update    
Яков committed
614
            <Modal
Яков's avatar
update    
Яков committed
615
                title="Текст на картинке"
Яков's avatar
update    
Яков committed
616
617
                open={altModalVisible}
                onOk={() => {
Яков's avatar
update    
Яков committed
618
                    updateAttributes({ alt: tempAlt, frontAlt: tempFrontAlt });
Яков's avatar
update    
Яков committed
619
620
621
622
623
624
                    setAltModalVisible(false);
                }}
                onCancel={() => setAltModalVisible(false)}
                okText="Применить"
                cancelText="Отмена"
            >
Яков's avatar
update    
Яков committed
625
626
627
628
629
630
631
632
                <div style={{marginBottom: '5px'}}><Text>Лицевая сторона</Text></div>
                <TextArea
                    value={tempFrontAlt}
                    onChange={(e) => setTempFrontAlt(e.target.value)}
                    rows={4}
                    placeholder="Введите текст"
                />
                <div style={{marginTop: '15px', marginBottom: '5px'}}><Text>Обратная сторона</Text></div>
Яков's avatar
update    
Яков committed
633
634
635
636
                <TextArea
                    value={tempAlt}
                    onChange={(e) => setTempAlt(e.target.value)}
                    rows={4}
Яков's avatar
update    
Яков committed
637
                    placeholder="Введите текст"
Яков's avatar
update    
Яков committed
638
639
                />
            </Modal>
yakoff94's avatar
yakoff94 committed
640
641
642
643
644
645
646
647
        </NodeViewWrapper>
    );
};

const ResizableImageExtension = TipTapImage.extend({
    addAttributes() {
        return {
            ...this.parent?.(),
Яков's avatar
fix    
Яков committed
648
            src: { default: null },
Яков's avatar
update    
Яков committed
649
650
651
652
653
654
655
656
657
658
659
660
            alt: {
                default: null,
                parseHTML: element => {
                    const raw = element.getAttribute('alt')
                    return raw?.replace(/&#10;/g, '\n') || null
                },
                renderHTML: attributes => {
                    return attributes.alt
                        ? { alt: attributes.alt.replace(/\n/g, '&#10;') }
                        : {}
                }
            },
Яков's avatar
update    
Яков committed
661
662
663
664
665
666
667
668
669
670
671
672
            frontAlt: {
                default: null,
                parseHTML: element => {
                    const raw = element.getAttribute('frontAlt')
                    return raw?.replace(/&#10;/g, '\n') || null
                },
                renderHTML: attributes => {
                    return attributes.frontAlt
                        ? { frontAlt: attributes.frontAlt.replace(/\n/g, '&#10;') }
                        : {}
                }
            },
Яков's avatar
fix    
Яков committed
673
            title: { default: null },
674
675
            width: {
                default: null,
Яков's avatar
fix    
Яков committed
676
                parseHTML: element => parseInt(element.getAttribute('width'), 10) || null,
677
678
679
680
                renderHTML: attributes => attributes.width ? { width: attributes.width } : {}
            },
            height: {
                default: null,
Яков's avatar
fix    
Яков committed
681
                parseHTML: element => parseInt(element.getAttribute('height'), 10) || null,
682
683
684
685
686
687
                renderHTML: attributes => attributes.height ? { height: attributes.height } : {}
            },
            align: {
                default: 'left',
                parseHTML: element => element.getAttribute('data-align') || 'left',
                renderHTML: attributes => ({ 'data-align': attributes.align })
Яков's avatar
fix    
Яков committed
688
689
690
691
692
            },
            'data-node-id': {
                default: null,
                parseHTML: element => element.getAttribute('data-node-id'),
                renderHTML: attributes => ({ 'data-node-id': attributes['data-node-id'] })
693
            }
yakoff94's avatar
yakoff94 committed
694
695
        };
    },
Яков's avatar
update    
Яков committed
696
697
698
699
700
701
702
703
704
705
706
707
708
709
    renderHTML({ node, HTMLAttributes }) {
        const {
            src,
            alt = '',
            title = '',
            width,
            height,
            ...rest
        } = HTMLAttributes;

        const align = node.attrs.align || 'left';

        const style = [];

Яков's avatar
fix    
Яков committed
710

Яков's avatar
update    
Яков committed
711
712
713
        if (align === 'center') {
            style.push('display: block', 'margin-left: auto', 'margin-right: auto');
        } else if (align === 'left') {
Яков's avatar
fix    
Яков committed
714
            style.push('float: left', 'margin-right: 1rem');
Яков's avatar
update    
Яков committed
715
        } else if (align === 'right') {
Яков's avatar
fix    
Яков committed
716
            style.push('float: right', 'margin-left: 1rem');
Яков's avatar
update    
Яков committed
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
        } else if (align === 'text') {
            style.push('display: inline-block', 'vertical-align: middle', 'margin: 0 0.2rem');
        }

        if (width) style.push(`width: ${width}px`);
        if (height) style.push(`height: ${height}px`);

        return [
            'img',
            {
                src,
                alt,
                title,
                width,
                height,
                'data-align': align,
                style: style.join('; '),
                ...rest,
            }
        ];
    },
738

yakoff94's avatar
yakoff94 committed
739
740
    addNodeView() {
        return ReactNodeViewRenderer(ResizableImageTemplate);
Яков's avatar
fix    
Яков committed
741
742
743
744
745
746
747
748
    },

    addKeyboardShortcuts() {
        return {
            'Mod-ArrowLeft': () => this.editor.commands.updateAttributes(this.type.name, { align: 'left' }),
            'Mod-ArrowRight': () => this.editor.commands.updateAttributes(this.type.name, { align: 'right' }),
            'Mod-ArrowDown': () => this.editor.commands.updateAttributes(this.type.name, { align: 'center' }),
        };
749
750
751
752
753
    }
}).configure({
    inline: true,
    group: 'inline',
    draggable: true,
Яков's avatar
fix    
Яков committed
754
    selectable: true
755
});
yakoff94's avatar
yakoff94 committed
756
757

export default ResizableImageExtension;