]> BookStack Code Mirror - bookstack/blobdiff - resources/js/components/index.js
Updated callout link formatting
[bookstack] / resources / js / components / index.js
index 14cf08ae2da41014e4703c57bfae6f79c4dc1c9e..a0269ea61f771422d97f27e49e173681dd5885e9 100644 (file)
-import dropdown from "./dropdown";
-import overlay from "./overlay";
-import backToTop from "./back-to-top";
-import notification from "./notification";
-import chapterToggle from "./chapter-toggle";
-import expandToggle from "./expand-toggle";
-import entitySelectorPopup from "./entity-selector-popup";
-import entitySelector from "./entity-selector";
-import sidebar from "./sidebar";
-import pagePicker from "./page-picker";
-import pageComments from "./page-comments";
-import wysiwygEditor from "./wysiwyg-editor";
-import markdownEditor from "./markdown-editor";
-import editorToolbox from "./editor-toolbox";
-import imagePicker from "./image-picker";
-import collapsible from "./collapsible";
-import toggleSwitch from "./toggle-switch";
-import pageDisplay from "./page-display";
-import shelfSort from "./shelf-sort";
-import homepageControl from "./homepage-control";
-import headerMobileToggle from "./header-mobile-toggle";
-import listSortControl from "./list-sort-control";
-import triLayout from "./tri-layout";
-import breadcrumbListing from "./breadcrumb-listing";
-import permissionsTable from "./permissions-table";
-import customCheckbox from "./custom-checkbox";
-import bookSort from "./book-sort";
-import settingAppColorPicker from "./setting-app-color-picker";
-import entityPermissionsEditor from "./entity-permissions-editor";
-import templateManager from "./template-manager";
-import newUserPassword from "./new-user-password";
-
-const componentMapping = {
-    'dropdown': dropdown,
-    'overlay': overlay,
-    'back-to-top': backToTop,
-    'notification': notification,
-    'chapter-toggle': chapterToggle,
-    'expand-toggle': expandToggle,
-    'entity-selector-popup': entitySelectorPopup,
-    'entity-selector': entitySelector,
-    'sidebar': sidebar,
-    'page-picker': pagePicker,
-    'page-comments': pageComments,
-    'wysiwyg-editor': wysiwygEditor,
-    'markdown-editor': markdownEditor,
-    'editor-toolbox': editorToolbox,
-    'image-picker': imagePicker,
-    'collapsible': collapsible,
-    'toggle-switch': toggleSwitch,
-    'page-display': pageDisplay,
-    'shelf-sort': shelfSort,
-    'homepage-control': homepageControl,
-    'header-mobile-toggle': headerMobileToggle,
-    'list-sort-control': listSortControl,
-    'tri-layout': triLayout,
-    'breadcrumb-listing': breadcrumbListing,
-    'permissions-table': permissionsTable,
-    'custom-checkbox': customCheckbox,
-    'book-sort': bookSort,
-    'setting-app-color-picker': settingAppColorPicker,
-    'entity-permissions-editor': entityPermissionsEditor,
-    'template-manager': templateManager,
-    'new-user-password': newUserPassword,
-};
+const componentMapping = {};
 
 
-window.components = {};
+const definitionFiles = require.context('./', false, /\.js$/);
+for (const fileName of definitionFiles.keys()) {
+    const name = fileName.replace('./', '').split('.')[0];
+    if (name !== 'index') {
+        componentMapping[name] = definitionFiles(fileName).default;
+    }
+}
 
 
-const componentNames = Object.keys(componentMapping);
+window.components = {};
 
 /**
  * Initialize components of the given name within the given element.
  * @param {String} componentName
  * @param {HTMLElement|Document} parentElement
  */
 
 /**
  * Initialize components of the given name within the given element.
  * @param {String} componentName
  * @param {HTMLElement|Document} parentElement
  */
-function initComponent(componentName, parentElement) {
-    let elems = parentElement.querySelectorAll(`[${componentName}]`);
-    if (elems.length === 0) return;
-
-    let component = componentMapping[componentName];
-    if (typeof window.components[componentName] === "undefined") window.components[componentName] = [];
+function searchForComponentInParent(componentName, parentElement) {
+    const elems = parentElement.querySelectorAll(`[${componentName}]`);
     for (let j = 0, jLen = elems.length; j < jLen; j++) {
     for (let j = 0, jLen = elems.length; j < jLen; j++) {
-        let instance = new component(elems[j]);
-        if (typeof elems[j].components === 'undefined') elems[j].components = {};
-        elems[j].components[componentName] = instance;
-        window.components[componentName].push(instance);
+        initComponent(componentName, elems[j]);
+    }
+}
+
+/**
+ * Initialize a component instance on the given dom element.
+ * @param {String} name
+ * @param {Element} element
+ */
+function initComponent(name, element) {
+    const componentModel = componentMapping[name];
+    if (componentModel === undefined) return;
+
+    // Create our component instance
+    let instance;
+    try {
+        instance = new componentModel(element);
+        instance.$el = element;
+        const allRefs = parseRefs(name, element);
+        instance.$refs = allRefs.refs;
+        instance.$manyRefs = allRefs.manyRefs;
+        instance.$opts = parseOpts(name, element);
+        instance.$emit = (eventName, data = {}) => {
+            data.from = instance;
+            const event = new CustomEvent(`${name}-${eventName}`, {
+                bubbles: true,
+                detail: data
+            });
+            instance.$el.dispatchEvent(event);
+        };
+        if (typeof instance.setup === 'function') {
+            instance.setup();
+        }
+    } catch (e) {
+        console.error('Failed to create component', e, name, element);
+    }
+
+
+    // Add to global listing
+    if (typeof window.components[name] === "undefined") {
+        window.components[name] = [];
+    }
+    window.components[name].push(instance);
+
+    // Add to element listing
+    if (typeof element.components === 'undefined') {
+        element.components = {};
     }
     }
+    element.components[name] = instance;
+}
+
+/**
+ * Parse out the element references within the given element
+ * for the given component name.
+ * @param {String} name
+ * @param {Element} element
+ */
+function parseRefs(name, element) {
+    const refs = {};
+    const manyRefs = {};
+
+    const prefix = `${name}@`
+    const selector = `[refs*="${prefix}"]`;
+    const refElems = [...element.querySelectorAll(selector)];
+    if (element.matches(selector)) {
+        refElems.push(element);
+    }
+
+    for (const el of refElems) {
+        const refNames = el.getAttribute('refs')
+            .split(' ')
+            .filter(str => str.startsWith(prefix))
+            .map(str => str.replace(prefix, ''))
+            .map(kebabToCamel);
+        for (const ref of refNames) {
+            refs[ref] = el;
+            if (typeof manyRefs[ref] === 'undefined') {
+                manyRefs[ref] = [];
+            }
+            manyRefs[ref].push(el);
+        }
+    }
+    return {refs, manyRefs};
+}
+
+/**
+ * Parse out the element component options.
+ * @param {String} name
+ * @param {Element} element
+ * @return {Object<String, String>}
+ */
+function parseOpts(name, element) {
+    const opts = {};
+    const prefix = `option:${name}:`;
+    for (const {name, value} of element.attributes) {
+        if (name.startsWith(prefix)) {
+            const optName = name.replace(prefix, '');
+            opts[kebabToCamel(optName)] = value || '';
+        }
+    }
+    return opts;
+}
+
+/**
+ * Convert a kebab-case string to camelCase
+ * @param {String} kebab
+ * @returns {string}
+ */
+function kebabToCamel(kebab) {
+    const ucFirst = (word) => word.slice(0,1).toUpperCase() + word.slice(1);
+    const words = kebab.split('-');
+    return words[0] + words.slice(1).map(ucFirst).join('');
 }
 
 /**
 }
 
 /**
@@ -93,11 +138,33 @@ function initComponent(componentName, parentElement) {
  */
 function initAll(parentElement) {
     if (typeof parentElement === 'undefined') parentElement = document;
  */
 function initAll(parentElement) {
     if (typeof parentElement === 'undefined') parentElement = document;
-    for (let i = 0, len = componentNames.length; i < len; i++) {
-        initComponent(componentNames[i], parentElement);
+
+    // Old attribute system
+    for (const componentName of Object.keys(componentMapping)) {
+        searchForComponentInParent(componentName, parentElement);
+    }
+
+    // New component system
+    const componentElems = parentElement.querySelectorAll(`[component],[components]`);
+
+    for (const el of componentElems) {
+        const componentNames = `${el.getAttribute('component') || ''} ${(el.getAttribute('components'))}`.toLowerCase().split(' ').filter(Boolean);
+        for (const name of componentNames) {
+            initComponent(name, el);
+        }
     }
 }
 
 window.components.init = initAll;
     }
 }
 
 window.components.init = initAll;
+window.components.first = (name) => (window.components[name] || [null])[0];
 
 export default initAll;
 
 export default initAll;
+
+/**
+ * @typedef Component
+ * @property {HTMLElement} $el
+ * @property {Object<String, HTMLElement>} $refs
+ * @property {Object<String, HTMLElement[]>} $manyRefs
+ * @property {Object<String, String>} $opts
+ * @property {function(string, Object)} $emit
+ */
\ No newline at end of file