Skip to content

Commit 19d1569

Browse files
authored
feat: use remirror link extension (#257)
* feat: add Remirror link extension * add aria label
1 parent 9c79121 commit 19d1569

File tree

1 file changed

+244
-10
lines changed

1 file changed

+244
-10
lines changed

ui/src/components/nodes/Rich.tsx

Lines changed: 244 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,13 @@ import {
5555
BoldExtension,
5656
CalloutExtension,
5757
ItalicExtension,
58+
LinkExtension,
5859
PlaceholderExtension,
60+
ShortcutHandlerProps,
5961
SubExtension,
6062
SupExtension,
6163
TextHighlightExtension,
64+
createMarkPositioner,
6265
wysiwygPreset,
6366
} from "remirror/extensions";
6467
import {
@@ -84,16 +87,246 @@ import {
8487
BaselineButtonGroup,
8588
CommandButton,
8689
CommandButtonProps,
90+
useChainedCommands,
91+
useCurrentSelection,
92+
useAttrs,
93+
useUpdateReason,
94+
FloatingWrapper,
8795
} from "@remirror/react";
8896
import { WysiwygEditor } from "@remirror/react-editors/wysiwyg";
89-
import { FloatingToolbar } from "@remirror/react";
97+
import { FloatingToolbar, useExtensionEvent } from "@remirror/react";
9098
import { TableExtension } from "@remirror/extension-react-tables";
9199
import { GenIcon, IconBase } from "@remirror/react-components";
92100
import "remirror/styles/all.css";
93101

94102
import { htmlToProsemirrorNode } from "remirror";
95103
import { styled } from "@mui/material";
96104

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+
97330
export interface SetHighlightButtonProps
98331
extends Omit<
99332
CommandButtonProps,
@@ -175,6 +408,7 @@ const MyEditor = ({
175408
new TextHighlightExtension(),
176409
new SupExtension(),
177410
new SubExtension(),
411+
new LinkExtension({ autoLink: true }),
178412
// new CalloutExtension({ defaultType: "warn" }),
179413
...wysiwygPreset(),
180414
],
@@ -230,33 +464,33 @@ const MyEditor = ({
230464
>
231465
{/* <WysiwygToolbar /> */}
232466
<EditorComponent />
467+
233468
<TableComponents />
234469

235470
{!isGuest && (
236-
<FloatingToolbar>
471+
<FloatingLinkToolbar>
472+
<FormattingButtonGroup />
473+
<VerticalDivider />
237474
<CommandButtonGroup>
238475
{/* <HeadingLevelButtonGroup /> */}
239-
{/* <VerticalDivider /> */}
240-
<FormattingButtonGroup />
476+
241477
{/* <ListButtonGroup /> */}
242478
<SetHighlightButton color="lightpink" />
243479
<SetHighlightButton color="yellow" />
244480
<SetHighlightButton color="lightgreen" />
245481
<SetHighlightButton color="lightcyan" />
246482
<SetHighlightButton />
247483
</CommandButtonGroup>
484+
<VerticalDivider />
485+
{/* <FormattingButtonGroup /> */}
248486
{/* <DecreaseIndentButton /> */}
249487
{/* <IncreaseIndentButton /> */}
250488
{/* <TextAlignmentButtonGroup /> */}
251489
{/* <IndentationButtonGroup /> */}
252490
{/* <BaselineButtonGroup /> */}
253-
</FloatingToolbar>
254-
)}
255-
{!isGuest && (
256-
<FloatingToolbar positioner="emptyBlockStart">
257-
<HeadingLevelButtonGroup />
258-
</FloatingToolbar>
491+
</FloatingLinkToolbar>
259492
)}
493+
260494
{/* <Menu /> */}
261495
</Remirror>
262496
</MyStyledWrapper>

0 commit comments

Comments
 (0)