2022-02-06 07:15:58 +08:00
|
|
|
|
function elemIsCodeBlock(elem) {
|
2022-02-10 03:24:27 +08:00
|
|
|
|
return elem.tagName.toLowerCase() === 'code-block';
|
2022-02-06 07:15:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
2022-02-10 03:24:27 +08:00
|
|
|
|
/**
|
|
|
|
|
* @param {Editor} editor
|
|
|
|
|
* @param {String} code
|
|
|
|
|
* @param {String} language
|
2024-05-03 20:35:30 +08:00
|
|
|
|
* @param {String} direction
|
2022-02-10 03:24:27 +08:00
|
|
|
|
* @param {function(string, string)} callback (Receives (code: string,language: string)
|
|
|
|
|
*/
|
2024-05-03 20:35:30 +08:00
|
|
|
|
function showPopup(editor, code, language, direction, callback) {
|
2023-05-08 02:36:10 +08:00
|
|
|
|
/** @var {CodeEditor} codeEditor * */
|
|
|
|
|
const codeEditor = window.$components.first('code-editor');
|
|
|
|
|
const bookMark = editor.selection.getBookmark();
|
2024-05-03 20:35:30 +08:00
|
|
|
|
codeEditor.open(code, language, direction, (newCode, newLang) => {
|
2023-04-19 05:20:02 +08:00
|
|
|
|
callback(newCode, newLang);
|
|
|
|
|
editor.focus();
|
2023-05-08 02:36:10 +08:00
|
|
|
|
editor.selection.moveToBookmark(bookMark);
|
|
|
|
|
}, () => {
|
|
|
|
|
editor.focus();
|
|
|
|
|
editor.selection.moveToBookmark(bookMark);
|
2022-02-10 03:24:27 +08:00
|
|
|
|
});
|
|
|
|
|
}
|
2022-02-06 07:15:58 +08:00
|
|
|
|
|
2022-02-10 03:24:27 +08:00
|
|
|
|
/**
|
|
|
|
|
* @param {Editor} editor
|
|
|
|
|
* @param {CodeBlockElement} codeBlock
|
|
|
|
|
*/
|
|
|
|
|
function showPopupForCodeBlock(editor, codeBlock) {
|
2024-05-03 20:35:30 +08:00
|
|
|
|
const direction = codeBlock.getAttribute('dir') || '';
|
|
|
|
|
showPopup(editor, codeBlock.getContent(), codeBlock.getLanguage(), direction, (newCode, newLang) => {
|
2022-02-10 03:24:27 +08:00
|
|
|
|
codeBlock.setContent(newCode, newLang);
|
|
|
|
|
});
|
|
|
|
|
}
|
2022-02-06 07:15:58 +08:00
|
|
|
|
|
2022-02-10 03:24:27 +08:00
|
|
|
|
/**
|
|
|
|
|
* 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;
|
|
|
|
|
|
|
|
|
|
class CodeBlockElement extends win.HTMLElement {
|
2023-04-17 20:24:29 +08:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @type {?SimpleEditorInterface}
|
|
|
|
|
*/
|
|
|
|
|
editor = null;
|
|
|
|
|
|
2022-02-10 03:24:27 +08:00
|
|
|
|
constructor() {
|
|
|
|
|
super();
|
|
|
|
|
this.attachShadow({mode: 'open'});
|
2022-09-28 01:44:06 +08:00
|
|
|
|
|
2023-05-08 19:21:53 +08:00
|
|
|
|
const stylesToCopy = document.head.querySelectorAll('link[rel="stylesheet"]:not([media="print"]),style');
|
|
|
|
|
const copiedStyles = Array.from(stylesToCopy).map(styleEl => styleEl.cloneNode(true));
|
2022-02-10 03:24:27 +08:00
|
|
|
|
|
|
|
|
|
const cmContainer = document.createElement('div');
|
|
|
|
|
cmContainer.style.pointerEvents = 'none';
|
|
|
|
|
cmContainer.contentEditable = 'false';
|
|
|
|
|
cmContainer.classList.add('CodeMirrorContainer');
|
2023-04-18 20:41:28 +08:00
|
|
|
|
cmContainer.classList.toggle('dark-mode', document.documentElement.classList.contains('dark-mode'));
|
2022-02-10 03:24:27 +08:00
|
|
|
|
|
2022-09-28 01:44:06 +08:00
|
|
|
|
this.shadowRoot.append(...copiedStyles, cmContainer);
|
2022-02-10 03:24:27 +08:00
|
|
|
|
}
|
2022-02-06 07:15:58 +08:00
|
|
|
|
|
2022-02-10 03:24:27 +08:00
|
|
|
|
getLanguage() {
|
2023-04-19 05:20:02 +08:00
|
|
|
|
const getLanguageFromClassList = classes => {
|
2022-02-10 03:24:27 +08:00
|
|
|
|
const langClasses = classes.split(' ').filter(cssClass => cssClass.startsWith('language-'));
|
|
|
|
|
return (langClasses[0] || '').replace('language-', '');
|
|
|
|
|
};
|
2022-02-06 07:15:58 +08:00
|
|
|
|
|
2022-02-10 03:24:27 +08:00
|
|
|
|
const code = this.querySelector('code');
|
|
|
|
|
const pre = this.querySelector('pre');
|
|
|
|
|
return getLanguageFromClassList(pre.className) || (code && getLanguageFromClassList(code.className)) || '';
|
2022-02-06 07:15:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
2022-02-10 03:24:27 +08:00
|
|
|
|
setContent(content, language) {
|
2023-04-17 20:24:29 +08:00
|
|
|
|
if (this.editor) {
|
|
|
|
|
this.editor.setContent(content);
|
|
|
|
|
this.editor.setMode(language, content);
|
2022-02-10 03:24:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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}`;
|
|
|
|
|
}
|
2022-02-06 07:15:58 +08:00
|
|
|
|
|
2022-02-10 03:24:27 +08:00
|
|
|
|
getContent() {
|
|
|
|
|
const code = this.querySelector('code') || this.querySelector('pre');
|
|
|
|
|
const tempEl = document.createElement('pre');
|
2022-06-21 19:01:06 +08:00
|
|
|
|
tempEl.innerHTML = code.innerHTML.replace(/\ufeff/g, '');
|
|
|
|
|
|
|
|
|
|
const brs = tempEl.querySelectorAll('br');
|
|
|
|
|
for (const br of brs) {
|
|
|
|
|
br.replaceWith('\n');
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-10 03:24:27 +08:00
|
|
|
|
return tempEl.textContent;
|
|
|
|
|
}
|
2022-02-06 07:15:58 +08:00
|
|
|
|
|
2022-02-10 03:24:27 +08:00
|
|
|
|
connectedCallback() {
|
2022-02-28 01:18:08 +08:00
|
|
|
|
const connectedTime = Date.now();
|
2023-04-17 20:24:29 +08:00
|
|
|
|
if (this.editor) {
|
2022-02-10 03:24:27 +08:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-28 01:18:08 +08:00
|
|
|
|
this.cleanChildContent();
|
2022-03-25 19:13:04 +08:00
|
|
|
|
const content = this.getContent();
|
|
|
|
|
const lines = content.split('\n').length;
|
|
|
|
|
const height = (lines * 19.2) + 18 + 24;
|
|
|
|
|
this.style.height = `${height}px`;
|
2022-02-28 01:18:08 +08:00
|
|
|
|
|
2022-02-10 03:24:27 +08:00
|
|
|
|
const container = this.shadowRoot.querySelector('.CodeMirrorContainer');
|
2023-04-19 05:20:02 +08:00
|
|
|
|
const renderEditor = Code => {
|
2023-04-17 20:24:29 +08:00
|
|
|
|
this.editor = Code.wysiwygView(container, this.shadowRoot, content, this.getLanguage());
|
2023-04-19 17:46:13 +08:00
|
|
|
|
setTimeout(() => {
|
|
|
|
|
this.style.height = null;
|
|
|
|
|
}, 12);
|
2022-02-28 01:18:08 +08:00
|
|
|
|
};
|
|
|
|
|
|
2023-04-19 05:20:02 +08:00
|
|
|
|
window.importVersioned('code').then(Code => {
|
2022-02-28 01:18:08 +08:00
|
|
|
|
const timeout = (Date.now() - connectedTime < 20) ? 20 : 0;
|
2023-04-17 20:24:29 +08:00
|
|
|
|
setTimeout(() => renderEditor(Code), timeout);
|
2022-02-10 03:24:27 +08:00
|
|
|
|
});
|
|
|
|
|
}
|
2022-02-28 01:18:08 +08:00
|
|
|
|
|
|
|
|
|
cleanChildContent() {
|
|
|
|
|
const pre = this.querySelector('pre');
|
|
|
|
|
if (!pre) return;
|
|
|
|
|
|
|
|
|
|
for (const preChild of pre.childNodes) {
|
|
|
|
|
if (preChild.nodeName === '#text' && preChild.textContent === '') {
|
|
|
|
|
preChild.remove();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-04-19 05:20:02 +08:00
|
|
|
|
|
2022-02-10 03:24:27 +08:00
|
|
|
|
}
|
2022-02-06 07:15:58 +08:00
|
|
|
|
|
2022-02-10 03:24:27 +08:00
|
|
|
|
win.customElements.define('code-block', CodeBlockElement);
|
2022-02-06 07:15:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param {Editor} editor
|
|
|
|
|
*/
|
2023-04-19 17:46:13 +08:00
|
|
|
|
function register(editor) {
|
2023-04-19 05:20:02 +08:00
|
|
|
|
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>');
|
2022-02-06 07:15:58 +08:00
|
|
|
|
|
|
|
|
|
editor.ui.registry.addButton('codeeditor', {
|
2022-02-07 05:17:08 +08:00
|
|
|
|
tooltip: 'Insert code block',
|
2022-02-06 07:15:58 +08:00
|
|
|
|
icon: 'codeblock',
|
|
|
|
|
onAction() {
|
|
|
|
|
editor.execCommand('codeeditor');
|
2023-04-19 05:20:02 +08:00
|
|
|
|
},
|
2022-02-06 07:15:58 +08:00
|
|
|
|
});
|
|
|
|
|
|
2022-10-15 22:47:34 +08:00
|
|
|
|
editor.ui.registry.addButton('editcodeeditor', {
|
|
|
|
|
tooltip: 'Edit code block',
|
|
|
|
|
icon: 'edit-block',
|
|
|
|
|
onAction() {
|
|
|
|
|
editor.execCommand('codeeditor');
|
2023-04-19 05:20:02 +08:00
|
|
|
|
},
|
2022-10-15 22:47:34 +08:00
|
|
|
|
});
|
|
|
|
|
|
2022-02-06 07:15:58 +08:00
|
|
|
|
editor.addCommand('codeeditor', () => {
|
2022-02-10 03:24:27 +08:00
|
|
|
|
const selectedNode = editor.selection.getNode();
|
|
|
|
|
const doc = selectedNode.ownerDocument;
|
|
|
|
|
if (elemIsCodeBlock(selectedNode)) {
|
|
|
|
|
showPopupForCodeBlock(editor, selectedNode);
|
|
|
|
|
} else {
|
|
|
|
|
const textContent = editor.selection.getContent({format: 'text'});
|
2024-05-03 20:35:30 +08:00
|
|
|
|
const direction = document.dir === 'rtl' ? 'ltr' : '';
|
|
|
|
|
showPopup(editor, textContent, '', direction, (newCode, newLang) => {
|
2022-02-10 03:24:27 +08:00
|
|
|
|
const pre = doc.createElement('pre');
|
|
|
|
|
const code = doc.createElement('code');
|
|
|
|
|
code.classList.add(`language-${newLang}`);
|
|
|
|
|
code.innerText = newCode;
|
2024-05-03 20:35:30 +08:00
|
|
|
|
if (direction) {
|
|
|
|
|
pre.setAttribute('dir', direction);
|
|
|
|
|
}
|
2022-02-10 03:24:27 +08:00
|
|
|
|
|
2024-05-03 20:35:30 +08:00
|
|
|
|
pre.append(code);
|
2022-02-28 01:18:08 +08:00
|
|
|
|
editor.insertContent(pre.outerHTML);
|
2022-02-10 03:24:27 +08:00
|
|
|
|
});
|
|
|
|
|
}
|
2022-02-06 07:15:58 +08:00
|
|
|
|
});
|
|
|
|
|
|
2023-04-19 17:46:13 +08:00
|
|
|
|
editor.on('dblclick', () => {
|
2023-04-19 05:20:02 +08:00
|
|
|
|
const selectedNode = editor.selection.getNode();
|
2022-02-10 03:24:27 +08:00
|
|
|
|
if (elemIsCodeBlock(selectedNode)) {
|
|
|
|
|
showPopupForCodeBlock(editor, selectedNode);
|
|
|
|
|
}
|
2022-02-06 07:15:58 +08:00
|
|
|
|
});
|
|
|
|
|
|
2022-02-10 03:24:27 +08:00
|
|
|
|
editor.on('PreInit', () => {
|
2023-04-19 05:20:02 +08:00
|
|
|
|
editor.parser.addNodeFilter('pre', elms => {
|
2022-02-10 03:24:27 +08:00
|
|
|
|
for (const el of elms) {
|
2023-04-19 17:46:13 +08:00
|
|
|
|
const wrapper = window.tinymce.html.Node.create('code-block', {
|
2022-02-10 03:24:27 +08:00
|
|
|
|
contenteditable: 'false',
|
|
|
|
|
});
|
|
|
|
|
|
2024-05-03 20:35:30 +08:00
|
|
|
|
const childCodeBlock = el.children().filter(child => child.name === 'code')[0] || null;
|
|
|
|
|
const direction = el.attr('dir') || (childCodeBlock && childCodeBlock.attr('dir')) || '';
|
2024-02-19 01:55:56 +08:00
|
|
|
|
if (direction) {
|
|
|
|
|
wrapper.attr('dir', direction);
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-10 03:24:27 +08:00
|
|
|
|
const spans = el.getAll('span');
|
|
|
|
|
for (const span of spans) {
|
|
|
|
|
span.unwrap();
|
|
|
|
|
}
|
|
|
|
|
el.attr('style', null);
|
|
|
|
|
el.wrap(wrapper);
|
|
|
|
|
}
|
2022-02-06 07:15:58 +08:00
|
|
|
|
});
|
|
|
|
|
|
2023-04-19 05:20:02 +08:00
|
|
|
|
editor.parser.addNodeFilter('code-block', elms => {
|
2022-02-10 03:24:27 +08:00
|
|
|
|
for (const el of elms) {
|
2022-02-28 01:18:08 +08:00
|
|
|
|
el.attr('contenteditable', 'false');
|
2022-02-10 03:24:27 +08:00
|
|
|
|
}
|
2022-02-06 07:15:58 +08:00
|
|
|
|
});
|
|
|
|
|
|
2023-04-19 05:20:02 +08:00
|
|
|
|
editor.serializer.addNodeFilter('code-block', elms => {
|
2022-02-10 03:24:27 +08:00
|
|
|
|
for (const el of elms) {
|
2024-02-19 01:55:56 +08:00
|
|
|
|
const direction = el.attr('dir');
|
|
|
|
|
if (direction && el.firstChild) {
|
|
|
|
|
el.firstChild.attr('dir', direction);
|
|
|
|
|
} else if (el.firstChild) {
|
|
|
|
|
el.firstChild.attr('dir', null);
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-10 03:24:27 +08:00
|
|
|
|
el.unwrap();
|
|
|
|
|
}
|
2022-02-06 07:15:58 +08:00
|
|
|
|
});
|
2022-02-10 03:24:27 +08:00
|
|
|
|
});
|
2022-02-06 07:15:58 +08:00
|
|
|
|
|
2022-10-15 22:47:34 +08:00
|
|
|
|
editor.ui.registry.addContextToolbar('codeeditor', {
|
2023-04-19 05:20:02 +08:00
|
|
|
|
predicate(node) {
|
2022-10-15 22:47:34 +08:00
|
|
|
|
return node.nodeName.toLowerCase() === 'code-block';
|
|
|
|
|
},
|
|
|
|
|
items: 'editcodeeditor',
|
|
|
|
|
position: 'node',
|
2023-04-19 05:20:02 +08:00
|
|
|
|
scope: 'node',
|
2022-10-15 22:47:34 +08:00
|
|
|
|
});
|
|
|
|
|
|
2022-02-10 03:24:27 +08:00
|
|
|
|
editor.on('PreInit', () => {
|
|
|
|
|
defineCodeBlockCustomElement(editor);
|
2022-02-06 07:15:58 +08:00
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @return {register}
|
|
|
|
|
*/
|
2023-04-19 17:46:13 +08:00
|
|
|
|
export function getPlugin() {
|
2022-02-06 07:15:58 +08:00
|
|
|
|
return register;
|
2023-04-19 05:20:02 +08:00
|
|
|
|
}
|