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) {
+ /** @var {CodeEditor} codeEditor * */
+ const codeEditor = window.$components.first('code-editor');
+ const bookMark = editor.selection.getBookmark();
+ codeEditor.open(code, language, (newCode, newLang) => {
+ callback(newCode, newLang);
+ editor.focus();
+ editor.selection.moveToBookmark(bookMark);
+ }, () => {
+ editor.focus();
+ editor.selection.moveToBookmark(bookMark);
+ });
+}
- if (!elemIsCodeBlock(selectedNode)) {
- const providedCode = editor.selection.getNode().textContent;
- 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.formatter.toggle('pre');
- const node = editor.selection.getNode();
- editor.dom.setHTML(node, wrap.querySelector('pre').innerHTML);
- editor.fire('SetContent');
+/**
+ * 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;
- editor.focus()
- });
- return;
- }
+ class CodeBlockElement extends win.HTMLElement {
+
+ /**
+ * @type {?SimpleEditorInterface}
+ */
+ editor = null;
+
+ constructor() {
+ super();
+ this.attachShadow({mode: 'open'});
+
+ const stylesToCopy = document.head.querySelectorAll('link[rel="stylesheet"]:not([media="print"]),style');
+ const copiedStyles = Array.from(stylesToCopy).map(styleEl => styleEl.cloneNode(true));
+
+ const cmContainer = document.createElement('div');
+ cmContainer.style.pointerEvents = 'none';
+ cmContainer.contentEditable = 'false';
+ cmContainer.classList.add('CodeMirrorContainer');
+ cmContainer.classList.toggle('dark-mode', document.documentElement.classList.contains('dark-mode'));
+
+ this.shadowRoot.append(...copiedStyles, 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.editor) {
+ this.editor.setContent(content);
+ this.editor.setMode(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(/\ufeff/g, '');
- const lang = selectedNode.hasAttribute('data-lang') ? selectedNode.getAttribute('data-lang') : '';
- const currentCode = selectedNode.querySelector('textarea').textContent;
+ const brs = tempEl.querySelectorAll('br');
+ for (const br of brs) {
+ br.replaceWith('\n');
+ }
+
+ return tempEl.textContent;
+ }
+
+ connectedCallback() {
+ const connectedTime = Date.now();
+ if (this.editor) {
+ 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 renderEditor = Code => {
+ this.editor = Code.wysiwygView(container, this.shadowRoot, content, this.getLanguage());
+ setTimeout(() => {
+ this.style.height = null;
+ }, 12);
+ };
- 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 timeout = (Date.now() - connectedTime < 20) ? 20 : 0;
+ setTimeout(() => renderEditor(Code), timeout);
});
}
- const textArea = selectedNode.querySelector('textarea');
- if (textArea) textArea.textContent = code;
- selectedNode.setAttribute('data-lang', lang);
- editor.focus()
- });
-}
+ cleanChildContent() {
+ const pre = this.querySelector('pre');
+ if (!pre) return;
-function codeMirrorContainerToPre(codeMirrorContainer) {
- const textArea = codeMirrorContainer.querySelector('textarea');
- const code = textArea.textContent;
- const lang = codeMirrorContainer.getAttribute('data-lang');
+ for (const preChild of pre.childNodes) {
+ if (preChild.nodeName === '#text' && preChild.textContent === '') {
+ preChild.remove();
+ }
+ }
+ }
- codeMirrorContainer.removeAttribute('contentEditable');
- const pre = document.createElement('pre');
- const codeElem = document.createElement('code');
- codeElem.classList.add(`language-${lang}`);
- codeElem.textContent = code;
- pre.appendChild(codeElem);
+ }
- codeMirrorContainer.parentElement.replaceChild(pre, codeMirrorContainer);
+ win.customElements.define('code-block', CodeBlockElement);
}
-
/**
* @param {Editor} editor
- * @param {String} url
*/
-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>')
+function register(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', {
tooltip: 'Insert code block',
icon: 'codeblock',
onAction() {
editor.execCommand('codeeditor');
- }
+ },
});
- editor.addCommand('codeeditor', () => {
- showPopup(editor);
+ editor.ui.registry.addButton('editcodeeditor', {
+ tooltip: 'Edit code block',
+ icon: 'edit-block',
+ onAction() {
+ editor.execCommand('codeeditor');
+ },
});
- // Convert
- editor.on('PreProcess', function (e) {
- $('div.CodeMirrorContainer', e.node).each((index, elem) => {
- codeMirrorContainerToPre(elem);
- });
+ editor.addCommand('codeeditor', () => {
+ 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);
+ editor.on('dblclick', () => {
+ const selectedNode = editor.selection.getNode();
+ 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', elms => {
+ for (const el of elms) {
+ const wrapper = window.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', elms => {
+ for (const el of elms) {
+ el.attr('contenteditable', 'false');
+ }
});
- codeSamples.each((index, elem) => {
- Code.wysiwygView(elem);
+ editor.serializer.addNodeFilter('code-block', 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.ui.registry.addContextToolbar('codeeditor', {
+ predicate(node) {
+ return node.nodeName.toLowerCase() === 'code-block';
+ },
+ items: 'editcodeeditor',
+ position: 'node',
+ scope: 'node',
+ });
+
+ editor.on('PreInit', () => {
+ defineCodeBlockCustomElement(editor);
});
}
/**
- * @param {WysiwygConfigOptions} options
* @return {register}
*/
-export function getPlugin(options) {
+export function getPlugin() {
return register;
-}
\ No newline at end of file
+}