154 lines
		
	
	
		
			5.1 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
			
		
		
	
	
			154 lines
		
	
	
		
			5.1 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
| 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;
 | |
|     }
 | |
| }
 |