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(/<br\s*[\/]?>/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 = `<pre><code class="language-${lang}"></code></pre>`;
-            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(/<br\s*[\/]?>/gi ,'\n').replace(/\ufeff/g, '');
+            return tempEl.textContent;
+        }
+
+        connectedCallback() {
+            if (this.cm) {
+                return;
+            }
+
+            const container = this.shadowRoot.querySelector('.CodeMirrorContainer');
+            importVersioned('code').then(Code => {
+                this.cm = Code.wysiwygView(container, this.getContent(), this.getLanguage());
+            });
+        }
+    }
+
+    win.customElements.define('code-block', CodeBlockElement);
 }
 
 
@@ -60,8 +112,6 @@ function codeMirrorContainerToPre(codeMirrorContainer) {
  */
 function register(editor, url) {
 
-    const $ = editor.$;
-
     editor.ui.registry.addIcon('codeblock', '<svg width="24" height="24"><path d="M4 3h16c.6 0 1 .4 1 1v16c0 .6-.4 1-1 1H4a1 1 0 0 1-1-1V4c0-.6.4-1 1-1Zm1 2v14h14V5Z"/><path d="M11.103 15.423c.277.277.277.738 0 .922a.692.692 0 0 1-1.106 0l-4.057-3.78a.738.738 0 0 1 0-1.107l4.057-3.872c.276-.277.83-.277 1.106 0a.724.724 0 0 1 0 1.014L7.6 12.012ZM12.897 8.577c-.245-.312-.2-.675.08-.955.28-.281.727-.27 1.027.033l4.057 3.78a.738.738 0 0 1 0 1.107l-4.057 3.872c-.277.277-.83.277-1.107 0a.724.724 0 0 1 0-1.014l3.504-3.412z"/></svg>')
 
     editor.ui.registry.addButton('codeeditor', {
@@ -73,54 +123,64 @@ function register(editor, url) {
     });
 
     editor.addCommand('codeeditor', () => {
-        showPopup(editor);
-    });
+        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 wrap = doc.createElement('code-block');
+                const pre = doc.createElement('pre');
+                const code = doc.createElement('code');
+                code.classList.add(`language-${newLang}`);
+                code.innerText = newCode;
+                pre.append(code);
+                wrap.append(pre);
 
-    // Convert
-    editor.on('PreProcess', function (e) {
-        $('div.CodeMirrorContainer', e.node).each((index, elem) => {
-            codeMirrorContainerToPre(elem);
-        });
+                editor.insertContent(wrap.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) {
+    editor.on('PreInit', () => {
+        editor.parser.addNodeFilter('pre', function(elms) {
+            for (const el of elms) {
+                const wrapper = new tinymce.html.Node.create('code-block', {
+                    contenteditable: 'false',
+                });
 
-        // Recover broken codemirror instances
-        $('.CodeMirrorContainer').filter((index ,elem) => {
-            return typeof elem.querySelector('.CodeMirror').CodeMirror === 'undefined';
-        }).each((index, elem) => {
-            codeMirrorContainerToPre(elem);
+                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('content-editable', '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);
     });
 }
 
diff --git a/resources/js/wysiwyg/plugins-details.js b/resources/js/wysiwyg/plugins-details.js
index 0f089fc8e..9b5287947 100644
--- a/resources/js/wysiwyg/plugins-details.js
+++ b/resources/js/wysiwyg/plugins-details.js
@@ -29,12 +29,15 @@ function register(editor, url) {
         icon: 'togglelabel',
         tooltip: 'Edit label',
         onAction() {
-            const details = getSelectedDetailsBlock(editor);
-            const dialog = editor.windowManager.open(detailsDialog(editor));
-            dialog.setData({summary: getSummaryTextFromDetails(details)});
+            showDetailLabelEditWindow(editor);
         }
     });
 
+    editor.on('dblclick', event => {
+        if (!getSelectedDetailsBlock(editor) || event.target.closest('doc-root')) return;
+        showDetailLabelEditWindow(editor);
+    });
+
     editor.ui.registry.addButton('toggledetails', {
         icon: 'togglefold',
         tooltip: 'Toggle open/closed',
@@ -46,13 +49,29 @@ function register(editor, url) {
     });
 
     editor.addCommand('InsertDetailsBlock', function () {
-        const content = editor.selection.getContent({format: 'html'});
+        let content = editor.selection.getContent({format: 'html'});
         const details = document.createElement('details');
         const summary = document.createElement('summary');
+        const id = 'details-' + Date.now();
+        details.setAttribute('data-id', id)
         details.appendChild(summary);
-        details.innerHTML += content;
 
+        if (!content) {
+            content = '<p><br></p>';
+        }
+
+        details.innerHTML += content;
         editor.insertContent(details.outerHTML);
+        editor.focus();
+
+        const domDetails = editor.dom.$(`[data-id="${id}"]`);
+        if (domDetails) {
+            const firstChild = domDetails.find('doc-root > *');
+            if (firstChild) {
+                firstChild[0].focus();
+            }
+            domDetails.removeAttr('data-id');
+        }
     });
 
     editor.ui.registry.addContextToolbar('details', {
@@ -69,6 +88,15 @@ function register(editor, url) {
     });
 }
 
+/**
+ * @param {Editor} editor
+ */
+function showDetailLabelEditWindow(editor) {
+    const details = getSelectedDetailsBlock(editor);
+    const dialog = editor.windowManager.open(detailsDialog(editor));
+    dialog.setData({summary: getSummaryTextFromDetails(details)});
+}
+
 /**
  * @param {Editor} editor
  */
@@ -99,7 +127,7 @@ function detailsDialog(editor) {
                 {
                     type: 'input',
                     name: 'summary',
-                    label: 'Toggle label text',
+                    label: 'Toggle label',
                 },
             ],
         },
@@ -141,14 +169,13 @@ function setSummary(editor, summaryContent) {
  */
 function unwrapDetailsInSelection(editor) {
     const details = editor.selection.getNode().closest('details');
+
     if (details) {
-        const summary = details.querySelector('summary');
+        const elements = details.querySelectorAll('details > *:not(summary, doc-root), doc-root > *');
+
         editor.undoManager.transact(() => {
-            if (summary) {
-                summary.remove();
-            }
-            while (details.firstChild) {
-                details.parentNode.insertBefore(details.firstChild, details);
+            for (const element of elements) {
+                details.parentNode.insertBefore(element, details);
             }
             details.remove();
         });
@@ -172,6 +199,12 @@ function setupElementFilters(editor) {
             el.attr('open', null);
         }
     });
+
+    editor.serializer.addNodeFilter('doc-root', function(elms) {
+        for (const el of elms) {
+            el.unwrap();
+        }
+    });
 }
 
 /**
diff --git a/resources/lang/en/editor.php b/resources/lang/en/editor.php
index 2b1d1a519..76a9f7fca 100644
--- a/resources/lang/en/editor.php
+++ b/resources/lang/en/editor.php
@@ -136,6 +136,7 @@ return [
     'edit_label' => 'Edit label',
     'toggle_open_closed' => 'Toggle open/closed',
     'collapsible_edit' => 'Edit collapsible block',
+    'toggle_label' => 'Toggle label',
 
     // About view
     'about_title' => 'About the WYSIWYG Editor',
diff --git a/resources/sass/_pages.scss b/resources/sass/_pages.scss
index 4c54c1045..af5bea0f1 100755
--- a/resources/sass/_pages.scss
+++ b/resources/sass/_pages.scss
@@ -158,6 +158,11 @@ body.tox-fullscreen, body.markdown-fullscreen {
   details > summary + * {
     margin-top: .2em;
   }
+  details:after {
+    content: '';
+    display: block;
+    clear: both;
+  }
 
   &.page-revision {
     pre code {
diff --git a/resources/sass/_tinymce.scss b/resources/sass/_tinymce.scss
index ecb258a53..6add27f45 100644
--- a/resources/sass/_tinymce.scss
+++ b/resources/sass/_tinymce.scss
@@ -21,6 +21,9 @@
 .page-content.mce-content-body doc-root {
   display: block;
 }
+.page-content.mce-content-body code-block {
+  display: block;
+}
 
 // In editor line height override
 .page-content.mce-content-body p {
@@ -38,9 +41,12 @@ body.page-content.mce-content-body  {
 }
 
 // Prevent scroll jumps on codemirror clicks
-.page-content.mce-content-body .CodeMirror {
+.page-content.mce-content-body code-block > * {
   pointer-events: none;
 }
+.page-content.mce-content-body code-block pre {
+  display: none;
+}
 
 // Details/summary editor usability
 .page-content.mce-content-body details summary {
@@ -51,6 +57,8 @@ body.page-content.mce-content-body  {
   margin-left: (2px - $-s);
   margin-right: (2px - $-s);
   margin-bottom: (2px - $-s);
+  margin-top: (2px - $-s);
+  overflow: hidden;
 }
 
 /**