Commit ca576c2c authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch '332099-render-emoji-content-editor' into 'master'

Render emojis in the Content Editor

See merge request gitlab-org/gitlab!67986
parents 3b7490d0 27836b59
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'; ...@@ -9,6 +9,7 @@ import Code from '../extensions/code';
import CodeBlockHighlight from '../extensions/code_block_highlight'; import CodeBlockHighlight from '../extensions/code_block_highlight';
import Document from '../extensions/document'; import Document from '../extensions/document';
import Dropcursor from '../extensions/dropcursor'; import Dropcursor from '../extensions/dropcursor';
import Emoji from '../extensions/emoji';
import Gapcursor from '../extensions/gapcursor'; import Gapcursor from '../extensions/gapcursor';
import HardBreak from '../extensions/hard_break'; import HardBreak from '../extensions/hard_break';
import Heading from '../extensions/heading'; import Heading from '../extensions/heading';
...@@ -62,6 +63,7 @@ export const createContentEditor = ({ ...@@ -62,6 +63,7 @@ export const createContentEditor = ({
CodeBlockHighlight, CodeBlockHighlight,
Document, Document,
Dropcursor, Dropcursor,
Emoji,
Gapcursor, Gapcursor,
HardBreak, HardBreak,
Heading, Heading,
......
...@@ -8,6 +8,7 @@ import Bold from '../extensions/bold'; ...@@ -8,6 +8,7 @@ import Bold from '../extensions/bold';
import BulletList from '../extensions/bullet_list'; import BulletList from '../extensions/bullet_list';
import Code from '../extensions/code'; import Code from '../extensions/code';
import CodeBlockHighlight from '../extensions/code_block_highlight'; import CodeBlockHighlight from '../extensions/code_block_highlight';
import Emoji from '../extensions/emoji';
import HardBreak from '../extensions/hard_break'; import HardBreak from '../extensions/hard_break';
import Heading from '../extensions/heading'; import Heading from '../extensions/heading';
import HorizontalRule from '../extensions/horizontal_rule'; import HorizontalRule from '../extensions/horizontal_rule';
...@@ -51,6 +52,11 @@ const defaultSerializerConfig = { ...@@ -51,6 +52,11 @@ const defaultSerializerConfig = {
[Blockquote.name]: defaultMarkdownSerializer.nodes.blockquote, [Blockquote.name]: defaultMarkdownSerializer.nodes.blockquote,
[BulletList.name]: defaultMarkdownSerializer.nodes.bullet_list, [BulletList.name]: defaultMarkdownSerializer.nodes.bullet_list,
[CodeBlockHighlight.name]: defaultMarkdownSerializer.nodes.code_block, [CodeBlockHighlight.name]: defaultMarkdownSerializer.nodes.code_block,
[Emoji.name]: (state, node) => {
const { name } = node.attrs;
state.write(`:${name}:`);
},
[HardBreak.name]: defaultMarkdownSerializer.nodes.hard_break, [HardBreak.name]: defaultMarkdownSerializer.nodes.hard_break,
[Heading.name]: defaultMarkdownSerializer.nodes.heading, [Heading.name]: defaultMarkdownSerializer.nodes.heading,
[HorizontalRule.name]: defaultMarkdownSerializer.nodes.horizontal_rule, [HorizontalRule.name]: defaultMarkdownSerializer.nodes.horizontal_rule,
......
...@@ -6,6 +6,8 @@ import ContentEditor from '~/content_editor/components/content_editor.vue'; ...@@ -6,6 +6,8 @@ import ContentEditor from '~/content_editor/components/content_editor.vue';
import TopToolbar from '~/content_editor/components/top_toolbar.vue'; import TopToolbar from '~/content_editor/components/top_toolbar.vue';
import { createContentEditor } from '~/content_editor/services/create_content_editor'; import { createContentEditor } from '~/content_editor/services/create_content_editor';
jest.mock('~/emoji');
describe('ContentEditor', () => { describe('ContentEditor', () => {
let wrapper; let wrapper;
let editor; 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 { createContentEditor } from '~/content_editor';
import { loadMarkdownApiExamples, loadMarkdownApiResult } from './markdown_processing_examples'; import { loadMarkdownApiExamples, loadMarkdownApiResult } from './markdown_processing_examples';
jest.mock('~/emoji');
describe('markdown processing', () => { describe('markdown processing', () => {
// Ensure we generate same markdown that was provided to Markdown API. // Ensure we generate same markdown that was provided to Markdown API.
it.each(loadMarkdownApiExamples())( it.each(loadMarkdownApiExamples())(
......
...@@ -2,7 +2,9 @@ import { PROVIDE_SERIALIZER_OR_RENDERER_ERROR } from '~/content_editor/constants ...@@ -2,7 +2,9 @@ import { PROVIDE_SERIALIZER_OR_RENDERER_ERROR } from '~/content_editor/constants
import { createContentEditor } from '~/content_editor/services/create_content_editor'; import { createContentEditor } from '~/content_editor/services/create_content_editor';
import { createTestContentEditorExtension } from '../test_utils'; import { createTestContentEditorExtension } from '../test_utils';
describe('content_editor/services/create_editor', () => { jest.mock('~/emoji');
describe('content_editor/services/create_content_editor', () => {
let renderMarkdown; let renderMarkdown;
let editor; let editor;
const uploadsPath = '/uploads'; const uploadsPath = '/uploads';
......
...@@ -86,4 +86,5 @@ ...@@ -86,4 +86,5 @@
|--------|------------|----------| |--------|------------|----------|
| cell | cell | cell | | cell | cell | cell |
| cell | cell | cell | | cell | cell | cell |
- name: emoji
markdown: ':sparkles: :heart: :100:'
...@@ -15,6 +15,8 @@ import { ...@@ -15,6 +15,8 @@ import {
import MarkdownField from '~/vue_shared/components/markdown/field.vue'; import MarkdownField from '~/vue_shared/components/markdown/field.vue';
jest.mock('~/emoji');
describe('WikiForm', () => { describe('WikiForm', () => {
let wrapper; let wrapper;
let mock; 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