JS: Converted come common services to typescript

This commit is contained in:
Dan Brown 2024-07-03 11:00:57 +01:00
parent feca1f0502
commit a8f1160743
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
9 changed files with 172 additions and 187 deletions

View File

@ -14,7 +14,8 @@
"livereload": "livereload ./public/dist/", "livereload": "livereload ./public/dist/",
"permissions": "chown -R $USER:$USER bootstrap/cache storage public/uploads", "permissions": "chown -R $USER:$USER bootstrap/cache storage public/uploads",
"lint": "eslint \"resources/**/*.js\" \"resources/**/*.mjs\"", "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": { "devDependencies": {
"@lezer/generator": "^1.5.1", "@lezer/generator": "^1.5.1",

View File

@ -1,9 +1,8 @@
import * as events from './services/events'; import * as events from './services/events';
import * as httpInstance from './services/http'; import * as httpInstance from './services/http';
import Translations from './services/translations'; import Translations from './services/translations';
import * as components from './services/components';
import * as componentMap from './components'; import * as componentMap from './components';
import {ComponentStore} from './services/components.ts';
// Url retrieval function // Url retrieval function
window.baseUrl = function baseUrl(path) { window.baseUrl = function baseUrl(path) {
@ -32,6 +31,6 @@ window.trans_choice = translator.getPlural.bind(translator);
window.trans_plural = translator.parsePlural.bind(translator); window.trans_plural = translator.parsePlural.bind(translator);
// Load & initialise components // Load & initialise components
components.register(componentMap); window.$components = new ComponentStore();
window.$components = components; window.$components.register(componentMap);
components.init(); window.$components.init();

4
resources/js/custom.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
declare module '*.svg' {
const content: string;
export default content;
}

View File

@ -1,12 +1,7 @@
declare module '*.svg' { import {ComponentStore} from "./services/components";
const content: string;
export default content;
}
declare global { declare global {
interface Window { interface Window {
$components: { $components: ComponentStore,
first: (string) => Object,
}
} }
} }

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -1,19 +1,15 @@
/** /**
* Convert a kebab-case string to camelCase * Convert a kebab-case string to camelCase
* @param {String} kebab
* @returns {string}
*/ */
export function kebabToCamel(kebab) { export function kebabToCamel(kebab: string): string {
const ucFirst = word => word.slice(0, 1).toUpperCase() + word.slice(1); const ucFirst = (word: string) => word.slice(0, 1).toUpperCase() + word.slice(1);
const words = kebab.split('-'); const words = kebab.split('-');
return words[0] + words.slice(1).map(ucFirst).join(''); return words[0] + words.slice(1).map(ucFirst).join('');
} }
/** /**
* Convert a camelCase string to a kebab-case string. * 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()); return camelStr.replace(/[A-Z]/g, (str, offset) => (offset > 0 ? '-' : '') + str.toLowerCase());
} }

View File

@ -10,6 +10,7 @@ import {
import type {EditorConfig} from "lexical/LexicalEditor"; import type {EditorConfig} from "lexical/LexicalEditor";
import {el} from "../helpers"; import {el} from "../helpers";
import {EditorDecoratorAdapter} from "../ui/framework/decorator"; import {EditorDecoratorAdapter} from "../ui/framework/decorator";
import {CodeEditor} from "../../components";
export type SerializedCodeBlockNode = Spread<{ export type SerializedCodeBlockNode = Spread<{
language: string; language: string;
@ -170,7 +171,7 @@ export function $openCodeEditorForNode(editor: LexicalEditor, node: CodeBlockNod
const language = node.getLanguage(); const language = node.getLanguage();
// @ts-ignore // @ts-ignore
const codeEditor = window.$components.first('code-editor'); const codeEditor = window.$components.first('code-editor') as CodeEditor;
// TODO - Handle direction // TODO - Handle direction
codeEditor.open(code, language, 'ltr', (newCode: string, newLang: string) => { codeEditor.open(code, language, 'ltr', (newCode: string, newLang: string) => {
editor.update(() => { editor.update(() => {

View File

@ -1,4 +1,5 @@
{ {
"include": ["resources/js/**/*"],
"compilerOptions": { "compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */ /* Visit https://aka.ms/tsconfig to read more about this file */
@ -26,7 +27,7 @@
/* Modules */ /* Modules */
"module": "commonjs", /* Specify what module code is generated. */ "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. */ // "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. */ // "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. */ "paths": { /* Specify a set of entries that re-map imports to additional lookup locations. */