Skip to content
Snippets Groups Projects
wysiwyg.js 7.64 KiB
Newer Older
/**
 * wysiwyg.js - Replace HTML textareas with WYSIWYG editor.
 */
import parseOptions from './parse_options.js';
import WikiLink from '../cke/wiki-link/wiki-link.js';

const wysiwyg = {
    // NOTE keep this function in sync with Markup class
    htmlMarker: '<!--HTML-->',
    htmlMarkerRegExp: /^\s*<!--\s*HTML.*?-->/i,

    isHtml: function isHtml(text) {
        // NOTE keep this function in sync with
        // Markup::isHtml in Markup.class.php
        return this.hasHtmlMarker(text);
    },
    hasHtmlMarker: function hasHtmlMarker(text) {
        // NOTE keep this function in sync with
        // Markup::hasHtmlMarker in Markup.class.php
        return this.htmlMarkerRegExp.test(text);
    },
    markAsHtml: function markAsHtml(text) {
        // NOTE keep this function in sync with
        // Markup::markAsHtml in Markup.class.php
        if (this.hasHtmlMarker(text) || text.trim() == '') {
            return text; // marker already set, don't set twice
        }
        return this.htmlMarker + '\n' + text;
    },

    replace(textarea) {
        if (!hasEditor(textarea)) {
            if (isTextareaVisible(textarea)) {
                replaceTextarea(textarea);
            }
        } else if (isEditorHidden(textarea)) {
            destroyTextarea(textarea);
function isTextareaVisible(textarea) {
    return $(textarea).is(':visible');
}
function isEditorHidden(textarea) {
    if (!hasEditor(textarea)) {
        return false;
    const editor = getEditor(textarea);
    return editor && editor.ui && $(editor.ui.element).is(':hidden');
}
function replaceTextarea(textarea) {
    setEditor(textarea, {});
    const $textarea = textarea instanceof jQuery ? textarea : $(textarea);
Ron Lucke's avatar
Ron Lucke committed

    if ($textarea.attr('data-editor')) {
        const parsed = parseOptions($textarea.attr('data-editor'));

        if (parsed.toolbar === 'small') {
            options.toolbar = {
                removeItems: [
                    'undo',
                    'redo',
                    'findAndReplace',
                    'strikethrough',
                    'horizontalLine',
                    'insertBlockQuote',
                    'splitBlockQuote',
                    'removeBlockQuote',
                ]
            };
        } else if (parsed.toolbar === 'minimal') {
            options.toolbar = {
                items: [
                    'bold',
                    'italic',
                    'underline',
                    'subscript',
                    'superscript',
                    '|',
                    'removeFormat',
                    '|',
                    'bulletedList',
                    'numberedList',
                    '|',
                    'fontColor',
                    'fontBackgroundColor',
                    '|',
                    'link',
                    'math',
                    'specialCharacters',
                ]
            };
        }

        if (parsed.removePlugins) {
            options.removePlugins = parsed.removePlugins.split(",")
        }

        if (parsed.extraPlugins) {
            const pluginMap = { WikiLink };
            options.extraPlugins = parsed.extraPlugins.split(",").reduce((memo, plugin) => {
                if (plugin in pluginMap) {
                    memo.push(pluginMap[plugin]);
                }
                return memo;
            }, []);
        }
    }
    return STUDIP.loadChunk('wysiwyg')
        .then(loadMathJax)
        .then(createEditor)
        .then(setEditorInstance)
        .then(enhanceEditor)
        .then(emitLoadEvent);
    function createEditor(ClassicEditor) {
Ron Lucke's avatar
Ron Lucke committed
        return ClassicEditor.create(textarea, options).then(editor => {
            function getViewportOffsetTop() {
                const topBar = document.getElementById('top-bar');
                const responsiveContentbar = document.getElementById('responsive-contentbar');

                let top = topBar.clientHeight + topBar.clientTop;
                if (responsiveContentbar) {
                    top += responsiveContentbar?.clientHeight + responsiveContentbar.clientTop;
                }

                return top;
            }

            function updateOffsetTop() {
                // This needs to be delayed since some events will fire before
                // changing the DOM
                setTimeout(() => {
                    editor.ui.viewportOffset = {top: getViewportOffsetTop()};
                    editor.ui.update();
                }, 50);
            }

            // Set initial offset top
            updateOffsetTop();

            // Listen to relevant events that may require the sticky panel to be misplaced
            STUDIP.eventBus.on('toggle-compact-navigation', updateOffsetTop);
            STUDIP.eventBus.on('switch-focus-mode', updateOffsetTop);

            // Stop listening if editor is destroyed
            editor.on('destroy', () => {
                STUDIP.eventBus.off('toggle-compact-navigation', updateOffsetTop);
                STUDIP.eventBus.off('switch-focus-mode', updateOffsetTop);
            });

            return editor;
        });
    function setEditorInstance(ckeditor) {
        setEditor(textarea, ckeditor);
        return ckeditor;
    }
    function enhanceEditor(ckeditor) {
        // make sure HTML marker is always set, in
        // case contents are cut-off by the backend
        $textarea.closest('form').submit(() => {
            ckeditor.sourceElement.value = wysiwyg.markAsHtml(ckeditor.getData());
        // focus the editor if requested
        if ($textarea.is('[autofocus]')) {
            ckeditor.focus();
        }
        ckeditor.ui.focusTracker.on('change:isFocused', (evt, name, isFocused) => {
            if (!isFocused) {
                ckeditor.updateSourceElement();
Ron Lucke's avatar
Ron Lucke committed
        const button = ckeditor.ui.view.toolbar.items.find( item => item.class === "ck-source-editing-button");
        if (button) {
            button.withText = false;
        }

        // TODO: Kein updateSourceElement im SourceEditing-Modus
        //     $(ckeditor.container.$).on('blur', '.CodeMirror', function (event) {
        //         ckeditor.updateElement(); // also update in source mode
        //     });
    }

    function emitLoadEvent(ckeditor) {
        $textarea.trigger('load.wysiwyg');

        return ckeditor;
    }

    async function loadMathJax(ckeditor) {
        let mathjaxP;

        if (window.MathJax && window.MathJax.Hub) {
            mathjaxP = Promise.resolve(window.MathJax);
        } else if (window.STUDIP && window.STUDIP.loadChunk) {
            mathjaxP = window.STUDIP.loadChunk('mathjax');

        await mathjaxP;

        //console.log('loading MathJaxP...', mathjaxP);
        return ckeditor;
    }
function destroyTextarea(textarea) {
    if (!hasEditor(textarea)) {
        throw new Error('Trying to destroy a non-existing editor instance.');
    }

    const editor = getEditor(textarea);
    editor.destroy(true);
    unsetEditor(textarea);
}

// create an unused id
function createNewId(prefix) {
    var i = 0;
    while ($('#' + prefix + i).length > 0) {
        i++;
    }
    return prefix + i;
}

const instances = new Map();

function getEditor(textarea) {
    return textarea?.id !== '' ? instances.get(textarea.id) : null;
}

function hasEditor(textarea) {
    return textarea.id !== '' && instances.has(textarea.id);
}

function setEditor(textarea, editor) {
    if (textarea.id === '') {
        textarea.id = createNewId('wysiwyg');
    }
    instances.set(textarea.id, editor);
}

function unsetEditor(textarea) {
    instances.delete(textarea.id);
}