]> BookStack Code Mirror - bookstack/blob - resources/js/services/components.ts
Deps: Updated composer/npm packages, fixed test namespace
[bookstack] / resources / js / services / components.ts
1 import {kebabToCamel, camelToKebab} from './text';
2 import {Component} from "../components/component";
3
4 /**
5  * Parse out the element references within the given element
6  * for the given component name.
7  */
8 function parseRefs(name: string, element: HTMLElement):
9     {refs: Record<string, HTMLElement>, manyRefs: Record<string, HTMLElement[]>} {
10     const refs: Record<string, HTMLElement> = {};
11     const manyRefs: Record<string, HTMLElement[]> = {};
12
13     const prefix = `${name}@`;
14     const selector = `[refs*="${prefix}"]`;
15     const refElems = [...element.querySelectorAll(selector)];
16     if (element.matches(selector)) {
17         refElems.push(element);
18     }
19
20     for (const el of refElems as HTMLElement[]) {
21         const refNames = (el.getAttribute('refs') || '')
22             .split(' ')
23             .filter(str => str.startsWith(prefix))
24             .map(str => str.replace(prefix, ''))
25             .map(kebabToCamel);
26         for (const ref of refNames) {
27             refs[ref] = el;
28             if (typeof manyRefs[ref] === 'undefined') {
29                 manyRefs[ref] = [];
30             }
31             manyRefs[ref].push(el);
32         }
33     }
34     return {refs, manyRefs};
35 }
36
37 /**
38  * Parse out the element component options.
39  */
40 function parseOpts(componentName: string, element: HTMLElement): Record<string, string> {
41     const opts: Record<string, string> = {};
42     const prefix = `option:${componentName}:`;
43     for (const {name, value} of element.attributes) {
44         if (name.startsWith(prefix)) {
45             const optName = name.replace(prefix, '');
46             opts[kebabToCamel(optName)] = value || '';
47         }
48     }
49     return opts;
50 }
51
52 export class ComponentStore {
53     /**
54      * A mapping of active components keyed by name, with values being arrays of component
55      * instances since there can be multiple components of the same type.
56      */
57     protected components: Record<string, Component[]> = {};
58
59     /**
60      * A mapping of component class models, keyed by name.
61      */
62     protected componentModelMap: Record<string, typeof Component> = {};
63
64     /**
65      * A mapping of active component maps, keyed by the element components are assigned to.
66      */
67     protected elementComponentMap: WeakMap<HTMLElement, Record<string, Component>> = new WeakMap();
68
69     /**
70      * Initialize a component instance on the given dom element.
71      */
72      protected initComponent(name: string, element: HTMLElement): void {
73         const ComponentModel = this.componentModelMap[name];
74         if (ComponentModel === undefined) return;
75
76         // Create our component instance
77         let instance: Component|null = null;
78         try {
79             instance = new ComponentModel();
80             instance.$name = name;
81             instance.$el = element;
82             const allRefs = parseRefs(name, element);
83             instance.$refs = allRefs.refs;
84             instance.$manyRefs = allRefs.manyRefs;
85             instance.$opts = parseOpts(name, element);
86             instance.setup();
87         } catch (e) {
88             console.error('Failed to create component', e, name, element);
89         }
90
91         if (!instance) {
92             return;
93         }
94
95         // Add to global listing
96         if (typeof this.components[name] === 'undefined') {
97             this.components[name] = [];
98         }
99         this.components[name].push(instance);
100
101         // Add to element mapping
102         const elComponents = this.elementComponentMap.get(element) || {};
103         elComponents[name] = instance;
104         this.elementComponentMap.set(element, elComponents);
105     }
106
107     /**
108      * Initialize all components found within the given element.
109      */
110     public init(parentElement: Document|HTMLElement = document) {
111         const componentElems = parentElement.querySelectorAll('[component],[components]');
112
113         for (const el of componentElems) {
114             const componentNames = `${el.getAttribute('component') || ''} ${(el.getAttribute('components'))}`.toLowerCase().split(' ').filter(Boolean);
115             for (const name of componentNames) {
116                 this.initComponent(name, el as HTMLElement);
117             }
118         }
119     }
120
121     /**
122      * Register the given component mapping into the component system.
123      * @param {Object<String, ObjectConstructor<Component>>} mapping
124      */
125     public register(mapping: Record<string, typeof Component>) {
126         const keys = Object.keys(mapping);
127         for (const key of keys) {
128             this.componentModelMap[camelToKebab(key)] = mapping[key];
129         }
130     }
131
132     /**
133      * Get the first component of the given name.
134      */
135     public first(name: string): Component|null {
136         return (this.components[name] || [null])[0];
137     }
138
139     /**
140      * Get all the components of the given name.
141      */
142     public get<T extends Component>(name: string): T[] {
143         return (this.components[name] || []) as T[];
144     }
145
146     /**
147      * Get the first component, of the given name, that's assigned to the given element.
148      */
149     public firstOnElement(element: HTMLElement, name: string): Component|null {
150         const elComponents = this.elementComponentMap.get(element) || {};
151         return elComponents[name] || null;
152     }
153
154     public allWithinElement<T extends Component>(element: HTMLElement, name: string): T[] {
155         const components = this.get<T>(name);
156         return components.filter(c => element.contains(c.$el));
157     }
158 }
Morty Proxy This is a proxified and sanitized view of the page, visit original site.