From be736b3939a428674ccab33c7aaca95c05ef6437 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 16 Nov 2022 15:46:41 +0000 Subject: [PATCH] Replaced el.components mapping with component service weakmap Old system was hard to track in terms of usage and it's application of 'components' properties directly to elements was shoddy. This routes usage via the components service, with element-specific component usage tracked via a local weakmap. Updated existing found usages to use the new system. --- resources/js/components/attachments.js | 4 +- resources/js/components/code-editor.js | 11 ++++- resources/js/components/confirm-dialog.js | 2 +- .../js/components/entity-selector-popup.js | 16 +++++-- resources/js/components/image-manager.js | 11 ++++- resources/js/components/page-editor.js | 3 +- resources/js/components/tag-manager.js | 3 +- resources/js/components/user-select.js | 11 +++-- resources/js/services/components.js | 42 +++++++++++++++---- 9 files changed, 81 insertions(+), 22 deletions(-) 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} + */ const components = {}; -const componentMap = {}; + +/** + * A mapping of component class models, keyed by name. + * @type {Object>} + */ +const componentModelMap = {}; + +/** + * A mapping of active component maps, keyed by the element components are assigned to. + * @type {WeakMap>} + */ +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|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