]> BookStack Code Mirror - bookstack/blob - resources/js/services/components.js
Started refactor and alignment of component system
[bookstack] / resources / js / services / components.js
1 const components = {};
2 const componentMap = {};
3
4 /**
5  * Initialize a component instance on the given dom element.
6  * @param {String} name
7  * @param {Element} element
8  */
9 function initComponent(name, element) {
10     /** @type {Function<Component>|undefined} **/
11     const componentModel = componentMap[name];
12     if (componentModel === undefined) return;
13
14     // Create our component instance
15     /** @type {Component} **/
16     let instance;
17     try {
18         instance = new componentModel();
19         instance.$name = name;
20         instance.$el = element;
21         const allRefs = parseRefs(name, element);
22         instance.$refs = allRefs.refs;
23         instance.$manyRefs = allRefs.manyRefs;
24         instance.$opts = parseOpts(name, element);
25         instance.setup();
26     } catch (e) {
27         console.error('Failed to create component', e, name, element);
28     }
29
30     // Add to global listing
31     if (typeof components[name] === "undefined") {
32         components[name] = [];
33     }
34     components[name].push(instance);
35
36     // Add to element listing
37     if (typeof element.components === 'undefined') {
38         element.components = {};
39     }
40     element.components[name] = instance;
41 }
42
43 /**
44  * Parse out the element references within the given element
45  * for the given component name.
46  * @param {String} name
47  * @param {Element} element
48  */
49 function parseRefs(name, element) {
50     const refs = {};
51     const manyRefs = {};
52
53     const prefix = `${name}@`
54     const selector = `[refs*="${prefix}"]`;
55     const refElems = [...element.querySelectorAll(selector)];
56     if (element.matches(selector)) {
57         refElems.push(element);
58     }
59
60     for (const el of refElems) {
61         const refNames = el.getAttribute('refs')
62             .split(' ')
63             .filter(str => str.startsWith(prefix))
64             .map(str => str.replace(prefix, ''))
65             .map(kebabToCamel);
66         for (const ref of refNames) {
67             refs[ref] = el;
68             if (typeof manyRefs[ref] === 'undefined') {
69                 manyRefs[ref] = [];
70             }
71             manyRefs[ref].push(el);
72         }
73     }
74     return {refs, manyRefs};
75 }
76
77 /**
78  * Parse out the element component options.
79  * @param {String} name
80  * @param {Element} element
81  * @return {Object<String, String>}
82  */
83 function parseOpts(name, element) {
84     const opts = {};
85     const prefix = `option:${name}:`;
86     for (const {name, value} of element.attributes) {
87         if (name.startsWith(prefix)) {
88             const optName = name.replace(prefix, '');
89             opts[kebabToCamel(optName)] = value || '';
90         }
91     }
92     return opts;
93 }
94
95 /**
96  * Convert a kebab-case string to camelCase
97  * @param {String} kebab
98  * @returns {string}
99  */
100 function kebabToCamel(kebab) {
101     const ucFirst = (word) => word.slice(0,1).toUpperCase() + word.slice(1);
102     const words = kebab.split('-');
103     return words[0] + words.slice(1).map(ucFirst).join('');
104 }
105
106 /**
107  * Initialize all components found within the given element.
108  * @param {Element|Document} parentElement
109  */
110 export function init(parentElement = 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             initComponent(name, el);
117         }
118     }
119 }
120
121 /**
122  * Register the given component mapping into the component system.
123  * @param {Object<String, ObjectConstructor<Component>>} mapping
124  */
125 export function register(mapping) {
126     const keys = Object.keys(mapping);
127     for (const key of keys) {
128         componentMap[camelToKebab(key)] = mapping[key];
129     }
130     console.log(componentMap);
131 }
132
133 /**
134  * Get the first component of the given name.
135  * @param {String} name
136  * @returns {Component|null}
137  */
138 export function first(name) {
139     return (components[name] || [null])[0];
140 }
141
142 /**
143  * Get all the components of the given name.
144  * @param {String} name
145  * @returns {Component[]}
146  */
147 export function get(name = '') {
148     return components[name] || [];
149 }
150
151 function camelToKebab(camelStr) {
152     return camelStr.replace(/[A-Z]/g, (str, offset) =>  (offset > 0 ? '-' : '') + str.toLowerCase());
153 }