From 2cfe34c02b0a8e17ac6c1df6e5bf318091d53aa2 Mon Sep 17 00:00:00 2001 From: wxm <157215725@qq.com> Date: Sat, 23 Nov 2024 15:15:18 +0800 Subject: [PATCH] feat: EventEmitter --- src/EventEmitter.ts | 123 ++++++++++++++++++++++++++++++++++++++++++++ src/Text.ts | 23 ++++++--- 2 files changed, 139 insertions(+), 7 deletions(-) create mode 100644 src/EventEmitter.ts diff --git a/src/EventEmitter.ts b/src/EventEmitter.ts new file mode 100644 index 0000000..8f81b33 --- /dev/null +++ b/src/EventEmitter.ts @@ -0,0 +1,123 @@ +export type EventListenerValue = (ev: T) => void +export type EventListenerOptions = boolean | AddEventListenerOptions +export interface EventListener { + value: EventListenerValue + options?: EventListenerOptions +} + +export class EventEmitter = Record> { + eventListeners = new Map() + + addEventListener(event: K, listener: EventListenerValue, options?: EventListenerOptions): this { + const object = { value: listener, options } + const listeners = this.eventListeners.get(event) + if (!listeners) { + this.eventListeners.set(event, object) + } + else if (Array.isArray(listeners)) { + listeners.push(object) + } + else { + this.eventListeners.set(event, [listeners, object]) + } + return this + } + + removeEventListener(event: K, listener: EventListenerValue, options?: EventListenerOptions): this { + if (!listener) { + this.eventListeners.delete(event) + return this + } + + const listeners = this.eventListeners.get(event) + + if (!listeners) { + return this + } + + if (Array.isArray(listeners)) { + const events = [] + for (let i = 0, length = listeners.length; i < length; i++) { + const object = listeners[i] + if ( + object.value !== listener + || ( + typeof options === 'object' && options?.once + && (typeof object.options === 'boolean' || !object.options?.once) + ) + ) { + events.push(object) + } + } + if (events.length) { + this.eventListeners.set(event, events.length === 1 ? events[0] : events) + } + else { + this.eventListeners.delete(event) + } + } + else { + if ( + listeners.value === listener + && ( + (typeof options === 'boolean' || !options?.once) + || (typeof listeners.options === 'boolean' || listeners.options?.once) + ) + ) { + this.eventListeners.delete(event) + } + } + return this + } + + removeAllListeners(): this { + this.eventListeners.clear() + return this + } + + hasEventListener(event: string): boolean { + return this.eventListeners.has(event) + } + + dispatchEvent(event: K, args: T[K]): boolean { + const listeners = this.eventListeners.get(event) + + if (listeners) { + if (Array.isArray(listeners)) { + for (let len = listeners.length, i = 0; i < len; i++) { + const object = listeners[i] + if (typeof object.options === 'object' && object.options?.once) { + this.off(event, object.value, object.options) + } + object.value.apply(this, args) + } + } + else { + if (typeof listeners.options === 'object' && listeners.options?.once) { + this.off(event, listeners.value, listeners.options) + } + listeners.value.apply(this, args) + } + return true + } + else { + return false + } + } + + on(event: K, listener: EventListenerValue, options?: EventListenerOptions): this { + return this.addEventListener(event, listener, options) + } + + once(event: K, listener: EventListenerValue): this { + return this.addEventListener(event, listener, { once: true }) + } + + off(event: K, listener: EventListenerValue, options?: EventListenerOptions): this { + return this.removeEventListener(event, listener, options) + } + + emit(event: K, args: T[K]): void { + this.dispatchEvent(event, args) + } +} diff --git a/src/Text.ts b/src/Text.ts index f736925..6c183b2 100644 --- a/src/Text.ts +++ b/src/Text.ts @@ -4,6 +4,7 @@ import type { TextContent, TextOptions, TextPlugin, TextStyle } from './types' import { BoundingBox, getPathsBoundingBox, Vector2 } from 'modern-path2d' import { drawPath, setupView, uploadColors } from './canvas' import { Paragraph } from './content' +import { EventEmitter } from './EventEmitter' import { Measurer } from './Measurer' import { highlight, listStyle, render, textDecoration } from './plugins' @@ -69,7 +70,13 @@ export const defaultTextStyles: TextStyle = { skewY: 0, } -export class Text { +export interface TextEventMap { + update: { text: Text } + measure: { text: Text, result: MeasureResult } + render: { text: Text } +} + +export class Text extends EventEmitter { debug: boolean content: TextContent style: Partial @@ -100,6 +107,8 @@ export class Text { } constructor(options: TextOptions = {}) { + super() + this.debug = options.debug ?? false this.content = options.content ?? '' this.style = options.style ?? {} @@ -204,8 +213,7 @@ export class Text { c.update(this.fonts) }) this.rawGlyphBox = this.getGlyphBox() - const plugins = [...this.plugins.values()] - plugins + Array.from(this.plugins.values()) .sort((a, b) => (a.updateOrder ?? 0) - (b.updateOrder ?? 0)) .forEach((plugin) => { plugin.update?.(this) @@ -218,6 +226,7 @@ export class Text { ;(result as any)[key] = (this as any)[key] ;(this as any)[key] = (old as any)[key] } + this.emit('measure', { text: this, result }) return result } @@ -242,10 +251,9 @@ export class Text { } updatePathBox(): this { - const plugins = [...this.plugins.values()] this.pathBox = BoundingBox.from( this.glyphBox, - ...plugins + ...Array.from(this.plugins.values()) .map((plugin) => { return plugin.getBoundingBox ? plugin.getBoundingBox(this) @@ -281,6 +289,7 @@ export class Text { for (const key in result) { (this as any)[key] = (result as any)[key] } + this.emit('update', { text: this }) return this } @@ -295,8 +304,7 @@ export class Text { } setupView(ctx, pixelRatio, this.boundingBox) uploadColors(ctx, this) - const plugins = [...this.plugins.values()] - plugins + Array.from(this.plugins.values()) .sort((a, b) => (a.renderOrder ?? 0) - (b.renderOrder ?? 0)) .forEach((plugin) => { if (plugin.render) { @@ -313,6 +321,7 @@ export class Text { }) } }) + this.emit('render', { text: this }) return this } }