X-Git-Url: http://source.bookstackapp.com/bookstack/blobdiff_plain/9ec114641c35eaedfa5d6b00b82b06dad2b8c050..d6456e961afde217e045140fdf8cf88921897536:/resources/assets/js/components/wysiwyg-editor.js diff --git a/resources/assets/js/components/wysiwyg-editor.js b/resources/assets/js/components/wysiwyg-editor.js index 701a1fec6..c03c0d2aa 100644 --- a/resources/assets/js/components/wysiwyg-editor.js +++ b/resources/assets/js/components/wysiwyg-editor.js @@ -1,25 +1,36 @@ -const Code = require('../services/code'); -const DrawIO = require('../services/drawio'); +import Code from "../services/code"; +import DrawIO from "../services/drawio"; /** * Handle pasting images from clipboard. * @param {ClipboardEvent} event + * @param {WysiwygEditor} wysiwygComponent * @param editor */ -function editorPaste(event, editor) { - if (!event.clipboardData || !event.clipboardData.items) return; - let items = event.clipboardData.items; +function editorPaste(event, editor, wysiwygComponent) { + const clipboardItems = event.clipboardData.items; + if (!event.clipboardData || !clipboardItems) return; - for (let i = 0; i < items.length; i++) { - if (items[i].type.indexOf("image") === -1) continue; - event.preventDefault(); + // Don't handle if clipboard includes text content + for (let clipboardItem of clipboardItems) { + if (clipboardItem.type.includes('text/')) { + return; + } + } + + for (let clipboardItem of clipboardItems) { + if (!clipboardItem.type.includes("image")) { + continue; + } + + const id = "image-" + Math.random().toString(16).slice(2); + const loadingImage = window.baseUrl('/loading.gif'); + const file = clipboardItem.getAsFile(); - let id = "image-" + Math.random().toString(16).slice(2); - let loadingImage = window.baseUrl('/loading.gif'); - let file = items[i].getAsFile(); setTimeout(() => { editor.insertContent(`

`); - uploadImageFile(file).then(resp => { + + uploadImageFile(file, wysiwygComponent).then(resp => { editor.dom.setAttrib(id, 'src', resp.thumbs.display); }).catch(err => { editor.dom.remove(id); @@ -33,9 +44,12 @@ function editorPaste(event, editor) { /** * Upload an image file to the server * @param {File} file + * @param {WysiwygEditor} wysiwygComponent */ -function uploadImageFile(file) { - if (file === null || file.type.indexOf('image') !== 0) return Promise.reject(`Not an image file`); +async function uploadImageFile(file, wysiwygComponent) { + if (file === null || file.type.indexOf('image') !== 0) { + throw new Error(`Not an image file`); + } let ext = 'png'; if (file.name) { @@ -43,11 +57,13 @@ function uploadImageFile(file) { if (fileNameMatches.length > 1) ext = fileNameMatches[1]; } - let remoteFilename = "image-" + Date.now() + "." + ext; - let formData = new FormData(); + const remoteFilename = "image-" + Date.now() + "." + ext; + const formData = new FormData(); formData.append('file', file, remoteFilename); + formData.append('uploaded_to', wysiwygComponent.pageId); - return window.$http.post(window.baseUrl('/images/gallery/upload'), formData).then(resp => (resp.data)); + const resp = await window.$http.post(window.baseUrl('/images/gallery'), formData); + return resp.data; } function registerEditorShortcuts(editor) { @@ -152,23 +168,24 @@ function codePlugin() { }); } - function codeMirrorContainerToPre($codeMirrorContainer) { - let textArea = $codeMirrorContainer[0].querySelector('textarea'); - let code = textArea.textContent; - let lang = $codeMirrorContainer[0].getAttribute('data-lang'); - - $codeMirrorContainer.removeAttr('contentEditable'); - let $pre = $('
');
-        $pre.append($('').each((index, elem) => {
-            // Needs to be textContent since innerText produces BR:s
-            elem.textContent = code;
-        }).attr('class', `language-${lang}`));
-        $codeMirrorContainer.replaceWith($pre);
+    function codeMirrorContainerToPre(codeMirrorContainer) {
+        const textArea = codeMirrorContainer.querySelector('textarea');
+        const code = textArea.textContent;
+        const lang = codeMirrorContainer.getAttribute('data-lang');
+
+        codeMirrorContainer.removeAttribute('contentEditable');
+        const pre = document.createElement('pre');
+        const codeElem = document.createElement('code');
+        codeElem.classList.add(`language-${lang}`);
+        codeElem.textContent = code;
+        pre.appendChild(codeElem);
+
+        codeMirrorContainer.parentElement.replaceChild(pre, codeMirrorContainer);
     }
 
     window.tinymce.PluginManager.add('codeeditor', function(editor, url) {
 
-        let $ = editor.$;
+        const $ = editor.$;
 
         editor.addButton('codeeditor', {
             text: 'Code block',
@@ -182,10 +199,8 @@ function codePlugin() {
 
         // Convert
         editor.on('PreProcess', function (e) {
-            $('div.CodeMirrorContainer', e.node).
-            each((index, elem) => {
-                let $elem = $(elem);
-                codeMirrorContainerToPre($elem);
+            $('div.CodeMirrorContainer', e.node).each((index, elem) => {
+                codeMirrorContainerToPre(elem);
             });
         });
 
@@ -201,10 +216,10 @@ function codePlugin() {
             $('.CodeMirrorContainer').filter((index ,elem) => {
                 return typeof elem.querySelector('.CodeMirror').CodeMirror === 'undefined';
             }).each((index, elem) => {
-                codeMirrorContainerToPre($(elem));
+                codeMirrorContainerToPre(elem);
             });
 
-            let codeSamples = $('body > pre').filter((index, elem) => {
+            const codeSamples = $('body > pre').filter((index, elem) => {
                 return elem.contentEditable !== "false";
             });
 
@@ -221,8 +236,6 @@ function codePlugin() {
 
 function drawIoPlugin() {
 
-    const drawIoUrl = 'https://www.draw.io/?embed=1&ui=atlas&spin=1&proto=json';
-    let iframe = null;
     let pageEditor = null;
     let currentNode = null;
 
@@ -230,45 +243,60 @@ function drawIoPlugin() {
         return node.hasAttribute('drawio-diagram');
     }
 
+    function showDrawingManager(mceEditor, selectedNode = null) {
+        pageEditor = mceEditor;
+        currentNode = selectedNode;
+        // Show image manager
+        window.ImageManager.show(function (image) {
+            if (selectedNode) {
+                let imgElem = selectedNode.querySelector('img');
+                pageEditor.dom.setAttrib(imgElem, 'src', image.url);
+                pageEditor.dom.setAttrib(selectedNode, 'drawio-diagram', image.id);
+            } else {
+                let imgHTML = `
`; + pageEditor.insertContent(imgHTML); + } + }, 'drawio'); + } + function showDrawingEditor(mceEditor, selectedNode = null) { pageEditor = mceEditor; currentNode = selectedNode; DrawIO.show(drawingInit, updateContent); } - function updateContent(pngData) { - let id = "image-" + Math.random().toString(16).slice(2); - let loadingImage = window.baseUrl('/loading.gif'); - let data = { - image: pngData, - uploaded_to: Number(document.getElementById('page-editor').getAttribute('page-id')) - }; + async function updateContent(pngData) { + const id = "image-" + Math.random().toString(16).slice(2); + const loadingImage = window.baseUrl('/loading.gif'); + const pageId = Number(document.getElementById('page-editor').getAttribute('page-id')); // Handle updating an existing image if (currentNode) { DrawIO.close(); let imgElem = currentNode.querySelector('img'); - let drawingId = currentNode.getAttribute('drawio-diagram'); - window.$http.put(window.baseUrl(`/images/drawing/upload/${drawingId}`), data).then(resp => { - pageEditor.dom.setAttrib(imgElem, 'src', `${resp.data.url}?updated=${Date.now()}`); - }).catch(err => { + try { + const img = await DrawIO.upload(pngData, pageId); + pageEditor.dom.setAttrib(imgElem, 'src', img.url); + pageEditor.dom.setAttrib(currentNode, 'drawio-diagram', img.id); + } catch (err) { window.$events.emit('error', trans('errors.image_upload_error')); console.log(err); - }); + } return; } - setTimeout(() => { + setTimeout(async () => { pageEditor.insertContent(`
`); DrawIO.close(); - window.$http.post(window.baseUrl('/images/drawing/upload'), data).then(resp => { - pageEditor.dom.setAttrib(id, 'src', resp.data.url); - pageEditor.dom.get(id).parentNode.setAttribute('drawio-diagram', resp.data.id); - }).catch(err => { + try { + const img = await DrawIO.upload(pngData, pageId); + pageEditor.dom.setAttrib(id, 'src', img.url); + pageEditor.dom.get(id).parentNode.setAttribute('drawio-diagram', img.id); + } catch (err) { pageEditor.dom.remove(id); window.$events.emit('error', trans('errors.image_upload_error')); console.log(err); - }); + } }, 5); } @@ -279,21 +307,30 @@ function drawIoPlugin() { } let drawingId = currentNode.getAttribute('drawio-diagram'); - return window.$http.get(window.baseUrl(`/images/base64/${drawingId}`)).then(resp => { - return `data:image/png;base64,${resp.data.content}`; - }); + return DrawIO.load(drawingId); } window.tinymce.PluginManager.add('drawio', function(editor, url) { editor.addCommand('drawio', () => { - showDrawingEditor(editor); + let selectedNode = editor.selection.getNode(); + showDrawingEditor(editor, isDrawing(selectedNode) ? selectedNode : null); }); editor.addButton('drawio', { + type: 'splitbutton', tooltip: 'Drawing', image: `data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9IiMwMDAwMDAiICB4bWxucz0iaHR0cDovL3d3 dy53My5vcmcvMjAwMC9zdmciPgogICAgPHBhdGggZD0iTTIzIDdWMWgtNnYySDdWMUgxdjZoMnYx MEgxdjZoNnYtMmgxMHYyaDZ2LTZoLTJWN2gyek0zIDNoMnYySDNWM3ptMiAxOEgzdi0yaDJ2Mnpt MTItMkg3di0ySDVWN2gyVjVoMTB2MmgydjEwaC0ydjJ6bTQgMmgtMnYtMmgydjJ6TTE5IDVWM2gy djJoLTJ6bS01LjI3IDloLTMuNDlsLS43MyAySDcuODlsMy40LTloMS40bDMuNDEgOWgtMS42M2wt Ljc0LTJ6bS0zLjA0LTEuMjZoMi42MUwxMiA4LjkxbC0xLjMxIDMuODN6Ii8+CiAgICA8cGF0aCBk PSJNMCAwaDI0djI0SDB6IiBmaWxsPSJub25lIi8+Cjwvc3ZnPg==`, - cmd: 'drawio' + cmd: 'drawio', + menu: [ + { + text: 'Drawing Manager', + onclick() { + let selectedNode = editor.selection.getNode(); + showDrawingManager(editor, isDrawing(selectedNode) ? selectedNode : null); + } + } + ] }); editor.on('dblclick', event => { @@ -303,7 +340,7 @@ function drawIoPlugin() { }); editor.on('SetContent', function () { - let drawings = editor.$('body > div[drawio-diagram]'); + const drawings = editor.$('body > div[drawio-diagram]'); if (!drawings.length) return; editor.undoManager.transact(function () { @@ -341,11 +378,36 @@ function customHrPlugin() { } +function listenForBookStackEditorEvents(editor) { + + // Replace editor content + window.$events.listen('editor::replace', ({html}) => { + editor.setContent(html); + }); + + // Append editor content + window.$events.listen('editor::append', ({html}) => { + const content = editor.getContent() + html; + editor.setContent(content); + }); + + // Prepend editor content + window.$events.listen('editor::prepend', ({html}) => { + const content = html + editor.getContent(); + editor.setContent(content); + }); + +} + class WysiwygEditor { constructor(elem) { this.elem = elem; + const pageEditor = document.getElementById('page-editor'); + this.pageId = pageEditor.getAttribute('page-id'); + this.textDirection = pageEditor.getAttribute('text-direction'); + this.plugins = "image table textcolor paste link autolink fullscreen imagetools code customhr autosave lists codeeditor media"; this.loadPlugins(); @@ -360,9 +422,20 @@ class WysiwygEditor { drawIoPlugin(); this.plugins += ' drawio'; } + if (this.textDirection === 'rtl') { + this.plugins += ' directionality' + } + } + + getToolBar() { + const textDirPlugins = this.textDirection === 'rtl' ? 'ltr rtl' : ''; + return `undo redo | styleselect | bold italic underline strikethrough superscript subscript | forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | table image-insert link hr drawio media | removeformat code ${textDirPlugins} fullscreen` } getTinyMceConfig() { + + const context = this; + return { selector: '#html-editor', content_css: [ @@ -372,8 +445,10 @@ class WysiwygEditor { body_class: 'page-content', browser_spellcheck: true, relative_urls: false, + directionality : this.textDirection, remove_script_host: false, document_base_url: window.baseUrl('/'), + end_container_on_empty_block: true, statusbar: false, menubar: false, paste_data_images: false, @@ -382,8 +457,8 @@ class WysiwygEditor { valid_children: "-div[p|h1|h2|h3|h4|h5|h6|blockquote],+div[pre],+div[img]", plugins: this.plugins, imagetools_toolbar: 'imageoptions', - toolbar: "undo redo | styleselect | bold italic underline strikethrough superscript subscript | forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | table image-insert link hr drawio media | removeformat code fullscreen", - content_style: "body {padding-left: 15px !important; padding-right: 15px !important; margin:0!important; margin-left:auto!important;margin-right:auto!important;}", + toolbar: this.getToolBar(), + content_style: "html, body {background: #FFF;} body {padding-left: 15px !important; padding-right: 15px !important; margin:0!important; margin-left:auto!important;margin-right:auto!important;}", style_formats: [ {title: "Header Large", format: "h2"}, {title: "Header Medium", format: "h3"}, @@ -417,9 +492,10 @@ class WysiwygEditor { if (type === 'file') { window.EntitySelectorPopup.show(function(entity) { - let originalField = win.document.getElementById(field_name); + const originalField = win.document.getElementById(field_name); originalField.value = entity.link; - $(originalField).closest('.mce-form').find('input').eq(2).val(entity.name); + const mceForm = originalField.closest('.mce-form'); + mceForm.querySelectorAll('input')[2].value = entity.name; }); } @@ -443,7 +519,7 @@ class WysiwygEditor { html += `${image.name}`; html += ''; win.tinyMCE.activeEditor.execCommand('mceInsertContent', false, html); - }); + }, 'gallery'); } }, @@ -458,13 +534,50 @@ class WysiwygEditor { }, setup: function (editor) { - editor.on('init ExecCommand change input NodeChange ObjectResized', editorChange); + editor.on('ExecCommand change input NodeChange ObjectResized', editorChange); + + editor.on('init', () => { + editorChange(); + // Scroll to the content if needed. + const queryParams = (new URL(window.location)).searchParams; + const scrollId = queryParams.get('content-id'); + if (scrollId) { + scrollToText(scrollId); + } + + // Override for touch events to allow scroll on mobile + const container = editor.getContainer(); + const toolbarButtons = container.querySelectorAll('.mce-btn'); + for (let button of toolbarButtons) { + button.addEventListener('touchstart', event => { + event.stopPropagation(); + }); + } + window.editor = editor; + }); function editorChange() { let content = editor.getContent(); window.$events.emit('editor-html-change', content); } + function scrollToText(scrollId) { + const element = editor.dom.get(encodeURIComponent(scrollId).replace(/!/g, '%21')); + if (!element) { + return; + } + + // scroll the element into the view and put the cursor at the end. + element.scrollIntoView(); + editor.selection.select(element, true); + editor.selection.collapse(false); + editor.focus(); + } + + listenForBookStackEditorEvents(editor); + + // TODO - Update to standardise across both editors + // Use events within listenForBookStackEditorEvents instead (Different event signature) window.$events.listen('editor-html-update', html => { editor.setContent(html); editor.selection.select(editor.getBody(), true); @@ -495,6 +608,18 @@ class WysiwygEditor { let dom = editor.dom, rng = tinymce.dom.RangeUtils.getCaretRangeFromPoint(event.clientX, event.clientY, editor.getDoc()); + // Template insertion + const templateId = event.dataTransfer.getData('bookstack/template'); + if (templateId) { + event.preventDefault(); + window.$http.get(`/templates/${templateId}`).then(resp => { + editor.selection.setRng(rng); + editor.undoManager.transact(function () { + editor.execCommand('mceInsertContent', false, resp.data.html); + }); + }); + } + // Don't allow anything to be dropped in a captioned image. if (dom.getParent(rng.startContainer, '.mceTemp')) { event.preventDefault(); @@ -522,16 +647,17 @@ class WysiwygEditor { html += `${image.name}`; html += ''; editor.execCommand('mceInsertContent', false, html); - }); + }, 'gallery'); } }); // Paste image-uploads - editor.on('paste', event => editorPaste(event, editor)); + editor.on('paste', event => editorPaste(event, editor, context)); + } }; } } -module.exports = WysiwygEditor; +export default WysiwygEditor;