Skip to main content

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:

image

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 {
CalloutTypeButtonGroup,
Remirror,
ThemeProvider,
Toolbar,
useRemirror,
} from '@remirror/react';

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'
>
<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;

API