-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(`<p><img src="${loadingImage}" id="${id}"></p>`);
- uploadImageFile(file).then(resp => {
+
+ uploadImageFile(file, wysiwygComponent).then(resp => {
editor.dom.setAttrib(id, 'src', resp.thumbs.display);
}).catch(err => {
editor.dom.remove(id);
/**
* 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) {
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) {
});
}
- 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></pre>');
- $pre.append($('<code></code>').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',
// 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);
});
});
$('.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";
});
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;
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 = `<div drawio-diagram="${image.id}" contenteditable="false"><img src="${image.url}"></div>`;
+ 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(`<div drawio-diagram contenteditable="false"><img src="${loadingImage}" id="${id}"></div>`);
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);
}
}
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 => {
});
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 () {
}
+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();
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: [
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,
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"},
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;
});
}
html += `<img src="${image.thumbs.display}" alt="${image.name}">`;
html += '</a>';
win.tinyMCE.activeEditor.execCommand('mceInsertContent', false, html);
- });
+ }, 'gallery');
}
},
},
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);
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();
html += `<img src="${image.thumbs.display}" alt="${image.name}">`;
html += '</a>';
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;