mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-07-28 17:02:04 +03:00
MD Editor: Finished conversion to Typescript
This commit is contained in:
@ -1,19 +1,16 @@
|
||||
import {provideKeyBindings} from './shortcuts';
|
||||
import {debounce} from '../services/util.ts';
|
||||
import {Clipboard} from '../services/clipboard.ts';
|
||||
import {debounce} from '../services/util';
|
||||
import {Clipboard} from '../services/clipboard';
|
||||
import {EditorView, ViewUpdate} from "@codemirror/view";
|
||||
import {MarkdownEditor} from "./index.mjs";
|
||||
|
||||
/**
|
||||
* Initiate the codemirror instance for the markdown editor.
|
||||
* @param {MarkdownEditor} editor
|
||||
* @returns {Promise<EditorView>}
|
||||
*/
|
||||
export async function init(editor) {
|
||||
const Code = await window.importVersioned('code');
|
||||
export async function init(editor: MarkdownEditor): Promise<EditorView> {
|
||||
const Code = await window.importVersioned('code') as (typeof import('../code/index.mjs'));
|
||||
|
||||
/**
|
||||
* @param {ViewUpdate} v
|
||||
*/
|
||||
function onViewUpdate(v) {
|
||||
function onViewUpdate(v: ViewUpdate) {
|
||||
if (v.docChanged) {
|
||||
editor.actions.updateAndRender();
|
||||
}
|
||||
@ -27,9 +24,13 @@ export async function init(editor) {
|
||||
|
||||
const domEventHandlers = {
|
||||
// Handle scroll to sync display view
|
||||
scroll: event => syncActive && onScrollDebounced(event),
|
||||
scroll: (event: Event) => syncActive && onScrollDebounced(event),
|
||||
// Handle image & content drag n drop
|
||||
drop: event => {
|
||||
drop: (event: DragEvent) => {
|
||||
if (!event.dataTransfer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const templateId = event.dataTransfer.getData('bookstack/template');
|
||||
if (templateId) {
|
||||
event.preventDefault();
|
||||
@ -45,12 +46,16 @@ export async function init(editor) {
|
||||
}
|
||||
},
|
||||
// Handle dragover event to allow as drop-target in chrome
|
||||
dragover: event => {
|
||||
dragover: (event: DragEvent) => {
|
||||
event.preventDefault();
|
||||
},
|
||||
// Handle image paste
|
||||
paste: event => {
|
||||
const clipboard = new Clipboard(event.clipboardData || event.dataTransfer);
|
||||
paste: (event: ClipboardEvent) => {
|
||||
if (!event.clipboardData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const clipboard = new Clipboard(event.clipboardData);
|
||||
|
||||
// Don't handle the event ourselves if no items exist of contains table-looking data
|
||||
if (!clipboard.hasItems() || clipboard.containsTabularData()) {
|
||||
@ -71,8 +76,9 @@ export async function init(editor) {
|
||||
provideKeyBindings(editor),
|
||||
);
|
||||
|
||||
// Add editor view to window for easy access/debugging.
|
||||
// Add editor view to the window for easy access/debugging.
|
||||
// Not part of official API/Docs
|
||||
// @ts-ignore
|
||||
window.mdEditorView = cm;
|
||||
|
||||
return cm;
|
@ -1,35 +1,36 @@
|
||||
import {patchDomFromHtmlString} from '../services/vdom.ts';
|
||||
import { patchDomFromHtmlString } from '../services/vdom';
|
||||
import {MarkdownEditor} from "./index.mjs";
|
||||
|
||||
export class Display {
|
||||
protected editor: MarkdownEditor;
|
||||
protected container: HTMLIFrameElement;
|
||||
protected doc: Document | null = null;
|
||||
protected lastDisplayClick: number = 0;
|
||||
|
||||
/**
|
||||
* @param {MarkdownEditor} editor
|
||||
*/
|
||||
constructor(editor) {
|
||||
constructor(editor: MarkdownEditor) {
|
||||
this.editor = editor;
|
||||
this.container = editor.config.displayEl;
|
||||
|
||||
this.doc = null;
|
||||
this.lastDisplayClick = 0;
|
||||
|
||||
if (this.container.contentDocument.readyState === 'complete') {
|
||||
if (this.container.contentDocument?.readyState === 'complete') {
|
||||
this.onLoad();
|
||||
} else {
|
||||
this.container.addEventListener('load', this.onLoad.bind(this));
|
||||
}
|
||||
|
||||
this.updateVisibility(editor.settings.get('showPreview'));
|
||||
editor.settings.onChange('showPreview', show => this.updateVisibility(show));
|
||||
this.updateVisibility(Boolean(editor.settings.get('showPreview')));
|
||||
editor.settings.onChange('showPreview', (show) => this.updateVisibility(Boolean(show)));
|
||||
}
|
||||
|
||||
updateVisibility(show) {
|
||||
const wrap = this.container.closest('.markdown-editor-wrap');
|
||||
wrap.style.display = show ? null : 'none';
|
||||
protected updateVisibility(show: boolean): void {
|
||||
const wrap = this.container.closest('.markdown-editor-wrap') as HTMLElement;
|
||||
wrap.style.display = show ? '' : 'none';
|
||||
}
|
||||
|
||||
onLoad() {
|
||||
protected onLoad(): void {
|
||||
this.doc = this.container.contentDocument;
|
||||
|
||||
if (!this.doc) return;
|
||||
|
||||
this.loadStylesIntoDisplay();
|
||||
this.doc.body.className = 'page-content';
|
||||
|
||||
@ -37,20 +38,20 @@ export class Display {
|
||||
this.doc.addEventListener('click', this.onDisplayClick.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
onDisplayClick(event) {
|
||||
protected onDisplayClick(event: MouseEvent): void {
|
||||
const isDblClick = Date.now() - this.lastDisplayClick < 300;
|
||||
|
||||
const link = event.target.closest('a');
|
||||
const link = (event.target as Element).closest('a');
|
||||
if (link !== null) {
|
||||
event.preventDefault();
|
||||
window.open(link.getAttribute('href'));
|
||||
const href = link.getAttribute('href');
|
||||
if (href) {
|
||||
window.open(href);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const drawing = event.target.closest('[drawio-diagram]');
|
||||
const drawing = (event.target as Element).closest('[drawio-diagram]') as HTMLElement;
|
||||
if (drawing !== null && isDblClick) {
|
||||
this.editor.actions.editDrawing(drawing);
|
||||
return;
|
||||
@ -59,10 +60,12 @@ export class Display {
|
||||
this.lastDisplayClick = Date.now();
|
||||
}
|
||||
|
||||
loadStylesIntoDisplay() {
|
||||
protected loadStylesIntoDisplay(): void {
|
||||
if (!this.doc) return;
|
||||
|
||||
this.doc.documentElement.classList.add('markdown-editor-display');
|
||||
|
||||
// Set display to be dark mode if parent is
|
||||
// Set display to be dark mode if the parent is
|
||||
if (document.documentElement.classList.contains('dark-mode')) {
|
||||
this.doc.documentElement.style.backgroundColor = '#222';
|
||||
this.doc.documentElement.classList.add('dark-mode');
|
||||
@ -71,24 +74,25 @@ export class Display {
|
||||
this.doc.head.innerHTML = '';
|
||||
const styles = document.head.querySelectorAll('style,link[rel=stylesheet]');
|
||||
for (const style of styles) {
|
||||
const copy = style.cloneNode(true);
|
||||
const copy = style.cloneNode(true) as HTMLElement;
|
||||
this.doc.head.appendChild(copy);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Patch the display DOM with the given HTML content.
|
||||
* @param {String} html
|
||||
*/
|
||||
patchWithHtml(html) {
|
||||
const {body} = this.doc;
|
||||
public patchWithHtml(html: string): void {
|
||||
if (!this.doc) return;
|
||||
|
||||
const { body } = this.doc;
|
||||
|
||||
if (body.children.length === 0) {
|
||||
const wrap = document.createElement('div');
|
||||
this.doc.body.append(wrap);
|
||||
}
|
||||
|
||||
const target = body.children[0];
|
||||
const target = body.children[0] as HTMLElement;
|
||||
|
||||
patchDomFromHtmlString(target, html);
|
||||
}
|
||||
@ -96,14 +100,16 @@ export class Display {
|
||||
/**
|
||||
* Scroll to the given block index within the display content.
|
||||
* Will scroll to the end if the index is -1.
|
||||
* @param {Number} index
|
||||
*/
|
||||
scrollToIndex(index) {
|
||||
const elems = this.doc.body?.children[0]?.children;
|
||||
if (elems && elems.length <= index) return;
|
||||
public scrollToIndex(index: number): void {
|
||||
const elems = this.doc?.body?.children[0]?.children;
|
||||
if (!elems || elems.length <= index) return;
|
||||
|
||||
const topElem = (index === -1) ? elems[elems.length - 1] : elems[index];
|
||||
topElem.scrollIntoView({block: 'start', inline: 'nearest', behavior: 'smooth'});
|
||||
(topElem as Element).scrollIntoView({
|
||||
block: 'start',
|
||||
inline: 'nearest',
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -9,7 +9,7 @@ import {EditorView} from "@codemirror/view";
|
||||
export interface MarkdownEditorConfig {
|
||||
pageId: string;
|
||||
container: Element;
|
||||
displayEl: Element;
|
||||
displayEl: HTMLIFrameElement;
|
||||
inputEl: HTMLTextAreaElement;
|
||||
drawioUrl: string;
|
||||
settingInputs: HTMLInputElement[];
|
||||
@ -27,18 +27,13 @@ export interface MarkdownEditor {
|
||||
|
||||
/**
|
||||
* Initiate a new Markdown editor instance.
|
||||
* @param {MarkdownEditorConfig} config
|
||||
* @returns {Promise<MarkdownEditor>}
|
||||
*/
|
||||
export async function init(config) {
|
||||
/**
|
||||
* @type {MarkdownEditor}
|
||||
*/
|
||||
export async function init(config: MarkdownEditorConfig): Promise<MarkdownEditor> {
|
||||
const editor: MarkdownEditor = {
|
||||
config,
|
||||
markdown: new Markdown(),
|
||||
settings: new Settings(config.settingInputs),
|
||||
};
|
||||
} as MarkdownEditor;
|
||||
|
||||
editor.actions = new Actions(editor);
|
||||
editor.display = new Display(editor);
|
||||
|
@ -1,7 +1,9 @@
|
||||
import MarkdownIt from 'markdown-it';
|
||||
// @ts-ignore
|
||||
import mdTasksLists from 'markdown-it-task-lists';
|
||||
|
||||
export class Markdown {
|
||||
protected renderer: MarkdownIt;
|
||||
|
||||
constructor() {
|
||||
this.renderer = new MarkdownIt({html: true});
|
||||
@ -9,19 +11,16 @@ export class Markdown {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the front-end render used to convert markdown to HTML.
|
||||
* @returns {MarkdownIt}
|
||||
* Get the front-end render used to convert Markdown to HTML.
|
||||
*/
|
||||
getRenderer() {
|
||||
getRenderer(): MarkdownIt {
|
||||
return this.renderer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the given Markdown to HTML.
|
||||
* @param {String} markdown
|
||||
* @returns {String}
|
||||
*/
|
||||
render(markdown) {
|
||||
render(markdown: string): string {
|
||||
return this.renderer.render(markdown);
|
||||
}
|
||||
|
@ -1,64 +0,0 @@
|
||||
export class Settings {
|
||||
|
||||
constructor(settingInputs) {
|
||||
this.settingMap = {
|
||||
scrollSync: true,
|
||||
showPreview: true,
|
||||
editorWidth: 50,
|
||||
plainEditor: false,
|
||||
};
|
||||
this.changeListeners = {};
|
||||
this.loadFromLocalStorage();
|
||||
this.applyToInputs(settingInputs);
|
||||
this.listenToInputChanges(settingInputs);
|
||||
}
|
||||
|
||||
applyToInputs(inputs) {
|
||||
for (const input of inputs) {
|
||||
const name = input.getAttribute('name').replace('md-', '');
|
||||
input.checked = this.settingMap[name];
|
||||
}
|
||||
}
|
||||
|
||||
listenToInputChanges(inputs) {
|
||||
for (const input of inputs) {
|
||||
input.addEventListener('change', () => {
|
||||
const name = input.getAttribute('name').replace('md-', '');
|
||||
this.set(name, input.checked);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
loadFromLocalStorage() {
|
||||
const lsValString = window.localStorage.getItem('md-editor-settings');
|
||||
if (!lsValString) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lsVals = JSON.parse(lsValString);
|
||||
for (const [key, value] of Object.entries(lsVals)) {
|
||||
if (value !== null && this.settingMap[key] !== undefined) {
|
||||
this.settingMap[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
set(key, value) {
|
||||
this.settingMap[key] = value;
|
||||
window.localStorage.setItem('md-editor-settings', JSON.stringify(this.settingMap));
|
||||
for (const listener of (this.changeListeners[key] || [])) {
|
||||
listener(value);
|
||||
}
|
||||
}
|
||||
|
||||
get(key) {
|
||||
return this.settingMap[key] || null;
|
||||
}
|
||||
|
||||
onChange(key, callback) {
|
||||
const listeners = this.changeListeners[key] || [];
|
||||
listeners.push(callback);
|
||||
this.changeListeners[key] = listeners;
|
||||
}
|
||||
|
||||
}
|
82
resources/js/markdown/settings.ts
Normal file
82
resources/js/markdown/settings.ts
Normal file
@ -0,0 +1,82 @@
|
||||
type ChangeListener = (value: boolean|number) => void;
|
||||
|
||||
export class Settings {
|
||||
protected changeListeners: Record<string, ChangeListener[]> = {};
|
||||
|
||||
protected settingMap: Record<string, boolean|number> = {
|
||||
scrollSync: true,
|
||||
showPreview: true,
|
||||
editorWidth: 50,
|
||||
plainEditor: false,
|
||||
};
|
||||
|
||||
constructor(settingInputs: HTMLInputElement[]) {
|
||||
this.loadFromLocalStorage();
|
||||
this.applyToInputs(settingInputs);
|
||||
this.listenToInputChanges(settingInputs);
|
||||
}
|
||||
|
||||
protected applyToInputs(inputs: HTMLInputElement[]): void {
|
||||
for (const input of inputs) {
|
||||
const name = input.getAttribute('name')?.replace('md-', '');
|
||||
if (name && name in this.settingMap) {
|
||||
const value = this.settingMap[name];
|
||||
if (typeof value === 'boolean') {
|
||||
input.checked = value;
|
||||
} else {
|
||||
input.value = value.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected listenToInputChanges(inputs: HTMLInputElement[]): void {
|
||||
for (const input of inputs) {
|
||||
input.addEventListener('change', () => {
|
||||
const name = input.getAttribute('name')?.replace('md-', '');
|
||||
if (name && name in this.settingMap) {
|
||||
let value = (input.type === 'checkbox') ? input.checked : Number(input.value);
|
||||
this.set(name, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected loadFromLocalStorage(): void {
|
||||
const lsValString = window.localStorage.getItem('md-editor-settings');
|
||||
if (!lsValString) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const lsVals = JSON.parse(lsValString);
|
||||
for (const [key, value] of Object.entries(lsVals)) {
|
||||
if (value !== null && value !== undefined && key in this.settingMap) {
|
||||
this.settingMap[key] = value as boolean|number;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to parse settings from localStorage:', error);
|
||||
}
|
||||
}
|
||||
|
||||
public set(key: string, value: boolean|number): void {
|
||||
this.settingMap[key] = value;
|
||||
window.localStorage.setItem('md-editor-settings', JSON.stringify(this.settingMap));
|
||||
|
||||
const listeners = this.changeListeners[key] || [];
|
||||
for (const listener of listeners) {
|
||||
listener(value);
|
||||
}
|
||||
}
|
||||
|
||||
public get(key: string): number|boolean|null {
|
||||
return this.settingMap[key] ?? null;
|
||||
}
|
||||
|
||||
public onChange(key: string, callback: ChangeListener): void {
|
||||
const listeners = this.changeListeners[key] || [];
|
||||
listeners.push(callback);
|
||||
this.changeListeners[key] = listeners;
|
||||
}
|
||||
}
|
@ -1,10 +1,11 @@
|
||||
import {MarkdownEditor} from "./index.mjs";
|
||||
import {KeyBinding} from "@codemirror/view";
|
||||
|
||||
/**
|
||||
* Provide shortcuts for the editor instance.
|
||||
* @param {MarkdownEditor} editor
|
||||
* @returns {Object<String, Function>}
|
||||
*/
|
||||
function provide(editor) {
|
||||
const shortcuts = {};
|
||||
function provide(editor: MarkdownEditor): Record<string, () => void> {
|
||||
const shortcuts: Record<string, () => void> = {};
|
||||
|
||||
// Insert Image shortcut
|
||||
shortcuts['Shift-Mod-i'] = () => editor.actions.insertImage();
|
||||
@ -42,14 +43,12 @@ function provide(editor) {
|
||||
|
||||
/**
|
||||
* Get the editor shortcuts in CodeMirror keybinding format.
|
||||
* @param {MarkdownEditor} editor
|
||||
* @return {{key: String, run: function, preventDefault: boolean}[]}
|
||||
*/
|
||||
export function provideKeyBindings(editor) {
|
||||
export function provideKeyBindings(editor: MarkdownEditor): KeyBinding[] {
|
||||
const shortcuts = provide(editor);
|
||||
const keyBindings = [];
|
||||
|
||||
const wrapAction = action => () => {
|
||||
const wrapAction = (action: ()=>void) => () => {
|
||||
action();
|
||||
return true;
|
||||
};
|
Reference in New Issue
Block a user