/**
* 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
}`.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}
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 {
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,
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 = `<pre><code class="language-${lang}"></code></pre>`;
- 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().replace(/<br\s*[\/]?>/gi ,'\n').replace(/\ufeff/g, '');
+ return tempEl.textContent;
+ }
- codeMirrorContainer.removeAttribute('contentEditable');
- const pre = document.createElement('pre');
- const codeElem = document.createElement('code');
- codeElem.classList.add(`language-${lang}`);
- codeElem.textContent = code;
- pre.appendChild(codeElem);
+ connectedCallback() {
+ if (this.cm) {
+ return;
+ }
+
+ const container = this.shadowRoot.querySelector('.CodeMirrorContainer');
+ importVersioned('code').then(Code => {
+ this.cm = Code.wysiwygView(container, this.getContent(), this.getLanguage());
+ });
+ }
+ }
- codeMirrorContainer.parentElement.replaceChild(pre, codeMirrorContainer);
+ win.customElements.define('code-block', CodeBlockElement);
}
*/
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', {
});
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 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);
+
+ 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) {
-
- // 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 = new 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('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);
});
}
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',
});
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', {
});
}
+/**
+ * @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
*/
{
type: 'input',
name: 'summary',
- label: 'Toggle label text',
+ label: 'Toggle label',
},
],
},
*/
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();
});
el.attr('open', null);
}
});
+
+ editor.serializer.addNodeFilter('doc-root', function(elms) {
+ for (const el of elms) {
+ el.unwrap();
+ }
+ });
}
/**