UNPKG

zrender

Version:

A lightweight graphic library providing 2d draw for Apache ECharts

312 lines (268 loc) 9.49 kB
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; } }