refactor: editor events sub/unsub refactor (#7483)
This commit is contained in:
parent
5f40a4cad4
commit
c72e853c85
@ -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(
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// view+edit mode listeners
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
if (this.props.handleKeyboardGlobally) {
|
||||
this.onRemoveEventListenersEmitter.once(
|
||||
addEventListener(document, EVENT.KEYDOWN, this.onKeyDown, false),
|
||||
);
|
||||
}
|
||||
|
||||
this.onRemoveEventListenersEmitter.once(
|
||||
addEventListener(
|
||||
this.excalidrawContainerRef.current,
|
||||
EVENT.WHEEL,
|
||||
this.onWheel,
|
||||
{ passive: false },
|
||||
);
|
||||
|
||||
if (this.props.handleKeyboardGlobally) {
|
||||
document.addEventListener(EVENT.KEYDOWN, this.onKeyDown, false);
|
||||
}
|
||||
document.addEventListener(EVENT.KEYUP, this.onKeyUp, { passive: true });
|
||||
document.addEventListener(
|
||||
),
|
||||
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
|
||||
document.fonts?.addEventListener?.("loadingdone", (event) => {
|
||||
addEventListener(document.fonts, "loadingdone", (event) => {
|
||||
const loadedFontFaces = (event as FontFaceSetLoadEvent).fontfaces;
|
||||
this.fonts.onFontsLoaded(loadedFontFaces);
|
||||
});
|
||||
|
||||
}),
|
||||
// Safari-only desktop pinch zoom
|
||||
document.addEventListener(
|
||||
addEventListener(
|
||||
document,
|
||||
EVENT.GESTURE_START,
|
||||
this.onGestureStart as any,
|
||||
false,
|
||||
);
|
||||
document.addEventListener(
|
||||
),
|
||||
addEventListener(
|
||||
document,
|
||||
EVENT.GESTURE_CHANGE,
|
||||
this.onGestureChange as any,
|
||||
false,
|
||||
);
|
||||
document.addEventListener(
|
||||
),
|
||||
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);
|
||||
if (this.props.detectScroll) {
|
||||
this.nearestScrollableContainer = getNearestScrollableContainer(
|
||||
this.excalidrawContainerRef.current!,
|
||||
);
|
||||
this.nearestScrollableContainer.addEventListener(
|
||||
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(
|
||||
// -------------------------------------------------------------------------
|
||||
// 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,
|
||||
);
|
||||
this.excalidrawContainerRef.current?.addEventListener(
|
||||
),
|
||||
addEventListener(
|
||||
this.excalidrawContainerRef.current,
|
||||
EVENT.DROP,
|
||||
this.disableEvent,
|
||||
false,
|
||||
),
|
||||
);
|
||||
|
||||
if (this.props.detectScroll) {
|
||||
this.onRemoveEventListenersEmitter.once(
|
||||
addEventListener(
|
||||
getNearestScrollableContainer(this.excalidrawContainerRef.current!),
|
||||
EVENT.SCROLL,
|
||||
this.onScroll,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: AppProps, prevState: AppState) {
|
||||
|
@ -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(
|
||||
|
13
packages/excalidraw/global.d.ts
vendored
13
packages/excalidraw/global.d.ts
vendored
@ -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;
|
||||
|
@ -641,7 +641,7 @@ export type PointerDownState = Readonly<{
|
||||
};
|
||||
}>;
|
||||
|
||||
type UnsubscribeCallback = () => void;
|
||||
export type UnsubscribeCallback = () => void;
|
||||
|
||||
export type ExcalidrawImperativeAPI = {
|
||||
updateScene: InstanceType<typeof App>["updateScene"];
|
||||
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user