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 window.components.first('code-editor').open(code, language, (newCode, newLang) => {
13 callback(newCode, newLang)
19 * @param {Editor} editor
20 * @param {CodeBlockElement} codeBlock
22 function showPopupForCodeBlock(editor, codeBlock) {
23 showPopup(editor, codeBlock.getContent(), codeBlock.getLanguage(), (newCode, newLang) => {
24 codeBlock.setContent(newCode, newLang);
29 * Define our custom code-block HTML element that we use.
30 * Needs to be delayed since it needs to be defined within the context of the
31 * child editor window and document, hence its definition within a callback.
32 * @param {Editor} editor
34 function defineCodeBlockCustomElement(editor) {
35 const doc = editor.getDoc();
36 const win = doc.defaultView;
38 class CodeBlockElement extends win.HTMLElement {
41 this.attachShadow({mode: 'open'});
42 const linkElem = document.createElement('link');
43 linkElem.setAttribute('rel', 'stylesheet');
44 linkElem.setAttribute('href', window.baseUrl('/dist/styles.css'));
46 const cmContainer = document.createElement('div');
47 cmContainer.style.pointerEvents = 'none';
48 cmContainer.contentEditable = 'false';
49 cmContainer.classList.add('CodeMirrorContainer');
51 this.shadowRoot.append(linkElem, cmContainer);
55 const getLanguageFromClassList = (classes) => {
56 const langClasses = classes.split(' ').filter(cssClass => cssClass.startsWith('language-'));
57 return (langClasses[0] || '').replace('language-', '');
60 const code = this.querySelector('code');
61 const pre = this.querySelector('pre');
62 return getLanguageFromClassList(pre.className) || (code && getLanguageFromClassList(code.className)) || '';
65 setContent(content, language) {
67 importVersioned('code').then(Code => {
68 Code.setContent(this.cm, content);
69 Code.setMode(this.cm, language, content);
73 let pre = this.querySelector('pre');
75 pre = doc.createElement('pre');
80 const code = doc.createElement('code');
82 code.innerText = content;
83 code.className = `language-${language}`;
87 const code = this.querySelector('code') || this.querySelector('pre');
88 const tempEl = document.createElement('pre');
89 tempEl.innerHTML = code.innerHTML.replace(/\ufeff/g, '');
91 const brs = tempEl.querySelectorAll('br');
92 for (const br of brs) {
96 return tempEl.textContent;
100 const connectedTime = Date.now();
105 this.cleanChildContent();
106 const content = this.getContent();
107 const lines = content.split('\n').length;
108 const height = (lines * 19.2) + 18 + 24;
109 this.style.height = `${height}px`;
111 const container = this.shadowRoot.querySelector('.CodeMirrorContainer');
112 const renderCodeMirror = (Code) => {
113 console.log({content});
114 this.cm = Code.wysiwygView(container, content, this.getLanguage());
115 Code.updateLayout(this.cm);
117 this.style.height = null;
121 window.importVersioned('code').then((Code) => {
122 const timeout = (Date.now() - connectedTime < 20) ? 20 : 0;
123 setTimeout(() => renderCodeMirror(Code), timeout);
127 cleanChildContent() {
128 const pre = this.querySelector('pre');
131 for (const preChild of pre.childNodes) {
132 if (preChild.nodeName === '#text' && preChild.textContent === '') {
139 win.customElements.define('code-block', CodeBlockElement);
144 * @param {Editor} editor
145 * @param {String} url
147 function register(editor, url) {
149 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>')
151 editor.ui.registry.addButton('codeeditor', {
152 tooltip: 'Insert code block',
155 editor.execCommand('codeeditor');
159 editor.addCommand('codeeditor', () => {
160 const selectedNode = editor.selection.getNode();
161 const doc = selectedNode.ownerDocument;
162 if (elemIsCodeBlock(selectedNode)) {
163 showPopupForCodeBlock(editor, selectedNode);
165 const textContent = editor.selection.getContent({format: 'text'});
166 showPopup(editor, textContent, '', (newCode, newLang) => {
167 const pre = doc.createElement('pre');
168 const code = doc.createElement('code');
169 console.log(newCode);
170 code.classList.add(`language-${newLang}`);
171 code.innerText = newCode;
174 editor.insertContent(pre.outerHTML);
179 editor.on('dblclick', event => {
180 let selectedNode = editor.selection.getNode();
181 if (elemIsCodeBlock(selectedNode)) {
182 showPopupForCodeBlock(editor, selectedNode);
186 editor.on('PreInit', () => {
187 editor.parser.addNodeFilter('pre', function(elms) {
188 for (const el of elms) {
189 const wrapper = new tinymce.html.Node.create('code-block', {
190 contenteditable: 'false',
193 const spans = el.getAll('span');
194 for (const span of spans) {
197 el.attr('style', null);
202 editor.parser.addNodeFilter('code-block', function(elms) {
203 for (const el of elms) {
204 el.attr('contenteditable', 'false');
208 editor.serializer.addNodeFilter('code-block', function(elms) {
209 for (const el of elms) {
215 editor.on('PreInit', () => {
216 defineCodeBlockCustomElement(editor);
221 * @param {WysiwygConfigOptions} options
224 export function getPlugin(options) {