JS: Converted come common services to typescript
This commit is contained in:
		
							parent
							
								
									feca1f0502
								
							
						
					
					
						commit
						a8f1160743
					
				|  | @ -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", | ||||
|  |  | |||
|  | @ -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(); | ||||
|  |  | |||
|  | @ -0,0 +1,4 @@ | |||
| declare module '*.svg' { | ||||
|     const content: string; | ||||
|     export default content; | ||||
| } | ||||
|  | @ -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, | ||||
|     } | ||||
| } | ||||
|  | @ -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; | ||||
| } | ||||
|  | @ -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; | ||||
|     } | ||||
| } | ||||
|  | @ -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()); | ||||
| } | ||||
|  | @ -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(() => { | ||||
|  |  | |||
|  | @ -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. */ | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue