diff --git a/resources/assets/js/components/markdown-editor.js b/resources/assets/js/components/markdown-editor.js index c98bac3cb..46c54408b 100644 --- a/resources/assets/js/components/markdown-editor.js +++ b/resources/assets/js/components/markdown-editor.js @@ -1,8 +1,8 @@ const MarkdownIt = require("markdown-it"); const mdTasksLists = require('markdown-it-task-lists'); -const code = require('../libs/code'); +const code = require('../services/code'); -const DrawIO = require('../libs/drawio'); +const DrawIO = require('../services/drawio'); class MarkdownEditor { diff --git a/resources/assets/js/components/page-display.js b/resources/assets/js/components/page-display.js index 5d9f9edee..fc360ff69 100644 --- a/resources/assets/js/components/page-display.js +++ b/resources/assets/js/components/page-display.js @@ -1,5 +1,5 @@ import Clipboard from "clipboard"; -import Code from "../libs/code"; +import Code from "../services/code"; class PageDisplay { diff --git a/resources/assets/js/components/wysiwyg-editor.js b/resources/assets/js/components/wysiwyg-editor.js index ae369ff42..539233204 100644 --- a/resources/assets/js/components/wysiwyg-editor.js +++ b/resources/assets/js/components/wysiwyg-editor.js @@ -1,9 +1,533 @@ +const Code = require('../services/code'); +const DrawIO = require('../services/drawio'); + +/** + * Handle pasting images from clipboard. + * @param {ClipboardEvent} event + * @param editor + */ +function editorPaste(event, editor) { + if (!event.clipboardData || !event.clipboardData.items) return; + let items = event.clipboardData.items; + + for (let i = 0; i < items.length; i++) { + if (items[i].type.indexOf("image") === -1) continue; + event.preventDefault(); + + 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 => { + editor.dom.setAttrib(id, 'src', resp.thumbs.display); + }).catch(err => { + editor.dom.remove(id); + window.$events.emit('error', trans('errors.image_upload_error')); + console.log(err); + }); + }, 10); + } +} + +/** + * Upload an image file to the server + * @param {File} file + */ +function uploadImageFile(file) { + if (file === null || file.type.indexOf('image') !== 0) return Promise.reject(`Not an image file`); + + let ext = 'png'; + if (file.name) { + let fileNameMatches = file.name.match(/\.(.+)$/); + if (fileNameMatches.length > 1) ext = fileNameMatches[1]; + } + + let remoteFilename = "image-" + Date.now() + "." + ext; + let formData = new FormData(); + formData.append('file', file, remoteFilename); + + return window.$http.post(window.baseUrl('/images/gallery/upload'), formData).then(resp => (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() { + let selectedNode = editor.selection.getNode(); + let formats = ['info', 'success', 'warning', 'danger']; + + if (!selectedNode || selectedNode.className.indexOf('callout') === -1) { + editor.formatter.apply('calloutinfo'); + return; + } + + for (let i = 0; i < formats.length; i++) { + if (selectedNode.className.indexOf(formats[i]) === -1) continue; + let newFormat = (i === formats.length -1) ? formats[0] : formats[i+1]; + editor.formatter.apply('callout' + newFormat); + return; + } + editor.formatter.apply('p'); + }); + +} + +/** + * 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) { + let selectedNode = editor.selection.getNode(); + + if (!elemIsCodeBlock(selectedNode)) { + let providedCode = editor.selection.getNode().textContent; + window.vues['code-editor'].open(providedCode, '', (code, lang) => { + let wrap = document.createElement('div'); + wrap.innerHTML = `
`; + wrap.querySelector('code').innerText = code; + + editor.formatter.toggle('pre'); + let node = editor.selection.getNode(); + editor.dom.setHTML(node, wrap.querySelector('pre').innerHTML); + editor.fire('SetContent'); + }); + return; + } + + let lang = selectedNode.hasAttribute('data-lang') ? selectedNode.getAttribute('data-lang') : ''; + let currentCode = selectedNode.querySelector('textarea').textContent; + + window.vues['code-editor'].open(currentCode, lang, (code, lang) => { + let editorElem = selectedNode.querySelector('.CodeMirror'); + let cmInstance = editorElem.CodeMirror; + if (cmInstance) { + Code.setContent(cmInstance, code); + Code.setMode(cmInstance, lang); + } + let textArea = selectedNode.querySelector('textarea'); + if (textArea) textArea.textContent = code; + selectedNode.setAttribute('data-lang', lang); + }); + } + + 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);
+    }
+
+    window.tinymce.PluginManager.add('codeeditor', function(editor, url) {
+
+        let $ = 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) => {
+                let $elem = $(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));
+            });
+
+            let 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() {
+
+    const drawIoUrl = 'https://www.draw.io/?embed=1&ui=atlas&spin=1&proto=json';
+    let iframe = null;
+    let pageEditor = null;
+    let currentNode = null;
+
+    function isDrawing(node) {
+        return node.hasAttribute('drawio-diagram');
+    }
+
+    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'))
+        };
+
+        // 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 => {
+                window.$events.emit('error', trans('errors.image_upload_error'));
+                console.log(err);
+            });
+            return;
+        }
+
+        setTimeout(() => {
+            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 => { + pageEditor.dom.remove(id); + window.$events.emit('error', trans('errors.image_upload_error')); + console.log(err); + }); + }, 5); + } + + + function drawingInit() { + if (!currentNode) { + return Promise.resolve(''); + } + + 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}`; + }); + } + + window.tinymce.PluginManager.add('drawio', function(editor, url) { + + editor.addCommand('drawio', () => { + showDrawingEditor(editor); + }); + + editor.addButton('drawio', { + tooltip: 'Drawing', + image: window.baseUrl('/icon/drawing.svg?color=000000'), + cmd: 'drawio' + }); + + editor.on('dblclick', event => { + let selectedNode = editor.selection.getNode(); + if (!isDrawing(selectedNode)) return; + showDrawingEditor(editor, selectedNode); + }); + + editor.on('SetContent', function () { + let 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' + }); + }); +} + + class WysiwygEditor { constructor(elem) { this.elem = elem; - this.options = require("../pages/page-form"); - tinymce.init(this.options); + + this.plugins = "image table textcolor paste link autolink fullscreen imagetools code customhr autosave lists codeeditor"; + this.loadPlugins(); + + this.tinyMceConfig = this.getTinyMceConfig(); + window.tinymce.init(this.tinyMceConfig); + } + + loadPlugins() { + codePlugin(); + customHrPlugin(); + if (document.querySelector('[drawio-enabled]').getAttribute('drawio-enabled') === 'true') { + drawIoPlugin(); + this.plugins += ' drawio'; + } + } + + getTinyMceConfig() { + return { + selector: '#html-editor', + content_css: [ + window.baseUrl('/dist/styles.css'), + ], + branding: false, + body_class: 'page-content', + browser_spellcheck: true, + relative_urls: false, + remove_script_host: false, + document_base_url: window.baseUrl('/'), + 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: "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 | 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;}", + 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, + 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) { + let originalField = win.document.getElementById(field_name); + originalField.value = entity.link; + $(originalField).closest('.mce-form').find('input').eq(2).val(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); + }); + } + + }, + paste_preprocess: function (plugin, args) { + let content = args.content; + if (content.indexOf('`; + html += `${image.name}`; + html += ''; + editor.execCommand('mceInsertContent', false, html); + }); + } + }); + + // Paste image-uploads + editor.on('paste', event => editorPaste(event, editor)); + } + }; } } diff --git a/resources/assets/js/index.js b/resources/assets/js/index.js index d72ee3fdc..bb323a08b 100644 --- a/resources/assets/js/index.js +++ b/resources/assets/js/index.js @@ -1,15 +1,6 @@ +// Global Polyfills import "babel-polyfill" -import "./dom-polyfills" - -import jQuery from "jquery" -window.jQuery = window.$ = jQuery; - -import Translations from "./translations" -import vues from "./vues/vues" -import components from "./components" - -import Vue from "vue" -import axios from "axios" +import "./services/dom-polyfills" // Url retrieval function window.baseUrl = function(path) { @@ -19,114 +10,30 @@ window.baseUrl = function(path) { return basePath + '/' + path; }; -// Global Event System -class EventManager { - constructor() { - this.listeners = {}; - this.stack = []; - } - - emit(eventName, eventData) { - this.stack.push({name: eventName, data: eventData}); - if (typeof this.listeners[eventName] === 'undefined') return this; - let eventsToStart = this.listeners[eventName]; - for (let i = 0; i < eventsToStart.length; i++) { - let event = eventsToStart[i]; - event(eventData); - } - return this; - } - - listen(eventName, callback) { - if (typeof this.listeners[eventName] === 'undefined') this.listeners[eventName] = []; - this.listeners[eventName].push(callback); - return this; - } -} - -window.$events = new EventManager(); - -let axiosInstance = axios.create({ - headers: { - 'X-CSRF-TOKEN': document.querySelector('meta[name=token]').getAttribute('content'), - 'baseURL': window.baseUrl('') - } -}); -axiosInstance.interceptors.request.use(resp => { - return resp; -}, err => { - if (typeof err.response === "undefined" || typeof err.response.data === "undefined") return Promise.reject(err); - if (typeof err.response.data.error !== "undefined") window.$events.emit('error', err.response.data.error); - if (typeof err.response.data.message !== "undefined") window.$events.emit('error', err.response.data.message); -}); -window.$http = axiosInstance; - -Vue.prototype.$http = axiosInstance; -Vue.prototype.$events = window.$events; +// Set events and http services on window +import Events from "./services/events" +import Http from "./services/http" +let httpInstance = Http(); +window.$http = httpInstance; +window.$events = new Events(); // Translation setup // Creates a global function with name 'trans' to be used in the same way as Laravel's translation system +import Translations from "./services/translations" let translator = new Translations(window.translations); window.trans = translator.get.bind(translator); window.trans_choice = translator.getPlural.bind(translator); +// Load in global UI helpers and libraries including jQuery +import "./services/global-ui" -//Global jQuery Config & Extensions - -/** - * Scroll the view to a specific element. - * @param {HTMLElement} element - */ -window.scrollToElement = function(element) { - if (!element) return; - let offset = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop; - let top = element.getBoundingClientRect().top + offset; - $('html, body').animate({ - scrollTop: top - 60 // Adjust to change final scroll position top margin - }, 300); -}; - -/** - * Scroll and highlight an element. - * @param {HTMLElement} element - */ -window.scrollAndHighlight = function(element) { - if (!element) return; - window.scrollToElement(element); - let color = document.getElementById('custom-styles').getAttribute('data-color-light'); - let initColor = window.getComputedStyle(element).getPropertyValue('background-color'); - element.style.backgroundColor = color; - setTimeout(() => { - element.classList.add('selectFade'); - element.style.backgroundColor = initColor; - }, 10); - setTimeout(() => { - element.classList.remove('selectFade'); - element.style.backgroundColor = ''; - }, 3000); -}; - -// Smooth scrolling -jQuery.fn.smoothScrollTo = function () { - if (this.length === 0) return; - window.scrollToElement(this[0]); - return this; -}; - -// Making contains text expression not worry about casing -jQuery.expr[":"].contains = $.expr.createPseudo(function (arg) { - return function (elem) { - return $(elem).text().toUpperCase().indexOf(arg.toUpperCase()) >= 0; - }; -}); - -// Detect IE for css -if(navigator.userAgent.indexOf('MSIE')!==-1 - || navigator.appVersion.indexOf('Trident/') > 0 - || navigator.userAgent.indexOf('Safari') !== -1){ - document.body.classList.add('flexbox-support'); -} +// Set services on Vue +import Vue from "vue" +Vue.prototype.$http = httpInstance; +Vue.prototype.$events = window.$events; // Load vues and components +import vues from "./vues/vues" +import components from "./components" vues(); components(); \ No newline at end of file diff --git a/resources/assets/js/pages/page-form.js b/resources/assets/js/pages/page-form.js deleted file mode 100644 index 6b659a92b..000000000 --- a/resources/assets/js/pages/page-form.js +++ /dev/null @@ -1,514 +0,0 @@ -"use strict"; -const Code = require('../libs/code'); -const DrawIO = require('../libs/drawio'); - -/** - * Handle pasting images from clipboard. - * @param {ClipboardEvent} event - * @param editor - */ -function editorPaste(event, editor) { - if (!event.clipboardData || !event.clipboardData.items) return; - let items = event.clipboardData.items; - - for (let i = 0; i < items.length; i++) { - if (items[i].type.indexOf("image") === -1) continue; - event.preventDefault(); - - 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 => { - editor.dom.setAttrib(id, 'src', resp.thumbs.display); - }).catch(err => { - editor.dom.remove(id); - window.$events.emit('error', trans('errors.image_upload_error')); - console.log(err); - }); - }, 10); - } -} - -/** - * Upload an image file to the server - * @param {File} file - */ -function uploadImageFile(file) { - if (file === null || file.type.indexOf('image') !== 0) return Promise.reject(`Not an image file`); - - let ext = 'png'; - if (file.name) { - let fileNameMatches = file.name.match(/\.(.+)$/); - if (fileNameMatches.length > 1) ext = fileNameMatches[1]; - } - - let remoteFilename = "image-" + Date.now() + "." + ext; - let formData = new FormData(); - formData.append('file', file, remoteFilename); - - return window.$http.post(window.baseUrl('/images/gallery/upload'), formData).then(resp => (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() { - let selectedNode = editor.selection.getNode(); - let formats = ['info', 'success', 'warning', 'danger']; - - if (!selectedNode || selectedNode.className.indexOf('callout') === -1) { - editor.formatter.apply('calloutinfo'); - return; - } - - for (let i = 0; i < formats.length; i++) { - if (selectedNode.className.indexOf(formats[i]) === -1) continue; - let newFormat = (i === formats.length -1) ? formats[0] : formats[i+1]; - editor.formatter.apply('callout' + newFormat); - return; - } - editor.formatter.apply('p'); - }); - -} - -/** - * 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) { - let selectedNode = editor.selection.getNode(); - - if (!elemIsCodeBlock(selectedNode)) { - let providedCode = editor.selection.getNode().textContent; - window.vues['code-editor'].open(providedCode, '', (code, lang) => { - let wrap = document.createElement('div'); - wrap.innerHTML = `
`; - wrap.querySelector('code').innerText = code; - - editor.formatter.toggle('pre'); - let node = editor.selection.getNode(); - editor.dom.setHTML(node, wrap.querySelector('pre').innerHTML); - editor.fire('SetContent'); - }); - return; - } - - let lang = selectedNode.hasAttribute('data-lang') ? selectedNode.getAttribute('data-lang') : ''; - let currentCode = selectedNode.querySelector('textarea').textContent; - - window.vues['code-editor'].open(currentCode, lang, (code, lang) => { - let editorElem = selectedNode.querySelector('.CodeMirror'); - let cmInstance = editorElem.CodeMirror; - if (cmInstance) { - Code.setContent(cmInstance, code); - Code.setMode(cmInstance, lang); - } - let textArea = selectedNode.querySelector('textarea'); - if (textArea) textArea.textContent = code; - selectedNode.setAttribute('data-lang', lang); - }); - } - - 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);
-    }
-
-    window.tinymce.PluginManager.add('codeeditor', function(editor, url) {
-
-        let $ = 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) => {
-                let $elem = $(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));
-            });
-
-            let 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() {
-
-    const drawIoUrl = 'https://www.draw.io/?embed=1&ui=atlas&spin=1&proto=json';
-    let iframe = null;
-    let pageEditor = null;
-    let currentNode = null;
-
-    function isDrawing(node) {
-        return node.hasAttribute('drawio-diagram');
-    }
-
-    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'))
-        };
-
-        // 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 => {
-                window.$events.emit('error', trans('errors.image_upload_error'));
-                console.log(err);
-            });
-            return;
-        }
-
-        setTimeout(() => {
-            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 => { - pageEditor.dom.remove(id); - window.$events.emit('error', trans('errors.image_upload_error')); - console.log(err); - }); - }, 5); - } - - - function drawingInit() { - if (!currentNode) { - return Promise.resolve(''); - } - - 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}`; - }); - } - - window.tinymce.PluginManager.add('drawio', function(editor, url) { - - editor.addCommand('drawio', () => { - showDrawingEditor(editor); - }); - - editor.addButton('drawio', { - tooltip: 'Drawing', - image: window.baseUrl('/icon/drawing.svg?color=000000'), - cmd: 'drawio' - }); - - editor.on('dblclick', event => { - let selectedNode = editor.selection.getNode(); - if (!isDrawing(selectedNode)) return; - showDrawingEditor(editor, selectedNode); - }); - - editor.on('SetContent', function () { - let drawings = editor.$('body > div[drawio-diagram]'); - if (!drawings.length) return; - - editor.undoManager.transact(function () { - drawings.each((index, elem) => { - elem.setAttribute('contenteditable', 'false'); - }); - }); - }); - - }); -} - -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' - }); -}); - -// Load plugins -let plugins = "image table textcolor paste link autolink fullscreen imagetools code customhr autosave lists codeeditor"; -codePlugin(); -if (document.querySelector('[drawio-enabled]').getAttribute('drawio-enabled') === 'true') { - drawIoPlugin(); - plugins += ' drawio'; -} - -module.exports = { - selector: '#html-editor', - content_css: [ - window.baseUrl('/dist/styles.css'), - ], - branding: false, - body_class: 'page-content', - browser_spellcheck: true, - relative_urls: false, - remove_script_host: false, - document_base_url: window.baseUrl('/'), - 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: 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 | 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;}", - 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, - 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) { - let originalField = win.document.getElementById(field_name); - originalField.value = entity.link; - $(originalField).closest('.mce-form').find('input').eq(2).val(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); - }); - } - - }, - paste_preprocess: function (plugin, args) { - let content = args.content; - if (content.indexOf('`; - html += `${image.name}`; - html += ''; - editor.execCommand('mceInsertContent', false, html); - }); - } - }); - - // Paste image-uploads - editor.on('paste', event => { editorPaste(event, editor) }); - } -}; \ No newline at end of file diff --git a/resources/assets/js/libs/code.js b/resources/assets/js/services/code.js similarity index 100% rename from resources/assets/js/libs/code.js rename to resources/assets/js/services/code.js diff --git a/resources/assets/js/dom-polyfills.js b/resources/assets/js/services/dom-polyfills.js similarity index 78% rename from resources/assets/js/dom-polyfills.js rename to resources/assets/js/services/dom-polyfills.js index fcd89b766..d32af9118 100644 --- a/resources/assets/js/dom-polyfills.js +++ b/resources/assets/js/services/dom-polyfills.js @@ -2,10 +2,12 @@ * Polyfills for DOM API's */ +// https://developer.mozilla.org/en-US/docs/Web/API/Element/matches if (!Element.prototype.matches) { Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector; } +// https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Browser_compatibility if (!Element.prototype.closest) { Element.prototype.closest = function (s) { var el = this; diff --git a/resources/assets/js/libs/drawio.js b/resources/assets/js/services/drawio.js similarity index 100% rename from resources/assets/js/libs/drawio.js rename to resources/assets/js/services/drawio.js diff --git a/resources/assets/js/services/events.js b/resources/assets/js/services/events.js new file mode 100644 index 000000000..cd747ede2 --- /dev/null +++ b/resources/assets/js/services/events.js @@ -0,0 +1,28 @@ +/** + * Simple global events manager + */ +class Events { + constructor() { + this.listeners = {}; + this.stack = []; + } + + emit(eventName, eventData) { + this.stack.push({name: eventName, data: eventData}); + if (typeof this.listeners[eventName] === 'undefined') return this; + let eventsToStart = this.listeners[eventName]; + for (let i = 0; i < eventsToStart.length; i++) { + let event = eventsToStart[i]; + event(eventData); + } + return this; + } + + listen(eventName, callback) { + if (typeof this.listeners[eventName] === 'undefined') this.listeners[eventName] = []; + this.listeners[eventName].push(callback); + return this; + } +} + +module.exports = Events; \ No newline at end of file diff --git a/resources/assets/js/services/global-ui.js b/resources/assets/js/services/global-ui.js new file mode 100644 index 000000000..948e8e880 --- /dev/null +++ b/resources/assets/js/services/global-ui.js @@ -0,0 +1,58 @@ +// Global jQuery Config & Extensions + +import jQuery from "jquery" +window.jQuery = window.$ = jQuery; + +/** + * Scroll the view to a specific element. + * @param {HTMLElement} element + */ +window.scrollToElement = function(element) { + if (!element) return; + let offset = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop; + let top = element.getBoundingClientRect().top + offset; + $('html, body').animate({ + scrollTop: top - 60 // Adjust to change final scroll position top margin + }, 300); +}; + +/** + * Scroll and highlight an element. + * @param {HTMLElement} element + */ +window.scrollAndHighlight = function(element) { + if (!element) return; + window.scrollToElement(element); + let color = document.getElementById('custom-styles').getAttribute('data-color-light'); + let initColor = window.getComputedStyle(element).getPropertyValue('background-color'); + element.style.backgroundColor = color; + setTimeout(() => { + element.classList.add('selectFade'); + element.style.backgroundColor = initColor; + }, 10); + setTimeout(() => { + element.classList.remove('selectFade'); + element.style.backgroundColor = ''; + }, 3000); +}; + +// Smooth scrolling +jQuery.fn.smoothScrollTo = function () { + if (this.length === 0) return; + window.scrollToElement(this[0]); + return this; +}; + +// Making contains text expression not worry about casing +jQuery.expr[":"].contains = $.expr.createPseudo(function (arg) { + return function (elem) { + return $(elem).text().toUpperCase().indexOf(arg.toUpperCase()) >= 0; + }; +}); + +// Detect IE for css +if(navigator.userAgent.indexOf('MSIE')!==-1 + || navigator.appVersion.indexOf('Trident/') > 0 + || navigator.userAgent.indexOf('Safari') !== -1){ + document.body.classList.add('flexbox-support'); +} \ No newline at end of file diff --git a/resources/assets/js/services/http.js b/resources/assets/js/services/http.js new file mode 100644 index 000000000..1e50fe2ae --- /dev/null +++ b/resources/assets/js/services/http.js @@ -0,0 +1,21 @@ +import axios from "axios" + +function instance() { + let axiosInstance = axios.create({ + headers: { + 'X-CSRF-TOKEN': document.querySelector('meta[name=token]').getAttribute('content'), + 'baseURL': window.baseUrl('') + } + }); + axiosInstance.interceptors.request.use(resp => { + return resp; + }, err => { + if (typeof err.response === "undefined" || typeof err.response.data === "undefined") return Promise.reject(err); + if (typeof err.response.data.error !== "undefined") window.$events.emit('error', err.response.data.error); + if (typeof err.response.data.message !== "undefined") window.$events.emit('error', err.response.data.message); + }); + return axiosInstance; +} + + +export default instance; \ No newline at end of file diff --git a/resources/assets/js/translations.js b/resources/assets/js/services/translations.js similarity index 100% rename from resources/assets/js/translations.js rename to resources/assets/js/services/translations.js diff --git a/resources/assets/js/vues/code-editor.js b/resources/assets/js/vues/code-editor.js index c7926cf28..6453c6966 100644 --- a/resources/assets/js/vues/code-editor.js +++ b/resources/assets/js/vues/code-editor.js @@ -1,4 +1,4 @@ -const codeLib = require('../libs/code'); +const codeLib = require('../services/code'); const methods = { show() {