• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

alkem-io / client-web / #10360

12 Feb 2025 09:56AM UTC coverage: 5.784%. First build
#10360

Pull #7659

travis-ci

Pull Request #7659: Client/7571 bugfix md editor paste

192 of 11044 branches covered (1.74%)

Branch coverage included in aggregate %.

0 of 15 new or added lines in 1 file covered. (0.0%)

1546 of 19002 relevant lines covered (8.14%)

0.18 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/core/ui/forms/MarkdownInput/MarkdownInput.tsx
1
import React, { memo, useCallback, useEffect, useImperativeHandle, useRef } from 'react';
2
import { Box, useTheme } from '@mui/material';
3
import { InputBaseComponentProps } from '@mui/material/InputBase/InputBase';
4
import { CharacterCountContainer } from './CharacterCountContext';
5
import MarkdownInputControls from '../MarkdownInputControls/MarkdownInputControls';
6
import { gutters } from '@/core/ui/grid/utils';
7
import { EditorContent, Editor } from '@tiptap/react';
8
import { useStorageConfigContext } from '@/domain/storage/StorageBucket/StorageConfigContext';
9
import { useEditorConfig } from './hooks/useEditorConfig';
10
import { useImageUpload } from './hooks/useImageUpload';
11
import { useMarkdownInputUI } from './hooks/useMarkdownInputUI';
12
import { useMarkdownEditor } from './hooks/useMarkdownEditor';
13
import { MarkdownInputStyles } from './hooks/MarkdownInputStyles';
14

15
export interface MarkdownInputProps extends InputBaseComponentProps {
16
  controlsVisible?: 'always' | 'focused';
17
  maxLength?: number;
18
  hideImageOptions?: boolean;
19
  temporaryLocation?: boolean;
20
  // In React 19, ref becomes a regular prop
21
  ref?: React.Ref<MarkdownInputRefApi>;
22
}
23

24
type Offset = {
25
  x: string;
26
  y: string;
27
};
28

29
export interface MarkdownInputRefApi {
30
  focus: () => void;
31
  value: string | undefined;
32
  getLabelOffset: () => Offset;
33
}
34

35
const proseMirrorStyles = {
36
  outline: 'none',
37
  minHeight: gutters(4),
38
  padding: gutters(0.5),
39
  '& p:first-of-type': { marginTop: 0 },
40
  '& p:last-child': { marginBottom: 0 },
41
  '& img': { maxWidth: '100%' },
42
} as const;
43

44
export const MarkdownInput = memo<MarkdownInputProps>(
45
  ({
46
    ref,
47
    value,
48
    onChange,
49
    maxLength,
50
    controlsVisible = 'focused',
51
    hideImageOptions,
52
    onFocus,
53
    onBlur,
×
54
    temporaryLocation = false,
55
  }) => {
×
56
    const containerRef = useRef<HTMLDivElement>(null);
57
    const toolbarRef = useRef<HTMLDivElement>(null);
58
    const theme = useTheme();
59
    const editorRef = useRef<Editor | null>(null);
60

61
    const storageConfig = useStorageConfigContext();
62
    const storageBucketId = storageConfig?.storageBucketId;
63

64
    const {
×
65
      areControlsVisible,
66
      getLabelOffset,
67
      handleFocus,
68
      handleBlur,
69
      handleDialogOpen,
70
      handleDialogClose,
71
      prevEditorHeight,
×
72
      setPrevEditorHeight,
73
      isInteractingWithInput,
74
    } = useMarkdownInputUI({
75
      controlsVisible,
×
76
      disabled: false,
77
      toolbarRef,
78
      containerRef,
79
      onFocus,
×
80
      onBlur,
×
81
    });
82

×
83
    const { handlePaste } = useImageUpload({
×
84
      storageBucketId,
×
85
      hideImageOptions,
86
      temporaryLocation,
×
87
      getEditor: () => editorRef.current,
88
    });
×
89

90
    const editorConfig = useEditorConfig({
×
91
      handlePaste,
92
      disabled: false,
×
93
    });
×
94

×
95
    const { editor, shadowEditor } = useMarkdownEditor({
96
      value,
97
      onChange,
×
98
      maxLength,
99
      editorConfig,
×
100
      isInteractingWithInput,
101
    });
×
102

103
    // Update ref when editor changes
×
104
    useEffect(() => {
105
      editorRef.current = editor;
×
106
    }, [editor]);
107

108
    useImperativeHandle(
109
      ref,
×
110
      () => ({
111
        getLabelOffset,
112
        focus: () => editor?.commands.focus(),
113
        get value() {
×
NEW
114
          return editor?.getText();
×
115
        },
×
116
      }),
117
      [editor, getLabelOffset]
118
    );
×
119

×
120
    const keepScrollPositionOnEditorReset = useCallback(
×
121
      (editorInstance: Editor) => {
122
        const handleCreate = () => {
123
          setPrevEditorHeight(0);
×
124
        };
125

126
        editorInstance.on('create', handleCreate);
×
127

128
        return () => {
129
          editorInstance.off('create', handleCreate);
130
        };
131
      },
132
      [setPrevEditorHeight]
133
    );
134

135
    useEffect(() => {
136
      if (editor) {
137
        return keepScrollPositionOnEditorReset(editor);
138
      }
×
139
    }, [editor, keepScrollPositionOnEditorReset]);
NEW
140

×
NEW
141
    const handleBlurWithScrollPosition = useCallback(
×
142
      (event: React.FocusEvent<HTMLDivElement>) => {
143
        if (containerRef.current?.contains(event.relatedTarget)) {
144
          return;
×
145
        }
×
146
        setPrevEditorHeight(editor?.view.dom.clientHeight ?? 0);
147
        handleBlur(event);
×
NEW
148
      },
×
149
      [containerRef, editor, setPrevEditorHeight, handleBlur]
150
    );
NEW
151

×
152
    if (!editor) {
NEW
153
      return null;
×
154
    }
×
155

156
    return (
×
157
      <Box ref={containerRef} width="100%" onFocus={handleFocus} onBlur={handleBlurWithScrollPosition}>
×
158
        {MarkdownInputStyles}
×
159
        <MarkdownInputControls
160
          ref={toolbarRef}
NEW
161
          editor={editor}
×
NEW
162
          visible={areControlsVisible()}
×
163
          hideImageOptions={hideImageOptions}
NEW
164
          onDialogOpen={handleDialogOpen}
×
NEW
165
          onDialogClose={handleDialogClose}
×
166
          temporaryLocation={temporaryLocation}
NEW
167
        />
×
NEW
168
        <Box width="100%" maxHeight="50vh" sx={{ overflowY: 'auto', '.ProseMirror': proseMirrorStyles }}>
×
169
          <Box position="relative" style={{ minHeight: prevEditorHeight }}>
170
            <EditorContent editor={editor} />
NEW
171

×
NEW
172
            <CharacterCountContainer>
×
NEW
173
              {({ characterCount }) =>
×
174
                typeof maxLength === 'undefined' || characterCount <= maxLength ? null : (
175
                  <Box
176
                    position="absolute"
177
                    top={0}
178
                    left={0}
×
179
                    bottom={0}
180
                    right={0}
181
                    sx={{
182
                      pointerEvents: 'none',
183
                      color: 'transparent',
×
184
                      mark: {
×
185
                        color: theme.palette.error.main,
186
                        backgroundColor: 'transparent',
187
                      },
188
                    }}
189
                  >
190
                    <EditorContent editor={shadowEditor} />
191
                  </Box>
×
192
                )
193
              }
194
            </CharacterCountContainer>
×
195
          </Box>
196
        </Box>
×
197
      </Box>
×
198
    );
×
199
  }
200
);
201

202
export default MarkdownInput;
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc