Skip to content

Commit 89cb225

Browse files
committed
refactor: event managers
1 parent eef3526 commit 89cb225

File tree

11 files changed

+176
-401
lines changed

11 files changed

+176
-401
lines changed

src/cdk-experimental/listbox/listbox.ts

-2
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@ import {
1515
inject,
1616
input,
1717
model,
18-
OnDestroy,
19-
signal,
2018
} from '@angular/core';
2119
import {ListboxPattern, OptionPattern} from '@angular/cdk-experimental/ui-patterns';
2220
import {Directionality} from '@angular/cdk/bidi';

src/cdk-experimental/ui-patterns/behaviors/event-manager/event-manager.ts

+27-118
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,19 @@ export interface EventHandlerOptions {
2828
preventDefault: boolean;
2929
}
3030

31-
/**
32-
* A config that specifies how to handle a particular event.
33-
*/
31+
/** A basic event handler. */
32+
export type EventHandler<T extends Event> = (event: T) => void;
33+
34+
/** A function that determines whether an event is to be handled. */
35+
export type EventMatcher<T extends Event> = (event: T) => boolean;
36+
37+
/** A config that specifies how to handle a particular event. */
3438
export interface EventHandlerConfig<T extends Event> extends EventHandlerOptions {
35-
handler: (event: T) => boolean | void;
39+
matcher: EventMatcher<T>;
40+
handler: EventHandler<T>;
3641
}
3742

38-
/**
39-
* Bit flag representation of the possible modifier keys that can be present on an event.
40-
*/
43+
/** Bit flag representation of the possible modifier keys that can be present on an event. */
4144
export enum ModifierKey {
4245
None = 0,
4346
Ctrl = 0b1,
@@ -46,134 +49,40 @@ export enum ModifierKey {
4649
Meta = 0b1000,
4750
}
4851

52+
export type ModifierInputs = ModifierKey | ModifierKey[];
53+
4954
/**
5055
* Abstract base class for all event managers.
5156
*
5257
* Event managers are designed to normalize how event handlers are authored and create a safety net
5358
* for common event handling gotchas like remembering to call preventDefault or stopPropagation.
5459
*/
5560
export abstract class EventManager<T extends Event> {
56-
private _submanagers: EventManager<T>[] = [];
57-
5861
protected configs: EventHandlerConfig<T>[] = [];
59-
protected beforeFns: ((event: T) => void)[] = [];
60-
protected afterFns: ((event: T) => void)[] = [];
62+
abstract options: EventHandlerOptions;
6163

62-
protected defaultHandlerOptions: EventHandlerOptions = {
63-
preventDefault: false,
64-
stopPropagation: false,
65-
};
64+
/** Runs the handlers that match with the given event. */
65+
handle(event: T): void {
66+
for (const config of this.configs) {
67+
if (config.matcher(event)) {
68+
config.handler(event);
6669

67-
constructor(defaultHandlerOptions?: Partial<EventHandlerOptions>) {
68-
this.defaultHandlerOptions = {
69-
...this.defaultHandlerOptions,
70-
...defaultHandlerOptions,
71-
};
72-
}
73-
74-
/**
75-
* Composes together multiple event managers into a single event manager that delegates to the
76-
* individual managers.
77-
*/
78-
static compose<T extends Event>(...managers: EventManager<T>[]) {
79-
const composedManager = new GenericEventManager<T>();
80-
composedManager._submanagers = managers;
81-
return composedManager;
82-
}
70+
if (config.preventDefault) {
71+
event.preventDefault();
72+
}
8373

84-
/**
85-
* Runs any handlers that have been configured to handle this event. If multiple handlers are
86-
* configured for this event, they are run in the order they were configured. Returns
87-
* `true` if the event has been handled, otherwise returns `undefined`.
88-
*
89-
* Note: the use of `undefined` instead of `false` in the unhandled case is necessary to avoid
90-
* accidentally preventing the default behavior on an unhandled event.
91-
*/
92-
handle(event: T): true | undefined {
93-
if (!this.isHandled(event)) {
94-
return undefined;
95-
}
96-
for (const fn of this.beforeFns) {
97-
fn(event);
98-
}
99-
for (const submanager of this._submanagers) {
100-
submanager.handle(event);
101-
}
102-
for (const config of this.getHandlersForKey(event)) {
103-
config.handler(event);
104-
if (config.stopPropagation) {
105-
event.stopPropagation();
74+
if (config.stopPropagation) {
75+
event.stopPropagation();
76+
}
10677
}
107-
if (config.preventDefault) {
108-
event.preventDefault();
109-
}
110-
}
111-
for (const fn of this.afterFns) {
112-
fn(event);
11378
}
114-
return true;
11579
}
11680

117-
/**
118-
* Configures the event manager to run a function immediately before it as about to handle
119-
* any event.
120-
*/
121-
beforeHandling(fn: (event: T) => void): this {
122-
this.beforeFns.push(fn);
123-
return this;
124-
}
125-
126-
/**
127-
* Configures the event manager to run a function immediately after it handles any event.
128-
*/
129-
afterHandling(fn: (event: T) => void): this {
130-
this.afterFns.push(fn);
131-
return this;
132-
}
133-
134-
/**
135-
* Configures the event manager to handle specific events. (See subclasses for more).
136-
*/
81+
/** Configures the event manager to handle specific events. (See subclasses for more). */
13782
abstract on(...args: [...unknown[]]): this;
138-
139-
/**
140-
* Gets all of the handler configs that are applicable to the given event.
141-
*/
142-
protected abstract getHandlersForKey(event: T): EventHandlerConfig<T>[];
143-
144-
/**
145-
* Checks whether this event manager is confugred to handle the given event.
146-
*/
147-
protected isHandled(event: T): boolean {
148-
return (
149-
this.getHandlersForKey(event).length > 0 || this._submanagers.some(sm => sm.isHandled(event))
150-
);
151-
}
152-
}
153-
154-
/**
155-
* A generic event manager that can work with any type of event.
156-
*/
157-
export class GenericEventManager<T extends Event> extends EventManager<T> {
158-
/**
159-
* Configures this event manager to handle all events with the given handler.
160-
*/
161-
on(handler: (event: T) => boolean | void): this {
162-
this.configs.push({
163-
...this.defaultHandlerOptions,
164-
handler,
165-
});
166-
return this;
167-
}
168-
169-
getHandlersForKey(_event: T): EventHandlerConfig<T>[] {
170-
return this.configs;
171-
}
17283
}
17384

174-
/**
175-
* Gets bit flag representation of the modifier keys present on the given event.
176-
*/
85+
/** Gets bit flag representation of the modifier keys present on the given event. */
17786
export function getModifiers(event: EventWithModifiers): number {
17887
return (
17988
(+event.ctrlKey && ModifierKey.Ctrl) |
@@ -187,7 +96,7 @@ export function getModifiers(event: EventWithModifiers): number {
18796
* Checks if the given event has modifiers that are an exact match for any of the given modifier
18897
* flag combinations.
18998
*/
190-
export function hasModifiers(event: EventWithModifiers, modifiers: number | number[]): boolean {
99+
export function hasModifiers(event: EventWithModifiers, modifiers: ModifierInputs): boolean {
191100
const eventModifiers = getModifiers(event);
192101
const modifiersList = Array.isArray(modifiers) ? modifiers : [modifiers];
193102
return modifiersList.some(modifiers => eventModifiers === modifiers);

src/cdk-experimental/ui-patterns/behaviors/event-manager/keyboard-event-manager.ts

+32-56
Original file line numberDiff line numberDiff line change
@@ -8,92 +8,68 @@
88

99
import {Signal} from '@angular/core';
1010
import {
11-
EventHandlerConfig,
11+
EventHandler,
1212
EventHandlerOptions,
1313
EventManager,
1414
hasModifiers,
15+
ModifierInputs,
1516
ModifierKey,
1617
} from './event-manager';
1718

1819
/**
19-
* A config that specifies how to handle a particular keyboard event.
20+
* Used to represent a keycode.
21+
*
22+
* This is used to match whether an events keycode should be handled. The ability to match using a
23+
* string, Signal, or Regexp gives us more flexibility when authoring event handlers.
2024
*/
21-
export interface KeyboardEventHandlerConfig extends EventHandlerConfig<KeyboardEvent> {
22-
key: string | Signal<string> | RegExp;
23-
modifiers: number | number[];
24-
}
25+
type KeyCode = string | Signal<string> | RegExp;
2526

2627
/**
2728
* An event manager that is specialized for handling keyboard events. By default this manager stops
2829
* propagation and prevents default on all events it handles.
2930
*/
30-
export class KeyboardEventManager extends EventManager<KeyboardEvent> {
31-
override configs: KeyboardEventHandlerConfig[] = [];
32-
33-
protected override defaultHandlerOptions: EventHandlerOptions = {
31+
export class KeyboardEventManager<T extends KeyboardEvent> extends EventManager<T> {
32+
options: EventHandlerOptions = {
3433
preventDefault: true,
3534
stopPropagation: true,
3635
};
3736

38-
/**
39-
* Configures this event manager to handle events with a specific modifer and key combination.
40-
*
41-
* @param modifiers The modifier combinations that this handler should run for.
42-
* @param key The key that this handler should run for (or a predicate function that takes the
43-
* event's key and returns whether to run this handler).
44-
* @param handler The handler function
45-
* @param options Options for whether to stop propagation or prevent default.
46-
*/
47-
on(
48-
modifiers: number | number[],
49-
key: string | Signal<string> | RegExp,
50-
handler: ((event: KeyboardEvent) => void) | ((event: KeyboardEvent) => boolean),
51-
options?: Partial<EventHandlerOptions>,
52-
): this;
37+
/** Configures this event manager to handle events with a specific key and no modifiers. */
38+
on(key: KeyCode, handler: EventHandler<T>): this;
5339

54-
/**
55-
* Configures this event manager to handle events with a specific key and no modifiers.
56-
*
57-
* @param key The key that this handler should run for (or a predicate function that takes the
58-
* event's key and returns whether to run this handler).
59-
* @param handler The handler function
60-
* @param options Options for whether to stop propagation or prevent default.
61-
*/
62-
on(
63-
key: string | Signal<string> | RegExp,
64-
handler: ((event: KeyboardEvent) => void) | ((event: KeyboardEvent) => boolean),
65-
options?: Partial<EventHandlerOptions>,
66-
): this;
40+
/** Configures this event manager to handle events with a specific modifer and key combination. */
41+
on(modifiers: ModifierInputs, key: KeyCode, handler: EventHandler<T>): this;
6742

6843
on(...args: any[]) {
69-
const key = args.length === 3 ? args[1] : args[0];
70-
const handler = args.length === 3 ? args[2] : args[1];
71-
const modifiers = args.length === 3 ? args[0] : ModifierKey.None;
72-
73-
// TODO: Add strict type checks again when finalizing this API.
44+
const {modifiers, key, handler} = this._normalizeInputs(...args);
7445

7546
this.configs.push({
76-
key,
77-
handler,
78-
modifiers,
79-
...this.defaultHandlerOptions,
47+
handler: handler,
48+
matcher: event => this._isMatch(event, key, modifiers),
49+
...this.options,
8050
});
8151

8252
return this;
8353
}
8454

85-
getHandlersForKey(event: KeyboardEvent) {
86-
return this.configs.filter(config => this._isKeyMatch(config, event));
87-
}
55+
private _normalizeInputs(...args: any[]) {
56+
const key = args.length === 3 ? args[1] : args[0];
57+
const handler = args.length === 3 ? args[2] : args[1];
58+
const modifiers = args.length === 3 ? args[0] : ModifierKey.None;
8859

89-
// TODO: Make modifiers accept a signal as well.
60+
return {
61+
key: key as KeyCode,
62+
handler: handler as EventHandler<T>,
63+
modifiers: modifiers as ModifierInputs,
64+
};
65+
}
9066

91-
private _isKeyMatch(config: KeyboardEventHandlerConfig, event: KeyboardEvent) {
92-
if (config.key instanceof RegExp) {
93-
return config.key.test(event.key);
67+
private _isMatch(event: T, key: KeyCode, modifiers: ModifierInputs) {
68+
if (key instanceof RegExp) {
69+
return key.test(event.key);
9470
}
9571

96-
const key = typeof config.key === 'string' ? config.key : config.key();
97-
return key.toLowerCase() === event.key.toLowerCase() && hasModifiers(event, config.modifiers);
72+
const keyStr = typeof key === 'string' ? key : key();
73+
return keyStr.toLowerCase() === event.key.toLowerCase() && hasModifiers(event, modifiers);
9874
}
9975
}

0 commit comments

Comments
 (0)