]> BookStack Code Mirror - bookstack/commitdiff
JS: Converted come common services to typescript
authorDan Brown <redacted>
Wed, 3 Jul 2024 10:00:57 +0000 (11:00 +0100)
committerDan Brown <redacted>
Wed, 3 Jul 2024 10:00:57 +0000 (11:00 +0100)
package.json
resources/js/app.js
resources/js/custom.d.ts [new file with mode: 0644]
resources/js/global.d.ts
resources/js/services/components.js [deleted file]
resources/js/services/components.ts [new file with mode: 0644]
resources/js/services/text.ts [moved from resources/js/services/text.js with 55% similarity]
resources/js/wysiwyg/nodes/code-block.ts
tsconfig.json

index 439eaa5a13dc7979090e414e082f9f0d183600f7..71debf2bd68b6700020925d96d12f7faaaa52cec 100644 (file)
@@ -14,7 +14,8 @@
     "livereload": "livereload ./public/dist/",
     "permissions": "chown -R $USER:$USER bootstrap/cache storage public/uploads",
     "lint": "eslint \"resources/**/*.js\" \"resources/**/*.mjs\"",
-    "fix": "eslint --fix \"resources/**/*.js\" \"resources/**/*.mjs\""
+    "fix": "eslint --fix \"resources/**/*.js\" \"resources/**/*.mjs\"",
+    "ts:lint": "tsc --noEmit"
   },
   "devDependencies": {
     "@lezer/generator": "^1.5.1",
index 5b822e9009e26f58f4695215c4570c9a86bd59de..123d6c8f5cd55ef30f9403f961db3d5532ab46e2 100644 (file)
@@ -1,9 +1,8 @@
 import * as events from './services/events';
 import * as httpInstance from './services/http';
 import Translations from './services/translations';
-
-import * as components from './services/components';
 import * as componentMap from './components';
+import {ComponentStore} from './services/components.ts';
 
 // Url retrieval function
 window.baseUrl = function baseUrl(path) {
@@ -32,6 +31,6 @@ window.trans_choice = translator.getPlural.bind(translator);
 window.trans_plural = translator.parsePlural.bind(translator);
 
 // Load & initialise components
-components.register(componentMap);
-window.$components = components;
-components.init();
+window.$components = new ComponentStore();
+window.$components.register(componentMap);
+window.$components.init();
diff --git a/resources/js/custom.d.ts b/resources/js/custom.d.ts
new file mode 100644 (file)
index 0000000..c5aba8e
--- /dev/null
@@ -0,0 +1,4 @@
+declare module '*.svg' {
+    const content: string;
+    export default content;
+}
\ No newline at end of file
index 537da63685455d1f4cdca2bf5c088528948b0a73..da19545d1101ee47e5670d7ead71fbf91820c566 100644 (file)
@@ -1,12 +1,7 @@
-declare module '*.svg' {
-    const content: string;
-    export default content;
-}
+import {ComponentStore} from "./services/components";
 
 declare global {
     interface Window {
-        $components: {
-            first: (string) => Object,
-        }
+        $components: ComponentStore,
     }
 }
\ No newline at end of file
diff --git a/resources/js/services/components.js b/resources/js/services/components.js
deleted file mode 100644 (file)
index beb0ce9..0000000
+++ /dev/null
@@ -1,165 +0,0 @@
-import {kebabToCamel, camelToKebab} from './text';
-
-/**
- * A mapping of active components keyed by name, with values being arrays of component
- * instances since there can be multiple components of the same type.
- * @type {Object<String, Component[]>}
- */
-const components = {};
-
-/**
- * A mapping of component class models, keyed by name.
- * @type {Object<String, Constructor<Component>>}
- */
-const componentModelMap = {};
-
-/**
- * A mapping of active component maps, keyed by the element components are assigned to.
- * @type {WeakMap<Element, Object<String, Component>>}
- */
-const elementComponentMap = new WeakMap();
-
-/**
- * Parse out the element references within the given element
- * for the given component name.
- * @param {String} name
- * @param {Element} element
- */
-function parseRefs(name, element) {
-    const refs = {};
-    const manyRefs = {};
-
-    const prefix = `${name}@`;
-    const selector = `[refs*="${prefix}"]`;
-    const refElems = [...element.querySelectorAll(selector)];
-    if (element.matches(selector)) {
-        refElems.push(element);
-    }
-
-    for (const el of refElems) {
-        const refNames = el.getAttribute('refs')
-            .split(' ')
-            .filter(str => str.startsWith(prefix))
-            .map(str => str.replace(prefix, ''))
-            .map(kebabToCamel);
-        for (const ref of refNames) {
-            refs[ref] = el;
-            if (typeof manyRefs[ref] === 'undefined') {
-                manyRefs[ref] = [];
-            }
-            manyRefs[ref].push(el);
-        }
-    }
-    return {refs, manyRefs};
-}
-
-/**
- * Parse out the element component options.
- * @param {String} componentName
- * @param {Element} element
- * @return {Object<String, String>}
- */
-function parseOpts(componentName, element) {
-    const opts = {};
-    const prefix = `option:${componentName}:`;
-    for (const {name, value} of element.attributes) {
-        if (name.startsWith(prefix)) {
-            const optName = name.replace(prefix, '');
-            opts[kebabToCamel(optName)] = value || '';
-        }
-    }
-    return opts;
-}
-
-/**
- * Initialize a component instance on the given dom element.
- * @param {String} name
- * @param {Element} element
- */
-function initComponent(name, element) {
-    /** @type {Function<Component>|undefined} * */
-    const ComponentModel = componentModelMap[name];
-    if (ComponentModel === undefined) return;
-
-    // Create our component instance
-    /** @type {Component} * */
-    let instance;
-    try {
-        instance = new ComponentModel();
-        instance.$name = name;
-        instance.$el = element;
-        const allRefs = parseRefs(name, element);
-        instance.$refs = allRefs.refs;
-        instance.$manyRefs = allRefs.manyRefs;
-        instance.$opts = parseOpts(name, element);
-        instance.setup();
-    } catch (e) {
-        console.error('Failed to create component', e, name, element);
-    }
-
-    // Add to global listing
-    if (typeof components[name] === 'undefined') {
-        components[name] = [];
-    }
-    components[name].push(instance);
-
-    // Add to element mapping
-    const elComponents = elementComponentMap.get(element) || {};
-    elComponents[name] = instance;
-    elementComponentMap.set(element, elComponents);
-}
-
-/**
- * Initialize all components found within the given element.
- * @param {Element|Document} parentElement
- */
-export function init(parentElement = document) {
-    const componentElems = parentElement.querySelectorAll('[component],[components]');
-
-    for (const el of componentElems) {
-        const componentNames = `${el.getAttribute('component') || ''} ${(el.getAttribute('components'))}`.toLowerCase().split(' ').filter(Boolean);
-        for (const name of componentNames) {
-            initComponent(name, el);
-        }
-    }
-}
-
-/**
- * Register the given component mapping into the component system.
- * @param {Object<String, ObjectConstructor<Component>>} mapping
- */
-export function register(mapping) {
-    const keys = Object.keys(mapping);
-    for (const key of keys) {
-        componentModelMap[camelToKebab(key)] = mapping[key];
-    }
-}
-
-/**
- * Get the first component of the given name.
- * @param {String} name
- * @returns {Component|null}
- */
-export function first(name) {
-    return (components[name] || [null])[0];
-}
-
-/**
- * Get all the components of the given name.
- * @param {String} name
- * @returns {Component[]}
- */
-export function get(name) {
-    return components[name] || [];
-}
-
-/**
- * Get the first component, of the given name, that's assigned to the given element.
- * @param {Element} element
- * @param {String} name
- * @returns {Component|null}
- */
-export function firstOnElement(element, name) {
-    const elComponents = elementComponentMap.get(element) || {};
-    return elComponents[name] || null;
-}
diff --git a/resources/js/services/components.ts b/resources/js/services/components.ts
new file mode 100644 (file)
index 0000000..c19939e
--- /dev/null
@@ -0,0 +1,153 @@
+import {kebabToCamel, camelToKebab} from './text';
+import {Component} from "../components/component";
+
+/**
+ * Parse out the element references within the given element
+ * for the given component name.
+ */
+function parseRefs(name: string, element: HTMLElement):
+    {refs: Record<string, HTMLElement>, manyRefs: Record<string, HTMLElement[]>} {
+    const refs: Record<string, HTMLElement> = {};
+    const manyRefs: Record<string, HTMLElement[]> = {};
+
+    const prefix = `${name}@`;
+    const selector = `[refs*="${prefix}"]`;
+    const refElems = [...element.querySelectorAll(selector)];
+    if (element.matches(selector)) {
+        refElems.push(element);
+    }
+
+    for (const el of refElems as HTMLElement[]) {
+        const refNames = (el.getAttribute('refs') || '')
+            .split(' ')
+            .filter(str => str.startsWith(prefix))
+            .map(str => str.replace(prefix, ''))
+            .map(kebabToCamel);
+        for (const ref of refNames) {
+            refs[ref] = el;
+            if (typeof manyRefs[ref] === 'undefined') {
+                manyRefs[ref] = [];
+            }
+            manyRefs[ref].push(el);
+        }
+    }
+    return {refs, manyRefs};
+}
+
+/**
+ * Parse out the element component options.
+ */
+function parseOpts(componentName: string, element: HTMLElement): Record<string, string> {
+    const opts: Record<string, string> = {};
+    const prefix = `option:${componentName}:`;
+    for (const {name, value} of element.attributes) {
+        if (name.startsWith(prefix)) {
+            const optName = name.replace(prefix, '');
+            opts[kebabToCamel(optName)] = value || '';
+        }
+    }
+    return opts;
+}
+
+export class ComponentStore {
+    /**
+     * A mapping of active components keyed by name, with values being arrays of component
+     * instances since there can be multiple components of the same type.
+     */
+    protected components: Record<string, Component[]> = {};
+
+    /**
+     * A mapping of component class models, keyed by name.
+     */
+    protected componentModelMap: Record<string, typeof Component> = {};
+
+    /**
+     * A mapping of active component maps, keyed by the element components are assigned to.
+     */
+    protected elementComponentMap: WeakMap<HTMLElement, Record<string, Component>> = new WeakMap();
+
+    /**
+     * Initialize a component instance on the given dom element.
+     */
+     protected initComponent(name: string, element: HTMLElement): void {
+        const ComponentModel = this.componentModelMap[name];
+        if (ComponentModel === undefined) return;
+
+        // Create our component instance
+        let instance: Component|null = null;
+        try {
+            instance = new ComponentModel();
+            instance.$name = name;
+            instance.$el = element;
+            const allRefs = parseRefs(name, element);
+            instance.$refs = allRefs.refs;
+            instance.$manyRefs = allRefs.manyRefs;
+            instance.$opts = parseOpts(name, element);
+            instance.setup();
+        } catch (e) {
+            console.error('Failed to create component', e, name, element);
+        }
+
+        if (!instance) {
+            return;
+        }
+
+        // Add to global listing
+        if (typeof this.components[name] === 'undefined') {
+            this.components[name] = [];
+        }
+        this.components[name].push(instance);
+
+        // Add to element mapping
+        const elComponents = this.elementComponentMap.get(element) || {};
+        elComponents[name] = instance;
+        this.elementComponentMap.set(element, elComponents);
+    }
+
+    /**
+     * Initialize all components found within the given element.
+     */
+    public init(parentElement: Document|HTMLElement = document) {
+        const componentElems = parentElement.querySelectorAll('[component],[components]');
+
+        for (const el of componentElems) {
+            const componentNames = `${el.getAttribute('component') || ''} ${(el.getAttribute('components'))}`.toLowerCase().split(' ').filter(Boolean);
+            for (const name of componentNames) {
+                this.initComponent(name, el as HTMLElement);
+            }
+        }
+    }
+
+    /**
+     * Register the given component mapping into the component system.
+     * @param {Object<String, ObjectConstructor<Component>>} mapping
+     */
+    public register(mapping: Record<string, typeof Component>) {
+        const keys = Object.keys(mapping);
+        for (const key of keys) {
+            this.componentModelMap[camelToKebab(key)] = mapping[key];
+        }
+    }
+
+    /**
+     * Get the first component of the given name.
+     */
+    public first(name: string): Component|null {
+        return (this.components[name] || [null])[0];
+    }
+
+    /**
+     * Get all the components of the given name.
+     */
+    public get(name: string): Component[] {
+        return this.components[name] || [];
+    }
+
+    /**
+     * Get the first component, of the given name, that's assigned to the given element.
+     */
+    public firstOnElement(element: HTMLElement, name: string): Component|null {
+        const elComponents = this.elementComponentMap.get(element) || {};
+        return elComponents[name] || null;
+    }
+}
similarity index 55%
rename from resources/js/services/text.js
rename to resources/js/services/text.ts
index d5e6fa7987438e1ab771ec63dba124d463440540..351e8016734941e0b56c04a735a32c263381207b 100644 (file)
@@ -1,19 +1,15 @@
 /**
  * Convert a kebab-case string to camelCase
- * @param {String} kebab
- * @returns {string}
  */
-export function kebabToCamel(kebab) {
-    const ucFirst = word => word.slice(0, 1).toUpperCase() + word.slice(1);
+export function kebabToCamel(kebab: string): string {
+    const ucFirst = (word: string) => word.slice(0, 1).toUpperCase() + word.slice(1);
     const words = kebab.split('-');
     return words[0] + words.slice(1).map(ucFirst).join('');
 }
 
 /**
  * Convert a camelCase string to a kebab-case string.
- * @param {String} camelStr
- * @returns {String}
  */
-export function camelToKebab(camelStr) {
+export function camelToKebab(camelStr: string): string {
     return camelStr.replace(/[A-Z]/g, (str, offset) => (offset > 0 ? '-' : '') + str.toLowerCase());
 }
index f839501db2ef5c721c997fd2b520f5c9481b39a0..2478ba24900d0447eabe7b4ee9495d894bb1a383 100644 (file)
@@ -10,6 +10,7 @@ import {
 import type {EditorConfig} from "lexical/LexicalEditor";
 import {el} from "../helpers";
 import {EditorDecoratorAdapter} from "../ui/framework/decorator";
+import {CodeEditor} from "../../components";
 
 export type SerializedCodeBlockNode = Spread<{
     language: string;
@@ -170,7 +171,7 @@ export function $openCodeEditorForNode(editor: LexicalEditor, node: CodeBlockNod
     const language = node.getLanguage();
 
     // @ts-ignore
-    const codeEditor = window.$components.first('code-editor');
+    const codeEditor = window.$components.first('code-editor') as CodeEditor;
     // TODO - Handle direction
     codeEditor.open(code, language, 'ltr', (newCode: string, newLang: string) => {
         editor.update(() => {
index 9913c1235a498955a50e4de88dc4a28c9d8de55a..3ca03da303d781cb1d8ce48f5e1de64462539eba 100644 (file)
@@ -1,4 +1,5 @@
 {
+  "include": ["resources/js/**/*"],
   "compilerOptions": {
     /* Visit https://aka.ms/tsconfig to read more about this file */
 
@@ -26,7 +27,7 @@
 
     /* Modules */
     "module": "commonjs",                                /* Specify what module code is generated. */
-    // "rootDir": "./",                                  /* Specify the root folder within your source files. */
+    "rootDir": "./resources/js/",                        /* Specify the root folder within your source files. */
     // "moduleResolution": "node10",                     /* Specify how TypeScript looks up a file from a given module specifier. */
     // "baseUrl": "./",                                  /* Specify the base directory to resolve non-relative module names. */
     "paths": {                                           /* Specify a set of entries that re-map imports to additional lookup locations. */