From 968e7b8b72dba148166253c00e6b0d09b714888f Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 1 Jul 2017 13:23:46 +0100 Subject: [PATCH] Finished off main functionality of custom tinymce code editor --- resources/assets/js/code.js | 84 +++++++++++-------- resources/assets/js/pages/page-form.js | 69 ++++++++++----- resources/assets/js/vues/code-editor.js | 39 +++++++++ resources/assets/js/vues/vues.js | 5 +- resources/assets/sass/_codemirror.scss | 4 + resources/assets/sass/_components.scss | 4 + resources/assets/sass/_text.scss | 16 ++++ resources/lang/en/components.php | 10 ++- .../views/components/code-editor.blade.php | 29 +++++++ resources/views/pages/edit.blade.php | 1 + 10 files changed, 204 insertions(+), 57 deletions(-) create mode 100644 resources/assets/js/vues/code-editor.js create mode 100644 resources/views/components/code-editor.blade.php diff --git a/resources/assets/js/code.js b/resources/assets/js/code.js index 83cb664a1..ef6bca2e2 100644 --- a/resources/assets/js/code.js +++ b/resources/assets/js/code.js @@ -62,7 +62,7 @@ function highlightElem(elem) { let mode = ''; if (innerCodeElem !== null) { let langName = innerCodeElem.className.replace('language-', ''); - if (typeof modeMap[langName] !== 'undefined') mode = modeMap[langName]; + mode = getMode(langName); } elem.innerHTML = elem.innerHTML.replace(//gi ,'\n'); let content = elem.textContent; @@ -78,16 +78,35 @@ function highlightElem(elem) { }); } +/** + * Search for a codemirror code based off a user suggestion + * @param suggestion + * @returns {string} + */ +function getMode(suggestion) { + suggestion = suggestion.trim().replace(/^\./g, '').toLowerCase(); + return (typeof modeMap[suggestion] !== 'undefined') ? modeMap[suggestion] : ''; +} + module.exports.highlightElem = highlightElem; module.exports.wysiwygView = function(elem) { let doc = elem.ownerDocument; + let codeElem = elem.querySelector('code'); + + let lang = (elem.className || '').replace('language-', ''); + if (lang === '' && codeElem) { + console.log(codeElem.className); + lang = (codeElem.className || '').replace('language-', '') + } + elem.innerHTML = elem.innerHTML.replace(//gi ,'\n'); let content = elem.textContent; let newWrap = doc.createElement('div'); let newTextArea = doc.createElement('textarea'); newWrap.className = 'CodeMirrorContainer'; + newWrap.setAttribute('data-lang', lang); newTextArea.style.display = 'none'; elem.parentNode.replaceChild(newWrap, elem); @@ -99,7 +118,7 @@ module.exports.wysiwygView = function(elem) { newWrap.appendChild(elt); }, { value: content, - mode: '', + mode: getMode(lang), lineNumbers: true, theme: 'base16-light', readOnly: true @@ -107,50 +126,47 @@ module.exports.wysiwygView = function(elem) { setTimeout(() => { cm.refresh(); }, 300); - return newWrap; + return {wrap: newWrap, editor: cm}; }; -// module.exports.wysiwygEditor = function(elem) { -// let doc = elem.ownerDocument; -// let newWrap = doc.createElement('div'); -// newWrap.className = 'CodeMirrorContainer'; -// let newTextArea = doc.createElement('textarea'); -// newTextArea.style.display = 'none'; -// elem.innerHTML = elem.innerHTML.replace(//gi ,'\n'); -// let content = elem.textContent; -// elem.parentNode.replaceChild(newWrap, elem); -// newWrap.appendChild(newTextArea); -// let cm = CodeMirror(function(elt) { -// newWrap.appendChild(elt); -// }, { -// value: content, -// mode: '', -// lineNumbers: true, -// theme: 'base16-light', -// readOnly: true -// }); -// cm.on('change', event => { -// newTextArea.innerText = cm.getValue(); -// }); -// setTimeout(() => { -// cm.refresh(); -// }, 300); -// }; - -module.exports.markdownEditor = function(elem) { +module.exports.popupEditor = function(elem, modeSuggestion) { let content = elem.textContent; - let cm = CodeMirror(function(elt) { + return CodeMirror(function(elt) { elem.parentNode.insertBefore(elt, elem); elem.style.display = 'none'; }, { value: content, - mode: "markdown", + mode: getMode(modeSuggestion), + lineNumbers: true, + theme: 'base16-light', + lineWrapping: true + }); +}; + +module.exports.setMode = function(cmInstance, modeSuggestion) { + cmInstance.setOption('mode', getMode(modeSuggestion)); +}; +module.exports.setContent = function(cmInstance, codeContent) { + cmInstance.setValue(codeContent); + setTimeout(() => { + cmInstance.refresh(); + }, 10); +}; + +module.exports.markdownEditor = function(elem) { + let content = elem.textContent; + + return CodeMirror(function (elt) { + elem.parentNode.insertBefore(elt, elem); + elem.style.display = 'none'; + }, { + value: content, + mode: "markdown", lineNumbers: true, theme: 'base16-light', lineWrapping: true }); - return cm; }; diff --git a/resources/assets/js/pages/page-form.js b/resources/assets/js/pages/page-form.js index 871d2b528..a443213bf 100644 --- a/resources/assets/js/pages/page-form.js +++ b/resources/assets/js/pages/page-form.js @@ -58,11 +58,14 @@ function registerEditorShortcuts(editor) { // Other block shortcuts editor.addShortcut('meta+q', '', ['FormatBlock', false, 'blockquote']); editor.addShortcut('meta+d', '', ['FormatBlock', false, 'p']); - editor.addShortcut('meta+e', '', ['FormatBlock', false, 'pre']); + editor.addShortcut('meta+e', '', ['codeeditor', false, 'pre']); editor.addShortcut('meta+shift+E', '', ['FormatBlock', false, 'code']); } +/** + * Create and enable our custom code plugin + */ function codePlugin() { function elemIsCodeBlock(elem) { @@ -71,14 +74,35 @@ function codePlugin() { 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-language') ? selectedNode.getAttribute('data-language') : ''; + let lang = selectedNode.hasAttribute('data-lang') ? selectedNode.getAttribute('data-lang') : ''; let currentCode = selectedNode.querySelector('textarea').textContent; - console.log('SHOW POPUP'); - // TODO - Show custom editor + + 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); + }); } window.tinymce.PluginManager.add('codeeditor', (editor, url) => { @@ -88,9 +112,11 @@ function codePlugin() { editor.addButton('codeeditor', { text: 'Code block', icon: false, - onclick() { - showPopup(editor); - } + cmd: 'codeeditor' + }); + + editor.addCommand('codeeditor', () => { + showPopup(editor); }); // Convert @@ -98,32 +124,33 @@ function codePlugin() { $('div.CodeMirrorContainer', e.node). each((index, elem) => { let $elem = $(elem); - let code = elem.querySelector('textarea').textContent; + let textArea = elem.querySelector('textarea'); + let code = textArea.textContent; + let lang = elem.getAttribute('data-lang'); // $elem.attr('class', $.trim($elem.attr('class'))); $elem.removeAttr('contentEditable'); - - $elem.empty().append('
').find('pre').first().append($('').each((index, elem) => {
+                let $pre = $('
');
+                $pre.append($('').each((index, elem) => {
                     // Needs to be textContent since innerText produces BR:s
                     elem.textContent = code;
-                }).attr('class', $elem.attr('class')));
-                console.log($elem[0].outerHTML);
+                }).attr('class', `language-${lang}`));
+                $elem.replaceWith($pre);
             });
         });
 
         editor.on('SetContent', function () {
-            let codeSamples = $('pre').filter((index, elem) => {
+            let codeSamples = $('body > pre').filter((index, elem) => {
                 return elem.contentEditable !== "false";
             });
 
             if (codeSamples.length) {
                 editor.undoManager.transact(function () {
                     codeSamples.each((index, elem) => {
-                        console.log(elem.textContent);
-                        let outerWrap = Code.wysiwygView(elem);
-                        outerWrap.addEventListener('dblclick', () => {
-                            showPopup(editor);
-                        })
+                        let editDetails = Code.wysiwygView(elem);
+                        editDetails.wrap.addEventListener('dblclick', () => {
+                            showPopup(editor, editDetails.wrap, editDetails.editor);
+                        });
                     });
                 });
             }
@@ -154,7 +181,7 @@ module.exports = function() {
         valid_children: "-div[p|pre|h1|h2|h3|h4|h5|h6|blockquote]",
         plugins: "image table textcolor paste link autolink fullscreen imagetools code customhr autosave lists codeeditor",
         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 | removeformat code fullscreen codeeditor",
+        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 | 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"},
@@ -163,14 +190,14 @@ module.exports = function() {
             {title: "Header Tiny", format: "h5"},
             {title: "Paragraph", format: "p", exact: true, classes: ''},
             {title: "Blockquote", format: "blockquote"},
-            {title: "Code Block", icon: "code", format: "pre"},
+            {title: "Code Block", icon: "code", cmd: 'codeeditor'},
             {title: "Inline Code", icon: "code", inline: "code"},
             {title: "Callouts", items: [
                 {title: "Success", block: 'p', exact: true, attributes : {'class' : 'callout success'}},
                 {title: "Info", block: 'p', exact: true, attributes : {'class' : 'callout info'}},
                 {title: "Warning", block: 'p', exact: true, attributes : {'class' : 'callout warning'}},
                 {title: "Danger", block: 'p', exact: true, attributes : {'class' : 'callout danger'}}
-            ]}
+            ]},
         ],
         style_formats_merge: false,
         formats: {
diff --git a/resources/assets/js/vues/code-editor.js b/resources/assets/js/vues/code-editor.js
new file mode 100644
index 000000000..87bb28cce
--- /dev/null
+++ b/resources/assets/js/vues/code-editor.js
@@ -0,0 +1,39 @@
+const codeLib = require('../code');
+
+const methods = {
+    show() {
+        if (!this.editor) this.editor = codeLib.popupEditor(this.$refs.editor, this.language);
+        this.$refs.overlay.style.display = 'flex';
+    },
+    hide() {
+        this.$refs.overlay.style.display = 'none';
+    },
+    updateEditorMode(language) {
+        codeLib.setMode(this.editor, language);
+    },
+    open(code, language, callback) {
+        this.show();
+        this.updateEditorMode(language);
+        this.language = language;
+        codeLib.setContent(this.editor, code);
+        this.code = code;
+        this.callback = callback;
+    },
+    save() {
+        if (!this.callback) return;
+        this.callback(this.editor.getValue(), this.language);
+        this.hide();
+    }
+};
+
+const data = {
+    editor: null,
+    language: '',
+    code: '',
+    callback: null
+};
+
+module.exports = {
+    methods,
+    data
+};
\ No newline at end of file
diff --git a/resources/assets/js/vues/vues.js b/resources/assets/js/vues/vues.js
index 8cc1dd656..31d833bfb 100644
--- a/resources/assets/js/vues/vues.js
+++ b/resources/assets/js/vues/vues.js
@@ -7,12 +7,15 @@ function exists(id) {
 let vueMapping = {
     'search-system': require('./search'),
     'entity-dashboard': require('./entity-search'),
+    'code-editor': require('./code-editor')
 };
 
+window.vues = {};
+
 Object.keys(vueMapping).forEach(id => {
     if (exists(id)) {
         let config = vueMapping[id];
         config.el = '#' + id;
-        new Vue(config);
+        window.vues[id] = new Vue(config);
     }
 });
\ No newline at end of file
diff --git a/resources/assets/sass/_codemirror.scss b/resources/assets/sass/_codemirror.scss
index 9f9e38f55..bd85218a5 100644
--- a/resources/assets/sass/_codemirror.scss
+++ b/resources/assets/sass/_codemirror.scss
@@ -248,6 +248,10 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
   -webkit-tap-highlight-color: transparent;
   -webkit-font-variant-ligatures: contextual;
   font-variant-ligatures: contextual;
+  &:after {
+    content: none;
+    display: none;
+  }
 }
 .CodeMirror-wrap pre {
   word-wrap: break-word;
diff --git a/resources/assets/sass/_components.scss b/resources/assets/sass/_components.scss
index 5328057d9..f45db84b7 100644
--- a/resources/assets/sass/_components.scss
+++ b/resources/assets/sass/_components.scss
@@ -466,4 +466,8 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
 
 .image-picker .none {
   display: none;
+}
+
+#code-editor .CodeMirror {
+  height: 400px;
 }
\ No newline at end of file
diff --git a/resources/assets/sass/_text.scss b/resources/assets/sass/_text.scss
index 4eaa492e7..ccef2a70f 100644
--- a/resources/assets/sass/_text.scss
+++ b/resources/assets/sass/_text.scss
@@ -135,6 +135,21 @@ pre {
   font-size: 12px;
   background-color: #f5f5f5;
   border: 1px solid #DDD;
+  padding-left: 31px;
+  position: relative;
+  padding-top: 3px;
+  padding-bottom: 3px;
+  &:after {
+    content: '';
+    display: block;
+    position: absolute;
+    top: 0;
+    width: 29px;
+    left: 0;
+    background-color: #f5f5f5;
+    height: 100%;
+    border-right: 1px solid #DDD;
+  }
 }
 
 
@@ -182,6 +197,7 @@ pre code {
   border: 0;
   font-size: 1em;
   display: block;
+  line-height: 1.6;
 }
 /*
  * Text colors
diff --git a/resources/lang/en/components.php b/resources/lang/en/components.php
index b9108702a..334502d05 100644
--- a/resources/lang/en/components.php
+++ b/resources/lang/en/components.php
@@ -20,5 +20,13 @@ return [
     'image_preview' => 'Image Preview',
     'image_upload_success' => 'Image uploaded successfully',
     'image_update_success' => 'Image details successfully updated',
-    'image_delete_success' => 'Image successfully deleted'
+    'image_delete_success' => 'Image successfully deleted',
+
+    /**
+     * Code editor
+     */
+    'code_editor' => 'Edit Code',
+    'code_language' => 'Code Language',
+    'code_content' => 'Code Content',
+    'code_save' => 'Save Code',
 ];
\ No newline at end of file
diff --git a/resources/views/components/code-editor.blade.php b/resources/views/components/code-editor.blade.php
new file mode 100644
index 000000000..23deaad99
--- /dev/null
+++ b/resources/views/components/code-editor.blade.php
@@ -0,0 +1,29 @@
+
+
+ +
+
\ No newline at end of file diff --git a/resources/views/pages/edit.blade.php b/resources/views/pages/edit.blade.php index 5ab25d1cc..6de47aaf1 100644 --- a/resources/views/pages/edit.blade.php +++ b/resources/views/pages/edit.blade.php @@ -21,6 +21,7 @@ @include('components.image-manager', ['imageType' => 'gallery', 'uploaded_to' => $page->id]) + @include('components.code-editor') @include('components.entity-selector-popup') @stop \ No newline at end of file