Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js')
-rw-r--r--app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js167
1 files changed, 167 insertions, 0 deletions
diff --git a/app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js b/app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js
new file mode 100644
index 00000000000..9d53268c340
--- /dev/null
+++ b/app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js
@@ -0,0 +1,167 @@
+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';
+
+const fetchPreview = (text, previewMarkdownPath) => {
+ return axios
+ .post(previewMarkdownPath, {
+ 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 EditorMarkdownPreviewExtension {
+ static get extensionName() {
+ return 'EditorMarkdownPreview';
+ }
+
+ onSetup(instance, setupOptions) {
+ this.preview = {
+ el: undefined,
+ action: undefined,
+ shown: false,
+ modelChangeListener: undefined,
+ path: setupOptions.previewMarkdownPath,
+ };
+ this.setupPreviewAction(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();
+ }
+ }
+ });
+ }
+
+ togglePreviewLayout(instance) {
+ const { width, height } = instance.getLayoutInfo();
+ const newWidth = this.preview.shown
+ ? width / EXTENSION_MARKDOWN_PREVIEW_PANEL_WIDTH
+ : width * EXTENSION_MARKDOWN_PREVIEW_PANEL_WIDTH;
+ instance.layout({ width: newWidth, height });
+ }
+
+ togglePreviewPanel(instance) {
+ const parentEl = instance.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(instance);
+ } else {
+ // Hide the preview panel
+ previewEl.style.display = 'none';
+ }
+ }
+
+ fetchPreview(instance) {
+ const { el: previewEl } = this.preview;
+ fetchPreview(instance.getValue(), this.preview.path)
+ .then((data) => {
+ previewEl.innerHTML = sanitize(data);
+ syntaxHighlight(previewEl.querySelectorAll('.js-syntax-highlight'));
+ previewEl.style.display = 'block';
+ })
+ .catch(() => createFlash(BLOB_PREVIEW_ERROR));
+ }
+
+ setupPreviewAction(instance) {
+ if (instance.getAction(EXTENSION_MARKDOWN_PREVIEW_ACTION_ID)) return;
+
+ this.preview.action = instance.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(inst) {
+ inst.togglePreview();
+ },
+ });
+ }
+
+ provides() {
+ return {
+ markdownPreview: this.preview,
+
+ cleanup: (instance) => {
+ if (this.preview.modelChangeListener) {
+ this.preview.modelChangeListener.dispose();
+ }
+ this.preview.action.dispose();
+ if (this.preview.shown) {
+ this.togglePreviewPanel(instance);
+ this.togglePreviewLayout(instance);
+ }
+ this.preview.shown = false;
+ },
+
+ fetchPreview: (instance) => this.fetchPreview(instance),
+
+ setupPreviewAction: (instance) => this.setupPreviewAction(instance),
+
+ togglePreview: (instance) => {
+ if (!this.preview?.el) {
+ this.preview.el = setupDomElement({ injectToEl: instance.getDomNode().parentElement });
+ }
+ this.togglePreviewLayout(instance);
+ this.togglePreviewPanel(instance);
+
+ if (!this.preview?.shown) {
+ this.preview.modelChangeListener = instance.onDidChangeModelContent(
+ debounce(
+ this.fetchPreview.bind(this, instance),
+ EXTENSION_MARKDOWN_PREVIEW_UPDATE_DELAY,
+ ),
+ );
+ } else {
+ this.preview.modelChangeListener.dispose();
+ }
+
+ this.preview.shown = !this.preview?.shown;
+ },
+ };
+ }
+}