diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-06-07 18:10:23 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-06-07 18:10:23 +0000 |
commit | 21e144f387bc4d77f6128ee87549daf174467518 (patch) | |
tree | a1f7ca7673e157e9175b527fc435b480c974645a /app/assets/javascripts/vue_shared | |
parent | 79f98200f84590af39cf1af7f57f6e8ba89d2bb6 (diff) | |
download | gitlab-ce-21e144f387bc4d77f6128ee87549daf174467518.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/vue_shared')
21 files changed, 0 insertions, 1058 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 deleted file mode 100644 index cbb30baa488..00000000000 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/constants.js +++ /dev/null @@ -1,57 +0,0 @@ -import { __ } from '~/locale'; - -export const CUSTOM_EVENTS = { - openAddImageModal: 'gl_openAddImageModal', - openInsertVideoModal: 'gl_openInsertVideoModal', -}; - -export const YOUTUBE_URL = 'https://www.youtube.com'; - -export const YOUTUBE_EMBED_URL = `${YOUTUBE_URL}/embed`; - -export const ALLOWED_VIDEO_ORIGINS = [YOUTUBE_URL]; - -/* eslint-disable @gitlab/require-i18n-strings */ -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') }, - { icon: 'strikethrough', command: 'Strike', tooltip: __('Add strikethrough text') }, - { isDivider: true }, - { icon: 'quote', command: 'Blockquote', tooltip: __('Insert a quote') }, - { icon: 'link', event: 'openPopupAddLink', tooltip: __('Add a link') }, - { isDivider: true }, - { icon: 'list-bulleted', command: 'UL', tooltip: __('Add a bullet list') }, - { icon: 'list-numbered', command: 'OL', tooltip: __('Add a numbered list') }, - { icon: 'list-task', command: 'Task', tooltip: __('Add a task list') }, - { icon: 'list-indent', command: 'Indent', tooltip: __('Indent') }, - { icon: 'list-outdent', command: 'Outdent', tooltip: __('Outdent') }, - { 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') }, - { icon: 'live-preview', event: CUSTOM_EVENTS.openInsertVideoModal, tooltip: __('Insert video') }, - { isDivider: true }, - { icon: 'code', command: 'Code', tooltip: __('Insert inline code') }, - { icon: 'doc-code', command: 'CodeBlock', tooltip: __('Insert a code block') }, -]; - -export const EDITOR_TYPES = { - markdown: 'markdown', - wysiwyg: 'wysiwyg', -}; - -export const EDITOR_HEIGHT = '100%'; - -export const EDITOR_PREVIEW_STYLE = 'horizontal'; - -export const IMAGE_TABS = { UPLOAD_TAB: 0, URL_TAB: 1 }; - -export const MAX_FILE_SIZE = 2097152; // 2Mb - -export const VIDEO_ATTRIBUTES = { - width: '560', - height: '315', - frameBorder: '0', - allow: 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture', -}; diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal.vue b/app/assets/javascripts/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal.vue deleted file mode 100644 index 82060d2e4ad..00000000000 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal.vue +++ /dev/null @@ -1,134 +0,0 @@ -<script> -import { GlModal, GlFormGroup, GlFormInput, GlTabs, GlTab } from '@gitlab/ui'; -import { isSafeURL, joinPaths } from '~/lib/utils/url_utility'; -import { __ } from '~/locale'; -import { IMAGE_TABS } from '../../constants'; -import UploadImageTab from './upload_image_tab.vue'; - -export default { - components: { - UploadImageTab, - GlModal, - GlFormGroup, - GlFormInput, - GlTabs, - GlTab, - }, - props: { - imageRoot: { - type: String, - required: true, - }, - }, - data() { - return { - file: null, - urlError: null, - imageUrl: null, - description: null, - tabIndex: IMAGE_TABS.UPLOAD_TAB, - uploadImageTab: null, - }; - }, - modalTitle: __('Image details'), - okTitle: __('Insert image'), - urlTabTitle: __('Link to an image'), - urlLabel: __('Image URL'), - descriptionLabel: __('Description'), - uploadTabTitle: __('Upload an image'), - computed: { - altText() { - return this.description; - }, - }, - methods: { - show() { - this.file = null; - this.urlError = null; - this.imageUrl = null; - this.description = null; - this.tabIndex = IMAGE_TABS.UPLOAD_TAB; - - this.$refs.modal.show(); - }, - onOk(event) { - if (this.tabIndex === IMAGE_TABS.UPLOAD_TAB) { - this.submitFile(event); - return; - } - this.submitURL(event); - }, - setFile(file) { - this.file = file; - }, - submitFile(event) { - const { file, altText } = this; - const { uploadImageTab } = this.$refs; - - uploadImageTab.validateFile(); - - if (uploadImageTab.fileError) { - event.preventDefault(); - return; - } - - const imageUrl = joinPaths(this.imageRoot, file.name); - - this.$emit('addImage', { imageUrl, file, altText: altText || file.name }); - }, - submitURL(event) { - if (!this.validateUrl()) { - event.preventDefault(); - return; - } - - const { imageUrl, altText } = this; - - this.$emit('addImage', { imageUrl, altText: altText || imageUrl }); - }, - validateUrl() { - if (!isSafeURL(this.imageUrl)) { - this.urlError = __('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="$options.modalTitle" - :ok-title="$options.okTitle" - @ok="onOk" - > - <gl-tabs v-model="tabIndex"> - <!-- Upload file Tab --> - <gl-tab :title="$options.uploadTabTitle"> - <upload-image-tab ref="uploadImageTab" @input="setFile" /> - </gl-tab> - - <!-- By URL Tab --> - <gl-tab :title="$options.urlTabTitle"> - <gl-form-group - class="gl-mt-5 gl-mb-3" - :label="$options.urlLabel" - label-for="url-input" - :state="!Boolean(urlError)" - :invalid-feedback="urlError" - > - <gl-form-input id="url-input" ref="urlInput" v-model="imageUrl" /> - </gl-form-group> - </gl-tab> - </gl-tabs> - - <!-- Description Input --> - <gl-form-group :label="$options.descriptionLabel" label-for="description-input"> - <gl-form-input id="description-input" ref="descriptionInput" v-model="description" /> - </gl-form-group> - </gl-modal> -</template> diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/modals/add_image/upload_image_tab.vue b/app/assets/javascripts/vue_shared/components/rich_content_editor/modals/add_image/upload_image_tab.vue deleted file mode 100644 index 9baa7f286d7..00000000000 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/modals/add_image/upload_image_tab.vue +++ /dev/null @@ -1,56 +0,0 @@ -<script> -import { GlFormGroup } from '@gitlab/ui'; -import { __ } from '~/locale'; -import { MAX_FILE_SIZE } from '../../constants'; - -export default { - components: { - GlFormGroup, - }, - data() { - return { - file: null, - fileError: null, - }; - }, - fileLabel: __('Select file'), - methods: { - onInput(event) { - [this.file] = event.target.files; - - this.validateFile(); - - if (!this.fileError) { - this.$emit('input', this.file); - } - }, - validateFile() { - this.fileError = null; - - if (!this.file) { - this.fileError = __('Please choose a file'); - } else if (this.file.size > MAX_FILE_SIZE) { - this.fileError = __('Maximum file size is 2MB. Please select a smaller file.'); - } - }, - }, -}; -</script> -<template> - <gl-form-group - class="gl-mt-5 gl-mb-3" - :label="$options.fileLabel" - label-for="file-input" - :state="!Boolean(fileError)" - :invalid-feedback="fileError" - > - <input - id="file-input" - ref="fileInput" - class="gl-mt-3 gl-mb-2" - type="file" - accept="image/*" - @input="onInput" - /> - </gl-form-group> -</template> diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/modals/insert_video_modal.vue b/app/assets/javascripts/vue_shared/components/rich_content_editor/modals/insert_video_modal.vue deleted file mode 100644 index 99bb2080610..00000000000 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/modals/insert_video_modal.vue +++ /dev/null @@ -1,91 +0,0 @@ -<script> -import { GlModal, GlFormGroup, GlFormInput, GlSprintf } from '@gitlab/ui'; -import { isSafeURL } from '~/lib/utils/url_utility'; -import { __ } from '~/locale'; -import { YOUTUBE_URL, YOUTUBE_EMBED_URL } from '../constants'; - -export default { - components: { - GlModal, - GlFormGroup, - GlFormInput, - GlSprintf, - }, - data() { - return { - url: null, - urlError: null, - description: __( - 'If the YouTube URL is https://www.youtube.com/watch?v=0t1DgySidms then the video ID is %{id}', - ), - }; - }, - modalTitle: __('Insert a video'), - okTitle: __('Insert video'), - label: __('YouTube URL or ID'), - methods: { - show() { - this.urlError = null; - this.url = null; - - this.$refs.modal.show(); - }, - onPrimary(event) { - this.submitURL(event); - }, - submitURL(event) { - const url = this.generateUrl(); - - if (!url) { - event.preventDefault(); - return; - } - - this.$emit('insertVideo', url); - }, - generateUrl() { - let { url } = this; - const reYouTubeId = /^[A-z0-9]*$/; - const reYouTubeUrl = RegExp(`${YOUTUBE_URL}/(embed/|watch\\?v=)([A-z0-9]+)`); - - if (reYouTubeId.test(url)) { - url = `${YOUTUBE_EMBED_URL}/${url}`; - } else if (reYouTubeUrl.test(url)) { - url = `${YOUTUBE_EMBED_URL}/${reYouTubeUrl.exec(url)[2]}`; - } - - if (!isSafeURL(url) || !reYouTubeUrl.test(url)) { - this.urlError = __('Please provide a valid YouTube URL or ID'); - this.$refs.urlInput.$el.focus(); - return null; - } - - return url; - }, - }, -}; -</script> -<template> - <gl-modal - ref="modal" - size="sm" - modal-id="insert-video-modal" - :title="$options.modalTitle" - :ok-title="$options.okTitle" - @primary="onPrimary" - > - <gl-form-group - :label="$options.label" - label-for="video-modal-url-input" - :state="!Boolean(urlError)" - :invalid-feedback="urlError" - > - <gl-form-input id="video-modal-url-input" ref="urlInput" v-model="url" /> - <gl-sprintf slot="description" :message="description" class="text-gl-muted"> - <template #id> - <strong>{{ __('0t1DgySidms') }}</strong> - </template> - </gl-sprintf> - </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 deleted file mode 100644 index 8988dab85d2..00000000000 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue +++ /dev/null @@ -1,150 +0,0 @@ -<script> -import 'codemirror/lib/codemirror.css'; -import '@toast-ui/editor/dist/toastui-editor.css'; - -import { EDITOR_TYPES, EDITOR_HEIGHT, EDITOR_PREVIEW_STYLE, CUSTOM_EVENTS } from './constants'; -import AddImageModal from './modals/add_image/add_image_modal.vue'; -import InsertVideoModal from './modals/insert_video_modal.vue'; - -import { - registerHTMLToMarkdownRenderer, - getEditorOptions, - addCustomEventListener, - removeCustomEventListener, - addImage, - getMarkdown, - insertVideo, -} from './services/editor_service'; - -export default { - components: { - ToastEditor: () => - import(/* webpackChunkName: 'toast_editor' */ '@toast-ui/vue-editor').then( - (toast) => toast.Editor, - ), - AddImageModal, - InsertVideoModal, - }, - props: { - content: { - type: String, - required: true, - }, - options: { - type: Object, - required: false, - default: () => null, - }, - initialEditType: { - type: String, - required: false, - default: EDITOR_TYPES.wysiwyg, - }, - height: { - type: String, - required: false, - default: EDITOR_HEIGHT, - }, - previewStyle: { - type: String, - required: false, - default: EDITOR_PREVIEW_STYLE, - }, - imageRoot: { - type: String, - required: true, - }, - }, - data() { - return { - editorApi: null, - previousMode: null, - }; - }, - computed: { - editorInstance() { - return this.$refs.editor; - }, - customEventListeners() { - return [ - { event: CUSTOM_EVENTS.openAddImageModal, listener: this.onOpenAddImageModal }, - { event: CUSTOM_EVENTS.openInsertVideoModal, listener: this.onOpenInsertVideoModal }, - ]; - }, - }, - created() { - this.editorOptions = getEditorOptions(this.options); - }, - beforeDestroy() { - this.removeListeners(); - }, - methods: { - addListeners(editorApi) { - this.customEventListeners.forEach(({ event, listener }) => { - addCustomEventListener(editorApi, event, listener); - }); - - editorApi.eventManager.listen('changeMode', this.onChangeMode); - }, - removeListeners() { - this.customEventListeners.forEach(({ event, listener }) => { - removeCustomEventListener(this.editorApi, event, listener); - }); - - this.editorApi.eventManager.removeEventHandler('changeMode', this.onChangeMode); - }, - resetInitialValue(newVal) { - this.editorInstance.invoke('setMarkdown', newVal); - }, - onContentChanged() { - this.$emit('input', getMarkdown(this.editorInstance)); - }, - onLoad(editorApi) { - this.editorApi = editorApi; - - registerHTMLToMarkdownRenderer(editorApi); - - this.addListeners(editorApi); - - this.$emit('load', { formattedMarkdown: editorApi.getMarkdown() }); - }, - onOpenAddImageModal() { - this.$refs.addImageModal.show(); - }, - onAddImage({ imageUrl, altText, file }) { - const image = { imageUrl, altText }; - - if (file) { - this.$emit('uploadImage', { file, imageUrl }); - } - - addImage(this.editorInstance, image, file); - }, - onOpenInsertVideoModal() { - this.$refs.insertVideoModal.show(); - }, - onInsertVideo(url) { - insertVideo(this.editorInstance, url); - }, - onChangeMode(newMode) { - this.$emit('modeChange', newMode); - }, - }, -}; -</script> -<template> - <div> - <toast-editor - ref="editor" - :initial-value="content" - :options="editorOptions" - :preview-style="previewStyle" - :initial-edit-type="initialEditType" - :height="height" - @change="onContentChanged" - @load="onLoad" - /> - <add-image-modal ref="addImageModal" :image-root="imageRoot" @addImage="onAddImage" /> - <insert-video-modal ref="insertVideoModal" @insertVideo="onInsertVideo" /> - </div> -</template> diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js deleted file mode 100644 index 6ffd280e005..00000000000 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js +++ /dev/null @@ -1,42 +0,0 @@ -import { union, mapValues } from 'lodash'; -import renderAttributeDefinition from './renderers/render_attribute_definition'; -import renderFontAwesomeHtmlInline from './renderers/render_font_awesome_html_inline'; -import renderHeading from './renderers/render_heading'; -import renderBlockHtml from './renderers/render_html_block'; -import renderIdentifierInstanceText from './renderers/render_identifier_instance_text'; -import renderIdentifierParagraph from './renderers/render_identifier_paragraph'; -import renderListItem from './renderers/render_list_item'; -import renderSoftbreak from './renderers/render_softbreak'; - -const htmlInlineRenderers = [renderFontAwesomeHtmlInline]; -const htmlBlockRenderers = [renderBlockHtml]; -const headingRenderers = [renderHeading]; -const paragraphRenderers = [renderIdentifierParagraph, renderBlockHtml]; -const textRenderers = [renderIdentifierInstanceText, renderAttributeDefinition]; -const listItemRenderers = [renderListItem]; -const softbreakRenderers = [renderSoftbreak]; - -const executeRenderer = (renderers, node, context) => { - const availableRenderer = renderers.find((renderer) => renderer.canRender(node, context)); - - return availableRenderer ? availableRenderer.render(node, context) : context.origin(); -}; - -const buildCustomHTMLRenderer = (customRenderers) => { - const renderersByType = { - ...customRenderers, - htmlBlock: union(htmlBlockRenderers, customRenderers?.htmlBlock), - htmlInline: union(htmlInlineRenderers, customRenderers?.htmlInline), - heading: union(headingRenderers, customRenderers?.heading), - item: union(listItemRenderers, customRenderers?.listItem), - paragraph: union(paragraphRenderers, customRenderers?.paragraph), - text: union(textRenderers, customRenderers?.text), - softbreak: union(softbreakRenderers, customRenderers?.softbreak), - }; - - return mapValues(renderersByType, (renderers) => { - return (node, context) => executeRenderer(renderers, node, context); - }); -}; - -export default buildCustomHTMLRenderer; diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer.js deleted file mode 100644 index 273e0a59963..00000000000 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer.js +++ /dev/null @@ -1,109 +0,0 @@ -/* eslint-disable @gitlab/require-i18n-strings */ -import { defaults, repeat } from 'lodash'; - -const DEFAULTS = { - subListIndentSpaces: 4, - unorderedListBulletChar: '-', - incrementListMarker: false, - strong: '*', - emphasis: '_', -}; - -const countIndentSpaces = (text) => { - const matches = text.match(/^\s+/m); - - return matches ? matches[0].length : 0; -}; - -const buildHTMLToMarkdownRender = (baseRenderer, formattingPreferences = {}) => { - const { - subListIndentSpaces, - unorderedListBulletChar, - incrementListMarker, - strong, - emphasis, - } = defaults(formattingPreferences, DEFAULTS); - const sublistNode = 'LI OL, LI UL'; - const unorderedListItemNode = 'UL LI'; - const orderedListItemNode = 'OL LI'; - const emphasisNode = 'EM, I'; - const strongNode = 'STRONG, B'; - const headingNode = 'H1, H2, H3, H4, H5, H6'; - const preCodeNode = 'PRE CODE'; - - return { - TEXT_NODE(node) { - return baseRenderer.getSpaceControlled( - baseRenderer.trim(baseRenderer.getSpaceCollapsedText(node.nodeValue)), - node, - ); - }, - /* - * This converter overwrites the default indented list converter - * to allow us to parameterize the number of indent spaces for - * sublists. - * - * See the original implementation in - * https://github.com/nhn/tui.editor/blob/master/libs/to-mark/src/renderer.basic.js#L161 - */ - [sublistNode](node, subContent) { - const baseResult = baseRenderer.convert(node, subContent); - // Default to 1 to prevent possible divide by 0 - const firstLevelIndentSpacesCount = countIndentSpaces(baseResult) || 1; - const reindentedList = baseResult - .split('\n') - .map((line) => { - const itemIndentSpacesCount = countIndentSpaces(line); - const nestingLevel = Math.ceil(itemIndentSpacesCount / firstLevelIndentSpacesCount); - const indentSpaces = repeat(' ', subListIndentSpaces * nestingLevel); - - return line.replace(/^ +/, indentSpaces); - }) - .join('\n'); - - return reindentedList; - }, - [unorderedListItemNode](node, subContent) { - const baseResult = baseRenderer.convert(node, subContent); - const formatted = baseResult.replace(/^(\s*)([*|-])/, `$1${unorderedListBulletChar}`); - const { attributeDefinition } = node.dataset; - - return attributeDefinition ? `${formatted.trimRight()}\n${attributeDefinition}\n` : formatted; - }, - [orderedListItemNode](node, subContent) { - const baseResult = baseRenderer.convert(node, subContent); - - return incrementListMarker ? baseResult : baseResult.replace(/^(\s*)\d+?\./, '$11.'); - }, - [emphasisNode](node, subContent) { - const result = baseRenderer.convert(node, subContent); - - return result.replace(/(^[*_]{1}|[*_]{1}$)/g, emphasis); - }, - [strongNode](node, subContent) { - const result = baseRenderer.convert(node, subContent); - const strongSyntax = repeat(strong, 2); - - return result.replace(/^[*_]{2}/, strongSyntax).replace(/[*_]{2}$/, strongSyntax); - }, - [headingNode](node, subContent) { - const result = baseRenderer.convert(node, subContent); - const { attributeDefinition } = node.dataset; - - return attributeDefinition ? `${result.trimRight()}\n${attributeDefinition}\n\n` : result; - }, - [preCodeNode](node, subContent) { - const isReferenceDefinition = Boolean(node.dataset.sseReferenceDefinition); - - return isReferenceDefinition - ? `\n\n${node.innerText}\n\n` - : baseRenderer.convert(node, subContent); - }, - IMG(node) { - const { originalSrc } = node.dataset; - return `![${node.alt}](${originalSrc || node.src})`; - }, - }; -}; - -export default buildHTMLToMarkdownRender; diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/editor_service.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/editor_service.js deleted file mode 100644 index 026a4069d9b..00000000000 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/editor_service.js +++ /dev/null @@ -1,116 +0,0 @@ -import { defaults } from 'lodash'; -import Vue from 'vue'; -import { TOOLBAR_ITEM_CONFIGS, VIDEO_ATTRIBUTES } from '../constants'; -import ToolbarItem from '../toolbar_item.vue'; -import buildCustomHTMLRenderer from './build_custom_renderer'; -import buildHtmlToMarkdownRenderer from './build_html_to_markdown_renderer'; -import sanitizeHTML from './sanitize_html'; - -const buildWrapper = (propsData) => { - const instance = new Vue({ - render(createElement) { - return createElement(ToolbarItem, propsData); - }, - }); - - instance.$mount(); - return instance.$el; -}; - -const buildVideoIframe = (src) => { - const wrapper = document.createElement('figure'); - const iframe = document.createElement('iframe'); - const videoAttributes = { ...VIDEO_ATTRIBUTES, src }; - const wrapperClasses = ['gl-relative', 'gl-h-0', 'video_container']; - const iframeClasses = ['gl-absolute', 'gl-top-0', 'gl-left-0', 'gl-w-full', 'gl-h-full']; - - wrapper.setAttribute('contenteditable', 'false'); - wrapper.classList.add(...wrapperClasses); - iframe.classList.add(...iframeClasses); - Object.assign(iframe, videoAttributes); - - wrapper.appendChild(iframe); - - return wrapper; -}; - -const buildImg = (alt, originalSrc, file) => { - const img = document.createElement('img'); - const src = file ? URL.createObjectURL(file) : originalSrc; - const attributes = { alt, src }; - - if (file) { - img.dataset.originalSrc = originalSrc; - } - - Object.assign(img, attributes); - - return img; -}; - -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 }, { altText, imageUrl }, file) => { - if (editor.isWysiwygMode()) { - const img = buildImg(altText, imageUrl, file); - editor.getSquire().insertElement(img); - } else { - editor.insertText(`![${altText}](${imageUrl})`); - } -}; - -export const insertVideo = ({ editor }, url) => { - const videoIframe = buildVideoIframe(url); - - if (editor.isWysiwygMode()) { - editor.getSquire().insertElement(videoIframe); - } else { - editor.insertText(videoIframe.outerHTML); - } -}; - -export const getMarkdown = (editorInstance) => editorInstance.invoke('getMarkdown'); - -/** - * This function allow us to extend Toast UI HTML to Markdown renderer. It is - * a temporary measure because Toast UI does not provide an API - * to achieve this goal. - */ -export const registerHTMLToMarkdownRenderer = (editorApi) => { - const { renderer } = editorApi.toMarkOptions; - - Object.assign(editorApi.toMarkOptions, { - 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)), - customHTMLSanitizer: (html) => sanitizeHTML(html), - }); -}; diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token.js deleted file mode 100644 index 638e5fd6f60..00000000000 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token.js +++ /dev/null @@ -1,63 +0,0 @@ -const buildToken = (type, tagName, props) => { - return { type, tagName, ...props }; -}; - -const TAG_TYPES = { - block: 'div', - inline: 'a', -}; - -// Open helpers (singular and multiple) - -const buildUneditableOpenToken = (tagType = TAG_TYPES.block) => - buildToken('openTag', tagType, { - attributes: { contenteditable: false }, - classNames: [ - 'gl-px-4 gl-py-2 gl-my-5 gl-opacity-5 gl-bg-gray-100 gl-user-select-none gl-cursor-not-allowed', - ], - }); - -export const buildUneditableOpenTokens = (token, tagType = TAG_TYPES.block) => { - return [buildUneditableOpenToken(tagType), token]; -}; - -// Close helpers (singular and multiple) - -export const buildUneditableCloseToken = (tagType = TAG_TYPES.block) => - buildToken('closeTag', tagType); - -export const buildUneditableCloseTokens = (token, tagType = TAG_TYPES.block) => { - return [token, buildUneditableCloseToken(tagType)]; -}; - -// Complete helpers (open plus close) - -export const buildTextToken = (content) => buildToken('text', null, { content }); - -export const buildUneditableBlockTokens = (token) => { - return [...buildUneditableOpenTokens(token), buildUneditableCloseToken()]; -}; - -export const buildUneditableInlineTokens = (token) => { - return [ - ...buildUneditableOpenTokens(token, TAG_TYPES.inline), - buildUneditableCloseToken(TAG_TYPES.inline), - ]; -}; - -export const buildUneditableHtmlAsTextTokens = (node) => { - /* - Toast UI internally appends ' data-tomark-pass ' attribute flags so it can target certain - nested nodes for internal use during Markdown <=> WYSIWYG conversions. In our case, we want - to prevent HTML being rendered completely in WYSIWYG mode and thus we use a `text` vs. `html` - type when building the token. However, in doing so, we need to strip out the ` data-tomark-pass ` - to prevent their persistence within the `text` content as the user did not intend these as edits. - - https://github.com/nhn/tui.editor/blob/cc54ec224fc3a4b6e5a2b19a71650959f41adc0e/apps/editor/src/js/convertor.js#L72 - */ - const regex = / data-tomark-pass /gm; - const content = node.literal.replace(regex, ''); - const htmlAsTextToken = buildToken('text', null, { content }); - - return [buildUneditableOpenToken(), htmlAsTextToken, buildUneditableCloseToken()]; -}; diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_attribute_definition.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_attribute_definition.js deleted file mode 100644 index bd419447a48..00000000000 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_attribute_definition.js +++ /dev/null @@ -1,7 +0,0 @@ -import { isAttributeDefinition } from './render_utils'; - -const canRender = ({ literal }) => isAttributeDefinition(literal); - -const render = () => ({ type: 'html', content: '<!-- sse-attribute-definition -->' }); - -export default { canRender, render }; diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_embedded_ruby_text.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_embedded_ruby_text.js deleted file mode 100644 index 0e122f598e5..00000000000 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_embedded_ruby_text.js +++ /dev/null @@ -1,9 +0,0 @@ -import { renderUneditableLeaf as render } from './render_utils'; - -const embeddedRubyRegex = /(^<%.+%>$)/; - -const canRender = ({ literal }) => { - return embeddedRubyRegex.test(literal); -}; - -export default { canRender, render }; diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_font_awesome_html_inline.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_font_awesome_html_inline.js deleted file mode 100644 index 572f6e3cf9d..00000000000 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_font_awesome_html_inline.js +++ /dev/null @@ -1,11 +0,0 @@ -import { buildUneditableInlineTokens } from './build_uneditable_token'; - -const fontAwesomeRegexOpen = /<i class="fa.+>/; - -const canRender = ({ literal }) => { - return fontAwesomeRegexOpen.test(literal); -}; - -const render = (_, { origin }) => buildUneditableInlineTokens(origin()); - -export default { canRender, render }; diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_heading.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_heading.js deleted file mode 100644 index 71026fd0d65..00000000000 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_heading.js +++ /dev/null @@ -1,6 +0,0 @@ -import { - renderWithAttributeDefinitions as render, - willAlwaysRender as canRender, -} from './render_utils'; - -export default { render, canRender }; diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_html_block.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_html_block.js deleted file mode 100644 index 710b807275b..00000000000 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_html_block.js +++ /dev/null @@ -1,23 +0,0 @@ -import { getURLOrigin } from '~/lib/utils/url_utility'; -import { ALLOWED_VIDEO_ORIGINS } from '../../constants'; -import { buildUneditableHtmlAsTextTokens } from './build_uneditable_token'; - -const isVideoFrame = (html) => { - const parser = new DOMParser(); - const doc = parser.parseFromString(html, 'text/html'); - const { - children: { length }, - } = doc; - const iframe = doc.querySelector('iframe'); - const origin = iframe && getURLOrigin(iframe.getAttribute('src')); - - return length === 1 && ALLOWED_VIDEO_ORIGINS.includes(origin); -}; - -const canRender = ({ type, literal }) => { - return type === 'htmlBlock' && !isVideoFrame(literal); -}; - -const render = (node) => buildUneditableHtmlAsTextTokens(node); - -export default { canRender, render }; diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_identifier_instance_text.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_identifier_instance_text.js deleted file mode 100644 index d7716543b53..00000000000 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_identifier_instance_text.js +++ /dev/null @@ -1,40 +0,0 @@ -import { buildTextToken, buildUneditableInlineTokens } from './build_uneditable_token'; - -/* -Use case examples: -- Majority: two bracket pairs, back-to-back, each with content (including spaces) - - `[environment terraform plans][terraform]` - - `[an issue labelled `~"master:broken"`][broken-master-issues]` -- Minority: two bracket pairs the latter being empty or only one pair with content (including spaces) - - `[this link][]` - - `[this link]` - -Regexp notes: - - `(?:\[.+?\]){1}`: Always one bracket pair with content (including spaces) - - `(?:\[\]|\[.+?\])?`: Optional second pair that may or may not contain content (including spaces) - - `(?!:)`: Never followed by a `:` which is reserved for identifier definition syntax (`[identifier]: /the-link`) - - Each of the three parts is non-captured, but the match as a whole is captured -*/ -const identifierInstanceRegex = /((?:\[.+?\]){1}(?:\[\]|\[.+?\])?(?!:))/g; - -const isIdentifierInstance = (literal) => { - // Reset lastIndex as global flag in regexp are stateful (https://stackoverflow.com/a/11477448) - identifierInstanceRegex.lastIndex = 0; - return identifierInstanceRegex.test(literal); -}; - -const canRender = ({ literal }) => isIdentifierInstance(literal); - -const tokenize = (text) => { - const matches = text.split(identifierInstanceRegex); - const tokens = matches.map((match) => { - const token = buildTextToken(match); - return isIdentifierInstance(match) ? buildUneditableInlineTokens(token) : token; - }); - - return tokens.flat(); -}; - -const render = (_, { origin }) => tokenize(origin().content); - -export default { canRender, render }; diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph.js deleted file mode 100644 index 4829f0f2243..00000000000 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph.js +++ /dev/null @@ -1,40 +0,0 @@ -const identifierRegex = /(^\[.+\]: .+)/; - -const isIdentifier = (text) => { - return identifierRegex.test(text); -}; - -const canRender = (node, context) => { - return isIdentifier(context.getChildrenText(node)); -}; - -const getReferenceDefinitions = (node, definitions = '') => { - if (!node) { - return definitions; - } - - const definition = node.type === 'text' ? node.literal : '\n'; - - return getReferenceDefinitions(node.next, `${definitions}${definition}`); -}; - -const render = (node, { skipChildren }) => { - const content = getReferenceDefinitions(node.firstChild); - - skipChildren(); - - return [ - { - type: 'openTag', - tagName: 'pre', - classNames: ['code-block', 'language-markdown'], - attributes: { 'data-sse-reference-definition': true }, - }, - { type: 'openTag', tagName: 'code' }, - { type: 'text', content }, - { type: 'closeTag', tagName: 'code' }, - { type: 'closeTag', tagName: 'pre' }, - ]; -}; - -export default { canRender, render }; diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_list_item.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_list_item.js deleted file mode 100644 index 71026fd0d65..00000000000 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_list_item.js +++ /dev/null @@ -1,6 +0,0 @@ -import { - renderWithAttributeDefinitions as render, - willAlwaysRender as canRender, -} from './render_utils'; - -export default { render, canRender }; diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_softbreak.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_softbreak.js deleted file mode 100644 index c004e839821..00000000000 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_softbreak.js +++ /dev/null @@ -1,7 +0,0 @@ -const canRender = (node) => ['emph', 'strong'].includes(node.parent?.type); -const render = () => ({ - type: 'text', - content: ' ', -}); - -export default { canRender, render }; diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_utils.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_utils.js deleted file mode 100644 index eff5dbf59f2..00000000000 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_utils.js +++ /dev/null @@ -1,38 +0,0 @@ -import { - buildUneditableBlockTokens, - buildUneditableOpenTokens, - buildUneditableCloseToken, -} from './build_uneditable_token'; - -export const renderUneditableLeaf = (_, { origin }) => buildUneditableBlockTokens(origin()); - -export const renderUneditableBranch = (_, { entering, origin }) => - entering ? buildUneditableOpenTokens(origin()) : buildUneditableCloseToken(); - -const attributeDefinitionRegexp = /(^{:.+}$)/; - -export const isAttributeDefinition = (text) => attributeDefinitionRegexp.test(text); - -const findAttributeDefinition = (node) => { - const literal = - node?.next?.firstChild?.literal || node?.firstChild?.firstChild?.next?.next?.literal; // for headings // for list items; - - return isAttributeDefinition(literal) ? literal : null; -}; - -export const renderWithAttributeDefinitions = (node, { origin }) => { - const attributes = findAttributeDefinition(node); - const token = origin(); - - if (token.type === 'openTag' && attributes) { - Object.assign(token, { - attributes: { - 'data-attribute-definition': attributes, - }, - }); - } - - return token; -}; - -export const willAlwaysRender = () => true; diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/sanitize_html.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/sanitize_html.js deleted file mode 100644 index 486d88466b7..00000000000 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/sanitize_html.js +++ /dev/null @@ -1,22 +0,0 @@ -import createSanitizer from 'dompurify'; -import { getURLOrigin } from '~/lib/utils/url_utility'; -import { ALLOWED_VIDEO_ORIGINS } from '../constants'; - -const sanitizer = createSanitizer(window); -const ADD_TAGS = ['iframe']; - -sanitizer.addHook('uponSanitizeElement', (node) => { - if (node.tagName !== 'IFRAME') { - return; - } - - const origin = getURLOrigin(node.getAttribute('src')); - - if (!ALLOWED_VIDEO_ORIGINS.includes(origin)) { - node.remove(); - } -}); - -const sanitize = (content) => sanitizer.sanitize(content, { ADD_TAGS }); - -export default sanitize; 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 deleted file mode 100644 index 85a67c087bb..00000000000 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/toolbar_item.vue +++ /dev/null @@ -1,31 +0,0 @@ -<script> -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 - v-gl-tooltip="{ title: tooltip }" - :aria-label="tooltip" - class="p-0 gl-display-flex toolbar-button" - > - <gl-icon class="gl-mx-auto gl-align-self-center" :name="icon" /> - </button> -</template> |