1 function elemIsCodeBlock(elem) {
2 return elem.tagName.toLowerCase() === 'code-block';
6 * @param {Editor} editor
8 * @param {String} language
9 * @param {String} direction
10 * @param {function(string, string)} callback (Receives (code: string,language: string)
12 function showPopup(editor, code, language, direction, callback) {
13 /** @var {CodeEditor} codeEditor * */
14 const codeEditor = window.$components.first('code-editor');
15 const bookMark = editor.selection.getBookmark();
16 codeEditor.open(code, language, direction, (newCode, newLang) => {
17 callback(newCode, newLang);
19 editor.selection.moveToBookmark(bookMark);
22 editor.selection.moveToBookmark(bookMark);
27 * @param {Editor} editor
28 * @param {CodeBlockElement} codeBlock
30 function showPopupForCodeBlock(editor, codeBlock) {
31 const direction = codeBlock.getAttribute('dir') || '';
32 showPopup(editor, codeBlock.getContent(), codeBlock.getLanguage(), direction, (newCode, newLang) => {
33 codeBlock.setContent(newCode, newLang);
38 * Define our custom code-block HTML element that we use.
39 * Needs to be delayed since it needs to be defined within the context of the
40 * child editor window and document, hence its definition within a callback.
41 * @param {Editor} editor
43 function defineCodeBlockCustomElement(editor) {
44 const doc = editor.getDoc();
45 const win = doc.defaultView;
47 class CodeBlockElement extends win.HTMLElement {
50 * @type {?SimpleEditorInterface}
56 this.attachShadow({mode: 'open'});
58 const stylesToCopy = document.head.querySelectorAll('link[rel="stylesheet"]:not([media="print"]),style');
59 const copiedStyles = Array.from(stylesToCopy).map(styleEl => styleEl.cloneNode(true));
61 const cmContainer = document.createElement('div');
62 cmContainer.style.pointerEvents = 'none';
63 cmContainer.contentEditable = 'false';
64 cmContainer.classList.add('CodeMirrorContainer');
65 cmContainer.classList.toggle('dark-mode', document.documentElement.classList.contains('dark-mode'));
67 this.shadowRoot.append(...copiedStyles, cmContainer);
71 const getLanguageFromClassList = classes => {
72 const langClasses = classes.split(' ').filter(cssClass => cssClass.startsWith('language-'));
73 return (langClasses[0] || '').replace('language-', '');
76 const code = this.querySelector('code');
77 const pre = this.querySelector('pre');
78 return getLanguageFromClassList(pre.className) || (code && getLanguageFromClassList(code.className)) || '';
81 setContent(content, language) {
83 this.editor.setContent(content);
84 this.editor.setMode(language, content);
87 let pre = this.querySelector('pre');
89 pre = doc.createElement('pre');
94 const code = doc.createElement('code');
96 code.innerText = content;
97 code.className = `language-${language}`;
101 const code = this.querySelector('code') || this.querySelector('pre');
102 const tempEl = document.createElement('pre');
103 tempEl.innerHTML = code.innerHTML.replace(/\ufeff/g, '');
105 const brs = tempEl.querySelectorAll('br');
106 for (const br of brs) {
107 br.replaceWith('\n');
110 return tempEl.textContent;
113 connectedCallback() {
114 const connectedTime = Date.now();
119 this.cleanChildContent();
120 const content = this.getContent();
121 const lines = content.split('\n').length;
122 const height = (lines * 19.2) + 18 + 24;
123 this.style.height = `${height}px`;
125 const container = this.shadowRoot.querySelector('.CodeMirrorContainer');
126 const renderEditor = Code => {
127 this.editor = Code.wysiwygView(container, this.shadowRoot, content, this.getLanguage());
129 this.style.height = null;
133 window.importVersioned('code').then(Code => {
134 const timeout = (Date.now() - connectedTime < 20) ? 20 : 0;
135 setTimeout(() => renderEditor(Code), timeout);
139 cleanChildContent() {
140 const pre = this.querySelector('pre');
143 for (const preChild of pre.childNodes) {
144 if (preChild.nodeName === '#text' && preChild.textContent === '') {
152 win.customElements.define('code-block', CodeBlockElement);
156 * @param {Editor} editor
158 function register(editor) {
159 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>');
161 editor.ui.registry.addButton('codeeditor', {
162 tooltip: 'Insert code block',
165 editor.execCommand('codeeditor');
169 editor.ui.registry.addButton('editcodeeditor', {
170 tooltip: 'Edit code block',
173 editor.execCommand('codeeditor');
177 editor.addCommand('codeeditor', () => {
178 const selectedNode = editor.selection.getNode();
179 const doc = selectedNode.ownerDocument;
180 if (elemIsCodeBlock(selectedNode)) {
181 showPopupForCodeBlock(editor, selectedNode);
183 const textContent = editor.selection.getContent({format: 'text'});
184 const direction = document.dir === 'rtl' ? 'ltr' : '';
185 showPopup(editor, textContent, '', direction, (newCode, newLang) => {
186 const pre = doc.createElement('pre');
187 const code = doc.createElement('code');
188 code.classList.add(`language-${newLang}`);
189 code.innerText = newCode;
191 pre.setAttribute('dir', direction);
195 editor.insertContent(pre.outerHTML);
200 editor.on('dblclick', () => {
201 const selectedNode = editor.selection.getNode();
202 if (elemIsCodeBlock(selectedNode)) {
203 showPopupForCodeBlock(editor, selectedNode);
207 editor.on('PreInit', () => {
208 editor.parser.addNodeFilter('pre', elms => {
209 for (const el of elms) {
210 const wrapper = window.tinymce.html.Node.create('code-block', {
211 contenteditable: 'false',
214 const childCodeBlock = el.children().filter(child => child.name === 'code')[0] || null;
215 const direction = el.attr('dir') || (childCodeBlock && childCodeBlock.attr('dir')) || '';
217 wrapper.attr('dir', direction);
220 const spans = el.getAll('span');
221 for (const span of spans) {
224 el.attr('style', null);
229 editor.parser.addNodeFilter('code-block', elms => {
230 for (const el of elms) {
231 el.attr('contenteditable', 'false');
235 editor.serializer.addNodeFilter('code-block', elms => {
236 for (const el of elms) {
237 const direction = el.attr('dir');
238 if (direction && el.firstChild) {
239 el.firstChild.attr('dir', direction);
240 } else if (el.firstChild) {
241 el.firstChild.attr('dir', null);
249 editor.ui.registry.addContextToolbar('codeeditor', {
251 return node.nodeName.toLowerCase() === 'code-block';
253 items: 'editcodeeditor',
258 editor.on('PreInit', () => {
259 defineCodeBlockCustomElement(editor);
266 export function getPlugin() {