mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-11-26 02:10:19 +08:00
f9e087330b
Editor popup will now reflect the direction of the opened code block. This also updates in-editor codemirror instances to correcly reflect/use the direction if set on the inner code elem. This also defaults new code blocks, when in RTL languages, to be started in LTR, which can then be changed via in-editor direction controls if needed. This is on the assumption that most code will be LTR (could not find much examples of RTL code use). Fixes #4943
224 lines
6.8 KiB
JavaScript
224 lines
6.8 KiB
JavaScript
import {EditorView, keymap} from '@codemirror/view';
|
|
|
|
import {copyTextToClipboard} from '../services/clipboard';
|
|
import {viewerExtensions, editorExtensions} from './setups';
|
|
import {createView} from './views';
|
|
import {SimpleEditorInterface} from './simple-editor-interface';
|
|
|
|
/**
|
|
* Add a button to a CodeMirror instance which copies the contents to the clipboard upon click.
|
|
* @param {EditorView} editorView
|
|
*/
|
|
function addCopyIcon(editorView) {
|
|
const copyIcon = '<svg viewBox="0 0 24 24" width="16" height="16" xmlns="http://www.w3.org/2000/svg"><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>';
|
|
const checkIcon = '<svg viewBox="0 0 24 24" width="16" height="16" xmlns="http://www.w3.org/2000/svg"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>';
|
|
const copyButton = document.createElement('button');
|
|
copyButton.setAttribute('type', 'button');
|
|
copyButton.classList.add('cm-copy-button');
|
|
copyButton.innerHTML = copyIcon;
|
|
editorView.dom.appendChild(copyButton);
|
|
|
|
const notifyTime = 620;
|
|
const transitionTime = 60;
|
|
copyButton.addEventListener('click', () => {
|
|
copyTextToClipboard(editorView.state.doc.toString());
|
|
copyButton.classList.add('success');
|
|
|
|
setTimeout(() => {
|
|
copyButton.innerHTML = checkIcon;
|
|
}, transitionTime / 2);
|
|
|
|
setTimeout(() => {
|
|
copyButton.classList.remove('success');
|
|
}, notifyTime);
|
|
|
|
setTimeout(() => {
|
|
copyButton.innerHTML = copyIcon;
|
|
}, notifyTime + (transitionTime / 2));
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @param {HTMLElement} codeElem
|
|
* @returns {String}
|
|
*/
|
|
function getDirectionFromCodeBlock(codeElem) {
|
|
let dir = '';
|
|
const innerCodeElem = codeElem.querySelector('code');
|
|
|
|
if (innerCodeElem && innerCodeElem.hasAttribute('dir')) {
|
|
dir = innerCodeElem.getAttribute('dir');
|
|
} else if (codeElem.hasAttribute('dir')) {
|
|
dir = codeElem.getAttribute('dir');
|
|
}
|
|
|
|
return dir;
|
|
}
|
|
|
|
/**
|
|
* Add code highlighting to a single element.
|
|
* @param {HTMLElement} elem
|
|
*/
|
|
function highlightElem(elem) {
|
|
const innerCodeElem = elem.querySelector('code[class^=language-]');
|
|
elem.innerHTML = elem.innerHTML.replace(/<br\s*\/?>/gi, '\n');
|
|
const content = elem.textContent.trimEnd();
|
|
|
|
let langName = '';
|
|
if (innerCodeElem !== null) {
|
|
langName = innerCodeElem.className.replace('language-', '');
|
|
}
|
|
|
|
const wrapper = document.createElement('div');
|
|
elem.parentNode.insertBefore(wrapper, elem);
|
|
|
|
const direction = getDirectionFromCodeBlock(elem);
|
|
if (direction) {
|
|
wrapper.setAttribute('dir', direction);
|
|
}
|
|
|
|
const ev = createView('content-code-block', {
|
|
parent: wrapper,
|
|
doc: content,
|
|
extensions: viewerExtensions(wrapper),
|
|
});
|
|
|
|
const editor = new SimpleEditorInterface(ev);
|
|
editor.setMode(langName, content);
|
|
|
|
elem.remove();
|
|
addCopyIcon(ev);
|
|
}
|
|
|
|
/**
|
|
* Highlight all code blocks within the given parent element
|
|
* @param {HTMLElement} parent
|
|
*/
|
|
export function highlightWithin(parent) {
|
|
const codeBlocks = parent.querySelectorAll('pre');
|
|
for (const codeBlock of codeBlocks) {
|
|
highlightElem(codeBlock);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Highlight pre elements on a page
|
|
*/
|
|
export function highlight() {
|
|
const codeBlocks = document.querySelectorAll('.page-content pre, .comment-box .content pre');
|
|
for (const codeBlock of codeBlocks) {
|
|
highlightElem(codeBlock);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a CodeMirror instance for showing inside the WYSIWYG editor.
|
|
* Manages a textarea element to hold code content.
|
|
* @param {HTMLElement} cmContainer
|
|
* @param {ShadowRoot} shadowRoot
|
|
* @param {String} content
|
|
* @param {String} language
|
|
* @returns {SimpleEditorInterface}
|
|
*/
|
|
export function wysiwygView(cmContainer, shadowRoot, content, language) {
|
|
const ev = createView('content-code-block', {
|
|
parent: cmContainer,
|
|
doc: content,
|
|
extensions: viewerExtensions(cmContainer),
|
|
root: shadowRoot,
|
|
});
|
|
|
|
const editor = new SimpleEditorInterface(ev);
|
|
editor.setMode(language, content);
|
|
|
|
return editor;
|
|
}
|
|
|
|
/**
|
|
* Create a CodeMirror instance to show in the WYSIWYG pop-up editor
|
|
* @param {HTMLElement} elem
|
|
* @param {String} modeSuggestion
|
|
* @returns {SimpleEditorInterface}
|
|
*/
|
|
export function popupEditor(elem, modeSuggestion) {
|
|
const content = elem.textContent;
|
|
const config = {
|
|
parent: elem.parentElement,
|
|
doc: content,
|
|
extensions: [
|
|
...editorExtensions(elem.parentElement),
|
|
],
|
|
};
|
|
|
|
// Create editor, hide original input
|
|
const editor = new SimpleEditorInterface(createView('code-editor', config));
|
|
editor.setMode(modeSuggestion, content);
|
|
elem.style.display = 'none';
|
|
|
|
return editor;
|
|
}
|
|
|
|
/**
|
|
* Create an inline editor to replace the given textarea.
|
|
* @param {HTMLTextAreaElement} textArea
|
|
* @param {String} mode
|
|
* @returns {SimpleEditorInterface}
|
|
*/
|
|
export function inlineEditor(textArea, mode) {
|
|
const content = textArea.value;
|
|
const config = {
|
|
parent: textArea.parentElement,
|
|
doc: content,
|
|
extensions: [
|
|
...editorExtensions(textArea.parentElement),
|
|
EditorView.updateListener.of(v => {
|
|
if (v.docChanged) {
|
|
textArea.value = v.state.doc.toString();
|
|
}
|
|
}),
|
|
],
|
|
};
|
|
|
|
// Create editor view, hide original input
|
|
const ev = createView('code-input', config);
|
|
const editor = new SimpleEditorInterface(ev);
|
|
editor.setMode(mode, content);
|
|
textArea.style.display = 'none';
|
|
|
|
return editor;
|
|
}
|
|
|
|
/**
|
|
* Get a CodeMirror instance to use for the markdown editor.
|
|
* @param {HTMLElement} elem
|
|
* @param {function} onChange
|
|
* @param {object} domEventHandlers
|
|
* @param {Array} keyBindings
|
|
* @returns {EditorView}
|
|
*/
|
|
export function markdownEditor(elem, onChange, domEventHandlers, keyBindings) {
|
|
const content = elem.textContent;
|
|
const config = {
|
|
parent: elem.parentElement,
|
|
doc: content,
|
|
extensions: [
|
|
keymap.of(keyBindings),
|
|
...editorExtensions(elem.parentElement),
|
|
EditorView.updateListener.of(v => {
|
|
onChange(v);
|
|
}),
|
|
EditorView.domEventHandlers(domEventHandlers),
|
|
],
|
|
};
|
|
|
|
// Emit a pre-event public event to allow tweaking of the configure before view creation.
|
|
window.$events.emitPublic(elem, 'editor-markdown-cm6::pre-init', {editorViewConfig: config});
|
|
|
|
// Create editor view, hide original input
|
|
const ev = createView('markdown-editor', config);
|
|
(new SimpleEditorInterface(ev)).setMode('markdown', '');
|
|
elem.style.display = 'none';
|
|
|
|
return ev;
|
|
}
|