diff --git a/package.json b/package.json index 6b6dbbe46..9ee57afa5 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "build:css:watch": "sass ./resources/sass:./public/dist --watch --embed-sources", "build:css:production": "sass ./resources/sass:./public/dist -s compressed", "build:js:dev": "node dev/build/esbuild.js", - "build:js:watch": "chokidar --initial \"./resources/**/*.js\" -c \"npm run build:js:dev\"", + "build:js:watch": "chokidar --initial \"./resources/**/*.js\" \"./resources/**/*.mjs\" -c \"npm run build:js:dev\"", "build:js:production": "node dev/build/esbuild.js production", "build": "npm-run-all --parallel build:*:dev", "production": "npm-run-all --parallel build:*:production", diff --git a/resources/js/code/index.mjs b/resources/js/code/index.mjs index 6ef659994..6cad052f4 100644 --- a/resources/js/code/index.mjs +++ b/resources/js/code/index.mjs @@ -2,7 +2,7 @@ import {EditorView} from "@codemirror/view" import Clipboard from "clipboard/dist/clipboard.min"; // Modes -import {viewer} from "./setups.js"; +import {viewer, editor} from "./setups.js"; import {createView, updateViewLanguage} from "./views.js"; /** @@ -180,25 +180,31 @@ export function updateLayout(cmInstance) { /** * Get a CodeMirror instance to use for the markdown editor. * @param {HTMLElement} elem + * @param {function} onChange + * @param {object} domEventHandlers * @returns {*} */ -export function markdownEditor(elem) { +export function markdownEditor(elem, onChange, domEventHandlers) { const content = elem.textContent; - const config = { - value: content, - mode: "markdown", - lineNumbers: true, - lineWrapping: true, - theme: getTheme(), - scrollPastEnd: true, - }; - window.$events.emitPublic(elem, 'editor-markdown-cm::pre-init', {config}); + // TODO - Change to pass something else that's useful, probably extension array? + // window.$events.emitPublic(elem, 'editor-markdown-cm::pre-init', {config}); - return CodeMirror(function (elt) { - elem.parentNode.insertBefore(elt, elem); - elem.style.display = 'none'; - }, config); + const ev = createView({ + parent: elem.parentNode, + doc: content, + extensions: [ + ...editor('markdown'), + EditorView.updateListener.of((v) => { + onChange(v); + }), + EditorView.domEventHandlers(domEventHandlers), + ], + }); + + elem.style.display = 'none'; + + return ev; } /** @@ -206,6 +212,7 @@ export function markdownEditor(elem) { * @returns {string} */ export function getMetaKey() { - let mac = CodeMirror.keyMap["default"] == CodeMirror.keyMap.macDefault; + // TODO - Redo, Is needed? No CodeMirror instance to use. + const mac = CodeMirror.keyMap["default"] == CodeMirror.keyMap.macDefault; return mac ? "Cmd" : "Ctrl"; } \ No newline at end of file diff --git a/resources/js/code/setups.js b/resources/js/code/setups.js index e1a150856..00366ee5e 100644 --- a/resources/js/code/setups.js +++ b/resources/js/code/setups.js @@ -1,12 +1,12 @@ -import {keymap, highlightSpecialChars, drawSelection, highlightActiveLine, dropCursor, +import {EditorView, keymap, highlightSpecialChars, drawSelection, highlightActiveLine, dropCursor, rectangularSelection, lineNumbers, highlightActiveLineGutter} from "@codemirror/view" -import {defaultHighlightStyle, syntaxHighlighting, bracketMatching, - foldKeymap} from "@codemirror/language" +import {syntaxHighlighting, bracketMatching} from "@codemirror/language" import {defaultKeymap, history, historyKeymap} from "@codemirror/commands" import {EditorState} from "@codemirror/state" import {defaultLight} from "./themes"; +import {getLanguageExtension} from "./languages"; export function viewer() { return [ @@ -23,8 +23,27 @@ export function viewer() { keymap.of([ ...defaultKeymap, ...historyKeymap, - ...foldKeymap, ]), EditorState.readOnly.of(true), ]; +} + +export function editor(language) { + return [ + lineNumbers(), + highlightActiveLineGutter(), + highlightSpecialChars(), + history(), + drawSelection(), + dropCursor(), + syntaxHighlighting(defaultLight, {fallback: true}), + bracketMatching(), + rectangularSelection(), + highlightActiveLine(), + keymap.of([ + ...defaultKeymap, + ...historyKeymap, + ]), + getLanguageExtension(language, ''), + ]; } \ No newline at end of file diff --git a/resources/js/components/markdown-editor.js b/resources/js/components/markdown-editor.js index 5cd92cae2..6b4682d1e 100644 --- a/resources/js/components/markdown-editor.js +++ b/resources/js/components/markdown-editor.js @@ -45,7 +45,8 @@ export class MarkdownEditor extends Component { window.$events.emitPublic(this.elem, 'editor-markdown::setup', { markdownIt: this.editor.markdown.getRenderer(), displayEl: this.display, - codeMirrorInstance: this.editor.cm, + // TODO + // codeMirrorInstance: this.editor.cm, }); } @@ -81,9 +82,10 @@ export class MarkdownEditor extends Component { }); // Refresh CodeMirror on container resize - const resizeDebounced = debounce(() => this.editor.cm.refresh(), 100, false); - const observer = new ResizeObserver(resizeDebounced); - observer.observe(this.elem); + // TODO + // const resizeDebounced = debounce(() => this.editor.cm.refresh(), 100, false); + // const observer = new ResizeObserver(resizeDebounced); + // observer.observe(this.elem); this.handleDividerDrag(); } @@ -102,7 +104,8 @@ export class MarkdownEditor extends Component { window.removeEventListener('pointerup', upListener); this.display.style.pointerEvents = null; document.body.style.userSelect = null; - this.editor.cm.refresh(); + // TODO + // this.editor.cm.refresh(); }; this.display.style.pointerEvents = 'none'; diff --git a/resources/js/markdown/actions.js b/resources/js/markdown/actions.js index 9faf43de3..666998723 100644 --- a/resources/js/markdown/actions.js +++ b/resources/js/markdown/actions.js @@ -13,7 +13,7 @@ export class Actions { } updateAndRender() { - const content = this.editor.cm.getValue(); + const content = this.editor.cm.state.doc.toString(); this.editor.config.inputEl.value = content; const html = this.editor.markdown.render(content); @@ -411,17 +411,17 @@ export class Actions { }); } - syncDisplayPosition() { + syncDisplayPosition(event) { // Thanks to http://liuhao.im/english/2015/11/10/the-sync-scroll-of-markdown-editor-in-javascript.html - const scroll = this.editor.cm.getScrollInfo(); - const atEnd = scroll.top + scroll.clientHeight === scroll.height; + const scrollEl = event.target; + const atEnd = Math.abs(scrollEl.scrollHeight - scrollEl.clientHeight - scrollEl.scrollTop) < 1; if (atEnd) { this.editor.display.scrollToIndex(-1); return; } - const lineNum = this.editor.cm.lineAtHeight(scroll.top, 'local'); - const range = this.editor.cm.getRange({line: 0, ch: null}, {line: lineNum, ch: null}); + const blockInfo = this.editor.cm.lineBlockAtHeight(scrollEl.scrollTop); + const range = this.editor.cm.state.sliceDoc(0, blockInfo.from); const parser = new DOMParser(); const doc = parser.parseFromString(this.editor.markdown.render(range), 'text/html'); const totalLines = doc.documentElement.querySelectorAll('body > *'); diff --git a/resources/js/markdown/codemirror.js b/resources/js/markdown/codemirror.js index 8724a23c8..dad999e7a 100644 --- a/resources/js/markdown/codemirror.js +++ b/resources/js/markdown/codemirror.js @@ -9,62 +9,71 @@ import Clipboard from "../services/clipboard"; */ export async function init(editor) { const Code = await window.importVersioned('code'); - const cm = Code.markdownEditor(editor.config.inputEl); - // Will force to remain as ltr for now due to issues when HTML is in editor. - cm.setOption('direction', 'ltr'); - // Register shortcuts - cm.setOption('extraKeys', provideShortcuts(editor, Code.getMetaKey())); + /** + * @param {ViewUpdate} v + */ + function onViewUpdate(v) { + if (v.docChanged) { + editor.actions.updateAndRender(); + } + } - - // Register codemirror events - - // Update data on content change - cm.on('change', (instance, changeObj) => editor.actions.updateAndRender()); - - // Handle scroll to sync display view const onScrollDebounced = debounce(editor.actions.syncDisplayPosition.bind(editor.actions), 100, false); let syncActive = editor.settings.get('scrollSync'); editor.settings.onChange('scrollSync', val => syncActive = val); - cm.on('scroll', instance => { - if (syncActive) { - onScrollDebounced(instance); - } - }); + + const domEventHandlers = { + // Handle scroll to sync display view + scroll: (event) => syncActive && onScrollDebounced(event) + } + + const cm = Code.markdownEditor(editor.config.inputEl, onViewUpdate, domEventHandlers); + window.cm = cm; + + // Will force to remain as ltr for now due to issues when HTML is in editor. + // TODO + // cm.setOption('direction', 'ltr'); + // Register shortcuts + // TODO + // cm.setOption('extraKeys', provideShortcuts(editor, Code.getMetaKey())); + // Handle image paste - cm.on('paste', (cm, event) => { - const clipboard = new Clipboard(event.clipboardData || event.dataTransfer); - - // Don't handle the event ourselves if no items exist of contains table-looking data - if (!clipboard.hasItems() || clipboard.containsTabularData()) { - return; - } - - const images = clipboard.getImages(); - for (const image of images) { - editor.actions.uploadImage(image); - } - }); + // TODO + // cm.on('paste', (cm, event) => { + // const clipboard = new Clipboard(event.clipboardData || event.dataTransfer); + // + // // Don't handle the event ourselves if no items exist of contains table-looking data + // if (!clipboard.hasItems() || clipboard.containsTabularData()) { + // return; + // } + // + // const images = clipboard.getImages(); + // for (const image of images) { + // editor.actions.uploadImage(image); + // } + // }); // Handle image & content drag n drop - cm.on('drop', (cm, event) => { - - const templateId = event.dataTransfer.getData('bookstack/template'); - if (templateId) { - event.preventDefault(); - editor.actions.insertTemplate(templateId, event.pageX, event.pageY); - } - - const clipboard = new Clipboard(event.dataTransfer); - const clipboardImages = clipboard.getImages(); - if (clipboardImages.length > 0) { - event.stopPropagation(); - event.preventDefault(); - editor.actions.insertClipboardImages(clipboardImages); - } - - }); + // TODO + // cm.on('drop', (cm, event) => { + // + // const templateId = event.dataTransfer.getData('bookstack/template'); + // if (templateId) { + // event.preventDefault(); + // editor.actions.insertTemplate(templateId, event.pageX, event.pageY); + // } + // + // const clipboard = new Clipboard(event.dataTransfer); + // const clipboardImages = clipboard.getImages(); + // if (clipboardImages.length > 0) { + // event.stopPropagation(); + // event.preventDefault(); + // editor.actions.insertClipboardImages(clipboardImages); + // } + // + // }); return cm; } \ No newline at end of file diff --git a/resources/sass/_forms.scss b/resources/sass/_forms.scss index b7fc52f7d..b7b1b6d4d 100644 --- a/resources/sass/_forms.scss +++ b/resources/sass/_forms.scss @@ -82,6 +82,10 @@ flex-grow: 0; } +.markdown-editor-wrap .cm-editor { + flex: 1; +} + .markdown-panel-divider { width: 2px; @include lightDark(background-color, #ddd, #000);