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