feat: support creating text containers programatically

This commit is contained in:
Aakansha Doshi 2023-05-02 22:10:04 +05:30
parent 2a39d0b9a7
commit 15b5295baf
6 changed files with 105 additions and 13 deletions

View file

@ -1,3 +1,4 @@
//@ts-nocheck
import { import {
ExcalidrawElement, ExcalidrawElement,
ExcalidrawSelectionElement, ExcalidrawSelectionElement,
@ -40,6 +41,7 @@ import {
getDefaultLineHeight, getDefaultLineHeight,
measureBaseline, measureBaseline,
} from "../element/textElement"; } from "../element/textElement";
import { updateElementChildren } from "../element/newElement";
type RestoredAppState = Omit< type RestoredAppState = Omit<
AppState, AppState,
@ -194,6 +196,7 @@ const restoreElement = (
lineHeight, lineHeight,
); );
element = restoreElementWithProperties(element, { element = restoreElementWithProperties(element, {
type: "text",
fontSize, fontSize,
fontFamily, fontFamily,
text, text,
@ -390,8 +393,17 @@ export const restoreElements = (
migratedElement = { ...migratedElement, id: randomId() }; migratedElement = { ...migratedElement, id: randomId() };
} }
existingIds.add(migratedElement.id); existingIds.add(migratedElement.id);
if (element.children?.length) {
const newElements = updateElementChildren(element);
if (newElements) {
elements.push(...newElements);
} else {
elements.push(migratedElement); elements.push(migratedElement);
} }
} else {
elements.push(migratedElement);
}
}
} }
return elements; return elements;
}, [] as ExcalidrawElement[]); }, [] as ExcalidrawElement[]);
@ -540,6 +552,7 @@ export const restore = (
localElements: readonly ExcalidrawElement[] | null | undefined, localElements: readonly ExcalidrawElement[] | null | undefined,
elementsConfig?: { refreshDimensions?: boolean; repairBindings?: boolean }, elementsConfig?: { refreshDimensions?: boolean; repairBindings?: boolean },
): RestoredDataState => { ): RestoredDataState => {
console.log(restoreElements(data?.elements, localElements, elementsConfig));
return { return {
elements: restoreElements(data?.elements, localElements, elementsConfig), elements: restoreElements(data?.elements, localElements, elementsConfig),
appState: restoreAppState(data?.appState, localAppState || null), appState: restoreAppState(data?.appState, localAppState || null),

View file

@ -1,4 +1,4 @@
import { ExcalidrawElement } from "../element/types"; import { ExcalidrawElement, ExcalidrawTextContainer } from "../element/types";
import { import {
AppState, AppState,
BinaryFiles, BinaryFiles,
@ -8,6 +8,8 @@ import {
import type { cleanAppStateForExport } from "../appState"; import type { cleanAppStateForExport } from "../appState";
import { VERSIONS } from "../constants"; import { VERSIONS } from "../constants";
import { ElementConstructorOpts } from "../element/newElement";
export interface ExportedDataState { export interface ExportedDataState {
type: string; type: string;
version: number; version: number;

View file

@ -35,6 +35,8 @@ import {
wrapText, wrapText,
getBoundTextMaxWidth, getBoundTextMaxWidth,
getDefaultLineHeight, getDefaultLineHeight,
bindTextToContainer,
isValidTextContainer,
} from "./textElement"; } from "./textElement";
import { import {
DEFAULT_ELEMENT_PROPS, DEFAULT_ELEMENT_PROPS,
@ -44,10 +46,10 @@ import {
DEFAULT_VERTICAL_ALIGN, DEFAULT_VERTICAL_ALIGN,
VERTICAL_ALIGN, VERTICAL_ALIGN,
} from "../constants"; } from "../constants";
import { isArrowElement } from "./typeChecks"; import { isArrowElement, isTextElement } from "./typeChecks";
import { MarkOptional, Merge, Mutable } from "../utility-types"; import { MarkOptional, Merge, Mutable } from "../utility-types";
type ElementConstructorOpts = MarkOptional< export type ElementConstructorOpts = MarkOptional<
Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted" | "updated">, Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted" | "updated">,
| "width" | "width"
| "height" | "height"
@ -140,6 +142,7 @@ const getTextElementPositionOffsets = (
height: number; height: number;
}, },
) => { ) => {
console.log("metrics", metrics);
return { return {
x: x:
opts.textAlign === "center" opts.textAlign === "center"
@ -643,3 +646,17 @@ export const duplicateElements = (
return clonedElements; return clonedElements;
}; };
export const updateElementChildren = (
element: {
type: ExcalidrawElement["type"];
} & MarkOptional<ElementConstructorOpts, "x" | "y">,
) => {
const textElement = element.children!.find((child) => child.text !== null);
if (isValidTextContainer(element) && textElement) {
//@ts-ignore
const elements = bindTextToContainer(element, textElement);
return elements;
}
return null;
};

View file

@ -1,6 +1,7 @@
import { getFontString, arrayToMap, isTestEnv } from "../utils"; import { getFontString, arrayToMap, isTestEnv } from "../utils";
import { import {
ExcalidrawElement, ExcalidrawElement,
ExcalidrawGenericElement,
ExcalidrawTextContainer, ExcalidrawTextContainer,
ExcalidrawTextElement, ExcalidrawTextElement,
ExcalidrawTextElementWithContainer, ExcalidrawTextElementWithContainer,
@ -20,7 +21,7 @@ import {
} from "../constants"; } from "../constants";
import { MaybeTransformHandleType } from "./transformHandles"; import { MaybeTransformHandleType } from "./transformHandles";
import Scene from "../scene/Scene"; import Scene from "../scene/Scene";
import { isTextElement } from "."; import { isTextElement, newElement, newTextElement } from ".";
import { isBoundToContainer, isArrowElement } from "./typeChecks"; import { isBoundToContainer, isArrowElement } from "./typeChecks";
import { LinearElementEditor } from "./linearElementEditor"; import { LinearElementEditor } from "./linearElementEditor";
import { AppState } from "../types"; import { AppState } from "../types";
@ -32,7 +33,8 @@ import {
resetOriginalContainerCache, resetOriginalContainerCache,
updateOriginalContainerCache, updateOriginalContainerCache,
} from "./textWysiwyg"; } from "./textWysiwyg";
import { ExtractSetType } from "../utility-types"; import { ExtractSetType, MarkOptional } from "../utility-types";
import { ElementConstructorOpts } from "./newElement";
export const normalizeText = (text: string) => { export const normalizeText = (text: string) => {
return ( return (
@ -83,21 +85,27 @@ export const redrawTextBoundingBox = (
boundTextUpdates.baseline = metrics.baseline; boundTextUpdates.baseline = metrics.baseline;
if (container) { if (container) {
const containerDims = getContainerDims(container);
const maxContainerHeight = getBoundTextMaxHeight( const maxContainerHeight = getBoundTextMaxHeight(
container, container,
textElement as ExcalidrawTextElementWithContainer, textElement as ExcalidrawTextElementWithContainer,
); );
const maxContainerWidth = getBoundTextMaxWidth(container);
let nextHeight = containerDims.height;
if (metrics.height > maxContainerHeight) { if (metrics.height > maxContainerHeight) {
nextHeight = computeContainerDimensionForBoundText( const nextHeight = computeContainerDimensionForBoundText(
metrics.height, metrics.height,
container.type, container.type,
); );
mutateElement(container, { height: nextHeight }); mutateElement(container, { height: nextHeight });
updateOriginalContainerCache(container.id, nextHeight); updateOriginalContainerCache(container.id, nextHeight);
} }
if (metrics.width > maxContainerWidth) {
const nextWidth = computeContainerDimensionForBoundText(
metrics.width,
container.type,
);
mutateElement(container, { width: nextWidth });
}
const updatedTextElement = { const updatedTextElement = {
...textElement, ...textElement,
...boundTextUpdates, ...boundTextUpdates,
@ -864,8 +872,9 @@ const VALID_CONTAINER_TYPES = new Set([
"arrow", "arrow",
]); ]);
export const isValidTextContainer = (element: ExcalidrawElement) => export const isValidTextContainer = (element: {
VALID_CONTAINER_TYPES.has(element.type); type: ExcalidrawElement["type"];
}) => VALID_CONTAINER_TYPES.has(element.type);
export const computeContainerDimensionForBoundText = ( export const computeContainerDimensionForBoundText = (
dimension: number, dimension: number,
@ -971,3 +980,34 @@ export const getDefaultLineHeight = (fontFamily: FontFamilyValues) => {
} }
return DEFAULT_LINE_HEIGHT[DEFAULT_FONT_FAMILY]; return DEFAULT_LINE_HEIGHT[DEFAULT_FONT_FAMILY];
}; };
export const bindTextToContainer = (
containerProps: {
type: ExcalidrawGenericElement["type"];
} & MarkOptional<ElementConstructorOpts, "x" | "y">,
textProps: { text: string } & MarkOptional<ElementConstructorOpts, "x" | "y">,
) => {
const container = newElement({
x: 0,
y: 0,
...containerProps,
});
const textElement: ExcalidrawTextElement = newTextElement({
x: 0,
y: 0,
...textProps,
containerId: container.id,
textAlign: TEXT_ALIGN.CENTER,
verticalAlign: VERTICAL_ALIGN.MIDDLE,
});
mutateElement(container, {
boundElements: (container.boundElements || []).concat({
type: "text",
id: textElement.id,
}),
});
redrawTextBoundingBox(textElement, container);
return [container, textElement];
};

View file

@ -6,7 +6,8 @@ import {
THEME, THEME,
VERTICAL_ALIGN, VERTICAL_ALIGN,
} from "../constants"; } from "../constants";
import { MarkNonNullable, ValueOf } from "../utility-types"; import { MarkNonNullable, MarkOptional, ValueOf } from "../utility-types";
import { ElementConstructorOpts } from "./newElement";
export type ChartType = "bar" | "line"; export type ChartType = "bar" | "line";
export type FillStyle = "hachure" | "cross-hatch" | "solid" | "zigzag"; export type FillStyle = "hachure" | "cross-hatch" | "solid" | "zigzag";
@ -65,6 +66,9 @@ type _ExcalidrawElementBase = Readonly<{
link: string | null; link: string | null;
locked: boolean; locked: boolean;
customData?: Record<string, any>; customData?: Record<string, any>;
children?: [
{ text: string } & MarkOptional<ElementConstructorOpts, "x" | "y">,
];
}>; }>;
export type ExcalidrawSelectionElement = _ExcalidrawElementBase & { export type ExcalidrawSelectionElement = _ExcalidrawElementBase & {

View file

@ -344,7 +344,23 @@ const ExcalidrawWrapper = () => {
initializeScene({ collabAPI, excalidrawAPI }).then(async (data) => { initializeScene({ collabAPI, excalidrawAPI }).then(async (data) => {
loadImages(data, /* isInitialLoad */ true); loadImages(data, /* isInitialLoad */ true);
initialStatePromiseRef.current.promise.resolve(data.scene); initialStatePromiseRef.current.promise.resolve({
type: "excalidraw",
version: 2,
source: "http://localhost:3000",
elements: [
//@ts-ignore
{
type: "rectangle",
children: [{ text: "HELLO DAMMMMY" }],
},
],
appState: {
gridSize: null,
viewBackgroundColor: "#ffffff",
},
files: {},
});
}); });
const onHashChange = async (event: HashChangeEvent) => { const onHashChange = async (event: HashChangeEvent) => {