Subtypes: add another test.

This commit is contained in:
Daniel J. Geiger 2023-04-28 13:03:03 -05:00
parent ab3467973f
commit 63698572db
4 changed files with 104 additions and 30 deletions

View File

@ -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);
} }

View File

@ -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;
};

View File

@ -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);
} }

View File

@ -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);
});
});