]> BookStack Code Mirror - bookstack/blob - resources/js/services/components.js
Connected md editor settings to logic for functionality
[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  * Initialize a component instance on the given dom element.
24  * @param {String} name
25  * @param {Element} element
26  */
27 function initComponent(name, element) {
28     /** @type {Function<Component>|undefined} **/
29     const componentModel = componentModelMap[name];
30     if (componentModel === undefined) return;
31
32     // Create our component instance
33     /** @type {Component} **/
34     let instance;
35     try {
36         instance = new componentModel();
37         instance.$name = name;
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.setup();
44     } catch (e) {
45         console.error('Failed to create component', e, name, element);
46     }
47
48     // Add to global listing
49     if (typeof components[name] === "undefined") {
50         components[name] = [];
51     }
52     components[name].push(instance);
53
54     // Add to element mapping
55     const elComponents = elementComponentMap.get(element) || {};
56     elComponents[name] = instance;
57     elementComponentMap.set(element, elComponents);
58 }
59
60 /**
61  * Parse out the element references within the given element
62  * for the given component name.
63  * @param {String} name
64  * @param {Element} element
65  */
66 function parseRefs(name, element) {
67     const refs = {};
68     const manyRefs = {};
69
70     const prefix = `${name}@`
71     const selector = `[refs*="${prefix}"]`;
72     const refElems = [...element.querySelectorAll(selector)];
73     if (element.matches(selector)) {
74         refElems.push(element);
75     }
76
77     for (const el of refElems) {
78         const refNames = el.getAttribute('refs')
79             .split(' ')
80             .filter(str => str.startsWith(prefix))
81             .map(str => str.replace(prefix, ''))
82             .map(kebabToCamel);
83         for (const ref of refNames) {
84             refs[ref] = el;
85             if (typeof manyRefs[ref] === 'undefined') {
86                 manyRefs[ref] = [];
87             }
88             manyRefs[ref].push(el);
89         }
90     }
91     return {refs, manyRefs};
92 }
93
94 /**
95  * Parse out the element component options.
96  * @param {String} name
97  * @param {Element} element
98  * @return {Object<String, String>}
99  */
100 function parseOpts(name, element) {
101     const opts = {};
102     const prefix = `option:${name}:`;
103     for (const {name, value} of element.attributes) {
104         if (name.startsWith(prefix)) {
105             const optName = name.replace(prefix, '');
106             opts[kebabToCamel(optName)] = value || '';
107         }
108     }
109     return opts;
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 }