diff --git a/resources/js/components/attachments.js b/resources/js/components/attachments.js index b373e1d47..b4e400aeb 100644 --- a/resources/js/components/attachments.js +++ b/resources/js/components/attachments.js @@ -43,7 +43,9 @@ export class Attachments extends Component { reloadList() { this.stopEdit(); - this.mainTabs.components.tabs.show('items'); + /** @var {Tabs} */ + const tabs = window.$components.firstOnElement(this.mainTabs, 'tabs'); + tabs.show('items'); window.$http.get(`/attachments/get/page/${this.pageId}`).then(resp => { this.list.innerHTML = resp.data; window.$components.init(this.list); diff --git a/resources/js/components/code-editor.js b/resources/js/components/code-editor.js index 241cdece9..205cbd8fd 100644 --- a/resources/js/components/code-editor.js +++ b/resources/js/components/code-editor.js @@ -126,7 +126,7 @@ export class CodeEditor extends Component { } this.loadHistory(); - this.popup.components.popup.show(() => { + this.getPopup().show(() => { Code.updateLayout(this.editor); this.editor.focus(); }, () => { @@ -135,10 +135,17 @@ export class CodeEditor extends Component { } hide() { - this.popup.components.popup.hide(); + this.getPopup().hide(); this.addHistory(); } + /** + * @returns {Popup} + */ + getPopup() { + return window.$components.firstOnElement(this.popup, 'popup'); + } + async updateEditorMode(language) { const Code = await window.importVersioned('code'); Code.setMode(this.editor, language, this.editor.getValue()); diff --git a/resources/js/components/confirm-dialog.js b/resources/js/components/confirm-dialog.js index 215c0b94e..572945d5a 100644 --- a/resources/js/components/confirm-dialog.js +++ b/resources/js/components/confirm-dialog.js @@ -34,7 +34,7 @@ export class ConfirmDialog extends Component { * @returns {Popup} */ getPopup() { - return this.container.components.popup; + return window.$components.firstOnElement(this.container, 'popup'); } /** diff --git a/resources/js/components/entity-selector-popup.js b/resources/js/components/entity-selector-popup.js index 69534dea5..d455f7ee7 100644 --- a/resources/js/components/entity-selector-popup.js +++ b/resources/js/components/entity-selector-popup.js @@ -17,16 +17,26 @@ export class EntitySelectorPopup extends Component { show(callback) { this.callback = callback; - this.container.components.popup.show(); + this.getPopup().show(); this.getSelector().focusSearch(); } hide() { - this.container.components.popup.hide(); + this.getPopup().hide(); } + /** + * @returns {Popup} + */ + getPopup() { + return window.$components.firstOnElement(this.container, 'popup'); + } + + /** + * @returns {EntitySelector} + */ getSelector() { - return this.selectorEl.components['entity-selector']; + return window.$components.firstOnElement(this.selectorEl, 'entity-selector'); } onSelectButtonClick() { diff --git a/resources/js/components/image-manager.js b/resources/js/components/image-manager.js index a78aa3483..a44fffc1b 100644 --- a/resources/js/components/image-manager.js +++ b/resources/js/components/image-manager.js @@ -94,7 +94,7 @@ export class ImageManager extends Component { this.callback = callback; this.type = type; - this.popupEl.components.popup.show(); + this.getPopup().show(); this.dropzoneContainer.classList.toggle('hidden', type !== 'gallery'); if (!this.hasData) { @@ -104,7 +104,14 @@ export class ImageManager extends Component { } hide() { - this.popupEl.components.popup.hide(); + this.getPopup().hide(); + } + + /** + * @returns {Popup} + */ + getPopup() { + return window.$components.firstOnElement(this.popupEl, 'popup'); } async loadGallery() { diff --git a/resources/js/components/page-editor.js b/resources/js/components/page-editor.js index 41e070b9d..d6faabd05 100644 --- a/resources/js/components/page-editor.js +++ b/resources/js/components/page-editor.js @@ -196,7 +196,8 @@ export class PageEditor extends Component { event.preventDefault(); const link = event.target.closest('a').href; - const dialog = this.switchDialogContainer.components['confirm-dialog']; + /** @var {ConfirmDialog} **/ + const dialog = window.$components.firstOnElement(this.switchDialogContainer, 'confirm-dialog'); const [saved, confirmed] = await Promise.all([this.saveDraft(), dialog.show()]); if (saved && confirmed) { diff --git a/resources/js/components/tag-manager.js b/resources/js/components/tag-manager.js index b51cfe9b2..cfbc514a0 100644 --- a/resources/js/components/tag-manager.js +++ b/resources/js/components/tag-manager.js @@ -11,7 +11,8 @@ export class TagManager extends Component { setupListeners() { this.container.addEventListener('change', event => { - const addRemoveComponent = this.addRemoveComponentEl.components['add-remove-rows']; + /** @var {AddRemoveRows} **/ + const addRemoveComponent = window.$components.firstOnElement(this.addRemoveComponentEl, 'add-remove-rows'); if (!this.hasEmptyRows()) { addRemoveComponent.add(); } diff --git a/resources/js/components/user-select.js b/resources/js/components/user-select.js index 549963eed..d4d88a633 100644 --- a/resources/js/components/user-select.js +++ b/resources/js/components/user-select.js @@ -4,12 +4,11 @@ import {Component} from "./component"; export class UserSelect extends Component { setup() { + this.container = this.$el; this.input = this.$refs.input; this.userInfoContainer = this.$refs.userInfo; - this.hide = this.$el.components.dropdown.hide; - - onChildEvent(this.$el, 'a.dropdown-search-item', 'click', this.selectUser.bind(this)); + onChildEvent(this.container, 'a.dropdown-search-item', 'click', this.selectUser.bind(this)); } selectUser(event, userEl) { @@ -20,4 +19,10 @@ export class UserSelect extends Component { this.hide(); } + hide() { + /** @var {Dropdown} **/ + const dropdown = window.$components.firstOnElement(this.container, 'dropdown'); + dropdown.hide(); + } + } \ No newline at end of file diff --git a/resources/js/services/components.js b/resources/js/services/components.js index 04fd0dcf4..c0fc12524 100644 --- a/resources/js/services/components.js +++ b/resources/js/services/components.js @@ -1,5 +1,21 @@ +/** + * A mapping of active components keyed by name, with values being arrays of component + * instances since there can be multiple components of the same type. + * @type {Object<String, Component[]>} + */ const components = {}; -const componentMap = {}; + +/** + * A mapping of component class models, keyed by name. + * @type {Object<String, Constructor<Component>>} + */ +const componentModelMap = {}; + +/** + * A mapping of active component maps, keyed by the element components are assigned to. + * @type {WeakMap<Element, Object<String, Component>>} + */ +const elementComponentMap = new WeakMap(); /** * Initialize a component instance on the given dom element. @@ -8,7 +24,7 @@ const componentMap = {}; */ function initComponent(name, element) { /** @type {Function<Component>|undefined} **/ - const componentModel = componentMap[name]; + const componentModel = componentModelMap[name]; if (componentModel === undefined) return; // Create our component instance @@ -33,11 +49,10 @@ function initComponent(name, element) { } components[name].push(instance); - // Add to element listing - if (typeof element.components === 'undefined') { - element.components = {}; - } - element.components[name] = instance; + // Add to element mapping + const elComponents = elementComponentMap.get(element) || {}; + elComponents[name] = instance; + elementComponentMap.set(element, elComponents); } /** @@ -125,7 +140,7 @@ export function init(parentElement = document) { export function register(mapping) { const keys = Object.keys(mapping); for (const key of keys) { - componentMap[camelToKebab(key)] = mapping[key]; + componentModelMap[camelToKebab(key)] = mapping[key]; } } @@ -147,6 +162,17 @@ export function get(name = '') { return components[name] || []; } +/** + * Get the first component, of the given name, that's assigned to the given element. + * @param {Element} element + * @param {String} name + * @returns {Component|null} + */ +export function firstOnElement(element, name) { + const elComponents = elementComponentMap.get(element) || {}; + return elComponents[name] || null; +} + function camelToKebab(camelStr) { return camelStr.replace(/[A-Z]/g, (str, offset) => (offset > 0 ? '-' : '') + str.toLowerCase()); } \ No newline at end of file