]> BookStack Code Mirror - bookstack/commitdiff
MD Editor: Starting conversion to typescript
authorDan Brown <redacted>
Sun, 20 Jul 2025 11:33:22 +0000 (12:33 +0100)
committerDan Brown <redacted>
Sun, 20 Jul 2025 11:33:22 +0000 (12:33 +0100)
13 files changed:
dev/build/esbuild.js
lang/en/entities.php
resources/js/components/entity-selector-popup.ts [moved from resources/js/components/entity-selector-popup.js with 58% similarity]
resources/js/components/entity-selector.ts [moved from resources/js/components/entity-selector.js with 74% similarity]
resources/js/components/image-manager.js
resources/js/markdown/actions.ts [moved from resources/js/markdown/actions.js with 82% similarity]
resources/js/markdown/codemirror.js
resources/js/markdown/common-events.js [deleted file]
resources/js/markdown/common-events.ts [new file with mode: 0644]
resources/js/markdown/index.mjs [deleted file]
resources/js/markdown/index.mts [new file with mode: 0644]
resources/js/markdown/settings.js
resources/views/pages/parts/markdown-editor.blade.php

index cd8bf279f28db0858cbd3c238ea029d4582d107d..63387d612ce89a260f862857f28fb534a3058ae2 100644 (file)
@@ -13,7 +13,7 @@ const entryPoints = {
     app: path.join(__dirname, '../../resources/js/app.ts'),
     code: path.join(__dirname, '../../resources/js/code/index.mjs'),
     'legacy-modes': path.join(__dirname, '../../resources/js/code/legacy-modes.mjs'),
-    markdown: path.join(__dirname, '../../resources/js/markdown/index.mjs'),
+    markdown: path.join(__dirname, '../../resources/js/markdown/index.mts'),
     wysiwyg: path.join(__dirname, '../../resources/js/wysiwyg/index.ts'),
 };
 
index 561022ad6b6b9bd34c0579ce9379142c7f5aacab..ef625a3d261d28fda340010b652192f9391e8eef 100644 (file)
@@ -268,6 +268,7 @@ return [
     'pages_md_insert_drawing' => 'Insert Drawing',
     'pages_md_show_preview' => 'Show preview',
     'pages_md_sync_scroll' => 'Sync preview scroll',
+    'pages_md_plain_editor' => 'Plaintext editor',
     'pages_drawing_unsaved' => 'Unsaved Drawing Found',
     'pages_drawing_unsaved_confirm' => 'Unsaved drawing data was found from a previous failed drawing save attempt. Would you like to restore and continue editing this unsaved drawing?',
     'pages_not_in_chapter' => 'Page is not in a chapter',
similarity index 58%
rename from resources/js/components/entity-selector-popup.js
rename to resources/js/components/entity-selector-popup.ts
index 29c06e9095113420b1d744d2fa73cd607635ea69..468f074b5d4f9bab29cecc1a77ab8e0f447642ec 100644 (file)
@@ -1,15 +1,23 @@
 import {Component} from './component';
+import {EntitySelector, EntitySelectorEntity, EntitySelectorSearchOptions} from "./entity-selector";
+import {Popup} from "./popup";
+
+export type EntitySelectorPopupCallback = (entity: EntitySelectorEntity) => void;
 
 export class EntitySelectorPopup extends Component {
 
+    protected container!: HTMLElement;
+    protected selectButton!: HTMLElement;
+    protected selectorEl!: HTMLElement;
+
+    protected callback: EntitySelectorPopupCallback|null = null;
+    protected selection: EntitySelectorEntity|null = null;
+
     setup() {
         this.container = this.$el;
         this.selectButton = this.$refs.select;
         this.selectorEl = this.$refs.selector;
 
-        this.callback = null;
-        this.selection = null;
-
         this.selectButton.addEventListener('click', this.onSelectButtonClick.bind(this));
         window.$events.listen('entity-select-change', this.onSelectionChange.bind(this));
         window.$events.listen('entity-select-confirm', this.handleConfirmedSelection.bind(this));
@@ -17,10 +25,8 @@ export class EntitySelectorPopup extends Component {
 
     /**
      * Show the selector popup.
-     * @param {Function} callback
-     * @param {EntitySelectorSearchOptions} searchOptions
      */
-    show(callback, searchOptions = {}) {
+    show(callback: EntitySelectorPopupCallback, searchOptions: Partial<EntitySelectorSearchOptions> = {}) {
         this.callback = callback;
         this.getSelector().configureSearchOptions(searchOptions);
         this.getPopup().show();
@@ -32,34 +38,28 @@ export class EntitySelectorPopup extends Component {
         this.getPopup().hide();
     }
 
-    /**
-     * @returns {Popup}
-     */
-    getPopup() {
-        return window.$components.firstOnElement(this.container, 'popup');
+    getPopup(): Popup {
+        return window.$components.firstOnElement(this.container, 'popup') as Popup;
     }
 
-    /**
-     * @returns {EntitySelector}
-     */
-    getSelector() {
-        return window.$components.firstOnElement(this.selectorEl, 'entity-selector');
+    getSelector(): EntitySelector {
+        return window.$components.firstOnElement(this.selectorEl, 'entity-selector') as EntitySelector;
     }
 
     onSelectButtonClick() {
         this.handleConfirmedSelection(this.selection);
     }
 
-    onSelectionChange(entity) {
-        this.selection = entity;
-        if (entity === null) {
+    onSelectionChange(entity: EntitySelectorEntity|{}) {
+        this.selection = (entity.hasOwnProperty('id') ? entity : null) as EntitySelectorEntity|null;
+        if (!this.selection) {
             this.selectButton.setAttribute('disabled', 'true');
         } else {
             this.selectButton.removeAttribute('disabled');
         }
     }
 
-    handleConfirmedSelection(entity) {
+    handleConfirmedSelection(entity: EntitySelectorEntity|null): void {
         this.hide();
         this.getSelector().reset();
         if (this.callback && entity) this.callback(entity);
similarity index 74%
rename from resources/js/components/entity-selector.js
rename to resources/js/components/entity-selector.ts
index 7491119a137ffbb24d0d83f0f4be1ec46add9ac7..0ae9710f75e26043e7ac0e74625992a83a3a69ed 100644 (file)
@@ -1,24 +1,36 @@
-import {onChildEvent} from '../services/dom.ts';
+import {onChildEvent} from '../services/dom';
 import {Component} from './component';
 
-/**
- * @typedef EntitySelectorSearchOptions
- * @property entityTypes string
- * @property entityPermission string
- * @property searchEndpoint string
- * @property initialValue string
- */
-
-/**
- * Entity Selector
- */
+export interface EntitySelectorSearchOptions {
+    entityTypes: string;
+    entityPermission: string;
+    searchEndpoint: string;
+    initialValue: string;
+}
+
+export type EntitySelectorEntity = {
+    id: number,
+    name: string,
+    link: string,
+};
+
 export class EntitySelector extends Component {
+    protected elem!: HTMLElement;
+    protected input!: HTMLInputElement;
+    protected searchInput!: HTMLInputElement;
+    protected loading!: HTMLElement;
+    protected resultsContainer!: HTMLElement;
+
+    protected searchOptions!: EntitySelectorSearchOptions;
+
+    protected search = '';
+    protected lastClick = 0;
 
     setup() {
         this.elem = this.$el;
 
-        this.input = this.$refs.input;
-        this.searchInput = this.$refs.search;
+        this.input = this.$refs.input as HTMLInputElement;
+        this.searchInput = this.$refs.search as HTMLInputElement;
         this.loading = this.$refs.loading;
         this.resultsContainer = this.$refs.results;
 
@@ -29,9 +41,6 @@ export class EntitySelector extends Component {
             initialValue: this.searchInput.value || '',
         };
 
-        this.search = '';
-        this.lastClick = 0;
-
         this.setupListeners();
         this.showLoading();
 
@@ -40,16 +49,13 @@ export class EntitySelector extends Component {
         }
     }
 
-    /**
-     * @param {EntitySelectorSearchOptions} options
-     */
-    configureSearchOptions(options) {
+    configureSearchOptions(options: Partial<EntitySelectorSearchOptions>): void {
         Object.assign(this.searchOptions, options);
         this.reset();
         this.searchInput.value = this.searchOptions.initialValue;
     }
 
-    setupListeners() {
+    setupListeners(): void {
         this.elem.addEventListener('click', this.onClick.bind(this));
 
         let lastSearch = 0;
@@ -67,7 +73,7 @@ export class EntitySelector extends Component {
         });
 
         // Keyboard navigation
-        onChildEvent(this.$el, '[data-entity-type]', 'keydown', event => {
+        onChildEvent(this.$el, '[data-entity-type]', 'keydown', ((event: KeyboardEvent) => {
             if (event.ctrlKey && event.code === 'Enter') {
                 const form = this.$el.closest('form');
                 if (form) {
@@ -83,7 +89,7 @@ export class EntitySelector extends Component {
             if (event.code === 'ArrowUp') {
                 this.focusAdjacent(false);
             }
-        });
+        }) as (event: Event) => void);
 
         this.searchInput.addEventListener('keydown', event => {
             if (event.code === 'ArrowDown') {
@@ -93,10 +99,10 @@ export class EntitySelector extends Component {
     }
 
     focusAdjacent(forward = true) {
-        const items = Array.from(this.resultsContainer.querySelectorAll('[data-entity-type]'));
+        const items: (Element|null)[] = Array.from(this.resultsContainer.querySelectorAll('[data-entity-type]'));
         const selectedIndex = items.indexOf(document.activeElement);
         const newItem = items[selectedIndex + (forward ? 1 : -1)] || items[0];
-        if (newItem) {
+        if (newItem instanceof HTMLElement) {
             newItem.focus();
         }
     }
@@ -132,7 +138,7 @@ export class EntitySelector extends Component {
         }
 
         window.$http.get(this.searchUrl()).then(resp => {
-            this.resultsContainer.innerHTML = resp.data;
+            this.resultsContainer.innerHTML = resp.data as string;
             this.hideLoading();
         });
     }
@@ -142,7 +148,7 @@ export class EntitySelector extends Component {
         return `${this.searchOptions.searchEndpoint}?${query}`;
     }
 
-    searchEntities(searchTerm) {
+    searchEntities(searchTerm: string) {
         if (!this.searchOptions.searchEndpoint) {
             throw new Error('Search endpoint not set for entity-selector load');
         }
@@ -150,7 +156,7 @@ export class EntitySelector extends Component {
         this.input.value = '';
         const url = `${this.searchUrl()}&term=${encodeURIComponent(searchTerm)}`;
         window.$http.get(url).then(resp => {
-            this.resultsContainer.innerHTML = resp.data;
+            this.resultsContainer.innerHTML = resp.data as string;
             this.hideLoading();
         });
     }
@@ -162,16 +168,16 @@ export class EntitySelector extends Component {
         return answer;
     }
 
-    onClick(event) {
-        const listItem = event.target.closest('[data-entity-type]');
-        if (listItem) {
+    onClick(event: MouseEvent) {
+        const listItem = (event.target as HTMLElement).closest('[data-entity-type]');
+        if (listItem instanceof HTMLElement) {
             event.preventDefault();
             event.stopPropagation();
             this.selectItem(listItem);
         }
     }
 
-    selectItem(item) {
+    selectItem(item: HTMLElement): void {
         const isDblClick = this.isDoubleClick();
         const type = item.getAttribute('data-entity-type');
         const id = item.getAttribute('data-entity-id');
@@ -180,14 +186,14 @@ export class EntitySelector extends Component {
         this.unselectAll();
         this.input.value = isSelected ? `${type}:${id}` : '';
 
-        const link = item.getAttribute('href');
-        const name = item.querySelector('.entity-list-item-name').textContent;
-        const data = {id: Number(id), name, link};
+        const link = item.getAttribute('href') || '';
+        const name = item.querySelector('.entity-list-item-name')?.textContent || '';
+        const data: EntitySelectorEntity = {id: Number(id), name, link};
 
         if (isSelected) {
             item.classList.add('selected');
         } else {
-            window.$events.emit('entity-select-change', null);
+            window.$events.emit('entity-select-change');
         }
 
         if (!isDblClick && !isSelected) return;
@@ -200,7 +206,7 @@ export class EntitySelector extends Component {
         }
     }
 
-    confirmSelection(data) {
+    confirmSelection(data: EntitySelectorEntity) {
         window.$events.emit('entity-select-confirm', data);
     }
 
index c8108ab28c19f6b456e419a50833ae8cd98f9b34..84ba333f9da484253a71a67841fdbdc4dbe7701d 100644 (file)
@@ -127,6 +127,10 @@ export class ImageManager extends Component {
         });
     }
 
+    /**
+     * @param {({ thumbs: { display: string; }; url: string; name: string; }) => void} callback
+     * @param {String} type
+     */
     show(callback, type = 'gallery') {
         this.resetAll();
 
similarity index 82%
rename from resources/js/markdown/actions.js
rename to resources/js/markdown/actions.ts
index e99bbf3e14fe5cb668e2e7c22782c0cf0ce44508..c6210809cce0d9640f0ee32af17ceb3882830915 100644 (file)
@@ -1,16 +1,25 @@
-import * as DrawIO from '../services/drawio.ts';
+import * as DrawIO from '../services/drawio';
+import {MarkdownEditor} from "./index.mjs";
+import {EntitySelectorPopup, ImageManager} from "../components";
+import {ChangeSpec, SelectionRange, TransactionSpec} from "@codemirror/state";
+
+interface ImageManagerImage {
+    id: number;
+    name: string;
+    thumbs: { display: string; };
+    url: string;
+}
 
 export class Actions {
 
-    /**
-     * @param {MarkdownEditor} editor
-     */
-    constructor(editor) {
+    protected readonly editor: MarkdownEditor;
+    protected lastContent: { html: string; markdown: string } = {
+        html: '',
+        markdown: '',
+    };
+
+    constructor(editor: MarkdownEditor) {
         this.editor = editor;
-        this.lastContent = {
-            html: '',
-            markdown: '',
-        };
     }
 
     updateAndRender() {
@@ -30,10 +39,9 @@ export class Actions {
     }
 
     showImageInsert() {
-        /** @type {ImageManager} * */
-        const imageManager = window.$components.first('image-manager');
+        const imageManager = window.$components.first('image-manager') as ImageManager;
 
-        imageManager.show(image => {
+        imageManager.show((image: ImageManagerImage) => {
             const imageUrl = image.thumbs?.display || image.url;
             const selectedText = this.#getSelectionText();
             const newText = `[![${selectedText || image.name}](${imageUrl})](${image.url})`;
@@ -55,9 +63,8 @@ export class Actions {
 
     showImageManager() {
         const selectionRange = this.#getSelectionRange();
-        /** @type {ImageManager} * */
-        const imageManager = window.$components.first('image-manager');
-        imageManager.show(image => {
+        const imageManager = window.$components.first('image-manager') as ImageManager;
+        imageManager.show((image: ImageManagerImage) => {
             this.#insertDrawing(image, selectionRange);
         }, 'drawio');
     }
@@ -66,8 +73,7 @@ export class Actions {
     showLinkSelector() {
         const selectionRange = this.#getSelectionRange();
 
-        /** @type {EntitySelectorPopup} * */
-        const selector = window.$components.first('entity-selector-popup');
+        const selector = window.$components.first('entity-selector-popup') as EntitySelectorPopup;
         const selectionText = this.#getSelectionText(selectionRange);
         selector.show(entity => {
             const selectedText = selectionText || entity.name;
@@ -96,7 +102,7 @@ export class Actions {
 
             try {
                 const resp = await window.$http.post('/images/drawio', data);
-                this.#insertDrawing(resp.data, selectionRange);
+                this.#insertDrawing(resp.data as ImageManagerImage, selectionRange);
                 DrawIO.close();
             } catch (err) {
                 this.handleDrawingUploadError(err);
@@ -105,20 +111,23 @@ export class Actions {
         });
     }
 
-    #insertDrawing(image, originalSelectionRange) {
+    #insertDrawing(image: ImageManagerImage, originalSelectionRange: SelectionRange) {
         const newText = `<div drawio-diagram="${image.id}"><img src="${image.url}"></div>`;
         this.#replaceSelection(newText, newText.length, originalSelectionRange);
     }
 
     // Show draw.io if enabled and handle save.
-    editDrawing(imgContainer) {
+    editDrawing(imgContainer: HTMLElement) {
         const {drawioUrl} = this.editor.config;
         if (!drawioUrl) {
             return;
         }
 
         const selectionRange = this.#getSelectionRange();
-        const drawingId = imgContainer.getAttribute('drawio-diagram');
+        const drawingId = imgContainer.getAttribute('drawio-diagram') || '';
+        if (!drawingId) {
+            return;
+        }
 
         DrawIO.show(drawioUrl, () => DrawIO.load(drawingId), async pngData => {
             const data = {
@@ -128,7 +137,8 @@ export class Actions {
 
             try {
                 const resp = await window.$http.post('/images/drawio', data);
-                const newText = `<div drawio-diagram="${resp.data.id}"><img src="${resp.data.url}"></div>`;
+                const image = resp.data as ImageManagerImage;
+                const newText = `<div drawio-diagram="${image.id}"><img src="${image.url}"></div>`;
                 const newContent = this.#getText().split('\n').map(line => {
                     if (line.indexOf(`drawio-diagram="${drawingId}"`) !== -1) {
                         return newText;
@@ -144,7 +154,7 @@ export class Actions {
         });
     }
 
-    handleDrawingUploadError(error) {
+    handleDrawingUploadError(error: any): void {
         if (error.status === 413) {
             window.$events.emit('error', this.editor.config.text.serverUploadLimit);
         } else {
@@ -162,7 +172,7 @@ export class Actions {
     }
 
     // Scroll to a specified text
-    scrollToText(searchText) {
+    scrollToText(searchText: string): void {
         if (!searchText) {
             return;
         }
@@ -195,17 +205,15 @@ export class Actions {
 
     /**
      * Insert content into the editor.
-     * @param {String} content
      */
-    insertContent(content) {
+    insertContent(content: string) {
         this.#replaceSelection(content, content.length);
     }
 
     /**
      * Prepend content to the editor.
-     * @param {String} content
      */
-    prependContent(content) {
+    prependContent(content: string): void {
         content = this.#cleanTextForEditor(content);
         const selectionRange = this.#getSelectionRange();
         const selectFrom = selectionRange.from + content.length + 1;
@@ -215,19 +223,18 @@ export class Actions {
 
     /**
      * Append content to the editor.
-     * @param {String} content
      */
-    appendContent(content) {
+    appendContent(content: string): void {
         content = this.#cleanTextForEditor(content);
-        this.#dispatchChange(this.editor.cm.state.doc.length, `\n${content}`);
+        const end = this.editor.cm.state.doc.length;
+        this.#dispatchChange(end, end, `\n${content}`);
         this.focus();
     }
 
     /**
      * Replace the editor's contents
-     * @param {String} content
      */
-    replaceContent(content) {
+    replaceContent(content: string): void {
         this.#setText(content);
     }
 
@@ -235,7 +242,7 @@ export class Actions {
      * Replace the start of the line
      * @param {String} newStart
      */
-    replaceLineStart(newStart) {
+    replaceLineStart(newStart: string): void {
         const selectionRange = this.#getSelectionRange();
         const line = this.editor.cm.state.doc.lineAt(selectionRange.from);
 
@@ -264,10 +271,8 @@ export class Actions {
 
     /**
      * Wrap the selection in the given contents start and end contents.
-     * @param {String} start
-     * @param {String} end
      */
-    wrapSelection(start, end) {
+    wrapSelection(start: string, end: string): void {
         const selectRange = this.#getSelectionRange();
         const selectionText = this.#getSelectionText(selectRange);
         if (!selectionText) {
@@ -321,7 +326,7 @@ export class Actions {
         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(line.text);
+        const matches = regex.exec(line.text) || [''];
         const format = (matches ? (matches[2] || matches[3]) : '').toLowerCase();
 
         if (format === formats[formats.length - 1]) {
@@ -343,9 +348,9 @@ export class Actions {
         }
     }
 
-    syncDisplayPosition(event) {
+    syncDisplayPosition(event: Event): void {
         // Thanks to http://liuhao.im/english/2015/11/10/the-sync-scroll-of-markdown-editor-in-javascript.html
-        const scrollEl = event.target;
+        const scrollEl = event.target as HTMLElement;
         const atEnd = Math.abs(scrollEl.scrollHeight - scrollEl.clientHeight - scrollEl.scrollTop) < 1;
         if (atEnd) {
             this.editor.display.scrollToIndex(-1);
@@ -363,25 +368,19 @@ export class Actions {
     /**
      * 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
      */
-    async insertTemplate(templateId, posX, posY) {
+    async insertTemplate(templateId: string, posX: number, posY: number): Promise<void> {
         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;
+        const responseData = (await window.$http.get(`/templates/${templateId}`)).data as {markdown: string, html: string};
+        const content = responseData.markdown || responseData.html;
         this.#dispatchChange(cursorPos, cursorPos, content, cursorPos);
     }
 
     /**
      * 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, posX, posY) {
+    insertClipboardImages(images: File[], posX: number, posY: number): void {
         const cursorPos = this.editor.cm.posAtCoords({x: posX, y: posY}, false);
         for (const image of images) {
             this.uploadImage(image, cursorPos);
@@ -390,10 +389,8 @@ export class Actions {
 
     /**
      * Handle image upload and add image into markdown content
-     * @param {File} file
-     * @param {?Number} position
      */
-    async uploadImage(file, position = null) {
+    async uploadImage(file: File, position: number|null = null): Promise<void> {
         if (file === null || file.type.indexOf('image') !== 0) return;
         let ext = 'png';
 
@@ -403,7 +400,9 @@ export class Actions {
 
         if (file.name) {
             const fileNameMatches = file.name.match(/\.(.+)$/);
-            if (fileNameMatches.length > 1) ext = fileNameMatches[1];
+            if (fileNameMatches && fileNameMatches.length > 1) {
+                ext = fileNameMatches[1];
+            }
         }
 
         // Insert image into markdown
@@ -418,10 +417,10 @@ export class Actions {
         formData.append('uploaded_to', this.editor.config.pageId);
 
         try {
-            const {data} = await window.$http.post('/images/gallery', formData);
-            const newContent = `[![](${data.thumbs.display})](${data.url})`;
+            const image = (await window.$http.post('/images/gallery', formData)).data as ImageManagerImage;
+            const newContent = `[![](${image.thumbs.display})](${image.url})`;
             this.#findAndReplaceContent(placeHolderText, newContent);
-        } catch (err) {
+        } catch (err: any) {
             window.$events.error(err?.data?.message || this.editor.config.text.imageUploadError);
             this.#findAndReplaceContent(placeHolderText, '');
             console.error(err);
@@ -438,10 +437,8 @@ export class Actions {
 
     /**
      * Set the text of the current editor instance.
-     * @param {String} text
-     * @param {?SelectionRange} selectionRange
      */
-    #setText(text, selectionRange = null) {
+    #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);
@@ -457,12 +454,9 @@ export class Actions {
      * 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;
+    #replaceSelection(newContent: string, cursorOffset: number = 0, selectionRange: SelectionRange|null = null) {
+        selectionRange = selectionRange || this.#getSelectionRange();
         const selectFrom = selectionRange.from + cursorOffset;
         this.#dispatchChange(selectionRange.from, selectionRange.to, newContent, selectFrom);
         this.focus();
@@ -470,48 +464,39 @@ export class Actions {
 
     /**
      * Get the text content of the main current selection.
-     * @param {SelectionRange} selectionRange
-     * @return {string}
      */
-    #getSelectionText(selectionRange = null) {
+    #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.
-     * @return {SelectionRange}
      */
-    #getSelectionRange() {
+    #getSelectionRange(): SelectionRange {
         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) {
+    #cleanTextForEditor(text: string): string {
         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) {
+    #findAndReplaceContent(search: string, replace: string): void {
         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) {
+    #wrapLine(start: string, end: string): void {
         const selectionRange = this.#getSelectionRange();
         const line = this.editor.cm.state.doc.lineAt(selectionRange.from);
         const lineContent = line.text;
@@ -531,14 +516,16 @@ export class Actions {
 
     /**
      * 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}};
+    #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};
@@ -557,7 +544,7 @@ export class Actions {
      * @param {Number} to
      * @param {Boolean} scrollIntoView
      */
-    #setSelection(from, to, scrollIntoView = false) {
+    #setSelection(from: number, to: number, scrollIntoView = false) {
         this.editor.cm.dispatch({
             selection: {anchor: from, head: to},
             scrollIntoView,
index 664767605b8a91b33125512efc96476068da4c32..61b2e84573d8c2730cc8810d1962f2fbbc5320c9 100644 (file)
@@ -5,7 +5,7 @@ import {Clipboard} from '../services/clipboard.ts';
 /**
  * Initiate the codemirror instance for the markdown editor.
  * @param {MarkdownEditor} editor
- * @returns {Promise<void>}
+ * @returns {Promise<EditorView>}
  */
 export async function init(editor) {
     const Code = await window.importVersioned('code');
diff --git a/resources/js/markdown/common-events.js b/resources/js/markdown/common-events.js
deleted file mode 100644 (file)
index c3d803f..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-function getContentToInsert({html, markdown}) {
-    return markdown || html;
-}
-
-/**
- * @param {MarkdownEditor} editor
- */
-export function listen(editor) {
-    window.$events.listen('editor::replace', eventContent => {
-        const markdown = getContentToInsert(eventContent);
-        editor.actions.replaceContent(markdown);
-    });
-
-    window.$events.listen('editor::append', eventContent => {
-        const markdown = getContentToInsert(eventContent);
-        editor.actions.appendContent(markdown);
-    });
-
-    window.$events.listen('editor::prepend', eventContent => {
-        const markdown = getContentToInsert(eventContent);
-        editor.actions.prependContent(markdown);
-    });
-
-    window.$events.listen('editor::insert', eventContent => {
-        const markdown = getContentToInsert(eventContent);
-        editor.actions.insertContent(markdown);
-    });
-
-    window.$events.listen('editor::focus', () => {
-        editor.actions.focus();
-    });
-}
diff --git a/resources/js/markdown/common-events.ts b/resources/js/markdown/common-events.ts
new file mode 100644 (file)
index 0000000..4bfc4bb
--- /dev/null
@@ -0,0 +1,36 @@
+import {MarkdownEditor} from "./index.mjs";
+
+export interface HtmlOrMarkdown {
+    html: string;
+    markdown: string;
+}
+
+function getContentToInsert({html, markdown}: {html: string, markdown: string}): string {
+    return markdown || html;
+}
+
+export function listenToCommonEvents(editor: MarkdownEditor): void {
+    window.$events.listen('editor::replace', (eventContent: HtmlOrMarkdown) => {
+        const markdown = getContentToInsert(eventContent);
+        editor.actions.replaceContent(markdown);
+    });
+
+    window.$events.listen('editor::append', (eventContent: HtmlOrMarkdown) => {
+        const markdown = getContentToInsert(eventContent);
+        editor.actions.appendContent(markdown);
+    });
+
+    window.$events.listen('editor::prepend', (eventContent: HtmlOrMarkdown) => {
+        const markdown = getContentToInsert(eventContent);
+        editor.actions.prependContent(markdown);
+    });
+
+    window.$events.listen('editor::insert', (eventContent: HtmlOrMarkdown) => {
+        const markdown = getContentToInsert(eventContent);
+        editor.actions.insertContent(markdown);
+    });
+
+    window.$events.listen('editor::focus', () => {
+        editor.actions.focus();
+    });
+}
diff --git a/resources/js/markdown/index.mjs b/resources/js/markdown/index.mjs
deleted file mode 100644 (file)
index 46c35c8..0000000
+++ /dev/null
@@ -1,51 +0,0 @@
-import {Markdown} from './markdown';
-import {Display} from './display';
-import {Actions} from './actions';
-import {Settings} from './settings';
-import {listen} from './common-events';
-import {init as initCodemirror} from './codemirror';
-
-/**
- * Initiate a new markdown editor instance.
- * @param {MarkdownEditorConfig} config
- * @returns {Promise<MarkdownEditor>}
- */
-export async function init(config) {
-    /**
-     * @type {MarkdownEditor}
-     */
-    const editor = {
-        config,
-        markdown: new Markdown(),
-        settings: new Settings(config.settingInputs),
-    };
-
-    editor.actions = new Actions(editor);
-    editor.display = new Display(editor);
-    editor.cm = await initCodemirror(editor);
-
-    listen(editor);
-
-    return editor;
-}
-
-/**
- * @typedef MarkdownEditorConfig
- * @property {String} pageId
- * @property {Element} container
- * @property {Element} displayEl
- * @property {HTMLTextAreaElement} inputEl
- * @property {String} drawioUrl
- * @property {HTMLInputElement[]} settingInputs
- * @property {Object<String, String>} text
- */
-
-/**
- * @typedef MarkdownEditor
- * @property {MarkdownEditorConfig} config
- * @property {Display} display
- * @property {Markdown} markdown
- * @property {Actions} actions
- * @property {EditorView} cm
- * @property {Settings} settings
- */
diff --git a/resources/js/markdown/index.mts b/resources/js/markdown/index.mts
new file mode 100644 (file)
index 0000000..46345cc
--- /dev/null
@@ -0,0 +1,52 @@
+import {Markdown} from './markdown';
+import {Display} from './display';
+import {Actions} from './actions';
+import {Settings} from './settings';
+import {listenToCommonEvents} from './common-events';
+import {init as initCodemirror} from './codemirror';
+import {EditorView} from "@codemirror/view";
+
+export interface MarkdownEditorConfig {
+    pageId: string;
+    container: Element;
+    displayEl: Element;
+    inputEl: HTMLTextAreaElement;
+    drawioUrl: string;
+    settingInputs: HTMLInputElement[];
+    text: Record<string, string>;
+}
+
+export interface MarkdownEditor {
+    config: MarkdownEditorConfig;
+    display: Display;
+    markdown: Markdown;
+    actions: Actions;
+    cm: EditorView;
+    settings: Settings;
+}
+
+/**
+ * Initiate a new Markdown editor instance.
+ * @param {MarkdownEditorConfig} config
+ * @returns {Promise<MarkdownEditor>}
+ */
+export async function init(config) {
+    /**
+     * @type {MarkdownEditor}
+     */
+    const editor: MarkdownEditor = {
+        config,
+        markdown: new Markdown(),
+        settings: new Settings(config.settingInputs),
+    };
+
+    editor.actions = new Actions(editor);
+    editor.display = new Display(editor);
+    editor.cm = await initCodemirror(editor);
+
+    listenToCommonEvents(editor);
+
+    return editor;
+}
+
+
index b843aaa8a2b2bed55133a17c075473285db5c595..e2e1fce5e379ee0aa466e576bbe711af31741f97 100644 (file)
@@ -5,6 +5,7 @@ export class Settings {
             scrollSync: true,
             showPreview: true,
             editorWidth: 50,
+            plainEditor: false,
         };
         this.changeListeners = {};
         this.loadFromLocalStorage();
index ac62443f9859dd76fed93019bd38b1aa5b40b55b..5b1761b765607e751b352b9b45aaa0196719694e 100644 (file)
                     <div class="px-m">
                         @include('form.custom-checkbox', ['name' => 'md-scrollSync', 'label' => trans('entities.pages_md_sync_scroll'), 'value' => true, 'checked' => true])
                     </div>
+                    <hr class="m-none">
+                    <div class="px-m">
+                        @include('form.custom-checkbox', ['name' => 'md-plainEditor', 'label' => trans('entities.pages_md_plain_editor'), 'value' => true, 'checked' => false])
+                    </div>
                 </div>
             </div>
         </div>