Compare commits

..

3 Commits

Author SHA1 Message Date
Daniel J. Geiger 5c23fe8653 Merge remote-tracking branch 'origin/master' into non-wysiwyg-text 2023-04-20 18:46:33 -05:00
Daniel J. Geiger 95e796840a Merge remote-tracking branch 'origin/master' into non-wysiwyg-text 2023-04-14 18:58:10 -05:00
Daniel J. Geiger 7098013671 feat: Allow non-WYSIWYG measurements and wrapping for text elements. 2023-04-14 18:57:37 -05:00
32 changed files with 369 additions and 385 deletions
+2 -12
View File
@@ -12,24 +12,14 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
images: |
excalidraw/excalidraw
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v4
uses: docker/build-push-action@v3
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
tags: excalidraw/excalidraw:latest
+6 -6
View File
@@ -10,7 +10,7 @@ import {
computeBoundTextPosition,
computeContainerDimensionForBoundText,
getBoundTextElement,
measureText,
measureTextElement,
redrawTextBoundingBox,
} from "../element/textElement";
import {
@@ -31,7 +31,6 @@ import {
} from "../element/types";
import { getSelectedElements } from "../scene";
import { AppState } from "../types";
import { getFontString } from "../utils";
import { register } from "./register";
export const actionUnbindText = register({
@@ -51,10 +50,11 @@ export const actionUnbindText = register({
selectedElements.forEach((element) => {
const boundTextElement = getBoundTextElement(element);
if (boundTextElement) {
const { width, height, baseline } = measureText(
boundTextElement.originalText,
getFontString(boundTextElement),
boundTextElement.lineHeight,
const { width, height, baseline } = measureTextElement(
boundTextElement,
{
text: boundTextElement.originalText,
},
);
const originalContainerHeight = getOriginalContainerHeightFromCache(
element.id,
+1 -1
View File
@@ -18,7 +18,7 @@ export const actionCopy = register({
perform: (elements, appState, _, app) => {
const selectedElements = getSelectedElements(elements, appState, true);
copyToClipboard(selectedElements, app.files);
copyToClipboard(selectedElements, appState, app.files);
return {
commitToHistory: false,
+11 -27
View File
@@ -2,12 +2,12 @@ import {
ExcalidrawElement,
NonDeletedExcalidrawElement,
} from "./element/types";
import { BinaryFiles } from "./types";
import { AppState, BinaryFiles } from "./types";
import { SVG_EXPORT_TAG } from "./scene/export";
import { tryParseSpreadsheet, Spreadsheet, VALID_SPREADSHEET } from "./charts";
import { EXPORT_DATA_TYPES, MIME_TYPES } from "./constants";
import { isInitializedImageElement } from "./element/typeChecks";
import { isPromiseLike, isTestEnv } from "./utils";
import { isPromiseLike } from "./utils";
type ElementsClipboard = {
type: typeof EXPORT_DATA_TYPES.excalidrawClipboard;
@@ -55,40 +55,24 @@ const clipboardContainsElements = (
export const copyToClipboard = async (
elements: readonly NonDeletedExcalidrawElement[],
appState: AppState,
files: BinaryFiles | null,
) => {
let foundFile = false;
const _files = elements.reduce((acc, element) => {
if (isInitializedImageElement(element)) {
foundFile = true;
if (files && files[element.fileId]) {
acc[element.fileId] = files[element.fileId];
}
}
return acc;
}, {} as BinaryFiles);
if (foundFile && !files) {
console.warn(
"copyToClipboard: attempting to file element(s) without providing associated `files` object.",
);
}
// select binded text elements when copying
const contents: ElementsClipboard = {
type: EXPORT_DATA_TYPES.excalidrawClipboard,
elements,
files: files ? _files : undefined,
files: files
? elements.reduce((acc, element) => {
if (isInitializedImageElement(element) && files[element.fileId]) {
acc[element.fileId] = files[element.fileId];
}
return acc;
}, {} as BinaryFiles)
: undefined,
};
const json = JSON.stringify(contents);
if (isTestEnv()) {
return json;
}
CLIPBOARD = json;
try {
PREFER_APP_CLIPBOARD = false;
await copyTextToSystemClipboard(json);
+2 -15
View File
@@ -60,7 +60,6 @@ import {
ENV,
EVENT,
GRID_SIZE,
IMAGE_MIME_TYPES,
IMAGE_RENDER_TIMEOUT,
isAndroid,
isBrave,
@@ -1590,7 +1589,6 @@ class App extends React.Component<AppProps, AppState> {
elements: data.elements,
files: data.files || null,
position: "cursor",
retainSeed: isPlainPaste,
});
} else if (data.text) {
this.addTextFromPaste(data.text, isPlainPaste);
@@ -1604,7 +1602,6 @@ class App extends React.Component<AppProps, AppState> {
elements: readonly ExcalidrawElement[];
files: BinaryFiles | null;
position: { clientX: number; clientY: number } | "cursor" | "center";
retainSeed?: boolean;
}) => {
const elements = restoreElements(opts.elements, null);
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
@@ -1642,9 +1639,6 @@ class App extends React.Component<AppProps, AppState> {
y: element.y + gridY - minY,
});
}),
{
randomizeSeed: !opts.retainSeed,
},
);
const nextElements = [
@@ -4726,12 +4720,7 @@ class App extends React.Component<AppProps, AppState> {
pointerDownState.drag.hasOccurred = true;
// prevent dragging even if we're no longer holding cmd/ctrl otherwise
// it would have weird results (stuff jumping all over the screen)
// Checking for editingElement to avoid jump while editing on mobile #6503
if (
selectedElements.length > 0 &&
!pointerDownState.withCmdOrCtrl &&
!this.state.editingElement
) {
if (selectedElements.length > 0 && !pointerDownState.withCmdOrCtrl) {
const [dragX, dragY] = getGridPoint(
pointerCoords.x - pointerDownState.drag.offset.x,
pointerCoords.y - pointerDownState.drag.offset.y,
@@ -5754,9 +5743,7 @@ class App extends React.Component<AppProps, AppState> {
const imageFile = await fileOpen({
description: "Image",
extensions: Object.keys(
IMAGE_MIME_TYPES,
) as (keyof typeof IMAGE_MIME_TYPES)[],
extensions: ["jpg", "png", "svg", "gif"],
});
const imageElement = this.createImageElement({
-1
View File
@@ -183,7 +183,6 @@
width: 100%;
margin: 0;
font-size: 0.875rem;
font-family: inherit;
background-color: transparent;
color: var(--text-primary-color);
border: 0;
-1
View File
@@ -30,7 +30,6 @@
background-color: transparent;
border: none;
white-space: nowrap;
font-family: inherit;
display: grid;
grid-template-columns: 1fr 0.2fr;
+1 -1
View File
@@ -102,7 +102,7 @@ const LibraryMenuItems = ({
...item,
// duplicate each library item before inserting on canvas to confine
// ids and bindings to each library item. See #6465
elements: duplicateElements(item.elements, { randomizeSeed: true }),
elements: duplicateElements(item.elements),
};
});
};
-3
View File
@@ -2,9 +2,6 @@
// container in body where the actual tooltip is appended to
.excalidraw-tooltip {
--ui-font: Assistant, system-ui, BlinkMacSystemFont, -apple-system, Segoe UI,
Roboto, Helvetica, Arial, sans-serif;
font-family: var(--ui-font);
position: fixed;
z-index: 1000;
+16 -16
View File
@@ -105,30 +105,20 @@ export const CANVAS_ONLY_ACTIONS = ["selectAll"];
export const GRID_SIZE = 20; // TODO make it configurable?
export const IMAGE_MIME_TYPES = {
export const MIME_TYPES = {
excalidraw: "application/vnd.excalidraw+json",
excalidrawlib: "application/vnd.excalidrawlib+json",
json: "application/json",
svg: "image/svg+xml",
"excalidraw.svg": "image/svg+xml",
png: "image/png",
"excalidraw.png": "image/png",
jpg: "image/jpeg",
gif: "image/gif",
webp: "image/webp",
bmp: "image/bmp",
ico: "image/x-icon",
avif: "image/avif",
jfif: "image/jfif",
} as const;
export const MIME_TYPES = {
json: "application/json",
// excalidraw data
excalidraw: "application/vnd.excalidraw+json",
excalidrawlib: "application/vnd.excalidrawlib+json",
// image-encoded excalidraw data
"excalidraw.svg": "image/svg+xml",
"excalidraw.png": "image/png",
// binary
binary: "application/octet-stream",
// image
...IMAGE_MIME_TYPES,
} as const;
export const EXPORT_DATA_TYPES = {
@@ -199,6 +189,16 @@ export const DEFAULT_EXPORT_PADDING = 10; // px
export const DEFAULT_MAX_IMAGE_WIDTH_OR_HEIGHT = 1440;
export const ALLOWED_IMAGE_MIME_TYPES = [
MIME_TYPES.png,
MIME_TYPES.jpg,
MIME_TYPES.svg,
MIME_TYPES.gif,
MIME_TYPES.webp,
MIME_TYPES.bmp,
MIME_TYPES.ico,
] as const;
export const MAX_ALLOWED_FILE_BYTES = 2 * 1024 * 1024;
export const SVG_NS = "http://www.w3.org/2000/svg";
-2
View File
@@ -354,7 +354,6 @@
border-radius: var(--space-factor);
border: 1px solid var(--button-gray-2);
font-size: 0.8rem;
font-family: inherit;
outline: none;
appearance: none;
background-image: var(--dropdown-icon);
@@ -414,7 +413,6 @@
bottom: 30px;
transform: translateX(-50%);
pointer-events: all;
font-family: inherit;
&:hover {
background-color: var(--button-hover-bg);
+6 -4
View File
@@ -1,6 +1,6 @@
import { nanoid } from "nanoid";
import { cleanAppStateForExport } from "../appState";
import { IMAGE_MIME_TYPES, MIME_TYPES } from "../constants";
import { ALLOWED_IMAGE_MIME_TYPES, MIME_TYPES } from "../constants";
import { clearElementsForExport } from "../element";
import { ExcalidrawElement, FileId } from "../element/types";
import { CanvasError } from "../errors";
@@ -117,9 +117,11 @@ export const isImageFileHandle = (handle: FileSystemHandle | null) => {
export const isSupportedImageFile = (
blob: Blob | null | undefined,
): blob is Blob & { type: ValueOf<typeof IMAGE_MIME_TYPES> } => {
): blob is Blob & { type: typeof ALLOWED_IMAGE_MIME_TYPES[number] } => {
const { type } = blob || {};
return !!type && (Object.values(IMAGE_MIME_TYPES) as string[]).includes(type);
return (
!!type && (ALLOWED_IMAGE_MIME_TYPES as readonly string[]).includes(type)
);
};
export const loadSceneOrLibraryFromBlob = async (
@@ -155,7 +157,7 @@ export const loadSceneOrLibraryFromBlob = async (
},
localAppState,
localElements,
{ repairBindings: true, refreshDimensions: false },
{ repairBindings: true, refreshDimensions: true },
),
};
} else if (isValidLibrary(data)) {
+10 -1
View File
@@ -8,7 +8,16 @@ import { EVENT, MIME_TYPES } from "../constants";
import { AbortError } from "../errors";
import { debounce } from "../utils";
type FILE_EXTENSION = Exclude<keyof typeof MIME_TYPES, "binary">;
type FILE_EXTENSION =
| "gif"
| "jpg"
| "png"
| "excalidraw.png"
| "svg"
| "excalidraw.svg"
| "json"
| "excalidraw"
| "excalidrawlib";
const INPUT_CHANGE_INTERVAL_MS = 500;
+8 -8
View File
@@ -31,14 +31,14 @@ import {
import { getDefaultAppState } from "../appState";
import { LinearElementEditor } from "../element/linearElementEditor";
import { bumpVersion } from "../element/mutateElement";
import { getFontString, getUpdatedTimestamp, updateActiveTool } from "../utils";
import { getUpdatedTimestamp, updateActiveTool } from "../utils";
import { arrayToMap } from "../utils";
import oc from "open-color";
import { MarkOptional, Mutable } from "../utility-types";
import {
detectLineHeight,
getDefaultLineHeight,
measureBaseline,
measureTextElement,
} from "../element/textElement";
type RestoredAppState = Omit<
@@ -80,7 +80,8 @@ const getFontFamilyByName = (fontFamilyName: string): FontFamilyValues => {
};
const restoreElementWithProperties = <
T extends Required<Omit<ExcalidrawElement, "customData">> & {
T extends Required<Omit<ExcalidrawElement, "subtype" | "customData">> & {
subtype?: ExcalidrawElement["subtype"];
customData?: ExcalidrawElement["customData"];
/** @deprecated */
boundElementIds?: readonly ExcalidrawElement["id"][];
@@ -143,6 +144,9 @@ const restoreElementWithProperties = <
locked: element.locked ?? false,
};
if ("subtype" in element) {
base.subtype = element.subtype;
}
if ("customData" in element) {
base.customData = element.customData;
}
@@ -188,11 +192,7 @@ const restoreElement = (
: // no element height likely means programmatic use, so default
// to a fixed line height
getDefaultLineHeight(element.fontFamily));
const baseline = measureBaseline(
element.text,
getFontString(element),
lineHeight,
);
const baseline = measureTextElement(element, { text }).baseline;
element = restoreElementWithProperties(element, {
fontSize,
fontFamily,
+1 -15
View File
@@ -40,11 +40,7 @@ import { isBindingElement } from "./typeChecks";
import { shouldRotateWithDiscreteAngle } from "../keys";
import { getBoundTextElement, handleBindTextResize } from "./textElement";
import { getShapeForElement } from "../renderer/renderElement";
import {
BOUND_TEXT_PADDING,
DRAGGING_THRESHOLD,
VERTICAL_ALIGN,
} from "../constants";
import { DRAGGING_THRESHOLD } from "../constants";
import { Mutable } from "../utility-types";
const editorMidPointsCache: {
@@ -1308,16 +1304,6 @@ export class LinearElementEditor {
}
x = midSegmentMidpoint[0] - boundTextElement.width / 2;
y = midSegmentMidpoint[1] - boundTextElement.height / 2;
if (element.points.length === 2) {
if (boundTextElement.verticalAlign === VERTICAL_ALIGN.TOP) {
y =
midSegmentMidpoint[1] -
boundTextElement.height -
BOUND_TEXT_PADDING * 2;
} else if (boundTextElement.verticalAlign === VERTICAL_ALIGN.BOTTOM) {
y = midSegmentMidpoint[1] + BOUND_TEXT_PADDING * 2;
}
}
}
return { x, y };
};
+20 -38
View File
@@ -13,14 +13,9 @@ import {
FontFamilyValues,
ExcalidrawTextContainer,
} from "../element/types";
import {
arrayToMap,
getFontString,
getUpdatedTimestamp,
isTestEnv,
} from "../utils";
import { arrayToMap, getUpdatedTimestamp, isTestEnv } from "../utils";
import { randomInteger, randomId } from "../random";
import { bumpVersion, mutateElement, newElementWith } from "./mutateElement";
import { mutateElement, newElementWith } from "./mutateElement";
import { getNewGroupIdsForDuplication } from "../groups";
import { AppState } from "../types";
import { getElementAbsoluteCoords } from ".";
@@ -30,10 +25,10 @@ import {
getBoundTextElementOffset,
getContainerDims,
getContainerElement,
measureText,
measureTextElement,
normalizeText,
wrapText,
getBoundTextMaxWidth,
wrapTextElement,
getMaxContainerWidth,
getDefaultLineHeight,
} from "./textElement";
import {
@@ -58,6 +53,8 @@ type ElementConstructorOpts = MarkOptional<
| "version"
| "versionNonce"
| "link"
| "subtype"
| "customData"
| "strokeStyle"
| "fillStyle"
| "strokeColor"
@@ -167,10 +164,12 @@ export const newTextElement = (
const fontSize = opts.fontSize || DEFAULT_FONT_SIZE;
const lineHeight = opts.lineHeight || getDefaultLineHeight(fontFamily);
const text = normalizeText(opts.text);
const metrics = measureText(
text,
getFontString({ fontFamily, fontSize }),
lineHeight,
const metrics = measureTextElement(
{ ...opts, fontSize, fontFamily, lineHeight },
{
text,
customData: opts.customData,
},
);
const textAlign = opts.textAlign || DEFAULT_TEXT_ALIGN;
const verticalAlign = opts.verticalAlign || DEFAULT_VERTICAL_ALIGN;
@@ -217,7 +216,9 @@ const getAdjustedDimensions = (
width: nextWidth,
height: nextHeight,
baseline: nextBaseline,
} = measureText(nextText, getFontString(element), element.lineHeight);
} = measureTextElement(element, {
text: nextText,
});
const { textAlign, verticalAlign } = element;
let x: number;
let y: number;
@@ -226,11 +227,7 @@ const getAdjustedDimensions = (
verticalAlign === VERTICAL_ALIGN.MIDDLE &&
!element.containerId
) {
const prevMetrics = measureText(
element.text,
getFontString(element),
element.lineHeight,
);
const prevMetrics = measureTextElement(element);
const offsets = getTextElementPositionOffsets(element, {
width: nextWidth - prevMetrics.width,
height: nextHeight - prevMetrics.height,
@@ -307,11 +304,9 @@ export const refreshTextDimensions = (
}
const container = getContainerElement(textElement);
if (container) {
text = wrapText(
text = wrapTextElement(textElement, getMaxContainerWidth(container), {
text,
getFontString(textElement),
getBoundTextMaxWidth(container),
);
});
}
const dimensions = getAdjustedDimensions(textElement, text);
return { text, ...dimensions };
@@ -539,16 +534,8 @@ export const duplicateElement = <TElement extends ExcalidrawElement>(
* it's advised to supply the whole elements array, or sets of elements that
* are encapsulated (such as library items), if the purpose is to retain
* bindings to the cloned elements intact.
*
* NOTE by default does not randomize or regenerate anything except the id.
*/
export const duplicateElements = (
elements: readonly ExcalidrawElement[],
opts?: {
/** NOTE also updates version flags and `updated` */
randomizeSeed: boolean;
},
) => {
export const duplicateElements = (elements: readonly ExcalidrawElement[]) => {
const clonedElements: ExcalidrawElement[] = [];
const origElementsMap = arrayToMap(elements);
@@ -582,11 +569,6 @@ export const duplicateElements = (
clonedElement.id = maybeGetNewId(element.id)!;
if (opts?.randomizeSeed) {
clonedElement.seed = randomInteger();
bumpVersion(clonedElement);
}
if (clonedElement.groupIds) {
clonedElement.groupIds = clonedElement.groupIds.map((groupId) => {
if (!groupNewIdsMap.has(groupId)) {
+7 -7
View File
@@ -44,10 +44,10 @@ import {
getBoundTextElementId,
getContainerElement,
handleBindTextResize,
getBoundTextMaxWidth,
getMaxContainerWidth,
getApproxMinLineHeight,
measureText,
getBoundTextMaxHeight,
getMaxContainerHeight,
} from "./textElement";
export const normalizeAngle = (angle: number): number => {
@@ -204,7 +204,7 @@ const measureFontSizeFromWidth = (
if (hasContainer) {
const container = getContainerElement(element);
if (container) {
width = getBoundTextMaxWidth(container);
width = getMaxContainerWidth(container);
}
}
const nextFontSize = element.fontSize * (nextWidth / width);
@@ -435,8 +435,8 @@ export const resizeSingleElement = (
const nextFont = measureFontSizeFromWidth(
boundTextElement,
getBoundTextMaxWidth(updatedElement),
getBoundTextMaxHeight(updatedElement, boundTextElement),
getMaxContainerWidth(updatedElement),
getMaxContainerHeight(updatedElement),
);
if (nextFont === null) {
return;
@@ -718,10 +718,10 @@ const resizeMultipleElements = (
const metrics = measureFontSizeFromWidth(
boundTextElement ?? (element.orig as ExcalidrawTextElement),
boundTextElement
? getBoundTextMaxWidth(updatedElement)
? getMaxContainerWidth(updatedElement)
: updatedElement.width,
boundTextElement
? getBoundTextMaxHeight(updatedElement, boundTextElement)
? getMaxContainerHeight(updatedElement)
: updatedElement.height,
);
+11 -48
View File
@@ -3,15 +3,15 @@ import { API } from "../tests/helpers/api";
import {
computeContainerDimensionForBoundText,
getContainerCoords,
getBoundTextMaxWidth,
getBoundTextMaxHeight,
getMaxContainerWidth,
getMaxContainerHeight,
wrapText,
detectLineHeight,
getLineHeightInPx,
getDefaultLineHeight,
parseTokens,
} from "./textElement";
import { ExcalidrawTextElementWithContainer, FontString } from "./types";
import { FontString } from "./types";
describe("Test wrapText", () => {
const font = "20px Cascadia, width: Segoe UI Emoji" as FontString;
@@ -311,7 +311,7 @@ describe("Test measureText", () => {
});
});
describe("Test getBoundTextMaxWidth", () => {
describe("Test getMaxContainerWidth", () => {
const params = {
width: 178,
height: 194,
@@ -319,76 +319,39 @@ describe("Test measureText", () => {
it("should return max width when container is rectangle", () => {
const container = API.createElement({ type: "rectangle", ...params });
expect(getBoundTextMaxWidth(container)).toBe(168);
expect(getMaxContainerWidth(container)).toBe(168);
});
it("should return max width when container is ellipse", () => {
const container = API.createElement({ type: "ellipse", ...params });
expect(getBoundTextMaxWidth(container)).toBe(116);
expect(getMaxContainerWidth(container)).toBe(116);
});
it("should return max width when container is diamond", () => {
const container = API.createElement({ type: "diamond", ...params });
expect(getBoundTextMaxWidth(container)).toBe(79);
expect(getMaxContainerWidth(container)).toBe(79);
});
});
describe("Test getBoundTextMaxHeight", () => {
describe("Test getMaxContainerHeight", () => {
const params = {
width: 178,
height: 194,
id: '"container-id',
};
const boundTextElement = API.createElement({
type: "text",
id: "text-id",
x: 560.51171875,
y: 202.033203125,
width: 154,
height: 175,
fontSize: 20,
fontFamily: 1,
text: "Excalidraw is a\nvirtual \nopensource \nwhiteboard for \nsketching \nhand-drawn like\ndiagrams",
textAlign: "center",
verticalAlign: "middle",
containerId: params.id,
}) as ExcalidrawTextElementWithContainer;
it("should return max height when container is rectangle", () => {
const container = API.createElement({ type: "rectangle", ...params });
expect(getBoundTextMaxHeight(container, boundTextElement)).toBe(184);
expect(getMaxContainerHeight(container)).toBe(184);
});
it("should return max height when container is ellipse", () => {
const container = API.createElement({ type: "ellipse", ...params });
expect(getBoundTextMaxHeight(container, boundTextElement)).toBe(127);
expect(getMaxContainerHeight(container)).toBe(127);
});
it("should return max height when container is diamond", () => {
const container = API.createElement({ type: "diamond", ...params });
expect(getBoundTextMaxHeight(container, boundTextElement)).toBe(87);
});
it("should return max height when container is arrow", () => {
const container = API.createElement({
type: "arrow",
...params,
});
expect(getBoundTextMaxHeight(container, boundTextElement)).toBe(194);
});
it("should return max height when container is arrow and height is less than threshold", () => {
const container = API.createElement({
type: "arrow",
...params,
height: 70,
boundElements: [{ type: "text", id: "text-id" }],
});
expect(getBoundTextMaxHeight(container, boundTextElement)).toBe(
boundTextElement.height,
);
expect(getMaxContainerHeight(container)).toBe(87);
});
});
});
+82 -56
View File
@@ -1,3 +1,4 @@
import { getSubtypeMethods, SubtypeMethods } from "../subtypes";
import { getFontString, arrayToMap, isTestEnv } from "../utils";
import {
ExcalidrawElement,
@@ -34,6 +35,30 @@ import {
} from "./textWysiwyg";
import { ExtractSetType } from "../utility-types";
export const measureTextElement = function (element, next) {
const map = getSubtypeMethods(element.subtype);
if (map?.measureText) {
return map.measureText(element, next);
}
const fontSize = next?.fontSize ?? element.fontSize;
const font = getFontString({ fontSize, fontFamily: element.fontFamily });
const text = next?.text ?? element.text;
return measureText(text, font, element.lineHeight);
} as SubtypeMethods["measureText"];
export const wrapTextElement = function (element, containerWidth, next) {
const map = getSubtypeMethods(element.subtype);
if (map?.wrapText) {
return map.wrapText(element, containerWidth, next);
}
const fontSize = next?.fontSize ?? element.fontSize;
const font = getFontString({ fontSize, fontFamily: element.fontFamily });
const text = next?.text ?? element.originalText;
return wrapText(text, font, containerWidth);
} as SubtypeMethods["wrapText"];
export const normalizeText = (text: string) => {
return (
text
@@ -65,40 +90,47 @@ export const redrawTextBoundingBox = (
boundTextUpdates.text = textElement.text;
if (container) {
maxWidth = getBoundTextMaxWidth(container);
boundTextUpdates.text = wrapText(
textElement.originalText,
getFontString(textElement),
maxWidth,
);
maxWidth = getMaxContainerWidth(container);
boundTextUpdates.text = wrapTextElement(textElement, maxWidth);
}
const metrics = measureText(
boundTextUpdates.text,
getFontString(textElement),
textElement.lineHeight,
);
const metrics = measureTextElement(textElement, {
text: boundTextUpdates.text,
});
boundTextUpdates.width = metrics.width;
boundTextUpdates.height = metrics.height;
boundTextUpdates.baseline = metrics.baseline;
if (container) {
const containerDims = getContainerDims(container);
const maxContainerHeight = getBoundTextMaxHeight(
container,
textElement as ExcalidrawTextElementWithContainer,
);
let nextHeight = containerDims.height;
if (metrics.height > maxContainerHeight) {
nextHeight = computeContainerDimensionForBoundText(
metrics.height,
container.type,
);
mutateElement(container, { height: nextHeight });
updateOriginalContainerCache(container.id, nextHeight);
// Maintain coordX for non left-aligned text in case the width has changed
if (!container) {
if (textElement.textAlign === TEXT_ALIGN.RIGHT) {
boundTextUpdates.x += textElement.width - metrics.width;
} else if (textElement.textAlign === TEXT_ALIGN.CENTER) {
boundTextUpdates.x += textElement.width / 2 - metrics.width / 2;
}
if (!isArrowElement(container)) {
}
if (container) {
if (isArrowElement(container)) {
const centerX = textElement.x + textElement.width / 2;
const centerY = textElement.y + textElement.height / 2;
const diffWidth = metrics.width - textElement.width;
const diffHeight = metrics.height - textElement.height;
boundTextUpdates.x = centerY - (textElement.height + diffHeight) / 2;
boundTextUpdates.y = centerX - (textElement.width + diffWidth) / 2;
} else {
const containerDims = getContainerDims(container);
let maxContainerHeight = getMaxContainerHeight(container);
let nextHeight = containerDims.height;
if (metrics.height > maxContainerHeight) {
nextHeight = computeContainerDimensionForBoundText(
metrics.height,
container.type,
);
mutateElement(container, { height: nextHeight });
maxContainerHeight = getMaxContainerHeight(container);
updateOriginalContainerCache(container.id, nextHeight);
}
const updatedTextElement = {
...textElement,
...boundTextUpdates,
@@ -178,26 +210,15 @@ export const handleBindTextResize = (
let nextHeight = textElement.height;
let nextWidth = textElement.width;
const containerDims = getContainerDims(container);
const maxWidth = getBoundTextMaxWidth(container);
const maxHeight = getBoundTextMaxHeight(
container,
textElement as ExcalidrawTextElementWithContainer,
);
const maxWidth = getMaxContainerWidth(container);
const maxHeight = getMaxContainerHeight(container);
let containerHeight = containerDims.height;
let nextBaseLine = textElement.baseline;
if (transformHandleType !== "n" && transformHandleType !== "s") {
if (text) {
text = wrapText(
textElement.originalText,
getFontString(textElement),
maxWidth,
);
text = wrapTextElement(textElement, maxWidth);
}
const metrics = measureText(
text,
getFontString(textElement),
textElement.lineHeight,
);
const metrics = measureTextElement(textElement, { text });
nextHeight = metrics.height;
nextWidth = metrics.width;
nextBaseLine = metrics.baseline;
@@ -254,8 +275,8 @@ export const computeBoundTextPosition = (
);
}
const containerCoords = getContainerCoords(container);
const maxContainerHeight = getBoundTextMaxHeight(container, boundTextElement);
const maxContainerWidth = getBoundTextMaxWidth(container);
const maxContainerHeight = getMaxContainerHeight(container);
const maxContainerWidth = getMaxContainerWidth(container);
let x;
let y;
@@ -795,11 +816,7 @@ export const shouldAllowVerticalAlign = (
const hasBoundContainer = isBoundToContainer(element);
if (hasBoundContainer) {
const container = getContainerElement(element);
if (
isTextElement(element) &&
isArrowElement(container) &&
container.points.length > 2
) {
if (isTextElement(element) && isArrowElement(container)) {
return false;
}
return true;
@@ -892,10 +909,18 @@ export const computeContainerDimensionForBoundText = (
return dimension + padding;
};
export const getBoundTextMaxWidth = (container: ExcalidrawElement) => {
export const getMaxContainerWidth = (container: ExcalidrawElement) => {
const width = getContainerDims(container).width;
if (isArrowElement(container)) {
return width - BOUND_TEXT_PADDING * 8 * 2;
const containerWidth = width - BOUND_TEXT_PADDING * 8 * 2;
if (containerWidth <= 0) {
const boundText = getBoundTextElement(container);
if (boundText) {
return boundText.width;
}
return BOUND_TEXT_PADDING * 8 * 2;
}
return containerWidth;
}
if (container.type === "ellipse") {
@@ -912,15 +937,16 @@ export const getBoundTextMaxWidth = (container: ExcalidrawElement) => {
return width - BOUND_TEXT_PADDING * 2;
};
export const getBoundTextMaxHeight = (
container: ExcalidrawElement,
boundTextElement: ExcalidrawTextElementWithContainer,
) => {
export const getMaxContainerHeight = (container: ExcalidrawElement) => {
const height = getContainerDims(container).height;
if (isArrowElement(container)) {
const containerHeight = height - BOUND_TEXT_PADDING * 8 * 2;
if (containerHeight <= 0) {
return boundTextElement.height;
const boundText = getBoundTextElement(container);
if (boundText) {
return boundText.height;
}
return BOUND_TEXT_PADDING * 8 * 2;
}
return height;
}
+71 -21
View File
@@ -32,8 +32,8 @@ import {
normalizeText,
redrawTextBoundingBox,
wrapText,
getBoundTextMaxHeight,
getBoundTextMaxWidth,
getMaxContainerHeight,
getMaxContainerWidth,
computeContainerDimensionForBoundText,
detectLineHeight,
} from "./textElement";
@@ -47,6 +47,7 @@ import { LinearElementEditor } from "./linearElementEditor";
import { parseClipboard } from "../clipboard";
const getTransform = (
offsetX: number,
width: number,
height: number,
angle: number,
@@ -64,7 +65,7 @@ const getTransform = (
if (height > maxHeight && zoom.value !== 1) {
translateY = (maxHeight * (zoom.value - 1)) / 2;
}
return `translate(${translateX}px, ${translateY}px) scale(${zoom.value}) rotate(${degree}deg)`;
return `translate(${translateX}px, ${translateY}px) scale(${zoom.value}) rotate(${degree}deg) translate(${offsetX}px, 0px)`;
};
const originalContainerCache: {
@@ -158,13 +159,30 @@ export const textWysiwyg = ({
const container = getContainerElement(updatedTextElement);
let maxWidth = updatedTextElement.width;
let maxHeight = updatedTextElement.height;
let textElementWidth = updatedTextElement.width;
// Editing metrics
const eMetrics = measureText(
container && updatedTextElement.containerId
? wrapText(
updatedTextElement.originalText,
getFontString(updatedTextElement),
getMaxContainerWidth(container),
)
: updatedTextElement.originalText,
getFontString(updatedTextElement),
updatedTextElement.lineHeight,
);
let maxHeight = eMetrics.height;
let textElementWidth = Math.max(updatedTextElement.width, eMetrics.width);
// Set to element height by default since that's
// what is going to be used for unbounded text
let textElementHeight = updatedTextElement.height;
let textElementHeight = Math.max(updatedTextElement.height, maxHeight);
if (container && updatedTextElement.containerId) {
textElementHeight = Math.min(
getMaxContainerHeight(container),
textElementHeight,
);
if (isArrowElement(container)) {
const boundTextCoords =
LinearElementEditor.getBoundTextElementPosition(
@@ -173,6 +191,8 @@ export const textWysiwyg = ({
);
coordX = boundTextCoords.x;
coordY = boundTextCoords.y;
} else {
coordX = Math.max(coordX, getContainerCoords(container).x);
}
const propertiesUpdated = textPropertiesUpdated(
updatedTextElement,
@@ -186,7 +206,18 @@ export const textWysiwyg = ({
}
if (propertiesUpdated) {
// update height of the editor after properties updated
textElementHeight = updatedTextElement.height;
const font = getFontString(updatedTextElement);
textElementHeight =
updatedTextElement.lineHeight *
wrapText(
updatedTextElement.originalText,
font,
getMaxContainerWidth(container),
).split("\n").length;
textElementHeight = Math.max(
textElementHeight,
updatedTextElement.height,
);
}
let originalContainerData;
@@ -205,11 +236,8 @@ export const textWysiwyg = ({
}
}
maxWidth = getBoundTextMaxWidth(container);
maxHeight = getBoundTextMaxHeight(
container,
updatedTextElement as ExcalidrawTextElementWithContainer,
);
maxWidth = getMaxContainerWidth(container);
maxHeight = getMaxContainerHeight(container);
// autogrow container height if text exceeds
if (!isArrowElement(container) && textElementHeight > maxHeight) {
@@ -233,14 +261,17 @@ export const textWysiwyg = ({
);
mutateElement(container, { height: targetContainerHeight });
}
// update y coord as you type, not needed for arrow as we calculate
// position from the container element for editor and canvas when rendering labelled arrows
else if (!isArrowElement(container)) {
// Start pushing text upward until a diff of 30px (padding)
// is reached
else {
const containerCoords = getContainerCoords(container);
// vertically center align the text
if (verticalAlign === VERTICAL_ALIGN.MIDDLE) {
coordY = containerCoords.y + maxHeight / 2 - textElementHeight / 2;
if (!isArrowElement(container)) {
coordY =
containerCoords.y + maxHeight / 2 - textElementHeight / 2;
}
}
if (verticalAlign === VERTICAL_ALIGN.BOTTOM) {
coordY = containerCoords.y + (maxHeight - textElementHeight);
@@ -266,12 +297,29 @@ export const textWysiwyg = ({
editable.selectionEnd = editable.value.length - diff;
}
let transformWidth = updatedTextElement.width;
if (!container) {
maxWidth = (appState.width - 8 - viewportX) / appState.zoom.value;
textElementWidth = Math.min(textElementWidth, maxWidth);
} else {
textElementWidth += 0.5;
transformWidth += 0.5;
}
// Horizontal offset in case updatedTextElement has a non-WYSIWYG subtype
const offWidth = container
? Math.min(
0,
updatedTextElement.width - Math.min(maxWidth, eMetrics.width),
)
: Math.min(maxWidth, updatedTextElement.width) -
Math.min(maxWidth, eMetrics.width);
const offsetX =
textAlign === "right"
? offWidth
: textAlign === "center"
? offWidth / 2
: 0;
const { width: w, height: h } = updatedTextElement;
let lineHeight = updatedTextElement.lineHeight;
@@ -290,13 +338,15 @@ export const textWysiwyg = ({
font: getFontString(updatedTextElement),
// must be defined *after* font ¯\_(ツ)_/¯
lineHeight,
width: `${textElementWidth}px`,
width: `${Math.min(textElementWidth, maxWidth)}px`,
height: `${textElementHeight}px`,
left: `${viewportX}px`,
top: `${viewportY}px`,
transformOrigin: `${w / 2}px ${h / 2}px`,
transform: getTransform(
textElementWidth,
textElementHeight,
offsetX,
transformWidth,
updatedTextElement.height,
getTextElementAngle(updatedTextElement),
appState,
maxWidth,
@@ -377,7 +427,7 @@ export const textWysiwyg = ({
const wrappedText = wrapText(
`${editable.value}${data}`,
font,
getBoundTextMaxWidth(container),
getMaxContainerWidth(container),
);
const width = getTextWidth(wrappedText, font);
editable.style.width = `${width}px`;
@@ -394,7 +444,7 @@ export const textWysiwyg = ({
const wrappedText = wrapText(
normalizeText(editable.value),
font,
getBoundTextMaxWidth(container!),
getMaxContainerWidth(container!),
);
const { width, height } = measureText(
wrappedText,
+2
View File
@@ -1,3 +1,4 @@
import { Subtype } from "../subtypes";
import { Point } from "../types";
import {
FONT_FAMILY,
@@ -64,6 +65,7 @@ type _ExcalidrawElementBase = Readonly<{
updated: number;
link: string | null;
locked: boolean;
subtype?: Subtype;
customData?: Record<string, any>;
}>;
+1 -1
View File
@@ -263,7 +263,7 @@ export const loadScene = async (
await importFromBackend(id, privateKey),
localDataState?.appState,
localDataState?.elements,
{ repairBindings: true, refreshDimensions: false },
{ repairBindings: true, refreshDimensions: true },
);
} else {
data = restore(localDataState || null, null, null, {
+9 -1
View File
@@ -220,7 +220,15 @@ export const exportToClipboard = async (
} else if (opts.type === "png") {
await copyBlobToClipboardAsPng(exportToBlob(opts));
} else if (opts.type === "json") {
await copyToClipboard(opts.elements, opts.files);
const appState = {
offsetTop: 0,
offsetLeft: 0,
width: 0,
height: 0,
...getDefaultAppState(),
...opts.appState,
};
await copyToClipboard(opts.elements, appState, opts.files);
} else {
throw new Error("Invalid export type");
}
+4 -7
View File
@@ -44,8 +44,8 @@ import {
getContainerCoords,
getContainerElement,
getLineHeightInPx,
getBoundTextMaxHeight,
getBoundTextMaxWidth,
getMaxContainerHeight,
getMaxContainerWidth,
} from "../element/textElement";
import { LinearElementEditor } from "../element/linearElementEditor";
@@ -868,17 +868,14 @@ const drawElementFromCanvas = (
"true" &&
hasBoundTextElement(element)
) {
const textElement = getBoundTextElement(
element,
) as ExcalidrawTextElementWithContainer;
const coords = getContainerCoords(element);
context.strokeStyle = "#c92a2a";
context.lineWidth = 3;
context.strokeRect(
(coords.x + renderConfig.scrollX) * window.devicePixelRatio,
(coords.y + renderConfig.scrollY) * window.devicePixelRatio,
getBoundTextMaxWidth(element) * window.devicePixelRatio,
getBoundTextMaxHeight(element, textElement) * window.devicePixelRatio,
getMaxContainerWidth(element) * window.devicePixelRatio,
getMaxContainerHeight(element) * window.devicePixelRatio,
);
}
}
+1 -2
View File
@@ -1,6 +1,5 @@
import { isTextElement, refreshTextDimensions } from "../element";
import { newElementWith } from "../element/mutateElement";
import { isBoundToContainer } from "../element/typeChecks";
import { ExcalidrawElement, ExcalidrawTextElement } from "../element/types";
import { invalidateShapeForElement } from "../renderer/renderElement";
import { getFontString } from "../utils";
@@ -53,7 +52,7 @@ export class Fonts {
let didUpdate = false;
this.scene.mapElements((element) => {
if (isTextElement(element) && !isBoundToContainer(element)) {
if (isTextElement(element)) {
invalidateShapeForElement(element);
didUpdate = true;
return newElementWith(element, {
+52
View File
@@ -0,0 +1,52 @@
import { ExcalidrawElement, ExcalidrawTextElement } from "./element/types";
// Subtype Names
export type Subtype = string;
// Subtype Methods
export type SubtypeMethods = {
measureText: (
element: Pick<
ExcalidrawTextElement,
| "subtype"
| "customData"
| "fontSize"
| "fontFamily"
| "text"
| "lineHeight"
>,
next?: {
fontSize?: number;
text?: string;
customData?: ExcalidrawElement["customData"];
},
) => { width: number; height: number; baseline: number };
wrapText: (
element: Pick<
ExcalidrawTextElement,
| "subtype"
| "customData"
| "fontSize"
| "fontFamily"
| "originalText"
| "lineHeight"
>,
containerWidth: number,
next?: {
fontSize?: number;
text?: string;
customData?: ExcalidrawElement["customData"];
},
) => string;
};
type MethodMap = { subtype: Subtype; methods: Partial<SubtypeMethods> };
const methodMaps = [] as Array<MethodMap>;
// Use `getSubtypeMethods` to call subtype-specialized methods, like `render`.
export const getSubtypeMethods = (
subtype: Subtype | undefined,
): Partial<SubtypeMethods> | undefined => {
const map = methodMaps.find((method) => method.subtype === subtype);
return map?.methods;
};
@@ -5,7 +5,7 @@ exports[`Test Linear Elements Test bound text element should match styles for te
class="excalidraw-wysiwyg"
data-type="wysiwyg"
dir="auto"
style="position: absolute; display: inline-block; min-height: 1em; margin: 0px; padding: 0px; border: 0px; outline: 0; resize: none; background: transparent; overflow: hidden; z-index: var(--zIndex-wysiwyg); word-break: break-word; white-space: pre-wrap; overflow-wrap: break-word; box-sizing: content-box; width: 10.5px; height: 25px; left: 35px; top: 7.5px; transform: translate(0px, 0px) scale(1) rotate(0deg); text-align: center; vertical-align: middle; color: rgb(0, 0, 0); opacity: 1; filter: var(--theme-filter); max-height: -7.5px; font: Emoji 20px 20px; line-height: 1.25; font-family: Virgil, Segoe UI Emoji;"
style="position: absolute; display: inline-block; min-height: 1em; margin: 0px; padding: 0px; border: 0px; outline: 0; resize: none; background: transparent; overflow: hidden; z-index: var(--zIndex-wysiwyg); word-break: break-word; white-space: pre-wrap; overflow-wrap: break-word; box-sizing: content-box; width: 10.5px; height: 25px; left: 35px; top: 7.5px; transform-origin: 5px 12.5px; transform: translate(0px, 0px) scale(1) rotate(0deg) translate(0px, 0px); text-align: center; vertical-align: middle; color: rgb(0, 0, 0); opacity: 1; filter: var(--theme-filter); max-height: -7.5px; font: Emoji 20px 20px; line-height: 1.25; font-family: Virgil, Segoe UI Emoji;"
tabindex="0"
wrap="off"
/>
+19 -42
View File
@@ -1,10 +1,5 @@
import ReactDOM from "react-dom";
import {
render,
waitFor,
GlobalTestState,
createPasteEvent,
} from "./test-utils";
import { render, waitFor, GlobalTestState } from "./test-utils";
import { Pointer, Keyboard } from "./helpers/ui";
import ExcalidrawApp from "../excalidraw-app";
import { KEYS } from "../keys";
@@ -14,8 +9,6 @@ import {
} from "../element/textElement";
import { getElementBounds } from "../element";
import { NormalizedZoomValue } from "../types";
import { API } from "./helpers/api";
import { copyToClipboard } from "../clipboard";
const { h } = window;
@@ -42,28 +35,38 @@ const setClipboardText = (text: string) => {
});
};
const sendPasteEvent = (text?: string) => {
const clipboardEvent = createPasteEvent(
text || (() => window.navigator.clipboard.readText()),
);
const sendPasteEvent = () => {
const clipboardEvent = new Event("paste", {
bubbles: true,
cancelable: true,
composed: true,
});
// set `clipboardData` properties.
// @ts-ignore
clipboardEvent.clipboardData = {
getData: () => window.navigator.clipboard.readText(),
files: [],
};
document.dispatchEvent(clipboardEvent);
};
const pasteWithCtrlCmdShiftV = (text?: string) => {
const pasteWithCtrlCmdShiftV = () => {
Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => {
//triggering keydown with an empty clipboard
Keyboard.keyPress(KEYS.V);
//triggering paste event with faked clipboard
sendPasteEvent(text);
sendPasteEvent();
});
};
const pasteWithCtrlCmdV = (text?: string) => {
const pasteWithCtrlCmdV = () => {
Keyboard.withModifierKeys({ ctrl: true }, () => {
//triggering keydown with an empty clipboard
Keyboard.keyPress(KEYS.V);
//triggering paste event with faked clipboard
sendPasteEvent(text);
sendPasteEvent();
});
};
@@ -86,32 +89,6 @@ beforeEach(async () => {
});
});
describe("general paste behavior", () => {
it("should randomize seed on paste", async () => {
const rectangle = API.createElement({ type: "rectangle" });
const clipboardJSON = (await copyToClipboard([rectangle], null))!;
pasteWithCtrlCmdV(clipboardJSON);
await waitFor(() => {
expect(h.elements.length).toBe(1);
expect(h.elements[0].seed).not.toBe(rectangle.seed);
});
});
it("should retain seed on shift-paste", async () => {
const rectangle = API.createElement({ type: "rectangle" });
const clipboardJSON = (await copyToClipboard([rectangle], null))!;
// assert we don't randomize seed on shift-paste
pasteWithCtrlCmdShiftV(clipboardJSON);
await waitFor(() => {
expect(h.elements.length).toBe(1);
expect(h.elements[0].seed).toBe(rectangle.seed);
});
});
});
describe("paste text as single lines", () => {
it("should create an element for each line when copying with Ctrl/Cmd+V", async () => {
const text = "sajgfakfn\naaksfnknas\nakefnkasf";
+14 -7
View File
@@ -1,10 +1,5 @@
import ReactDOM from "react-dom";
import {
createPasteEvent,
GlobalTestState,
render,
waitFor,
} from "./test-utils";
import { GlobalTestState, render, waitFor } from "./test-utils";
import { UI, Pointer } from "./helpers/ui";
import { API } from "./helpers/api";
import { actionFlipHorizontal, actionFlipVertical } from "../actions";
@@ -685,7 +680,19 @@ describe("freedraw", () => {
describe("image", () => {
const createImage = async () => {
const sendPasteEvent = (file?: File) => {
const clipboardEvent = createPasteEvent("", file ? [file] : []);
const clipboardEvent = new Event("paste", {
bubbles: true,
cancelable: true,
composed: true,
});
// set `clipboardData` properties.
// @ts-ignore
clipboardEvent.clipboardData = {
getData: () => window.navigator.clipboard.readText(),
files: [file],
};
document.dispatchEvent(clipboardEvent);
};
+7 -16
View File
@@ -20,7 +20,7 @@ import { resize, rotate } from "./utils";
import {
getBoundTextElementPosition,
wrapText,
getBoundTextMaxWidth,
getMaxContainerWidth,
} from "../element/textElement";
import * as textElementUtils from "../element/textElement";
import { ROUNDNESS, VERTICAL_ALIGN } from "../constants";
@@ -729,11 +729,10 @@ describe("Test Linear Elements", () => {
type: "text",
x: 0,
y: 0,
text: wrapText(text, font, getBoundTextMaxWidth(container)),
text: wrapText(text, font, getMaxContainerWidth(container)),
containerId: container.id,
width: 30,
height: 20,
verticalAlign: VERTICAL_ALIGN.MIDDLE,
}) as ExcalidrawTextElementWithContainer;
container = {
@@ -1109,17 +1108,9 @@ describe("Test Linear Elements", () => {
`);
});
it("should render vertical align tool when element selected only for two pointer arrow", () => {
let arrow = createTwoPointerLinearElement("arrow");
createBoundTextElement(DEFAULT_TEXT, arrow);
API.setSelectedElements([arrow]);
expect(queryByTestId(container, "align-top")).not.toBeNull();
expect(queryByTestId(container, "align-middle")).not.toBeNull();
expect(queryByTestId(container, "align-bottom")).not.toBeNull();
arrow = createThreePointerLinearElement("arrow");
it("should not render vertical align tool when element selected", () => {
createTwoPointerLinearElement("arrow");
const arrow = h.elements[0] as ExcalidrawLinearElement;
createBoundTextElement(DEFAULT_TEXT, arrow);
API.setSelectedElements([arrow]);
@@ -1158,7 +1149,7 @@ describe("Test Linear Elements", () => {
expect(rect.x).toBe(400);
expect(rect.y).toBe(0);
expect(
wrapText(textElement.originalText, font, getBoundTextMaxWidth(arrow)),
wrapText(textElement.originalText, font, getMaxContainerWidth(arrow)),
).toMatchInlineSnapshot(`
"Online whiteboard collaboration
made easy"
@@ -1181,7 +1172,7 @@ describe("Test Linear Elements", () => {
false,
);
expect(
wrapText(textElement.originalText, font, getBoundTextMaxWidth(arrow)),
wrapText(textElement.originalText, font, getMaxContainerWidth(arrow)),
).toMatchInlineSnapshot(`
"Online whiteboard
collaboration made
-21
View File
@@ -190,24 +190,3 @@ export const toggleMenu = (container: HTMLElement) => {
// open menu
fireEvent.click(container.querySelector(".dropdown-menu-button")!);
};
export const createPasteEvent = (
text:
| string
| /* getData function */ ((type: string) => string | Promise<string>),
files?: File[],
) => {
return Object.assign(
new Event("paste", {
bubbles: true,
cancelable: true,
composed: true,
}),
{
clipboardData: {
getData: typeof text === "string" ? () => text : text,
files: files || [],
},
},
);
};
+4 -4
View File
@@ -29,9 +29,9 @@ import { isOverScrollBars } from "./scene";
import { MaybeTransformHandleType } from "./element/transformHandles";
import Library from "./data/library";
import type { FileSystemHandle } from "./data/filesystem";
import type { IMAGE_MIME_TYPES, MIME_TYPES } from "./constants";
import type { ALLOWED_IMAGE_MIME_TYPES, MIME_TYPES } from "./constants";
import { ContextMenuItems } from "./components/ContextMenu";
import { Merge, ForwardRef, ValueOf } from "./utility-types";
import { Merge, ForwardRef } from "./utility-types";
import React from "react";
export type Point = Readonly<RoughPoint>;
@@ -60,7 +60,7 @@ export type DataURL = string & { _brand: "DataURL" };
export type BinaryFileData = {
mimeType:
| ValueOf<typeof IMAGE_MIME_TYPES>
| typeof ALLOWED_IMAGE_MIME_TYPES[number]
// future user or unknown file type
| typeof MIME_TYPES.binary;
id: FileId;
@@ -419,7 +419,7 @@ export type AppClassProperties = {
FileId,
{
image: HTMLImageElement | Promise<HTMLImageElement>;
mimeType: ValueOf<typeof IMAGE_MIME_TYPES>;
mimeType: typeof ALLOWED_IMAGE_MIME_TYPES[number];
}
>;
files: BinaryFiles;