X-Git-Url: http://source.bookstackapp.com/bookstack/blobdiff_plain/a6633642232efd164d4708967ab59e498fbff896..refs/pull/4794/head:/resources/js/components/wysiwyg-editor.js diff --git a/resources/js/components/wysiwyg-editor.js b/resources/js/components/wysiwyg-editor.js index 41b2273e2..82f60827d 100644 --- a/resources/js/components/wysiwyg-editor.js +++ b/resources/js/components/wysiwyg-editor.js @@ -1,725 +1,48 @@ -import Code from "../services/code"; -import DrawIO from "../services/drawio"; -import Clipboard from "../services/clipboard"; +import {buildForEditor as buildEditorConfig} from '../wysiwyg/config'; +import {Component} from './component'; -/** - * Handle pasting images from clipboard. - * @param {ClipboardEvent} event - * @param {WysiwygEditor} wysiwygComponent - * @param editor - */ -function editorPaste(event, editor, wysiwygComponent) { - const clipboard = new Clipboard(event.clipboardData || event.dataTransfer); - - // Don't handle the event ourselves if no items exist of contains table-looking data - if (!clipboard.hasItems() || clipboard.containsTabularData()) { - return; - } - - const images = clipboard.getImages(); - for (const imageFile of images) { - - const id = "image-" + Math.random().toString(16).slice(2); - const loadingImage = window.baseUrl('/loading.gif'); - event.preventDefault(); - - setTimeout(() => { - editor.insertContent(`

`); - - uploadImageFile(imageFile, wysiwygComponent).then(resp => { - const safeName = resp.name.replace(/"/g, ''); - const newImageHtml = `${safeName}`; - - const newEl = editor.dom.create('a', { - target: '_blank', - href: resp.url, - }, newImageHtml); - - editor.dom.replace(newEl, id); - }).catch(err => { - editor.dom.remove(id); - window.$events.emit('error', wysiwygComponent.imageUploadErrorText); - console.log(err); - }); - }, 10); - } -} - -/** - * Upload an image file to the server - * @param {File} file - * @param {WysiwygEditor} wysiwygComponent - */ -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) { - let fileNameMatches = file.name.match(/\.(.+)$/); - if (fileNameMatches.length > 1) ext = fileNameMatches[1]; - } - - const remoteFilename = "image-" + Date.now() + "." + ext; - const formData = new FormData(); - formData.append('file', file, remoteFilename); - formData.append('uploaded_to', wysiwygComponent.pageId); - - const resp = await window.$http.post(window.baseUrl('/images/gallery'), formData); - return resp.data; -} - -function registerEditorShortcuts(editor) { - // Headers - for (let i = 1; i < 5; i++) { - editor.shortcuts.add('meta+' + i, '', ['FormatBlock', false, 'h' + (i+1)]); - } - - // Other block shortcuts - editor.shortcuts.add('meta+5', '', ['FormatBlock', false, 'p']); - editor.shortcuts.add('meta+d', '', ['FormatBlock', false, 'p']); - editor.shortcuts.add('meta+6', '', ['FormatBlock', false, 'blockquote']); - editor.shortcuts.add('meta+q', '', ['FormatBlock', false, 'blockquote']); - editor.shortcuts.add('meta+7', '', ['codeeditor', false, 'pre']); - editor.shortcuts.add('meta+e', '', ['codeeditor', false, 'pre']); - editor.shortcuts.add('meta+8', '', ['FormatBlock', false, 'code']); - editor.shortcuts.add('meta+shift+E', '', ['FormatBlock', false, 'code']); - - // Save draft shortcut - editor.shortcuts.add('meta+S', '', () => { - window.$events.emit('editor-save-draft'); - }); - - // Save page shortcut - editor.shortcuts.add('meta+13', '', () => { - window.$events.emit('editor-save-page'); - }); - - // Loop through callout styles - editor.shortcuts.add('meta+9', '', function() { - const selectedNode = editor.selection.getNode(); - const callout = selectedNode ? selectedNode.closest('.callout') : null; - - const formats = ['info', 'success', 'warning', 'danger']; - const currentFormatIndex = formats.findIndex(format => callout && callout.classList.contains(format)); - const newFormatIndex = (currentFormatIndex + 1) % formats.length; - const newFormat = formats[newFormatIndex]; - - editor.formatter.apply('callout' + newFormat); - }); - -} - -/** - * Load custom HTML head content from the settings into the editor. - * @param editor - */ -function loadCustomHeadContent(editor) { - window.$http.get(window.baseUrl('/custom-head-content')).then(resp => { - if (!resp.data) return; - let head = editor.getDoc().querySelector('head'); - head.innerHTML += resp.data; - }); -} - -/** - * Create and enable our custom code plugin - */ -function codePlugin() { - - function elemIsCodeBlock(elem) { - return elem.className === 'CodeMirrorContainer'; - } - - function showPopup(editor) { - const selectedNode = editor.selection.getNode(); - - if (!elemIsCodeBlock(selectedNode)) { - const providedCode = editor.selection.getNode().textContent; - window.components.first('code-editor').open(providedCode, '', (code, lang) => { - const wrap = document.createElement('div'); - wrap.innerHTML = `
`; - wrap.querySelector('code').innerText = code; - - editor.formatter.toggle('pre'); - const node = editor.selection.getNode(); - editor.dom.setHTML(node, wrap.querySelector('pre').innerHTML); - editor.fire('SetContent'); - - editor.focus() - }); - return; - } - - let lang = selectedNode.hasAttribute('data-lang') ? selectedNode.getAttribute('data-lang') : ''; - let currentCode = selectedNode.querySelector('textarea').textContent; - - window.components.first('code-editor').open(currentCode, lang, (code, lang) => { - const editorElem = selectedNode.querySelector('.CodeMirror'); - const cmInstance = editorElem.CodeMirror; - if (cmInstance) { - Code.setContent(cmInstance, code); - Code.setMode(cmInstance, lang, code); - } - const textArea = selectedNode.querySelector('textarea'); - if (textArea) textArea.textContent = code; - selectedNode.setAttribute('data-lang', lang); - - editor.focus() - }); - } - - 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) { - - const $ = editor.$; - - editor.addButton('codeeditor', { - text: 'Code block', - icon: false, - cmd: 'codeeditor' - }); - - editor.addCommand('codeeditor', () => { - showPopup(editor); - }); - - // Convert - editor.on('PreProcess', function (e) { - $('div.CodeMirrorContainer', e.node).each((index, elem) => { - codeMirrorContainerToPre(elem); - }); - }); - - editor.on('dblclick', event => { - let selectedNode = editor.selection.getNode(); - if (!elemIsCodeBlock(selectedNode)) return; - showPopup(editor); - }); - - editor.on('SetContent', function () { - - // Recover broken codemirror instances - $('.CodeMirrorContainer').filter((index ,elem) => { - return typeof elem.querySelector('.CodeMirror').CodeMirror === 'undefined'; - }).each((index, elem) => { - codeMirrorContainerToPre(elem); - }); - - const codeSamples = $('body > pre').filter((index, elem) => { - return elem.contentEditable !== "false"; - }); - - if (!codeSamples.length) return; - editor.undoManager.transact(function () { - codeSamples.each((index, elem) => { - Code.wysiwygView(elem); - }); - }); - }); - - }); -} - -function drawIoPlugin(drawioUrl, isDarkMode, pageId, wysiwygComponent) { - - let pageEditor = null; - let currentNode = null; - - function isDrawing(node) { - 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(drawioUrl, drawingInit, updateContent); - } - - async function updateContent(pngData) { - const id = "image-" + Math.random().toString(16).slice(2); - const loadingImage = window.baseUrl('/loading.gif'); - - // Handle updating an existing image - if (currentNode) { - DrawIO.close(); - let imgElem = currentNode.querySelector('img'); - 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', wysiwygComponent.imageUploadErrorText); - console.log(err); - } - return; - } - - setTimeout(async () => { - pageEditor.insertContent(`
`); - DrawIO.close(); - 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', wysiwygComponent.imageUploadErrorText); - console.log(err); - } - }, 5); - } - - - function drawingInit() { - if (!currentNode) { - return Promise.resolve(''); - } - - let drawingId = currentNode.getAttribute('drawio-diagram'); - return DrawIO.load(drawingId); - } - - window.tinymce.PluginManager.add('drawio', function(editor, url) { - - editor.addCommand('drawio', () => { - const selectedNode = editor.selection.getNode(); - showDrawingEditor(editor, isDrawing(selectedNode) ? selectedNode : null); - }); - - editor.addButton('drawio', { - type: 'splitbutton', - tooltip: 'Drawing', - image: `data:image/svg+xml;base64,${btoa(` - - -`)}`, - cmd: 'drawio', - menu: [ - { - text: 'Drawing Manager', - onclick() { - let selectedNode = editor.selection.getNode(); - showDrawingManager(editor, isDrawing(selectedNode) ? selectedNode : null); - } - } - ] - }); - - editor.on('dblclick', event => { - let selectedNode = editor.selection.getNode(); - if (!isDrawing(selectedNode)) return; - showDrawingEditor(editor, selectedNode); - }); - - editor.on('SetContent', function () { - const drawings = editor.$('body > div[drawio-diagram]'); - if (!drawings.length) return; - - editor.undoManager.transact(function () { - drawings.each((index, elem) => { - elem.setAttribute('contenteditable', 'false'); - }); - }); - }); - - }); -} - -function customHrPlugin() { - window.tinymce.PluginManager.add('customhr', function (editor) { - editor.addCommand('InsertHorizontalRule', function () { - let hrElem = document.createElement('hr'); - let cNode = editor.selection.getNode(); - let parentNode = cNode.parentNode; - parentNode.insertBefore(hrElem, cNode); - }); - - editor.addButton('hr', { - icon: 'hr', - tooltip: 'Horizontal line', - cmd: 'InsertHorizontalRule' - }); - - editor.addMenuItem('hr', { - icon: 'hr', - text: 'Horizontal line', - cmd: 'InsertHorizontalRule', - context: 'insert' - }); - }); -} - - -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); - }); - - // Insert editor content at the current location - window.$events.listen('editor::insert', ({html}) => { - editor.insertContent(html); - }); - - // Focus on the editor - window.$events.listen('editor::focus', () => { - editor.focus(); - }); -} - -class WysiwygEditor { +export class WysiwygEditor extends Component { setup() { this.elem = this.$el; - this.pageId = this.$opts.pageId; - this.textDirection = this.$opts.textDirection; - this.imageUploadErrorText = this.$opts.imageUploadErrorText; - this.isDarkMode = document.documentElement.classList.contains('dark-mode'); - - this.plugins = "image imagetools table textcolor paste link autolink fullscreen code customhr autosave lists codeeditor media"; - this.loadPlugins(); + this.tinyMceConfig = buildEditorConfig({ + language: this.$opts.language, + containerElement: this.elem, + darkMode: document.documentElement.classList.contains('dark-mode'), + textDirection: this.$opts.textDirection, + drawioUrl: this.getDrawIoUrl(), + pageId: Number(this.$opts.pageId), + translations: { + imageUploadErrorText: this.$opts.imageUploadErrorText, + serverUploadLimitText: this.$opts.serverUploadLimitText, + }, + translationMap: window.editor_translations, + }); - this.tinyMceConfig = this.getTinyMceConfig(); window.$events.emitPublic(this.elem, 'editor-tinymce::pre-init', {config: this.tinyMceConfig}); - window.tinymce.init(this.tinyMceConfig); + window.tinymce.init(this.tinyMceConfig).then(editors => { + this.editor = editors[0]; + }); } - loadPlugins() { - codePlugin(); - customHrPlugin(); - + getDrawIoUrl() { const drawioUrlElem = document.querySelector('[drawio-url]'); if (drawioUrlElem) { - const url = drawioUrlElem.getAttribute('drawio-url'); - drawIoPlugin(url, this.isDarkMode, this.pageId, this); - this.plugins += ' drawio'; - } - - if (this.textDirection === 'rtl') { - this.plugins += ' directionality' + return drawioUrlElem.getAttribute('drawio-url'); } + return ''; } - 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; - + /** + * Get the content of this editor. + * Used by the parent page editor component. + * @return {{html: String}} + */ + getContent() { return { - selector: '#html-editor', - content_css: [ - window.baseUrl('/dist/styles.css'), - ], - branding: false, - skin: this.isDarkMode ? 'dark' : 'lightgray', - 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, - extended_valid_elements: 'pre[*],svg[*],div[drawio-diagram]', - automatic_uploads: false, - valid_children: "-div[p|h1|h2|h3|h4|h5|h6|blockquote],+div[pre],+div[img]", - plugins: this.plugins, - imagetools_toolbar: 'imageoptions', - toolbar: this.getToolBar(), - content_style: `html, body, html.dark-mode {background: ${this.isDarkMode ? '#222' : '#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"}, - {title: "Header Small", format: "h4"}, - {title: "Header Tiny", format: "h5"}, - {title: "Paragraph", format: "p", exact: true, classes: ''}, - {title: "Blockquote", format: "blockquote"}, - {title: "Code Block", icon: "code", cmd: 'codeeditor', format: 'codeeditor'}, - {title: "Inline Code", icon: "code", inline: "code"}, - {title: "Callouts", items: [ - {title: "Info", format: 'calloutinfo'}, - {title: "Success", format: 'calloutsuccess'}, - {title: "Warning", format: 'calloutwarning'}, - {title: "Danger", format: 'calloutdanger'} - ]}, - ], - style_formats_merge: false, - media_alt_source: false, - media_poster: false, - formats: { - codeeditor: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div'}, - alignleft: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-left'}, - aligncenter: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-center'}, - alignright: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-right'}, - calloutsuccess: {block: 'p', exact: true, attributes: {class: 'callout success'}}, - calloutinfo: {block: 'p', exact: true, attributes: {class: 'callout info'}}, - calloutwarning: {block: 'p', exact: true, attributes: {class: 'callout warning'}}, - calloutdanger: {block: 'p', exact: true, attributes: {class: 'callout danger'}} - }, - file_browser_callback: function (field_name, url, type, win) { - - if (type === 'file') { - window.EntitySelectorPopup.show(function(entity) { - const originalField = win.document.getElementById(field_name); - originalField.value = entity.link; - const mceForm = originalField.closest('.mce-form'); - const inputs = mceForm.querySelectorAll('input'); - - // Set text to display if not empty - if (!inputs[1].value) { - inputs[1].value = entity.name; - } - - // Set title field - inputs[2].value = entity.name; - }); - } - - if (type === 'image') { - // Show image manager - window.ImageManager.show(function (image) { - - // Set popover link input to image url then fire change event - // to ensure the new value sticks - win.document.getElementById(field_name).value = image.url; - if ("createEvent" in document) { - let evt = document.createEvent("HTMLEvents"); - evt.initEvent("change", false, true); - win.document.getElementById(field_name).dispatchEvent(evt); - } else { - win.document.getElementById(field_name).fireEvent("onchange"); - } - - // Replace the actively selected content with the linked image - let html = ``; - html += `${image.name}`; - html += ''; - win.tinyMCE.activeEditor.execCommand('mceInsertContent', false, html); - }, 'gallery'); - } - - }, - paste_preprocess: function (plugin, args) { - let content = args.content; - if (content.indexOf('`; - html += `${image.name}`; - html += ''; - editor.execCommand('mceInsertContent', false, html); - }, 'gallery'); - } - }); - - // Paste image-uploads - editor.on('paste', event => editorPaste(event, editor, context)); - - // Custom handler hook - window.$events.emitPublic(context.elem, 'editor-tinymce::setup', {editor}); - } + html: this.editor.getContent(), }; } } - -export default WysiwygEditor;