excalidraw/src/scene/Scene.ts
Aakansha Doshi d2181847be
fix: Always bind to container selected by user (#5880)
* fix: Always bind to container selected by user

* Don't bind to container when using text tool

* adjust z-index for bound text

* fix

* Add spec

* Add test

* Allow double click on transparent container and add spec

* fix spec

* adjust z-index only when binding

* update index

* fix

* add index check

* Update src/scene/Scene.ts

Co-authored-by: dwelle <luzar.david@gmail.com>
2022-11-25 15:45:34 +05:30

174 lines
5.2 KiB
TypeScript

import {
ExcalidrawElement,
NonDeletedExcalidrawElement,
NonDeleted,
} from "../element/types";
import { getNonDeletedElements, isNonDeletedElement } from "../element";
import { LinearElementEditor } from "../element/linearElementEditor";
type ElementIdKey = InstanceType<typeof LinearElementEditor>["elementId"];
type ElementKey = ExcalidrawElement | ElementIdKey;
type SceneStateCallback = () => void;
type SceneStateCallbackRemover = () => void;
const isIdKey = (elementKey: ElementKey): elementKey is ElementIdKey => {
if (typeof elementKey === "string") {
return true;
}
return false;
};
class Scene {
// ---------------------------------------------------------------------------
// static methods/props
// ---------------------------------------------------------------------------
private static sceneMapByElement = new WeakMap<ExcalidrawElement, Scene>();
private static sceneMapById = new Map<string, Scene>();
static mapElementToScene(elementKey: ElementKey, scene: Scene) {
if (isIdKey(elementKey)) {
// for cases where we don't have access to the element object
// (e.g. restore serialized appState with id references)
this.sceneMapById.set(elementKey, scene);
} else {
this.sceneMapByElement.set(elementKey, scene);
// if mapping element objects, also cache the id string when later
// looking up by id alone
this.sceneMapById.set(elementKey.id, scene);
}
}
static getScene(elementKey: ElementKey): Scene | null {
if (isIdKey(elementKey)) {
return this.sceneMapById.get(elementKey) || null;
}
return this.sceneMapByElement.get(elementKey) || null;
}
// ---------------------------------------------------------------------------
// instance methods/props
// ---------------------------------------------------------------------------
private callbacks: Set<SceneStateCallback> = new Set();
private nonDeletedElements: readonly NonDeletedExcalidrawElement[] = [];
private elements: readonly ExcalidrawElement[] = [];
private elementsMap = new Map<ExcalidrawElement["id"], ExcalidrawElement>();
getElementsIncludingDeleted() {
return this.elements;
}
getNonDeletedElements(): readonly NonDeletedExcalidrawElement[] {
return this.nonDeletedElements;
}
getElement<T extends ExcalidrawElement>(id: T["id"]): T | null {
return (this.elementsMap.get(id) as T | undefined) || null;
}
getNonDeletedElement(
id: ExcalidrawElement["id"],
): NonDeleted<ExcalidrawElement> | null {
const element = this.getElement(id);
if (element && isNonDeletedElement(element)) {
return element;
}
return null;
}
/**
* A utility method to help with updating all scene elements, with the added
* performance optimization of not renewing the array if no change is made.
*
* Maps all current excalidraw elements, invoking the callback for each
* element. The callback should either return a new mapped element, or the
* original element if no changes are made. If no changes are made to any
* element, this results in a no-op. Otherwise, the newly mapped elements
* are set as the next scene's elements.
*
* @returns whether a change was made
*/
mapElements(
iteratee: (element: ExcalidrawElement) => ExcalidrawElement,
): boolean {
let didChange = false;
const newElements = this.elements.map((element) => {
const nextElement = iteratee(element);
if (nextElement !== element) {
didChange = true;
}
return nextElement;
});
if (didChange) {
this.replaceAllElements(newElements);
}
return didChange;
}
replaceAllElements(nextElements: readonly ExcalidrawElement[]) {
this.elements = nextElements;
this.elementsMap.clear();
nextElements.forEach((element) => {
this.elementsMap.set(element.id, element);
Scene.mapElementToScene(element, this);
});
this.nonDeletedElements = getNonDeletedElements(this.elements);
this.informMutation();
}
informMutation() {
for (const callback of Array.from(this.callbacks)) {
callback();
}
}
addCallback(cb: SceneStateCallback): SceneStateCallbackRemover {
if (this.callbacks.has(cb)) {
throw new Error();
}
this.callbacks.add(cb);
return () => {
if (!this.callbacks.has(cb)) {
throw new Error();
}
this.callbacks.delete(cb);
};
}
destroy() {
Scene.sceneMapById.forEach((scene, elementKey) => {
if (scene === this) {
Scene.sceneMapById.delete(elementKey);
}
});
// done not for memory leaks, but to guard against possible late fires
// (I guess?)
this.callbacks.clear();
}
insertElementAtIndex(element: ExcalidrawElement, index: number) {
if (!Number.isFinite(index) || index < 0) {
throw new Error(
"insertElementAtIndex can only be called with index >= 0",
);
}
const nextElements = [
...this.elements.slice(0, index),
element,
...this.elements.slice(index),
];
this.replaceAllElements(nextElements);
}
getElementIndex(elementId: string) {
return this.elements.findIndex((element) => element.id === elementId);
}
}
export default Scene;