1
0
mirror of https://github.com/koloml/furbooru-tagging-assistant.git synced 2025-12-24 07:12:57 +00:00

Merge pull request #83 from koloml/feature/typeified-custom-events

Adding separate methods for events dispatching/listening with better type safety
This commit is contained in:
2025-02-06 22:46:19 +04:00
committed by GitHub
7 changed files with 133 additions and 13 deletions

View File

@@ -4,6 +4,12 @@ import {BaseComponent} from "$lib/components/base/BaseComponent.js";
import {getComponent} from "$lib/components/base/ComponentUtils.js";
import ScrapedAPI from "$lib/booru/scraped/ScrapedAPI.js";
import {tagsBlacklist} from "$config/tags.ts";
import {emitterAt} from "$lib/components/events/comms";
import {
eventActiveProfileChanged,
eventMaintenanceStateChanged,
eventTagsUpdated
} from "$lib/components/events/maintenance-popup-events";
class BlackListedTagsEncounteredError extends Error {
/**
@@ -45,6 +51,8 @@ export class MaintenancePopup extends BaseComponent {
/** @type {number|null} */
#tagsSubmissionTimer = null;
#emitter = emitterAt(this);
/**
* @protected
*/
@@ -95,7 +103,8 @@ export class MaintenancePopup extends BaseComponent {
this.#activeProfile = activeProfile;
this.container.classList.toggle('is-active', activeProfile !== null);
this.#refreshTagsList();
this.emit('active-profile-changed', activeProfile);
this.#emitter.emit(eventActiveProfileChanged, activeProfile);
}
#refreshTagsList() {
@@ -181,7 +190,7 @@ export class MaintenancePopup extends BaseComponent {
}
this.#isPlanningToSubmit = true;
this.emit('maintenance-state-change', 'waiting');
this.#emitter.emit(eventMaintenanceStateChanged, 'waiting');
}
}
@@ -208,7 +217,7 @@ export class MaintenancePopup extends BaseComponent {
this.#isPlanningToSubmit = false;
this.#isSubmitting = true;
this.emit('maintenance-state-change', 'processing');
this.#emitter.emit(eventMaintenanceStateChanged, 'processing');
let maybeTagsAndAliasesAfterUpdate;
@@ -249,17 +258,18 @@ export class MaintenancePopup extends BaseComponent {
}
MaintenancePopup.#notifyAboutPendingSubmission(false);
this.emit('maintenance-state-change', 'failed');
this.#emitter.emit(eventMaintenanceStateChanged, 'failed');
this.#isSubmitting = false;
return;
}
if (maybeTagsAndAliasesAfterUpdate) {
this.emit('tags-updated', maybeTagsAndAliasesAfterUpdate);
this.#emitter.emit(eventTagsUpdated, maybeTagsAndAliasesAfterUpdate);
}
this.emit('maintenance-state-change', 'complete');
this.#emitter.emit(eventMaintenanceStateChanged, 'complete');
this.#tagsToAdd.clear();
this.#tagsToRemove.clear();

View File

@@ -1,5 +1,7 @@
import {BaseComponent} from "$lib/components/base/BaseComponent.js";
import {getComponent} from "$lib/components/base/ComponentUtils.js";
import {on} from "$lib/components/events/comms";
import {eventMaintenanceStateChanged} from "$lib/components/events/maintenance-popup-events";
export class MaintenanceStatusIcon extends BaseComponent {
/** @type {import('MediaBoxTools.js').MediaBoxTools} */
@@ -16,7 +18,7 @@ export class MaintenanceStatusIcon extends BaseComponent {
throw new Error('Status icon element initialized outside of the media box!');
}
this.#mediaBoxTools.on('maintenance-state-change', this.#onMaintenanceStateChanged.bind(this));
on(this.#mediaBoxTools, eventMaintenanceStateChanged, this.#onMaintenanceStateChanged.bind(this));
}
/**

View File

@@ -1,6 +1,8 @@
import {BaseComponent} from "$lib/components/base/BaseComponent.js";
import {getComponent} from "$lib/components/base/ComponentUtils.js";
import {MaintenancePopup} from "$lib/components/MaintenancePopup.js";
import {on} from "$lib/components/events/comms";
import {eventActiveProfileChanged} from "$lib/components/events/maintenance-popup-events";
export class MediaBoxTools extends BaseComponent {
/** @type {import('MediaBoxWrapper.js').MediaBoxWrapper|null} */
@@ -34,11 +36,11 @@ export class MediaBoxTools extends BaseComponent {
}
}
this.on('active-profile-changed', this.#onActiveProfileChanged.bind(this));
on(this, eventActiveProfileChanged, this.#onActiveProfileChanged.bind(this));
}
/**
* @param {CustomEvent<MaintenanceProfile|null>} profileChangedEvent
* @param {CustomEvent<import('$entities/MaintenanceProfile.js').default|null>} profileChangedEvent
*/
#onActiveProfileChanged(profileChangedEvent) {
this.container.classList.toggle('has-active-profile', profileChangedEvent.detail !== null);
@@ -61,7 +63,7 @@ export class MediaBoxTools extends BaseComponent {
/**
* Create a maintenance popup element.
* @param {HTMLElement} childrenElements List of children elements to append to the component.
* @param {HTMLElement[]} childrenElements List of children elements to append to the component.
* @return {HTMLElement} The maintenance popup element.
*/
export function createMediaBoxTools(...childrenElements) {

View File

@@ -1,6 +1,8 @@
import {BaseComponent} from "$lib/components/base/BaseComponent.js";
import {getComponent} from "$lib/components/base/ComponentUtils.js";
import {buildTagsAndAliasesMap} from "$lib/booru/TagsUtils.js";
import {on} from "$lib/components/events/comms";
import {eventTagsUpdated} from "$lib/components/events/maintenance-popup-events";
export class MediaBoxWrapper extends BaseComponent {
#thumbnailContainer = null;
@@ -13,11 +15,11 @@ export class MediaBoxWrapper extends BaseComponent {
this.#thumbnailContainer = this.container.querySelector('.image-container');
this.#imageLinkElement = this.#thumbnailContainer.querySelector('a');
this.on('tags-updated', this.#onTagsUpdatedRefreshTagsAndAliases.bind(this));
on(this, eventTagsUpdated, this.#onTagsUpdatedRefreshTagsAndAliases.bind(this));
}
/**
* @param {CustomEvent<Map<string,string>>} tagsUpdatedEvent
* @param {CustomEvent<Map<string,string>|null>} tagsUpdatedEvent
*/
#onTagsUpdatedRefreshTagsAndAliases(tagsUpdatedEvent) {
const updatedMap = tagsUpdatedEvent.detail;

View File

@@ -45,7 +45,6 @@ export class BaseComponent {
/**
* @return {HTMLElement}
* @protected
*/
get container() {
return this.#container;

View File

@@ -0,0 +1,92 @@
import type {MaintenancePopupEventsMap} from "$lib/components/events/maintenance-popup-events.ts";
import {BaseComponent} from "$lib/components/base/BaseComponent";
interface EventsMapping extends MaintenancePopupEventsMap {
}
type EventCallback<EventDetails> = (event: CustomEvent<EventDetails>) => void;
type UnsubscribeFunction = () => void;
type ResolvableTarget = EventTarget | BaseComponent;
function resolveTarget(componentOrElement: ResolvableTarget): EventTarget {
if (componentOrElement instanceof BaseComponent) {
return componentOrElement.container;
}
return componentOrElement;
}
export function emit<Event extends keyof EventsMapping>(
targetOrComponent: ResolvableTarget,
event: Event,
details: EventsMapping[Event]
) {
const target = resolveTarget(targetOrComponent);
target.dispatchEvent(
new CustomEvent(event, {
detail: details,
bubbles: true
})
);
}
export function on<Event extends keyof EventsMapping>(
targetOrComponent: ResolvableTarget,
eventName: Event,
callback: EventCallback<EventsMapping[Event]>,
options: AddEventListenerOptions | null = null
): UnsubscribeFunction {
const target = resolveTarget(targetOrComponent);
const controller = new AbortController();
target.addEventListener(
eventName,
callback as EventListener,
{
signal: controller.signal,
once: options?.once
}
);
return () => controller.abort();
}
const onceOptions = {once: true};
export function once<Event extends keyof EventsMapping>(
targetOrComponent: ResolvableTarget,
eventName: Event,
callback: EventCallback<EventsMapping[Event]>
): UnsubscribeFunction {
return on(
targetOrComponent,
eventName,
callback,
onceOptions
);
}
class TargetedEmitter {
readonly #element: ResolvableTarget;
constructor(targetOrComponent: ResolvableTarget) {
this.#element = targetOrComponent;
}
emit<Event extends keyof EventsMapping>(eventName: Event, details: EventsMapping[Event]): void {
emit(this.#element, eventName, details);
}
on<Event extends keyof EventsMapping>(eventName: Event, callback: EventCallback<EventsMapping[Event]>, options: AddEventListenerOptions | null = null): UnsubscribeFunction {
return on(this.#element, eventName, callback, options);
}
once<Event extends keyof EventsMapping>(eventName: Event, callback: EventCallback<EventsMapping[Event]>): UnsubscribeFunction {
return once(this.#element, eventName, callback);
}
}
export function emitterAt(targetOrComponent: ResolvableTarget): TargetedEmitter {
return new TargetedEmitter(targetOrComponent);
}

View File

@@ -0,0 +1,13 @@
import type MaintenanceProfile from "$entities/MaintenanceProfile.ts";
export const eventActiveProfileChanged = 'active-profile-changed';
export const eventMaintenanceStateChanged = 'maintenance-state-change';
export const eventTagsUpdated = 'tags-updated';
type MaintenanceState = 'processing' | 'failed' | 'complete' | 'waiting';
export interface MaintenancePopupEventsMap {
[eventActiveProfileChanged]: MaintenanceProfile | null;
[eventMaintenanceStateChanged]: MaintenanceState;
[eventTagsUpdated]: Map<string, string> | null;
}