import {EditorView, keymap} from '@codemirror/view'; import {copyTextToClipboard} from '../services/clipboard.ts'; 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 = ''; const checkIcon = ''; 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(//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; }