Skip to content

Commit

Permalink
feat(extension-callout): add configurable emoji (#980)
Browse files Browse the repository at this point in the history
Co-authored-by: ocavue <ocavue@gmail.com>
  • Loading branch information
Gaaaga and ocavue committed Jun 24, 2021
1 parent 6adcc95 commit 3e0925f
Show file tree
Hide file tree
Showing 17 changed files with 617 additions and 49 deletions.
8 changes: 8 additions & 0 deletions .changeset/heavy-fishes-hammer.md
@@ -0,0 +1,8 @@
---
'@remirror/extension-callout': minor
'@remirror/styles': minor
'@remirror/theme': minor
---

- Added configurable emoji to the start of the `CalloutExtension`.
- Added a new type 'blank' to the `CalloutExtension`.
5 changes: 1 addition & 4 deletions .github/workflows/storybook.yml
Expand Up @@ -17,9 +17,6 @@ jobs:
timeout-minutes: 10
runs-on: ubuntu-latest

# don't run this job from a fork repository
if: github.repository_owner == 'remirror'

steps:
- name: checkout code repository
uses: actions/checkout@v2
Expand Down Expand Up @@ -55,7 +52,7 @@ jobs:
- name: deploy storybook for pull request
uses: amondnet/vercel-action@v20
if: github.ref != 'refs/heads/beta'
if: github.ref != 'refs/heads/beta' && github.event.pull_request.head.repo.full_name == github.repository # don't run this job from a fork repository
with:
working-directory: ./packages/storybook-react/storybook-static
vercel-token: ${{ secrets.VERCEL_STORYBOOK_TOKEN }}
Expand Down
133 changes: 128 additions & 5 deletions packages/remirror__extension-callout/__stories__/callout.stories.tsx
@@ -1,15 +1,87 @@
import 'remirror/styles/all.css';

import React from 'react';
import { EmojiButton } from '@joeattardi/emoji-button';
import { Blobmoji } from '@svgmoji/blob';
import React, { useCallback } from 'react';
import { CalloutExtension } from 'remirror/extensions';
import { htmlToProsemirrorNode } from '@remirror/core';
import svgmojiData from 'svgmoji/emoji.json';
import { EditorView, htmlToProsemirrorNode, ProsemirrorNode } from '@remirror/core';
import { ProsemirrorDevTools } from '@remirror/dev';
import { Remirror, ThemeProvider, useRemirror } from '@remirror/react';

export default { title: 'Callouts' };

const basicExtensions = () => [new CalloutExtension()];
const renderDialogEmoji = (node: ProsemirrorNode, view: EditorView, getPos: () => number) => {
const { emoji: prevEmoji, type } = node.attrs;
const emoji = document.createElement('span');
emoji.textContent = prevEmoji;

export const Basic: React.FC = () => {
const picker = new EmojiButton({
position: 'bottom',
autoFocusSearch: false,
});

/**
* Handle the selected emoji here
* Pass the new attributes to transaction to enable updating it from within the extension.
*/
picker.on('emoji', (selection) => {
const transaction = view.state.tr.setNodeMarkup(getPos(), undefined, {
emoji: selection.emoji,
type,
});
view.dispatch(transaction);
});

emoji.addEventListener('click', (e) => {
e.preventDefault();
picker.togglePicker(emoji);
});

// 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 renderRandomEmoji = (node: ProsemirrorNode, view: EditorView, getPos: () => number) => {
const { emoji: emojiCode, type } = node.attrs;
const emoji = document.createElement('img');
emoji.style.height = '48px';
emoji.style.width = '48px';
const blobmoji = new Blobmoji({ data: svgmojiData, type: 'individual' });
emoji.src = blobmoji.url(emojiCode);

const choiceRandomEmoji = (currEmoji: string): string => {
const availableEmojis = ['😭', '😊', '🥰', '😂', '🙄', '😫', '🤔', '😌', '😍', '🤣'];
const nextEmoji = availableEmojis[Math.floor(Math.random() * availableEmojis.length)];
return currEmoji === nextEmoji ? choiceRandomEmoji(currEmoji) : nextEmoji!;
};

emoji.addEventListener('click', (e) => {
e.preventDefault();

const transaction = view.state.tr.setNodeMarkup(getPos(), undefined, {
emoji: choiceRandomEmoji(node.attrs.emoji),
type,
});
view.dispatch(transaction);
});

// 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;
};

export const Basic = (): JSX.Element => {
const basicExtensions = useCallback(() => [new CalloutExtension()], []);
const { manager, state, onChange } = useRemirror({
extensions: basicExtensions,
content:
Expand All @@ -22,7 +94,58 @@ export const Basic: React.FC = () => {

return (
<ThemeProvider>
<Remirror manager={manager} autoFocus onChange={onChange} state={state} autoRender='end' />
<Remirror manager={manager} autoFocus onChange={onChange} state={state} autoRender='end'>
<ProsemirrorDevTools />
</Remirror>
</ThemeProvider>
);
};

Basic.args = {
autoLink: true,
openLinkOnClick: true,
};

export 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} state={state} autoRender='end'>
<ProsemirrorDevTools />
</Remirror>
</ThemeProvider>
);
};

export 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} state={state} autoRender='end'>
<ProsemirrorDevTools />
</Remirror>
</ThemeProvider>
);
};
140 changes: 127 additions & 13 deletions packages/remirror__extension-callout/__tests__/callout-extension.spec.ts
@@ -1,6 +1,6 @@
import { pmBuild } from 'jest-prosemirror';
import { extensionValidityTest, renderEditor } from 'jest-remirror';
import { htmlToProsemirrorNode, prosemirrorNodeToHtml } from 'remirror';
import { htmlToProsemirrorNode, ProsemirrorNode, prosemirrorNodeToHtml } from 'remirror';
import { createCoreManager } from 'remirror/extensions';

import { CalloutExtension } from '../';
Expand Down Expand Up @@ -72,9 +72,11 @@ describe('commands', () => {
commands.toggleCallout({ type: 'error' });
expect(view.dom.innerHTML).toMatchInlineSnapshot(`
<div data-callout-type="error">
<p>
Make this a callout
</p>
<div>
<p>
Make this a callout
</p>
</div>
</div>
`);
expect(view.state.doc).toEqualRemirrorDocument(
Expand All @@ -96,9 +98,11 @@ describe('commands', () => {
commands.toggleCallout();
expect(view.dom.innerHTML).toMatchInlineSnapshot(`
<div data-callout-type="info">
<p>
Make this a callout
</p>
<div>
<p>
Make this a callout
</p>
</div>
</div>
`);
expect(view.state.doc).toEqualRemirrorDocument(
Expand Down Expand Up @@ -130,9 +134,11 @@ describe('commands', () => {
commands.toggleCallout();
expect(view.dom.innerHTML).toMatchInlineSnapshot(`
<div data-callout-type="success">
<p>
Make this a callout
</p>
<div>
<p>
Make this a callout
</p>
</div>
</div>
`);
expect(view.state.doc).toEqualRemirrorDocument(
Expand All @@ -148,9 +154,11 @@ describe('commands', () => {
commands.updateCallout({ type: 'error' });
expect(view.dom.innerHTML).toMatchInlineSnapshot(`
<div data-callout-type="error">
<p>
This is a callout
</p>
<div>
<p>
This is a callout
</p>
</div>
</div>
`);
expect(view.state.doc).toEqualRemirrorDocument(
Expand Down Expand Up @@ -341,6 +349,22 @@ describe('inputRules', () => {
});
});

describe(':::blank', () => {
it('followed by space creates a blank callout', () => {
const { state } = add(doc(p('<cursor>'))).insertText(':::blank ');

expect(state.doc).toEqualRemirrorDocument(doc(callout({ type: 'blank' })(p(''))));
});

it('followed by enter creates a blank callout', () => {
const { state } = add(doc(p('<cursor>')))
.insertText(':::blank')
.press('Enter');

expect(state.doc).toEqualRemirrorDocument(doc(callout({ type: 'blank' })(p(''))));
});
});

describe('unknown type', () => {
it('followed by space creates the default type callout', () => {
const { state } = add(doc(p('<cursor>'))).insertText(':::unknown ');
Expand All @@ -357,3 +381,93 @@ describe('inputRules', () => {
});
});
});

const renderEmoji = (node: ProsemirrorNode) => {
const emoji = document.createElement('span');
emoji.textContent = node.attrs.emoji;
return emoji;
};

function createWithNoEmoji() {
const calloutExtension = new CalloutExtension({ renderEmoji });
return renderEditor([calloutExtension]);
}

function createAndSetEmoji() {
const calloutExtension = new CalloutExtension({ defaultEmoji: '💓' });
return renderEditor([calloutExtension]);
}

describe('emoji', () => {
describe('without defaultEmoji setup', () => {
const {
add,
view,
nodes: { p, doc },
attributeNodes: { callout },
} = createWithNoEmoji();

it('will not render emoji', () => {
add(doc(callout({ type: 'info' })(p(`This is a callout<cursor>`))));
expect(view.dom.innerHTML).toMatchInlineSnapshot(`
<div data-callout-type="info">
<div>
<p>
This is a callout
</p>
</div>
</div>
`);
});

it('passes an emoji attribute, will render the emoji', () => {
add(doc(callout({ type: 'info', emoji: '🦦' })(p(`This is a callout<cursor>`))));

expect(view.dom.innerHTML).toMatchInlineSnapshot(`
<div data-callout-type="info"
data-callout-emoji="🦦"
>
<div class="remirror-callout-emoji-wrapper">
<span>
🦦
</span>
</div>
<div>
<p>
This is a callout
</p>
</div>
</div>
`);
});
});

describe('with defaultEmoji setup', () => {
const {
add,
view,
nodes: { p, doc },
attributeNodes: { callout },
} = createAndSetEmoji();

it('render emoji', () => {
add(doc(callout({ type: 'info' })(p(`This is a callout<cursor>`))));
expect(view.dom.innerHTML).toMatchInlineSnapshot(`
<div data-callout-type="info"
data-callout-emoji="💓"
>
<div class="remirror-callout-emoji-wrapper">
<span>
💓
</span>
</div>
<div>
<p>
This is a callout
</p>
</div>
</div>
`);
});
});
});
8 changes: 6 additions & 2 deletions packages/remirror__extension-callout/package.json
Expand Up @@ -37,10 +37,14 @@
"dependencies": {
"@babel/runtime": "^7.13.10",
"@remirror/core": "1.0.0-next.60",
"@remirror/messages": "0.0.0"
"@remirror/messages": "0.0.0",
"@remirror/theme": "1.0.0-next.60"
},
"devDependencies": {
"@remirror/pm": "1.0.0-next.60"
"@joeattardi/emoji-button": "^4.6.0",
"@remirror/pm": "1.0.0-next.60",
"@svgmoji/blob": "^3.1.0",
"svgmoji": "^3.1.0"
},
"peerDependencies": {
"@remirror/pm": "1.0.0-next.60"
Expand Down

0 comments on commit 3e0925f

Please sign in to comment.