BookStackApp/BookStack

View on GitHub
resources/js/components/markdown-editor.js

Summary

Maintainability
A
1 hr
Test Coverage
import {Component} from './component';

export class MarkdownEditor extends Component {

    setup() {
        this.elem = this.$el;

        this.pageId = this.$opts.pageId;
        this.textDirection = this.$opts.textDirection;
        this.imageUploadErrorText = this.$opts.imageUploadErrorText;
        this.serverUploadLimitText = this.$opts.serverUploadLimitText;

        this.display = this.$refs.display;
        this.input = this.$refs.input;
        this.divider = this.$refs.divider;
        this.displayWrap = this.$refs.displayWrap;

        const {settingContainer} = this.$refs;
        const settingInputs = settingContainer.querySelectorAll('input[type="checkbox"]');

        this.editor = null;
        window.importVersioned('markdown').then(markdown => {
            return markdown.init({
                pageId: this.pageId,
                container: this.elem,
                displayEl: this.display,
                inputEl: this.input,
                drawioUrl: this.getDrawioUrl(),
                settingInputs: Array.from(settingInputs),
                text: {
                    serverUploadLimit: this.serverUploadLimitText,
                    imageUploadError: this.imageUploadErrorText,
                },
            });
        }).then(editor => {
            this.editor = editor;
            this.setupListeners();
            this.emitEditorEvents();
            this.scrollToTextIfNeeded();
            this.editor.actions.updateAndRender();
        });
    }

    emitEditorEvents() {
        window.$events.emitPublic(this.elem, 'editor-markdown::setup', {
            markdownIt: this.editor.markdown.getRenderer(),
            displayEl: this.display,
            cmEditorView: this.editor.cm,
        });
    }

    setupListeners() {
        // Button actions
        this.elem.addEventListener('click', event => {
            const button = event.target.closest('button[data-action]');
            if (button === null) return;

            const action = button.getAttribute('data-action');
            if (action === 'insertImage') this.editor.actions.showImageInsert();
            if (action === 'insertLink') this.editor.actions.showLinkSelector();
            if (action === 'insertDrawing' && (event.ctrlKey || event.metaKey)) {
                this.editor.actions.showImageManager();
                return;
            }
            if (action === 'insertDrawing') this.editor.actions.startDrawing();
            if (action === 'fullscreen') this.editor.actions.fullScreen();
        });

        // Mobile section toggling
        this.elem.addEventListener('click', event => {
            const toolbarLabel = event.target.closest('.editor-toolbar-label');
            if (!toolbarLabel) return;

            const currentActiveSections = this.elem.querySelectorAll('.markdown-editor-wrap');
            for (const activeElem of currentActiveSections) {
                activeElem.classList.remove('active');
            }

            toolbarLabel.closest('.markdown-editor-wrap').classList.add('active');
        });

        this.handleDividerDrag();
    }

    handleDividerDrag() {
        this.divider.addEventListener('pointerdown', () => {
            const wrapRect = this.elem.getBoundingClientRect();
            const moveListener = event => {
                const xRel = event.pageX - wrapRect.left;
                const xPct = Math.min(Math.max(20, Math.floor((xRel / wrapRect.width) * 100)), 80);
                this.displayWrap.style.flexBasis = `${100 - xPct}%`;
                this.editor.settings.set('editorWidth', xPct);
            };
            const upListener = () => {
                window.removeEventListener('pointermove', moveListener);
                window.removeEventListener('pointerup', upListener);
                this.display.style.pointerEvents = null;
                document.body.style.userSelect = null;
            };

            this.display.style.pointerEvents = 'none';
            document.body.style.userSelect = 'none';
            window.addEventListener('pointermove', moveListener);
            window.addEventListener('pointerup', upListener);
        });
        const widthSetting = this.editor.settings.get('editorWidth');
        if (widthSetting) {
            this.displayWrap.style.flexBasis = `${100 - widthSetting}%`;
        }
    }

    scrollToTextIfNeeded() {
        const queryParams = (new URL(window.location)).searchParams;
        const scrollText = queryParams.get('content-text');
        if (scrollText) {
            this.editor.actions.scrollToText(scrollText);
        }
    }

    /**
     * Get the URL for the configured drawio instance.
     * @returns {String}
     */
    getDrawioUrl() {
        const drawioAttrEl = document.querySelector('[drawio-url]');
        if (!drawioAttrEl) {
            return '';
        }

        return drawioAttrEl.getAttribute('drawio-url') || '';
    }

    /**
     * Get the content of this editor.
     * Used by the parent page editor component.
     * @return {Promise<{html: String, markdown: String}>}
     */
    async getContent() {
        return this.editor.actions.getContent();
    }

}