diff --git a/resources/js/code.mjs b/resources/js/code.mjs
index 3a7706573..8e2ed72c8 100644
--- a/resources/js/code.mjs
+++ b/resources/js/code.mjs
@@ -204,56 +204,22 @@ function getTheme() {
/**
* Create a CodeMirror instance for showing inside the WYSIWYG editor.
* Manages a textarea element to hold code content.
- * @param {HTMLElement} elem
+ * @param {HTMLElement} cmContainer
+ * @param {String} content
+ * @param {String} language
* @returns {{wrap: Element, editor: *}}
*/
-export function wysiwygView(elem) {
- const doc = elem.ownerDocument;
- const codeElem = elem.querySelector('code');
-
- let lang = getLanguageFromCssClasses(elem.className || '');
- if (!lang && codeElem) {
- lang = getLanguageFromCssClasses(codeElem.className || '');
- }
-
- elem.innerHTML = elem.innerHTML.replace(/
/gi ,'\n');
- const content = elem.textContent;
- const newWrap = doc.createElement('div');
- const newTextArea = doc.createElement('textarea');
-
- newWrap.className = 'CodeMirrorContainer';
- newWrap.setAttribute('data-lang', lang);
- newWrap.setAttribute('dir', 'ltr');
- newTextArea.style.display = 'none';
- elem.parentNode.replaceChild(newWrap, elem);
-
- newWrap.appendChild(newTextArea);
- newWrap.contentEditable = 'false';
- newTextArea.textContent = content;
-
- let cm = CodeMirror(function(elt) {
- newWrap.appendChild(elt);
- }, {
+export function wysiwygView(cmContainer, content, language) {
+ return CodeMirror(cmContainer, {
value: content,
- mode: getMode(lang, content),
+ mode: getMode(language, content),
lineNumbers: true,
lineWrapping: false,
theme: getTheme(),
readOnly: true
});
-
- return {wrap: newWrap, editor: cm};
}
-/**
- * Get the code language from the given css classes.
- * @param {String} classes
- * @return {String}
- */
-function getLanguageFromCssClasses(classes) {
- const langClasses = classes.split(' ').filter(cssClass => cssClass.startsWith('language-'));
- return (langClasses[0] || '').replace('language-', '');
-}
/**
* Create a CodeMirror instance to show in the WYSIWYG pop-up editor
diff --git a/resources/js/wysiwyg/config.js b/resources/js/wysiwyg/config.js
index 7fa3b0f26..1b3b6e7b5 100644
--- a/resources/js/wysiwyg/config.js
+++ b/resources/js/wysiwyg/config.js
@@ -210,16 +210,6 @@ body {
}`.trim().replace('\n', '');
}
-// Custom "Document Root" element, a custom element to identify/define
-// block that may act as another "editable body".
-// Using a custom node means we can identify and add/remove these as desired
-// without affecting user content.
-class DocRootElement extends HTMLDivElement {
- constructor() {
- super();
- }
-}
-
/**
* @param {WysiwygConfigOptions} options
* @return {Object}
@@ -230,8 +220,6 @@ export function build(options) {
window.tinymce.addI18n(options.language, options.translationMap);
// Build toolbar content
const {toolbar, groupButtons: toolBarGroupButtons} = buildToolbar(options);
- // Define our custom root node
- customElements.define('doc-root', DocRootElement, {extends: 'div'});
// Return config object
return {
@@ -254,10 +242,17 @@ export function build(options) {
statusbar: false,
menubar: false,
paste_data_images: false,
- extended_valid_elements: 'pre[*],svg[*],div[drawio-diagram],details[*],summary[*],doc-root',
+ extended_valid_elements: 'pre[*],svg[*],div[drawio-diagram],details[*],summary[*],div[*]',
automatic_uploads: false,
- custom_elements: 'doc-root',
- valid_children: "-div[p|h1|h2|h3|h4|h5|h6|blockquote|div],+div[pre],+div[img],+doc-root[p|h1|h2|h3|h4|h5|h6|blockquote|pre|img|ul|ol],-doc-root[doc-root|#text]",
+ custom_elements: 'doc-root,code-block',
+ valid_children: [
+ "-div[p|h1|h2|h3|h4|h5|h6|blockquote|code-block]",
+ "+div[pre|img]",
+ "-doc-root[doc-root|#text]",
+ "-li[details]",
+ "+code-block[pre]",
+ "+doc-root[code-block]"
+ ].join(','),
plugins: gatherPlugins(options),
imagetools_toolbar: 'imageoptions',
contextmenu: false,
diff --git a/resources/js/wysiwyg/plugin-codeeditor.js b/resources/js/wysiwyg/plugin-codeeditor.js
index 0d591217a..12b2c25fb 100644
--- a/resources/js/wysiwyg/plugin-codeeditor.js
+++ b/resources/js/wysiwyg/plugin-codeeditor.js
@@ -1,56 +1,108 @@
function elemIsCodeBlock(elem) {
- return elem.className === 'CodeMirrorContainer';
+ return elem.tagName.toLowerCase() === 'code-block';
}
-function showPopup(editor) {
- const selectedNode = editor.selection.getNode();
-
- 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;
-
- editor.insertContent(wrap.innerHTML);
- editor.focus();
- });
- return;
- }
-
- const lang = selectedNode.hasAttribute('data-lang') ? selectedNode.getAttribute('data-lang') : '';
- const currentCode = selectedNode.querySelector('textarea').textContent;
-
- 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 textArea = selectedNode.querySelector('textarea');
- if (textArea) textArea.textContent = code;
- selectedNode.setAttribute('data-lang', lang);
-
+/**
+ * @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()
});
}
-function codeMirrorContainerToPre(codeMirrorContainer) {
- const textArea = codeMirrorContainer.querySelector('textarea');
- const code = textArea.textContent;
- const lang = codeMirrorContainer.getAttribute('data-lang');
+/**
+ * @param {Editor} editor
+ * @param {CodeBlockElement} codeBlock
+ */
+function showPopupForCodeBlock(editor, codeBlock) {
+ showPopup(editor, codeBlock.getContent(), codeBlock.getLanguage(), (newCode, newLang) => {
+ codeBlock.setContent(newCode, newLang);
+ });
+}
- codeMirrorContainer.removeAttribute('contentEditable');
- const pre = document.createElement('pre');
- const codeElem = document.createElement('code');
- codeElem.classList.add(`language-${lang}`);
- codeElem.textContent = code;
- pre.appendChild(codeElem);
+/**
+ * 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;
- codeMirrorContainer.parentElement.replaceChild(pre, codeMirrorContainer);
+ 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);
+ }
+
+ getLanguage() {
+ const getLanguageFromClassList = (classes) => {
+ const langClasses = classes.split(' ').filter(cssClass => cssClass.startsWith('language-'));
+ return (langClasses[0] || '').replace('language-', '');
+ };
+
+ const code = this.querySelector('code');
+ const pre = this.querySelector('pre');
+ return getLanguageFromClassList(pre.className) || (code && getLanguageFromClassList(code.className)) || '';
+ }
+
+ 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}`;
+ }
+
+ getContent() {
+ const code = this.querySelector('code') || this.querySelector('pre');
+ const tempEl = document.createElement('pre');
+ tempEl.innerHTML = code.innerHTML.replace().replace(/