import addRemoveRows from "./add-remove-rows.js" import ajaxDeleteRow from "./ajax-delete-row.js" import ajaxForm from "./ajax-form.js" import attachments from "./attachments.js" import autoSuggest from "./auto-suggest.js" import backToTop from "./back-to-top.js" import bookSort from "./book-sort.js" import breadcrumbListing from "./breadcrumb-listing.js" import chapterToggle from "./chapter-toggle.js" import codeEditor from "./code-editor.js" import codeHighlighter from "./code-highlighter.js" import collapsible from "./collapsible.js" import customCheckbox from "./custom-checkbox.js" import detailsHighlighter from "./details-highlighter.js" import dropdown from "./dropdown.js" import dropzone from "./dropzone.js" import editorToolbox from "./editor-toolbox.js" import entityPermissionsEditor from "./entity-permissions-editor.js" import entitySearch from "./entity-search.js" import entitySelector from "./entity-selector.js" import entitySelectorPopup from "./entity-selector-popup.js" import eventEmitSelect from "./event-emit-select.js" import expandToggle from "./expand-toggle.js" import headerMobileToggle from "./header-mobile-toggle.js" import homepageControl from "./homepage-control.js" import imageManager from "./image-manager.js" import imagePicker from "./image-picker.js" import index from "./index.js" import listSortControl from "./list-sort-control.js" import markdownEditor from "./markdown-editor.js" import newUserPassword from "./new-user-password.js" import notification from "./notification.js" import optionalInput from "./optional-input.js" import pageComments from "./page-comments.js" import pageDisplay from "./page-display.js" import pageEditor from "./page-editor.js" import pagePicker from "./page-picker.js" import permissionsTable from "./permissions-table.js" import popup from "./popup.js" import settingAppColorPicker from "./setting-app-color-picker.js" import settingColorPicker from "./setting-color-picker.js" import shelfSort from "./shelf-sort.js" import sidebar from "./sidebar.js" import sortableList from "./sortable-list.js" import tabs from "./tabs.js" import tagManager from "./tag-manager.js" import templateManager from "./template-manager.js" import toggleSwitch from "./toggle-switch.js" import triLayout from "./tri-layout.js" import wysiwygEditor from "./wysiwyg-editor.js" const componentMapping = { "add-remove-rows": addRemoveRows, "ajax-delete-row": ajaxDeleteRow, "ajax-form": ajaxForm, "attachments": attachments, "auto-suggest": autoSuggest, "back-to-top": backToTop, "book-sort": bookSort, "breadcrumb-listing": breadcrumbListing, "chapter-toggle": chapterToggle, "code-editor": codeEditor, "code-highlighter": codeHighlighter, "collapsible": collapsible, "custom-checkbox": customCheckbox, "details-highlighter": detailsHighlighter, "dropdown": dropdown, "dropzone": dropzone, "editor-toolbox": editorToolbox, "entity-permissions-editor": entityPermissionsEditor, "entity-search": entitySearch, "entity-selector": entitySelector, "entity-selector-popup": entitySelectorPopup, "event-emit-select": eventEmitSelect, "expand-toggle": expandToggle, "header-mobile-toggle": headerMobileToggle, "homepage-control": homepageControl, "image-manager": imageManager, "image-picker": imagePicker, "index": index, "list-sort-control": listSortControl, "markdown-editor": markdownEditor, "new-user-password": newUserPassword, "notification": notification, "optional-input": optionalInput, "page-comments": pageComments, "page-display": pageDisplay, "page-editor": pageEditor, "page-picker": pagePicker, "permissions-table": permissionsTable, "popup": popup, "setting-app-color-picker": settingAppColorPicker, "setting-color-picker": settingColorPicker, "shelf-sort": shelfSort, "sidebar": sidebar, "sortable-list": sortableList, "tabs": tabs, "tag-manager": tagManager, "template-manager": templateManager, "toggle-switch": toggleSwitch, "tri-layout": triLayout, "wysiwyg-editor": wysiwygEditor, }; window.components = {}; /** * Initialize components of the given name within the given element. * @param {String} componentName * @param {HTMLElement|Document} parentElement */ function searchForComponentInParent(componentName, parentElement) { const elems = parentElement.querySelectorAll(`[${componentName}]`); for (let j = 0, jLen = elems.length; j < jLen; j++) { 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} */ 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(''); } /** * Initialize all components found within the given element. * @param parentElement */ function initAll(parentElement) { if (typeof parentElement === 'undefined') parentElement = document; // 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.first = (name) => (window.components[name] || [null])[0]; export default initAll; /** * @typedef Component * @property {HTMLElement} $el * @property {Object} $refs * @property {Object} $manyRefs * @property {Object} $opts * @property {function(string, Object)} $emit */