Subtypes: add another test.
This commit is contained in:
parent
ab3467973f
commit
63698572db
@ -93,7 +93,6 @@ import {
|
|||||||
getCursorForResizingElement,
|
getCursorForResizingElement,
|
||||||
getDragOffsetXY,
|
getDragOffsetXY,
|
||||||
getElementWithTransformHandleType,
|
getElementWithTransformHandleType,
|
||||||
getNonDeletedElements,
|
|
||||||
getNormalizedDimensions,
|
getNormalizedDimensions,
|
||||||
getResizeArrowDirection,
|
getResizeArrowDirection,
|
||||||
getResizeOffsetXY,
|
getResizeOffsetXY,
|
||||||
@ -246,12 +245,14 @@ import LayerUI from "./LayerUI";
|
|||||||
import { Toast } from "./Toast";
|
import { Toast } from "./Toast";
|
||||||
import { actionToggleViewMode } from "../actions/actionToggleViewMode";
|
import { actionToggleViewMode } from "../actions/actionToggleViewMode";
|
||||||
import {
|
import {
|
||||||
|
SubtypeLoadedCb,
|
||||||
SubtypeRecord,
|
SubtypeRecord,
|
||||||
SubtypePrepFn,
|
SubtypePrepFn,
|
||||||
|
checkRefreshOnSubtypeLoad,
|
||||||
|
isSubtypeAction,
|
||||||
prepareSubtype,
|
prepareSubtype,
|
||||||
selectSubtype,
|
selectSubtype,
|
||||||
subtypeActionPredicate,
|
subtypeActionPredicate,
|
||||||
isSubtypeAction,
|
|
||||||
} from "../subtypes";
|
} from "../subtypes";
|
||||||
import {
|
import {
|
||||||
dataURLToFile,
|
dataURLToFile,
|
||||||
@ -516,31 +517,15 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private addSubtype(record: SubtypeRecord, subtypePrepFn: SubtypePrepFn) {
|
private addSubtype(record: SubtypeRecord, subtypePrepFn: SubtypePrepFn) {
|
||||||
// Call this method after finishing any async loading for
|
const subtypeLoadedCb: SubtypeLoadedCb = (hasSubtype) => {
|
||||||
// subtypes of ExcalidrawElement if the newly loaded code
|
|
||||||
// would change the rendering.
|
|
||||||
const refresh = (hasSubtype: (element: ExcalidrawElement) => boolean) => {
|
|
||||||
const elements = this.getSceneElementsIncludingDeleted();
|
const elements = this.getSceneElementsIncludingDeleted();
|
||||||
let refreshNeeded = false;
|
|
||||||
getNonDeletedElements(elements).forEach((element) => {
|
|
||||||
// If the element is of the subtype that was just
|
|
||||||
// registered, update the element's dimensions, mark the
|
|
||||||
// element for a re-render, and mark the scene for a refresh.
|
|
||||||
if (hasSubtype(element)) {
|
|
||||||
invalidateShapeForElement(element);
|
|
||||||
if (isTextElement(element)) {
|
|
||||||
redrawTextBoundingBox(element, getContainerElement(element));
|
|
||||||
}
|
|
||||||
refreshNeeded = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// If there are any elements of the just-registered subtype,
|
// If there are any elements of the just-registered subtype,
|
||||||
// refresh the scene to re-render each such element.
|
// refresh the scene to re-render each such element.
|
||||||
if (refreshNeeded) {
|
if (checkRefreshOnSubtypeLoad(hasSubtype, elements)) {
|
||||||
this.refresh();
|
this.refresh();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const prep = prepareSubtype(record, subtypePrepFn, refresh);
|
const prep = prepareSubtype(record, subtypePrepFn, subtypeLoadedCb);
|
||||||
if (prep.actions) {
|
if (prep.actions) {
|
||||||
this.actionManager.registerAll(prep.actions);
|
this.actionManager.registerAll(prep.actions);
|
||||||
}
|
}
|
||||||
|
@ -14,8 +14,13 @@ import {
|
|||||||
registerCustomShortcuts,
|
registerCustomShortcuts,
|
||||||
} from "./actions/shortcuts";
|
} from "./actions/shortcuts";
|
||||||
import { register } from "./actions/register";
|
import { register } from "./actions/register";
|
||||||
import { hasBoundTextElement } from "./element/typeChecks";
|
import { hasBoundTextElement, isTextElement } from "./element/typeChecks";
|
||||||
import { getBoundTextElement } from "./element/textElement";
|
import {
|
||||||
|
getBoundTextElement,
|
||||||
|
getContainerElement,
|
||||||
|
redrawTextBoundingBox,
|
||||||
|
} from "./element/textElement";
|
||||||
|
import { invalidateShapeForElement } from "./renderer/renderElement";
|
||||||
|
|
||||||
// Use "let" instead of "const" so we can dynamically add subtypes
|
// Use "let" instead of "const" so we can dynamically add subtypes
|
||||||
let subtypeNames: readonly Subtype[] = [];
|
let subtypeNames: readonly Subtype[] = [];
|
||||||
@ -319,9 +324,8 @@ export const selectSubtype = (
|
|||||||
|
|
||||||
// Callback to re-render subtyped `ExcalidrawElement`s after completing
|
// Callback to re-render subtyped `ExcalidrawElement`s after completing
|
||||||
// async loading of the subtype.
|
// async loading of the subtype.
|
||||||
export type SubtypeLoadedCb = (
|
export type SubtypeLoadedCb = (hasSubtype: SubtypeCheckFn) => void;
|
||||||
hasSubtype: (element: ExcalidrawElement) => boolean,
|
export type SubtypeCheckFn = (element: ExcalidrawElement) => boolean;
|
||||||
) => void;
|
|
||||||
|
|
||||||
// Functions to prepare subtypes for use
|
// Functions to prepare subtypes for use
|
||||||
export type SubtypePrepFn = (
|
export type SubtypePrepFn = (
|
||||||
@ -440,3 +444,26 @@ export const ensureSubtypesLoaded = async (
|
|||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Call this method after finishing any async loading for
|
||||||
|
// subtypes of ExcalidrawElement if the newly loaded code
|
||||||
|
// would change the rendering.
|
||||||
|
export const checkRefreshOnSubtypeLoad = (
|
||||||
|
hasSubtype: SubtypeCheckFn,
|
||||||
|
elements: readonly ExcalidrawElement[],
|
||||||
|
) => {
|
||||||
|
let refreshNeeded = false;
|
||||||
|
getNonDeletedElements(elements).forEach((element) => {
|
||||||
|
// If the element is of the subtype that was just
|
||||||
|
// registered, update the element's dimensions, mark the
|
||||||
|
// element for a re-render, and indicate the scene needs a refresh.
|
||||||
|
if (hasSubtype(element)) {
|
||||||
|
invalidateShapeForElement(element);
|
||||||
|
if (isTextElement(element)) {
|
||||||
|
redrawTextBoundingBox(element, getContainerElement(element));
|
||||||
|
}
|
||||||
|
refreshNeeded = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return refreshNeeded;
|
||||||
|
};
|
||||||
|
@ -16,8 +16,10 @@ import util from "util";
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import { getMimeType } from "../../data/blob";
|
import { getMimeType } from "../../data/blob";
|
||||||
import {
|
import {
|
||||||
|
SubtypeLoadedCb,
|
||||||
SubtypePrepFn,
|
SubtypePrepFn,
|
||||||
SubtypeRecord,
|
SubtypeRecord,
|
||||||
|
checkRefreshOnSubtypeLoad,
|
||||||
prepareSubtype,
|
prepareSubtype,
|
||||||
selectSubtype,
|
selectSubtype,
|
||||||
subtypeActionPredicate,
|
subtypeActionPredicate,
|
||||||
@ -45,7 +47,12 @@ export class API {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static addSubtype = (record: SubtypeRecord, subtypePrepFn: SubtypePrepFn) => {
|
static addSubtype = (record: SubtypeRecord, subtypePrepFn: SubtypePrepFn) => {
|
||||||
const prep = prepareSubtype(record, subtypePrepFn);
|
const subtypeLoadedCb: SubtypeLoadedCb = (hasSubtype) => {
|
||||||
|
if (checkRefreshOnSubtypeLoad(hasSubtype, h.elements)) {
|
||||||
|
h.app.refresh();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const prep = prepareSubtype(record, subtypePrepFn, subtypeLoadedCb);
|
||||||
if (prep.actions) {
|
if (prep.actions) {
|
||||||
h.app.actionManager.registerAll(prep.actions);
|
h.app.actionManager.registerAll(prep.actions);
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import fallbackLangData from "./helpers/locales/en.json";
|
import fallbackLangData from "./helpers/locales/en.json";
|
||||||
import {
|
import {
|
||||||
|
SubtypeLoadedCb,
|
||||||
SubtypeRecord,
|
SubtypeRecord,
|
||||||
SubtypeMethods,
|
SubtypeMethods,
|
||||||
SubtypePrepFn,
|
SubtypePrepFn,
|
||||||
addSubtypeMethods,
|
addSubtypeMethods,
|
||||||
|
ensureSubtypesLoadedForElements,
|
||||||
getSubtypeMethods,
|
getSubtypeMethods,
|
||||||
getSubtypeNames,
|
getSubtypeNames,
|
||||||
hasAlwaysEnabledActions,
|
hasAlwaysEnabledActions,
|
||||||
@ -16,7 +18,12 @@ import { render } from "./test-utils";
|
|||||||
import { API } from "./helpers/api";
|
import { API } from "./helpers/api";
|
||||||
import ExcalidrawApp from "../excalidraw-app";
|
import ExcalidrawApp from "../excalidraw-app";
|
||||||
|
|
||||||
import { ExcalidrawElement, FontString, Theme } from "../element/types";
|
import {
|
||||||
|
ExcalidrawElement,
|
||||||
|
ExcalidrawTextElement,
|
||||||
|
FontString,
|
||||||
|
Theme,
|
||||||
|
} from "../element/types";
|
||||||
import { createIcon, iconFillColor } from "../components/icons";
|
import { createIcon, iconFillColor } from "../components/icons";
|
||||||
import { SubtypeButton } from "../components/Subtypes";
|
import { SubtypeButton } from "../components/Subtypes";
|
||||||
import { registerAuxLangData } from "../i18n";
|
import { registerAuxLangData } from "../i18n";
|
||||||
@ -155,6 +162,18 @@ const prepareTest1Subtype = function (
|
|||||||
return { actions, methods };
|
return { actions, methods };
|
||||||
} as SubtypePrepFn;
|
} as SubtypePrepFn;
|
||||||
|
|
||||||
|
let test2Loaded = false;
|
||||||
|
|
||||||
|
const ensureLoadedTest2: SubtypeMethods["ensureLoaded"] = async (callback) => {
|
||||||
|
test2Loaded = true;
|
||||||
|
if (onTest2Loaded) {
|
||||||
|
onTest2Loaded((el) => isTextElement(el) && el.subtype === test2.subtype);
|
||||||
|
}
|
||||||
|
if (callback) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const measureTest2: SubtypeMethods["measureText"] = function (element, next) {
|
const measureTest2: SubtypeMethods["measureText"] = function (element, next) {
|
||||||
const text = next?.text ?? element.text;
|
const text = next?.text ?? element.text;
|
||||||
const customData = next?.customData ?? {};
|
const customData = next?.customData ?? {};
|
||||||
@ -165,8 +184,12 @@ const measureTest2: SubtypeMethods["measureText"] = function (element, next) {
|
|||||||
const fontString = getFontString({ fontSize, fontFamily });
|
const fontString = getFontString({ fontSize, fontFamily });
|
||||||
const lineHeight = element.lineHeight;
|
const lineHeight = element.lineHeight;
|
||||||
const metrics = textElementUtils.measureText(text, fontString, lineHeight);
|
const metrics = textElementUtils.measureText(text, fontString, lineHeight);
|
||||||
const width = Math.max(metrics.width - 10, 0);
|
const width = test2Loaded
|
||||||
const height = Math.max(metrics.height - 5, 0);
|
? metrics.width * 2
|
||||||
|
: Math.max(metrics.width - 10, 0);
|
||||||
|
const height = test2Loaded
|
||||||
|
? metrics.height * 2
|
||||||
|
: Math.max(metrics.height - 5, 0);
|
||||||
return { width, height, baseline: 1 };
|
return { width, height, baseline: 1 };
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -185,12 +208,15 @@ const wrapTest2: SubtypeMethods["wrapText"] = function (
|
|||||||
return `${text.split(" ").join("\n")}\nHello world.`;
|
return `${text.split(" ").join("\n")}\nHello world.`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let onTest2Loaded: SubtypeLoadedCb | undefined;
|
||||||
|
|
||||||
const prepareTest2Subtype = function (
|
const prepareTest2Subtype = function (
|
||||||
addSubtypeAction,
|
addSubtypeAction,
|
||||||
addLangData,
|
addLangData,
|
||||||
onSubtypeLoaded,
|
onSubtypeLoaded,
|
||||||
) {
|
) {
|
||||||
const methods = {
|
const methods = {
|
||||||
|
ensureLoaded: ensureLoadedTest2,
|
||||||
measureText: measureTest2,
|
measureText: measureTest2,
|
||||||
wrapText: wrapTest2,
|
wrapText: wrapTest2,
|
||||||
} as SubtypeMethods;
|
} as SubtypeMethods;
|
||||||
@ -201,6 +227,8 @@ const prepareTest2Subtype = function (
|
|||||||
const actions = [test2Button];
|
const actions = [test2Button];
|
||||||
actions.forEach((action) => addSubtypeAction(action));
|
actions.forEach((action) => addSubtypeAction(action));
|
||||||
|
|
||||||
|
onTest2Loaded = onSubtypeLoaded;
|
||||||
|
|
||||||
return { actions, methods };
|
return { actions, methods };
|
||||||
} as SubtypePrepFn;
|
} as SubtypePrepFn;
|
||||||
|
|
||||||
@ -255,6 +283,7 @@ describe("subtype registration", () => {
|
|||||||
let prep2 = API.addSubtype(test2, prepareTest2Subtype);
|
let prep2 = API.addSubtype(test2, prepareTest2Subtype);
|
||||||
expect(prep2.actions).toStrictEqual([test2Button]);
|
expect(prep2.actions).toStrictEqual([test2Button]);
|
||||||
expect(prep2.methods).toStrictEqual({
|
expect(prep2.methods).toStrictEqual({
|
||||||
|
ensureLoaded: ensureLoadedTest2,
|
||||||
measureText: measureTest2,
|
measureText: measureTest2,
|
||||||
wrapText: wrapTest2,
|
wrapText: wrapTest2,
|
||||||
});
|
});
|
||||||
@ -262,6 +291,7 @@ describe("subtype registration", () => {
|
|||||||
prep2 = API.addSubtype(test2, prepareNullSubtype);
|
prep2 = API.addSubtype(test2, prepareNullSubtype);
|
||||||
expect(prep2.actions).toBeNull();
|
expect(prep2.actions).toBeNull();
|
||||||
expect(prep2.methods).toStrictEqual({
|
expect(prep2.methods).toStrictEqual({
|
||||||
|
ensureLoaded: ensureLoadedTest2,
|
||||||
measureText: measureTest2,
|
measureText: measureTest2,
|
||||||
wrapText: wrapTest2,
|
wrapText: wrapTest2,
|
||||||
});
|
});
|
||||||
@ -630,3 +660,28 @@ describe("subtype actions", () => {
|
|||||||
expect(am.isActionEnabled(TEST_DISABLE3, { elements })).toBe(true);
|
expect(am.isActionEnabled(TEST_DISABLE3, { elements })).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe("subtype loading", () => {
|
||||||
|
let elements: ExcalidrawElement[];
|
||||||
|
beforeEach(async () => {
|
||||||
|
const testString = "A quick brown fox jumps over the lazy dog.";
|
||||||
|
elements = [
|
||||||
|
API.createElement({
|
||||||
|
type: "text",
|
||||||
|
id: "A",
|
||||||
|
subtype: test2.subtype,
|
||||||
|
text: testString,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
await render(<ExcalidrawApp />, { localStorageData: { elements } });
|
||||||
|
});
|
||||||
|
it("should redraw text bounding boxes", async () => {
|
||||||
|
h.setState({ selectedElementIds: { A: true } });
|
||||||
|
const el = h.elements[0] as ExcalidrawTextElement;
|
||||||
|
expect(el.width).toEqual(100);
|
||||||
|
expect(el.height).toEqual(100);
|
||||||
|
ensureSubtypesLoadedForElements(elements);
|
||||||
|
expect(el.width).toEqual(TWIDTH * 2);
|
||||||
|
expect(el.height).toEqual(THEIGHT * 2);
|
||||||
|
expect(el.baseline).toEqual(TBASELINE + 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user