Barazo default frontend barazo.forum
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(editor): smart link paste wraps selected text as markdown link (#217)

When text is selected in the MarkdownEditor and a URL (http/https) is
pasted, the selected text is preserved and wrapped as [text](url)
instead of being replaced. Normal paste behavior is unchanged when no
text is selected or when pasted content is not a URL.

Closes singi-labs/barazo-workspace#124

authored by

Guido X Jansen and committed by
GitHub
c2cab861 e643c985

+112
+83
src/components/markdown-editor.test.tsx
··· 132 132 expect(screen.getByRole('textbox', { name: 'Content' })).toHaveAttribute('aria-invalid', 'true') 133 133 }) 134 134 135 + describe('smart link paste', () => { 136 + function pasteUrl(textarea: HTMLTextAreaElement, url: string) { 137 + const event = new Event('paste', { bubbles: true, cancelable: true }) 138 + Object.defineProperty(event, 'clipboardData', { 139 + value: { getData: (type: string) => (type === 'text/plain' ? url : '') }, 140 + }) 141 + textarea.dispatchEvent(event) 142 + return event 143 + } 144 + 145 + it('wraps selected text as markdown link when pasting a URL', () => { 146 + const onChange = vi.fn() 147 + render( 148 + <MarkdownEditor value="check this out" onChange={onChange} id="content" label="Content" /> 149 + ) 150 + const textarea = screen.getByRole('textbox', { name: 'Content' }) as HTMLTextAreaElement 151 + 152 + // Select "this" 153 + textarea.setSelectionRange(6, 10) 154 + pasteUrl(textarea, 'https://example.com') 155 + 156 + expect(onChange).toHaveBeenCalledWith('check [this](https://example.com) out') 157 + }) 158 + 159 + it('does not intercept paste when no text is selected', () => { 160 + const onChange = vi.fn() 161 + render(<MarkdownEditor value="hello " onChange={onChange} id="content" label="Content" />) 162 + const textarea = screen.getByRole('textbox', { name: 'Content' }) as HTMLTextAreaElement 163 + 164 + textarea.setSelectionRange(6, 6) 165 + const event = pasteUrl(textarea, 'https://example.com') 166 + 167 + // Event should not be prevented — default paste behavior 168 + expect(event.defaultPrevented).toBe(false) 169 + expect(onChange).not.toHaveBeenCalled() 170 + }) 171 + 172 + it('does not intercept paste when pasted text is not a URL', () => { 173 + const onChange = vi.fn() 174 + render( 175 + <MarkdownEditor value="hello world" onChange={onChange} id="content" label="Content" /> 176 + ) 177 + const textarea = screen.getByRole('textbox', { name: 'Content' }) as HTMLTextAreaElement 178 + 179 + textarea.setSelectionRange(6, 11) 180 + const event = pasteUrl(textarea, 'not a url') 181 + 182 + expect(event.defaultPrevented).toBe(false) 183 + expect(onChange).not.toHaveBeenCalled() 184 + }) 185 + 186 + it('handles http:// URLs', () => { 187 + const onChange = vi.fn() 188 + render( 189 + <MarkdownEditor 190 + value="click here please" 191 + onChange={onChange} 192 + id="content" 193 + label="Content" 194 + /> 195 + ) 196 + const textarea = screen.getByRole('textbox', { name: 'Content' }) as HTMLTextAreaElement 197 + 198 + textarea.setSelectionRange(6, 10) 199 + pasteUrl(textarea, 'http://example.com') 200 + 201 + expect(onChange).toHaveBeenCalledWith('click [here](http://example.com) please') 202 + }) 203 + 204 + it('places cursor after the inserted link', () => { 205 + const onChange = vi.fn() 206 + render( 207 + <MarkdownEditor value="see docs now" onChange={onChange} id="content" label="Content" /> 208 + ) 209 + const textarea = screen.getByRole('textbox', { name: 'Content' }) as HTMLTextAreaElement 210 + 211 + textarea.setSelectionRange(4, 8) 212 + pasteUrl(textarea, 'https://docs.example.com') 213 + 214 + expect(onChange).toHaveBeenCalledWith('see [docs](https://docs.example.com) now') 215 + }) 216 + }) 217 + 135 218 it('passes axe accessibility check', async () => { 136 219 const { container } = render( 137 220 <MarkdownEditor value="Some content" onChange={vi.fn()} id="content" label="Content" />
+29
src/components/markdown-editor.tsx
··· 59 59 [value, onChange] 60 60 ) 61 61 62 + const handlePaste = useCallback( 63 + (e: React.ClipboardEvent<HTMLTextAreaElement>) => { 64 + const textarea = textareaRef.current 65 + if (!textarea) return 66 + 67 + const start = textarea.selectionStart 68 + const end = textarea.selectionEnd 69 + if (start === end) return // No selection — let default paste happen 70 + 71 + const pasted = e.clipboardData.getData('text/plain') 72 + if (!/^https?:\/\/\S+$/.test(pasted)) return // Not a URL 73 + 74 + e.preventDefault() 75 + const before = value.slice(0, start) 76 + const selected = value.slice(start, end) 77 + const after = value.slice(end) 78 + const link = `[${selected}](${pasted})` 79 + onChange(before + link + after) 80 + 81 + const cursorPos = start + link.length 82 + requestAnimationFrame(() => { 83 + textarea.focus() 84 + textarea.setSelectionRange(cursorPos, cursorPos) 85 + }) 86 + }, 87 + [value, onChange] 88 + ) 89 + 62 90 const handleToolbarKeyDown = useCallback( 63 91 (e: React.KeyboardEvent<HTMLDivElement>) => { 64 92 const buttons = toolbarRef.current?.querySelectorAll<HTMLButtonElement>('button') ··· 127 155 id={id} 128 156 value={value} 129 157 onChange={(e) => onChange(e.target.value)} 158 + onPaste={handlePaste} 130 159 placeholder={placeholder ?? 'Write your content using Markdown...'} 131 160 required={required} 132 161 aria-invalid={error ? 'true' : undefined}