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