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(/<br\s*[\/]?>/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(/<br\s*[\/]?>/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(/<br\s*[\/]?>/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 = `<pre><code class="language-${lang}"></code></pre>`; + 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('<pre></pre>').find('pre').first().append($('<code></code>').each((index, elem) => { + 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', $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 @@ +<div id="code-editor"> + <div class="overlay" ref="overlay" v-cloak @click="hide()"> + <div class="popup-body" @click.stop> + + <div class="popup-header primary-background"> + <div class="popup-title">{{ trans('components.code_editor') }}</div> + <button class="popup-close neg corner-button button" @click="hide()">x</button> + </div> + + <div class="padded"> + <div class="form-group"> + <label for="code-editor-language">{{ trans('components.code_language') }}</label> + <input @keypress.enter="save()" id="code-editor-language" type="text" @input="updateEditorMode(language)" v-model="language"> + </div> + + <div class="form-group"> + <label for="code-editor-content">{{ trans('components.code_content') }}</label> + <textarea ref="editor" v-model="code"></textarea> + </div> + + <div class="form-group"> + <button type="button" class="button pos" @click="save()">{{ trans('components.code_save') }}</button> + </div> + + </div> + + </div> + </div> +</div> \ 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 @@ </div> @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