diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-08-19 12:08:42 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-08-19 12:08:42 +0300 |
commit | b76ae638462ab0f673e5915986070518dd3f9ad3 (patch) | |
tree | bdab0533383b52873be0ec0eb4d3c66598ff8b91 /app/assets/javascripts/editor | |
parent | 434373eabe7b4be9593d18a585fb763f1e5f1a6f (diff) |
Add latest changes from gitlab-org/gitlab@14-2-stable-eev14.2.0-rc42
Diffstat (limited to 'app/assets/javascripts/editor')
-rw-r--r-- | app/assets/javascripts/editor/constants.js | 6 | ||||
-rw-r--r-- | app/assets/javascripts/editor/extensions/source_editor_markdown_ext.js | 159 | ||||
-rw-r--r-- | app/assets/javascripts/editor/source_editor.js | 29 | ||||
-rw-r--r-- | app/assets/javascripts/editor/utils.js | 31 |
4 files changed, 198 insertions, 27 deletions
diff --git a/app/assets/javascripts/editor/constants.js b/app/assets/javascripts/editor/constants.js index 849ff91841a..d40d19000fb 100644 --- a/app/assets/javascripts/editor/constants.js +++ b/app/assets/javascripts/editor/constants.js @@ -28,3 +28,9 @@ export const EDITOR_DIFF_INSTANCE_FN = 'createDiffInstance'; // '*.gitlab-ci.yml' regardless of project configuration. // https://gitlab.com/gitlab-org/gitlab/-/issues/293641 export const EXTENSION_CI_SCHEMA_FILE_NAME_MATCH = '.gitlab-ci.yml'; + +export const EXTENSION_MARKDOWN_PREVIEW_PANEL_CLASS = 'md'; +export const EXTENSION_MARKDOWN_PREVIEW_PANEL_PARENT_CLASS = 'source-editor-preview'; +export const EXTENSION_MARKDOWN_PREVIEW_ACTION_ID = 'markdown-preview'; +export const EXTENSION_MARKDOWN_PREVIEW_PANEL_WIDTH = 0.5; // 50% of the width +export const EXTENSION_MARKDOWN_PREVIEW_UPDATE_DELAY = 250; // ms diff --git a/app/assets/javascripts/editor/extensions/source_editor_markdown_ext.js b/app/assets/javascripts/editor/extensions/source_editor_markdown_ext.js index 997503a12f5..76e009164f7 100644 --- a/app/assets/javascripts/editor/extensions/source_editor_markdown_ext.js +++ b/app/assets/javascripts/editor/extensions/source_editor_markdown_ext.js @@ -1,6 +1,165 @@ +import { debounce } from 'lodash'; +import { BLOB_PREVIEW_ERROR } from '~/blob_edit/constants'; +import createFlash from '~/flash'; +import { sanitize } from '~/lib/dompurify'; +import axios from '~/lib/utils/axios_utils'; +import { __ } from '~/locale'; +import syntaxHighlight from '~/syntax_highlight'; +import { + EXTENSION_MARKDOWN_PREVIEW_PANEL_CLASS, + EXTENSION_MARKDOWN_PREVIEW_ACTION_ID, + EXTENSION_MARKDOWN_PREVIEW_PANEL_WIDTH, + EXTENSION_MARKDOWN_PREVIEW_PANEL_PARENT_CLASS, + EXTENSION_MARKDOWN_PREVIEW_UPDATE_DELAY, +} from '../constants'; import { SourceEditorExtension } from './source_editor_extension_base'; +const getPreview = (text, projectPath = '') => { + let url; + + if (projectPath) { + url = `/${projectPath}/preview_markdown`; + } else { + const { group, project } = document.body.dataset; + url = `/${group}/${project}/preview_markdown`; + } + return axios + .post(url, { + text, + }) + .then(({ data }) => { + return data.body; + }); +}; + +const setupDomElement = ({ injectToEl = null } = {}) => { + const previewEl = document.createElement('div'); + previewEl.classList.add(EXTENSION_MARKDOWN_PREVIEW_PANEL_CLASS); + previewEl.style.display = 'none'; + if (injectToEl) { + injectToEl.appendChild(previewEl); + } + return previewEl; +}; + export class EditorMarkdownExtension extends SourceEditorExtension { + constructor({ instance, projectPath, ...args } = {}) { + super({ instance, ...args }); + Object.assign(instance, { + projectPath, + preview: { + el: undefined, + action: undefined, + shown: false, + modelChangeListener: undefined, + }, + }); + this.setupPreviewAction.call(instance); + + instance.getModel().onDidChangeLanguage(({ newLanguage, oldLanguage } = {}) => { + if (newLanguage === 'markdown' && oldLanguage !== newLanguage) { + instance.setupPreviewAction(); + } else { + instance.cleanup(); + } + }); + + instance.onDidChangeModel(() => { + const model = instance.getModel(); + if (model) { + const { language } = model.getLanguageIdentifier(); + instance.cleanup(); + if (language === 'markdown') { + instance.setupPreviewAction(); + } + } + }); + } + + static togglePreviewLayout() { + const { width, height } = this.getLayoutInfo(); + const newWidth = this.preview.shown + ? width / EXTENSION_MARKDOWN_PREVIEW_PANEL_WIDTH + : width * EXTENSION_MARKDOWN_PREVIEW_PANEL_WIDTH; + this.layout({ width: newWidth, height }); + } + + static togglePreviewPanel() { + const parentEl = this.getDomNode().parentElement; + const { el: previewEl } = this.preview; + parentEl.classList.toggle(EXTENSION_MARKDOWN_PREVIEW_PANEL_PARENT_CLASS); + + if (previewEl.style.display === 'none') { + // Show the preview panel + this.fetchPreview(); + } else { + // Hide the preview panel + previewEl.style.display = 'none'; + } + } + + cleanup() { + if (this.preview.modelChangeListener) { + this.preview.modelChangeListener.dispose(); + } + this.preview.action.dispose(); + if (this.preview.shown) { + EditorMarkdownExtension.togglePreviewPanel.call(this); + EditorMarkdownExtension.togglePreviewLayout.call(this); + } + this.preview.shown = false; + } + + fetchPreview() { + const { el: previewEl } = this.preview; + getPreview(this.getValue(), this.projectPath) + .then((data) => { + previewEl.innerHTML = sanitize(data); + syntaxHighlight(previewEl.querySelectorAll('.js-syntax-highlight')); + previewEl.style.display = 'block'; + }) + .catch(() => createFlash(BLOB_PREVIEW_ERROR)); + } + + setupPreviewAction() { + if (this.getAction(EXTENSION_MARKDOWN_PREVIEW_ACTION_ID)) return; + + this.preview.action = this.addAction({ + id: EXTENSION_MARKDOWN_PREVIEW_ACTION_ID, + label: __('Preview Markdown'), + keybindings: [ + // eslint-disable-next-line no-bitwise,no-undef + monaco.KeyMod.chord(monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KEY_P), + ], + contextMenuGroupId: 'navigation', + contextMenuOrder: 1.5, + + // Method that will be executed when the action is triggered. + // @param ed The editor instance is passed in as a convenience + run(instance) { + instance.togglePreview(); + }, + }); + } + + togglePreview() { + if (!this.preview?.el) { + this.preview.el = setupDomElement({ injectToEl: this.getDomNode().parentElement }); + } + EditorMarkdownExtension.togglePreviewLayout.call(this); + EditorMarkdownExtension.togglePreviewPanel.call(this); + + if (!this.preview?.shown) { + this.preview.modelChangeListener = this.onDidChangeModelContent( + debounce(this.fetchPreview.bind(this), EXTENSION_MARKDOWN_PREVIEW_UPDATE_DELAY), + ); + } else { + this.preview.modelChangeListener.dispose(); + } + + this.preview.shown = !this.preview?.shown; + } + getSelectedText(selection = this.getSelection()) { const { startLineNumber, endLineNumber, startColumn, endColumn } = selection; const valArray = this.getValue().split('\n'); diff --git a/app/assets/javascripts/editor/source_editor.js b/app/assets/javascripts/editor/source_editor.js index ee97714824e..81ddf8d77fa 100644 --- a/app/assets/javascripts/editor/source_editor.js +++ b/app/assets/javascripts/editor/source_editor.js @@ -1,7 +1,6 @@ -import { editor as monacoEditor, languages as monacoLanguages, Uri } from 'monaco-editor'; +import { editor as monacoEditor, Uri } from 'monaco-editor'; import { defaultEditorOptions } from '~/ide/lib/editor_options'; import languages from '~/ide/lib/languages'; -import { DEFAULT_THEME, themes } from '~/ide/lib/themes'; import { registerLanguages } from '~/ide/utils'; import { joinPaths } from '~/lib/utils/url_utility'; import { uuids } from '~/lib/utils/uuids'; @@ -11,7 +10,7 @@ import { EDITOR_READY_EVENT, EDITOR_TYPE_DIFF, } from './constants'; -import { clearDomElement } from './utils'; +import { clearDomElement, setupEditorTheme, getBlobLanguage } from './utils'; export default class SourceEditor { constructor(options = {}) { @@ -22,26 +21,11 @@ export default class SourceEditor { ...options, }; - SourceEditor.setupMonacoTheme(); + setupEditorTheme(); registerLanguages(...languages); } - static setupMonacoTheme() { - const themeName = window.gon?.user_color_scheme || DEFAULT_THEME; - const theme = themes.find((t) => t.name === themeName); - if (theme) monacoEditor.defineTheme(themeName, theme.data); - monacoEditor.setTheme(theme ? themeName : DEFAULT_THEME); - } - - static getModelLanguage(path) { - const ext = `.${path.split('.').pop()}`; - const language = monacoLanguages - .getLanguages() - .find((lang) => lang.extensions.indexOf(ext) !== -1); - return language ? language.id : 'plaintext'; - } - static pushToImportsArray(arr, toImport) { arr.push(import(toImport)); } @@ -124,10 +108,7 @@ export default class SourceEditor { return model; } const diffModel = { - original: monacoEditor.createModel( - blobOriginalContent, - SourceEditor.getModelLanguage(model.uri.path), - ), + original: monacoEditor.createModel(blobOriginalContent, getBlobLanguage(model.uri.path)), modified: model, }; instance.setModel(diffModel); @@ -155,7 +136,7 @@ export default class SourceEditor { }; static instanceUpdateLanguage(inst, path) { - const lang = SourceEditor.getModelLanguage(path); + const lang = getBlobLanguage(path); const model = inst.getModel(); return monacoEditor.setModelLanguage(model, lang); } diff --git a/app/assets/javascripts/editor/utils.js b/app/assets/javascripts/editor/utils.js index af4473413f4..df9d3f2b9fb 100644 --- a/app/assets/javascripts/editor/utils.js +++ b/app/assets/javascripts/editor/utils.js @@ -1,3 +1,6 @@ +import { editor as monacoEditor, languages as monacoLanguages } from 'monaco-editor'; +import { DEFAULT_THEME, themes } from '~/ide/lib/themes'; + export const clearDomElement = (el) => { if (!el || !el.firstChild) return; @@ -6,6 +9,28 @@ export const clearDomElement = (el) => { } }; -export default () => ({ - clearDomElement, -}); +export const setupEditorTheme = () => { + const themeName = window.gon?.user_color_scheme || DEFAULT_THEME; + const theme = themes.find((t) => t.name === themeName); + if (theme) monacoEditor.defineTheme(themeName, theme.data); + monacoEditor.setTheme(theme ? themeName : DEFAULT_THEME); +}; + +export const getBlobLanguage = (blobPath) => { + const defaultLanguage = 'plaintext'; + + if (!blobPath) { + return defaultLanguage; + } + + const ext = `.${blobPath.split('.').pop()}`; + const language = monacoLanguages + .getLanguages() + .find((lang) => lang.extensions.indexOf(ext) !== -1); + return language ? language.id : defaultLanguage; +}; + +export const setupCodeSnippet = (el) => { + monacoEditor.colorizeElement(el); + setupEditorTheme(); +}; |