import { Dictionary, WithThisType } from './types';
|
|
// Return true to cancel bubble
|
export type EventCallbackSingleParam<EvtParam = any> = EvtParam extends any
|
? (params: EvtParam) => boolean | void
|
: never
|
|
export type EventCallback<EvtParams = any[]> = EvtParams extends any[]
|
? (...args: EvtParams) => boolean | void
|
: never
|
export type EventQuery = string | Object
|
|
type CbThis<Ctx, Impl> = unknown extends Ctx ? Impl : Ctx;
|
|
type EventHandler<Ctx, Impl, EvtParams> = {
|
h: EventCallback<EvtParams>
|
ctx: CbThis<Ctx, Impl>
|
query: EventQuery
|
|
callAtLast: boolean
|
}
|
|
type DefaultEventDefinition = Dictionary<EventCallback<any[]>>;
|
|
export interface EventProcessor<EvtDef = DefaultEventDefinition> {
|
normalizeQuery?: (query: EventQuery) => EventQuery
|
filter?: (eventType: keyof EvtDef, query: EventQuery) => boolean
|
afterTrigger?: (eventType: keyof EvtDef) => void
|
}
|
|
/**
|
* Event dispatcher.
|
*
|
* Event can be defined in EvtDef to enable type check. For example:
|
* ```ts
|
* interface FooEvents {
|
* // key: event name, value: the first event param in `trigger` and `callback`.
|
* myevent: {
|
* aa: string;
|
* bb: number;
|
* };
|
* }
|
* class Foo extends Eventful<FooEvents> {
|
* fn() {
|
* // Type check of event name and the first event param is enabled here.
|
* this.trigger('myevent', {aa: 'xx', bb: 3});
|
* }
|
* }
|
* let foo = new Foo();
|
* // Type check of event name and the first event param is enabled here.
|
* foo.on('myevent', (eventParam) => { ... });
|
* ```
|
*
|
* @param eventProcessor The object eventProcessor is the scope when
|
* `eventProcessor.xxx` called.
|
* @param eventProcessor.normalizeQuery
|
* param: {string|Object} Raw query.
|
* return: {string|Object} Normalized query.
|
* @param eventProcessor.filter Event will be dispatched only
|
* if it returns `true`.
|
* param: {string} eventType
|
* param: {string|Object} query
|
* return: {boolean}
|
* @param eventProcessor.afterTrigger Called after all handlers called.
|
* param: {string} eventType
|
*/
|
export default class Eventful<EvtDef extends DefaultEventDefinition = DefaultEventDefinition> {
|
|
private _$handlers: Dictionary<EventHandler<any, any, any[]>[]>
|
|
protected _$eventProcessor: EventProcessor<EvtDef>
|
|
constructor(eventProcessors?: EventProcessor<EvtDef>) {
|
if (eventProcessors) {
|
this._$eventProcessor = eventProcessors;
|
}
|
}
|
|
on<Ctx, EvtNm extends keyof EvtDef>(
|
event: EvtNm,
|
handler: WithThisType<EvtDef[EvtNm], CbThis<Ctx, this>>,
|
context?: Ctx
|
): this
|
on<Ctx, EvtNm extends keyof EvtDef>(
|
event: EvtNm,
|
query: EventQuery,
|
handler: WithThisType<EvtDef[EvtNm], CbThis<Ctx, this>>,
|
context?: Ctx
|
): this
|
/**
|
* Bind a handler.
|
*
|
* @param event The event name.
|
* @param Condition used on event filter.
|
* @param handler The event handler.
|
* @param context
|
*/
|
on<Ctx, EvtNm extends keyof EvtDef>(
|
event: EvtNm,
|
query: EventQuery | WithThisType<EventCallback<EvtDef[EvtNm]>, CbThis<Ctx, this>>,
|
handler?: WithThisType<EventCallback<EvtDef[EvtNm]>, CbThis<Ctx, this>> | Ctx,
|
context?: Ctx
|
): this {
|
if (!this._$handlers) {
|
this._$handlers = {};
|
}
|
|
const _h = this._$handlers;
|
|
if (typeof query === 'function') {
|
context = handler as Ctx;
|
handler = query as (...args: any) => any;
|
query = null;
|
}
|
|
if (!handler || !event) {
|
return this;
|
}
|
|
const eventProcessor = this._$eventProcessor;
|
if (query != null && eventProcessor && eventProcessor.normalizeQuery) {
|
query = eventProcessor.normalizeQuery(query);
|
}
|
|
if (!_h[event as string]) {
|
_h[event as string] = [];
|
}
|
|
for (let i = 0; i < _h[event as string].length; i++) {
|
if (_h[event as string][i].h === handler) {
|
return this;
|
}
|
}
|
|
const wrap: EventHandler<Ctx, this, unknown[]> = {
|
h: handler as EventCallback<unknown[]>,
|
query: query,
|
ctx: (context || this) as CbThis<Ctx, this>,
|
// FIXME
|
// Do not publish this feature util it is proved that it makes sense.
|
callAtLast: (handler as any).zrEventfulCallAtLast
|
};
|
|
const lastIndex = _h[event as string].length - 1;
|
const lastWrap = _h[event as string][lastIndex];
|
(lastWrap && lastWrap.callAtLast)
|
? _h[event as string].splice(lastIndex, 0, wrap)
|
: _h[event as string].push(wrap);
|
|
return this;
|
}
|
|
/**
|
* Whether any handler has bound.
|
*/
|
isSilent(eventName: keyof EvtDef): boolean {
|
const _h = this._$handlers;
|
return !_h || !_h[eventName as string] || !_h[eventName as string].length;
|
}
|
|
/**
|
* Unbind a event.
|
*
|
* @param eventType The event name.
|
* If no `event` input, "off" all listeners.
|
* @param handler The event handler.
|
* If no `handler` input, "off" all listeners of the `event`.
|
*/
|
off(eventType?: keyof EvtDef, handler?: Function): this {
|
const _h = this._$handlers;
|
|
if (!_h) {
|
return this;
|
}
|
|
if (!eventType) {
|
this._$handlers = {};
|
return this;
|
}
|
|
if (handler) {
|
if (_h[eventType as string]) {
|
const newList = [];
|
for (let i = 0, l = _h[eventType as string].length; i < l; i++) {
|
if (_h[eventType as string][i].h !== handler) {
|
newList.push(_h[eventType as string][i]);
|
}
|
}
|
_h[eventType as string] = newList;
|
}
|
|
if (_h[eventType as string] && _h[eventType as string].length === 0) {
|
delete _h[eventType as string];
|
}
|
}
|
else {
|
delete _h[eventType as string];
|
}
|
|
return this;
|
}
|
|
/**
|
* Dispatch a event.
|
*
|
* @param {string} eventType The event name.
|
*/
|
trigger<EvtNm extends keyof EvtDef>(
|
eventType: EvtNm,
|
...args: Parameters<EvtDef[EvtNm]>
|
): this {
|
if (!this._$handlers) {
|
return this;
|
}
|
|
const _h = this._$handlers[eventType as string];
|
const eventProcessor = this._$eventProcessor;
|
|
if (_h) {
|
const argLen = args.length;
|
|
const len = _h.length;
|
for (let i = 0; i < len; i++) {
|
const hItem = _h[i];
|
if (eventProcessor
|
&& eventProcessor.filter
|
&& hItem.query != null
|
&& !eventProcessor.filter(eventType, hItem.query)
|
) {
|
continue;
|
}
|
|
// Optimize advise from backbone
|
switch (argLen) {
|
case 0:
|
hItem.h.call(hItem.ctx);
|
break;
|
case 1:
|
hItem.h.call(hItem.ctx, args[0]);
|
break;
|
case 2:
|
hItem.h.call(hItem.ctx, args[0], args[1]);
|
break;
|
default:
|
// have more than 2 given arguments
|
hItem.h.apply(hItem.ctx, args);
|
break;
|
}
|
}
|
}
|
|
eventProcessor && eventProcessor.afterTrigger
|
&& eventProcessor.afterTrigger(eventType);
|
|
return this;
|
}
|
|
/**
|
* Dispatch a event with context, which is specified at the last parameter.
|
*
|
* @param {string} type The event name.
|
*/
|
triggerWithContext(type: keyof EvtDef, ...args: any[]): this {
|
if (!this._$handlers) {
|
return this;
|
}
|
|
const _h = this._$handlers[type as string];
|
const eventProcessor = this._$eventProcessor;
|
|
if (_h) {
|
const argLen = args.length;
|
const ctx = args[argLen - 1];
|
|
const len = _h.length;
|
for (let i = 0; i < len; i++) {
|
const hItem = _h[i];
|
if (eventProcessor
|
&& eventProcessor.filter
|
&& hItem.query != null
|
&& !eventProcessor.filter(type, hItem.query)
|
) {
|
continue;
|
}
|
|
// Optimize advise from backbone
|
switch (argLen) {
|
case 0:
|
hItem.h.call(ctx);
|
break;
|
case 1:
|
hItem.h.call(ctx, args[0]);
|
break;
|
case 2:
|
hItem.h.call(ctx, args[0], args[1]);
|
break;
|
default:
|
// have more than 2 given arguments
|
hItem.h.apply(ctx, args.slice(1, argLen - 1));
|
break;
|
}
|
}
|
}
|
|
eventProcessor && eventProcessor.afterTrigger
|
&& eventProcessor.afterTrigger(type);
|
|
return this;
|
}
|
|
}
|