mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-07-30 04:23:11 +03:00
Revamped workings of WYSIWYG code blocks
Code blocks in tinymce could sometimes end up exploded into the sub elements of the codemirror display. This changes the strategy to render codemirror within the shadow dom of a custom element while preserving the normal pre/code DOM structure. Still a little instability when moving/adding code blocks within details blocks but much harder to break things now.
This commit is contained in:
@ -1,56 +1,108 @@
|
||||
function elemIsCodeBlock(elem) {
|
||||
return elem.className === 'CodeMirrorContainer';
|
||||
return elem.tagName.toLowerCase() === 'code-block';
|
||||
}
|
||||
|
||||
function showPopup(editor) {
|
||||
const selectedNode = editor.selection.getNode();
|
||||
|
||||
if (!elemIsCodeBlock(selectedNode)) {
|
||||
const providedCode = editor.selection.getContent({format: 'text'});
|
||||
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;
|
||||
|
||||
editor.insertContent(wrap.innerHTML);
|
||||
editor.focus();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const lang = selectedNode.hasAttribute('data-lang') ? selectedNode.getAttribute('data-lang') : '';
|
||||
const currentCode = selectedNode.querySelector('textarea').textContent;
|
||||
|
||||
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 textArea = selectedNode.querySelector('textarea');
|
||||
if (textArea) textArea.textContent = code;
|
||||
selectedNode.setAttribute('data-lang', lang);
|
||||
|
||||
/**
|
||||
* @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) {
|
||||
window.components.first('code-editor').open(code, language, (newCode, newLang) => {
|
||||
callback(newCode, newLang)
|
||||
editor.focus()
|
||||
});
|
||||
}
|
||||
|
||||
function codeMirrorContainerToPre(codeMirrorContainer) {
|
||||
const textArea = codeMirrorContainer.querySelector('textarea');
|
||||
const code = textArea.textContent;
|
||||
const lang = codeMirrorContainer.getAttribute('data-lang');
|
||||
/**
|
||||
* @param {Editor} editor
|
||||
* @param {CodeBlockElement} codeBlock
|
||||
*/
|
||||
function showPopupForCodeBlock(editor, codeBlock) {
|
||||
showPopup(editor, codeBlock.getContent(), codeBlock.getLanguage(), (newCode, newLang) => {
|
||||
codeBlock.setContent(newCode, newLang);
|
||||
});
|
||||
}
|
||||
|
||||
codeMirrorContainer.removeAttribute('contentEditable');
|
||||
const pre = document.createElement('pre');
|
||||
const codeElem = document.createElement('code');
|
||||
codeElem.classList.add(`language-${lang}`);
|
||||
codeElem.textContent = code;
|
||||
pre.appendChild(codeElem);
|
||||
/**
|
||||
* 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;
|
||||
|
||||
codeMirrorContainer.parentElement.replaceChild(pre, codeMirrorContainer);
|
||||
class CodeBlockElement extends win.HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({mode: 'open'});
|
||||
const linkElem = document.createElement('link');
|
||||
linkElem.setAttribute('rel', 'stylesheet');
|
||||
linkElem.setAttribute('href', window.baseUrl('/dist/styles.css'));
|
||||
|
||||
const cmContainer = document.createElement('div');
|
||||
cmContainer.style.pointerEvents = 'none';
|
||||
cmContainer.contentEditable = 'false';
|
||||
cmContainer.classList.add('CodeMirrorContainer');
|
||||
|
||||
this.shadowRoot.append(linkElem, 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.cm) {
|
||||
importVersioned('code').then(Code => {
|
||||
Code.setContent(this.cm, content);
|
||||
Code.setMode(this.cm, 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().replace(/<br\s*[\/]?>/gi ,'\n').replace(/\ufeff/g, '');
|
||||
return tempEl.textContent;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
if (this.cm) {
|
||||
return;
|
||||
}
|
||||
|
||||
const container = this.shadowRoot.querySelector('.CodeMirrorContainer');
|
||||
importVersioned('code').then(Code => {
|
||||
this.cm = Code.wysiwygView(container, this.getContent(), this.getLanguage());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
win.customElements.define('code-block', CodeBlockElement);
|
||||
}
|
||||
|
||||
|
||||
@ -60,8 +112,6 @@ function codeMirrorContainerToPre(codeMirrorContainer) {
|
||||
*/
|
||||
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>')
|
||||
|
||||
editor.ui.registry.addButton('codeeditor', {
|
||||
@ -73,54 +123,64 @@ function register(editor, url) {
|
||||
});
|
||||
|
||||
editor.addCommand('codeeditor', () => {
|
||||
showPopup(editor);
|
||||
});
|
||||
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 wrap = doc.createElement('code-block');
|
||||
const pre = doc.createElement('pre');
|
||||
const code = doc.createElement('code');
|
||||
code.classList.add(`language-${newLang}`);
|
||||
code.innerText = newCode;
|
||||
pre.append(code);
|
||||
wrap.append(pre);
|
||||
|
||||
// Convert
|
||||
editor.on('PreProcess', function (e) {
|
||||
$('div.CodeMirrorContainer', e.node).each((index, elem) => {
|
||||
codeMirrorContainerToPre(elem);
|
||||
});
|
||||
editor.insertContent(wrap.outerHTML);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
editor.on('dblclick', event => {
|
||||
let selectedNode = editor.selection.getNode();
|
||||
if (!elemIsCodeBlock(selectedNode)) return;
|
||||
showPopup(editor);
|
||||
if (elemIsCodeBlock(selectedNode)) {
|
||||
showPopupForCodeBlock(editor, selectedNode);
|
||||
}
|
||||
});
|
||||
|
||||
function parseCodeMirrorInstances(Code) {
|
||||
editor.on('PreInit', () => {
|
||||
editor.parser.addNodeFilter('pre', function(elms) {
|
||||
for (const el of elms) {
|
||||
const wrapper = new tinymce.html.Node.create('code-block', {
|
||||
contenteditable: 'false',
|
||||
});
|
||||
|
||||
// Recover broken codemirror instances
|
||||
$('.CodeMirrorContainer').filter((index ,elem) => {
|
||||
return typeof elem.querySelector('.CodeMirror').CodeMirror === 'undefined';
|
||||
}).each((index, elem) => {
|
||||
codeMirrorContainerToPre(elem);
|
||||
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', function(elms) {
|
||||
for (const el of elms) {
|
||||
el.attr('content-editable', 'false');
|
||||
}
|
||||
});
|
||||
|
||||
codeSamples.each((index, elem) => {
|
||||
Code.wysiwygView(elem);
|
||||
editor.serializer.addNodeFilter('code-block', function(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.on('PreInit', () => {
|
||||
defineCodeBlockCustomElement(editor);
|
||||
});
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user