Commit e7135c97 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '280798-markdown-live-preview' into 'master'

Markdown Live preview for Source Editor

See merge request gitlab-org/gitlab!68020
parents dfa611f0 54febab7
import $ from 'jquery'; import $ from 'jquery';
import { FileTemplateExtension } from '~/editor/extensions/source_editor_file_template_ext'; import { FileTemplateExtension } from '~/editor/extensions/source_editor_file_template_ext';
import SourceEditor from '~/editor/source_editor'; import SourceEditor from '~/editor/source_editor';
import { getBlobLanguage } from '~/editor/utils';
import createFlash from '~/flash'; import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { addEditorMarkdownListeners } from '~/lib/utils/text_markdown'; import { addEditorMarkdownListeners } from '~/lib/utils/text_markdown';
...@@ -16,16 +17,7 @@ export default class EditBlob { ...@@ -16,16 +17,7 @@ export default class EditBlob {
this.configureMonacoEditor(); this.configureMonacoEditor();
if (this.options.isMarkdown) { if (this.options.isMarkdown) {
import('~/editor/extensions/source_editor_markdown_ext') this.fetchMarkdownExtension();
.then(({ EditorMarkdownExtension: MarkdownExtension } = {}) => {
this.editor.use(new MarkdownExtension());
addEditorMarkdownListeners(this.editor);
})
.catch((e) =>
createFlash({
message: `${BLOB_EDITOR_ERROR}: ${e}`,
}),
);
} }
this.initModePanesAndLinks(); this.initModePanesAndLinks();
...@@ -34,12 +26,30 @@ export default class EditBlob { ...@@ -34,12 +26,30 @@ export default class EditBlob {
this.editor.focus(); this.editor.focus();
} }
fetchMarkdownExtension() {
import('~/editor/extensions/source_editor_markdown_ext')
.then(({ EditorMarkdownExtension: MarkdownExtension } = {}) => {
this.editor.use(
new MarkdownExtension({ instance: this.editor, projectPath: this.options.projectPath }),
);
this.hasMarkdownExtension = true;
addEditorMarkdownListeners(this.editor);
})
.catch((e) =>
createFlash({
message: `${BLOB_EDITOR_ERROR}: ${e}`,
}),
);
}
configureMonacoEditor() { configureMonacoEditor() {
const editorEl = document.getElementById('editor'); const editorEl = document.getElementById('editor');
const fileNameEl = document.getElementById('file_path') || document.getElementById('file_name'); const fileNameEl = document.getElementById('file_path') || document.getElementById('file_name');
const fileContentEl = document.getElementById('file-content'); const fileContentEl = document.getElementById('file-content');
const form = document.querySelector('.js-edit-blob-form'); const form = document.querySelector('.js-edit-blob-form');
this.hasMarkdownExtension = false;
const rootEditor = new SourceEditor(); const rootEditor = new SourceEditor();
this.editor = rootEditor.createInstance({ this.editor = rootEditor.createInstance({
...@@ -51,6 +61,12 @@ export default class EditBlob { ...@@ -51,6 +61,12 @@ export default class EditBlob {
fileNameEl.addEventListener('change', () => { fileNameEl.addEventListener('change', () => {
this.editor.updateModelLanguage(fileNameEl.value); this.editor.updateModelLanguage(fileNameEl.value);
const newLang = getBlobLanguage(fileNameEl.value);
if (newLang === 'markdown') {
if (!this.hasMarkdownExtension) {
this.fetchMarkdownExtension();
}
}
}); });
form.addEventListener('submit', () => { form.addEventListener('submit', () => {
......
...@@ -28,3 +28,8 @@ export const EDITOR_DIFF_INSTANCE_FN = 'createDiffInstance'; ...@@ -28,3 +28,8 @@ export const EDITOR_DIFF_INSTANCE_FN = 'createDiffInstance';
// '*.gitlab-ci.yml' regardless of project configuration. // '*.gitlab-ci.yml' regardless of project configuration.
// https://gitlab.com/gitlab-org/gitlab/-/issues/293641 // https://gitlab.com/gitlab-org/gitlab/-/issues/293641
export const EXTENSION_CI_SCHEMA_FILE_NAME_MATCH = '.gitlab-ci.yml'; 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
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,
} from '../constants';
import { SourceEditorExtension } from './source_editor_extension_base'; 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 { export class EditorMarkdownExtension extends SourceEditorExtension {
constructor({ instance, projectPath, ...args } = {}) {
super({ instance, ...args });
Object.assign(instance, {
projectPath,
preview: {
el: undefined,
action: undefined,
shown: false,
},
});
this.setupPreviewAction.call(instance);
}
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() {
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.modelChangeListener = this.onDidChangeModelContent(
debounce(this.fetchPreview.bind(this), 250),
);
} else {
this.modelChangeListener.dispose();
}
this.preview.shown = !this.preview?.shown;
this.getModel().onDidChangeLanguage(({ newLanguage, oldLanguage } = {}) => {
if (newLanguage === 'markdown' && oldLanguage !== newLanguage) {
this.setupPreviewAction();
} else {
this.cleanup();
}
});
}
getSelectedText(selection = this.getSelection()) { getSelectedText(selection = this.getSelection()) {
const { startLineNumber, endLineNumber, startColumn, endColumn } = selection; const { startLineNumber, endLineNumber, startColumn, endColumn } = selection;
const valArray = this.getValue().split('\n'); const valArray = this.getValue().split('\n');
......
...@@ -25,6 +25,17 @@ ...@@ -25,6 +25,17 @@
height: 500px; height: 500px;
} }
.source-editor-preview {
@include gl-display-flex;
.md {
@include gl-overflow-scroll;
@include gl-px-6;
@include gl-py-4;
@include gl-w-full;
}
}
.monaco-editor.gl-source-editor { .monaco-editor.gl-source-editor {
.margin-view-overlays { .margin-view-overlays {
.line-numbers { .line-numbers {
......
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