]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/plugin-codeeditor.js
Merge pull request #3579 from BookStackApp/dompdf_and_php_deps
[bookstack] / resources / js / wysiwyg / plugin-codeeditor.js
1 function elemIsCodeBlock(elem) {
2     return elem.tagName.toLowerCase() === 'code-block';
3 }
4
5 /**
6  * @param {Editor} editor
7  * @param {String} code
8  * @param {String} language
9  * @param {function(string, string)} callback (Receives (code: string,language: string)
10  */
11 function showPopup(editor, code, language, callback) {
12     window.components.first('code-editor').open(code, language, (newCode, newLang) => {
13         callback(newCode, newLang)
14         editor.focus()
15     });
16 }
17
18 /**
19  * @param {Editor} editor
20  * @param {CodeBlockElement} codeBlock
21  */
22 function showPopupForCodeBlock(editor, codeBlock) {
23     showPopup(editor, codeBlock.getContent(), codeBlock.getLanguage(), (newCode, newLang) => {
24         codeBlock.setContent(newCode, newLang);
25     });
26 }
27
28 /**
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
33  */
34 function defineCodeBlockCustomElement(editor) {
35     const doc = editor.getDoc();
36     const win = doc.defaultView;
37
38     class CodeBlockElement extends win.HTMLElement {
39         constructor() {
40             super();
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'));
45
46             const cmContainer = document.createElement('div');
47             cmContainer.style.pointerEvents = 'none';
48             cmContainer.contentEditable = 'false';
49             cmContainer.classList.add('CodeMirrorContainer');
50
51             this.shadowRoot.append(linkElem, cmContainer);
52         }
53
54         getLanguage() {
55             const getLanguageFromClassList = (classes) => {
56                 const langClasses = classes.split(' ').filter(cssClass => cssClass.startsWith('language-'));
57                 return (langClasses[0] || '').replace('language-', '');
58             };
59
60             const code = this.querySelector('code');
61             const pre = this.querySelector('pre');
62             return getLanguageFromClassList(pre.className) || (code && getLanguageFromClassList(code.className)) || '';
63         }
64
65         setContent(content, language) {
66             if (this.cm) {
67                 importVersioned('code').then(Code => {
68                     Code.setContent(this.cm, content);
69                     Code.setMode(this.cm, language, content);
70                 });
71             }
72
73             let pre = this.querySelector('pre');
74             if (!pre) {
75                 pre = doc.createElement('pre');
76                 this.append(pre);
77             }
78             pre.innerHTML = '';
79
80             const code = doc.createElement('code');
81             pre.append(code);
82             code.innerText = content;
83             code.className = `language-${language}`;
84         }
85
86         getContent() {
87             const code = this.querySelector('code') || this.querySelector('pre');
88             const tempEl = document.createElement('pre');
89             tempEl.innerHTML = code.innerHTML.replace(/\ufeff/g, '');
90
91             const brs = tempEl.querySelectorAll('br');
92             for (const br of brs) {
93                 br.replaceWith('\n');
94             }
95
96             return tempEl.textContent;
97         }
98
99         connectedCallback() {
100             const connectedTime = Date.now();
101             if (this.cm) {
102                 return;
103             }
104
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`;
110
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);
116                 setTimeout(() => {
117                     this.style.height = null;
118                 }, 1);
119             };
120
121             window.importVersioned('code').then((Code) => {
122                 const timeout = (Date.now() - connectedTime < 20) ? 20 : 0;
123                 setTimeout(() => renderCodeMirror(Code), timeout);
124             });
125         }
126
127         cleanChildContent() {
128             const pre = this.querySelector('pre');
129             if (!pre) return;
130
131             for (const preChild of pre.childNodes) {
132                 if (preChild.nodeName === '#text' && preChild.textContent === '') {
133                     preChild.remove();
134                 }
135             }
136         }
137     }
138
139     win.customElements.define('code-block', CodeBlockElement);
140 }
141
142
143 /**
144  * @param {Editor} editor
145  * @param {String} url
146  */
147 function register(editor, url) {
148
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>')
150
151     editor.ui.registry.addButton('codeeditor', {
152         tooltip: 'Insert code block',
153         icon: 'codeblock',
154         onAction() {
155             editor.execCommand('codeeditor');
156         }
157     });
158
159     editor.addCommand('codeeditor', () => {
160         const selectedNode = editor.selection.getNode();
161         const doc = selectedNode.ownerDocument;
162         if (elemIsCodeBlock(selectedNode)) {
163             showPopupForCodeBlock(editor, selectedNode);
164         } else {
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;
172                 pre.append(code);
173
174                 editor.insertContent(pre.outerHTML);
175             });
176         }
177     });
178
179     editor.on('dblclick', event => {
180         let selectedNode = editor.selection.getNode();
181         if (elemIsCodeBlock(selectedNode)) {
182             showPopupForCodeBlock(editor, selectedNode);
183         }
184     });
185
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',
191                 });
192
193                 const spans = el.getAll('span');
194                 for (const span of spans) {
195                     span.unwrap();
196                 }
197                 el.attr('style', null);
198                 el.wrap(wrapper);
199             }
200         });
201
202         editor.parser.addNodeFilter('code-block', function(elms) {
203             for (const el of elms) {
204                 el.attr('contenteditable', 'false');
205             }
206         });
207
208         editor.serializer.addNodeFilter('code-block', function(elms) {
209             for (const el of elms) {
210                 el.unwrap();
211             }
212         });
213     });
214
215     editor.on('PreInit', () => {
216         defineCodeBlockCustomElement(editor);
217     });
218 }
219
220 /**
221  * @param {WysiwygConfigOptions} options
222  * @return {register}
223  */
224 export function getPlugin(options) {
225     return register;
226 }