diff --git a/dev/build/esbuild.js b/dev/build/esbuild.js index 20193db7f..7f180fc07 100644 --- a/dev/build/esbuild.js +++ b/dev/build/esbuild.js @@ -14,7 +14,7 @@ const entryPoints = { 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'), - wysiwyg: path.join(__dirname, '../../resources/js/wysiwyg/index.mjs'), + wysiwyg: path.join(__dirname, '../../resources/js/wysiwyg/index.ts'), }; // Locate our output directory diff --git a/package-lock.json b/package-lock.json index 6cd32760d..2b6b677c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,7 +43,8 @@ "eslint-plugin-import": "^2.29.0", "livereload": "^0.9.3", "npm-run-all": "^4.1.5", - "sass": "^1.69.5" + "sass": "^1.69.5", + "typescript": "^5.4.5" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -4099,6 +4100,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/uc.micro": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", diff --git a/package.json b/package.json index 42b86fdc7..97a796126 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ "eslint-plugin-import": "^2.29.0", "livereload": "^0.9.3", "npm-run-all": "^4.1.5", - "sass": "^1.69.5" + "sass": "^1.69.5", + "typescript": "^5.4.5" }, "dependencies": { "@codemirror/commands": "^6.3.2", @@ -65,7 +66,8 @@ }, "extends": "airbnb-base", "ignorePatterns": [ - "resources/**/*-stub.js" + "resources/**/*-stub.js", + "resources/**/*.ts" ], "overrides": [], "parserOptions": { diff --git a/resources/js/wysiwyg/index.mjs b/resources/js/wysiwyg/index.ts similarity index 83% rename from resources/js/wysiwyg/index.mjs rename to resources/js/wysiwyg/index.ts index decfa4f22..266866c62 100644 --- a/resources/js/wysiwyg/index.mjs +++ b/resources/js/wysiwyg/index.ts @@ -4,18 +4,18 @@ import { $getSelection, COMMAND_PRIORITY_LOW, createCommand, - createEditor + createEditor, CreateEditorArgs, } from 'lexical'; import {createEmptyHistoryState, registerHistory} from '@lexical/history'; import {registerRichText} from '@lexical/rich-text'; import {$getNearestBlockElementAncestorOrThrow, mergeRegister} from '@lexical/utils'; import {$generateNodesFromDOM} from '@lexical/html'; -import {getNodesForPageEditor} from "./nodes/index.js"; -import {$createCalloutNode, $isCalloutNode} from "./nodes/callout.js"; -import {$setBlocksType} from "@lexical/selection"; +import {$setBlocksType} from '@lexical/selection'; +import {getNodesForPageEditor} from './nodes'; +import {$createCalloutNode, $isCalloutNode, CalloutCategory} from './nodes/callout'; -export function createPageEditorInstance(editArea) { - const config = { +export function createPageEditorInstance(editArea: HTMLElement) { + const config: CreateEditorArgs = { namespace: 'BookStackPageEditor', nodes: getNodesForPageEditor(), onError: console.error, @@ -52,7 +52,7 @@ export function createPageEditorInstance(editArea) { // Example of creating, registering and using a custom command const SET_BLOCK_CALLOUT_COMMAND = createCommand(); - editor.registerCommand(SET_BLOCK_CALLOUT_COMMAND, (category = 'info') => { + editor.registerCommand(SET_BLOCK_CALLOUT_COMMAND, (category: CalloutCategory = 'info') => { const selection = $getSelection(); const blockElement = $getNearestBlockElementAncestorOrThrow(selection.getNodes()[0]); if ($isCalloutNode(blockElement)) { @@ -67,4 +67,4 @@ export function createPageEditorInstance(editArea) { button.addEventListener('click', event => { editor.dispatchCommand(SET_BLOCK_CALLOUT_COMMAND, 'info'); }); -} \ No newline at end of file +} diff --git a/resources/js/wysiwyg/nodes/callout.js b/resources/js/wysiwyg/nodes/callout.ts similarity index 58% rename from resources/js/wysiwyg/nodes/callout.js rename to resources/js/wysiwyg/nodes/callout.ts index db90f22a9..4fba5ee5b 100644 --- a/resources/js/wysiwyg/nodes/callout.js +++ b/resources/js/wysiwyg/nodes/callout.ts @@ -1,35 +1,51 @@ -import {$createParagraphNode, ElementNode} from 'lexical'; +import { + $createParagraphNode, + DOMConversion, + DOMConversionMap, DOMConversionOutput, + ElementNode, + LexicalEditor, + LexicalNode, + ParagraphNode, SerializedElementNode, Spread +} from 'lexical'; +import type {EditorConfig} from "lexical/LexicalEditor"; +import type {RangeSelection} from "lexical/LexicalSelection"; + +export type CalloutCategory = 'info' | 'danger' | 'warning' | 'success'; + +export type SerializedCalloutNode = Spread<{ + category: CalloutCategory; +}, SerializedElementNode> export class Callout extends ElementNode { - __category = 'info'; + __category: CalloutCategory = 'info'; static getType() { return 'callout'; } - static clone(node) { + static clone(node: Callout) { return new Callout(node.__category, node.__key); } - constructor(category, key) { + constructor(category: CalloutCategory, key?: string) { super(key); this.__category = category; } - createDOM(_config, _editor) { + createDOM(_config: EditorConfig, _editor: LexicalEditor) { const element = document.createElement('p'); element.classList.add('callout', this.__category || ''); return element; } - updateDOM(prevNode, dom) { + updateDOM(prevNode: unknown, dom: HTMLElement) { // Returning false tells Lexical that this node does not need its // DOM element replacing with a new copy from createDOM. return false; } - insertNewAfter(selection, restoreSelection) { + insertNewAfter(selection: RangeSelection, restoreSelection?: boolean): Callout|ParagraphNode { const anchorOffset = selection ? selection.anchor.offset : 0; const newElement = anchorOffset === this.getTextContentSize() || !selection ? $createParagraphNode() : $createCalloutNode(this.__category); @@ -46,14 +62,14 @@ export class Callout extends ElementNode { return newElement; } - static importDOM() { + static importDOM(): DOMConversionMap|null { return { - p: node => { + p(node: HTMLElement): DOMConversion|null { if (node.classList.contains('callout')) { return { - conversion: element => { - let category = 'info'; - const categories = ['info', 'success', 'warning', 'danger']; + conversion: (element: HTMLElement): DOMConversionOutput|null => { + let category: CalloutCategory = 'info'; + const categories: CalloutCategory[] = ['info', 'success', 'warning', 'danger']; for (const c of categories) { if (element.classList.contains(c)) { @@ -74,7 +90,7 @@ export class Callout extends ElementNode { }; } - exportJSON() { + exportJSON(): SerializedCalloutNode { return { ...super.exportJSON(), type: 'callout', @@ -83,16 +99,16 @@ export class Callout extends ElementNode { }; } - static importJSON(serializedNode) { + static importJSON(serializedNode: SerializedCalloutNode): Callout { return $createCalloutNode(serializedNode.category); } } -export function $createCalloutNode(category = 'info') { +export function $createCalloutNode(category: CalloutCategory = 'info') { return new Callout(category); } -export function $isCalloutNode(node) { +export function $isCalloutNode(node: LexicalNode | null | undefined) { return node instanceof Callout; } diff --git a/resources/js/wysiwyg/nodes/index.js b/resources/js/wysiwyg/nodes/index.ts similarity index 60% rename from resources/js/wysiwyg/nodes/index.js rename to resources/js/wysiwyg/nodes/index.ts index ada229d9e..77f582877 100644 --- a/resources/js/wysiwyg/nodes/index.js +++ b/resources/js/wysiwyg/nodes/index.ts @@ -1,11 +1,11 @@ import {HeadingNode, QuoteNode} from '@lexical/rich-text'; import {Callout} from './callout'; +import {KlassConstructor, LexicalNode} from "lexical"; /** * Load the nodes for lexical. - * @returns {LexicalNode[]} */ -export function getNodesForPageEditor() { +export function getNodesForPageEditor(): KlassConstructor<typeof LexicalNode>[] { return [ Callout, HeadingNode,