@@ -55,10 +55,13 @@ import {
55
55
BoldExtension ,
56
56
CalloutExtension ,
57
57
ItalicExtension ,
58
+ LinkExtension ,
58
59
PlaceholderExtension ,
60
+ ShortcutHandlerProps ,
59
61
SubExtension ,
60
62
SupExtension ,
61
63
TextHighlightExtension ,
64
+ createMarkPositioner ,
62
65
wysiwygPreset ,
63
66
} from "remirror/extensions" ;
64
67
import {
@@ -84,16 +87,246 @@ import {
84
87
BaselineButtonGroup ,
85
88
CommandButton ,
86
89
CommandButtonProps ,
90
+ useChainedCommands ,
91
+ useCurrentSelection ,
92
+ useAttrs ,
93
+ useUpdateReason ,
94
+ FloatingWrapper ,
87
95
} from "@remirror/react" ;
88
96
import { WysiwygEditor } from "@remirror/react-editors/wysiwyg" ;
89
- import { FloatingToolbar } from "@remirror/react" ;
97
+ import { FloatingToolbar , useExtensionEvent } from "@remirror/react" ;
90
98
import { TableExtension } from "@remirror/extension-react-tables" ;
91
99
import { GenIcon , IconBase } from "@remirror/react-components" ;
92
100
import "remirror/styles/all.css" ;
93
101
94
102
import { htmlToProsemirrorNode } from "remirror" ;
95
103
import { styled } from "@mui/material" ;
96
104
105
+ function useLinkShortcut ( ) {
106
+ const [ linkShortcut , setLinkShortcut ] = useState <
107
+ ShortcutHandlerProps | undefined
108
+ > ( ) ;
109
+ const [ isEditing , setIsEditing ] = useState ( false ) ;
110
+
111
+ useExtensionEvent (
112
+ LinkExtension ,
113
+ "onShortcut" ,
114
+ useCallback (
115
+ ( props ) => {
116
+ if ( ! isEditing ) {
117
+ setIsEditing ( true ) ;
118
+ }
119
+
120
+ return setLinkShortcut ( props ) ;
121
+ } ,
122
+ [ isEditing ]
123
+ )
124
+ ) ;
125
+
126
+ return { linkShortcut, isEditing, setIsEditing } ;
127
+ }
128
+
129
+ function useFloatingLinkState ( ) {
130
+ const chain = useChainedCommands ( ) ;
131
+ const { isEditing, linkShortcut, setIsEditing } = useLinkShortcut ( ) ;
132
+ const { to, empty } = useCurrentSelection ( ) ;
133
+
134
+ const url = ( useAttrs ( ) . link ( ) ?. href as string ) ?? "" ;
135
+ const [ href , setHref ] = useState < string > ( url ) ;
136
+
137
+ // A positioner which only shows for links.
138
+ const linkPositioner = React . useMemo (
139
+ ( ) => createMarkPositioner ( { type : "link" } ) ,
140
+ [ ]
141
+ ) ;
142
+
143
+ const onRemove = useCallback ( ( ) => {
144
+ return chain . removeLink ( ) . focus ( ) . run ( ) ;
145
+ } , [ chain ] ) ;
146
+
147
+ const updateReason = useUpdateReason ( ) ;
148
+
149
+ React . useLayoutEffect ( ( ) => {
150
+ if ( ! isEditing ) {
151
+ return ;
152
+ }
153
+
154
+ if ( updateReason . doc || updateReason . selection ) {
155
+ setIsEditing ( false ) ;
156
+ }
157
+ } , [ isEditing , setIsEditing , updateReason . doc , updateReason . selection ] ) ;
158
+
159
+ useEffect ( ( ) => {
160
+ setHref ( url ) ;
161
+ } , [ url ] ) ;
162
+
163
+ const submitHref = useCallback ( ( ) => {
164
+ setIsEditing ( false ) ;
165
+ const range = linkShortcut ?? undefined ;
166
+
167
+ if ( href === "" ) {
168
+ chain . removeLink ( ) ;
169
+ } else {
170
+ chain . updateLink ( { href, auto : false } , range ) ;
171
+ }
172
+
173
+ chain . focus ( range ?. to ?? to ) . run ( ) ;
174
+ } , [ setIsEditing , linkShortcut , chain , href , to ] ) ;
175
+
176
+ const cancelHref = useCallback ( ( ) => {
177
+ setIsEditing ( false ) ;
178
+ } , [ setIsEditing ] ) ;
179
+
180
+ const clickEdit = useCallback ( ( ) => {
181
+ if ( empty ) {
182
+ chain . selectLink ( ) ;
183
+ }
184
+
185
+ setIsEditing ( true ) ;
186
+ } , [ chain , empty , setIsEditing ] ) ;
187
+
188
+ return React . useMemo (
189
+ ( ) => ( {
190
+ href,
191
+ setHref,
192
+ linkShortcut,
193
+ linkPositioner,
194
+ isEditing,
195
+ clickEdit,
196
+ onRemove,
197
+ submitHref,
198
+ cancelHref,
199
+ } ) ,
200
+ [
201
+ href ,
202
+ linkShortcut ,
203
+ linkPositioner ,
204
+ isEditing ,
205
+ clickEdit ,
206
+ onRemove ,
207
+ submitHref ,
208
+ cancelHref ,
209
+ ]
210
+ ) ;
211
+ }
212
+
213
+ const DelayAutoFocusInput = ( {
214
+ autoFocus,
215
+ ...rest
216
+ } : React . HTMLProps < HTMLInputElement > ) => {
217
+ const inputRef = useRef < HTMLInputElement > ( null ) ;
218
+
219
+ useEffect ( ( ) => {
220
+ if ( ! autoFocus ) {
221
+ return ;
222
+ }
223
+
224
+ const frame = window . requestAnimationFrame ( ( ) => {
225
+ inputRef . current ?. focus ( ) ;
226
+ } ) ;
227
+
228
+ return ( ) => {
229
+ window . cancelAnimationFrame ( frame ) ;
230
+ } ;
231
+ } , [ autoFocus ] ) ;
232
+
233
+ return < input ref = { inputRef } { ...rest } /> ;
234
+ } ;
235
+
236
+ const FloatingLinkToolbar = ( { children } ) => {
237
+ const {
238
+ isEditing,
239
+ linkPositioner,
240
+ clickEdit,
241
+ onRemove,
242
+ submitHref,
243
+ href,
244
+ setHref,
245
+ cancelHref,
246
+ } = useFloatingLinkState ( ) ;
247
+ const active = useActive ( ) ;
248
+ const activeLink = active . link ( ) ;
249
+ const { empty } = useCurrentSelection ( ) ;
250
+
251
+ const handleClickEdit = useCallback ( ( ) => {
252
+ clickEdit ( ) ;
253
+ } , [ clickEdit ] ) ;
254
+
255
+ const linkEditButtons = activeLink ? (
256
+ < CommandButtonGroup >
257
+ < CommandButton
258
+ commandName = "updateLink"
259
+ aria-label = "Edit link"
260
+ onSelect = { handleClickEdit }
261
+ icon = "pencilLine"
262
+ enabled
263
+ />
264
+ < CommandButton
265
+ commandName = "removeLink"
266
+ aria-label = "Remove link"
267
+ onSelect = { onRemove }
268
+ icon = "linkUnlink"
269
+ enabled
270
+ />
271
+ </ CommandButtonGroup >
272
+ ) : (
273
+ < CommandButtonGroup >
274
+ < CommandButton
275
+ commandName = "updateLink"
276
+ aria-label = "Add link"
277
+ onSelect = { handleClickEdit }
278
+ icon = "link"
279
+ enabled
280
+ />
281
+ </ CommandButtonGroup >
282
+ ) ;
283
+
284
+ return (
285
+ < >
286
+ { ! isEditing && (
287
+ < FloatingToolbar >
288
+ { linkEditButtons }
289
+ { children }
290
+ </ FloatingToolbar >
291
+ ) }
292
+ { ! isEditing && empty && (
293
+ < FloatingToolbar positioner = { linkPositioner } >
294
+ { linkEditButtons }
295
+ { children }
296
+ </ FloatingToolbar >
297
+ ) }
298
+
299
+ < FloatingWrapper
300
+ positioner = "always"
301
+ placement = "bottom"
302
+ enabled = { isEditing }
303
+ renderOutsideEditor
304
+ >
305
+ < DelayAutoFocusInput
306
+ style = { { zIndex : 20 } }
307
+ autoFocus
308
+ placeholder = "Enter link..."
309
+ onChange = { ( event : React . ChangeEvent < HTMLInputElement > ) =>
310
+ setHref ( event . target . value )
311
+ }
312
+ value = { href }
313
+ onKeyPress = { ( event : React . KeyboardEvent < HTMLInputElement > ) => {
314
+ const { code } = event ;
315
+
316
+ if ( code === "Enter" ) {
317
+ submitHref ( ) ;
318
+ }
319
+
320
+ if ( code === "Escape" ) {
321
+ cancelHref ( ) ;
322
+ }
323
+ } }
324
+ />
325
+ </ FloatingWrapper >
326
+ </ >
327
+ ) ;
328
+ } ;
329
+
97
330
export interface SetHighlightButtonProps
98
331
extends Omit <
99
332
CommandButtonProps ,
@@ -175,6 +408,7 @@ const MyEditor = ({
175
408
new TextHighlightExtension ( ) ,
176
409
new SupExtension ( ) ,
177
410
new SubExtension ( ) ,
411
+ new LinkExtension ( { autoLink : true } ) ,
178
412
// new CalloutExtension({ defaultType: "warn" }),
179
413
...wysiwygPreset ( ) ,
180
414
] ,
@@ -230,33 +464,33 @@ const MyEditor = ({
230
464
>
231
465
{ /* <WysiwygToolbar /> */ }
232
466
< EditorComponent />
467
+
233
468
< TableComponents />
234
469
235
470
{ ! isGuest && (
236
- < FloatingToolbar >
471
+ < FloatingLinkToolbar >
472
+ < FormattingButtonGroup />
473
+ < VerticalDivider />
237
474
< CommandButtonGroup >
238
475
{ /* <HeadingLevelButtonGroup /> */ }
239
- { /* <VerticalDivider /> */ }
240
- < FormattingButtonGroup />
476
+
241
477
{ /* <ListButtonGroup /> */ }
242
478
< SetHighlightButton color = "lightpink" />
243
479
< SetHighlightButton color = "yellow" />
244
480
< SetHighlightButton color = "lightgreen" />
245
481
< SetHighlightButton color = "lightcyan" />
246
482
< SetHighlightButton />
247
483
</ CommandButtonGroup >
484
+ < VerticalDivider />
485
+ { /* <FormattingButtonGroup /> */ }
248
486
{ /* <DecreaseIndentButton /> */ }
249
487
{ /* <IncreaseIndentButton /> */ }
250
488
{ /* <TextAlignmentButtonGroup /> */ }
251
489
{ /* <IndentationButtonGroup /> */ }
252
490
{ /* <BaselineButtonGroup /> */ }
253
- </ FloatingToolbar >
254
- ) }
255
- { ! isGuest && (
256
- < FloatingToolbar positioner = "emptyBlockStart" >
257
- < HeadingLevelButtonGroup />
258
- </ FloatingToolbar >
491
+ </ FloatingLinkToolbar >
259
492
) }
493
+
260
494
{ /* <Menu /> */ }
261
495
</ Remirror >
262
496
</ MyStyledWrapper >
0 commit comments