CalloutExtension
Summary
This extension adds callouts to your text editor.
Features
Classify with semantic types
Not all callouts are the same. For example, a callout could contain an urgent warning to the reader that should stand out brightly. In contrast, a callout might contain an informational note which shouldn't overshadow the other text.
These different use cases can be represented by setting the type
property to info
, warning
, error
, success
, or blank
:
commands.toggleCallout({ type: 'error' });
The extension will use different background colors for each type like red for errors.
Add custom emoji
The semantic types are too limiting for some use cases - especially "info" callouts could benefit from more, context-specific sub categories like:
Context-specific sub categories can be created by configuring an emoji for the callout, additionally to the semantic type:
import { CalloutExtension } from '@remirror/extension-callout';
const basicExtensions = () => [new CalloutExtension({ renderEmoji, defaultEmoji: '💡' })];
/**
* If you want to update the emoji to a new one, you can dispatch a transaction to update the `emoji` attrs inside this function.
*/
const renderEmoji = (node: ProsemirrorNode) => {
const emoji = document.createElement('span');
emoji.textContent = node.attrs.emoji;
return emoji;
};
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 { CalloutExtension } from 'remirror/extensions';
The extension is provided by the @remirror/extension-callout
package.
Examples
Source code
import 'remirror/styles/all.css';
import React, { useCallback } from 'react';
import { htmlToProsemirrorNode } from 'remirror';
import { CalloutExtension } from 'remirror/extensions';
import { i18nFormat } from '@remirror/i18n';
import { Remirror, ThemeProvider, useRemirror } from '@remirror/react';
import { CalloutTypeButtonGroup, Toolbar } from '@remirror/react-ui';
const Basic = (): JSX.Element => {
const basicExtensions = useCallback(() => [new CalloutExtension()], []);
const { manager, state, onChange } = useRemirror({
extensions: basicExtensions,
content:
'<div data-callout-type="info"><p>Info callout</p></div><p />' +
'<div data-callout-type="warning"><p>Warning callout</p></div><p />' +
'<div data-callout-type="error"><p>Error callout</p></div><p />' +
'<div data-callout-type="success"><p>Success callout</p></div>',
stringHandler: htmlToProsemirrorNode,
});
return (
<ThemeProvider>
<Remirror
manager={manager}
autoFocus
onChange={onChange}
initialContent={state}
autoRender='end'
i18nFormat={i18nFormat}
>
<Toolbar>
<CalloutTypeButtonGroup />
</Toolbar>
</Remirror>
</ThemeProvider>
);
};
export default Basic;
Source code
import 'remirror/styles/all.css';
import { EmojiButton } from '@joeattardi/emoji-button';
import React, { useCallback, useEffect, useRef } from 'react';
import { htmlToProsemirrorNode, ProsemirrorNode } from 'remirror';
import { CalloutExtension } from 'remirror/extensions';
import {
Remirror,
ThemeProvider,
useCommands,
useRemirror,
useRemirrorContext,
} from '@remirror/react';
const EmojiPicker = () => {
const pickerRef = useRef(new EmojiButton({ position: 'bottom', autoFocusSearch: false }));
const { updateCallout } = useCommands();
const { view } = useRemirrorContext();
const pos = useRef(-1);
const handleClickEmoji = useCallback(
(e: MouseEvent) => {
const target = e.target as HTMLElement;
e.preventDefault();
if (!target.matches('[data-emoji-container]')) {
return;
}
/**
* Find the document position of the click element.
*/
pos.current = view.posAtDOM(target, 0);
pickerRef.current.togglePicker(target);
},
[view],
);
/**
* Handle the selected emoji here.
* Use `updateCallout` commands to update new emoji.
* Need to pass pos information to commands, otherwise it will update the node where the cursor is located.
*/
const handleSelectEmoji = useCallback(
(selection: { emoji: string }) => {
updateCallout({ emoji: selection.emoji }, pos.current);
},
[updateCallout],
);
useEffect(() => {
const picker = pickerRef.current;
picker.on('emoji', handleSelectEmoji);
document.addEventListener('click', handleClickEmoji);
return () => {
picker.destroyPicker();
document.removeEventListener('click', handleClickEmoji);
};
}, [handleClickEmoji, handleSelectEmoji]);
return null;
};
const renderDialogEmoji = (node: ProsemirrorNode) => {
const { emoji: prevEmoji } = node.attrs;
const emoji = document.createElement('span');
emoji.dataset.emojiContainer = '';
emoji.textContent = prevEmoji;
emoji.style.cursor = 'pointer';
// Prevent ProseMirror from handling the `mousedown` event so that the cursor
// won't move when users click the emoji.
emoji.addEventListener('mousedown', (e) => e.preventDefault());
return emoji;
};
const WithEmojiPicker: React.FC = () => {
const basicExtensions = useCallback(
() => [new CalloutExtension({ renderEmoji: renderDialogEmoji, defaultEmoji: '💡' })],
[],
);
const { manager, state, onChange } = useRemirror({
extensions: basicExtensions,
content:
'<div data-callout-type="blank" data-callout-emoji="💡"><p>Blank callout</p></div><p />' +
'<div data-callout-type="warning" data-callout-emoji="💡"><p>Click the emoji to open a emoji picker.</p></div><p />' +
'<div data-callout-type="success" data-callout-emoji="💡"><p>Powered by https://www.npmjs.com/package/@joeattardi/emoji-button</p></div>',
stringHandler: htmlToProsemirrorNode,
});
return (
<ThemeProvider>
<Remirror
manager={manager}
autoFocus
onChange={onChange}
initialContent={state}
autoRender='end'
>
<EmojiPicker />
</Remirror>
</ThemeProvider>
);
};
export default WithEmojiPicker;
Source code
import 'remirror/styles/all.css';
import { Blobmoji } from '@svgmoji/blob';
import React, { useCallback, useEffect, useRef } from 'react';
import { htmlToProsemirrorNode, ProsemirrorNode } from 'remirror';
import { CalloutExtension } from 'remirror/extensions';
import svgmojiData from 'svgmoji/emoji.json';
import {
Remirror,
ThemeProvider,
useCommands,
useRemirror,
useRemirrorContext,
} from '@remirror/react';
const RandomEmoji: React.FC = () => {
const { updateCallout } = useCommands();
const { view } = useRemirrorContext();
const pos = useRef(-1);
const choiceRandomEmoji = useCallback((currEmoji: string): string => {
const availableEmojis = ['😭', '😊', '🥰', '😂', '🙄', '😫', '🤔', '😌', '😍', '🤣'];
const nextEmoji = availableEmojis[Math.floor(Math.random() * availableEmojis.length)];
return currEmoji === nextEmoji ? choiceRandomEmoji(currEmoji) : nextEmoji;
}, []);
const handleClickEmoji = useCallback(
(e: MouseEvent) => {
const target = e.target as HTMLImageElement;
e.preventDefault();
if (!target.matches('[data-emoji-container]')) {
return;
}
/**
* Find the document position of the click element.
*/
pos.current = view.posAtDOM(target, 0);
updateCallout({ emoji: choiceRandomEmoji(target.alt) }, pos.current);
},
[view, updateCallout, choiceRandomEmoji],
);
useEffect(() => {
document.addEventListener('click', handleClickEmoji);
return () => {
document.removeEventListener('click', handleClickEmoji);
};
}, [handleClickEmoji]);
return null;
};
const renderRandomEmoji = (node: ProsemirrorNode) => {
const { emoji: emojiCode } = node.attrs;
const emoji = document.createElement('img');
emoji.style.height = '48px';
emoji.style.width = '48px';
emoji.dataset.emojiContainer = '';
emoji.style.cursor = 'pointer';
emoji.alt = emojiCode;
const blobmoji = new Blobmoji({ data: svgmojiData, type: 'individual' });
emoji.src = blobmoji.url(emojiCode);
// Prevent ProseMirror from handle the `mousedown` event so that the cursor
// won't move when users click the emoji.
emoji.addEventListener('mousedown', (e) => {
e.preventDefault();
});
return emoji;
};
const WithRandomEmoji: React.FC = () => {
const basicExtensions = useCallback(
() => [new CalloutExtension({ renderEmoji: renderRandomEmoji, defaultEmoji: '💡' })],
[],
);
const { manager, state, onChange } = useRemirror({
extensions: basicExtensions,
content:
'<div data-callout-type data-callout-emoji="💡"><p>Click the emoji to get a new random emoji.</p><p> Powered by https://github.com/svgmoji/svgmoji</p></div>',
stringHandler: htmlToProsemirrorNode,
});
return (
<ThemeProvider>
<Remirror
manager={manager}
autoFocus
onChange={onChange}
initialContent={state}
autoRender='end'
>
<RandomEmoji />
</Remirror>
</ThemeProvider>
);
};
export default WithRandomEmoji;