X-Git-Url: http://source.bookstackapp.com/bookstack/blobdiff_plain/c6ad16dba657c82512ae495a4a38b99b8cfa9eeb..refs/pull/3656/head:/resources/js/wysiwyg/plugin-codeeditor.js diff --git a/resources/js/wysiwyg/plugin-codeeditor.js b/resources/js/wysiwyg/plugin-codeeditor.js index 0d591217a..b9fc355e1 100644 --- a/resources/js/wysiwyg/plugin-codeeditor.js +++ b/resources/js/wysiwyg/plugin-codeeditor.js @@ -1,56 +1,139 @@ function elemIsCodeBlock(elem) { - return elem.className === 'CodeMirrorContainer'; + return elem.tagName.toLowerCase() === 'code-block'; } -function showPopup(editor) { - const selectedNode = editor.selection.getNode(); +/** + * @param {Editor} editor + * @param {String} code + * @param {String} language + * @param {function(string, string)} callback (Receives (code: string,language: string) + */ +function showPopup(editor, code, language, callback) { + window.components.first('code-editor').open(code, language, (newCode, newLang) => { + callback(newCode, newLang) + editor.focus() + }); +} - if (!elemIsCodeBlock(selectedNode)) { - const providedCode = editor.selection.getContent({format: 'text'}); - window.components.first('code-editor').open(providedCode, '', (code, lang) => { - const wrap = document.createElement('div'); - wrap.innerHTML = `
`;
- wrap.querySelector('code').innerText = code;
+/**
+ * @param {Editor} editor
+ * @param {CodeBlockElement} codeBlock
+ */
+function showPopupForCodeBlock(editor, codeBlock) {
+ showPopup(editor, codeBlock.getContent(), codeBlock.getLanguage(), (newCode, newLang) => {
+ codeBlock.setContent(newCode, newLang);
+ });
+}
- editor.insertContent(wrap.innerHTML);
- editor.focus();
- });
- return;
- }
+/**
+ * Define our custom code-block HTML element that we use.
+ * Needs to be delayed since it needs to be defined within the context of the
+ * child editor window and document, hence its definition within a callback.
+ * @param {Editor} editor
+ */
+function defineCodeBlockCustomElement(editor) {
+ const doc = editor.getDoc();
+ const win = doc.defaultView;
+
+ class CodeBlockElement extends win.HTMLElement {
+ constructor() {
+ super();
+ this.attachShadow({mode: 'open'});
+ const linkElem = document.createElement('link');
+ linkElem.setAttribute('rel', 'stylesheet');
+ linkElem.setAttribute('href', window.baseUrl('/dist/styles.css'));
+
+ const cmContainer = document.createElement('div');
+ cmContainer.style.pointerEvents = 'none';
+ cmContainer.contentEditable = 'false';
+ cmContainer.classList.add('CodeMirrorContainer');
+
+ this.shadowRoot.append(linkElem, cmContainer);
+ }
- const lang = selectedNode.hasAttribute('data-lang') ? selectedNode.getAttribute('data-lang') : '';
- const currentCode = selectedNode.querySelector('textarea').textContent;
+ getLanguage() {
+ const getLanguageFromClassList = (classes) => {
+ const langClasses = classes.split(' ').filter(cssClass => cssClass.startsWith('language-'));
+ return (langClasses[0] || '').replace('language-', '');
+ };
- window.components.first('code-editor').open(currentCode, lang, (code, lang) => {
- const editorElem = selectedNode.querySelector('.CodeMirror');
- const cmInstance = editorElem.CodeMirror;
- if (cmInstance) {
- window.importVersioned('code').then(Code => {
- Code.setContent(cmInstance, code);
- Code.setMode(cmInstance, lang, code);
- });
+ const code = this.querySelector('code');
+ const pre = this.querySelector('pre');
+ return getLanguageFromClassList(pre.className) || (code && getLanguageFromClassList(code.className)) || '';
}
- const textArea = selectedNode.querySelector('textarea');
- if (textArea) textArea.textContent = code;
- selectedNode.setAttribute('data-lang', lang);
- editor.focus()
- });
-}
+ setContent(content, language) {
+ if (this.cm) {
+ importVersioned('code').then(Code => {
+ Code.setContent(this.cm, content);
+ Code.setMode(this.cm, language, content);
+ });
+ }
+
+ let pre = this.querySelector('pre');
+ if (!pre) {
+ pre = doc.createElement('pre');
+ this.append(pre);
+ }
+ pre.innerHTML = '';
+
+ const code = doc.createElement('code');
+ pre.append(code);
+ code.innerText = content;
+ code.className = `language-${language}`;
+ }
-function codeMirrorContainerToPre(codeMirrorContainer) {
- const textArea = codeMirrorContainer.querySelector('textarea');
- const code = textArea.textContent;
- const lang = codeMirrorContainer.getAttribute('data-lang');
+ getContent() {
+ const code = this.querySelector('code') || this.querySelector('pre');
+ const tempEl = document.createElement('pre');
+ tempEl.innerHTML = code.innerHTML.replace(/\ufeff/g, '');
- codeMirrorContainer.removeAttribute('contentEditable');
- const pre = document.createElement('pre');
- const codeElem = document.createElement('code');
- codeElem.classList.add(`language-${lang}`);
- codeElem.textContent = code;
- pre.appendChild(codeElem);
+ const brs = tempEl.querySelectorAll('br');
+ for (const br of brs) {
+ br.replaceWith('\n');
+ }
- codeMirrorContainer.parentElement.replaceChild(pre, codeMirrorContainer);
+ return tempEl.textContent;
+ }
+
+ connectedCallback() {
+ const connectedTime = Date.now();
+ if (this.cm) {
+ return;
+ }
+
+ this.cleanChildContent();
+ const content = this.getContent();
+ const lines = content.split('\n').length;
+ const height = (lines * 19.2) + 18 + 24;
+ this.style.height = `${height}px`;
+
+ const container = this.shadowRoot.querySelector('.CodeMirrorContainer');
+ const renderCodeMirror = (Code) => {
+ this.cm = Code.wysiwygView(container, content, this.getLanguage());
+ setTimeout(() => Code.updateLayout(this.cm), 10);
+ setTimeout(() => this.style.height = null, 12);
+ };
+
+ window.importVersioned('code').then((Code) => {
+ const timeout = (Date.now() - connectedTime < 20) ? 20 : 0;
+ setTimeout(() => renderCodeMirror(Code), timeout);
+ });
+ }
+
+ cleanChildContent() {
+ const pre = this.querySelector('pre');
+ if (!pre) return;
+
+ for (const preChild of pre.childNodes) {
+ if (preChild.nodeName === '#text' && preChild.textContent === '') {
+ preChild.remove();
+ }
+ }
+ }
+ }
+
+ win.customElements.define('code-block', CodeBlockElement);
}
@@ -60,8 +143,6 @@ function codeMirrorContainerToPre(codeMirrorContainer) {
*/
function register(editor, url) {
- const $ = editor.$;
-
editor.ui.registry.addIcon('codeblock', '')
editor.ui.registry.addButton('codeeditor', {
@@ -73,54 +154,62 @@ function register(editor, url) {
});
editor.addCommand('codeeditor', () => {
- showPopup(editor);
- });
-
- // Convert
- editor.on('PreProcess', function (e) {
- $('div.CodeMirrorContainer', e.node).each((index, elem) => {
- codeMirrorContainerToPre(elem);
- });
+ const selectedNode = editor.selection.getNode();
+ const doc = selectedNode.ownerDocument;
+ if (elemIsCodeBlock(selectedNode)) {
+ showPopupForCodeBlock(editor, selectedNode);
+ } else {
+ const textContent = editor.selection.getContent({format: 'text'});
+ showPopup(editor, textContent, '', (newCode, newLang) => {
+ const pre = doc.createElement('pre');
+ const code = doc.createElement('code');
+ code.classList.add(`language-${newLang}`);
+ code.innerText = newCode;
+ pre.append(code);
+
+ editor.insertContent(pre.outerHTML);
+ });
+ }
});
editor.on('dblclick', event => {
let selectedNode = editor.selection.getNode();
- if (!elemIsCodeBlock(selectedNode)) return;
- showPopup(editor);
+ if (elemIsCodeBlock(selectedNode)) {
+ showPopupForCodeBlock(editor, selectedNode);
+ }
});
- function parseCodeMirrorInstances(Code) {
-
- // Recover broken codemirror instances
- $('.CodeMirrorContainer').filter((index ,elem) => {
- return typeof elem.querySelector('.CodeMirror').CodeMirror === 'undefined';
- }).each((index, elem) => {
- codeMirrorContainerToPre(elem);
+ editor.on('PreInit', () => {
+ editor.parser.addNodeFilter('pre', function(elms) {
+ for (const el of elms) {
+ const wrapper = tinymce.html.Node.create('code-block', {
+ contenteditable: 'false',
+ });
+
+ const spans = el.getAll('span');
+ for (const span of spans) {
+ span.unwrap();
+ }
+ el.attr('style', null);
+ el.wrap(wrapper);
+ }
});
- const codeSamples = $('body > pre').filter((index, elem) => {
- return elem.contentEditable !== "false";
+ editor.parser.addNodeFilter('code-block', function(elms) {
+ for (const el of elms) {
+ el.attr('contenteditable', 'false');
+ }
});
- codeSamples.each((index, elem) => {
- Code.wysiwygView(elem);
+ editor.serializer.addNodeFilter('code-block', function(elms) {
+ for (const el of elms) {
+ el.unwrap();
+ }
});
- }
+ });
- editor.on('init', async function() {
- const Code = await window.importVersioned('code');
- // Parse code mirror instances on init, but delay a little so this runs after
- // initial styles are fetched into the editor.
- editor.undoManager.transact(function () {
- parseCodeMirrorInstances(Code);
- });
- // Parsed code mirror blocks when content is set but wait before setting this handler
- // to avoid any init 'SetContent' events.
- setTimeout(() => {
- editor.on('SetContent', () => {
- setTimeout(() => parseCodeMirrorInstances(Code), 100);
- });
- }, 200);
+ editor.on('PreInit', () => {
+ defineCodeBlockCustomElement(editor);
});
}