]> BookStack Code Mirror - bookstack/blob - resources/js/components/index.js
Fix English translations
[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         const allRefs = parseRefs(name, element);
40         instance.$refs = allRefs.refs;
41         instance.$manyRefs = allRefs.manyRefs;
42         instance.$opts = parseOpts(name, element);
43         instance.$emit = (eventName, data = {}) => {
44             data.from = instance;
45             const event = new CustomEvent(`${name}-${eventName}`, {
46                 bubbles: true,
47                 detail: data
48             });
49             instance.$el.dispatchEvent(event);
50         };
51         if (typeof instance.setup === 'function') {
52             instance.setup();
53         }
54     } catch (e) {
55         console.error('Failed to create component', e, name, element);
56     }
57
58
59     // Add to global listing
60     if (typeof window.components[name] === "undefined") {
61         window.components[name] = [];
62     }
63     window.components[name].push(instance);
64
65     // Add to element listing
66     if (typeof element.components === 'undefined') {
67         element.components = {};
68     }
69     element.components[name] = instance;
70 }
71
72 /**
73  * Parse out the element references within the given element
74  * for the given component name.
75  * @param {String} name
76  * @param {Element} element
77  */
78 function parseRefs(name, element) {
79     const refs = {};
80     const manyRefs = {};
81
82     const prefix = `${name}@`
83     const selector = `[refs*="${prefix}"]`;
84     const refElems = [...element.querySelectorAll(selector)];
85     if (element.matches(selector)) {
86         refElems.push(element);
87     }
88
89     for (const el of refElems) {
90         const refNames = el.getAttribute('refs')
91             .split(' ')
92             .filter(str => str.startsWith(prefix))
93             .map(str => str.replace(prefix, ''))
94             .map(kebabToCamel);
95         for (const ref of refNames) {
96             refs[ref] = el;
97             if (typeof manyRefs[ref] === 'undefined') {
98                 manyRefs[ref] = [];
99             }
100             manyRefs[ref].push(el);
101         }
102     }
103     return {refs, manyRefs};
104 }
105
106 /**
107  * Parse out the element component options.
108  * @param {String} name
109  * @param {Element} element
110  * @return {Object<String, String>}
111  */
112 function parseOpts(name, element) {
113     const opts = {};
114     const prefix = `option:${name}:`;
115     for (const {name, value} of element.attributes) {
116         if (name.startsWith(prefix)) {
117             const optName = name.replace(prefix, '');
118             opts[kebabToCamel(optName)] = value || '';
119         }
120     }
121     return opts;
122 }
123
124 /**
125  * Convert a kebab-case string to camelCase
126  * @param {String} kebab
127  * @returns {string}
128  */
129 function kebabToCamel(kebab) {
130     const ucFirst = (word) => word.slice(0,1).toUpperCase() + word.slice(1);
131     const words = kebab.split('-');
132     return words[0] + words.slice(1).map(ucFirst).join('');
133 }
134
135 /**
136  * Initialize all components found within the given element.
137  * @param parentElement
138  */
139 function initAll(parentElement) {
140     if (typeof parentElement === 'undefined') parentElement = document;
141
142     // Old attribute system
143     for (const componentName of Object.keys(componentMapping)) {
144         searchForComponentInParent(componentName, parentElement);
145     }
146
147     // New component system
148     const componentElems = parentElement.querySelectorAll(`[component],[components]`);
149
150     for (const el of componentElems) {
151         const componentNames = `${el.getAttribute('component') || ''} ${(el.getAttribute('components'))}`.toLowerCase().split(' ').filter(Boolean);
152         for (const name of componentNames) {
153             initComponent(name, el);
154         }
155     }
156 }
157
158 window.components.init = initAll;
159 window.components.first = (name) => (window.components[name] || [null])[0];
160
161 export default initAll;
162
163 /**
164  * @typedef Component
165  * @property {HTMLElement} $el
166  * @property {Object<String, HTMLElement>} $refs
167  * @property {Object<String, HTMLElement[]>} $manyRefs
168  * @property {Object<String, String>} $opts
169  * @property {function(string, Object)} $emit
170  */