Skip to main content

LinkExtension

Summary

This extension adds links to your text editor.

Features

The editor will automatically detect links if autoLink attribute is enabled:

import { Remirror } from '@remirror/react';

export const AutoLink: React.FC = () => {
const { manager } = useRemirror({
extensions: () => [new LinkExtension({ autoLink: true })],
});

return <Remirror manager={manager} />;
};

Handle clicks

The extension calls the onClick handlers when the user clicks on a link. This can be used to e.g. open the link in another tab:

const extension = new LinkExtension();
extension.addHandler('onClick', (_, data) => {
window.location.href = data.href;
return true;
});

Usage

Installation

This extension is installed for you when you install the main remirror package.

You can use the imports in the following way:

import { LinkExtension } from 'remirror/extensions';

The extension is provided by the @remirror/extension-link package.

Examples

Source code
import { LinkExtension } from 'remirror/extensions';
import { Remirror, ThemeProvider, useRemirror } from '@remirror/react';

const Basic = (): JSX.Element => {
const { manager, state } = useRemirror({
extensions: () => [new LinkExtension({ autoLink: true })],
content: `Type "www.remirror.io" to insert a link:&nbsp;`,
stringHandler: 'html',
});

return (
<ThemeProvider>
<Remirror manager={manager} initialContent={state} />
</ThemeProvider>
);
};

export default Basic;
Source code
import type { ChangeEvent, HTMLProps, KeyboardEvent } from 'react';
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { createMarkPositioner, LinkExtension, ShortcutHandlerProps } from 'remirror/extensions';
import {
ComponentItem,
EditorComponent,
FloatingToolbar,
FloatingWrapper,
Remirror,
ThemeProvider,
ToolbarItemUnion,
useActive,
useAttrs,
useChainedCommands,
useCurrentSelection,
useExtensionEvent,
useRemirror,
useUpdateReason,
} from '@remirror/react';

function useLinkShortcut() {
const [linkShortcut, setLinkShortcut] = useState<ShortcutHandlerProps | undefined>();
const [isEditing, setIsEditing] = useState(false);

useExtensionEvent(
LinkExtension,
'onShortcut',
useCallback(
(props) => {
if (!isEditing) {
setIsEditing(true);
}

return setLinkShortcut(props);
},
[isEditing],
),
);

return { linkShortcut, isEditing, setIsEditing };
}

function useFloatingLinkState() {
const chain = useChainedCommands();
const { isEditing, linkShortcut, setIsEditing } = useLinkShortcut();
const { to, empty } = useCurrentSelection();

const url = (useAttrs().link()?.href as string) ?? '';
const [href, setHref] = useState<string>(url);

// A positioner which only shows for links.
const linkPositioner = useMemo(() => createMarkPositioner({ type: 'link' }), []);

const onRemove = useCallback(() => {
return chain.removeLink().focus().run();
}, [chain]);

const updateReason = useUpdateReason();

useLayoutEffect(() => {
if (!isEditing) {
return;
}

if (updateReason.doc || updateReason.selection) {
setIsEditing(false);
}
}, [isEditing, setIsEditing, updateReason.doc, updateReason.selection]);

useEffect(() => {
setHref(url);
}, [url]);

const submitHref = useCallback(() => {
setIsEditing(false);
const range = linkShortcut ?? undefined;

if (href === '') {
chain.removeLink();
} else {
chain.updateLink({ href, auto: false }, range);
}

chain.focus(range?.to ?? to).run();
}, [setIsEditing, linkShortcut, chain, href, to]);

const cancelHref = useCallback(() => {
setIsEditing(false);
}, [setIsEditing]);

const clickEdit = useCallback(() => {
if (empty) {
chain.selectLink();
}

setIsEditing(true);
}, [chain, empty, setIsEditing]);

return useMemo(
() => ({
href,
setHref,
linkShortcut,
linkPositioner,
isEditing,
clickEdit,
onRemove,
submitHref,
cancelHref,
}),
[href, linkShortcut, linkPositioner, isEditing, clickEdit, onRemove, submitHref, cancelHref],
);
}

const DelayAutoFocusInput = ({ autoFocus, ...rest }: HTMLProps<HTMLInputElement>) => {
const inputRef = useRef<HTMLInputElement>(null);

useEffect(() => {
if (!autoFocus) {
return;
}

const frame = window.requestAnimationFrame(() => {
inputRef.current?.focus();
});

return () => {
window.cancelAnimationFrame(frame);
};
}, [autoFocus]);

return <input ref={inputRef} {...rest} />;
};

const FloatingLinkToolbar = () => {
const { isEditing, linkPositioner, clickEdit, onRemove, submitHref, href, setHref, cancelHref } =
useFloatingLinkState();
const active = useActive();
const activeLink = active.link();
const { empty } = useCurrentSelection();
const linkEditItems: ToolbarItemUnion[] = useMemo(
() => [
{
type: ComponentItem.ToolbarGroup,
label: 'Link',
items: activeLink
? [
{ type: ComponentItem.ToolbarButton, onClick: () => clickEdit(), icon: 'pencilLine' },
{ type: ComponentItem.ToolbarButton, onClick: onRemove, icon: 'linkUnlink' },
]
: [{ type: ComponentItem.ToolbarButton, onClick: () => clickEdit(), icon: 'link' }],
},
],
[clickEdit, onRemove, activeLink],
);

const items: ToolbarItemUnion[] = useMemo(() => linkEditItems, [linkEditItems]);

return (
<>
<FloatingToolbar items={items} positioner='selection' placement='top' enabled={!isEditing} />
<FloatingToolbar
items={linkEditItems}
positioner={linkPositioner}
placement='bottom'
enabled={!isEditing && empty}
/>

<FloatingWrapper
positioner='always'
placement='bottom'
enabled={isEditing}
renderOutsideEditor
>
<DelayAutoFocusInput
style={{ zIndex: 20 }}
autoFocus
placeholder='Enter link...'
onChange={(event: ChangeEvent<HTMLInputElement>) => setHref(event.target.value)}
value={href}
onKeyPress={(event: KeyboardEvent<HTMLInputElement>) => {
const { code } = event;

if (code === 'Enter') {
submitHref();
}

if (code === 'Escape') {
cancelHref();
}
}}
/>
</FloatingWrapper>
</>
);
};

const EditDialog = (): JSX.Element => {
const { manager, state } = useRemirror({
extensions: () => [new LinkExtension({ autoLink: true })],
content: `Click this <a href="https://remirror.io" target="_blank">link</a> to edit it`,
stringHandler: 'html',
});

return (
<ThemeProvider>
<Remirror manager={manager} initialContent={state}>
<EditorComponent />
<FloatingLinkToolbar />
</Remirror>
</ThemeProvider>
);
};

export default EditDialog;
Source code
import { useMemo } from 'react';
import { LinkExtension } from 'remirror/extensions';
import { Remirror, ThemeProvider, useRemirror } from '@remirror/react';

const ClickHandler = (): JSX.Element => {
const linkExtension = useMemo(() => {
const extension = new LinkExtension();
extension.addHandler('onClick', (_, data) => {
alert(`You clicked link: ${JSON.stringify(data)}`);
return true;
});
return extension;
}, []);
const { manager, state } = useRemirror({
extensions: () => [linkExtension],
content: `Click this <a href="https://remirror.io" target="_blank">link</a>`,
stringHandler: 'html',
});

return (
<ThemeProvider>
<Remirror manager={manager} initialContent={state} />
</ThemeProvider>
);
};

export default ClickHandler;

API