mirror of
				https://github.com/BookStackApp/BookStack.git
				synced 2025-11-03 02:13:16 +03:00 
			
		
		
		
	MD Editor: Updated actions to use input interface
This commit is contained in:
		@@ -1,7 +1,7 @@
 | 
				
			|||||||
import * as DrawIO from '../services/drawio';
 | 
					import * as DrawIO from '../services/drawio';
 | 
				
			||||||
import {MarkdownEditor} from "./index.mjs";
 | 
					import {MarkdownEditor} from "./index.mjs";
 | 
				
			||||||
import {EntitySelectorPopup, ImageManager} from "../components";
 | 
					import {EntitySelectorPopup, ImageManager} from "../components";
 | 
				
			||||||
import {ChangeSpec, SelectionRange, TransactionSpec} from "@codemirror/state";
 | 
					import {MarkdownEditorInputSelection} from "./inputs/interface";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface ImageManagerImage {
 | 
					interface ImageManagerImage {
 | 
				
			||||||
    id: number;
 | 
					    id: number;
 | 
				
			||||||
@@ -23,7 +23,7 @@ export class Actions {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    updateAndRender() {
 | 
					    updateAndRender() {
 | 
				
			||||||
        const content = this.#getText();
 | 
					        const content = this.editor.input.getText();
 | 
				
			||||||
        this.editor.config.inputEl.value = content;
 | 
					        this.editor.config.inputEl.value = content;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const html = this.editor.markdown.render(content);
 | 
					        const html = this.editor.markdown.render(content);
 | 
				
			||||||
@@ -43,26 +43,26 @@ export class Actions {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        imageManager.show((image: ImageManagerImage) => {
 | 
					        imageManager.show((image: ImageManagerImage) => {
 | 
				
			||||||
            const imageUrl = image.thumbs?.display || image.url;
 | 
					            const imageUrl = image.thumbs?.display || image.url;
 | 
				
			||||||
            const selectedText = this.#getSelectionText();
 | 
					            const selectedText = this.editor.input.getSelectionText();
 | 
				
			||||||
            const newText = `[](${image.url})`;
 | 
					            const newText = `[](${image.url})`;
 | 
				
			||||||
            this.#replaceSelection(newText, newText.length);
 | 
					            this.#replaceSelection(newText, newText.length);
 | 
				
			||||||
        }, 'gallery');
 | 
					        }, 'gallery');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    insertImage() {
 | 
					    insertImage() {
 | 
				
			||||||
        const newText = ``;
 | 
					        const newText = ``;
 | 
				
			||||||
        this.#replaceSelection(newText, newText.length - 1);
 | 
					        this.#replaceSelection(newText, newText.length - 1);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    insertLink() {
 | 
					    insertLink() {
 | 
				
			||||||
        const selectedText = this.#getSelectionText();
 | 
					        const selectedText = this.editor.input.getSelectionText();
 | 
				
			||||||
        const newText = `[${selectedText}]()`;
 | 
					        const newText = `[${selectedText}]()`;
 | 
				
			||||||
        const cursorPosDiff = (selectedText === '') ? -3 : -1;
 | 
					        const cursorPosDiff = (selectedText === '') ? -3 : -1;
 | 
				
			||||||
        this.#replaceSelection(newText, newText.length + cursorPosDiff);
 | 
					        this.#replaceSelection(newText, newText.length + cursorPosDiff);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    showImageManager() {
 | 
					    showImageManager() {
 | 
				
			||||||
        const selectionRange = this.#getSelectionRange();
 | 
					        const selectionRange = this.editor.input.getSelection();
 | 
				
			||||||
        const imageManager = window.$components.first('image-manager') as ImageManager;
 | 
					        const imageManager = window.$components.first('image-manager') as ImageManager;
 | 
				
			||||||
        imageManager.show((image: ImageManagerImage) => {
 | 
					        imageManager.show((image: ImageManagerImage) => {
 | 
				
			||||||
            this.#insertDrawing(image, selectionRange);
 | 
					            this.#insertDrawing(image, selectionRange);
 | 
				
			||||||
@@ -71,10 +71,10 @@ export class Actions {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    // Show the popup link selector and insert a link when finished
 | 
					    // Show the popup link selector and insert a link when finished
 | 
				
			||||||
    showLinkSelector() {
 | 
					    showLinkSelector() {
 | 
				
			||||||
        const selectionRange = this.#getSelectionRange();
 | 
					        const selectionRange = this.editor.input.getSelection();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const selector = window.$components.first('entity-selector-popup') as EntitySelectorPopup;
 | 
					        const selector = window.$components.first('entity-selector-popup') as EntitySelectorPopup;
 | 
				
			||||||
        const selectionText = this.#getSelectionText(selectionRange);
 | 
					        const selectionText = this.editor.input.getSelectionText(selectionRange);
 | 
				
			||||||
        selector.show(entity => {
 | 
					        selector.show(entity => {
 | 
				
			||||||
            const selectedText = selectionText || entity.name;
 | 
					            const selectedText = selectionText || entity.name;
 | 
				
			||||||
            const newText = `[${selectedText}](${entity.link})`;
 | 
					            const newText = `[${selectedText}](${entity.link})`;
 | 
				
			||||||
@@ -92,7 +92,7 @@ export class Actions {
 | 
				
			|||||||
        const url = this.editor.config.drawioUrl;
 | 
					        const url = this.editor.config.drawioUrl;
 | 
				
			||||||
        if (!url) return;
 | 
					        if (!url) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const selectionRange = this.#getSelectionRange();
 | 
					        const selectionRange = this.editor.input.getSelection();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        DrawIO.show(url, () => Promise.resolve(''), async pngData => {
 | 
					        DrawIO.show(url, () => Promise.resolve(''), async pngData => {
 | 
				
			||||||
            const data = {
 | 
					            const data = {
 | 
				
			||||||
@@ -111,7 +111,7 @@ export class Actions {
 | 
				
			|||||||
        });
 | 
					        });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    #insertDrawing(image: ImageManagerImage, originalSelectionRange: SelectionRange) {
 | 
					    #insertDrawing(image: ImageManagerImage, originalSelectionRange: MarkdownEditorInputSelection) {
 | 
				
			||||||
        const newText = `<div drawio-diagram="${image.id}"><img src="${image.url}"></div>`;
 | 
					        const newText = `<div drawio-diagram="${image.id}"><img src="${image.url}"></div>`;
 | 
				
			||||||
        this.#replaceSelection(newText, newText.length, originalSelectionRange);
 | 
					        this.#replaceSelection(newText, newText.length, originalSelectionRange);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -123,7 +123,7 @@ export class Actions {
 | 
				
			|||||||
            return;
 | 
					            return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const selectionRange = this.#getSelectionRange();
 | 
					        const selectionRange = this.editor.input.getSelection();
 | 
				
			||||||
        const drawingId = imgContainer.getAttribute('drawio-diagram') || '';
 | 
					        const drawingId = imgContainer.getAttribute('drawio-diagram') || '';
 | 
				
			||||||
        if (!drawingId) {
 | 
					        if (!drawingId) {
 | 
				
			||||||
            return;
 | 
					            return;
 | 
				
			||||||
@@ -139,13 +139,13 @@ export class Actions {
 | 
				
			|||||||
                const resp = await window.$http.post('/images/drawio', data);
 | 
					                const resp = await window.$http.post('/images/drawio', data);
 | 
				
			||||||
                const image = resp.data as ImageManagerImage;
 | 
					                const image = resp.data as ImageManagerImage;
 | 
				
			||||||
                const newText = `<div drawio-diagram="${image.id}"><img src="${image.url}"></div>`;
 | 
					                const newText = `<div drawio-diagram="${image.id}"><img src="${image.url}"></div>`;
 | 
				
			||||||
                const newContent = this.#getText().split('\n').map(line => {
 | 
					                const newContent = this.editor.input.getText().split('\n').map(line => {
 | 
				
			||||||
                    if (line.indexOf(`drawio-diagram="${drawingId}"`) !== -1) {
 | 
					                    if (line.indexOf(`drawio-diagram="${drawingId}"`) !== -1) {
 | 
				
			||||||
                        return newText;
 | 
					                        return newText;
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                    return line;
 | 
					                    return line;
 | 
				
			||||||
                }).join('\n');
 | 
					                }).join('\n');
 | 
				
			||||||
                this.#setText(newContent, selectionRange);
 | 
					                this.editor.input.setText(newContent, selectionRange);
 | 
				
			||||||
                DrawIO.close();
 | 
					                DrawIO.close();
 | 
				
			||||||
            } catch (err) {
 | 
					            } catch (err) {
 | 
				
			||||||
                this.handleDrawingUploadError(err);
 | 
					                this.handleDrawingUploadError(err);
 | 
				
			||||||
@@ -177,30 +177,15 @@ export class Actions {
 | 
				
			|||||||
            return;
 | 
					            return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const text = this.editor.cm.state.doc;
 | 
					        const lineRange = this.editor.input.searchForLineContaining(searchText);
 | 
				
			||||||
        let lineCount = 1;
 | 
					        if (lineRange) {
 | 
				
			||||||
        let scrollToLine = -1;
 | 
					            this.editor.input.setSelection(lineRange, true);
 | 
				
			||||||
        for (const line of text.iterLines()) {
 | 
					            this.editor.input.focus();
 | 
				
			||||||
            if (line.includes(searchText)) {
 | 
					 | 
				
			||||||
                scrollToLine = lineCount;
 | 
					 | 
				
			||||||
                break;
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
            lineCount += 1;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (scrollToLine === -1) {
 | 
					 | 
				
			||||||
            return;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const line = text.line(scrollToLine);
 | 
					 | 
				
			||||||
        this.#setSelection(line.from, line.to, true);
 | 
					 | 
				
			||||||
        this.focus();
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    focus() {
 | 
					    focus() {
 | 
				
			||||||
        if (!this.editor.cm.hasFocus) {
 | 
					        this.editor.input.focus();
 | 
				
			||||||
            this.editor.cm.focus();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
@@ -215,10 +200,10 @@ export class Actions {
 | 
				
			|||||||
     */
 | 
					     */
 | 
				
			||||||
    prependContent(content: string): void {
 | 
					    prependContent(content: string): void {
 | 
				
			||||||
        content = this.#cleanTextForEditor(content);
 | 
					        content = this.#cleanTextForEditor(content);
 | 
				
			||||||
        const selectionRange = this.#getSelectionRange();
 | 
					        const selectionRange = this.editor.input.getSelection();
 | 
				
			||||||
        const selectFrom = selectionRange.from + content.length + 1;
 | 
					        const selectFrom = selectionRange.from + content.length + 1;
 | 
				
			||||||
        this.#dispatchChange(0, 0, `${content}\n`, selectFrom);
 | 
					        this.editor.input.spliceText(0, 0, `${content}\n`, {from: selectFrom});
 | 
				
			||||||
        this.focus();
 | 
					        this.editor.input.focus();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
@@ -226,16 +211,15 @@ export class Actions {
 | 
				
			|||||||
     */
 | 
					     */
 | 
				
			||||||
    appendContent(content: string): void {
 | 
					    appendContent(content: string): void {
 | 
				
			||||||
        content = this.#cleanTextForEditor(content);
 | 
					        content = this.#cleanTextForEditor(content);
 | 
				
			||||||
        const end = this.editor.cm.state.doc.length;
 | 
					        this.editor.input.appendText(content);
 | 
				
			||||||
        this.#dispatchChange(end, end, `\n${content}`);
 | 
					        this.editor.input.focus();
 | 
				
			||||||
        this.focus();
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Replace the editor's contents
 | 
					     * Replace the editor's contents
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    replaceContent(content: string): void {
 | 
					    replaceContent(content: string): void {
 | 
				
			||||||
        this.#setText(content);
 | 
					        this.editor.input.setText(content);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
@@ -243,17 +227,16 @@ export class Actions {
 | 
				
			|||||||
     * @param {String} newStart
 | 
					     * @param {String} newStart
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    replaceLineStart(newStart: string): void {
 | 
					    replaceLineStart(newStart: string): void {
 | 
				
			||||||
        const selectionRange = this.#getSelectionRange();
 | 
					        const selectionRange = this.editor.input.getSelection();
 | 
				
			||||||
        const line = this.editor.cm.state.doc.lineAt(selectionRange.from);
 | 
					        const lineRange = this.editor.input.getLineRangeFromPosition(selectionRange.from);
 | 
				
			||||||
 | 
					        const lineContent = this.editor.input.getSelectionText(lineRange);
 | 
				
			||||||
        const lineContent = line.text;
 | 
					 | 
				
			||||||
        const lineStart = lineContent.split(' ')[0];
 | 
					        const lineStart = lineContent.split(' ')[0];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Remove symbol if already set
 | 
					        // Remove symbol if already set
 | 
				
			||||||
        if (lineStart === newStart) {
 | 
					        if (lineStart === newStart) {
 | 
				
			||||||
            const newLineContent = lineContent.replace(`${newStart} `, '');
 | 
					            const newLineContent = lineContent.replace(`${newStart} `, '');
 | 
				
			||||||
            const selectFrom = selectionRange.from + (newLineContent.length - lineContent.length);
 | 
					            const selectFrom = selectionRange.from + (newLineContent.length - lineContent.length);
 | 
				
			||||||
            this.#dispatchChange(line.from, line.to, newLineContent, selectFrom);
 | 
					            this.editor.input.spliceText(selectionRange.from, selectionRange.to, newLineContent, {from: selectFrom});
 | 
				
			||||||
            return;
 | 
					            return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -266,46 +249,46 @@ export class Actions {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const selectFrom = selectionRange.from + (newLineContent.length - lineContent.length);
 | 
					        const selectFrom = selectionRange.from + (newLineContent.length - lineContent.length);
 | 
				
			||||||
        this.#dispatchChange(line.from, line.to, newLineContent, selectFrom);
 | 
					        this.editor.input.spliceText(lineRange.from, lineRange.to, newLineContent, {from: selectFrom});
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Wrap the selection in the given contents start and end contents.
 | 
					     * Wrap the selection in the given contents start and end contents.
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    wrapSelection(start: string, end: string): void {
 | 
					    wrapSelection(start: string, end: string): void {
 | 
				
			||||||
        const selectRange = this.#getSelectionRange();
 | 
					        const selectRange = this.editor.input.getSelection();
 | 
				
			||||||
        const selectionText = this.#getSelectionText(selectRange);
 | 
					        const selectionText = this.editor.input.getSelectionText(selectRange);
 | 
				
			||||||
        if (!selectionText) {
 | 
					        if (!selectionText) {
 | 
				
			||||||
            this.#wrapLine(start, end);
 | 
					            this.#wrapLine(start, end);
 | 
				
			||||||
            return;
 | 
					            return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let newSelectionText = selectionText;
 | 
					        let newSelectionText: string;
 | 
				
			||||||
        let newRange;
 | 
					        let newRange = {from: selectRange.from, to: selectRange.to};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (selectionText.startsWith(start) && selectionText.endsWith(end)) {
 | 
					        if (selectionText.startsWith(start) && selectionText.endsWith(end)) {
 | 
				
			||||||
            newSelectionText = selectionText.slice(start.length, selectionText.length - end.length);
 | 
					            newSelectionText = selectionText.slice(start.length, selectionText.length - end.length);
 | 
				
			||||||
            newRange = selectRange.extend(selectRange.from, selectRange.to - (start.length + end.length));
 | 
					            newRange.to = selectRange.to - (start.length + end.length);
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            newSelectionText = `${start}${selectionText}${end}`;
 | 
					            newSelectionText = `${start}${selectionText}${end}`;
 | 
				
			||||||
            newRange = selectRange.extend(selectRange.from, selectRange.to + (start.length + end.length));
 | 
					            newRange.to = selectRange.to + (start.length + end.length);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.#dispatchChange(
 | 
					        this.editor.input.spliceText(
 | 
				
			||||||
            selectRange.from,
 | 
					            selectRange.from,
 | 
				
			||||||
            selectRange.to,
 | 
					            selectRange.to,
 | 
				
			||||||
            newSelectionText,
 | 
					            newSelectionText,
 | 
				
			||||||
            newRange.anchor,
 | 
					            newRange,
 | 
				
			||||||
            newRange.head,
 | 
					 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    replaceLineStartForOrderedList() {
 | 
					    replaceLineStartForOrderedList() {
 | 
				
			||||||
        const selectionRange = this.#getSelectionRange();
 | 
					        const selectionRange = this.editor.input.getSelection();
 | 
				
			||||||
        const line = this.editor.cm.state.doc.lineAt(selectionRange.from);
 | 
					        const lineRange = this.editor.input.getLineRangeFromPosition(selectionRange.from);
 | 
				
			||||||
        const prevLine = this.editor.cm.state.doc.line(line.number - 1);
 | 
					        const prevLineRange = this.editor.input.getLineRangeFromPosition(lineRange.from - 1);
 | 
				
			||||||
 | 
					        const prevLineText = this.editor.input.getSelectionText(prevLineRange);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const listMatch = prevLine.text.match(/^(\s*)(\d)([).])\s/) || [];
 | 
					        const listMatch = prevLineText.match(/^(\s*)(\d)([).])\s/) || [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const number = (Number(listMatch[2]) || 0) + 1;
 | 
					        const number = (Number(listMatch[2]) || 0) + 1;
 | 
				
			||||||
        const whiteSpace = listMatch[1] || '';
 | 
					        const whiteSpace = listMatch[1] || '';
 | 
				
			||||||
@@ -320,30 +303,32 @@ export class Actions {
 | 
				
			|||||||
     * Creates a callout block if none existing, and removes it if cycling past the danger type.
 | 
					     * Creates a callout block if none existing, and removes it if cycling past the danger type.
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    cycleCalloutTypeAtSelection() {
 | 
					    cycleCalloutTypeAtSelection() {
 | 
				
			||||||
        const selectionRange = this.#getSelectionRange();
 | 
					        const selectionRange = this.editor.input.getSelection();
 | 
				
			||||||
        const line = this.editor.cm.state.doc.lineAt(selectionRange.from);
 | 
					        const lineRange = this.editor.input.getLineRangeFromPosition(selectionRange.from);
 | 
				
			||||||
 | 
					        const lineText = this.editor.input.getSelectionText(lineRange);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const formats = ['info', 'success', 'warning', 'danger'];
 | 
					        const formats = ['info', 'success', 'warning', 'danger'];
 | 
				
			||||||
        const joint = formats.join('|');
 | 
					        const joint = formats.join('|');
 | 
				
			||||||
        const regex = new RegExp(`class="((${joint})\\s+callout|callout\\s+(${joint}))"`, 'i');
 | 
					        const regex = new RegExp(`class="((${joint})\\s+callout|callout\\s+(${joint}))"`, 'i');
 | 
				
			||||||
        const matches = regex.exec(line.text) || [''];
 | 
					        const matches = regex.exec(lineText);
 | 
				
			||||||
        const format = (matches ? (matches[2] || matches[3]) : '').toLowerCase();
 | 
					        const format = (matches ? (matches[2] || matches[3]) : '').toLowerCase();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (format === formats[formats.length - 1]) {
 | 
					        if (format === formats[formats.length - 1]) {
 | 
				
			||||||
            this.#wrapLine(`<p class="callout ${formats[formats.length - 1]}">`, '</p>');
 | 
					            this.#wrapLine(`<p class="callout ${formats[formats.length - 1]}">`, '</p>');
 | 
				
			||||||
        } else if (format === '') {
 | 
					        } else if (format === '') {
 | 
				
			||||||
            this.#wrapLine('<p class="callout info">', '</p>');
 | 
					            this.#wrapLine('<p class="callout info">', '</p>');
 | 
				
			||||||
        } else {
 | 
					        } else if (matches) {
 | 
				
			||||||
            const newFormatIndex = formats.indexOf(format) + 1;
 | 
					            const newFormatIndex = formats.indexOf(format) + 1;
 | 
				
			||||||
            const newFormat = formats[newFormatIndex];
 | 
					            const newFormat = formats[newFormatIndex];
 | 
				
			||||||
            const newContent = line.text.replace(matches[0], matches[0].replace(format, newFormat));
 | 
					            const newContent = lineText.replace(matches[0], matches[0].replace(format, newFormat));
 | 
				
			||||||
            const lineDiff = newContent.length - line.text.length;
 | 
					            const lineDiff = newContent.length - lineText.length;
 | 
				
			||||||
            this.#dispatchChange(
 | 
					            const anchor = Math.min(selectionRange.from, selectionRange.to);
 | 
				
			||||||
                line.from,
 | 
					            const head = Math.max(selectionRange.from, selectionRange.to);
 | 
				
			||||||
                line.to,
 | 
					            this.editor.input.spliceText(
 | 
				
			||||||
 | 
					                lineRange.from,
 | 
				
			||||||
 | 
					                lineRange.to,
 | 
				
			||||||
                newContent,
 | 
					                newContent,
 | 
				
			||||||
                selectionRange.anchor + lineDiff,
 | 
					                {from: anchor + lineDiff, to: head + lineDiff}
 | 
				
			||||||
                selectionRange.head + lineDiff,
 | 
					 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -357,8 +342,7 @@ export class Actions {
 | 
				
			|||||||
            return;
 | 
					            return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const blockInfo = this.editor.cm.lineBlockAtHeight(scrollEl.scrollTop);
 | 
					        const range = this.editor.input.getTextAboveView();
 | 
				
			||||||
        const range = this.editor.cm.state.sliceDoc(0, blockInfo.from);
 | 
					 | 
				
			||||||
        const parser = new DOMParser();
 | 
					        const parser = new DOMParser();
 | 
				
			||||||
        const doc = parser.parseFromString(this.editor.markdown.render(range), 'text/html');
 | 
					        const doc = parser.parseFromString(this.editor.markdown.render(range), 'text/html');
 | 
				
			||||||
        const totalLines = doc.documentElement.querySelectorAll('body > *');
 | 
					        const totalLines = doc.documentElement.querySelectorAll('body > *');
 | 
				
			||||||
@@ -370,10 +354,10 @@ export class Actions {
 | 
				
			|||||||
     * The page-relative position provided can be used to determine insert location if possible.
 | 
					     * The page-relative position provided can be used to determine insert location if possible.
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    async insertTemplate(templateId: string, posX: number, posY: number): Promise<void> {
 | 
					    async insertTemplate(templateId: string, posX: number, posY: number): Promise<void> {
 | 
				
			||||||
        const cursorPos = this.editor.cm.posAtCoords({x: posX, y: posY}, false);
 | 
					        const cursorPos = this.editor.input.coordsToSelection(posX, posY).from;
 | 
				
			||||||
        const responseData = (await window.$http.get(`/templates/${templateId}`)).data as {markdown: string, html: string};
 | 
					        const responseData = (await window.$http.get(`/templates/${templateId}`)).data as {markdown: string, html: string};
 | 
				
			||||||
        const content = responseData.markdown || responseData.html;
 | 
					        const content = responseData.markdown || responseData.html;
 | 
				
			||||||
        this.#dispatchChange(cursorPos, cursorPos, content, cursorPos);
 | 
					        this.editor.input.spliceText(cursorPos, cursorPos, content, {from: cursorPos});
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
@@ -381,21 +365,21 @@ export class Actions {
 | 
				
			|||||||
     * screen coordinates (Typically form a paste event).
 | 
					     * screen coordinates (Typically form a paste event).
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    insertClipboardImages(images: File[], posX: number, posY: number): void {
 | 
					    insertClipboardImages(images: File[], posX: number, posY: number): void {
 | 
				
			||||||
        const cursorPos = this.editor.cm.posAtCoords({x: posX, y: posY}, false);
 | 
					        const cursorPos = this.editor.input.coordsToSelection(posX, posY).from;
 | 
				
			||||||
        for (const image of images) {
 | 
					        for (const image of images) {
 | 
				
			||||||
            this.uploadImage(image, cursorPos);
 | 
					            this.uploadImage(image, cursorPos);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Handle image upload and add image into markdown content
 | 
					     * Handle image upload and add image into Markdown content
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    async uploadImage(file: File, position: number|null = null): Promise<void> {
 | 
					    async uploadImage(file: File, position: number|null = null): Promise<void> {
 | 
				
			||||||
        if (file === null || file.type.indexOf('image') !== 0) return;
 | 
					        if (file === null || file.type.indexOf('image') !== 0) return;
 | 
				
			||||||
        let ext = 'png';
 | 
					        let ext = 'png';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (position === null) {
 | 
					        if (position === null) {
 | 
				
			||||||
            position = this.#getSelectionRange().from;
 | 
					            position = this.editor.input.getSelection().from;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (file.name) {
 | 
					        if (file.name) {
 | 
				
			||||||
@@ -409,7 +393,7 @@ export class Actions {
 | 
				
			|||||||
        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 placeholderImage = window.baseUrl(`/loading.gif#upload${id}`);
 | 
				
			||||||
        const placeHolderText = ``;
 | 
					        const placeHolderText = ``;
 | 
				
			||||||
        this.#dispatchChange(position, position, placeHolderText, position);
 | 
					        this.editor.input.spliceText(position, position, placeHolderText, {from: position});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const remoteFilename = `image-${Date.now()}.${ext}`;
 | 
					        const remoteFilename = `image-${Date.now()}.${ext}`;
 | 
				
			||||||
        const formData = new FormData();
 | 
					        const formData = new FormData();
 | 
				
			||||||
@@ -427,54 +411,16 @@ export class Actions {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * 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.
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    #setText(text: string, selectionRange: SelectionRange|null = null) {
 | 
					 | 
				
			||||||
        selectionRange = selectionRange || this.#getSelectionRange();
 | 
					 | 
				
			||||||
        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;
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Replace the current selection and focus the editor.
 | 
					     * 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.
 | 
					     * 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.
 | 
					     * Can be provided a selection range to use instead of the current selection range.
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    #replaceSelection(newContent: string, cursorOffset: number = 0, selectionRange: SelectionRange|null = null) {
 | 
					    #replaceSelection(newContent: string, offset: number = 0, selection: MarkdownEditorInputSelection|null = null) {
 | 
				
			||||||
        selectionRange = selectionRange || this.#getSelectionRange();
 | 
					        selection = selection || this.editor.input.getSelection();
 | 
				
			||||||
        const selectFrom = selectionRange.from + cursorOffset;
 | 
					        const selectFrom = selection.from + offset;
 | 
				
			||||||
        this.#dispatchChange(selectionRange.from, selectionRange.to, newContent, selectFrom);
 | 
					        this.editor.input.spliceText(selection.from, selection.to, newContent, {from: selectFrom, to: selectFrom});
 | 
				
			||||||
        this.focus();
 | 
					        this.editor.input.focus();
 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Get the text content of the main current selection.
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    #getSelectionText(selectionRange: SelectionRange|null = null): string {
 | 
					 | 
				
			||||||
        selectionRange = selectionRange || this.#getSelectionRange();
 | 
					 | 
				
			||||||
        return this.editor.cm.state.sliceDoc(selectionRange.from, selectionRange.to);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Get the range of the current main selection.
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    #getSelectionRange(): SelectionRange {
 | 
					 | 
				
			||||||
        return this.editor.cm.state.selection.main;
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
@@ -489,19 +435,19 @@ export class Actions {
 | 
				
			|||||||
     * Find and replace the first occurrence of [search] with [replace]
 | 
					     * Find and replace the first occurrence of [search] with [replace]
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    #findAndReplaceContent(search: string, replace: string): void {
 | 
					    #findAndReplaceContent(search: string, replace: string): void {
 | 
				
			||||||
        const newText = this.#getText().replace(search, replace);
 | 
					        const newText = this.editor.input.getText().replace(search, replace);
 | 
				
			||||||
        this.#setText(newText);
 | 
					        this.editor.input.setText(newText);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Wrap the line in the given start and end contents.
 | 
					     * Wrap the line in the given start and end contents.
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    #wrapLine(start: string, end: string): void {
 | 
					    #wrapLine(start: string, end: string): void {
 | 
				
			||||||
        const selectionRange = this.#getSelectionRange();
 | 
					        const selectionRange = this.editor.input.getSelection();
 | 
				
			||||||
        const line = this.editor.cm.state.doc.lineAt(selectionRange.from);
 | 
					        const lineRange = this.editor.input.getLineRangeFromPosition(selectionRange.from);
 | 
				
			||||||
        const lineContent = line.text;
 | 
					        const lineContent = this.editor.input.getSelectionText(lineRange);
 | 
				
			||||||
        let newLineContent;
 | 
					        let newLineContent: string;
 | 
				
			||||||
        let lineOffset = 0;
 | 
					        let lineOffset: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (lineContent.startsWith(start) && lineContent.endsWith(end)) {
 | 
					        if (lineContent.startsWith(start) && lineContent.endsWith(end)) {
 | 
				
			||||||
            newLineContent = lineContent.slice(start.length, lineContent.length - end.length);
 | 
					            newLineContent = lineContent.slice(start.length, lineContent.length - end.length);
 | 
				
			||||||
@@ -511,44 +457,7 @@ export class Actions {
 | 
				
			|||||||
            lineOffset = start.length;
 | 
					            lineOffset = start.length;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.#dispatchChange(line.from, line.to, newLineContent, selectionRange.from + lineOffset);
 | 
					        this.editor.input.spliceText(lineRange.from, lineRange.to, newLineContent, {from: selectionRange.from + lineOffset});
 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Dispatch changes to the editor.
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    #dispatchChange(from: number, to: number|null = null, text: string|null = null, selectFrom: number|null = null, selectTo: number|null = null): void {
 | 
					 | 
				
			||||||
        const change: ChangeSpec = {from};
 | 
					 | 
				
			||||||
        if (to) {
 | 
					 | 
				
			||||||
            change.to = to;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if (text) {
 | 
					 | 
				
			||||||
            change.insert = text;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        const tr: TransactionSpec = {changes: change};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        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: number, to: number, scrollIntoView = false) {
 | 
					 | 
				
			||||||
        this.editor.cm.dispatch({
 | 
					 | 
				
			||||||
            selection: {anchor: from, head: to},
 | 
					 | 
				
			||||||
            scrollIntoView,
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,9 +4,9 @@ import {Actions} from './actions';
 | 
				
			|||||||
import {Settings} from './settings';
 | 
					import {Settings} from './settings';
 | 
				
			||||||
import {listenToCommonEvents} from './common-events';
 | 
					import {listenToCommonEvents} from './common-events';
 | 
				
			||||||
import {init as initCodemirror} from './codemirror';
 | 
					import {init as initCodemirror} from './codemirror';
 | 
				
			||||||
import {EditorView} from "@codemirror/view";
 | 
					 | 
				
			||||||
import {importVersioned} from "../services/util";
 | 
					 | 
				
			||||||
import {CodeModule} from "../global";
 | 
					import {CodeModule} from "../global";
 | 
				
			||||||
 | 
					import {MarkdownEditorInput} from "./inputs/interface";
 | 
				
			||||||
 | 
					import {CodemirrorInput} from "./inputs/codemirror";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface MarkdownEditorConfig {
 | 
					export interface MarkdownEditorConfig {
 | 
				
			||||||
    pageId: string;
 | 
					    pageId: string;
 | 
				
			||||||
@@ -23,7 +23,7 @@ export interface MarkdownEditor {
 | 
				
			|||||||
    display: Display;
 | 
					    display: Display;
 | 
				
			||||||
    markdown: Markdown;
 | 
					    markdown: Markdown;
 | 
				
			||||||
    actions: Actions;
 | 
					    actions: Actions;
 | 
				
			||||||
    cm: EditorView;
 | 
					    input: MarkdownEditorInput;
 | 
				
			||||||
    settings: Settings;
 | 
					    settings: Settings;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -41,7 +41,9 @@ export async function init(config: MarkdownEditorConfig): Promise<MarkdownEditor
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    editor.actions = new Actions(editor);
 | 
					    editor.actions = new Actions(editor);
 | 
				
			||||||
    editor.display = new Display(editor);
 | 
					    editor.display = new Display(editor);
 | 
				
			||||||
    editor.cm = initCodemirror(editor, Code);
 | 
					
 | 
				
			||||||
 | 
					    const codeMirror = initCodemirror(editor, Code);
 | 
				
			||||||
 | 
					    editor.input = new CodemirrorInput(codeMirror);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    listenToCommonEvents(editor);
 | 
					    listenToCommonEvents(editor);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,12 +1,9 @@
 | 
				
			|||||||
import {MarkdownEditorInput, MarkdownEditorInputSelection} from "./interface";
 | 
					import {MarkdownEditorInput, MarkdownEditorInputSelection} from "./interface";
 | 
				
			||||||
import {MarkdownEditor} from "../index.mjs";
 | 
					 | 
				
			||||||
import {EditorView} from "@codemirror/view";
 | 
					import {EditorView} from "@codemirror/view";
 | 
				
			||||||
import {ChangeSpec, SelectionRange, TransactionSpec} from "@codemirror/state";
 | 
					import {ChangeSpec, TransactionSpec} from "@codemirror/state";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class CodemirrorInput implements MarkdownEditorInput {
 | 
					export class CodemirrorInput implements MarkdownEditorInput {
 | 
				
			||||||
 | 
					 | 
				
			||||||
    protected editor: MarkdownEditor;
 | 
					 | 
				
			||||||
    protected cm: EditorView;
 | 
					    protected cm: EditorView;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    constructor(cm: EditorView) {
 | 
					    constructor(cm: EditorView) {
 | 
				
			||||||
@@ -14,86 +11,93 @@ export class CodemirrorInput implements MarkdownEditorInput {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    focus(): void {
 | 
					    focus(): void {
 | 
				
			||||||
        if (!this.editor.cm.hasFocus) {
 | 
					        if (!this.cm.hasFocus) {
 | 
				
			||||||
            this.editor.cm.focus();
 | 
					            this.cm.focus();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    getSelection(): MarkdownEditorInputSelection {
 | 
					    getSelection(): MarkdownEditorInputSelection {
 | 
				
			||||||
        return this.editor.cm.state.selection.main;
 | 
					        return this.cm.state.selection.main;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    getSelectionText(selection: MarkdownEditorInputSelection|null = null): string {
 | 
					    getSelectionText(selection?: MarkdownEditorInputSelection): string {
 | 
				
			||||||
        selection = selection || this.getSelection();
 | 
					        selection = selection || this.getSelection();
 | 
				
			||||||
        return this.editor.cm.state.sliceDoc(selection.from, selection.to);
 | 
					        return this.cm.state.sliceDoc(selection.from, selection.to);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    setSelection(selection: MarkdownEditorInputSelection, scrollIntoView: boolean = false) {
 | 
					    setSelection(selection: MarkdownEditorInputSelection, scrollIntoView: boolean = false) {
 | 
				
			||||||
        this.editor.cm.dispatch({
 | 
					        this.cm.dispatch({
 | 
				
			||||||
            selection: {anchor: selection.from, head: selection.to},
 | 
					            selection: {anchor: selection.from, head: selection.to},
 | 
				
			||||||
            scrollIntoView,
 | 
					            scrollIntoView,
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    getText(): string {
 | 
					    getText(): string {
 | 
				
			||||||
        return this.editor.cm.state.doc.toString();
 | 
					        return this.cm.state.doc.toString();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    getTextAboveView(): string {
 | 
					    getTextAboveView(): string {
 | 
				
			||||||
        const blockInfo = this.editor.cm.lineBlockAtHeight(scrollEl.scrollTop);
 | 
					        const blockInfo = this.cm.lineBlockAtHeight(this.cm.scrollDOM.scrollTop);
 | 
				
			||||||
        return this.editor.cm.state.sliceDoc(0, blockInfo.from);
 | 
					        return this.cm.state.sliceDoc(0, blockInfo.from);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    setText(text: string, selection: MarkdownEditorInputSelection | null = null) {
 | 
					    setText(text: string, selection?: MarkdownEditorInputSelection) {
 | 
				
			||||||
        selection = selection || this.getSelection();
 | 
					        selection = selection || this.getSelection();
 | 
				
			||||||
        const newDoc = this.editor.cm.state.toText(text);
 | 
					        const newDoc = this.cm.state.toText(text);
 | 
				
			||||||
        const newSelectFrom = Math.min(selection.from, newDoc.length);
 | 
					        const newSelectFrom = Math.min(selection.from, newDoc.length);
 | 
				
			||||||
        const scrollTop = this.editor.cm.scrollDOM.scrollTop;
 | 
					        const scrollTop = this.cm.scrollDOM.scrollTop;
 | 
				
			||||||
        this.dispatchChange(0, this.editor.cm.state.doc.length, text, newSelectFrom);
 | 
					        this.dispatchChange(0, this.cm.state.doc.length, text, newSelectFrom);
 | 
				
			||||||
        this.focus();
 | 
					        this.focus();
 | 
				
			||||||
        window.requestAnimationFrame(() => {
 | 
					        window.requestAnimationFrame(() => {
 | 
				
			||||||
            this.editor.cm.scrollDOM.scrollTop = scrollTop;
 | 
					            this.cm.scrollDOM.scrollTop = scrollTop;
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    spliceText(from: number, to: number, newText: string, selection: MarkdownEditorInputSelection | null = null) {
 | 
					    spliceText(from: number, to: number, newText: string, selection: Partial<MarkdownEditorInputSelection> | null = null) {
 | 
				
			||||||
        const end = (selection?.from === selection?.to) ? null : selection?.to;
 | 
					        const end = (selection?.from === selection?.to) ? null : selection?.to;
 | 
				
			||||||
        this.dispatchChange(from, to, newText, selection?.from, end)
 | 
					        this.dispatchChange(from, to, newText, selection?.from, end)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    appendText(text: string) {
 | 
					    appendText(text: string) {
 | 
				
			||||||
        const end = this.editor.cm.state.doc.length;
 | 
					        const end = this.cm.state.doc.length;
 | 
				
			||||||
        this.dispatchChange(end, end, `\n${text}`);
 | 
					        this.dispatchChange(end, end, `\n${text}`);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    getLineText(lineIndex: number = -1): string {
 | 
					    getLineText(lineIndex: number = -1): string {
 | 
				
			||||||
        const index = lineIndex > -1 ? lineIndex : this.getSelection().from;
 | 
					        const index = lineIndex > -1 ? lineIndex : this.getSelection().from;
 | 
				
			||||||
        return this.editor.cm.state.doc.lineAt(index).text;
 | 
					        return this.cm.state.doc.lineAt(index).text;
 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    wrapLine(start: string, end: string) {
 | 
					 | 
				
			||||||
        const selectionRange = this.getSelection();
 | 
					 | 
				
			||||||
        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.dispatchChange(line.from, line.to, newLineContent, selectionRange.from + lineOffset);
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    coordsToSelection(x: number, y: number): MarkdownEditorInputSelection {
 | 
					    coordsToSelection(x: number, y: number): MarkdownEditorInputSelection {
 | 
				
			||||||
        const cursorPos = this.editor.cm.posAtCoords({x, y}, false);
 | 
					        const cursorPos = this.cm.posAtCoords({x, y}, false);
 | 
				
			||||||
        return {from: cursorPos, to: cursorPos};
 | 
					        return {from: cursorPos, to: cursorPos};
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    getLineRangeFromPosition(position: number): MarkdownEditorInputSelection {
 | 
				
			||||||
 | 
					        const line = this.cm.state.doc.lineAt(position);
 | 
				
			||||||
 | 
					        return {from: line.from, to: line.to};
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    searchForLineContaining(text: string): MarkdownEditorInputSelection | null {
 | 
				
			||||||
 | 
					        const docText = this.cm.state.doc;
 | 
				
			||||||
 | 
					        let lineCount = 1;
 | 
				
			||||||
 | 
					        let scrollToLine = -1;
 | 
				
			||||||
 | 
					        for (const line of docText.iterLines()) {
 | 
				
			||||||
 | 
					            if (line.includes(text)) {
 | 
				
			||||||
 | 
					                scrollToLine = lineCount;
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            lineCount += 1;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (scrollToLine === -1) {
 | 
				
			||||||
 | 
					            return null;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const line = docText.line(scrollToLine);
 | 
				
			||||||
 | 
					        return {from: line.from, to: line.to};
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Dispatch changes to the editor.
 | 
					     * Dispatch changes to the editor.
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -18,12 +18,12 @@ export interface MarkdownEditorInput {
 | 
				
			|||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Get the text of the given (or current) selection range.
 | 
					     * Get the text of the given (or current) selection range.
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    getSelectionText(selection: MarkdownEditorInputSelection|null = null): string;
 | 
					    getSelectionText(selection?: MarkdownEditorInputSelection): string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Set the selection range of the editor.
 | 
					     * Set the selection range of the editor.
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    setSelection(selection: MarkdownEditorInputSelection, scrollIntoView: boolean = false): void;
 | 
					    setSelection(selection: MarkdownEditorInputSelection, scrollIntoView: boolean): void;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Get the full text of the input.
 | 
					     * Get the full text of the input.
 | 
				
			||||||
@@ -40,13 +40,13 @@ export interface MarkdownEditorInput {
 | 
				
			|||||||
     * Set the full text of the input.
 | 
					     * Set the full text of the input.
 | 
				
			||||||
     * Optionally can provide a selection to restore after setting text.
 | 
					     * Optionally can provide a selection to restore after setting text.
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    setText(text: string, selection: MarkdownEditorInputSelection|null = null): void;
 | 
					    setText(text: string, selection?: MarkdownEditorInputSelection): void;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Splice in/out text within the input.
 | 
					     * Splice in/out text within the input.
 | 
				
			||||||
     * Optionally can provide a selection to restore after setting text.
 | 
					     * Optionally can provide a selection to restore after setting text.
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    spliceText(from: number, to: number, newText: string, selection: MarkdownEditorInputSelection|null = null): void;
 | 
					    spliceText(from: number, to: number, newText: string, selection: Partial<MarkdownEditorInputSelection>|null): void;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Append text to the end of the editor.
 | 
					     * Append text to the end of the editor.
 | 
				
			||||||
@@ -57,15 +57,20 @@ export interface MarkdownEditorInput {
 | 
				
			|||||||
     * Get the text of the given line number otherwise the text
 | 
					     * Get the text of the given line number otherwise the text
 | 
				
			||||||
     * of the current selected line.
 | 
					     * of the current selected line.
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    getLineText(lineIndex:number = -1): string;
 | 
					    getLineText(lineIndex:number): string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Wrap the current line in the given start/end contents.
 | 
					     * Get a selection representing the line range from the given position.
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    wrapLine(start: string, end: string): void;
 | 
					    getLineRangeFromPosition(position: number): MarkdownEditorInputSelection;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Convert the given screen coords to a selection position within the input.
 | 
					     * Convert the given screen coords to a selection position within the input.
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    coordsToSelection(x: number, y: number): MarkdownEditorInputSelection;
 | 
					    coordsToSelection(x: number, y: number): MarkdownEditorInputSelection;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Search and return a line range which includes the provided text.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    searchForLineContaining(text: string): MarkdownEditorInputSelection|null;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user