Commit eb5a1cd1 authored by Enrique Alcántara's avatar Enrique Alcántara

Initialize editor options in a service function

Instead of initializing the Toast UI editor options
(configuration) in a constants file, create a service
function to initialize the options and call that
function when the RichContentEditor component is created
parent d7239da8
import { __ } from '~/locale'; import { __ } from '~/locale';
import { generateToolbarItem } from './services/editor_service';
import buildCustomHTMLRenderer from './services/build_custom_renderer';
export const CUSTOM_EVENTS = { export const CUSTOM_EVENTS = {
openAddImageModal: 'gl_openAddImageModal', openAddImageModal: 'gl_openAddImageModal',
}; };
/* eslint-disable @gitlab/require-i18n-strings */ /* eslint-disable @gitlab/require-i18n-strings */
const TOOLBAR_ITEM_CONFIGS = [ export const TOOLBAR_ITEM_CONFIGS = [
{ icon: 'heading', event: 'openHeadingSelect', classes: 'tui-heading', tooltip: __('Headings') }, { icon: 'heading', event: 'openHeadingSelect', classes: 'tui-heading', tooltip: __('Headings') },
{ icon: 'bold', command: 'Bold', tooltip: __('Add bold text') }, { icon: 'bold', command: 'Bold', tooltip: __('Add bold text') },
{ icon: 'italic', command: 'Italic', tooltip: __('Add italic text') }, { icon: 'italic', command: 'Italic', tooltip: __('Add italic text') },
...@@ -30,11 +28,6 @@ const TOOLBAR_ITEM_CONFIGS = [ ...@@ -30,11 +28,6 @@ const TOOLBAR_ITEM_CONFIGS = [
{ icon: 'doc-code', command: 'CodeBlock', tooltip: __('Insert a code block') }, { icon: 'doc-code', command: 'CodeBlock', tooltip: __('Insert a code block') },
]; ];
export const EDITOR_OPTIONS = {
toolbarItems: TOOLBAR_ITEM_CONFIGS.map(config => generateToolbarItem(config)),
customHTMLRenderer: buildCustomHTMLRenderer(),
};
export const EDITOR_TYPES = { export const EDITOR_TYPES = {
markdown: 'markdown', markdown: 'markdown',
wysiwyg: 'wysiwyg', wysiwyg: 'wysiwyg',
......
...@@ -3,16 +3,11 @@ import 'codemirror/lib/codemirror.css'; ...@@ -3,16 +3,11 @@ import 'codemirror/lib/codemirror.css';
import '@toast-ui/editor/dist/toastui-editor.css'; import '@toast-ui/editor/dist/toastui-editor.css';
import AddImageModal from './modals/add_image/add_image_modal.vue'; import AddImageModal from './modals/add_image/add_image_modal.vue';
import { import { EDITOR_TYPES, EDITOR_HEIGHT, EDITOR_PREVIEW_STYLE, CUSTOM_EVENTS } from './constants';
EDITOR_OPTIONS,
EDITOR_TYPES,
EDITOR_HEIGHT,
EDITOR_PREVIEW_STYLE,
CUSTOM_EVENTS,
} from './constants';
import { import {
registerHTMLToMarkdownRenderer, registerHTMLToMarkdownRenderer,
getEditorOptions,
addCustomEventListener, addCustomEventListener,
removeCustomEventListener, removeCustomEventListener,
addImage, addImage,
...@@ -35,7 +30,7 @@ export default { ...@@ -35,7 +30,7 @@ export default {
options: { options: {
type: Object, type: Object,
required: false, required: false,
default: () => EDITOR_OPTIONS, default: () => null,
}, },
initialEditType: { initialEditType: {
type: String, type: String,
...@@ -65,13 +60,13 @@ export default { ...@@ -65,13 +60,13 @@ export default {
}; };
}, },
computed: { computed: {
editorOptions() {
return { ...EDITOR_OPTIONS, ...this.options };
},
editorInstance() { editorInstance() {
return this.$refs.editor; return this.$refs.editor;
}, },
}, },
created() {
this.editorOptions = getEditorOptions(this.options);
},
beforeDestroy() { beforeDestroy() {
this.removeListeners(); this.removeListeners();
}, },
......
import { union, mapValues } from 'lodash';
import renderBlockHtml from './renderers/render_html_block'; import renderBlockHtml from './renderers/render_html_block';
import renderKramdownList from './renderers/render_kramdown_list'; import renderKramdownList from './renderers/render_kramdown_list';
import renderKramdownText from './renderers/render_kramdown_text'; import renderKramdownText from './renderers/render_kramdown_text';
...@@ -19,63 +20,20 @@ const executeRenderer = (renderers, node, context) => { ...@@ -19,63 +20,20 @@ const executeRenderer = (renderers, node, context) => {
return availableRenderer ? availableRenderer.render(node, context) : context.origin(); return availableRenderer ? availableRenderer.render(node, context) : context.origin();
}; };
const buildCustomRendererFunctions = (customRenderers, defaults) => { const buildCustomHTMLRenderer = customRenderers => {
const customTypes = Object.keys(customRenderers).filter(type => !defaults[type]); const renderersByType = {
const customEntries = customTypes.map(type => { ...customRenderers,
const fn = (node, context) => executeRenderer(customRenderers[type], node, context); htmlBlock: union(htmlBlockRenderers, customRenderers?.htmlBlock),
return [type, fn]; htmlInline: union(htmlInlineRenderers, customRenderers?.htmlInline),
}); list: union(listRenderers, customRenderers?.list),
paragraph: union(paragraphRenderers, customRenderers?.paragraph),
return Object.fromEntries(customEntries); text: union(textRenderers, customRenderers?.text),
}; softbreak: union(softbreakRenderers, customRenderers?.softbreak),
const buildCustomHTMLRenderer = (
customRenderers = {
htmlBlock: [],
htmlInline: [],
list: [],
paragraph: [],
text: [],
softbreak: [],
},
) => {
const defaults = {
htmlBlock(node, context) {
const allHtmlBlockRenderers = [...customRenderers.htmlBlock, ...htmlBlockRenderers];
return executeRenderer(allHtmlBlockRenderers, node, context);
},
htmlInline(node, context) {
const allHtmlInlineRenderers = [...customRenderers.htmlInline, ...htmlInlineRenderers];
return executeRenderer(allHtmlInlineRenderers, node, context);
},
list(node, context) {
const allListRenderers = [...customRenderers.list, ...listRenderers];
return executeRenderer(allListRenderers, node, context);
},
paragraph(node, context) {
const allParagraphRenderers = [...customRenderers.paragraph, ...paragraphRenderers];
return executeRenderer(allParagraphRenderers, node, context);
},
text(node, context) {
const allTextRenderers = [...customRenderers.text, ...textRenderers];
return executeRenderer(allTextRenderers, node, context);
},
softbreak(node, context) {
const allSoftbreakRenderers = [...customRenderers.softbreak, ...softbreakRenderers];
return executeRenderer(allSoftbreakRenderers, node, context);
},
}; };
return { return mapValues(renderersByType, renderers => {
...buildCustomRendererFunctions(customRenderers, defaults), return (node, context) => executeRenderer(renderers, node, context);
...defaults, });
};
}; };
export default buildCustomHTMLRenderer; export default buildCustomHTMLRenderer;
import Vue from 'vue'; import Vue from 'vue';
import { defaults } from 'lodash';
import ToolbarItem from '../toolbar_item.vue'; import ToolbarItem from '../toolbar_item.vue';
import buildHtmlToMarkdownRenderer from './build_html_to_markdown_renderer'; import buildHtmlToMarkdownRenderer from './build_html_to_markdown_renderer';
import buildCustomHTMLRenderer from './build_custom_renderer';
import { TOOLBAR_ITEM_CONFIGS } from '../constants';
const buildWrapper = propsData => { const buildWrapper = propsData => {
const instance = new Vue({ const instance = new Vue({
...@@ -54,3 +57,10 @@ export const registerHTMLToMarkdownRenderer = editorApi => { ...@@ -54,3 +57,10 @@ export const registerHTMLToMarkdownRenderer = editorApi => {
renderer: renderer.constructor.factory(renderer, buildHtmlToMarkdownRenderer(renderer)), renderer: renderer.constructor.factory(renderer, buildHtmlToMarkdownRenderer(renderer)),
}); });
}; };
export const getEditorOptions = externalOptions => {
return defaults({
customHTMLRenderer: buildCustomHTMLRenderer(externalOptions?.customRenderers),
toolbarItems: TOOLBAR_ITEM_CONFIGS.map(toolbarItem => generateToolbarItem(toolbarItem)),
});
};
...@@ -17,6 +17,17 @@ export const Editor = { ...@@ -17,6 +17,17 @@ export const Editor = {
type: String, type: String,
}, },
}, },
created() {
const mockEditorApi = {
eventManager: {
addEventType: jest.fn(),
listen: jest.fn(),
removeEventHandler: jest.fn(),
},
};
this.$emit('load', mockEditorApi);
},
render(h) { render(h) {
return h('div'); return h('div');
}, },
......
...@@ -5,10 +5,13 @@ import { ...@@ -5,10 +5,13 @@ import {
registerHTMLToMarkdownRenderer, registerHTMLToMarkdownRenderer,
addImage, addImage,
getMarkdown, getMarkdown,
getEditorOptions,
} from '~/vue_shared/components/rich_content_editor/services/editor_service'; } from '~/vue_shared/components/rich_content_editor/services/editor_service';
import buildHTMLToMarkdownRenderer from '~/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer'; import buildHTMLToMarkdownRenderer from '~/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer';
import buildCustomRenderer from '~/vue_shared/components/rich_content_editor/services/build_custom_renderer';
jest.mock('~/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer'); jest.mock('~/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer');
jest.mock('~/vue_shared/components/rich_content_editor/services/build_custom_renderer');
describe('Editor Service', () => { describe('Editor Service', () => {
let mockInstance; let mockInstance;
...@@ -120,4 +123,25 @@ describe('Editor Service', () => { ...@@ -120,4 +123,25 @@ describe('Editor Service', () => {
expect(mockInstance.toMarkOptions.renderer).toBe(extendedRenderer); expect(mockInstance.toMarkOptions.renderer).toBe(extendedRenderer);
}); });
}); });
describe('getEditorOptions', () => {
const externalOptions = {
customRenderers: {},
};
const renderer = {};
beforeEach(() => {
buildCustomRenderer.mockReturnValueOnce(renderer);
});
it('generates a configuration object with a custom HTML renderer and toolbarItems', () => {
expect(getEditorOptions()).toHaveProp('customHTMLRenderer', renderer);
expect(getEditorOptions()).toHaveProp('toolbarItems');
});
it('passes external renderers to the buildCustomRenderers function', () => {
getEditorOptions(externalOptions);
expect(buildCustomRenderer).toHaveBeenCalledWith(externalOptions.customRenderers);
});
});
}); });
...@@ -2,7 +2,6 @@ import { shallowMount } from '@vue/test-utils'; ...@@ -2,7 +2,6 @@ import { shallowMount } from '@vue/test-utils';
import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue'; import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue';
import AddImageModal from '~/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal.vue'; import AddImageModal from '~/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal.vue';
import { import {
EDITOR_OPTIONS,
EDITOR_TYPES, EDITOR_TYPES,
EDITOR_HEIGHT, EDITOR_HEIGHT,
EDITOR_PREVIEW_STYLE, EDITOR_PREVIEW_STYLE,
...@@ -14,6 +13,7 @@ import { ...@@ -14,6 +13,7 @@ import {
removeCustomEventListener, removeCustomEventListener,
addImage, addImage,
registerHTMLToMarkdownRenderer, registerHTMLToMarkdownRenderer,
getEditorOptions,
} from '~/vue_shared/components/rich_content_editor/services/editor_service'; } from '~/vue_shared/components/rich_content_editor/services/editor_service';
jest.mock('~/vue_shared/components/rich_content_editor/services/editor_service', () => ({ jest.mock('~/vue_shared/components/rich_content_editor/services/editor_service', () => ({
...@@ -22,6 +22,7 @@ jest.mock('~/vue_shared/components/rich_content_editor/services/editor_service', ...@@ -22,6 +22,7 @@ jest.mock('~/vue_shared/components/rich_content_editor/services/editor_service',
removeCustomEventListener: jest.fn(), removeCustomEventListener: jest.fn(),
addImage: jest.fn(), addImage: jest.fn(),
registerHTMLToMarkdownRenderer: jest.fn(), registerHTMLToMarkdownRenderer: jest.fn(),
getEditorOptions: jest.fn(),
})); }));
describe('Rich Content Editor', () => { describe('Rich Content Editor', () => {
...@@ -32,13 +33,25 @@ describe('Rich Content Editor', () => { ...@@ -32,13 +33,25 @@ describe('Rich Content Editor', () => {
const findEditor = () => wrapper.find({ ref: 'editor' }); const findEditor = () => wrapper.find({ ref: 'editor' });
const findAddImageModal = () => wrapper.find(AddImageModal); const findAddImageModal = () => wrapper.find(AddImageModal);
beforeEach(() => { const buildWrapper = () => {
wrapper = shallowMount(RichContentEditor, { wrapper = shallowMount(RichContentEditor, {
propsData: { content, imageRoot }, propsData: { content, imageRoot },
}); });
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
}); });
describe('when content is loaded', () => { describe('when content is loaded', () => {
const editorOptions = {};
beforeEach(() => {
getEditorOptions.mockReturnValueOnce(editorOptions);
buildWrapper();
});
it('renders an editor', () => { it('renders an editor', () => {
expect(findEditor().exists()).toBe(true); expect(findEditor().exists()).toBe(true);
}); });
...@@ -47,8 +60,8 @@ describe('Rich Content Editor', () => { ...@@ -47,8 +60,8 @@ describe('Rich Content Editor', () => {
expect(findEditor().props().initialValue).toBe(content); expect(findEditor().props().initialValue).toBe(content);
}); });
it('provides the correct editor options', () => { it('provides options generated by the getEditorOptions service', () => {
expect(findEditor().props().options).toEqual(EDITOR_OPTIONS); expect(findEditor().props().options).toBe(editorOptions);
}); });
it('has the correct preview style', () => { it('has the correct preview style', () => {
...@@ -65,6 +78,10 @@ describe('Rich Content Editor', () => { ...@@ -65,6 +78,10 @@ describe('Rich Content Editor', () => {
}); });
describe('when content is changed', () => { describe('when content is changed', () => {
beforeEach(() => {
buildWrapper();
});
it('emits an input event with the changed content', () => { it('emits an input event with the changed content', () => {
const changedMarkdown = '## Changed Markdown'; const changedMarkdown = '## Changed Markdown';
const getMarkdownMock = jest.fn().mockReturnValueOnce(changedMarkdown); const getMarkdownMock = jest.fn().mockReturnValueOnce(changedMarkdown);
...@@ -77,6 +94,10 @@ describe('Rich Content Editor', () => { ...@@ -77,6 +94,10 @@ describe('Rich Content Editor', () => {
}); });
describe('when content is reset', () => { describe('when content is reset', () => {
beforeEach(() => {
buildWrapper();
});
it('should reset the content via setMarkdown', () => { it('should reset the content via setMarkdown', () => {
const newContent = 'Just the body content excluding the front matter for example'; const newContent = 'Just the body content excluding the front matter for example';
const mockInstance = { invoke: jest.fn() }; const mockInstance = { invoke: jest.fn() };
...@@ -89,35 +110,33 @@ describe('Rich Content Editor', () => { ...@@ -89,35 +110,33 @@ describe('Rich Content Editor', () => {
}); });
describe('when editor is loaded', () => { describe('when editor is loaded', () => {
let mockEditorApi;
beforeEach(() => { beforeEach(() => {
mockEditorApi = { eventManager: { addEventType: jest.fn(), listen: jest.fn() } }; buildWrapper();
findEditor().vm.$emit('load', mockEditorApi);
}); });
it('adds the CUSTOM_EVENTS.openAddImageModal custom event listener', () => { it('adds the CUSTOM_EVENTS.openAddImageModal custom event listener', () => {
expect(addCustomEventListener).toHaveBeenCalledWith( expect(addCustomEventListener).toHaveBeenCalledWith(
mockEditorApi, wrapper.vm.editorApi,
CUSTOM_EVENTS.openAddImageModal, CUSTOM_EVENTS.openAddImageModal,
wrapper.vm.onOpenAddImageModal, wrapper.vm.onOpenAddImageModal,
); );
}); });
it('registers HTML to markdown renderer', () => { it('registers HTML to markdown renderer', () => {
expect(registerHTMLToMarkdownRenderer).toHaveBeenCalledWith(mockEditorApi); expect(registerHTMLToMarkdownRenderer).toHaveBeenCalledWith(wrapper.vm.editorApi);
}); });
}); });
describe('when editor is destroyed', () => { describe('when editor is destroyed', () => {
it('removes the CUSTOM_EVENTS.openAddImageModal custom event listener', () => { beforeEach(() => {
const mockEditorApi = { eventManager: { removeEventHandler: jest.fn() } }; buildWrapper();
});
wrapper.vm.editorApi = mockEditorApi; it('removes the CUSTOM_EVENTS.openAddImageModal custom event listener', () => {
wrapper.vm.$destroy(); wrapper.vm.$destroy();
expect(removeCustomEventListener).toHaveBeenCalledWith( expect(removeCustomEventListener).toHaveBeenCalledWith(
mockEditorApi, wrapper.vm.editorApi,
CUSTOM_EVENTS.openAddImageModal, CUSTOM_EVENTS.openAddImageModal,
wrapper.vm.onOpenAddImageModal, wrapper.vm.onOpenAddImageModal,
); );
...@@ -125,6 +144,10 @@ describe('Rich Content Editor', () => { ...@@ -125,6 +144,10 @@ describe('Rich Content Editor', () => {
}); });
describe('add image modal', () => { describe('add image modal', () => {
beforeEach(() => {
buildWrapper();
});
it('renders an addImageModal component', () => { it('renders an addImageModal component', () => {
expect(findAddImageModal().exists()).toBe(true); expect(findAddImageModal().exists()).toBe(true);
}); });
......
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