diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-06-18 11:18:50 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-06-18 11:18:50 +0000 |
commit | 8c7f4e9d5f36cff46365a7f8c4b9c21578c1e781 (patch) | |
tree | a77e7fe7a93de11213032ed4ab1f33a3db51b738 /app/assets/javascripts/vue_shared/components/rich_content_editor | |
parent | 00b35af3db1abfe813a778f643dad221aad51fca (diff) | |
download | gitlab-ce-8c7f4e9d5f36cff46365a7f8c4b9c21578c1e781.tar.gz |
Add latest changes from gitlab-org/gitlab@13-1-stable-ee
Diffstat (limited to 'app/assets/javascripts/vue_shared/components/rich_content_editor')
6 files changed, 218 insertions, 50 deletions
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/constants.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/constants.js index 457f1806452..1566c2c784b 100644 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/constants.js +++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/constants.js @@ -1,5 +1,9 @@ import { __ } from '~/locale'; -import { generateToolbarItem } from './toolbar_service'; +import { generateToolbarItem } from './editor_service'; + +export const CUSTOM_EVENTS = { + openAddImageModal: 'gl_openAddImageModal', +}; /* eslint-disable @gitlab/require-i18n-strings */ const TOOLBAR_ITEM_CONFIGS = [ @@ -10,7 +14,6 @@ const TOOLBAR_ITEM_CONFIGS = [ { isDivider: true }, { icon: 'quote', command: 'Blockquote', tooltip: __('Insert a quote') }, { icon: 'link', event: 'openPopupAddLink', tooltip: __('Add a link') }, - { icon: 'doc-code', command: 'CodeBlock', tooltip: __('Insert a code block') }, { isDivider: true }, { icon: 'list-bulleted', command: 'UL', tooltip: __('Add a bullet list') }, { icon: 'list-numbered', command: 'OL', tooltip: __('Add a numbered list') }, @@ -20,8 +23,10 @@ const TOOLBAR_ITEM_CONFIGS = [ { isDivider: true }, { icon: 'dash', command: 'HR', tooltip: __('Add a line') }, { icon: 'table', event: 'openPopupAddTable', classes: 'tui-table', tooltip: __('Add a table') }, + { icon: 'doc-image', event: CUSTOM_EVENTS.openAddImageModal, tooltip: __('Insert an image') }, { isDivider: true }, { icon: 'code', command: 'Code', tooltip: __('Insert inline code') }, + { icon: 'doc-code', command: 'CodeBlock', tooltip: __('Insert a code block') }, ]; export const EDITOR_OPTIONS = { @@ -29,6 +34,7 @@ export const EDITOR_OPTIONS = { }; export const EDITOR_TYPES = { + markdown: 'markdown', wysiwyg: 'wysiwyg', }; diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/editor_service.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/editor_service.js new file mode 100644 index 00000000000..278cd50a947 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/editor_service.js @@ -0,0 +1,42 @@ +import Vue from 'vue'; +import ToolbarItem from './toolbar_item.vue'; + +const buildWrapper = propsData => { + const instance = new Vue({ + render(createElement) { + return createElement(ToolbarItem, propsData); + }, + }); + + instance.$mount(); + return instance.$el; +}; + +export const generateToolbarItem = config => { + const { icon, classes, event, command, tooltip, isDivider } = config; + + if (isDivider) { + return 'divider'; + } + + return { + type: 'button', + options: { + el: buildWrapper({ props: { icon, tooltip }, class: classes }), + event, + command, + }, + }; +}; + +export const addCustomEventListener = (editorApi, event, handler) => { + editorApi.eventManager.addEventType(event); + editorApi.eventManager.listen(event, handler); +}; + +export const removeCustomEventListener = (editorApi, event, handler) => + editorApi.eventManager.removeEventHandler(event, handler); + +export const addImage = ({ editor }, image) => editor.exec('AddImage', image); + +export const getMarkdown = editorInstance => editorInstance.invoke('getMarkdown'); diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/modals/add_image_modal.vue b/app/assets/javascripts/vue_shared/components/rich_content_editor/modals/add_image_modal.vue new file mode 100644 index 00000000000..40063065926 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/modals/add_image_modal.vue @@ -0,0 +1,74 @@ +<script> +import { isSafeURL } from '~/lib/utils/url_utility'; +import { GlModal, GlFormGroup, GlFormInput } from '@gitlab/ui'; +import { __ } from '~/locale'; + +export default { + components: { + GlModal, + GlFormGroup, + GlFormInput, + }, + data() { + return { + error: null, + imageUrl: null, + altText: null, + modalTitle: __('Image Details'), + okTitle: __('Insert'), + urlLabel: __('Image URL'), + descriptionLabel: __('Description'), + }; + }, + methods: { + show() { + this.error = null; + this.imageUrl = null; + this.altText = null; + + this.$refs.modal.show(); + }, + onOk(event) { + if (!this.isValid()) { + event.preventDefault(); + return; + } + + const { imageUrl, altText } = this; + + this.$emit('addImage', { imageUrl, altText: altText || __('image') }); + }, + isValid() { + if (!isSafeURL(this.imageUrl)) { + this.error = __('Please provide a valid URL'); + this.$refs.urlInput.$el.focus(); + return false; + } + + return true; + }, + }, +}; +</script> +<template> + <gl-modal + ref="modal" + modal-id="add-image-modal" + :title="modalTitle" + :ok-title="okTitle" + @ok="onOk" + > + <gl-form-group + :label="urlLabel" + label-for="url-input" + :state="!Boolean(error)" + :invalid-feedback="error" + > + <gl-form-input id="url-input" ref="urlInput" v-model="imageUrl" /> + </gl-form-group> + + <gl-form-group :label="descriptionLabel" label-for="description-input"> + <gl-form-input id="description-input" ref="descriptionInput" v-model="altText" /> + </gl-form-group> + </gl-modal> +</template> diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue b/app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue index ba3696c8ad1..5c310fc059b 100644 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue +++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue @@ -2,7 +2,21 @@ import 'codemirror/lib/codemirror.css'; import '@toast-ui/editor/dist/toastui-editor.css'; -import { EDITOR_OPTIONS, EDITOR_TYPES, EDITOR_HEIGHT, EDITOR_PREVIEW_STYLE } from './constants'; +import AddImageModal from './modals/add_image_modal.vue'; +import { + EDITOR_OPTIONS, + EDITOR_TYPES, + EDITOR_HEIGHT, + EDITOR_PREVIEW_STYLE, + CUSTOM_EVENTS, +} from './constants'; + +import { + addCustomEventListener, + removeCustomEventListener, + addImage, + getMarkdown, +} from './editor_service'; export default { components: { @@ -10,6 +24,7 @@ export default { import(/* webpackChunkName: 'toast_editor' */ '@toast-ui/vue-editor').then( toast => toast.Editor, ), + AddImageModal, }, props: { value: { @@ -37,29 +52,85 @@ export default { default: EDITOR_PREVIEW_STYLE, }, }, + data() { + return { + editorApi: null, + previousMode: null, + }; + }, computed: { editorOptions() { return { ...EDITOR_OPTIONS, ...this.options }; }, + editorInstance() { + return this.$refs.editor; + }, + }, + watch: { + value(newVal) { + const isSameMode = this.previousMode === this.editorApi.currentMode; + if (!isSameMode) { + /* + The ToastUI Editor consumes its content via the `initial-value` prop and then internally + manages changes. If we desire the `v-model` to work as expected, we need to manually call + `setMarkdown`. However, if we do this in each v-model change we'll continually prevent + the editor from internally managing changes. Thus we use the `previousMode` flag as + confirmation to actually update its internals. This is initially designed so that front + matter is excluded from editing in wysiwyg mode, but included in markdown mode. + */ + this.editorInstance.invoke('setMarkdown', newVal); + this.previousMode = this.editorApi.currentMode; + } + }, + }, + beforeDestroy() { + removeCustomEventListener( + this.editorApi, + CUSTOM_EVENTS.openAddImageModal, + this.onOpenAddImageModal, + ); + + this.editorApi.eventManager.removeEventHandler('changeMode', this.onChangeMode); }, methods: { onContentChanged() { - this.$emit('input', this.getMarkdown()); + this.$emit('input', getMarkdown(this.editorInstance)); + }, + onLoad(editorApi) { + this.editorApi = editorApi; + + addCustomEventListener( + this.editorApi, + CUSTOM_EVENTS.openAddImageModal, + this.onOpenAddImageModal, + ); + + this.editorApi.eventManager.listen('changeMode', this.onChangeMode); + }, + onOpenAddImageModal() { + this.$refs.addImageModal.show(); + }, + onAddImage(image) { + addImage(this.editorInstance, image); }, - getMarkdown() { - return this.$refs.editor.invoke('getMarkdown'); + onChangeMode(newMode) { + this.$emit('modeChange', newMode); }, }, }; </script> <template> - <toast-editor - ref="editor" - :initial-value="value" - :options="editorOptions" - :preview-style="previewStyle" - :initial-edit-type="initialEditType" - :height="height" - @change="onContentChanged" - /> + <div> + <toast-editor + ref="editor" + :initial-value="value" + :options="editorOptions" + :preview-style="previewStyle" + :initial-edit-type="initialEditType" + :height="height" + @change="onContentChanged" + @load="onLoad" + /> + <add-image-modal ref="addImageModal" @addImage="onAddImage" /> + </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/toolbar_item.vue b/app/assets/javascripts/vue_shared/components/rich_content_editor/toolbar_item.vue index 58aaeef45f2..4271f6053ed 100644 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/toolbar_item.vue +++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/toolbar_item.vue @@ -1,20 +1,27 @@ <script> -import { GlIcon } from '@gitlab/ui'; +import { GlIcon, GlTooltipDirective } from '@gitlab/ui'; export default { components: { GlIcon, }, + directives: { + GlTooltip: GlTooltipDirective, + }, props: { icon: { type: String, required: true, }, + tooltip: { + type: String, + required: true, + }, }, }; </script> <template> - <button class="p-0 gl-display-flex toolbar-button"> - <gl-icon class="gl-mx-auto" :name="icon" /> + <button v-gl-tooltip="{ title: tooltip }" class="p-0 gl-display-flex toolbar-button"> + <gl-icon class="gl-mx-auto gl-align-self-center" :name="icon" /> </button> </template> diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/toolbar_service.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/toolbar_service.js deleted file mode 100644 index fff90f3e3fb..00000000000 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/toolbar_service.js +++ /dev/null @@ -1,32 +0,0 @@ -import Vue from 'vue'; -import ToolbarItem from './toolbar_item.vue'; - -const buildWrapper = propsData => { - const instance = new Vue({ - render(createElement) { - return createElement(ToolbarItem, propsData); - }, - }); - - instance.$mount(); - return instance.$el; -}; - -// eslint-disable-next-line import/prefer-default-export -export const generateToolbarItem = config => { - const { icon, classes, event, command, tooltip, isDivider } = config; - - if (isDivider) { - return 'divider'; - } - - return { - type: 'button', - options: { - el: buildWrapper({ props: { icon }, class: classes }), - event, - command, - tooltip, - }, - }; -}; |