Image.jsx 29.7 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
        const startWidth = node.attrs.width || imgRef.current.naturalWidth;
        const startHeight = node.attrs.height || imgRef.current.naturalHeight;
        const aspectRatio = startWidth / startHeight;
Яков's avatar
update    
Яков committed
281
282
283
284
285
286

        const getClientX = (e) => e.touches ? e.touches[0].clientX : e.clientX;
        const getClientY = (e) => e.touches ? e.touches[0].clientY : e.clientY;

        const startX = getClientX(e);
        const startY = getClientY(e);
Яков's avatar
Яков committed
287
        const { width: initialEditorWidth, availableSpace: initialAvailableSpace } = getEditorDimensions();
288

Яков's avatar
update    
Яков committed
289
290
        const onMove = (e) => {
            if (e.cancelable) e.preventDefault();
Яков's avatar
Яков committed
291
292
293
            requestAnimationFrame(() => {
                const maxWidth = node.attrs.align === 'center' ? initialEditorWidth : initialAvailableSpace;

Яков's avatar
update    
Яков committed
294
295
                const deltaX = getClientX(e) - startX;
                const deltaY = getClientY(e) - startY;
Яков's avatar
Яков committed
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312

                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);
                    }
313
                } else {
Яков's avatar
Яков committed
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
                    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);
                    }
330
331
                }

Яков's avatar
Яков committed
332
333
                safeUpdateAttributes({ width: newWidth, height: newHeight });
            });
yakoff94's avatar
yakoff94 committed
334
335
        };

Яков's avatar
update    
Яков committed
336
337
338
339
340
        const onEnd = () => {
            window.removeEventListener('mousemove', onMove);
            window.removeEventListener('mouseup', onEnd);
            window.removeEventListener('touchmove', onMove);
            window.removeEventListener('touchend', onEnd);
Яков's avatar
update    
Яков committed
341
            setIsResizing(false);
Яков's avatar
update    
Яков committed
342
343
344
345
346
347
348
349
            try {
                const pos = getPos?.()
                if (typeof pos === 'number') {
                    editor.commands.setNodeSelection(pos)
                }
            } catch (e) {
                console.warn('getPos() failed:', e)
            }
Яков's avatar
fix    
Яков committed
350
            editor.commands.focus();
yakoff94's avatar
yakoff94 committed
351
352
        };

Яков's avatar
update    
Яков committed
353
354
355
356
        window.addEventListener('mousemove', onMove);
        window.addEventListener('mouseup', onEnd);
        window.addEventListener('touchmove', onMove, { passive: false });
        window.addEventListener('touchend', onEnd);
357
    };
yakoff94's avatar
yakoff94 committed
358

Яков's avatar
fix    
Яков committed
359
    // Изменение выравнивания с автоматическим масштабированием
360
    const handleAlign = (align) => {
Яков's avatar
update    
Яков committed
361
        safeUpdateAttributes({ align });
Яков's avatar
Яков committed
362
        setTimeout(() => {
Яков's avatar
update    
Яков committed
363
364
365
366
367
368
369
370
371
            safeUpdateAttributes({ align });
            try {
                const pos = getPos?.()
                if (typeof pos === 'number') {
                    editor.commands.setNodeSelection(pos)
                }
            } catch (e) {
                console.warn('getPos() failed:', e)
            }
Яков's avatar
Яков committed
372
        }, 50);
373
374
375
        setShowAlignMenu(false);
    };

Яков's avatar
fix    
Яков committed
376
    // Стили для обертки изображения
Яков's avatar
update    
Яков committed
377
378
    const getWrapperStyle = () => {
        const baseStyle = {
379
            display: 'inline-block',
Яков's avatar
update    
Яков committed
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
            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',
Яков's avatar
update    
Яков committed
405
                maxWidth: '100%',
Яков's avatar
update    
Яков committed
406
407
408
409
410
            }),
            ...(node.attrs.align === 'right' && {
                float: 'right',
                marginLeft: '1rem',
                width: node.attrs.width ? `${node.attrs.width}px` : 'auto',
Яков's avatar
update    
Яков committed
411
                maxWidth: '100%',
Яков's avatar
update    
Яков committed
412
413
414
415
416
417
418
            }),
            ...(node.attrs.align === 'text' && {
                display: 'inline-block',
                float: 'none',
                margin: '0 0.2rem',
                verticalAlign: 'middle',
                width: node.attrs.width ? `${node.attrs.width}px` : 'auto',
Яков's avatar
update    
Яков committed
419
                maxWidth: '100%',
Яков's avatar
update    
Яков committed
420
421
422
            }),
        };
    };
yakoff94's avatar
yakoff94 committed
423

Яков's avatar
fix    
Яков committed
424
    // Стили для самого изображения
425
426
    const getImageStyle = () => ({
        width: node.attrs.width ? `${node.attrs.width}px` : 'auto',
Яков's avatar
fix    
Яков committed
427
        height: 'auto',
428
429
430
431
        maxWidth: '100%',
        display: 'block',
        cursor: 'default',
        userSelect: 'none',
Яков's avatar
Яков committed
432
        margin: node.attrs.align === 'center' ? '0 auto' : '0',
Яков's avatar
update    
Яков committed
433
        verticalAlign: node.attrs.align === 'text' ? 'middle' : 'top',
Яков's avatar
fix    
Яков committed
434
        objectFit: 'contain'
435
436
    });

Яков's avatar
update    
Яков committed
437
    console.log(node.attrs.frontAlt);
yakoff94's avatar
yakoff94 committed
438
439
    return (
        <NodeViewWrapper
440
            as="div"
441
442
443
444
            style={getWrapperStyle()}
            ref={wrapperRef}
            onClick={(e) => {
                e.stopPropagation();
Яков's avatar
update    
Яков committed
445
446
447
448
449
450
451
452
453
                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
454
            }}
455
456
            contentEditable={false}
            data-image-wrapper
yakoff94's avatar
yakoff94 committed
457
458
        >
            <img
459
460
                {...node.attrs}
                ref={imgRef}
Яков's avatar
update    
Яков committed
461
                draggable={true}
462
                style={getImageStyle()}
Яков's avatar
Яков committed
463
464
465
466
467
468
469
470
471
472
473
474
475
476
                // 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
477
            />
Яков's avatar
update    
Яков committed
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
            {
                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
499
500
501
            <Button
                size="default"
                shape={'circle'}
Яков's avatar
update    
Яков committed
502
                type={node.attrs.alt?.length > 0 || node.attrs.frontAlt?.length ? 'primary' : 'default'}
Яков's avatar
update    
Яков committed
503
504
505
                onClick={(e) => {
                    e.stopPropagation();
                    setTempAlt(node.attrs.alt || '');
Яков's avatar
update    
Яков committed
506
                    setTempFrontAlt(node.attrs.frontAlt || '');
Яков's avatar
update    
Яков committed
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
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
                    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
552
            {(selected || isResizing) && (
553
554
555
556
557
                <Fragment>
                    {['nw', 'ne', 'sw', 'se'].map(dir => (
                        <div
                            key={dir}
                            onMouseDown={handleResizeStart(dir)}
Яков's avatar
update    
Яков committed
558
                            onTouchStart={handleResizeStart(dir)}
559
560
561
562
563
564
565
566
567
568
569
570
571
572
                            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
573
                    ))}
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589

                    {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
590
                                    type="button"
591
592
593
594
                                    key={align}
                                    onClick={() => handleAlign(align)}
                                    style={{
                                        margin: '0 2px',
595
                                        padding: '10px 8px',
596
597
598
599
600
601
602
603
604
605
606
607
608
                                        background: node.attrs.align === align ? '#e6f7ff' : 'transparent',
                                        border: '1px solid #d9d9d9',
                                        borderRadius: 2,
                                        cursor: 'pointer'
                                    }}
                                >
                                    {align}
                                </button>
                            ))}
                        </div>
                    )}

                    <button
Яков's avatar
update    
Яков committed
609
                        type="button"
610
611
612
613
614
615
616
                        onClick={(e) => {
                            e.stopPropagation();
                            setShowAlignMenu(!showAlignMenu);
                        }}
                        style={{
                            position: 'absolute',
                            top: -30,
Яков's avatar
update    
Яков committed
617
                            left: 'calc(50% - 6px)',
618
619
620
621
                            transform: 'translateX(-50%)',
                            backgroundColor: 'white',
                            border: `1px solid ${BORDER_COLOR}`,
                            borderRadius: 4,
622
                            padding: '8px 8px',
623
624
625
626
627
628
629
630
                            cursor: 'pointer',
                            fontSize: 12,
                            zIndex: 10
                        }}
                    >
                        Align
                    </button>
                </Fragment>
yakoff94's avatar
yakoff94 committed
631
            )}
Яков's avatar
update    
Яков committed
632
            <Modal
Яков's avatar
update    
Яков committed
633
                title="Текст на картинке"
Яков's avatar
update    
Яков committed
634
635
                open={altModalVisible}
                onOk={() => {
Яков's avatar
update    
Яков committed
636
                    updateAttributes({ alt: tempAlt, frontAlt: tempFrontAlt });
Яков's avatar
update    
Яков committed
637
638
639
640
641
642
                    setAltModalVisible(false);
                }}
                onCancel={() => setAltModalVisible(false)}
                okText="Применить"
                cancelText="Отмена"
            >
Яков's avatar
update    
Яков committed
643
644
645
646
647
648
649
650
                <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
651
652
653
654
                <TextArea
                    value={tempAlt}
                    onChange={(e) => setTempAlt(e.target.value)}
                    rows={4}
Яков's avatar
update    
Яков committed
655
                    placeholder="Введите текст"
Яков's avatar
update    
Яков committed
656
657
                />
            </Modal>
yakoff94's avatar
yakoff94 committed
658
659
660
661
662
663
664
665
        </NodeViewWrapper>
    );
};

const ResizableImageExtension = TipTapImage.extend({
    addAttributes() {
        return {
            ...this.parent?.(),
Яков's avatar
fix    
Яков committed
666
            src: { default: null },
Яков's avatar
update    
Яков committed
667
668
669
670
671
672
673
674
675
676
677
678
            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
679
680
681
682
683
684
685
686
687
688
689
690
            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
691
            title: { default: null },
692
693
            width: {
                default: null,
Яков's avatar
fix    
Яков committed
694
                parseHTML: element => parseInt(element.getAttribute('width'), 10) || null,
695
696
697
698
                renderHTML: attributes => attributes.width ? { width: attributes.width } : {}
            },
            height: {
                default: null,
Яков's avatar
fix    
Яков committed
699
                parseHTML: element => parseInt(element.getAttribute('height'), 10) || null,
700
701
702
703
704
705
                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
706
707
708
709
710
            },
            'data-node-id': {
                default: null,
                parseHTML: element => element.getAttribute('data-node-id'),
                renderHTML: attributes => ({ 'data-node-id': attributes['data-node-id'] })
711
            }
yakoff94's avatar
yakoff94 committed
712
713
        };
    },
Яков's avatar
update    
Яков committed
714
715
716
717
718
719
720
721
722
723
724
725
726
727
    renderHTML({ node, HTMLAttributes }) {
        const {
            src,
            alt = '',
            title = '',
            width,
            height,
            ...rest
        } = HTMLAttributes;

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

        const style = [];

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

Яков's avatar
update    
Яков committed
729
730
731
        if (align === 'center') {
            style.push('display: block', 'margin-left: auto', 'margin-right: auto');
        } else if (align === 'left') {
Яков's avatar
fix    
Яков committed
732
            style.push('float: left', 'margin-right: 1rem');
Яков's avatar
update    
Яков committed
733
        } else if (align === 'right') {
Яков's avatar
fix    
Яков committed
734
            style.push('float: right', 'margin-left: 1rem');
Яков's avatar
update    
Яков committed
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
        } 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,
            }
        ];
    },
756

yakoff94's avatar
yakoff94 committed
757
758
    addNodeView() {
        return ReactNodeViewRenderer(ResizableImageTemplate);
Яков's avatar
fix    
Яков committed
759
760
761
762
763
764
765
766
    },

    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' }),
        };
767
768
769
770
771
    }
}).configure({
    inline: true,
    group: 'inline',
    draggable: true,
Яков's avatar
fix    
Яков committed
772
    selectable: true
773
});
yakoff94's avatar
yakoff94 committed
774
775

export default ResizableImageExtension;