From 6f45d34bf8cba306477d5ab34ba2fdd8fbbe0d87 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Thu, 13 Apr 2023 17:18:32 +0100 Subject: [PATCH] Finished update pass of all md editor actions to cm6 --- resources/js/markdown/actions.js | 96 ++++++++++++++--------------- resources/js/markdown/codemirror.js | 69 ++++++++++----------- 2 files changed, 78 insertions(+), 87 deletions(-) diff --git a/resources/js/markdown/actions.js b/resources/js/markdown/actions.js index d140bb284..3aa6b5e81 100644 --- a/resources/js/markdown/actions.js +++ b/resources/js/markdown/actions.js @@ -296,10 +296,11 @@ export class Actions { } replaceLineStartForOrderedList() { - // TODO - const cursor = this.editor.cm.getCursor(); - const prevLineContent = this.editor.cm.getLine(cursor.line - 1) || ''; - const listMatch = prevLineContent.match(/^(\s*)(\d)([).])\s/) || []; + const selectionRange = this.#getSelectionRange(); + const line = this.editor.cm.state.doc.lineAt(selectionRange.from); + const prevLine = this.editor.cm.state.doc.line(line.number - 1); + + const listMatch = prevLine.text.match(/^(\s*)(\d)([).])\s/) || []; const number = (Number(listMatch[2]) || 0) + 1; const whiteSpace = listMatch[1] || ''; @@ -314,37 +315,28 @@ export class Actions { * Creates a callout block if none existing, and removes it if cycling past the danger type. */ cycleCalloutTypeAtSelection() { - // TODO - const selectionRange = this.editor.cm.listSelections()[0]; - const lineContent = this.editor.cm.getLine(selectionRange.anchor.line); - const lineLength = lineContent.length; - const contentRange = { - anchor: {line: selectionRange.anchor.line, ch: 0}, - head: {line: selectionRange.anchor.line, ch: lineLength}, - }; + const selectionRange = this.#getSelectionRange(); + const line = this.editor.cm.state.doc.lineAt(selectionRange.from); const formats = ['info', 'success', 'warning', 'danger']; const joint = formats.join('|'); const regex = new RegExp(`class="((${joint})\\s+callout|callout\\s+(${joint}))"`, 'i'); - const matches = regex.exec(lineContent); + const matches = regex.exec(line.text); const format = (matches ? (matches[2] || matches[3]) : '').toLowerCase(); if (format === formats[formats.length - 1]) { - this.wrapLine(`

`, '

'); + this.#wrapLine(`

`, '

'); } else if (format === '') { - this.wrapLine('

', '

'); + this.#wrapLine('

', '

'); } else { const newFormatIndex = formats.indexOf(format) + 1; const newFormat = formats[newFormatIndex]; - const newContent = lineContent.replace(matches[0], matches[0].replace(format, newFormat)); - this.editor.cm.replaceRange(newContent, contentRange.anchor, contentRange.head); - - const chDiff = newContent.length - lineContent.length; - selectionRange.anchor.ch += chDiff; - if (selectionRange.anchor !== selectionRange.head) { - selectionRange.head.ch += chDiff; - } - this.editor.cm.setSelection(selectionRange.anchor, selectionRange.head); + const newContent = line.text.replace(matches[0], matches[0].replace(format, newFormat)); + const lineDiff = newContent.length - line.text.length; + this.editor.cm.dispatch({ + changes: {from: line.from, to: line.to, insert: newContent}, + selection: {anchor: selectionRange.anchor + lineDiff, head: selectionRange.head + lineDiff}, + }); } } @@ -372,38 +364,43 @@ export class Actions { * @param {Number} posX * @param {Number} posY */ - insertTemplate(templateId, posX, posY) { - // TODO - const cursorPos = this.editor.cm.coordsChar({left: posX, top: posY}); - this.editor.cm.setCursor(cursorPos); - window.$http.get(`/templates/${templateId}`).then(resp => { - const content = resp.data.markdown || resp.data.html; - this.editor.cm.replaceSelection(content); + async insertTemplate(templateId, posX, posY) { + const cursorPos = this.editor.cm.posAtCoords({x: posX, y: posY}, false); + const {data} = await window.$http.get(`/templates/${templateId}`); + const content = data.markdown || data.html; + this.editor.cm.dispatch({ + changes: {from: cursorPos, to: cursorPos, insert: content}, + selection: {anchor: cursorPos}, }); } /** - * Insert multiple images from the clipboard. + * Insert multiple images from the clipboard from an event at the provided + * screen coordinates (Typically form a paste event). * @param {File[]} images + * @param {Number} posX + * @param {Number} posY */ - insertClipboardImages(images) { - // TODO - const cursorPos = this.editor.cm.coordsChar({left: event.pageX, top: event.pageY}); - this.editor.cm.setCursor(cursorPos); + insertClipboardImages(images, posX, posY) { + const cursorPos = this.editor.cm.posAtCoords({x: posX, y: posY}, false); for (const image of images) { - this.#uploadImage(image); + this.uploadImage(image, cursorPos); } } /** * Handle image upload and add image into markdown content * @param {File} file + * @param {?Number} position */ - #uploadImage(file) { - // TODO + async uploadImage(file, position= null) { if (file === null || file.type.indexOf('image') !== 0) return; let ext = 'png'; + if (position === null) { + position = this.#getSelectionRange().from; + } + if (file.name) { let fileNameMatches = file.name.match(/\.(.+)$/); if (fileNameMatches.length > 1) ext = fileNameMatches[1]; @@ -412,25 +409,26 @@ export class Actions { // Insert image into markdown const id = "image-" + Math.random().toString(16).slice(2); const placeholderImage = window.baseUrl(`/loading.gif#upload${id}`); - const selectedText = this.editor.cm.getSelection(); - const placeHolderText = `![${selectedText}](${placeholderImage})`; - const cursor = this.editor.cm.getCursor(); - this.editor.cm.replaceSelection(placeHolderText); - this.editor.cm.setCursor({line: cursor.line, ch: cursor.ch + selectedText.length + 3}); + const placeHolderText = `![](${placeholderImage})`; + this.editor.cm.dispatch({ + changes: {from: position, to: position, insert: placeHolderText}, + selection: {anchor: position}, + }); const remoteFilename = "image-" + Date.now() + "." + ext; const formData = new FormData(); formData.append('file', file, remoteFilename); formData.append('uploaded_to', this.editor.config.pageId); - window.$http.post('/images/gallery', formData).then(resp => { - const newContent = `[![${selectedText}](${resp.data.thumbs.display})](${resp.data.url})`; + try { + const {data} = await window.$http.post('/images/gallery', formData); + const newContent = `[![](${data.thumbs.display})](${data.url})`; this.#findAndReplaceContent(placeHolderText, newContent); - }).catch(err => { + } catch (err) { window.$events.emit('error', this.editor.config.text.imageUploadError); - this.#findAndReplaceContent(placeHolderText, selectedText); + this.#findAndReplaceContent(placeHolderText, ''); console.log(err); - }); + } } /** diff --git a/resources/js/markdown/codemirror.js b/resources/js/markdown/codemirror.js index cd620137d..dbf1925c0 100644 --- a/resources/js/markdown/codemirror.js +++ b/resources/js/markdown/codemirror.js @@ -25,7 +25,37 @@ export async function init(editor) { const domEventHandlers = { // Handle scroll to sync display view - scroll: (event) => syncActive && onScrollDebounced(event) + scroll: (event) => syncActive && onScrollDebounced(event), + // Handle image & content drag n drop + drop: (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, event.pageX, event.pageY); + } + }, + // Handle image paste + paste: (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); + } + } } const cm = Code.markdownEditor( @@ -40,42 +70,5 @@ export async function init(editor) { // TODO // cm.setOption('direction', 'ltr'); - - // Handle image paste - // 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 - // 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