zrender
Version:
A lightweight graphic library providing 2d draw for Apache ECharts
312 lines (268 loc) • 9.49 kB
text/typescript
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;
}
}