0
# Event Handling
1
2
ZRender provides a comprehensive event system for handling mouse, touch, and custom events. The event system supports hit testing, event delegation, interaction handling, and custom event types with full event lifecycle management.
3
4
## Event System Architecture
5
6
ZRender's event system is built on several key components:
7
- **Event capture and bubbling** through the element hierarchy
8
- **Hit testing** for determining which elements receive events
9
- **Event delegation** for efficient event management
10
- **Custom event support** for application-specific interactions
11
12
## Core Event Interfaces
13
14
### Element Event Methods
15
16
All graphics elements inherit event handling capabilities:
17
18
```typescript { .api }
19
interface Element {
20
// Event binding
21
on<Ctx>(eventName: ElementEventName, eventHandler: ElementEventCallback<Ctx>, context?: Ctx): this;
22
on<Ctx>(eventName: string, eventHandler: Function, context?: Ctx): this;
23
24
// Event unbinding
25
off(eventName?: string, eventHandler?: Function): void;
26
27
// Event triggering
28
trigger(eventName: string, event?: any): void;
29
30
// Event state
31
silent: boolean; // Disable all events on this element
32
ignore: boolean; // Ignore in hit testing
33
}
34
```
35
36
### ZRender Instance Event Methods
37
38
The main ZRender instance provides global event handling:
39
40
```typescript { .api }
41
interface ZRender {
42
// Global event binding
43
on<Ctx>(eventName: ElementEventName, eventHandler: ElementEventCallback<Ctx>, context?: Ctx): this;
44
on<Ctx>(eventName: string, eventHandler: Function, context?: Ctx): this;
45
46
// Global event unbinding
47
off(eventName?: string, eventHandler?: Function): void;
48
49
// Manual event triggering
50
trigger(eventName: string, event?: unknown): void;
51
52
// Hit testing
53
findHover(x: number, y: number): { target: Displayable; topTarget: Displayable } | undefined;
54
55
// Cursor management
56
setCursorStyle(cursorStyle: string): void;
57
}
58
```
59
60
## Event Types
61
62
### Mouse Events
63
64
```typescript { .api }
65
type ElementEventName =
66
| 'click'
67
| 'dblclick'
68
| 'mousedown'
69
| 'mouseup'
70
| 'mousemove'
71
| 'mouseover'
72
| 'mouseout'
73
| 'mouseenter'
74
| 'mouseleave'
75
| 'contextmenu';
76
```
77
78
### Touch Events
79
80
```typescript { .api }
81
type TouchEventName =
82
| 'touchstart'
83
| 'touchmove'
84
| 'touchend'
85
| 'touchcancel';
86
```
87
88
### Drag Events
89
90
```typescript { .api }
91
type DragEventName =
92
| 'drag'
93
| 'dragstart'
94
| 'dragend'
95
| 'dragenter'
96
| 'dragleave'
97
| 'dragover'
98
| 'drop';
99
```
100
101
### Global Events
102
103
```typescript { .api }
104
type GlobalEventName =
105
| 'rendered' // Fired after rendering frame
106
| 'finished' // Fired when animation finishes
107
| 'frame'; // Fired on each animation frame
108
```
109
110
## Event Objects
111
112
### Base Event Interface
113
114
```typescript { .api }
115
interface ElementEvent {
116
type: string;
117
event: MouseEvent | TouchEvent | PointerEvent;
118
target: Element;
119
topTarget: Element;
120
cancelBubble: boolean;
121
offsetX: number;
122
offsetY: number;
123
gestureEvent?: GestureEvent;
124
pinchX?: number;
125
pinchY?: number;
126
pinchScale?: number;
127
wheelDelta?: number;
128
zrByTouch?: boolean;
129
which?: number;
130
stop(): void;
131
}
132
133
interface ElementEventCallback<Ctx = any, Impl = any> {
134
(this: CbThis$1<Ctx, Impl>, e: ElementEvent): boolean | void;
135
}
136
```
137
138
### Rendered Event
139
140
```typescript { .api }
141
interface RenderedEvent {
142
elapsedTime: number; // Time taken to render frame in milliseconds
143
}
144
```
145
146
## Usage Examples
147
148
### Basic Event Handling
149
150
```typescript
151
import { Circle, Rect } from "zrender";
152
153
const circle = new Circle({
154
shape: { cx: 150, cy: 150, r: 50 },
155
style: { fill: '#74b9ff', cursor: 'pointer' }
156
});
157
158
// Basic click handler
159
circle.on('click', (e) => {
160
console.log('Circle clicked!', e);
161
console.log('Click position:', e.offsetX, e.offsetY);
162
});
163
164
// Mouse enter/leave for hover effects
165
circle.on('mouseenter', (e) => {
166
console.log('Mouse entered circle');
167
circle.animate('style')
168
.when(200, { fill: '#a29bfe' })
169
.start();
170
});
171
172
circle.on('mouseleave', (e) => {
173
console.log('Mouse left circle');
174
circle.animate('style')
175
.when(200, { fill: '#74b9ff' })
176
.start();
177
});
178
179
// Double click handler
180
circle.on('dblclick', (e) => {
181
console.log('Double clicked!');
182
circle.animate('scale')
183
.when(300, [1.5, 1.5])
184
.when(600, [1, 1])
185
.start('easeOutBounce');
186
});
187
188
zr.add(circle);
189
```
190
191
### Advanced Event Handling
192
193
```typescript
194
import { Rect, Group } from "zrender";
195
196
// Event delegation using groups
197
const buttonGroup = new Group();
198
199
const createButton = (text: string, x: number, y: number, color: string) => {
200
const button = new Group({
201
position: [x, y]
202
});
203
204
const bg = new Rect({
205
shape: { x: 0, y: 0, width: 100, height: 40, r: 5 },
206
style: { fill: color }
207
});
208
209
const label = new Text({
210
style: {
211
text: text,
212
fill: '#ffffff',
213
fontSize: 14,
214
textAlign: 'center',
215
textVerticalAlign: 'middle'
216
},
217
position: [50, 20]
218
});
219
220
button.add(bg);
221
button.add(label);
222
223
// Store button data
224
button.buttonData = { text, color };
225
226
return button;
227
};
228
229
// Create buttons
230
const button1 = createButton('Button 1', 50, 50, '#e17055');
231
const button2 = createButton('Button 2', 170, 50, '#00b894');
232
const button3 = createButton('Button 3', 290, 50, '#fdcb6e');
233
234
buttonGroup.add(button1);
235
buttonGroup.add(button2);
236
buttonGroup.add(button3);
237
238
// Handle events on the group level
239
buttonGroup.on('click', (e) => {
240
// Find which button was clicked
241
let clickedButton = e.target;
242
while (clickedButton && !clickedButton.buttonData) {
243
clickedButton = clickedButton.parent;
244
}
245
246
if (clickedButton && clickedButton.buttonData) {
247
console.log('Clicked button:', clickedButton.buttonData.text);
248
249
// Visual feedback
250
const bg = clickedButton.children()[0];
251
bg.animate('style')
252
.when(100, { fill: '#2d3436' })
253
.when(200, { fill: clickedButton.buttonData.color })
254
.start();
255
}
256
});
257
258
zr.add(buttonGroup);
259
```
260
261
### Drag and Drop
262
263
```typescript
264
import { Circle, Rect } from "zrender";
265
266
// Draggable circle
267
const draggableCircle = new Circle({
268
shape: { cx: 200, cy: 200, r: 30 },
269
style: { fill: '#ff7675', cursor: 'move' },
270
draggable: true
271
});
272
273
let isDragging = false;
274
let dragOffset = { x: 0, y: 0 };
275
276
draggableCircle.on('mousedown', (e) => {
277
isDragging = true;
278
const pos = draggableCircle.position || [0, 0];
279
dragOffset.x = e.offsetX - pos[0];
280
dragOffset.y = e.offsetY - pos[1];
281
282
// Visual feedback
283
draggableCircle.animate('style')
284
.when(100, { shadowBlur: 10, shadowColor: '#ff7675' })
285
.start();
286
});
287
288
// Use global mouse events for smooth dragging
289
zr.on('mousemove', (e) => {
290
if (isDragging) {
291
const newX = e.offsetX - dragOffset.x;
292
const newY = e.offsetY - dragOffset.y;
293
draggableCircle.position = [newX, newY];
294
zr.refresh();
295
}
296
});
297
298
zr.on('mouseup', (e) => {
299
if (isDragging) {
300
isDragging = false;
301
302
// Remove visual feedback
303
draggableCircle.animate('style')
304
.when(100, { shadowBlur: 0 })
305
.start();
306
}
307
});
308
309
// Drop zone
310
const dropZone = new Rect({
311
shape: { x: 400, y: 150, width: 100, height: 100 },
312
style: {
313
fill: 'none',
314
stroke: '#ddd',
315
lineWidth: 2,
316
lineDash: [5, 5]
317
}
318
});
319
320
// Drop zone events
321
dropZone.on('dragover', (e) => {
322
dropZone.style.stroke = '#00b894';
323
zr.refresh();
324
});
325
326
dropZone.on('dragleave', (e) => {
327
dropZone.style.stroke = '#ddd';
328
zr.refresh();
329
});
330
331
dropZone.on('drop', (e) => {
332
console.log('Dropped on zone!');
333
dropZone.style.fill = 'rgba(0, 184, 148, 0.1)';
334
dropZone.style.stroke = '#00b894';
335
zr.refresh();
336
});
337
338
zr.add(draggableCircle);
339
zr.add(dropZone);
340
```
341
342
### Touch Events
343
344
```typescript
345
import { Circle } from "zrender";
346
347
const touchCircle = new Circle({
348
shape: { cx: 150, cy: 350, r: 40 },
349
style: { fill: '#fd79a8' }
350
});
351
352
// Touch event handling
353
touchCircle.on('touchstart', (e) => {
354
console.log('Touch started');
355
touchCircle.animate('scale')
356
.when(100, [0.9, 0.9])
357
.start();
358
});
359
360
touchCircle.on('touchend', (e) => {
361
console.log('Touch ended');
362
touchCircle.animate('scale')
363
.when(100, [1, 1])
364
.start();
365
});
366
367
touchCircle.on('touchmove', (e) => {
368
// Handle touch move
369
const touch = e.event.touches[0];
370
if (touch) {
371
const rect = zr.dom.getBoundingClientRect();
372
const x = touch.clientX - rect.left;
373
const y = touch.clientY - rect.top;
374
touchCircle.position = [x, y];
375
zr.refresh();
376
}
377
});
378
379
zr.add(touchCircle);
380
```
381
382
### Custom Events
383
384
```typescript
385
import { Group, Rect, Text } from "zrender";
386
387
// Custom component with custom events
388
class CustomButton extends Group {
389
constructor(options: { text: string, x: number, y: number }) {
390
super({ position: [options.x, options.y] });
391
392
this.background = new Rect({
393
shape: { x: 0, y: 0, width: 120, height: 40, r: 5 },
394
style: { fill: '#74b9ff' }
395
});
396
397
this.label = new Text({
398
style: {
399
text: options.text,
400
fill: '#ffffff',
401
fontSize: 14,
402
textAlign: 'center'
403
},
404
position: [60, 20]
405
});
406
407
this.add(this.background);
408
this.add(this.label);
409
410
this.setupEvents();
411
}
412
413
private setupEvents() {
414
this.on('click', () => {
415
// Trigger custom event
416
this.trigger('buttonClick', {
417
button: this,
418
text: this.label.style.text
419
});
420
});
421
422
this.on('mouseenter', () => {
423
this.trigger('buttonHover', { button: this, hovered: true });
424
});
425
426
this.on('mouseleave', () => {
427
this.trigger('buttonHover', { button: this, hovered: false });
428
});
429
}
430
}
431
432
// Create custom button
433
const customButton = new CustomButton({ text: 'Custom', x: 50, y: 400 });
434
435
// Listen to custom events
436
customButton.on('buttonClick', (e) => {
437
console.log('Custom button clicked:', e.text);
438
alert(`Button "${e.text}" was clicked!`);
439
});
440
441
customButton.on('buttonHover', (e) => {
442
const color = e.hovered ? '#a29bfe' : '#74b9ff';
443
e.button.background.animate('style')
444
.when(150, { fill: color })
445
.start();
446
});
447
448
zr.add(customButton);
449
```
450
451
### Event Bubbling and Propagation
452
453
```typescript
454
import { Group, Circle, Rect } from "zrender";
455
456
// Nested elements for testing event bubbling
457
const container = new Group({ position: [300, 300] });
458
459
const outerRect = new Rect({
460
shape: { x: 0, y: 0, width: 200, height: 150 },
461
style: { fill: 'rgba(116, 185, 255, 0.3)', stroke: '#74b9ff' }
462
});
463
464
const innerCircle = new Circle({
465
shape: { cx: 100, cy: 75, r: 30 },
466
style: { fill: '#e17055' }
467
});
468
469
container.add(outerRect);
470
container.add(innerCircle);
471
472
// Event handlers with bubbling
473
outerRect.on('click', (e) => {
474
console.log('Outer rect clicked');
475
// Event will bubble up to container
476
});
477
478
innerCircle.on('click', (e) => {
479
console.log('Inner circle clicked');
480
481
// Stop event bubbling
482
if (e.event.ctrlKey) {
483
e.cancelBubble = true;
484
console.log('Event bubbling stopped');
485
}
486
});
487
488
container.on('click', (e) => {
489
console.log('Container clicked (bubbled up)');
490
});
491
492
zr.add(container);
493
```
494
495
### Global Event Handling
496
497
```typescript
498
// Global event handlers
499
zr.on('click', (e) => {
500
console.log('Global click at:', e.offsetX, e.offsetY);
501
});
502
503
zr.on('rendered', (e: RenderedEvent) => {
504
console.log('Frame rendered in:', e.elapsedTime, 'ms');
505
});
506
507
// Performance monitoring
508
zr.on('rendered', (e: RenderedEvent) => {
509
if (e.elapsedTime > 16.67) { // > 60fps threshold
510
console.warn('Slow frame detected:', e.elapsedTime, 'ms');
511
}
512
});
513
514
// Global keyboard events (requires DOM focus)
515
document.addEventListener('keydown', (e) => {
516
switch(e.key) {
517
case 'Escape':
518
// Clear selections, cancel operations, etc.
519
console.log('Escape pressed - clearing state');
520
break;
521
case 'Delete':
522
// Delete selected elements
523
console.log('Delete pressed');
524
break;
525
}
526
});
527
```
528
529
### Event Cleanup and Memory Management
530
531
```typescript
532
import { Circle } from "zrender";
533
534
// Proper event cleanup
535
class ManagedElement {
536
private element: Circle;
537
private handlers: { [key: string]: Function } = {};
538
539
constructor() {
540
this.element = new Circle({
541
shape: { cx: 100, cy: 100, r: 30 },
542
style: { fill: '#00cec9' }
543
});
544
545
this.setupEvents();
546
}
547
548
private setupEvents() {
549
// Store handler references for cleanup
550
this.handlers.click = (e: any) => {
551
console.log('Managed element clicked');
552
};
553
554
this.handlers.hover = (e: any) => {
555
this.element.animate('style')
556
.when(200, { fill: '#55efc4' })
557
.start();
558
};
559
560
this.handlers.leave = (e: any) => {
561
this.element.animate('style')
562
.when(200, { fill: '#00cec9' })
563
.start();
564
};
565
566
// Bind events
567
this.element.on('click', this.handlers.click);
568
this.element.on('mouseenter', this.handlers.hover);
569
this.element.on('mouseleave', this.handlers.leave);
570
}
571
572
public dispose() {
573
// Clean up event handlers
574
this.element.off('click', this.handlers.click);
575
this.element.off('mouseenter', this.handlers.hover);
576
this.element.off('mouseleave', this.handlers.leave);
577
578
// Remove from scene
579
if (this.element.parent) {
580
this.element.parent.remove(this.element);
581
}
582
583
// Clear references
584
this.handlers = {};
585
}
586
587
public getElement() {
588
return this.element;
589
}
590
}
591
592
// Usage
593
const managedEl = new ManagedElement();
594
zr.add(managedEl.getElement());
595
596
// Later, clean up properly
597
// managedEl.dispose();
598
```
599
600
### Event Performance Optimization
601
602
```typescript
603
// Efficient event handling for many elements
604
const createOptimizedEventHandling = () => {
605
const container = new Group();
606
const elements: Circle[] = [];
607
608
// Create many elements
609
for (let i = 0; i < 100; i++) {
610
const circle = new Circle({
611
shape: {
612
cx: (i % 10) * 50 + 25,
613
cy: Math.floor(i / 10) * 50 + 25,
614
r: 20
615
},
616
style: { fill: `hsl(${i * 3.6}, 70%, 60%)` }
617
});
618
619
// Store identifier
620
circle.elementId = i;
621
elements.push(circle);
622
container.add(circle);
623
}
624
625
// Use single event handler on container instead of individual handlers
626
container.on('click', (e) => {
627
// Find clicked element
628
let target = e.target;
629
while (target && target.elementId === undefined) {
630
target = target.parent;
631
}
632
633
if (target && target.elementId !== undefined) {
634
console.log('Clicked element:', target.elementId);
635
636
// Apply effect
637
target.animate('scale')
638
.when(200, [1.2, 1.2])
639
.when(400, [1, 1])
640
.start();
641
}
642
});
643
644
return container;
645
};
646
647
const optimizedGroup = createOptimizedEventHandling();
648
zr.add(optimizedGroup);
649
```