]> BookStack Code Mirror - bookstack/blob - resources/js/components/index.js
Merge branch 'master' of git://github.com/alexmannuk/BookStack into alexmannuk-master
[bookstack] / resources / js / components / index.js
1 import addRemoveRows from "./add-remove-rows.js"
2 import ajaxDeleteRow from "./ajax-delete-row.js"
3 import ajaxForm from "./ajax-form.js"
4 import attachments from "./attachments.js"
5 import autoSuggest from "./auto-suggest.js"
6 import backToTop from "./back-to-top.js"
7 import bookSort from "./book-sort.js"
8 import breadcrumbListing from "./breadcrumb-listing.js"
9 import chapterToggle from "./chapter-toggle.js"
10 import codeEditor from "./code-editor.js"
11 import codeHighlighter from "./code-highlighter.js"
12 import collapsible from "./collapsible.js"
13 import customCheckbox from "./custom-checkbox.js"
14 import detailsHighlighter from "./details-highlighter.js"
15 import dropdown from "./dropdown.js"
16 import dropzone from "./dropzone.js"
17 import editorToolbox from "./editor-toolbox.js"
18 import entityPermissionsEditor from "./entity-permissions-editor.js"
19 import entitySearch from "./entity-search.js"
20 import entitySelector from "./entity-selector.js"
21 import entitySelectorPopup from "./entity-selector-popup.js"
22 import eventEmitSelect from "./event-emit-select.js"
23 import expandToggle from "./expand-toggle.js"
24 import headerMobileToggle from "./header-mobile-toggle.js"
25 import homepageControl from "./homepage-control.js"
26 import imageManager from "./image-manager.js"
27 import imagePicker from "./image-picker.js"
28 import index from "./index.js"
29 import listSortControl from "./list-sort-control.js"
30 import markdownEditor from "./markdown-editor.js"
31 import newUserPassword from "./new-user-password.js"
32 import notification from "./notification.js"
33 import optionalInput from "./optional-input.js"
34 import pageComments from "./page-comments.js"
35 import pageDisplay from "./page-display.js"
36 import pageEditor from "./page-editor.js"
37 import pagePicker from "./page-picker.js"
38 import permissionsTable from "./permissions-table.js"
39 import popup from "./popup.js"
40 import settingAppColorPicker from "./setting-app-color-picker.js"
41 import settingColorPicker from "./setting-color-picker.js"
42 import shelfSort from "./shelf-sort.js"
43 import sidebar from "./sidebar.js"
44 import sortableList from "./sortable-list.js"
45 import submitOnChange from "./submit-on-change.js"
46 import tabs from "./tabs.js"
47 import tagManager from "./tag-manager.js"
48 import templateManager from "./template-manager.js"
49 import toggleSwitch from "./toggle-switch.js"
50 import triLayout from "./tri-layout.js"
51 import wysiwygEditor from "./wysiwyg-editor.js"
52
53 const componentMapping = {
54     "add-remove-rows": addRemoveRows,
55     "ajax-delete-row": ajaxDeleteRow,
56     "ajax-form": ajaxForm,
57     "attachments": attachments,
58     "auto-suggest": autoSuggest,
59     "back-to-top": backToTop,
60     "book-sort": bookSort,
61     "breadcrumb-listing": breadcrumbListing,
62     "chapter-toggle": chapterToggle,
63     "code-editor": codeEditor,
64     "code-highlighter": codeHighlighter,
65     "collapsible": collapsible,
66     "custom-checkbox": customCheckbox,
67     "details-highlighter": detailsHighlighter,
68     "dropdown": dropdown,
69     "dropzone": dropzone,
70     "editor-toolbox": editorToolbox,
71     "entity-permissions-editor": entityPermissionsEditor,
72     "entity-search": entitySearch,
73     "entity-selector": entitySelector,
74     "entity-selector-popup": entitySelectorPopup,
75     "event-emit-select": eventEmitSelect,
76     "expand-toggle": expandToggle,
77     "header-mobile-toggle": headerMobileToggle,
78     "homepage-control": homepageControl,
79     "image-manager": imageManager,
80     "image-picker": imagePicker,
81     "index": index,
82     "list-sort-control": listSortControl,
83     "markdown-editor": markdownEditor,
84     "new-user-password": newUserPassword,
85     "notification": notification,
86     "optional-input": optionalInput,
87     "page-comments": pageComments,
88     "page-display": pageDisplay,
89     "page-editor": pageEditor,
90     "page-picker": pagePicker,
91     "permissions-table": permissionsTable,
92     "popup": popup,
93     "setting-app-color-picker": settingAppColorPicker,
94     "setting-color-picker": settingColorPicker,
95     "shelf-sort": shelfSort,
96     "sidebar": sidebar,
97     "sortable-list": sortableList,
98     "submit-on-change": submitOnChange,
99     "tabs": tabs,
100     "tag-manager": tagManager,
101     "template-manager": templateManager,
102     "toggle-switch": toggleSwitch,
103     "tri-layout": triLayout,
104     "wysiwyg-editor": wysiwygEditor,
105 };
106
107 window.components = {};
108
109 /**
110  * Initialize components of the given name within the given element.
111  * @param {String} componentName
112  * @param {HTMLElement|Document} parentElement
113  */
114 function searchForComponentInParent(componentName, parentElement) {
115     const elems = parentElement.querySelectorAll(`[${componentName}]`);
116     for (let j = 0, jLen = elems.length; j < jLen; j++) {
117         initComponent(componentName, elems[j]);
118     }
119 }
120
121 /**
122  * Initialize a component instance on the given dom element.
123  * @param {String} name
124  * @param {Element} element
125  */
126 function initComponent(name, element) {
127     const componentModel = componentMapping[name];
128     if (componentModel === undefined) return;
129
130     // Create our component instance
131     let instance;
132     try {
133         instance = new componentModel(element);
134         instance.$el = element;
135         const allRefs = parseRefs(name, element);
136         instance.$refs = allRefs.refs;
137         instance.$manyRefs = allRefs.manyRefs;
138         instance.$opts = parseOpts(name, element);
139         instance.$emit = (eventName, data = {}) => {
140             data.from = instance;
141             const event = new CustomEvent(`${name}-${eventName}`, {
142                 bubbles: true,
143                 detail: data
144             });
145             instance.$el.dispatchEvent(event);
146         };
147         if (typeof instance.setup === 'function') {
148             instance.setup();
149         }
150     } catch (e) {
151         console.error('Failed to create component', e, name, element);
152     }
153
154
155     // Add to global listing
156     if (typeof window.components[name] === "undefined") {
157         window.components[name] = [];
158     }
159     window.components[name].push(instance);
160
161     // Add to element listing
162     if (typeof element.components === 'undefined') {
163         element.components = {};
164     }
165     element.components[name] = instance;
166 }
167
168 /**
169  * Parse out the element references within the given element
170  * for the given component name.
171  * @param {String} name
172  * @param {Element} element
173  */
174 function parseRefs(name, element) {
175     const refs = {};
176     const manyRefs = {};
177
178     const prefix = `${name}@`
179     const selector = `[refs*="${prefix}"]`;
180     const refElems = [...element.querySelectorAll(selector)];
181     if (element.matches(selector)) {
182         refElems.push(element);
183     }
184
185     for (const el of refElems) {
186         const refNames = el.getAttribute('refs')
187             .split(' ')
188             .filter(str => str.startsWith(prefix))
189             .map(str => str.replace(prefix, ''))
190             .map(kebabToCamel);
191         for (const ref of refNames) {
192             refs[ref] = el;
193             if (typeof manyRefs[ref] === 'undefined') {
194                 manyRefs[ref] = [];
195             }
196             manyRefs[ref].push(el);
197         }
198     }
199     return {refs, manyRefs};
200 }
201
202 /**
203  * Parse out the element component options.
204  * @param {String} name
205  * @param {Element} element
206  * @return {Object<String, String>}
207  */
208 function parseOpts(name, element) {
209     const opts = {};
210     const prefix = `option:${name}:`;
211     for (const {name, value} of element.attributes) {
212         if (name.startsWith(prefix)) {
213             const optName = name.replace(prefix, '');
214             opts[kebabToCamel(optName)] = value || '';
215         }
216     }
217     return opts;
218 }
219
220 /**
221  * Convert a kebab-case string to camelCase
222  * @param {String} kebab
223  * @returns {string}
224  */
225 function kebabToCamel(kebab) {
226     const ucFirst = (word) => word.slice(0,1).toUpperCase() + word.slice(1);
227     const words = kebab.split('-');
228     return words[0] + words.slice(1).map(ucFirst).join('');
229 }
230
231 /**
232  * Initialize all components found within the given element.
233  * @param parentElement
234  */
235 function initAll(parentElement) {
236     if (typeof parentElement === 'undefined') parentElement = document;
237
238     // Old attribute system
239     for (const componentName of Object.keys(componentMapping)) {
240         searchForComponentInParent(componentName, parentElement);
241     }
242
243     // New component system
244     const componentElems = parentElement.querySelectorAll(`[component],[components]`);
245
246     for (const el of componentElems) {
247         const componentNames = `${el.getAttribute('component') || ''} ${(el.getAttribute('components'))}`.toLowerCase().split(' ').filter(Boolean);
248         for (const name of componentNames) {
249             initComponent(name, el);
250         }
251     }
252 }
253
254 window.components.init = initAll;
255 window.components.first = (name) => (window.components[name] || [null])[0];
256
257 export default initAll;
258
259 /**
260  * @typedef Component
261  * @property {HTMLElement} $el
262  * @property {Object<String, HTMLElement>} $refs
263  * @property {Object<String, HTMLElement[]>} $manyRefs
264  * @property {Object<String, String>} $opts
265  * @property {function(string, Object)} $emit
266  */