diff --git a/package.json b/package.json index 439eaa5a1..71debf2bd 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/resources/js/app.js b/resources/js/app.js index 5b822e900..123d6c8f5 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -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 index 000000000..c5aba8ee2 --- /dev/null +++ b/resources/js/custom.d.ts @@ -0,0 +1,4 @@ +declare module '*.svg' { + const content: string; + export default content; +} \ No newline at end of file diff --git a/resources/js/global.d.ts b/resources/js/global.d.ts index 537da6368..da19545d1 100644 --- a/resources/js/global.d.ts +++ b/resources/js/global.d.ts @@ -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 index beb0ce92f..000000000 --- a/resources/js/services/components.js +++ /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} - */ -const components = {}; - -/** - * A mapping of component class models, keyed by name. - * @type {Object>} - */ -const componentModelMap = {}; - -/** - * A mapping of active component maps, keyed by the element components are assigned to. - * @type {WeakMap>} - */ -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} - */ -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|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>} 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 index 000000000..c19939e92 --- /dev/null +++ b/resources/js/services/components.ts @@ -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, manyRefs: Record} { + const refs: Record = {}; + const manyRefs: Record = {}; + + 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 { + const opts: Record = {}; + 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 = {}; + + /** + * A mapping of component class models, keyed by name. + */ + protected componentModelMap: Record = {}; + + /** + * A mapping of active component maps, keyed by the element components are assigned to. + */ + protected elementComponentMap: WeakMap> = 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>} mapping + */ + public register(mapping: Record) { + 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; + } +} diff --git a/resources/js/services/text.js b/resources/js/services/text.ts similarity index 55% rename from resources/js/services/text.js rename to resources/js/services/text.ts index d5e6fa798..351e80167 100644 --- a/resources/js/services/text.js +++ b/resources/js/services/text.ts @@ -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()); } diff --git a/resources/js/wysiwyg/nodes/code-block.ts b/resources/js/wysiwyg/nodes/code-block.ts index f839501db..2478ba249 100644 --- a/resources/js/wysiwyg/nodes/code-block.ts +++ b/resources/js/wysiwyg/nodes/code-block.ts @@ -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(() => { diff --git a/tsconfig.json b/tsconfig.json index 9913c1235..3ca03da30 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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. */