import DrawIO from "../services/drawio"; export class Actions { /** * @param {MarkdownEditor} editor */ constructor(editor) { this.editor = editor; this.lastContent = { html: '', markdown: '', }; } updateAndRender() { const content = this.#getText(); this.editor.config.inputEl.value = content; const html = this.editor.markdown.render(content); window.$events.emit('editor-html-change', ''); window.$events.emit('editor-markdown-change', ''); this.lastContent.html = html; this.lastContent.markdown = content; this.editor.display.patchWithHtml(html); } getContent() { return this.lastContent; } showImageInsert() { /** @type {ImageManager} **/ const imageManager = window.$components.first('image-manager'); imageManager.show(image => { const imageUrl = image.thumbs.display || image.url; const selectedText = this.#getSelectionText(); const newText = "[](" + image.url + ")"; this.#replaceSelection(newText, newText.length); }, 'gallery'); } insertImage() { const newText = ``; this.#replaceSelection(newText, newText.length - 1); } insertLink() { const selectedText = this.#getSelectionText(); const newText = `[${selectedText}]()`; const cursorPosDiff = (selectedText === '') ? -3 : -1; this.#replaceSelection(newText, newText.length+cursorPosDiff); } showImageManager() { const selectionRange = this.#getSelectionRange(); /** @type {ImageManager} **/ const imageManager = window.$components.first('image-manager'); imageManager.show(image => { this.#insertDrawing(image, selectionRange); }, 'drawio'); } // Show the popup link selector and insert a link when finished showLinkSelector() { const selectionRange = this.#getSelectionRange(); /** @type {EntitySelectorPopup} **/ const selector = window.$components.first('entity-selector-popup'); selector.show(entity => { const selectedText = this.#getSelectionText(selectionRange) || entity.name; const newText = `[${selectedText}](${entity.link})`; this.#replaceSelection(newText, newText.length, selectionRange); }); } // Show draw.io if enabled and handle save. startDrawing() { const url = this.editor.config.drawioUrl; if (!url) return; const selectionRange = this.#getSelectionRange(); DrawIO.show(url,() => { return Promise.resolve(''); }, (pngData) => { const data = { image: pngData, uploaded_to: Number(this.editor.config.pageId), }; window.$http.post("/images/drawio", data).then(resp => { this.#insertDrawing(resp.data, selectionRange); DrawIO.close(); }).catch(err => { this.handleDrawingUploadError(err); }); }); } #insertDrawing(image, originalSelectionRange) { const newText = `
`, '
'); } else if (format === '') { 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); } } syncDisplayPosition(event) { // Thanks to http://liuhao.im/english/2015/11/10/the-sync-scroll-of-markdown-editor-in-javascript.html const scrollEl = event.target; const atEnd = Math.abs(scrollEl.scrollHeight - scrollEl.clientHeight - scrollEl.scrollTop) < 1; if (atEnd) { this.editor.display.scrollToIndex(-1); return; } 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 > *'); this.editor.display.scrollToIndex(totalLines.length); } /** * Fetch and insert the template of the given ID. * The page-relative position provided can be used to determine insert location if possible. * @param {String} templateId * @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); }); } /** * Insert multiple images from the clipboard. * @param {File[]} images */ insertClipboardImages(images) { // TODO const cursorPos = this.editor.cm.coordsChar({left: event.pageX, top: event.pageY}); this.editor.cm.setCursor(cursorPos); for (const image of images) { this.#uploadImage(image); } } /** * Handle image upload and add image into markdown content * @param {File} file */ #uploadImage(file) { // TODO if (file === null || file.type.indexOf('image') !== 0) return; let ext = 'png'; if (file.name) { let 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 placeholderImage = window.baseUrl(`/loading.gif#upload${id}`); const selectedText = this.editor.cm.getSelection(); const placeHolderText = ``; 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 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 = `[](${resp.data.url})`; this.#findAndReplaceContent(placeHolderText, newContent); }).catch(err => { window.$events.emit('error', this.editor.config.text.imageUploadError); this.#findAndReplaceContent(placeHolderText, selectedText); console.log(err); }); } /** * Get the current text of the editor instance. * @return {string} */ #getText() { return this.editor.cm.state.doc.toString(); } /** * Set the text of the current editor instance. * @param {String} text * @param {?SelectionRange} selectionRange */ #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}, }); this.focus(); } /** * Replace the current selection and focus the editor. * Takes an offset for the cursor, after the change, relative to the start of the provided string. * Can be provided a selection range to use instead of the current selection range. * @param {String} newContent * @param {Number} cursorOffset * @param {?SelectionRange} selectionRange */ #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}, }); this.focus(); } /** * Get the text content of the main current selection. * @param {SelectionRange} selectionRange * @return {string} */ #getSelectionText(selectionRange = null) { selectionRange = selectionRange || this.#getSelectionRange(); return this.editor.cm.state.sliceDoc(selectionRange.from, selectionRange.to); } /** * Get the range of the current main selection. * @return {SelectionRange} */ #getSelectionRange() { return this.editor.cm.state.selection.main; } /** * Cleans the given text to work with the editor. * Standardises line endings to what's expected. * @param {String} text * @return {String} */ #cleanTextForEditor(text) { return text.replace(/\r\n|\r/g, "\n"); } /** * Find and replace the first occurrence of [search] with [replace] * @param {String} search * @param {String} replace */ #findAndReplaceContent(search, replace) { const newText = this.#getText().replace(search, replace); this.#setText(newText); } /** * Wrap the line in the given start and end contents. * @param {String} start * @param {String} end */ #wrapLine(start, end) { const selectionRange = this.#getSelectionRange(); const line = this.editor.cm.state.doc.lineAt(selectionRange.from); const lineContent = line.text; let newLineContent; let lineOffset = 0; if (lineContent.startsWith(start) && lineContent.endsWith(end)) { newLineContent = lineContent.slice(start.length, lineContent.length - end.length); lineOffset = -(start.length); } else { newLineContent = `${start}${lineContent}${end}`; lineOffset = start.length; } this.editor.cm.dispatch({ changes: {from: line.from, to: line.to, insert: newLineContent}, selection: {anchor: selectionRange.from + lineOffset} }); } }