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