From: Dan Brown Date: Sun, 20 Jul 2025 11:33:22 +0000 (+0100) Subject: MD Editor: Starting conversion to typescript X-Git-Url: http://source.bookstackapp.com/bookstack/commitdiff_plain/7bbf591a7fdb46bb0da30669354b8768ac950506 MD Editor: Starting conversion to typescript --- diff --git a/dev/build/esbuild.js b/dev/build/esbuild.js index cd8bf279f..63387d612 100644 --- a/dev/build/esbuild.js +++ b/dev/build/esbuild.js @@ -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'), }; diff --git a/lang/en/entities.php b/lang/en/entities.php index 561022ad6..ef625a3d2 100644 --- a/lang/en/entities.php +++ b/lang/en/entities.php @@ -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', diff --git a/resources/js/components/entity-selector-popup.js b/resources/js/components/entity-selector-popup.ts similarity index 58% rename from resources/js/components/entity-selector-popup.js rename to resources/js/components/entity-selector-popup.ts index 29c06e909..468f074b5 100644 --- a/resources/js/components/entity-selector-popup.js +++ b/resources/js/components/entity-selector-popup.ts @@ -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 = {}) { 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); diff --git a/resources/js/components/entity-selector.js b/resources/js/components/entity-selector.ts similarity index 74% rename from resources/js/components/entity-selector.js rename to resources/js/components/entity-selector.ts index 7491119a1..0ae9710f7 100644 --- a/resources/js/components/entity-selector.js +++ b/resources/js/components/entity-selector.ts @@ -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): 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); } diff --git a/resources/js/components/image-manager.js b/resources/js/components/image-manager.js index c8108ab28..84ba333f9 100644 --- a/resources/js/components/image-manager.js +++ b/resources/js/components/image-manager.js @@ -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(); diff --git a/resources/js/markdown/actions.js b/resources/js/markdown/actions.ts similarity index 82% rename from resources/js/markdown/actions.js rename to resources/js/markdown/actions.ts index e99bbf3e1..c6210809c 100644 --- a/resources/js/markdown/actions.js +++ b/resources/js/markdown/actions.ts @@ -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 = `
`; 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 = `
`; + const image = resp.data as ImageManagerImage; + const newText = `
`; 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 { 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 { 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, diff --git a/resources/js/markdown/codemirror.js b/resources/js/markdown/codemirror.js index 664767605..61b2e8457 100644 --- a/resources/js/markdown/codemirror.js +++ b/resources/js/markdown/codemirror.js @@ -5,7 +5,7 @@ import {Clipboard} from '../services/clipboard.ts'; /** * Initiate the codemirror instance for the markdown editor. * @param {MarkdownEditor} editor - * @returns {Promise} + * @returns {Promise} */ 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 index c3d803f70..000000000 --- a/resources/js/markdown/common-events.js +++ /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 index 000000000..4bfc4bb46 --- /dev/null +++ b/resources/js/markdown/common-events.ts @@ -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 index 46c35c850..000000000 --- a/resources/js/markdown/index.mjs +++ /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} - */ -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} 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 index 000000000..46345ccfd --- /dev/null +++ b/resources/js/markdown/index.mts @@ -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; +} + +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} + */ +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; +} + + diff --git a/resources/js/markdown/settings.js b/resources/js/markdown/settings.js index b843aaa8a..e2e1fce5e 100644 --- a/resources/js/markdown/settings.js +++ b/resources/js/markdown/settings.js @@ -5,6 +5,7 @@ export class Settings { scrollSync: true, showPreview: true, editorWidth: 50, + plainEditor: false, }; this.changeListeners = {}; this.loadFromLocalStorage(); diff --git a/resources/views/pages/parts/markdown-editor.blade.php b/resources/views/pages/parts/markdown-editor.blade.php index ac62443f9..5b1761b76 100644 --- a/resources/views/pages/parts/markdown-editor.blade.php +++ b/resources/views/pages/parts/markdown-editor.blade.php @@ -26,6 +26,10 @@
@include('form.custom-checkbox', ['name' => 'md-scrollSync', 'label' => trans('entities.pages_md_sync_scroll'), 'value' => true, 'checked' => true])
+
+
+ @include('form.custom-checkbox', ['name' => 'md-plainEditor', 'label' => trans('entities.pages_md_plain_editor'), 'value' => true, 'checked' => false]) +