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 React from 'react';
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 React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { createMarkPositioner, LinkExtension, ShortcutHandlerProps } from 'remirror/extensions';
import {
EditorComponent,
FloatingWrapper,
Remirror,
ThemeProvider,
useActive,
useAttrs,
useChainedCommands,
useCurrentSelection,
useExtensionEvent,
useRemirror,
useUpdateReason,
} from '@remirror/react';
import { CommandButton, FloatingToolbar } from '@remirror/react-ui';

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(() => 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 handleClickEdit = useCallback(() => {
clickEdit();
}, [clickEdit]);

const linkEditButtons = activeLink ? (
<>
<CommandButton
commandName='updateLink'
onSelect={handleClickEdit}
icon='pencilLine'
enabled
/>
<CommandButton commandName='removeLink' onSelect={onRemove} icon='linkUnlink' enabled />
</>
) : (
<CommandButton commandName='updateLink' onSelect={handleClickEdit} icon='link' enabled />
);

return (
<>
{!isEditing && <FloatingToolbar>{linkEditButtons}</FloatingToolbar>}
{!isEditing && empty && (
<FloatingToolbar positioner={linkPositioner}>{linkEditButtons}</FloatingToolbar>
)}

<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 React, { useCallback, useState } from 'react';
import { LinkExtension } from 'remirror/extensions';
import { Remirror, ThemeProvider, useExtensionEvent, useRemirror } from '@remirror/react';

const ClickPrinter: React.FC = () => {
const [lastClickedLink, setLastClickedLink] = useState<string | null>(null);

useExtensionEvent(
LinkExtension,
'onClick',
useCallback((_, data) => {
setLastClickedLink(JSON.stringify(data, null, 2));
return true;
}, []),
);

if (!lastClickedLink) {
return null;
}

return (
<>
<h3>Last clicked link info</h3>
<pre>
<code>{lastClickedLink}</code>
</pre>
</>
);
};

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

return (
<ThemeProvider>
<Remirror manager={manager} initialContent={state} autoRender='start'>
<ClickPrinter />
</Remirror>
</ThemeProvider>
);
};

export default ClickHandler;

API