X-Git-Url: http://source.bookstackapp.com/bookstack/blobdiff_plain/6f45d34bf8cba306477d5ab34ba2fdd8fbbe0d87..refs/pull/5592/head:/resources/js/markdown/actions.js diff --git a/resources/js/markdown/actions.js b/resources/js/markdown/actions.js index 3aa6b5e81..e99bbf3e1 100644 --- a/resources/js/markdown/actions.js +++ b/resources/js/markdown/actions.js @@ -1,6 +1,7 @@ -import DrawIO from "../services/drawio"; +import * as DrawIO from '../services/drawio.ts'; export class Actions { + /** * @param {MarkdownEditor} editor */ @@ -29,13 +30,13 @@ export class Actions { } showImageInsert() { - /** @type {ImageManager} **/ + /** @type {ImageManager} * */ const imageManager = window.$components.first('image-manager'); imageManager.show(image => { - const imageUrl = image.thumbs.display || image.url; + const imageUrl = image.thumbs?.display || image.url; const selectedText = this.#getSelectionText(); - const newText = "[![" + (selectedText || image.name) + "](" + imageUrl + ")](" + image.url + ")"; + const newText = `[![${selectedText || image.name}](${imageUrl})](${image.url})`; this.#replaceSelection(newText, newText.length); }, 'gallery'); } @@ -49,12 +50,12 @@ export class Actions { const selectedText = this.#getSelectionText(); const newText = `[${selectedText}]()`; const cursorPosDiff = (selectedText === '') ? -3 : -1; - this.#replaceSelection(newText, newText.length+cursorPosDiff); + this.#replaceSelection(newText, newText.length + cursorPosDiff); } showImageManager() { const selectionRange = this.#getSelectionRange(); - /** @type {ImageManager} **/ + /** @type {ImageManager} * */ const imageManager = window.$components.first('image-manager'); imageManager.show(image => { this.#insertDrawing(image, selectionRange); @@ -65,12 +66,18 @@ export class Actions { showLinkSelector() { const selectionRange = this.#getSelectionRange(); - /** @type {EntitySelectorPopup} **/ + /** @type {EntitySelectorPopup} * */ const selector = window.$components.first('entity-selector-popup'); + const selectionText = this.#getSelectionText(selectionRange); selector.show(entity => { - const selectedText = this.#getSelectionText(selectionRange) || entity.name; + const selectedText = selectionText || entity.name; const newText = `[${selectedText}](${entity.link})`; this.#replaceSelection(newText, newText.length, selectionRange); + }, { + initialValue: selectionText, + searchEndpoint: '/search/entity-selector', + entityTypes: 'page,book,chapter,bookshelf', + entityPermission: 'view', }); } @@ -81,21 +88,20 @@ export class Actions { const selectionRange = this.#getSelectionRange(); - DrawIO.show(url,() => { - return Promise.resolve(''); - }, (pngData) => { - + DrawIO.show(url, () => Promise.resolve(''), async pngData => { const data = { image: pngData, uploaded_to: Number(this.editor.config.pageId), }; - window.$http.post("/images/drawio", data).then(resp => { + try { + const resp = await window.$http.post('/images/drawio', data); this.#insertDrawing(resp.data, selectionRange); DrawIO.close(); - }).catch(err => { + } catch (err) { this.handleDrawingUploadError(err); - }); + throw new Error(`Failed to save image with error: ${err}`); + } }); } @@ -106,7 +112,7 @@ export class Actions { // Show draw.io if enabled and handle save. editDrawing(imgContainer) { - const drawioUrl = this.editor.config.drawioUrl; + const {drawioUrl} = this.editor.config; if (!drawioUrl) { return; } @@ -114,16 +120,14 @@ export class Actions { const selectionRange = this.#getSelectionRange(); const drawingId = imgContainer.getAttribute('drawio-diagram'); - DrawIO.show(drawioUrl, () => { - return DrawIO.load(drawingId); - }, (pngData) => { - + DrawIO.show(drawioUrl, () => DrawIO.load(drawingId), async pngData => { const data = { image: pngData, uploaded_to: Number(this.editor.config.pageId), }; - window.$http.post("/images/drawio", data).then(resp => { + try { + const resp = await window.$http.post('/images/drawio', data); const newText = `
`; const newContent = this.#getText().split('\n').map(line => { if (line.indexOf(`drawio-diagram="${drawingId}"`) !== -1) { @@ -133,9 +137,10 @@ export class Actions { }).join('\n'); this.#setText(newContent, selectionRange); DrawIO.close(); - }).catch(err => { + } catch (err) { this.handleDrawingUploadError(err); - }); + throw new Error(`Failed to save image with error: ${err}`); + } }); } @@ -145,12 +150,12 @@ export class Actions { } else { window.$events.emit('error', this.editor.config.text.imageUploadError); } - console.log(error); + console.error(error); } // Make the editor full screen fullScreen() { - const container = this.editor.config.container; + const {container} = this.editor.config; const alreadyFullscreen = container.classList.contains('fullscreen'); container.classList.toggle('fullscreen', !alreadyFullscreen); document.body.classList.toggle('markdown-fullscreen', !alreadyFullscreen); @@ -170,7 +175,7 @@ export class Actions { scrollToLine = lineCount; break; } - lineCount++; + lineCount += 1; } if (scrollToLine === -1) { @@ -178,10 +183,7 @@ export class Actions { } const line = text.line(scrollToLine); - this.editor.cm.dispatch({ - selection: {anchor: line.from, head: line.to}, - scrollIntoView: true, - }); + this.#setSelection(line.from, line.to, true); this.focus(); } @@ -206,10 +208,8 @@ export class Actions { prependContent(content) { content = this.#cleanTextForEditor(content); const selectionRange = this.#getSelectionRange(); - this.editor.cm.dispatch({ - changes: {from: 0, to: 0, insert: content + '\n'}, - selection: {anchor: selectionRange.from + content.length + 1} - }); + const selectFrom = selectionRange.from + content.length + 1; + this.#dispatchChange(0, 0, `${content}\n`, selectFrom); this.focus(); } @@ -219,9 +219,7 @@ export class Actions { */ appendContent(content) { content = this.#cleanTextForEditor(content); - this.editor.cm.dispatch({ - changes: {from: this.editor.cm.state.doc.length, insert: '\n' + content}, - }); + this.#dispatchChange(this.editor.cm.state.doc.length, `\n${content}`); this.focus(); } @@ -230,7 +228,7 @@ export class Actions { * @param {String} content */ replaceContent(content) { - this.#setText(content) + this.#setText(content); } /** @@ -247,10 +245,8 @@ export class Actions { // Remove symbol if already set if (lineStart === newStart) { const newLineContent = lineContent.replace(`${newStart} `, ''); - this.editor.cm.dispatch({ - changes: {from: line.from, to: line.to, insert: newLineContent}, - selection: {anchor: selectionRange.from + (newLineContent.length - lineContent.length)} - }); + const selectFrom = selectionRange.from + (newLineContent.length - lineContent.length); + this.#dispatchChange(line.from, line.to, newLineContent, selectFrom); return; } @@ -259,13 +255,11 @@ export class Actions { if (alreadySymbol) { newLineContent = lineContent.replace(lineStart, newStart).trim(); } else if (newStart !== '') { - newLineContent = newStart + ' ' + lineContent; + newLineContent = `${newStart} ${lineContent}`; } - this.editor.cm.dispatch({ - changes: {from: line.from, to: line.to, insert: newLineContent}, - selection: {anchor: selectionRange.from + (newLineContent.length - lineContent.length)} - }); + const selectFrom = selectionRange.from + (newLineContent.length - lineContent.length); + this.#dispatchChange(line.from, line.to, newLineContent, selectFrom); } /** @@ -274,25 +268,31 @@ export class Actions { * @param {String} end */ wrapSelection(start, end) { - const selectionRange = this.#getSelectionRange(); - const selectionText = this.#getSelectionText(selectionRange); - if (!selectionText) return this.#wrapLine(start, end); + const selectRange = this.#getSelectionRange(); + const selectionText = this.#getSelectionText(selectRange); + if (!selectionText) { + this.#wrapLine(start, end); + return; + } let newSelectionText = selectionText; let newRange; if (selectionText.startsWith(start) && selectionText.endsWith(end)) { newSelectionText = selectionText.slice(start.length, selectionText.length - end.length); - newRange = selectionRange.extend(selectionRange.from, selectionRange.to - (start.length + end.length)); + newRange = selectRange.extend(selectRange.from, selectRange.to - (start.length + end.length)); } else { newSelectionText = `${start}${selectionText}${end}`; - newRange = selectionRange.extend(selectionRange.from, selectionRange.to + (start.length + end.length)); + newRange = selectRange.extend(selectRange.from, selectRange.to + (start.length + end.length)); } - this.editor.cm.dispatch({ - changes: {from: selectionRange.from, to: selectionRange.to, insert: newSelectionText}, - selection: {anchor: newRange.anchor, head: newRange.head}, - }); + this.#dispatchChange( + selectRange.from, + selectRange.to, + newSelectionText, + newRange.anchor, + newRange.head, + ); } replaceLineStartForOrderedList() { @@ -304,7 +304,7 @@ export class Actions { const number = (Number(listMatch[2]) || 0) + 1; const whiteSpace = listMatch[1] || ''; - const listMark = listMatch[3] || '.' + const listMark = listMatch[3] || '.'; const prefix = `${whiteSpace}${number}${listMark}`; return this.replaceLineStart(prefix); @@ -333,10 +333,13 @@ export class Actions { const newFormat = formats[newFormatIndex]; 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}, - }); + this.#dispatchChange( + line.from, + line.to, + newContent, + selectionRange.anchor + lineDiff, + selectionRange.head + lineDiff, + ); } } @@ -368,10 +371,7 @@ export class Actions { 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}, - }); + this.#dispatchChange(cursorPos, cursorPos, content, cursorPos); } /** @@ -393,7 +393,7 @@ export class Actions { * @param {File} file * @param {?Number} position */ - async uploadImage(file, position= null) { + async uploadImage(file, position = null) { if (file === null || file.type.indexOf('image') !== 0) return; let ext = 'png'; @@ -402,20 +402,17 @@ export class Actions { } if (file.name) { - let fileNameMatches = file.name.match(/\.(.+)$/); + const fileNameMatches = file.name.match(/\.(.+)$/); if (fileNameMatches.length > 1) ext = fileNameMatches[1]; } // Insert image into markdown - const id = "image-" + Math.random().toString(16).slice(2); + const id = `image-${Math.random().toString(16).slice(2)}`; const placeholderImage = window.baseUrl(`/loading.gif#upload${id}`); const placeHolderText = `![](${placeholderImage})`; - this.editor.cm.dispatch({ - changes: {from: position, to: position, insert: placeHolderText}, - selection: {anchor: position}, - }); + this.#dispatchChange(position, position, placeHolderText, position); - const remoteFilename = "image-" + Date.now() + "." + ext; + const remoteFilename = `image-${Date.now()}.${ext}`; const formData = new FormData(); formData.append('file', file, remoteFilename); formData.append('uploaded_to', this.editor.config.pageId); @@ -425,9 +422,9 @@ export class Actions { const newContent = `[![](${data.thumbs.display})](${data.url})`; this.#findAndReplaceContent(placeHolderText, newContent); } catch (err) { - window.$events.emit('error', this.editor.config.text.imageUploadError); + window.$events.error(err?.data?.message || this.editor.config.text.imageUploadError); this.#findAndReplaceContent(placeHolderText, ''); - console.log(err); + console.error(err); } } @@ -446,12 +443,14 @@ export class Actions { */ #setText(text, selectionRange = null) { selectionRange = selectionRange || this.#getSelectionRange(); - this.editor.cm.dispatch({ - changes: {from: 0, to: this.editor.cm.state.doc.length, insert: text}, - selection: {anchor: selectionRange.from}, - }); - + const newDoc = this.editor.cm.state.toText(text); + const newSelectFrom = Math.min(selectionRange.from, newDoc.length); + const scrollTop = this.editor.cm.scrollDOM.scrollTop; + this.#dispatchChange(0, this.editor.cm.state.doc.length, text, newSelectFrom); this.focus(); + window.requestAnimationFrame(() => { + this.editor.cm.scrollDOM.scrollTop = scrollTop; + }); } /** @@ -464,11 +463,8 @@ export class Actions { */ #replaceSelection(newContent, cursorOffset = 0, selectionRange = null) { selectionRange = selectionRange || this.editor.cm.state.selection.main; - this.editor.cm.dispatch({ - changes: {from: selectionRange.from, to: selectionRange.to, insert: newContent}, - selection: {anchor: selectionRange.from + cursorOffset}, - }); - + const selectFrom = selectionRange.from + cursorOffset; + this.#dispatchChange(selectionRange.from, selectionRange.to, newContent, selectFrom); this.focus(); } @@ -497,7 +493,7 @@ export class Actions { * @return {String} */ #cleanTextForEditor(text) { - return text.replace(/\r\n|\r/g, "\n"); + return text.replace(/\r\n|\r/g, '\n'); } /** @@ -530,9 +526,42 @@ export class Actions { lineOffset = start.length; } + this.#dispatchChange(line.from, line.to, newLineContent, selectionRange.from + lineOffset); + } + + /** + * Dispatch changes to the editor. + * @param {Number} from + * @param {?Number} to + * @param {?String} text + * @param {?Number} selectFrom + * @param {?Number} selectTo + */ + #dispatchChange(from, to = null, text = null, selectFrom = null, selectTo = null) { + const tr = {changes: {from, to, insert: text}}; + + if (selectFrom) { + tr.selection = {anchor: selectFrom}; + if (selectTo) { + tr.selection.head = selectTo; + } + } + + this.editor.cm.dispatch(tr); + } + + /** + * Set the current selection range. + * Optionally will scroll the new range into view. + * @param {Number} from + * @param {Number} to + * @param {Boolean} scrollIntoView + */ + #setSelection(from, to, scrollIntoView = false) { this.editor.cm.dispatch({ - changes: {from: line.from, to: line.to, insert: newLineContent}, - selection: {anchor: selectionRange.from + lineOffset} + selection: {anchor: from, head: to}, + scrollIntoView, }); } -} \ No newline at end of file + +}