diff --git a/app/Entities/Tools/ExportFormatter.php b/app/Entities/Tools/ExportFormatter.php
index f993d332d..5617db692 100644
--- a/app/Entities/Tools/ExportFormatter.php
+++ b/app/Entities/Tools/ExportFormatter.php
@@ -147,10 +147,31 @@ class ExportFormatter
{
$html = $this->containHtml($html);
$html = $this->replaceIframesWithLinks($html);
+ $html = $this->openDetailElements($html);
return $this->pdfGenerator->fromHtml($html);
}
+ /**
+ * Within the given HTML content, Open any detail blocks
+ */
+ protected function openDetailElements(string $html): string
+ {
+ libxml_use_internal_errors(true);
+
+ $doc = new DOMDocument();
+ $doc->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
+ $xPath = new DOMXPath($doc);
+
+ $details = $xPath->query('//details');
+ /** @var DOMElement $detail */
+ foreach ($details as $detail) {
+ $detail->setAttribute('open', 'open');
+ }
+
+ return $doc->saveHTML();
+ }
+
/**
* Within the given HTML content, replace any iframe elements
* with anchor links within paragraph blocks.
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 8e7669acc..1b3b6e7b5 100644
--- a/resources/js/wysiwyg/config.js
+++ b/resources/js/wysiwyg/config.js
@@ -8,6 +8,7 @@ import {getPlugin as getDrawioPlugin} from "./plugin-drawio";
import {getPlugin as getCustomhrPlugin} from "./plugins-customhr";
import {getPlugin as getImagemanagerPlugin} from "./plugins-imagemanager";
import {getPlugin as getAboutPlugin} from "./plugins-about";
+import {getPlugin as getDetailsPlugin} from "./plugins-details";
const style_formats = [
{title: "Large Header", format: "h2", preview: 'color: blue;'},
@@ -27,7 +28,6 @@ const style_formats = [
];
const formats = {
- codeeditor: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div'},
alignleft: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-left'},
aligncenter: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-center'},
alignright: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-right'},
@@ -79,7 +79,7 @@ function buildToolbar(options) {
insertoverflow: {
icon: 'more-drawer',
tooltip: 'More',
- items: 'hr codeeditor drawio media'
+ items: 'hr codeeditor drawio media details'
}
};
@@ -121,6 +121,7 @@ function gatherPlugins(options) {
"media",
"imagemanager",
"about",
+ "details",
options.textDirection === 'rtl' ? 'directionality' : '',
];
@@ -128,6 +129,7 @@ function gatherPlugins(options) {
window.tinymce.PluginManager.add('customhr', getCustomhrPlugin(options));
window.tinymce.PluginManager.add('imagemanager', getImagemanagerPlugin(options));
window.tinymce.PluginManager.add('about', getAboutPlugin(options));
+ window.tinymce.PluginManager.add('details', getDetailsPlugin(options));
if (options.drawioUrl) {
window.tinymce.PluginManager.add('drawio', getDrawioPlugin(options));
@@ -216,7 +218,7 @@ export function build(options) {
// Set language
window.tinymce.addI18n(options.language, options.translationMap);
-
+ // Build toolbar content
const {toolbar, groupButtons: toolBarGroupButtons} = buildToolbar(options);
// Return config object
@@ -240,9 +242,17 @@ export function build(options) {
statusbar: false,
menubar: false,
paste_data_images: false,
- extended_valid_elements: 'pre[*],svg[*],div[drawio-diagram]',
+ extended_valid_elements: 'pre[*],svg[*],div[drawio-diagram],details[*],summary[*],div[*]',
automatic_uploads: false,
- valid_children: "-div[p|h1|h2|h3|h4|h5|h6|blockquote],+div[pre],+div[img]",
+ 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(/https://www.youtube.com/embed/ShqUjt33uOs
', $pdfHtml); } + public function test_page_pdf_export_opens_details_blocks() + { + $page = Page::query()->first()->forceFill([ + 'html' => 'Content!