1 function elemIsCodeBlock(elem) {
2 return elem.tagName.toLowerCase() === 'code-block';
6 * @param {Editor} editor
8 * @param {String} language
9 * @param {function(string, string)} callback (Receives (code: string,language: string)
11 function showPopup(editor, code, language, callback) {
12 /** @var {CodeEditor} codeEditor * */
13 const codeEditor = window.$components.first('code-editor');
14 const bookMark = editor.selection.getBookmark();
15 codeEditor.open(code, language, (newCode, newLang) => {
16 callback(newCode, newLang);
18 editor.selection.moveToBookmark(bookMark);
21 editor.selection.moveToBookmark(bookMark);
26 * @param {Editor} editor
27 * @param {CodeBlockElement} codeBlock
29 function showPopupForCodeBlock(editor, codeBlock) {
30 showPopup(editor, codeBlock.getContent(), codeBlock.getLanguage(), (newCode, newLang) => {
31 codeBlock.setContent(newCode, newLang);
36 * Define our custom code-block HTML element that we use.
37 * Needs to be delayed since it needs to be defined within the context of the
38 * child editor window and document, hence its definition within a callback.
39 * @param {Editor} editor
41 function defineCodeBlockCustomElement(editor) {
42 const doc = editor.getDoc();
43 const win = doc.defaultView;
45 class CodeBlockElement extends win.HTMLElement {
48 * @type {?SimpleEditorInterface}
54 this.attachShadow({mode: 'open'});
56 const stylesToCopy = document.head.querySelectorAll('link[rel="stylesheet"]:not([media="print"]),style');
57 const copiedStyles = Array.from(stylesToCopy).map(styleEl => styleEl.cloneNode(true));
59 const cmContainer = document.createElement('div');
60 cmContainer.style.pointerEvents = 'none';
61 cmContainer.contentEditable = 'false';
62 cmContainer.classList.add('CodeMirrorContainer');
63 cmContainer.classList.toggle('dark-mode', document.documentElement.classList.contains('dark-mode'));
65 this.shadowRoot.append(...copiedStyles, cmContainer);
69 const getLanguageFromClassList = classes => {
70 const langClasses = classes.split(' ').filter(cssClass => cssClass.startsWith('language-'));
71 return (langClasses[0] || '').replace('language-', '');
74 const code = this.querySelector('code');
75 const pre = this.querySelector('pre');
76 return getLanguageFromClassList(pre.className) || (code && getLanguageFromClassList(code.className)) || '';
79 setContent(content, language) {
81 this.editor.setContent(content);
82 this.editor.setMode(language, content);
85 let pre = this.querySelector('pre');
87 pre = doc.createElement('pre');
92 const code = doc.createElement('code');
94 code.innerText = content;
95 code.className = `language-${language}`;
99 const code = this.querySelector('code') || this.querySelector('pre');
100 const tempEl = document.createElement('pre');
101 tempEl.innerHTML = code.innerHTML.replace(/\ufeff/g, '');
103 const brs = tempEl.querySelectorAll('br');
104 for (const br of brs) {
105 br.replaceWith('\n');
108 return tempEl.textContent;
111 connectedCallback() {
112 const connectedTime = Date.now();
117 this.cleanChildContent();
118 const content = this.getContent();
119 const lines = content.split('\n').length;
120 const height = (lines * 19.2) + 18 + 24;
121 this.style.height = `${height}px`;
123 const container = this.shadowRoot.querySelector('.CodeMirrorContainer');
124 const renderEditor = Code => {
125 this.editor = Code.wysiwygView(container, this.shadowRoot, content, this.getLanguage());
127 this.style.height = null;
131 window.importVersioned('code').then(Code => {
132 const timeout = (Date.now() - connectedTime < 20) ? 20 : 0;
133 setTimeout(() => renderEditor(Code), timeout);
137 cleanChildContent() {
138 const pre = this.querySelector('pre');
141 for (const preChild of pre.childNodes) {
142 if (preChild.nodeName === '#text' && preChild.textContent === '') {
150 win.customElements.define('code-block', CodeBlockElement);
154 * @param {Editor} editor
156 function register(editor) {
157 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>');
159 editor.ui.registry.addButton('codeeditor', {
160 tooltip: 'Insert code block',
163 editor.execCommand('codeeditor');
167 editor.ui.registry.addButton('editcodeeditor', {
168 tooltip: 'Edit code block',
171 editor.execCommand('codeeditor');
175 editor.addCommand('codeeditor', () => {
176 const selectedNode = editor.selection.getNode();
177 const doc = selectedNode.ownerDocument;
178 if (elemIsCodeBlock(selectedNode)) {
179 showPopupForCodeBlock(editor, selectedNode);
181 const textContent = editor.selection.getContent({format: 'text'});
182 showPopup(editor, textContent, '', (newCode, newLang) => {
183 const pre = doc.createElement('pre');
184 const code = doc.createElement('code');
185 code.classList.add(`language-${newLang}`);
186 code.innerText = newCode;
189 editor.insertContent(pre.outerHTML);
194 editor.on('dblclick', () => {
195 const selectedNode = editor.selection.getNode();
196 if (elemIsCodeBlock(selectedNode)) {
197 showPopupForCodeBlock(editor, selectedNode);
201 editor.on('PreInit', () => {
202 editor.parser.addNodeFilter('pre', elms => {
203 for (const el of elms) {
204 const wrapper = window.tinymce.html.Node.create('code-block', {
205 contenteditable: 'false',
208 const spans = el.getAll('span');
209 for (const span of spans) {
212 el.attr('style', null);
217 editor.parser.addNodeFilter('code-block', elms => {
218 for (const el of elms) {
219 el.attr('contenteditable', 'false');
223 editor.serializer.addNodeFilter('code-block', elms => {
224 for (const el of elms) {
230 editor.ui.registry.addContextToolbar('codeeditor', {
232 return node.nodeName.toLowerCase() === 'code-block';
234 items: 'editcodeeditor',
239 editor.on('PreInit', () => {
240 defineCodeBlockCustomElement(editor);
247 export function getPlugin() {