refactor: editor events sub/unsub refactor (#7483)

This commit is contained in:
David Luzar 2023-12-30 11:12:38 +01:00 committed by GitHub
parent 5f40a4cad4
commit c72e853c85
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 186 additions and 129 deletions

View File

@ -269,6 +269,7 @@ import {
isTestEnv,
easeOut,
updateStable,
addEventListener,
} from "../utils";
import {
createSrcDoc,
@ -559,6 +560,8 @@ class App extends React.Component<AppProps, AppState> {
[scrollX: number, scrollY: number, zoom: AppState["zoom"]]
>();
onRemoveEventListenersEmitter = new Emitter<[]>();
constructor(props: AppProps) {
super(props);
const defaultAppState = getDefaultAppState();
@ -2390,63 +2393,6 @@ class App extends React.Component<AppProps, AppState> {
this.setState({});
});
private removeEventListeners() {
document.removeEventListener(EVENT.POINTER_UP, this.removePointer);
document.removeEventListener(EVENT.COPY, this.onCopy);
document.removeEventListener(EVENT.PASTE, this.pasteFromClipboard);
document.removeEventListener(EVENT.CUT, this.onCut);
this.excalidrawContainerRef.current?.removeEventListener(
EVENT.WHEEL,
this.onWheel,
);
this.nearestScrollableContainer?.removeEventListener(
EVENT.SCROLL,
this.onScroll,
);
document.removeEventListener(EVENT.KEYDOWN, this.onKeyDown, false);
document.removeEventListener(
EVENT.MOUSE_MOVE,
this.updateCurrentCursorPosition,
false,
);
document.removeEventListener(EVENT.KEYUP, this.onKeyUp);
window.removeEventListener(EVENT.RESIZE, this.onResize, false);
window.removeEventListener(EVENT.UNLOAD, this.onUnload, false);
window.removeEventListener(EVENT.BLUR, this.onBlur, false);
this.excalidrawContainerRef.current?.removeEventListener(
EVENT.DRAG_OVER,
this.disableEvent,
false,
);
this.excalidrawContainerRef.current?.removeEventListener(
EVENT.DROP,
this.disableEvent,
false,
);
document.removeEventListener(
EVENT.GESTURE_START,
this.onGestureStart as any,
false,
);
document.removeEventListener(
EVENT.GESTURE_CHANGE,
this.onGestureChange as any,
false,
);
document.removeEventListener(
EVENT.GESTURE_END,
this.onGestureEnd as any,
false,
);
document.removeEventListener(
EVENT.FULLSCREENCHANGE,
this.onFullscreenChange,
);
window.removeEventListener(EVENT.MESSAGE, this.onWindowMessage, false);
}
/** generally invoked only if fullscreen was invoked programmatically */
private onFullscreenChange = () => {
if (
@ -2460,76 +2406,108 @@ class App extends React.Component<AppProps, AppState> {
}
};
private removeEventListeners() {
this.onRemoveEventListenersEmitter.trigger();
}
private addEventListeners() {
// remove first as we can add event listeners multiple times
this.removeEventListeners();
window.addEventListener(EVENT.MESSAGE, this.onWindowMessage, false);
document.addEventListener(EVENT.POINTER_UP, this.removePointer); // #3553
document.addEventListener(EVENT.COPY, this.onCopy);
this.excalidrawContainerRef.current?.addEventListener(
EVENT.WHEEL,
this.onWheel,
{ passive: false },
);
// -------------------------------------------------------------------------
// view+edit mode listeners
// -------------------------------------------------------------------------
if (this.props.handleKeyboardGlobally) {
document.addEventListener(EVENT.KEYDOWN, this.onKeyDown, false);
this.onRemoveEventListenersEmitter.once(
addEventListener(document, EVENT.KEYDOWN, this.onKeyDown, false),
);
}
document.addEventListener(EVENT.KEYUP, this.onKeyUp, { passive: true });
document.addEventListener(
EVENT.MOUSE_MOVE,
this.updateCurrentCursorPosition,
);
// rerender text elements on font load to fix #637 && #1553
document.fonts?.addEventListener?.("loadingdone", (event) => {
const loadedFontFaces = (event as FontFaceSetLoadEvent).fontfaces;
this.fonts.onFontsLoaded(loadedFontFaces);
});
// Safari-only desktop pinch zoom
document.addEventListener(
EVENT.GESTURE_START,
this.onGestureStart as any,
false,
);
document.addEventListener(
EVENT.GESTURE_CHANGE,
this.onGestureChange as any,
false,
);
document.addEventListener(
EVENT.GESTURE_END,
this.onGestureEnd as any,
false,
this.onRemoveEventListenersEmitter.once(
addEventListener(
this.excalidrawContainerRef.current,
EVENT.WHEEL,
this.onWheel,
{ passive: false },
),
addEventListener(window, EVENT.MESSAGE, this.onWindowMessage, false),
addEventListener(document, EVENT.POINTER_UP, this.removePointer), // #3553
addEventListener(document, EVENT.COPY, this.onCopy),
addEventListener(document, EVENT.KEYUP, this.onKeyUp, { passive: true }),
addEventListener(
document,
EVENT.MOUSE_MOVE,
this.updateCurrentCursorPosition,
),
// rerender text elements on font load to fix #637 && #1553
addEventListener(document.fonts, "loadingdone", (event) => {
const loadedFontFaces = (event as FontFaceSetLoadEvent).fontfaces;
this.fonts.onFontsLoaded(loadedFontFaces);
}),
// Safari-only desktop pinch zoom
addEventListener(
document,
EVENT.GESTURE_START,
this.onGestureStart as any,
false,
),
addEventListener(
document,
EVENT.GESTURE_CHANGE,
this.onGestureChange as any,
false,
),
addEventListener(
document,
EVENT.GESTURE_END,
this.onGestureEnd as any,
false,
),
);
if (this.state.viewModeEnabled) {
return;
}
document.addEventListener(EVENT.FULLSCREENCHANGE, this.onFullscreenChange);
document.addEventListener(EVENT.PASTE, this.pasteFromClipboard);
document.addEventListener(EVENT.CUT, this.onCut);
// -------------------------------------------------------------------------
// edit-mode listeners only
// -------------------------------------------------------------------------
this.onRemoveEventListenersEmitter.once(
addEventListener(
document,
EVENT.FULLSCREENCHANGE,
this.onFullscreenChange,
),
addEventListener(document, EVENT.PASTE, this.pasteFromClipboard),
addEventListener(document, EVENT.CUT, this.onCut),
addEventListener(window, EVENT.RESIZE, this.onResize, false),
addEventListener(window, EVENT.UNLOAD, this.onUnload, false),
addEventListener(window, EVENT.BLUR, this.onBlur, false),
addEventListener(
this.excalidrawContainerRef.current,
EVENT.DRAG_OVER,
this.disableEvent,
false,
),
addEventListener(
this.excalidrawContainerRef.current,
EVENT.DROP,
this.disableEvent,
false,
),
);
if (this.props.detectScroll) {
this.nearestScrollableContainer = getNearestScrollableContainer(
this.excalidrawContainerRef.current!,
);
this.nearestScrollableContainer.addEventListener(
EVENT.SCROLL,
this.onScroll,
this.onRemoveEventListenersEmitter.once(
addEventListener(
getNearestScrollableContainer(this.excalidrawContainerRef.current!),
EVENT.SCROLL,
this.onScroll,
),
);
}
window.addEventListener(EVENT.RESIZE, this.onResize, false);
window.addEventListener(EVENT.UNLOAD, this.onUnload, false);
window.addEventListener(EVENT.BLUR, this.onBlur, false);
this.excalidrawContainerRef.current?.addEventListener(
EVENT.DRAG_OVER,
this.disableEvent,
false,
);
this.excalidrawContainerRef.current?.addEventListener(
EVENT.DROP,
this.disableEvent,
false,
);
}
componentDidUpdate(prevProps: AppProps, prevState: AppState) {

View File

@ -1,3 +1,5 @@
import { UnsubscribeCallback } from "./types";
type Subscriber<T extends any[]> = (...payload: T) => void;
export class Emitter<T extends any[] = []> {
@ -15,7 +17,7 @@ export class Emitter<T extends any[] = []> {
*
* @returns unsubscribe function
*/
on(...handlers: Subscriber<T>[] | Subscriber<T>[][]) {
on(...handlers: Subscriber<T>[] | Subscriber<T>[][]): UnsubscribeCallback {
const _handlers = handlers
.flat()
.filter((item) => typeof item === "function");
@ -25,6 +27,17 @@ export class Emitter<T extends any[] = []> {
return () => this.off(_handlers);
}
once(...handlers: Subscriber<T>[] | Subscriber<T>[][]): UnsubscribeCallback {
const _handlers = handlers
.flat()
.filter((item) => typeof item === "function");
_handlers.push(() => detach());
const detach = this.on(..._handlers);
return detach;
}
off(...handlers: Subscriber<T>[] | Subscriber<T>[][]) {
const _handlers = handlers.flat();
this.subscribers = this.subscribers.filter(

View File

@ -1,16 +1,3 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface Document {
fonts?: {
ready?: Promise<void>;
check?: (font: string, text?: string) => boolean;
load?: (font: string, text?: string) => Promise<FontFace[]>;
addEventListener?(
type: "loading" | "loadingdone" | "loadingerror",
listener: (this: Document, ev: Event) => any,
): void;
};
}
interface Window {
ClipboardItem: any;
__EXCALIDRAW_SHA__: string | undefined;

View File

@ -641,7 +641,7 @@ export type PointerDownState = Readonly<{
};
}>;
type UnsubscribeCallback = () => void;
export type UnsubscribeCallback = () => void;
export type ExcalidrawImperativeAPI = {
updateScene: InstanceType<typeof App>["updateScene"];

View File

@ -7,7 +7,13 @@ import {
WINDOWS_EMOJI_FALLBACK_FONT,
} from "./constants";
import { FontFamilyValues, FontString } from "./element/types";
import { ActiveTool, AppState, ToolType, Zoom } from "./types";
import {
ActiveTool,
AppState,
ToolType,
UnsubscribeCallback,
Zoom,
} from "./types";
import { unstable_batchedUpdates } from "react-dom";
import { ResolutionType } from "./utility-types";
import React from "react";
@ -992,3 +998,76 @@ export const updateStable = <T extends any[] | Record<string, any>>(
}
return nextValue;
};
// Window
export function addEventListener<K extends keyof WindowEventMap>(
target: Window & typeof globalThis,
type: K,
listener: (this: Window, ev: WindowEventMap[K]) => any,
options?: boolean | AddEventListenerOptions,
): UnsubscribeCallback;
export function addEventListener(
target: Window & typeof globalThis,
type: string,
listener: (this: Window, ev: Event) => any,
options?: boolean | AddEventListenerOptions,
): UnsubscribeCallback;
// Document
export function addEventListener<K extends keyof DocumentEventMap>(
target: Document,
type: K,
listener: (this: Document, ev: DocumentEventMap[K]) => any,
options?: boolean | AddEventListenerOptions,
): UnsubscribeCallback;
export function addEventListener(
target: Document,
type: string,
listener: (this: Document, ev: Event) => any,
options?: boolean | AddEventListenerOptions,
): UnsubscribeCallback;
// FontFaceSet (document.fonts)
export function addEventListener<K extends keyof FontFaceSetEventMap>(
target: FontFaceSet,
type: K,
listener: (this: FontFaceSet, ev: FontFaceSetEventMap[K]) => any,
options?: boolean | AddEventListenerOptions,
): UnsubscribeCallback;
// HTMLElement / mix
export function addEventListener<K extends keyof HTMLElementEventMap>(
target:
| Document
| (Window & typeof globalThis)
| HTMLElement
| undefined
| null
| false,
type: K,
listener: (this: HTMLDivElement, ev: HTMLElementEventMap[K]) => any,
options?: boolean | AddEventListenerOptions,
): UnsubscribeCallback;
// implem
export function addEventListener(
/**
* allows for falsy values so you don't have to type check when adding
* event listeners to optional elements
*/
target:
| Document
| (Window & typeof globalThis)
| FontFaceSet
| HTMLElement
| undefined
| null
| false,
type: keyof WindowEventMap | keyof DocumentEventMap | string,
listener: (ev: Event) => any,
options?: boolean | AddEventListenerOptions,
): UnsubscribeCallback {
if (!target) {
return () => {};
}
target?.addEventListener?.(type, listener, options);
return () => {
target?.removeEventListener?.(type, listener, options);
};
}