Commit 27836b59 authored by Enrique Alcantara's avatar Enrique Alcantara

Render emojis in the Content Editor

Render GFM emojis in the Content Editor
and allows inserting an emoji using
input rules

Changelog: added
parent 0dd2dcdc
import { Node } from '@tiptap/core';
import { InputRule } from 'prosemirror-inputrules';
import { initEmojiMap, getAllEmoji } from '~/emoji';
export const emojiInputRegex = /(?:^|\s)((?::)((?:\w+))(?::))$/;
export default Node.create({
name: 'emoji',
inline: true,
group: 'inline',
draggable: true,
addAttributes() {
return {
moji: {
default: null,
parseHTML: (element) => {
return {
moji: element.textContent,
};
},
},
name: {
default: null,
parseHTML: (element) => {
return {
name: element.dataset.name,
};
},
},
title: {
default: null,
},
unicodeVersion: {
default: '6.0',
parseHTML: (element) => {
return {
unicodeVersion: element.dataset.unicodeVersion,
};
},
},
};
},
parseHTML() {
return [
{
tag: 'gl-emoji',
},
];
},
renderHTML({ node }) {
return [
'gl-emoji',
{
'data-name': node.attrs.name,
title: node.attrs.title,
'data-unicode-version': node.attrs.unicodeVersion,
},
node.attrs.moji,
];
},
addInputRules() {
return [
new InputRule(emojiInputRegex, (state, match, start, end) => {
const [, , name] = match;
const emojis = getAllEmoji();
const emoji = emojis[name];
const { tr } = state;
if (emoji) {
tr.replaceWith(start, end, [
state.schema.text(' '),
this.type.create({ name, moji: emoji.e, unicodeVersion: emoji.u, title: emoji.d }),
]);
return tr;
}
return null;
}),
];
},
onCreate() {
initEmojiMap();
},
});
......@@ -9,6 +9,7 @@ import Code from '../extensions/code';
import CodeBlockHighlight from '../extensions/code_block_highlight';
import Document from '../extensions/document';
import Dropcursor from '../extensions/dropcursor';
import Emoji from '../extensions/emoji';
import Gapcursor from '../extensions/gapcursor';
import HardBreak from '../extensions/hard_break';
import Heading from '../extensions/heading';
......@@ -62,6 +63,7 @@ export const createContentEditor = ({
CodeBlockHighlight,
Document,
Dropcursor,
Emoji,
Gapcursor,
HardBreak,
Heading,
......
......@@ -8,6 +8,7 @@ import Bold from '../extensions/bold';
import BulletList from '../extensions/bullet_list';
import Code from '../extensions/code';
import CodeBlockHighlight from '../extensions/code_block_highlight';
import Emoji from '../extensions/emoji';
import HardBreak from '../extensions/hard_break';
import Heading from '../extensions/heading';
import HorizontalRule from '../extensions/horizontal_rule';
......@@ -51,6 +52,11 @@ const defaultSerializerConfig = {
[Blockquote.name]: defaultMarkdownSerializer.nodes.blockquote,
[BulletList.name]: defaultMarkdownSerializer.nodes.bullet_list,
[CodeBlockHighlight.name]: defaultMarkdownSerializer.nodes.code_block,
[Emoji.name]: (state, node) => {
const { name } = node.attrs;
state.write(`:${name}:`);
},
[HardBreak.name]: defaultMarkdownSerializer.nodes.hard_break,
[Heading.name]: defaultMarkdownSerializer.nodes.heading,
[HorizontalRule.name]: defaultMarkdownSerializer.nodes.horizontal_rule,
......
......@@ -6,6 +6,8 @@ import ContentEditor from '~/content_editor/components/content_editor.vue';
import TopToolbar from '~/content_editor/components/top_toolbar.vue';
import { createContentEditor } from '~/content_editor/services/create_content_editor';
jest.mock('~/emoji');
describe('ContentEditor', () => {
let wrapper;
let editor;
......
import { initEmojiMock } from 'helpers/emoji';
import Emoji from '~/content_editor/extensions/emoji';
import { createTestEditor, createDocBuilder } from '../test_utils';
describe('content_editor/extensions/emoji', () => {
let tiptapEditor;
let doc;
let p;
let emoji;
let eq;
beforeEach(async () => {
await initEmojiMock();
});
beforeEach(() => {
tiptapEditor = createTestEditor({ extensions: [Emoji] });
({
builders: { doc, p, emoji },
eq,
} = createDocBuilder({
tiptapEditor,
names: {
loading: { nodeType: Emoji.name },
},
}));
});
describe('when typing a valid emoji input rule', () => {
it('inserts an emoji node', () => {
const { view } = tiptapEditor;
const { selection } = view.state;
const expectedDoc = doc(
p(
' ',
emoji({ moji: '', name: 'heart', title: 'heavy black heart', unicodeVersion: '1.1' }),
),
);
// Triggers the event handler that input rules listen to
view.someProp('handleTextInput', (f) => f(view, selection.from, selection.to, ':heart:'));
expect(eq(tiptapEditor.state.doc, expectedDoc)).toBe(true);
});
});
describe('when typing a invalid emoji input rule', () => {
it('does not insert an emoji node', () => {
const { view } = tiptapEditor;
const { selection } = view.state;
const invalidEmoji = ':invalid:';
const expectedDoc = doc(p());
// Triggers the event handler that input rules listen to
view.someProp('handleTextInput', (f) => f(view, selection.from, selection.to, invalidEmoji));
expect(eq(tiptapEditor.state.doc, expectedDoc)).toBe(true);
});
});
});
import { createContentEditor } from '~/content_editor';
import { loadMarkdownApiExamples, loadMarkdownApiResult } from './markdown_processing_examples';
jest.mock('~/emoji');
describe('markdown processing', () => {
// Ensure we generate same markdown that was provided to Markdown API.
it.each(loadMarkdownApiExamples())(
......
......@@ -2,7 +2,9 @@ import { PROVIDE_SERIALIZER_OR_RENDERER_ERROR } from '~/content_editor/constants
import { createContentEditor } from '~/content_editor/services/create_content_editor';
import { createTestContentEditorExtension } from '../test_utils';
describe('content_editor/services/create_editor', () => {
jest.mock('~/emoji');
describe('content_editor/services/create_content_editor', () => {
let renderMarkdown;
let editor;
const uploadsPath = '/uploads';
......
......@@ -86,4 +86,5 @@
|--------|------------|----------|
| cell | cell | cell |
| cell | cell | cell |
- name: emoji
markdown: ':sparkles: :heart: :100:'
......@@ -15,6 +15,8 @@ import {
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
jest.mock('~/emoji');
describe('WikiForm', () => {
let wrapper;
let mock;
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment