]> BookStack Code Mirror - bookstack/blob - resources/js/components/index.js
Merge branch 'master' of git://github.com/drzippie/BookStack into drzippie-master
[bookstack] / resources / js / components / index.js
1 const componentMapping = {};
2
3 const definitionFiles = require.context('./', false, /\.js$/);
4 for (const fileName of definitionFiles.keys()) {
5     const name = fileName.replace('./', '').split('.')[0];
6     if (name !== 'index') {
7         componentMapping[name] = definitionFiles(fileName).default;
8     }
9 }
10
11 window.components = {};
12
13 /**
14  * Initialize components of the given name within the given element.
15  * @param {String} componentName
16  * @param {HTMLElement|Document} parentElement
17  */
18 function searchForComponentInParent(componentName, parentElement) {
19     const elems = parentElement.querySelectorAll(`[${componentName}]`);
20     for (let j = 0, jLen = elems.length; j < jLen; j++) {
21         initComponent(componentName, elems[j]);
22     }
23 }
24
25 /**
26  * Initialize a component instance on the given dom element.
27  * @param {String} name
28  * @param {Element} element
29  */
30 function initComponent(name, element) {
31     const componentModel = componentMapping[name];
32     if (componentModel === undefined) return;
33
34     // Create our component instance
35     let instance;
36     try {
37         instance = new componentModel(element);
38         instance.$el = element;
39         instance.$refs = parseRefs(name, element);
40         instance.$opts = parseOpts(name, element);
41         if (typeof instance.setup === 'function') {
42             instance.setup();
43         }
44     } catch (e) {
45         console.error('Failed to create component', e, name, element);
46     }
47
48
49     // Add to global listing
50     if (typeof window.components[name] === "undefined") {
51         window.components[name] = [];
52     }
53     window.components[name].push(instance);
54
55     // Add to element listing
56     if (typeof element.components === 'undefined') {
57         element.components = {};
58     }
59     element.components[name] = instance;
60 }
61
62 /**
63  * Parse out the element references within the given element
64  * for the given component name.
65  * @param {String} name
66  * @param {Element} element
67  */
68 function parseRefs(name, element) {
69     const refs = {};
70     const prefix = `${name}@`
71     const refElems = element.querySelectorAll(`[refs*="${prefix}"]`);
72     for (const el of refElems) {
73         const refNames = el.getAttribute('refs')
74             .split(' ')
75             .filter(str => str.startsWith(prefix))
76             .map(str => str.replace(prefix, ''));
77         for (const ref of refNames) {
78             refs[ref] = el;
79         }
80     }
81     return refs;
82 }
83
84 /**
85  * Parse out the element component options.
86  * @param {String} name
87  * @param {Element} element
88  * @return {Object<String, String>}
89  */
90 function parseOpts(name, element) {
91     const opts = {};
92     const prefix = `option:${name}:`;
93     for (const {name, value} of element.attributes) {
94         if (name.startsWith(prefix)) {
95             const optName = name.replace(prefix, '');
96             opts[kebabToCamel(optName)] = value || '';
97         }
98     }
99     return opts;
100 }
101
102 /**
103  * Convert a kebab-case string to camelCase
104  * @param {String} kebab
105  * @returns {string}
106  */
107 function kebabToCamel(kebab) {
108     const ucFirst = (word) => word.slice(0,1).toUpperCase() + word.slice(1);
109     const words = kebab.split('-');
110     return words[0] + words.slice(1).map(ucFirst).join();
111 }
112
113 /**
114  * Initialize all components found within the given element.
115  * @param parentElement
116  */
117 function initAll(parentElement) {
118     if (typeof parentElement === 'undefined') parentElement = document;
119
120     // Old attribute system
121     for (const componentName of Object.keys(componentMapping)) {
122         searchForComponentInParent(componentName, parentElement);
123     }
124
125     // New component system
126     const componentElems = parentElement.querySelectorAll(`[component],[components]`);
127
128     for (const el of componentElems) {
129         const componentNames = `${el.getAttribute('component') || ''} ${(el.getAttribute('components'))}`.toLowerCase().split(' ').filter(Boolean);
130         for (const name of componentNames) {
131             initComponent(name, el);
132         }
133     }
134 }
135
136 window.components.init = initAll;
137
138 export default initAll;
139
140 /**
141  * @typedef Component
142  * @property {HTMLElement} $el
143  * @property {Object<String, HTMLElement>} $refs
144  * @property {Object<String, String>} $opts
145  */