/** * 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; }, getEditor, hasEditor, replace(textarea) { if (!hasEditor(textarea)) { if (isTextareaVisible(textarea)) { replaceTextarea(textarea); } } else if (isEditorHidden(textarea)) { destroyTextarea(textarea); } }, }; export default wysiwyg; 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); let options = {}; 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) { 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(); } }); 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); }