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:
26
package-lock.json
generated
26
package-lock.json
generated
@ -32,6 +32,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.21.0",
|
"@eslint/js": "^9.21.0",
|
||||||
"@lezer/generator": "^1.7.2",
|
"@lezer/generator": "^1.7.2",
|
||||||
|
"@types/markdown-it": "^14.1.2",
|
||||||
"@types/sortablejs": "^1.15.8",
|
"@types/sortablejs": "^1.15.8",
|
||||||
"chokidar-cli": "^3.0",
|
"chokidar-cli": "^3.0",
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
@ -2508,6 +2509,31 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/linkify-it": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/markdown-it": {
|
||||||
|
"version": "14.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
|
||||||
|
"integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/linkify-it": "^5",
|
||||||
|
"@types/mdurl": "^2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/mdurl": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "22.15.21",
|
"version": "22.15.21",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz",
|
||||||
|
@ -21,6 +21,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.21.0",
|
"@eslint/js": "^9.21.0",
|
||||||
"@lezer/generator": "^1.7.2",
|
"@lezer/generator": "^1.7.2",
|
||||||
|
"@types/markdown-it": "^14.1.2",
|
||||||
"@types/sortablejs": "^1.15.8",
|
"@types/sortablejs": "^1.15.8",
|
||||||
"chokidar-cli": "^3.0",
|
"chokidar-cli": "^3.0",
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
|
@ -1,19 +1,16 @@
|
|||||||
import {provideKeyBindings} from './shortcuts';
|
import {provideKeyBindings} from './shortcuts';
|
||||||
import {debounce} from '../services/util.ts';
|
import {debounce} from '../services/util';
|
||||||
import {Clipboard} from '../services/clipboard.ts';
|
import {Clipboard} from '../services/clipboard';
|
||||||
|
import {EditorView, ViewUpdate} from "@codemirror/view";
|
||||||
|
import {MarkdownEditor} from "./index.mjs";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initiate the codemirror instance for the markdown editor.
|
* Initiate the codemirror instance for the markdown editor.
|
||||||
* @param {MarkdownEditor} editor
|
|
||||||
* @returns {Promise<EditorView>}
|
|
||||||
*/
|
*/
|
||||||
export async function init(editor) {
|
export async function init(editor: MarkdownEditor): Promise<EditorView> {
|
||||||
const Code = await window.importVersioned('code');
|
const Code = await window.importVersioned('code') as (typeof import('../code/index.mjs'));
|
||||||
|
|
||||||
/**
|
function onViewUpdate(v: ViewUpdate) {
|
||||||
* @param {ViewUpdate} v
|
|
||||||
*/
|
|
||||||
function onViewUpdate(v) {
|
|
||||||
if (v.docChanged) {
|
if (v.docChanged) {
|
||||||
editor.actions.updateAndRender();
|
editor.actions.updateAndRender();
|
||||||
}
|
}
|
||||||
@ -27,9 +24,13 @@ export async function init(editor) {
|
|||||||
|
|
||||||
const domEventHandlers = {
|
const domEventHandlers = {
|
||||||
// Handle scroll to sync display view
|
// Handle scroll to sync display view
|
||||||
scroll: event => syncActive && onScrollDebounced(event),
|
scroll: (event: Event) => syncActive && onScrollDebounced(event),
|
||||||
// Handle image & content drag n drop
|
// Handle image & content drag n drop
|
||||||
drop: event => {
|
drop: (event: DragEvent) => {
|
||||||
|
if (!event.dataTransfer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const templateId = event.dataTransfer.getData('bookstack/template');
|
const templateId = event.dataTransfer.getData('bookstack/template');
|
||||||
if (templateId) {
|
if (templateId) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@ -45,12 +46,16 @@ export async function init(editor) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Handle dragover event to allow as drop-target in chrome
|
// Handle dragover event to allow as drop-target in chrome
|
||||||
dragover: event => {
|
dragover: (event: DragEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
},
|
},
|
||||||
// Handle image paste
|
// Handle image paste
|
||||||
paste: event => {
|
paste: (event: ClipboardEvent) => {
|
||||||
const clipboard = new Clipboard(event.clipboardData || event.dataTransfer);
|
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
|
// Don't handle the event ourselves if no items exist of contains table-looking data
|
||||||
if (!clipboard.hasItems() || clipboard.containsTabularData()) {
|
if (!clipboard.hasItems() || clipboard.containsTabularData()) {
|
||||||
@ -71,8 +76,9 @@ export async function init(editor) {
|
|||||||
provideKeyBindings(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
|
// Not part of official API/Docs
|
||||||
|
// @ts-ignore
|
||||||
window.mdEditorView = cm;
|
window.mdEditorView = cm;
|
||||||
|
|
||||||
return 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 {
|
export class Display {
|
||||||
|
protected editor: MarkdownEditor;
|
||||||
|
protected container: HTMLIFrameElement;
|
||||||
|
protected doc: Document | null = null;
|
||||||
|
protected lastDisplayClick: number = 0;
|
||||||
|
|
||||||
/**
|
constructor(editor: MarkdownEditor) {
|
||||||
* @param {MarkdownEditor} editor
|
|
||||||
*/
|
|
||||||
constructor(editor) {
|
|
||||||
this.editor = editor;
|
this.editor = editor;
|
||||||
this.container = editor.config.displayEl;
|
this.container = editor.config.displayEl;
|
||||||
|
|
||||||
this.doc = null;
|
if (this.container.contentDocument?.readyState === 'complete') {
|
||||||
this.lastDisplayClick = 0;
|
|
||||||
|
|
||||||
if (this.container.contentDocument.readyState === 'complete') {
|
|
||||||
this.onLoad();
|
this.onLoad();
|
||||||
} else {
|
} else {
|
||||||
this.container.addEventListener('load', this.onLoad.bind(this));
|
this.container.addEventListener('load', this.onLoad.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updateVisibility(editor.settings.get('showPreview'));
|
this.updateVisibility(Boolean(editor.settings.get('showPreview')));
|
||||||
editor.settings.onChange('showPreview', show => this.updateVisibility(show));
|
editor.settings.onChange('showPreview', (show) => this.updateVisibility(Boolean(show)));
|
||||||
}
|
}
|
||||||
|
|
||||||
updateVisibility(show) {
|
protected updateVisibility(show: boolean): void {
|
||||||
const wrap = this.container.closest('.markdown-editor-wrap');
|
const wrap = this.container.closest('.markdown-editor-wrap') as HTMLElement;
|
||||||
wrap.style.display = show ? null : 'none';
|
wrap.style.display = show ? '' : 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
onLoad() {
|
protected onLoad(): void {
|
||||||
this.doc = this.container.contentDocument;
|
this.doc = this.container.contentDocument;
|
||||||
|
|
||||||
|
if (!this.doc) return;
|
||||||
|
|
||||||
this.loadStylesIntoDisplay();
|
this.loadStylesIntoDisplay();
|
||||||
this.doc.body.className = 'page-content';
|
this.doc.body.className = 'page-content';
|
||||||
|
|
||||||
@ -37,20 +38,20 @@ export class Display {
|
|||||||
this.doc.addEventListener('click', this.onDisplayClick.bind(this));
|
this.doc.addEventListener('click', this.onDisplayClick.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
protected onDisplayClick(event: MouseEvent): void {
|
||||||
* @param {MouseEvent} event
|
|
||||||
*/
|
|
||||||
onDisplayClick(event) {
|
|
||||||
const isDblClick = Date.now() - this.lastDisplayClick < 300;
|
const isDblClick = Date.now() - this.lastDisplayClick < 300;
|
||||||
|
|
||||||
const link = event.target.closest('a');
|
const link = (event.target as Element).closest('a');
|
||||||
if (link !== null) {
|
if (link !== null) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
window.open(link.getAttribute('href'));
|
const href = link.getAttribute('href');
|
||||||
|
if (href) {
|
||||||
|
window.open(href);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const drawing = event.target.closest('[drawio-diagram]');
|
const drawing = (event.target as Element).closest('[drawio-diagram]') as HTMLElement;
|
||||||
if (drawing !== null && isDblClick) {
|
if (drawing !== null && isDblClick) {
|
||||||
this.editor.actions.editDrawing(drawing);
|
this.editor.actions.editDrawing(drawing);
|
||||||
return;
|
return;
|
||||||
@ -59,10 +60,12 @@ export class Display {
|
|||||||
this.lastDisplayClick = Date.now();
|
this.lastDisplayClick = Date.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
loadStylesIntoDisplay() {
|
protected loadStylesIntoDisplay(): void {
|
||||||
|
if (!this.doc) return;
|
||||||
|
|
||||||
this.doc.documentElement.classList.add('markdown-editor-display');
|
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')) {
|
if (document.documentElement.classList.contains('dark-mode')) {
|
||||||
this.doc.documentElement.style.backgroundColor = '#222';
|
this.doc.documentElement.style.backgroundColor = '#222';
|
||||||
this.doc.documentElement.classList.add('dark-mode');
|
this.doc.documentElement.classList.add('dark-mode');
|
||||||
@ -71,24 +74,25 @@ export class Display {
|
|||||||
this.doc.head.innerHTML = '';
|
this.doc.head.innerHTML = '';
|
||||||
const styles = document.head.querySelectorAll('style,link[rel=stylesheet]');
|
const styles = document.head.querySelectorAll('style,link[rel=stylesheet]');
|
||||||
for (const style of styles) {
|
for (const style of styles) {
|
||||||
const copy = style.cloneNode(true);
|
const copy = style.cloneNode(true) as HTMLElement;
|
||||||
this.doc.head.appendChild(copy);
|
this.doc.head.appendChild(copy);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Patch the display DOM with the given HTML content.
|
* Patch the display DOM with the given HTML content.
|
||||||
* @param {String} html
|
|
||||||
*/
|
*/
|
||||||
patchWithHtml(html) {
|
public patchWithHtml(html: string): void {
|
||||||
const {body} = this.doc;
|
if (!this.doc) return;
|
||||||
|
|
||||||
|
const { body } = this.doc;
|
||||||
|
|
||||||
if (body.children.length === 0) {
|
if (body.children.length === 0) {
|
||||||
const wrap = document.createElement('div');
|
const wrap = document.createElement('div');
|
||||||
this.doc.body.append(wrap);
|
this.doc.body.append(wrap);
|
||||||
}
|
}
|
||||||
|
|
||||||
const target = body.children[0];
|
const target = body.children[0] as HTMLElement;
|
||||||
|
|
||||||
patchDomFromHtmlString(target, html);
|
patchDomFromHtmlString(target, html);
|
||||||
}
|
}
|
||||||
@ -96,14 +100,16 @@ export class Display {
|
|||||||
/**
|
/**
|
||||||
* Scroll to the given block index within the display content.
|
* Scroll to the given block index within the display content.
|
||||||
* Will scroll to the end if the index is -1.
|
* Will scroll to the end if the index is -1.
|
||||||
* @param {Number} index
|
|
||||||
*/
|
*/
|
||||||
scrollToIndex(index) {
|
public scrollToIndex(index: number): void {
|
||||||
const elems = this.doc.body?.children[0]?.children;
|
const elems = this.doc?.body?.children[0]?.children;
|
||||||
if (elems && elems.length <= index) return;
|
if (!elems || elems.length <= index) return;
|
||||||
|
|
||||||
const topElem = (index === -1) ? elems[elems.length - 1] : elems[index];
|
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 {
|
export interface MarkdownEditorConfig {
|
||||||
pageId: string;
|
pageId: string;
|
||||||
container: Element;
|
container: Element;
|
||||||
displayEl: Element;
|
displayEl: HTMLIFrameElement;
|
||||||
inputEl: HTMLTextAreaElement;
|
inputEl: HTMLTextAreaElement;
|
||||||
drawioUrl: string;
|
drawioUrl: string;
|
||||||
settingInputs: HTMLInputElement[];
|
settingInputs: HTMLInputElement[];
|
||||||
@ -27,18 +27,13 @@ export interface MarkdownEditor {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Initiate a new Markdown editor instance.
|
* Initiate a new Markdown editor instance.
|
||||||
* @param {MarkdownEditorConfig} config
|
|
||||||
* @returns {Promise<MarkdownEditor>}
|
|
||||||
*/
|
*/
|
||||||
export async function init(config) {
|
export async function init(config: MarkdownEditorConfig): Promise<MarkdownEditor> {
|
||||||
/**
|
|
||||||
* @type {MarkdownEditor}
|
|
||||||
*/
|
|
||||||
const editor: MarkdownEditor = {
|
const editor: MarkdownEditor = {
|
||||||
config,
|
config,
|
||||||
markdown: new Markdown(),
|
markdown: new Markdown(),
|
||||||
settings: new Settings(config.settingInputs),
|
settings: new Settings(config.settingInputs),
|
||||||
};
|
} as MarkdownEditor;
|
||||||
|
|
||||||
editor.actions = new Actions(editor);
|
editor.actions = new Actions(editor);
|
||||||
editor.display = new Display(editor);
|
editor.display = new Display(editor);
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import MarkdownIt from 'markdown-it';
|
import MarkdownIt from 'markdown-it';
|
||||||
|
// @ts-ignore
|
||||||
import mdTasksLists from 'markdown-it-task-lists';
|
import mdTasksLists from 'markdown-it-task-lists';
|
||||||
|
|
||||||
export class Markdown {
|
export class Markdown {
|
||||||
|
protected renderer: MarkdownIt;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.renderer = new MarkdownIt({html: true});
|
this.renderer = new MarkdownIt({html: true});
|
||||||
@ -9,19 +11,16 @@ export class Markdown {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the front-end render used to convert markdown to HTML.
|
* Get the front-end render used to convert Markdown to HTML.
|
||||||
* @returns {MarkdownIt}
|
|
||||||
*/
|
*/
|
||||||
getRenderer() {
|
getRenderer(): MarkdownIt {
|
||||||
return this.renderer;
|
return this.renderer;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert the given Markdown to HTML.
|
* Convert the given Markdown to HTML.
|
||||||
* @param {String} markdown
|
|
||||||
* @returns {String}
|
|
||||||
*/
|
*/
|
||||||
render(markdown) {
|
render(markdown: string): string {
|
||||||
return this.renderer.render(markdown);
|
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.
|
* Provide shortcuts for the editor instance.
|
||||||
* @param {MarkdownEditor} editor
|
|
||||||
* @returns {Object<String, Function>}
|
|
||||||
*/
|
*/
|
||||||
function provide(editor) {
|
function provide(editor: MarkdownEditor): Record<string, () => void> {
|
||||||
const shortcuts = {};
|
const shortcuts: Record<string, () => void> = {};
|
||||||
|
|
||||||
// Insert Image shortcut
|
// Insert Image shortcut
|
||||||
shortcuts['Shift-Mod-i'] = () => editor.actions.insertImage();
|
shortcuts['Shift-Mod-i'] = () => editor.actions.insertImage();
|
||||||
@ -42,14 +43,12 @@ function provide(editor) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the editor shortcuts in CodeMirror keybinding format.
|
* 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 shortcuts = provide(editor);
|
||||||
const keyBindings = [];
|
const keyBindings = [];
|
||||||
|
|
||||||
const wrapAction = action => () => {
|
const wrapAction = (action: ()=>void) => () => {
|
||||||
action();
|
action();
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
Reference in New Issue
Block a user