mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-07-28 17:02:04 +03:00
MD Editor: Starting conversion to typescript
This commit is contained in:
@ -13,7 +13,7 @@ const entryPoints = {
|
|||||||
app: path.join(__dirname, '../../resources/js/app.ts'),
|
app: path.join(__dirname, '../../resources/js/app.ts'),
|
||||||
code: path.join(__dirname, '../../resources/js/code/index.mjs'),
|
code: path.join(__dirname, '../../resources/js/code/index.mjs'),
|
||||||
'legacy-modes': path.join(__dirname, '../../resources/js/code/legacy-modes.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'),
|
wysiwyg: path.join(__dirname, '../../resources/js/wysiwyg/index.ts'),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -268,6 +268,7 @@ return [
|
|||||||
'pages_md_insert_drawing' => 'Insert Drawing',
|
'pages_md_insert_drawing' => 'Insert Drawing',
|
||||||
'pages_md_show_preview' => 'Show preview',
|
'pages_md_show_preview' => 'Show preview',
|
||||||
'pages_md_sync_scroll' => 'Sync preview scroll',
|
'pages_md_sync_scroll' => 'Sync preview scroll',
|
||||||
|
'pages_md_plain_editor' => 'Plaintext editor',
|
||||||
'pages_drawing_unsaved' => 'Unsaved Drawing Found',
|
'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_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',
|
'pages_not_in_chapter' => 'Page is not in a chapter',
|
||||||
|
@ -1,15 +1,23 @@
|
|||||||
import {Component} from './component';
|
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 {
|
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() {
|
setup() {
|
||||||
this.container = this.$el;
|
this.container = this.$el;
|
||||||
this.selectButton = this.$refs.select;
|
this.selectButton = this.$refs.select;
|
||||||
this.selectorEl = this.$refs.selector;
|
this.selectorEl = this.$refs.selector;
|
||||||
|
|
||||||
this.callback = null;
|
|
||||||
this.selection = null;
|
|
||||||
|
|
||||||
this.selectButton.addEventListener('click', this.onSelectButtonClick.bind(this));
|
this.selectButton.addEventListener('click', this.onSelectButtonClick.bind(this));
|
||||||
window.$events.listen('entity-select-change', this.onSelectionChange.bind(this));
|
window.$events.listen('entity-select-change', this.onSelectionChange.bind(this));
|
||||||
window.$events.listen('entity-select-confirm', this.handleConfirmedSelection.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.
|
* Show the selector popup.
|
||||||
* @param {Function} callback
|
|
||||||
* @param {EntitySelectorSearchOptions} searchOptions
|
|
||||||
*/
|
*/
|
||||||
show(callback, searchOptions = {}) {
|
show(callback: EntitySelectorPopupCallback, searchOptions: Partial<EntitySelectorSearchOptions> = {}) {
|
||||||
this.callback = callback;
|
this.callback = callback;
|
||||||
this.getSelector().configureSearchOptions(searchOptions);
|
this.getSelector().configureSearchOptions(searchOptions);
|
||||||
this.getPopup().show();
|
this.getPopup().show();
|
||||||
@ -32,34 +38,28 @@ export class EntitySelectorPopup extends Component {
|
|||||||
this.getPopup().hide();
|
this.getPopup().hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
getPopup(): Popup {
|
||||||
* @returns {Popup}
|
return window.$components.firstOnElement(this.container, 'popup') as Popup;
|
||||||
*/
|
|
||||||
getPopup() {
|
|
||||||
return window.$components.firstOnElement(this.container, 'popup');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
getSelector(): EntitySelector {
|
||||||
* @returns {EntitySelector}
|
return window.$components.firstOnElement(this.selectorEl, 'entity-selector') as EntitySelector;
|
||||||
*/
|
|
||||||
getSelector() {
|
|
||||||
return window.$components.firstOnElement(this.selectorEl, 'entity-selector');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onSelectButtonClick() {
|
onSelectButtonClick() {
|
||||||
this.handleConfirmedSelection(this.selection);
|
this.handleConfirmedSelection(this.selection);
|
||||||
}
|
}
|
||||||
|
|
||||||
onSelectionChange(entity) {
|
onSelectionChange(entity: EntitySelectorEntity|{}) {
|
||||||
this.selection = entity;
|
this.selection = (entity.hasOwnProperty('id') ? entity : null) as EntitySelectorEntity|null;
|
||||||
if (entity === null) {
|
if (!this.selection) {
|
||||||
this.selectButton.setAttribute('disabled', 'true');
|
this.selectButton.setAttribute('disabled', 'true');
|
||||||
} else {
|
} else {
|
||||||
this.selectButton.removeAttribute('disabled');
|
this.selectButton.removeAttribute('disabled');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleConfirmedSelection(entity) {
|
handleConfirmedSelection(entity: EntitySelectorEntity|null): void {
|
||||||
this.hide();
|
this.hide();
|
||||||
this.getSelector().reset();
|
this.getSelector().reset();
|
||||||
if (this.callback && entity) this.callback(entity);
|
if (this.callback && entity) this.callback(entity);
|
@ -1,24 +1,36 @@
|
|||||||
import {onChildEvent} from '../services/dom.ts';
|
import {onChildEvent} from '../services/dom';
|
||||||
import {Component} from './component';
|
import {Component} from './component';
|
||||||
|
|
||||||
/**
|
export interface EntitySelectorSearchOptions {
|
||||||
* @typedef EntitySelectorSearchOptions
|
entityTypes: string;
|
||||||
* @property entityTypes string
|
entityPermission: string;
|
||||||
* @property entityPermission string
|
searchEndpoint: string;
|
||||||
* @property searchEndpoint string
|
initialValue: string;
|
||||||
* @property initialValue string
|
}
|
||||||
*/
|
|
||||||
|
export type EntitySelectorEntity = {
|
||||||
|
id: number,
|
||||||
|
name: string,
|
||||||
|
link: string,
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Entity Selector
|
|
||||||
*/
|
|
||||||
export class EntitySelector extends Component {
|
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() {
|
setup() {
|
||||||
this.elem = this.$el;
|
this.elem = this.$el;
|
||||||
|
|
||||||
this.input = this.$refs.input;
|
this.input = this.$refs.input as HTMLInputElement;
|
||||||
this.searchInput = this.$refs.search;
|
this.searchInput = this.$refs.search as HTMLInputElement;
|
||||||
this.loading = this.$refs.loading;
|
this.loading = this.$refs.loading;
|
||||||
this.resultsContainer = this.$refs.results;
|
this.resultsContainer = this.$refs.results;
|
||||||
|
|
||||||
@ -29,9 +41,6 @@ export class EntitySelector extends Component {
|
|||||||
initialValue: this.searchInput.value || '',
|
initialValue: this.searchInput.value || '',
|
||||||
};
|
};
|
||||||
|
|
||||||
this.search = '';
|
|
||||||
this.lastClick = 0;
|
|
||||||
|
|
||||||
this.setupListeners();
|
this.setupListeners();
|
||||||
this.showLoading();
|
this.showLoading();
|
||||||
|
|
||||||
@ -40,16 +49,13 @@ export class EntitySelector extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
configureSearchOptions(options: Partial<EntitySelectorSearchOptions>): void {
|
||||||
* @param {EntitySelectorSearchOptions} options
|
|
||||||
*/
|
|
||||||
configureSearchOptions(options) {
|
|
||||||
Object.assign(this.searchOptions, options);
|
Object.assign(this.searchOptions, options);
|
||||||
this.reset();
|
this.reset();
|
||||||
this.searchInput.value = this.searchOptions.initialValue;
|
this.searchInput.value = this.searchOptions.initialValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
setupListeners() {
|
setupListeners(): void {
|
||||||
this.elem.addEventListener('click', this.onClick.bind(this));
|
this.elem.addEventListener('click', this.onClick.bind(this));
|
||||||
|
|
||||||
let lastSearch = 0;
|
let lastSearch = 0;
|
||||||
@ -67,7 +73,7 @@ export class EntitySelector extends Component {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Keyboard navigation
|
// 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') {
|
if (event.ctrlKey && event.code === 'Enter') {
|
||||||
const form = this.$el.closest('form');
|
const form = this.$el.closest('form');
|
||||||
if (form) {
|
if (form) {
|
||||||
@ -83,7 +89,7 @@ export class EntitySelector extends Component {
|
|||||||
if (event.code === 'ArrowUp') {
|
if (event.code === 'ArrowUp') {
|
||||||
this.focusAdjacent(false);
|
this.focusAdjacent(false);
|
||||||
}
|
}
|
||||||
});
|
}) as (event: Event) => void);
|
||||||
|
|
||||||
this.searchInput.addEventListener('keydown', event => {
|
this.searchInput.addEventListener('keydown', event => {
|
||||||
if (event.code === 'ArrowDown') {
|
if (event.code === 'ArrowDown') {
|
||||||
@ -93,10 +99,10 @@ export class EntitySelector extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
focusAdjacent(forward = true) {
|
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 selectedIndex = items.indexOf(document.activeElement);
|
||||||
const newItem = items[selectedIndex + (forward ? 1 : -1)] || items[0];
|
const newItem = items[selectedIndex + (forward ? 1 : -1)] || items[0];
|
||||||
if (newItem) {
|
if (newItem instanceof HTMLElement) {
|
||||||
newItem.focus();
|
newItem.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -132,7 +138,7 @@ export class EntitySelector extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
window.$http.get(this.searchUrl()).then(resp => {
|
window.$http.get(this.searchUrl()).then(resp => {
|
||||||
this.resultsContainer.innerHTML = resp.data;
|
this.resultsContainer.innerHTML = resp.data as string;
|
||||||
this.hideLoading();
|
this.hideLoading();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -142,7 +148,7 @@ export class EntitySelector extends Component {
|
|||||||
return `${this.searchOptions.searchEndpoint}?${query}`;
|
return `${this.searchOptions.searchEndpoint}?${query}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
searchEntities(searchTerm) {
|
searchEntities(searchTerm: string) {
|
||||||
if (!this.searchOptions.searchEndpoint) {
|
if (!this.searchOptions.searchEndpoint) {
|
||||||
throw new Error('Search endpoint not set for entity-selector load');
|
throw new Error('Search endpoint not set for entity-selector load');
|
||||||
}
|
}
|
||||||
@ -150,7 +156,7 @@ export class EntitySelector extends Component {
|
|||||||
this.input.value = '';
|
this.input.value = '';
|
||||||
const url = `${this.searchUrl()}&term=${encodeURIComponent(searchTerm)}`;
|
const url = `${this.searchUrl()}&term=${encodeURIComponent(searchTerm)}`;
|
||||||
window.$http.get(url).then(resp => {
|
window.$http.get(url).then(resp => {
|
||||||
this.resultsContainer.innerHTML = resp.data;
|
this.resultsContainer.innerHTML = resp.data as string;
|
||||||
this.hideLoading();
|
this.hideLoading();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -162,16 +168,16 @@ export class EntitySelector extends Component {
|
|||||||
return answer;
|
return answer;
|
||||||
}
|
}
|
||||||
|
|
||||||
onClick(event) {
|
onClick(event: MouseEvent) {
|
||||||
const listItem = event.target.closest('[data-entity-type]');
|
const listItem = (event.target as HTMLElement).closest('[data-entity-type]');
|
||||||
if (listItem) {
|
if (listItem instanceof HTMLElement) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
this.selectItem(listItem);
|
this.selectItem(listItem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
selectItem(item) {
|
selectItem(item: HTMLElement): void {
|
||||||
const isDblClick = this.isDoubleClick();
|
const isDblClick = this.isDoubleClick();
|
||||||
const type = item.getAttribute('data-entity-type');
|
const type = item.getAttribute('data-entity-type');
|
||||||
const id = item.getAttribute('data-entity-id');
|
const id = item.getAttribute('data-entity-id');
|
||||||
@ -180,14 +186,14 @@ export class EntitySelector extends Component {
|
|||||||
this.unselectAll();
|
this.unselectAll();
|
||||||
this.input.value = isSelected ? `${type}:${id}` : '';
|
this.input.value = isSelected ? `${type}:${id}` : '';
|
||||||
|
|
||||||
const link = item.getAttribute('href');
|
const link = item.getAttribute('href') || '';
|
||||||
const name = item.querySelector('.entity-list-item-name').textContent;
|
const name = item.querySelector('.entity-list-item-name')?.textContent || '';
|
||||||
const data = {id: Number(id), name, link};
|
const data: EntitySelectorEntity = {id: Number(id), name, link};
|
||||||
|
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
item.classList.add('selected');
|
item.classList.add('selected');
|
||||||
} else {
|
} else {
|
||||||
window.$events.emit('entity-select-change', null);
|
window.$events.emit('entity-select-change');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isDblClick && !isSelected) return;
|
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);
|
window.$events.emit('entity-select-confirm', data);
|
||||||
}
|
}
|
||||||
|
|
@ -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') {
|
show(callback, type = 'gallery') {
|
||||||
this.resetAll();
|
this.resetAll();
|
||||||
|
|
||||||
|
@ -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 {
|
export class Actions {
|
||||||
|
|
||||||
/**
|
protected readonly editor: MarkdownEditor;
|
||||||
* @param {MarkdownEditor} editor
|
protected lastContent: { html: string; markdown: string } = {
|
||||||
*/
|
html: '',
|
||||||
constructor(editor) {
|
markdown: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(editor: MarkdownEditor) {
|
||||||
this.editor = editor;
|
this.editor = editor;
|
||||||
this.lastContent = {
|
|
||||||
html: '',
|
|
||||||
markdown: '',
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateAndRender() {
|
updateAndRender() {
|
||||||
@ -30,10 +39,9 @@ export class Actions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
showImageInsert() {
|
showImageInsert() {
|
||||||
/** @type {ImageManager} * */
|
const imageManager = window.$components.first('image-manager') as ImageManager;
|
||||||
const imageManager = window.$components.first('image-manager');
|
|
||||||
|
|
||||||
imageManager.show(image => {
|
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.#getSelectionText();
|
||||||
const newText = `[](${image.url})`;
|
const newText = `[](${image.url})`;
|
||||||
@ -55,9 +63,8 @@ export class Actions {
|
|||||||
|
|
||||||
showImageManager() {
|
showImageManager() {
|
||||||
const selectionRange = this.#getSelectionRange();
|
const selectionRange = this.#getSelectionRange();
|
||||||
/** @type {ImageManager} * */
|
const imageManager = window.$components.first('image-manager') as ImageManager;
|
||||||
const imageManager = window.$components.first('image-manager');
|
imageManager.show((image: ImageManagerImage) => {
|
||||||
imageManager.show(image => {
|
|
||||||
this.#insertDrawing(image, selectionRange);
|
this.#insertDrawing(image, selectionRange);
|
||||||
}, 'drawio');
|
}, 'drawio');
|
||||||
}
|
}
|
||||||
@ -66,8 +73,7 @@ export class Actions {
|
|||||||
showLinkSelector() {
|
showLinkSelector() {
|
||||||
const selectionRange = this.#getSelectionRange();
|
const selectionRange = this.#getSelectionRange();
|
||||||
|
|
||||||
/** @type {EntitySelectorPopup} * */
|
const selector = window.$components.first('entity-selector-popup') as EntitySelectorPopup;
|
||||||
const selector = window.$components.first('entity-selector-popup');
|
|
||||||
const selectionText = this.#getSelectionText(selectionRange);
|
const selectionText = this.#getSelectionText(selectionRange);
|
||||||
selector.show(entity => {
|
selector.show(entity => {
|
||||||
const selectedText = selectionText || entity.name;
|
const selectedText = selectionText || entity.name;
|
||||||
@ -96,7 +102,7 @@ export class Actions {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await window.$http.post('/images/drawio', data);
|
const resp = await window.$http.post('/images/drawio', data);
|
||||||
this.#insertDrawing(resp.data, selectionRange);
|
this.#insertDrawing(resp.data as ImageManagerImage, selectionRange);
|
||||||
DrawIO.close();
|
DrawIO.close();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.handleDrawingUploadError(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>`;
|
const newText = `<div drawio-diagram="${image.id}"><img src="${image.url}"></div>`;
|
||||||
this.#replaceSelection(newText, newText.length, originalSelectionRange);
|
this.#replaceSelection(newText, newText.length, originalSelectionRange);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show draw.io if enabled and handle save.
|
// Show draw.io if enabled and handle save.
|
||||||
editDrawing(imgContainer) {
|
editDrawing(imgContainer: HTMLElement) {
|
||||||
const {drawioUrl} = this.editor.config;
|
const {drawioUrl} = this.editor.config;
|
||||||
if (!drawioUrl) {
|
if (!drawioUrl) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectionRange = this.#getSelectionRange();
|
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 => {
|
DrawIO.show(drawioUrl, () => DrawIO.load(drawingId), async pngData => {
|
||||||
const data = {
|
const data = {
|
||||||
@ -128,7 +137,8 @@ export class Actions {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await window.$http.post('/images/drawio', data);
|
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 => {
|
const newContent = this.#getText().split('\n').map(line => {
|
||||||
if (line.indexOf(`drawio-diagram="${drawingId}"`) !== -1) {
|
if (line.indexOf(`drawio-diagram="${drawingId}"`) !== -1) {
|
||||||
return newText;
|
return newText;
|
||||||
@ -144,7 +154,7 @@ export class Actions {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDrawingUploadError(error) {
|
handleDrawingUploadError(error: any): void {
|
||||||
if (error.status === 413) {
|
if (error.status === 413) {
|
||||||
window.$events.emit('error', this.editor.config.text.serverUploadLimit);
|
window.$events.emit('error', this.editor.config.text.serverUploadLimit);
|
||||||
} else {
|
} else {
|
||||||
@ -162,7 +172,7 @@ export class Actions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Scroll to a specified text
|
// Scroll to a specified text
|
||||||
scrollToText(searchText) {
|
scrollToText(searchText: string): void {
|
||||||
if (!searchText) {
|
if (!searchText) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -195,17 +205,15 @@ export class Actions {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Insert content into the editor.
|
* Insert content into the editor.
|
||||||
* @param {String} content
|
|
||||||
*/
|
*/
|
||||||
insertContent(content) {
|
insertContent(content: string) {
|
||||||
this.#replaceSelection(content, content.length);
|
this.#replaceSelection(content, content.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prepend content to the editor.
|
* Prepend content to the editor.
|
||||||
* @param {String} content
|
|
||||||
*/
|
*/
|
||||||
prependContent(content) {
|
prependContent(content: string): void {
|
||||||
content = this.#cleanTextForEditor(content);
|
content = this.#cleanTextForEditor(content);
|
||||||
const selectionRange = this.#getSelectionRange();
|
const selectionRange = this.#getSelectionRange();
|
||||||
const selectFrom = selectionRange.from + content.length + 1;
|
const selectFrom = selectionRange.from + content.length + 1;
|
||||||
@ -215,19 +223,18 @@ export class Actions {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Append content to the editor.
|
* Append content to the editor.
|
||||||
* @param {String} content
|
|
||||||
*/
|
*/
|
||||||
appendContent(content) {
|
appendContent(content: string): void {
|
||||||
content = this.#cleanTextForEditor(content);
|
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();
|
this.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Replace the editor's contents
|
* Replace the editor's contents
|
||||||
* @param {String} content
|
|
||||||
*/
|
*/
|
||||||
replaceContent(content) {
|
replaceContent(content: string): void {
|
||||||
this.#setText(content);
|
this.#setText(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -235,7 +242,7 @@ export class Actions {
|
|||||||
* Replace the start of the line
|
* Replace the start of the line
|
||||||
* @param {String} newStart
|
* @param {String} newStart
|
||||||
*/
|
*/
|
||||||
replaceLineStart(newStart) {
|
replaceLineStart(newStart: string): void {
|
||||||
const selectionRange = this.#getSelectionRange();
|
const selectionRange = this.#getSelectionRange();
|
||||||
const line = this.editor.cm.state.doc.lineAt(selectionRange.from);
|
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.
|
* 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 selectRange = this.#getSelectionRange();
|
||||||
const selectionText = this.#getSelectionText(selectRange);
|
const selectionText = this.#getSelectionText(selectRange);
|
||||||
if (!selectionText) {
|
if (!selectionText) {
|
||||||
@ -321,7 +326,7 @@ export class Actions {
|
|||||||
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(line.text) || [''];
|
||||||
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]) {
|
||||||
@ -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
|
// 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;
|
const atEnd = Math.abs(scrollEl.scrollHeight - scrollEl.clientHeight - scrollEl.scrollTop) < 1;
|
||||||
if (atEnd) {
|
if (atEnd) {
|
||||||
this.editor.display.scrollToIndex(-1);
|
this.editor.display.scrollToIndex(-1);
|
||||||
@ -363,25 +368,19 @@ export class Actions {
|
|||||||
/**
|
/**
|
||||||
* Fetch and insert the template of the given ID.
|
* Fetch and insert the template of the given ID.
|
||||||
* 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.
|
||||||
* @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 cursorPos = this.editor.cm.posAtCoords({x: posX, y: posY}, false);
|
||||||
const {data} = await window.$http.get(`/templates/${templateId}`);
|
const responseData = (await window.$http.get(`/templates/${templateId}`)).data as {markdown: string, html: string};
|
||||||
const content = data.markdown || data.html;
|
const content = responseData.markdown || responseData.html;
|
||||||
this.#dispatchChange(cursorPos, cursorPos, content, cursorPos);
|
this.#dispatchChange(cursorPos, cursorPos, content, cursorPos);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Insert multiple images from the clipboard from an event at the provided
|
* Insert multiple images from the clipboard from an event at the provided
|
||||||
* screen coordinates (Typically form a paste event).
|
* 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);
|
const cursorPos = this.editor.cm.posAtCoords({x: posX, y: posY}, false);
|
||||||
for (const image of images) {
|
for (const image of images) {
|
||||||
this.uploadImage(image, cursorPos);
|
this.uploadImage(image, cursorPos);
|
||||||
@ -390,10 +389,8 @@ export class Actions {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle image upload and add image into markdown content
|
* 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;
|
if (file === null || file.type.indexOf('image') !== 0) return;
|
||||||
let ext = 'png';
|
let ext = 'png';
|
||||||
|
|
||||||
@ -403,7 +400,9 @@ export class Actions {
|
|||||||
|
|
||||||
if (file.name) {
|
if (file.name) {
|
||||||
const fileNameMatches = file.name.match(/\.(.+)$/);
|
const fileNameMatches = file.name.match(/\.(.+)$/);
|
||||||
if (fileNameMatches.length > 1) ext = fileNameMatches[1];
|
if (fileNameMatches && fileNameMatches.length > 1) {
|
||||||
|
ext = fileNameMatches[1];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert image into markdown
|
// Insert image into markdown
|
||||||
@ -418,10 +417,10 @@ export class Actions {
|
|||||||
formData.append('uploaded_to', this.editor.config.pageId);
|
formData.append('uploaded_to', this.editor.config.pageId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const {data} = await window.$http.post('/images/gallery', formData);
|
const image = (await window.$http.post('/images/gallery', formData)).data as ImageManagerImage;
|
||||||
const newContent = `[](${data.url})`;
|
const newContent = `[](${image.url})`;
|
||||||
this.#findAndReplaceContent(placeHolderText, newContent);
|
this.#findAndReplaceContent(placeHolderText, newContent);
|
||||||
} catch (err) {
|
} catch (err: any) {
|
||||||
window.$events.error(err?.data?.message || this.editor.config.text.imageUploadError);
|
window.$events.error(err?.data?.message || this.editor.config.text.imageUploadError);
|
||||||
this.#findAndReplaceContent(placeHolderText, '');
|
this.#findAndReplaceContent(placeHolderText, '');
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@ -438,10 +437,8 @@ export class Actions {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the text of the current editor instance.
|
* 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();
|
selectionRange = selectionRange || this.#getSelectionRange();
|
||||||
const newDoc = this.editor.cm.state.toText(text);
|
const newDoc = this.editor.cm.state.toText(text);
|
||||||
const newSelectFrom = Math.min(selectionRange.from, newDoc.length);
|
const newSelectFrom = Math.min(selectionRange.from, newDoc.length);
|
||||||
@ -457,12 +454,9 @@ export class Actions {
|
|||||||
* 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.
|
||||||
* @param {String} newContent
|
|
||||||
* @param {Number} cursorOffset
|
|
||||||
* @param {?SelectionRange} selectionRange
|
|
||||||
*/
|
*/
|
||||||
#replaceSelection(newContent, cursorOffset = 0, selectionRange = null) {
|
#replaceSelection(newContent: string, cursorOffset: number = 0, selectionRange: SelectionRange|null = null) {
|
||||||
selectionRange = selectionRange || this.editor.cm.state.selection.main;
|
selectionRange = selectionRange || this.#getSelectionRange();
|
||||||
const selectFrom = selectionRange.from + cursorOffset;
|
const selectFrom = selectionRange.from + cursorOffset;
|
||||||
this.#dispatchChange(selectionRange.from, selectionRange.to, newContent, selectFrom);
|
this.#dispatchChange(selectionRange.from, selectionRange.to, newContent, selectFrom);
|
||||||
this.focus();
|
this.focus();
|
||||||
@ -470,48 +464,39 @@ export class Actions {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the text content of the main current selection.
|
* 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();
|
selectionRange = selectionRange || this.#getSelectionRange();
|
||||||
return this.editor.cm.state.sliceDoc(selectionRange.from, selectionRange.to);
|
return this.editor.cm.state.sliceDoc(selectionRange.from, selectionRange.to);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the range of the current main selection.
|
* Get the range of the current main selection.
|
||||||
* @return {SelectionRange}
|
|
||||||
*/
|
*/
|
||||||
#getSelectionRange() {
|
#getSelectionRange(): SelectionRange {
|
||||||
return this.editor.cm.state.selection.main;
|
return this.editor.cm.state.selection.main;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cleans the given text to work with the editor.
|
* Cleans the given text to work with the editor.
|
||||||
* Standardises line endings to what's expected.
|
* 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');
|
return text.replace(/\r\n|\r/g, '\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find and replace the first occurrence of [search] with [replace]
|
* 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);
|
const newText = this.#getText().replace(search, replace);
|
||||||
this.#setText(newText);
|
this.#setText(newText);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wrap the line in the given start and end contents.
|
* 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 selectionRange = this.#getSelectionRange();
|
||||||
const line = this.editor.cm.state.doc.lineAt(selectionRange.from);
|
const line = this.editor.cm.state.doc.lineAt(selectionRange.from);
|
||||||
const lineContent = line.text;
|
const lineContent = line.text;
|
||||||
@ -531,14 +516,16 @@ export class Actions {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Dispatch changes to the editor.
|
* 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) {
|
#dispatchChange(from: number, to: number|null = null, text: string|null = null, selectFrom: number|null = null, selectTo: number|null = null): void {
|
||||||
const tr = {changes: {from, to, insert: text}};
|
const change: ChangeSpec = {from};
|
||||||
|
if (to) {
|
||||||
|
change.to = to;
|
||||||
|
}
|
||||||
|
if (text) {
|
||||||
|
change.insert = text;
|
||||||
|
}
|
||||||
|
const tr: TransactionSpec = {changes: change};
|
||||||
|
|
||||||
if (selectFrom) {
|
if (selectFrom) {
|
||||||
tr.selection = {anchor: selectFrom};
|
tr.selection = {anchor: selectFrom};
|
||||||
@ -557,7 +544,7 @@ export class Actions {
|
|||||||
* @param {Number} to
|
* @param {Number} to
|
||||||
* @param {Boolean} scrollIntoView
|
* @param {Boolean} scrollIntoView
|
||||||
*/
|
*/
|
||||||
#setSelection(from, to, scrollIntoView = false) {
|
#setSelection(from: number, to: number, scrollIntoView = false) {
|
||||||
this.editor.cm.dispatch({
|
this.editor.cm.dispatch({
|
||||||
selection: {anchor: from, head: to},
|
selection: {anchor: from, head: to},
|
||||||
scrollIntoView,
|
scrollIntoView,
|
@ -5,7 +5,7 @@ import {Clipboard} from '../services/clipboard.ts';
|
|||||||
/**
|
/**
|
||||||
* Initiate the codemirror instance for the markdown editor.
|
* Initiate the codemirror instance for the markdown editor.
|
||||||
* @param {MarkdownEditor} editor
|
* @param {MarkdownEditor} editor
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<EditorView>}
|
||||||
*/
|
*/
|
||||||
export async function init(editor) {
|
export async function init(editor) {
|
||||||
const Code = await window.importVersioned('code');
|
const Code = await window.importVersioned('code');
|
||||||
|
@ -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();
|
|
||||||
});
|
|
||||||
}
|
|
36
resources/js/markdown/common-events.ts
Normal file
36
resources/js/markdown/common-events.ts
Normal file
@ -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();
|
||||||
|
});
|
||||||
|
}
|
@ -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
|
|
||||||
*/
|
|
52
resources/js/markdown/index.mts
Normal file
52
resources/js/markdown/index.mts
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -5,6 +5,7 @@ export class Settings {
|
|||||||
scrollSync: true,
|
scrollSync: true,
|
||||||
showPreview: true,
|
showPreview: true,
|
||||||
editorWidth: 50,
|
editorWidth: 50,
|
||||||
|
plainEditor: false,
|
||||||
};
|
};
|
||||||
this.changeListeners = {};
|
this.changeListeners = {};
|
||||||
this.loadFromLocalStorage();
|
this.loadFromLocalStorage();
|
||||||
|
@ -26,6 +26,10 @@
|
|||||||
<div class="px-m">
|
<div class="px-m">
|
||||||
@include('form.custom-checkbox', ['name' => 'md-scrollSync', 'label' => trans('entities.pages_md_sync_scroll'), 'value' => true, 'checked' => true])
|
@include('form.custom-checkbox', ['name' => 'md-scrollSync', 'label' => trans('entities.pages_md_sync_scroll'), 'value' => true, 'checked' => true])
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Reference in New Issue
Block a user