From c22023775a209a76a828038fe7bf94d79fdc5a4d Mon Sep 17 00:00:00 2001 From: KoloMl Date: Sun, 31 Mar 2024 21:28:29 +0400 Subject: [PATCH] More comfortable simple components system --- src/content/listing.js | 7 +- src/lib/components/MaintenancePopup.js | 64 +++++++++++------- src/lib/components/MaintenanceTools.js | 54 ++++++++++++++++ src/lib/components/base/BaseComponent.js | 79 +++++++++++++++++++++++ src/lib/components/base/ComponentUtils.js | 22 +++++++ 5 files changed, 202 insertions(+), 24 deletions(-) create mode 100644 src/lib/components/MaintenanceTools.js create mode 100644 src/lib/components/base/BaseComponent.js create mode 100644 src/lib/components/base/ComponentUtils.js diff --git a/src/content/listing.js b/src/content/listing.js index ebcd68e..6673d05 100644 --- a/src/content/listing.js +++ b/src/content/listing.js @@ -1,5 +1,10 @@ import {createMaintenancePopup} from "$lib/components/MaintenancePopup.js"; +import {createMaintenanceTools} from "$lib/components/MaintenanceTools.js"; document.querySelectorAll('.media-box').forEach(mediaBoxElement => { - mediaBoxElement.appendChild(createMaintenancePopup()); + mediaBoxElement.appendChild( + createMaintenanceTools( + createMaintenancePopup() + ) + ); }); diff --git a/src/lib/components/MaintenancePopup.js b/src/lib/components/MaintenancePopup.js index bbe9673..f65ed6c 100644 --- a/src/lib/components/MaintenancePopup.js +++ b/src/lib/components/MaintenancePopup.js @@ -1,10 +1,9 @@ import MaintenanceSettings from "$lib/extension/settings/MaintenanceSettings.js"; import MaintenanceProfile from "$entities/MaintenanceProfile.js"; +import {maintenanceToolsEvents} from "$lib/components/MaintenanceTools.js"; +import {BaseComponent} from "$lib/components/base/BaseComponent.js"; -export class MaintenancePopup { - /** @type {HTMLElement} */ - #container; - +export class MaintenancePopup extends BaseComponent { /** @type {HTMLElement} */ #tagsListElement; @@ -14,33 +13,46 @@ export class MaintenancePopup { /** @type {MaintenanceProfile|null} */ #activeProfile = null; - /** - * @param {HTMLElement} container - */ - constructor(container) { - this.#container = container; - } + /** @type {import('MaintenanceTools.js').MaintenanceTools|null} */ + #parentTools = null; + /** + * @protected + */ build() { - this.#container.innerHTML = ''; - this.#container.classList.add('maintenance-popup'); + this.container.innerHTML = ''; + this.container.classList.add('maintenance-popup'); this.#tagsListElement = document.createElement('div'); this.#tagsListElement.classList.add('tags-list'); - this.#container.append( + this.container.append( this.#tagsListElement, ); - - return this; } + /** + * @protected + */ init() { - MaintenancePopup.#watchActiveProfile(activeProfile => { - this.#activeProfile = activeProfile; - this.#container.classList.toggle('is-active', activeProfile !== null); - this.#refreshTagsList(); - }); + this.once(maintenanceToolsEvents.init, this.#onToolsContainerInitialized.bind(this)); + MaintenancePopup.#watchActiveProfile(this.#onActiveProfileChanged.bind(this)); + } + + /** + * @param {import('MaintenanceTools.js').MaintenanceTools} toolsInstance + */ + #onToolsContainerInitialized(toolsInstance) { + this.#parentTools = toolsInstance; + } + + /** + * @param {MaintenanceProfile|null} activeProfile + */ + #onActiveProfileChanged(activeProfile) { + this.#activeProfile = activeProfile; + this.container.classList.toggle('is-active', activeProfile !== null); + this.#refreshTagsList(); } #refreshTagsList() { @@ -62,6 +74,13 @@ export class MaintenancePopup { }); } + /** + * @return {boolean} + */ + get isActive() { + return this.container.classList.contains('is-active'); + } + /** * @param {string} tagName * @return {HTMLElement} @@ -70,6 +89,7 @@ export class MaintenancePopup { const tagElement = document.createElement('span'); tagElement.classList.add('tag'); tagElement.innerText = tagName; + tagElement.dataset.name = tagName; return tagElement; } @@ -124,9 +144,7 @@ export class MaintenancePopup { export function createMaintenancePopup() { const container = document.createElement('div'); - new MaintenancePopup(container) - .build() - .init(); + new MaintenancePopup(container); return container; } diff --git a/src/lib/components/MaintenanceTools.js b/src/lib/components/MaintenanceTools.js new file mode 100644 index 0000000..ceab392 --- /dev/null +++ b/src/lib/components/MaintenanceTools.js @@ -0,0 +1,54 @@ +import {BaseComponent} from "$lib/components/base/BaseComponent.js"; +import {getComponent} from "$lib/components/base/ComponentUtils.js"; +import {MaintenancePopup} from "$lib/components/MaintenancePopup.js"; + +export const maintenanceToolsEvents = { + init: 'maintenance-tools-init' +} + +export class MaintenanceTools extends BaseComponent { + /** @type {MaintenancePopup|null} */ + #maintenancePopup = null; + + init() { + for (let childElement of this.container.children) { + const component = getComponent(childElement); + + if (!component) { + continue; + } + + if (!this.#maintenancePopup && component instanceof MaintenancePopup) { + this.#maintenancePopup = component; + } + + component.emit(maintenanceToolsEvents.init, this); + } + } + + /** + * @return {MaintenancePopup|null} + */ + get maintenancePopup() { + return this.#maintenancePopup; + } +} + +/** + * Create a maintenance popup element. + * @param {HTMLElement} childrenElements List of children elements to append to the component. + * @return {HTMLElement} The maintenance popup element. + */ +export function createMaintenanceTools(...childrenElements) { + const mediaBoxToolsContainer = document.createElement('div'); + mediaBoxToolsContainer.classList.add('media-box-tools'); + + if (childrenElements.length) { + mediaBoxToolsContainer.append(...childrenElements); + } + + new MaintenanceTools(mediaBoxToolsContainer) + .init(); + + return mediaBoxToolsContainer; +} diff --git a/src/lib/components/base/BaseComponent.js b/src/lib/components/base/BaseComponent.js new file mode 100644 index 0000000..b684292 --- /dev/null +++ b/src/lib/components/base/BaseComponent.js @@ -0,0 +1,79 @@ +import {bindComponent} from "$lib/components/base/ComponentUtils.js"; + +/** + * @abstract + */ +export class BaseComponent { + /** @type {HTMLElement} */ + #container; + + /** + * @param {HTMLElement} container + */ + constructor(container) { + this.#container = container; + + bindComponent(container, this); + + this.build(); + this.init(); + } + + /** + * @protected + */ + build() { + // This method can be implemented by the component classes to modify or create the inner elements. + } + + /** + * @protected + */ + init() { + // This method can be implemented by the component classes to initialize the component. + } + + /** + * @return {HTMLElement} + * @protected + */ + get container() { + return this.#container; + } + + /** + * Emit the custom event on the container element. + * @param {keyof HTMLElementEventMap|string} event The event name. + * @param {any} [detail] The event detail. Can be omitted. + */ + emit(event, detail = undefined) { + this.#container.dispatchEvent(new CustomEvent(event, {detail})); + } + + /** + * Subscribe to the DOM event on the container element. + * @param {keyof HTMLElementEventMap|string} event The event name. + * @param {function(Event): void} listener The event listener. + * @param {AddEventListenerOptions|undefined} [options] The event listener options. Can be omitted. + * @return {function(): void} The unsubscribe function. + */ + on(event, listener, options = undefined) { + this.#container.addEventListener(event, listener, options); + + return () => void this.#container.removeEventListener(event, listener, options); + } + + /** + * Subscribe to the DOM event on the container element. The event listener will be called only once. + * @param {keyof HTMLElementEventMap|string} event The event name. + * @param {function(Event): void} listener The event listener. + * @param {AddEventListenerOptions|undefined} [options] The event listener options. Can be omitted. + * @return {function(): void} The unsubscribe function. + */ + once(event, listener, options = undefined) { + options = options || {}; + options.once = true; + + return this.on(event, listener, options); + } +} diff --git a/src/lib/components/base/ComponentUtils.js b/src/lib/components/base/ComponentUtils.js new file mode 100644 index 0000000..16fca35 --- /dev/null +++ b/src/lib/components/base/ComponentUtils.js @@ -0,0 +1,22 @@ +const instanceSymbol = Symbol('instance'); + +/** + * @param {HTMLElement} element + * @return {import('./BaseComponent.js').BaseComponent|null} + */ +export function getComponent(element) { + return element[instanceSymbol] || null; +} + +/** + * Bind the component to the selected element. + * @param {HTMLElement} element The element to bind the component to. + * @param {import('./BaseComponent.js').BaseComponent} instance The component instance. + */ +export function bindComponent(element, instance) { + if (element[instanceSymbol]) { + throw new Error('The element is already bound to a component.'); + } + + element[instanceSymbol] = instance; +}