Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] Refactor services configuration #537

Merged
merged 5 commits into from
Nov 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ All notable changes to this project will be documented in this file. The format

### Changed

- Refactor services to classes ([#535](https://github.com/studiometa/js-toolkit/pull/535), [258bb590](https://github.com/studiometa/js-toolkit/commit/258bb590))
- Refactor services to classes ([#535](https://github.com/studiometa/js-toolkit/pull/535), [#537](https://github.com/studiometa/js-toolkit/pull/537))

## [v3.0.0-alpha.12](https://github.com/studiometa/js-toolkit/compare/3.0.0-alpha.11..3.0.0-alpha.12) (2024-11-15)

Expand Down
112 changes: 87 additions & 25 deletions packages/js-toolkit/services/AbstractService.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { isFunction } from '../utils/index.js';

export interface ServiceInterface<T> {
/**
* Remove a function from the resize service by its key.
Expand All @@ -17,10 +19,65 @@ export interface ServiceInterface<T> {
props(): T;
}

export class AbstractService<PropsType> {
isInit = false;
/**
* Service configuration of events to be attached to targets.
*/
export type ServiceConfig = [
EventTarget | ((instance: AbstractService) => EventTarget),
[string, AddEventListenerOptions?][],
][];

/**
* AbstractService class.
*/
export class AbstractService<PropsType = any> {
/**
* Used to type `this.constructor` correctly
* @see https://github.com/microsoft/TypeScript/issues/3841#issuecomment-2381594311
*/
declare ['constructor']: typeof AbstractService;

/**
* Service configuration.
*/
static config: ServiceConfig = [];

/**
* Cache for the created instances.
*/
static __instances = new Map();

/**
* Get a service instance as a singleton based on the given key.
*/
static getInstance<T extends ServiceInterface<any>>(key: any = this, ...args: any[]) {
if (!this.__instances.has(key)) {
// @ts-ignore
const instance = new this(...args);
this.__instances.set(key, {
add: (key, callback) => instance.add(key, callback),
remove: (key) => instance.remove(key),
has: (key) => instance.has(key),
props: () => instance.props,
} as T);
}

return this.__instances.get(key);
}

/**
* Is the service active or not?
*/
__isInit = false;

/**
* Props for the service.
*/
props = {};

/**
* Holds all the callbacks that will be triggered.
*/
callbacks: Map<string, (props: PropsType) => unknown> = new Map();

/**
Expand All @@ -47,9 +104,9 @@ export class AbstractService<PropsType> {
}

// Initialize the service when we add the first callback
if (this.callbacks.size === 0 && !this.isInit) {
if (this.callbacks.size === 0 && !this.__isInit) {
this.init();
this.isInit = true;
this.__isInit = true;
}

this.callbacks.set(key, callback);
Expand All @@ -62,9 +119,9 @@ export class AbstractService<PropsType> {
this.callbacks.delete(key);

// Kill the service when we remove the last callback
if (this.callbacks.size === 0 && this.isInit) {
if (this.callbacks.size === 0 && this.__isInit) {
this.kill();
this.isInit = false;
this.__isInit = false;
}
}

Expand All @@ -77,31 +134,36 @@ export class AbstractService<PropsType> {
}
}

init() {
throw new Error('The init method must be implemented.');
/**
* Implements the EventListenerObject interface.
*/
handleEvent(event: Event) {
// Should be implemented.
}

kill() {
throw new Error('The kill method must be implemented');
/**
* Add or remove event listeners based on the static `config` property.
*/
__manageEvents(mode: 'add' | 'remove') {
for (const [target, events] of this.constructor.config) {
const resolvedTarget = isFunction(target) ? target(this) : target;
for (const [type, options] of events) {
resolvedTarget[`${mode}EventListener`](type, this, options);
}
}
}

static __instances = new Map();

/**
* Get a service instance as a singleton based on the given key.
* Triggered when the service is initialized.
*/
static getInstance<T extends ServiceInterface<any>>(key: any = this, ...args: any[]) {
if (!this.__instances.has(key)) {
// @ts-ignore
const instance = new this(...args);
this.__instances.set(key, {
add: (key, callback) => instance.add(key, callback),
remove: (key) => instance.remove(key),
has: (key) => instance.has(key),
props: () => instance.props,
} as T);
}
init() {
this.__manageEvents('add');
}

return this.__instances.get(key);
/**
* Triggered when the service is killed.
*/
kill() {
this.__manageEvents('remove');
}
}
54 changes: 23 additions & 31 deletions packages/js-toolkit/services/DragService.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { ServiceInterface } from './AbstractService.js';
import type { ServiceConfig, ServiceInterface } from './AbstractService.js';
import { AbstractService } from './AbstractService.js';
import { useRaf } from './RafService.js';
import { isDefined, inertiaFinalValue } from '../utils/index.js';
import { PASSIVE_EVENT_OPTIONS, CAPTURE_EVENT_OPTIONS } from './utils.js';

type DragLifecycle = 'start' | 'drag' | 'drop' | 'inertia' | 'stop';

Expand Down Expand Up @@ -58,9 +59,6 @@ export interface DragServiceOptions {
let count = 0;

export class DragService extends AbstractService<DragServiceProps> {
static targetEvents = ['pointerdown'];
static windowEvents = ['pointerup', 'touchend'];
static passiveEventOptions = { passive: true };
static MODES: Record<Uppercase<DragLifecycle>, DragLifecycle> = {
START: 'start',
DRAG: 'drag',
Expand Down Expand Up @@ -124,31 +122,23 @@ export class DragService extends AbstractService<DragServiceProps> {
this.props.target = target;
}

init() {
const { target } = this.props;

for (const event of DragService.targetEvents) {
target.addEventListener(event, this, DragService.passiveEventOptions);
}
for (const event of DragService.windowEvents) {
window.addEventListener(event, this, DragService.passiveEventOptions);
}
target.addEventListener('dragstart', this, { capture: true });
target.addEventListener('click', this, { capture: true });
}

kill() {
const { target } = this.props;

for (const event of DragService.targetEvents) {
target.removeEventListener(event, this);
}
for (const event of DragService.windowEvents) {
window.removeEventListener(event, this);
}
target.removeEventListener('dragstart', this);
target.removeEventListener('click', this);
}
static config: ServiceConfig = [
[
(instance) => (instance as DragService).props.target,
[
['dragstart', CAPTURE_EVENT_OPTIONS],
['click', CAPTURE_EVENT_OPTIONS],
['pointerdown', PASSIVE_EVENT_OPTIONS],
],
],
[
window,
[
['pointerup', PASSIVE_EVENT_OPTIONS],
['touchend', PASSIVE_EVENT_OPTIONS],
],
],
];

/**
* Get the client value for the given axis.
Expand Down Expand Up @@ -194,17 +184,19 @@ export class DragService extends AbstractService<DragServiceProps> {

this.trigger(props);

document.addEventListener('touchmove', this, DragService.passiveEventOptions);
document.addEventListener('mousemove', this, DragService.passiveEventOptions);
document.addEventListener('touchmove', this, PASSIVE_EVENT_OPTIONS);
document.addEventListener('mousemove', this, PASSIVE_EVENT_OPTIONS);
}

/**
* Stop the drag, or drop.
*/
drop() {
const { props, dampFactor, id } = this;

document.removeEventListener('touchmove', this);
document.removeEventListener('mousemove', this);

props.isGrabbing = false;
props.mode = DragService.MODES.DROP;

Expand Down
18 changes: 5 additions & 13 deletions packages/js-toolkit/services/KeyService.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ServiceInterface } from './AbstractService.js';
import type { ServiceConfig, ServiceInterface } from './AbstractService.js';
import { AbstractService } from './AbstractService.js';
import keyCodes from '../utils/keyCodes.js';

Expand Down Expand Up @@ -28,16 +28,18 @@ export interface KeyServiceProps {
export type KeyServiceInterface = ServiceInterface<KeyServiceProps>;

export class KeyService extends AbstractService<KeyServiceProps> {
static config: ServiceConfig = [[document, [['keydown'], ['keyup']]]];

previousEvent: Event | null = null;

props = {
props: KeyServiceProps = {
event: null,
triggered: 0,
isUp: false,
isDown: false,
direction: 'none',
...getInitialKeyCodes(),
} as KeyServiceProps;
};

updateProps(event: KeyboardEvent): KeyServiceProps {
const { props } = this;
Expand Down Expand Up @@ -70,16 +72,6 @@ export class KeyService extends AbstractService<KeyServiceProps> {
handleEvent(event: KeyboardEvent) {
this.trigger(this.updateProps(event));
}

init() {
document.addEventListener('keydown', this);
document.addEventListener('keyup', this);
}

kill() {
document.removeEventListener('keydown', this);
document.removeEventListener('keyup', this);
}
}

/**
Expand Down
16 changes: 5 additions & 11 deletions packages/js-toolkit/services/LoadService.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ServiceInterface } from './AbstractService.js';
import type { ServiceInterface, ServiceConfig } from './AbstractService.js';
import { AbstractService } from './AbstractService.js';

export interface LoadServiceProps {
Expand All @@ -8,22 +8,16 @@ export interface LoadServiceProps {
export type LoadServiceInterface = ServiceInterface<LoadServiceProps>;

export class LoadService extends AbstractService<LoadServiceProps> {
props = {
static config: ServiceConfig = [[window, [['load']]]];

props: LoadServiceProps = {
time: performance.now(),
} as LoadServiceProps;
};

handleEvent() {
this.props.time = window.performance.now();
this.trigger(this.props);
}

init() {
window.addEventListener('load', this);
}

kill() {
window.removeEventListener('load', this);
}
}

/**
Expand Down
40 changes: 18 additions & 22 deletions packages/js-toolkit/services/PointerService.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ServiceInterface } from './AbstractService.js';
import type { ServiceConfig, ServiceInterface } from './AbstractService.js';
import { AbstractService } from './AbstractService.js';
import { ONCE_CAPTURE_EVENT_OPTIONS, PASSIVE_CAPTURE_EVENT_OPTIONS } from './utils.js';

export interface PointerServiceProps {
event: MouseEvent | TouchEvent;
Expand All @@ -16,11 +17,24 @@ export interface PointerServiceProps {
export type PointerServiceInterface = ServiceInterface<PointerServiceProps>;

export class PointerService extends AbstractService<PointerServiceProps> {
static events = ['mousemove', 'touchmove', 'mousedown', 'touchstart', 'mouseup', 'touchend'];
static config: ServiceConfig = [
[
document,
[
['mouseenter', ONCE_CAPTURE_EVENT_OPTIONS],
['mousemove', PASSIVE_CAPTURE_EVENT_OPTIONS],
['touchmove', PASSIVE_CAPTURE_EVENT_OPTIONS],
['mousedown', PASSIVE_CAPTURE_EVENT_OPTIONS],
['touchstart', PASSIVE_CAPTURE_EVENT_OPTIONS],
['mouseup', PASSIVE_CAPTURE_EVENT_OPTIONS],
['touchend', PASSIVE_CAPTURE_EVENT_OPTIONS],
],
],
];

target: HTMLElement | Window | undefined;

props = {
props: PointerServiceProps = {
event: null,
isDown: false,
x: 0,
Expand All @@ -45,7 +59,7 @@ export class PointerService extends AbstractService<PointerServiceProps> {
x: 0,
y: 0,
},
} as PointerServiceProps;
};

constructor(target: HTMLElement | undefined) {
super();
Expand Down Expand Up @@ -150,24 +164,6 @@ export class PointerService extends AbstractService<PointerServiceProps> {
break;
}
}

init() {
document.documentElement.addEventListener('mouseenter', this, {
once: true,
capture: true,
});

const options = { passive: true, capture: true };
for (const event of PointerService.events) {
document.addEventListener(event, this, options);
}
}

kill() {
for (const event of PointerService.events) {
document.removeEventListener(event, this);
}
}
}

/**
Expand Down
Loading
Loading