Compare commits

...
Author SHA1 Message Date
Aakansha Doshi e75f215838 Add specs 2023-04-27 13:44:07 +05:30
Aakansha Doshi 2264a66241 Merge remote-tracking branch 'origin/master' into aakansha-labelled-arrow-align 2023-04-27 12:57:47 +05:30
Aakansha Doshi 57a700f82e fix coords 2023-04-27 12:45:47 +05:30
Aakansha DoshiandGitHub 45a57d70de fix: don't refresh dimensions for text containers on font load (#6523) 2023-04-26 21:35:06 +05:30
Aakansha Doshi 66ccc254f0 feat: vertical align for labelled arrows 2023-04-26 18:01:49 +05:30
Aakansha DoshiandGitHub da8dd389a9 fix: cleanup getMaxContainerHeight and getMaxContainerWidth (#6519)
* fix: cleanup getMaxContainerHeight and getMaxContainerWidth

* rename getMaxContainerWidth -> getBoundTextMaxMaxWidth and getMaxContainerHeight -> getBoundTextMaxHeight

* add specs
2023-04-25 18:06:23 +05:30
Aakansha DoshiandGitHub dae81c0a2c fix: cleanup redrawTextBoundingBox (#6518)
* chore: cleanup redrawTextBoundingBox

* fix
2023-04-25 17:57:53 +05:30
suwalkanishkaandGitHub 1e9943323a style: fix font family inconsistencies (#6501)
style: font fix for four components

The browser default font was showing up in various locations. Fixed them to show the desired ui font.
2023-04-25 17:20:19 +05:30
NainterceptorandGitHub 1815cf3213 build: Add version tags to Docker build (#6508)
ci: Add version tags
2023-04-25 16:51:25 +05:30
David LuzarandGitHub d35386755f feat: retain seed on shift-paste (#6509)
thanks for the review 👍
2023-04-24 10:26:21 +02:00
zsviczianandGitHub 9d5cfbbfb7 fix: text jumps when editing on Android Chrome (#6503)
* debug logging

* debug

* debugging

* Update textWysiwyg.tsx

* Update textWysiwyg.tsx

* extended debug information

* debug

* debug

* trace

* further debug

* don't drag while editing

* removing all console.logs

* revert all changes to textWysiwyt.tsx

* updated comment
2023-04-22 14:17:13 +02:00
David LuzarandGitHub fee760d38c feat: allow avif, jfif, webp, bmp, ico image types (#6500
* feat: allow `avif`, `jfif`, `webp`, `bmp`, `ico` image types

* dedupe for SSOT

* more SSOT
2023-04-21 22:53:49 +02:00
Aakansha DoshiandGitHub 2a4799d8c8 chore: upgrade excalidraw version to 0.15.2 (#6496)
chore: updragde excalidraw version to 0.15.2
2023-04-20 17:40:08 +05:30
29 changed files with 325 additions and 191 deletions
+12 -2
View File
@@ -12,14 +12,24 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 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 - name: Login to DockerHub
uses: docker/login-action@v2 uses: docker/login-action@v2
with: with:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push - name: Build and push
uses: docker/build-push-action@v3 uses: docker/build-push-action@v4
with: with:
context: . context: .
push: true push: true
tags: excalidraw/excalidraw:latest tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+1 -1
View File
@@ -18,7 +18,7 @@
"@docusaurus/core": "2.2.0", "@docusaurus/core": "2.2.0",
"@docusaurus/preset-classic": "2.2.0", "@docusaurus/preset-classic": "2.2.0",
"@docusaurus/theme-live-codeblock": "2.2.0", "@docusaurus/theme-live-codeblock": "2.2.0",
"@excalidraw/excalidraw": "0.15.0", "@excalidraw/excalidraw": "0.15.2",
"@mdx-js/react": "^1.6.22", "@mdx-js/react": "^1.6.22",
"clsx": "^1.2.1", "clsx": "^1.2.1",
"docusaurus-plugin-sass": "0.2.3", "docusaurus-plugin-sass": "0.2.3",
+4 -4
View File
@@ -1631,10 +1631,10 @@
url-loader "^4.1.1" url-loader "^4.1.1"
webpack "^5.73.0" webpack "^5.73.0"
"@excalidraw/excalidraw@0.15.0": "@excalidraw/excalidraw@0.15.2":
version "0.15.0" version "0.15.2"
resolved "https://registry.yarnpkg.com/@excalidraw/excalidraw/-/excalidraw-0.15.0.tgz#47170de8d3ff006e9d09dfede2815682b0d4485b" resolved "https://registry.yarnpkg.com/@excalidraw/excalidraw/-/excalidraw-0.15.2.tgz#7dba4f6e10c52015a007efb75a9fc1afe598574c"
integrity sha512-PJmh1VcuRHG4l+Zgt9qhezxrJ16tYCZFZ8if5IEfmTL9A/7c5mXxY/qrPTqiGlVC7jYs+ciePXQ0YUDzfOfbzw== integrity sha512-rTI02kgWSTXiUdIkBxt9u/581F3eXcqQgJdIxmz54TFtG3ughoxO5fr4t7Fr2LZIturBPqfocQHGKZ0t2KLKgw==
"@hapi/hoek@^9.0.0": "@hapi/hoek@^9.0.0":
version "9.3.0" version "9.3.0"
+1 -1
View File
@@ -18,7 +18,7 @@ export const actionCopy = register({
perform: (elements, appState, _, app) => { perform: (elements, appState, _, app) => {
const selectedElements = getSelectedElements(elements, appState, true); const selectedElements = getSelectedElements(elements, appState, true);
copyToClipboard(selectedElements, appState, app.files); copyToClipboard(selectedElements, app.files);
return { return {
commitToHistory: false, commitToHistory: false,
+27 -11
View File
@@ -2,12 +2,12 @@ import {
ExcalidrawElement, ExcalidrawElement,
NonDeletedExcalidrawElement, NonDeletedExcalidrawElement,
} from "./element/types"; } from "./element/types";
import { AppState, BinaryFiles } from "./types"; import { BinaryFiles } from "./types";
import { SVG_EXPORT_TAG } from "./scene/export"; import { SVG_EXPORT_TAG } from "./scene/export";
import { tryParseSpreadsheet, Spreadsheet, VALID_SPREADSHEET } from "./charts"; import { tryParseSpreadsheet, Spreadsheet, VALID_SPREADSHEET } from "./charts";
import { EXPORT_DATA_TYPES, MIME_TYPES } from "./constants"; import { EXPORT_DATA_TYPES, MIME_TYPES } from "./constants";
import { isInitializedImageElement } from "./element/typeChecks"; import { isInitializedImageElement } from "./element/typeChecks";
import { isPromiseLike } from "./utils"; import { isPromiseLike, isTestEnv } from "./utils";
type ElementsClipboard = { type ElementsClipboard = {
type: typeof EXPORT_DATA_TYPES.excalidrawClipboard; type: typeof EXPORT_DATA_TYPES.excalidrawClipboard;
@@ -55,24 +55,40 @@ const clipboardContainsElements = (
export const copyToClipboard = async ( export const copyToClipboard = async (
elements: readonly NonDeletedExcalidrawElement[], elements: readonly NonDeletedExcalidrawElement[],
appState: AppState,
files: BinaryFiles | null, 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 // select binded text elements when copying
const contents: ElementsClipboard = { const contents: ElementsClipboard = {
type: EXPORT_DATA_TYPES.excalidrawClipboard, type: EXPORT_DATA_TYPES.excalidrawClipboard,
elements, elements,
files: files files: files ? _files : undefined,
? 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); const json = JSON.stringify(contents);
if (isTestEnv()) {
return json;
}
CLIPBOARD = json; CLIPBOARD = json;
try { try {
PREFER_APP_CLIPBOARD = false; PREFER_APP_CLIPBOARD = false;
await copyTextToSystemClipboard(json); await copyTextToSystemClipboard(json);
+15 -2
View File
@@ -60,6 +60,7 @@ import {
ENV, ENV,
EVENT, EVENT,
GRID_SIZE, GRID_SIZE,
IMAGE_MIME_TYPES,
IMAGE_RENDER_TIMEOUT, IMAGE_RENDER_TIMEOUT,
isAndroid, isAndroid,
isBrave, isBrave,
@@ -1589,6 +1590,7 @@ class App extends React.Component<AppProps, AppState> {
elements: data.elements, elements: data.elements,
files: data.files || null, files: data.files || null,
position: "cursor", position: "cursor",
retainSeed: isPlainPaste,
}); });
} else if (data.text) { } else if (data.text) {
this.addTextFromPaste(data.text, isPlainPaste); this.addTextFromPaste(data.text, isPlainPaste);
@@ -1602,6 +1604,7 @@ class App extends React.Component<AppProps, AppState> {
elements: readonly ExcalidrawElement[]; elements: readonly ExcalidrawElement[];
files: BinaryFiles | null; files: BinaryFiles | null;
position: { clientX: number; clientY: number } | "cursor" | "center"; position: { clientX: number; clientY: number } | "cursor" | "center";
retainSeed?: boolean;
}) => { }) => {
const elements = restoreElements(opts.elements, null); const elements = restoreElements(opts.elements, null);
const [minX, minY, maxX, maxY] = getCommonBounds(elements); const [minX, minY, maxX, maxY] = getCommonBounds(elements);
@@ -1639,6 +1642,9 @@ class App extends React.Component<AppProps, AppState> {
y: element.y + gridY - minY, y: element.y + gridY - minY,
}); });
}), }),
{
randomizeSeed: !opts.retainSeed,
},
); );
const nextElements = [ const nextElements = [
@@ -4720,7 +4726,12 @@ class App extends React.Component<AppProps, AppState> {
pointerDownState.drag.hasOccurred = true; pointerDownState.drag.hasOccurred = true;
// prevent dragging even if we're no longer holding cmd/ctrl otherwise // prevent dragging even if we're no longer holding cmd/ctrl otherwise
// it would have weird results (stuff jumping all over the screen) // it would have weird results (stuff jumping all over the screen)
if (selectedElements.length > 0 && !pointerDownState.withCmdOrCtrl) { // Checking for editingElement to avoid jump while editing on mobile #6503
if (
selectedElements.length > 0 &&
!pointerDownState.withCmdOrCtrl &&
!this.state.editingElement
) {
const [dragX, dragY] = getGridPoint( const [dragX, dragY] = getGridPoint(
pointerCoords.x - pointerDownState.drag.offset.x, pointerCoords.x - pointerDownState.drag.offset.x,
pointerCoords.y - pointerDownState.drag.offset.y, pointerCoords.y - pointerDownState.drag.offset.y,
@@ -5743,7 +5754,9 @@ class App extends React.Component<AppProps, AppState> {
const imageFile = await fileOpen({ const imageFile = await fileOpen({
description: "Image", description: "Image",
extensions: ["jpg", "png", "svg", "gif"], extensions: Object.keys(
IMAGE_MIME_TYPES,
) as (keyof typeof IMAGE_MIME_TYPES)[],
}); });
const imageElement = this.createImageElement({ const imageElement = this.createImageElement({
+1
View File
@@ -183,6 +183,7 @@
width: 100%; width: 100%;
margin: 0; margin: 0;
font-size: 0.875rem; font-size: 0.875rem;
font-family: inherit;
background-color: transparent; background-color: transparent;
color: var(--text-primary-color); color: var(--text-primary-color);
border: 0; border: 0;
+1
View File
@@ -30,6 +30,7 @@
background-color: transparent; background-color: transparent;
border: none; border: none;
white-space: nowrap; white-space: nowrap;
font-family: inherit;
display: grid; display: grid;
grid-template-columns: 1fr 0.2fr; grid-template-columns: 1fr 0.2fr;
+1 -1
View File
@@ -102,7 +102,7 @@ const LibraryMenuItems = ({
...item, ...item,
// duplicate each library item before inserting on canvas to confine // duplicate each library item before inserting on canvas to confine
// ids and bindings to each library item. See #6465 // ids and bindings to each library item. See #6465
elements: duplicateElements(item.elements), elements: duplicateElements(item.elements, { randomizeSeed: true }),
}; };
}); });
}; };
+3
View File
@@ -2,6 +2,9 @@
// container in body where the actual tooltip is appended to // container in body where the actual tooltip is appended to
.excalidraw-tooltip { .excalidraw-tooltip {
--ui-font: Assistant, system-ui, BlinkMacSystemFont, -apple-system, Segoe UI,
Roboto, Helvetica, Arial, sans-serif;
font-family: var(--ui-font);
position: fixed; position: fixed;
z-index: 1000; z-index: 1000;
+16 -16
View File
@@ -105,20 +105,30 @@ export const CANVAS_ONLY_ACTIONS = ["selectAll"];
export const GRID_SIZE = 20; // TODO make it configurable? export const GRID_SIZE = 20; // TODO make it configurable?
export const MIME_TYPES = { export const IMAGE_MIME_TYPES = {
excalidraw: "application/vnd.excalidraw+json",
excalidrawlib: "application/vnd.excalidrawlib+json",
json: "application/json",
svg: "image/svg+xml", svg: "image/svg+xml",
"excalidraw.svg": "image/svg+xml",
png: "image/png", png: "image/png",
"excalidraw.png": "image/png",
jpg: "image/jpeg", jpg: "image/jpeg",
gif: "image/gif", gif: "image/gif",
webp: "image/webp", webp: "image/webp",
bmp: "image/bmp", bmp: "image/bmp",
ico: "image/x-icon", 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", binary: "application/octet-stream",
// image
...IMAGE_MIME_TYPES,
} as const; } as const;
export const EXPORT_DATA_TYPES = { export const EXPORT_DATA_TYPES = {
@@ -189,16 +199,6 @@ export const DEFAULT_EXPORT_PADDING = 10; // px
export const DEFAULT_MAX_IMAGE_WIDTH_OR_HEIGHT = 1440; 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 MAX_ALLOWED_FILE_BYTES = 2 * 1024 * 1024;
export const SVG_NS = "http://www.w3.org/2000/svg"; export const SVG_NS = "http://www.w3.org/2000/svg";
+2
View File
@@ -354,6 +354,7 @@
border-radius: var(--space-factor); border-radius: var(--space-factor);
border: 1px solid var(--button-gray-2); border: 1px solid var(--button-gray-2);
font-size: 0.8rem; font-size: 0.8rem;
font-family: inherit;
outline: none; outline: none;
appearance: none; appearance: none;
background-image: var(--dropdown-icon); background-image: var(--dropdown-icon);
@@ -413,6 +414,7 @@
bottom: 30px; bottom: 30px;
transform: translateX(-50%); transform: translateX(-50%);
pointer-events: all; pointer-events: all;
font-family: inherit;
&:hover { &:hover {
background-color: var(--button-hover-bg); background-color: var(--button-hover-bg);
+4 -6
View File
@@ -1,6 +1,6 @@
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { cleanAppStateForExport } from "../appState"; import { cleanAppStateForExport } from "../appState";
import { ALLOWED_IMAGE_MIME_TYPES, MIME_TYPES } from "../constants"; import { IMAGE_MIME_TYPES, MIME_TYPES } from "../constants";
import { clearElementsForExport } from "../element"; import { clearElementsForExport } from "../element";
import { ExcalidrawElement, FileId } from "../element/types"; import { ExcalidrawElement, FileId } from "../element/types";
import { CanvasError } from "../errors"; import { CanvasError } from "../errors";
@@ -117,11 +117,9 @@ export const isImageFileHandle = (handle: FileSystemHandle | null) => {
export const isSupportedImageFile = ( export const isSupportedImageFile = (
blob: Blob | null | undefined, blob: Blob | null | undefined,
): blob is Blob & { type: typeof ALLOWED_IMAGE_MIME_TYPES[number] } => { ): blob is Blob & { type: ValueOf<typeof IMAGE_MIME_TYPES> } => {
const { type } = blob || {}; const { type } = blob || {};
return ( return !!type && (Object.values(IMAGE_MIME_TYPES) as string[]).includes(type);
!!type && (ALLOWED_IMAGE_MIME_TYPES as readonly string[]).includes(type)
);
}; };
export const loadSceneOrLibraryFromBlob = async ( export const loadSceneOrLibraryFromBlob = async (
@@ -157,7 +155,7 @@ export const loadSceneOrLibraryFromBlob = async (
}, },
localAppState, localAppState,
localElements, localElements,
{ repairBindings: true, refreshDimensions: true }, { repairBindings: true, refreshDimensions: false },
), ),
}; };
} else if (isValidLibrary(data)) { } else if (isValidLibrary(data)) {
+1 -10
View File
@@ -8,16 +8,7 @@ import { EVENT, MIME_TYPES } from "../constants";
import { AbortError } from "../errors"; import { AbortError } from "../errors";
import { debounce } from "../utils"; import { debounce } from "../utils";
type FILE_EXTENSION = type FILE_EXTENSION = Exclude<keyof typeof MIME_TYPES, "binary">;
| "gif"
| "jpg"
| "png"
| "excalidraw.png"
| "svg"
| "excalidraw.svg"
| "json"
| "excalidraw"
| "excalidrawlib";
const INPUT_CHANGE_INTERVAL_MS = 500; const INPUT_CHANGE_INTERVAL_MS = 500;
+15 -1
View File
@@ -40,7 +40,11 @@ import { isBindingElement } from "./typeChecks";
import { shouldRotateWithDiscreteAngle } from "../keys"; import { shouldRotateWithDiscreteAngle } from "../keys";
import { getBoundTextElement, handleBindTextResize } from "./textElement"; import { getBoundTextElement, handleBindTextResize } from "./textElement";
import { getShapeForElement } from "../renderer/renderElement"; import { getShapeForElement } from "../renderer/renderElement";
import { DRAGGING_THRESHOLD } from "../constants"; import {
BOUND_TEXT_PADDING,
DRAGGING_THRESHOLD,
VERTICAL_ALIGN,
} from "../constants";
import { Mutable } from "../utility-types"; import { Mutable } from "../utility-types";
const editorMidPointsCache: { const editorMidPointsCache: {
@@ -1304,6 +1308,16 @@ export class LinearElementEditor {
} }
x = midSegmentMidpoint[0] - boundTextElement.width / 2; x = midSegmentMidpoint[0] - boundTextElement.width / 2;
y = midSegmentMidpoint[1] - boundTextElement.height / 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 }; return { x, y };
}; };
+17 -4
View File
@@ -20,7 +20,7 @@ import {
isTestEnv, isTestEnv,
} from "../utils"; } from "../utils";
import { randomInteger, randomId } from "../random"; import { randomInteger, randomId } from "../random";
import { mutateElement, newElementWith } from "./mutateElement"; import { bumpVersion, mutateElement, newElementWith } from "./mutateElement";
import { getNewGroupIdsForDuplication } from "../groups"; import { getNewGroupIdsForDuplication } from "../groups";
import { AppState } from "../types"; import { AppState } from "../types";
import { getElementAbsoluteCoords } from "."; import { getElementAbsoluteCoords } from ".";
@@ -33,7 +33,7 @@ import {
measureText, measureText,
normalizeText, normalizeText,
wrapText, wrapText,
getMaxContainerWidth, getBoundTextMaxWidth,
getDefaultLineHeight, getDefaultLineHeight,
} from "./textElement"; } from "./textElement";
import { import {
@@ -310,7 +310,7 @@ export const refreshTextDimensions = (
text = wrapText( text = wrapText(
text, text,
getFontString(textElement), getFontString(textElement),
getMaxContainerWidth(container), getBoundTextMaxWidth(container),
); );
} }
const dimensions = getAdjustedDimensions(textElement, text); const dimensions = getAdjustedDimensions(textElement, text);
@@ -539,8 +539,16 @@ export const duplicateElement = <TElement extends ExcalidrawElement>(
* it's advised to supply the whole elements array, or sets of elements that * 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 * are encapsulated (such as library items), if the purpose is to retain
* bindings to the cloned elements intact. * bindings to the cloned elements intact.
*
* NOTE by default does not randomize or regenerate anything except the id.
*/ */
export const duplicateElements = (elements: readonly ExcalidrawElement[]) => { export const duplicateElements = (
elements: readonly ExcalidrawElement[],
opts?: {
/** NOTE also updates version flags and `updated` */
randomizeSeed: boolean;
},
) => {
const clonedElements: ExcalidrawElement[] = []; const clonedElements: ExcalidrawElement[] = [];
const origElementsMap = arrayToMap(elements); const origElementsMap = arrayToMap(elements);
@@ -574,6 +582,11 @@ export const duplicateElements = (elements: readonly ExcalidrawElement[]) => {
clonedElement.id = maybeGetNewId(element.id)!; clonedElement.id = maybeGetNewId(element.id)!;
if (opts?.randomizeSeed) {
clonedElement.seed = randomInteger();
bumpVersion(clonedElement);
}
if (clonedElement.groupIds) { if (clonedElement.groupIds) {
clonedElement.groupIds = clonedElement.groupIds.map((groupId) => { clonedElement.groupIds = clonedElement.groupIds.map((groupId) => {
if (!groupNewIdsMap.has(groupId)) { if (!groupNewIdsMap.has(groupId)) {
+7 -7
View File
@@ -44,10 +44,10 @@ import {
getBoundTextElementId, getBoundTextElementId,
getContainerElement, getContainerElement,
handleBindTextResize, handleBindTextResize,
getMaxContainerWidth, getBoundTextMaxWidth,
getApproxMinLineHeight, getApproxMinLineHeight,
measureText, measureText,
getMaxContainerHeight, getBoundTextMaxHeight,
} from "./textElement"; } from "./textElement";
export const normalizeAngle = (angle: number): number => { export const normalizeAngle = (angle: number): number => {
@@ -204,7 +204,7 @@ const measureFontSizeFromWidth = (
if (hasContainer) { if (hasContainer) {
const container = getContainerElement(element); const container = getContainerElement(element);
if (container) { if (container) {
width = getMaxContainerWidth(container); width = getBoundTextMaxWidth(container);
} }
} }
const nextFontSize = element.fontSize * (nextWidth / width); const nextFontSize = element.fontSize * (nextWidth / width);
@@ -435,8 +435,8 @@ export const resizeSingleElement = (
const nextFont = measureFontSizeFromWidth( const nextFont = measureFontSizeFromWidth(
boundTextElement, boundTextElement,
getMaxContainerWidth(updatedElement), getBoundTextMaxWidth(updatedElement),
getMaxContainerHeight(updatedElement), getBoundTextMaxHeight(updatedElement, boundTextElement),
); );
if (nextFont === null) { if (nextFont === null) {
return; return;
@@ -718,10 +718,10 @@ const resizeMultipleElements = (
const metrics = measureFontSizeFromWidth( const metrics = measureFontSizeFromWidth(
boundTextElement ?? (element.orig as ExcalidrawTextElement), boundTextElement ?? (element.orig as ExcalidrawTextElement),
boundTextElement boundTextElement
? getMaxContainerWidth(updatedElement) ? getBoundTextMaxWidth(updatedElement)
: updatedElement.width, : updatedElement.width,
boundTextElement boundTextElement
? getMaxContainerHeight(updatedElement) ? getBoundTextMaxHeight(updatedElement, boundTextElement)
: updatedElement.height, : updatedElement.height,
); );
+48 -11
View File
@@ -3,15 +3,15 @@ import { API } from "../tests/helpers/api";
import { import {
computeContainerDimensionForBoundText, computeContainerDimensionForBoundText,
getContainerCoords, getContainerCoords,
getMaxContainerWidth, getBoundTextMaxWidth,
getMaxContainerHeight, getBoundTextMaxHeight,
wrapText, wrapText,
detectLineHeight, detectLineHeight,
getLineHeightInPx, getLineHeightInPx,
getDefaultLineHeight, getDefaultLineHeight,
parseTokens, parseTokens,
} from "./textElement"; } from "./textElement";
import { FontString } from "./types"; import { ExcalidrawTextElementWithContainer, FontString } from "./types";
describe("Test wrapText", () => { describe("Test wrapText", () => {
const font = "20px Cascadia, width: Segoe UI Emoji" as FontString; const font = "20px Cascadia, width: Segoe UI Emoji" as FontString;
@@ -311,7 +311,7 @@ describe("Test measureText", () => {
}); });
}); });
describe("Test getMaxContainerWidth", () => { describe("Test getBoundTextMaxWidth", () => {
const params = { const params = {
width: 178, width: 178,
height: 194, height: 194,
@@ -319,39 +319,76 @@ describe("Test measureText", () => {
it("should return max width when container is rectangle", () => { it("should return max width when container is rectangle", () => {
const container = API.createElement({ type: "rectangle", ...params }); const container = API.createElement({ type: "rectangle", ...params });
expect(getMaxContainerWidth(container)).toBe(168); expect(getBoundTextMaxWidth(container)).toBe(168);
}); });
it("should return max width when container is ellipse", () => { it("should return max width when container is ellipse", () => {
const container = API.createElement({ type: "ellipse", ...params }); const container = API.createElement({ type: "ellipse", ...params });
expect(getMaxContainerWidth(container)).toBe(116); expect(getBoundTextMaxWidth(container)).toBe(116);
}); });
it("should return max width when container is diamond", () => { it("should return max width when container is diamond", () => {
const container = API.createElement({ type: "diamond", ...params }); const container = API.createElement({ type: "diamond", ...params });
expect(getMaxContainerWidth(container)).toBe(79); expect(getBoundTextMaxWidth(container)).toBe(79);
}); });
}); });
describe("Test getMaxContainerHeight", () => { describe("Test getBoundTextMaxHeight", () => {
const params = { const params = {
width: 178, width: 178,
height: 194, 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", () => { it("should return max height when container is rectangle", () => {
const container = API.createElement({ type: "rectangle", ...params }); const container = API.createElement({ type: "rectangle", ...params });
expect(getMaxContainerHeight(container)).toBe(184); expect(getBoundTextMaxHeight(container, boundTextElement)).toBe(184);
}); });
it("should return max height when container is ellipse", () => { it("should return max height when container is ellipse", () => {
const container = API.createElement({ type: "ellipse", ...params }); const container = API.createElement({ type: "ellipse", ...params });
expect(getMaxContainerHeight(container)).toBe(127); expect(getBoundTextMaxHeight(container, boundTextElement)).toBe(127);
}); });
it("should return max height when container is diamond", () => { it("should return max height when container is diamond", () => {
const container = API.createElement({ type: "diamond", ...params }); const container = API.createElement({ type: "diamond", ...params });
expect(getMaxContainerHeight(container)).toBe(87); 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,
);
}); });
}); });
}); });
+35 -42
View File
@@ -65,7 +65,7 @@ export const redrawTextBoundingBox = (
boundTextUpdates.text = textElement.text; boundTextUpdates.text = textElement.text;
if (container) { if (container) {
maxWidth = getMaxContainerWidth(container); maxWidth = getBoundTextMaxWidth(container);
boundTextUpdates.text = wrapText( boundTextUpdates.text = wrapText(
textElement.originalText, textElement.originalText,
getFontString(textElement), getFontString(textElement),
@@ -83,27 +83,22 @@ export const redrawTextBoundingBox = (
boundTextUpdates.baseline = metrics.baseline; boundTextUpdates.baseline = metrics.baseline;
if (container) { if (container) {
if (isArrowElement(container)) { const containerDims = getContainerDims(container);
const centerX = textElement.x + textElement.width / 2; const maxContainerHeight = getBoundTextMaxHeight(
const centerY = textElement.y + textElement.height / 2; container,
const diffWidth = metrics.width - textElement.width; textElement as ExcalidrawTextElementWithContainer,
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; let nextHeight = containerDims.height;
if (metrics.height > maxContainerHeight) { if (metrics.height > maxContainerHeight) {
nextHeight = computeContainerDimensionForBoundText( nextHeight = computeContainerDimensionForBoundText(
metrics.height, metrics.height,
container.type, container.type,
); );
mutateElement(container, { height: nextHeight }); mutateElement(container, { height: nextHeight });
maxContainerHeight = getMaxContainerHeight(container); updateOriginalContainerCache(container.id, nextHeight);
updateOriginalContainerCache(container.id, nextHeight); }
} if (!isArrowElement(container)) {
const updatedTextElement = { const updatedTextElement = {
...textElement, ...textElement,
...boundTextUpdates, ...boundTextUpdates,
@@ -183,8 +178,11 @@ export const handleBindTextResize = (
let nextHeight = textElement.height; let nextHeight = textElement.height;
let nextWidth = textElement.width; let nextWidth = textElement.width;
const containerDims = getContainerDims(container); const containerDims = getContainerDims(container);
const maxWidth = getMaxContainerWidth(container); const maxWidth = getBoundTextMaxWidth(container);
const maxHeight = getMaxContainerHeight(container); const maxHeight = getBoundTextMaxHeight(
container,
textElement as ExcalidrawTextElementWithContainer,
);
let containerHeight = containerDims.height; let containerHeight = containerDims.height;
let nextBaseLine = textElement.baseline; let nextBaseLine = textElement.baseline;
if (transformHandleType !== "n" && transformHandleType !== "s") { if (transformHandleType !== "n" && transformHandleType !== "s") {
@@ -256,8 +254,8 @@ export const computeBoundTextPosition = (
); );
} }
const containerCoords = getContainerCoords(container); const containerCoords = getContainerCoords(container);
const maxContainerHeight = getMaxContainerHeight(container); const maxContainerHeight = getBoundTextMaxHeight(container, boundTextElement);
const maxContainerWidth = getMaxContainerWidth(container); const maxContainerWidth = getBoundTextMaxWidth(container);
let x; let x;
let y; let y;
@@ -797,7 +795,11 @@ export const shouldAllowVerticalAlign = (
const hasBoundContainer = isBoundToContainer(element); const hasBoundContainer = isBoundToContainer(element);
if (hasBoundContainer) { if (hasBoundContainer) {
const container = getContainerElement(element); const container = getContainerElement(element);
if (isTextElement(element) && isArrowElement(container)) { if (
isTextElement(element) &&
isArrowElement(container) &&
container.points.length > 2
) {
return false; return false;
} }
return true; return true;
@@ -890,18 +892,10 @@ export const computeContainerDimensionForBoundText = (
return dimension + padding; return dimension + padding;
}; };
export const getMaxContainerWidth = (container: ExcalidrawElement) => { export const getBoundTextMaxWidth = (container: ExcalidrawElement) => {
const width = getContainerDims(container).width; const width = getContainerDims(container).width;
if (isArrowElement(container)) { if (isArrowElement(container)) {
const containerWidth = width - BOUND_TEXT_PADDING * 8 * 2; return 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") { if (container.type === "ellipse") {
@@ -918,16 +912,15 @@ export const getMaxContainerWidth = (container: ExcalidrawElement) => {
return width - BOUND_TEXT_PADDING * 2; return width - BOUND_TEXT_PADDING * 2;
}; };
export const getMaxContainerHeight = (container: ExcalidrawElement) => { export const getBoundTextMaxHeight = (
container: ExcalidrawElement,
boundTextElement: ExcalidrawTextElementWithContainer,
) => {
const height = getContainerDims(container).height; const height = getContainerDims(container).height;
if (isArrowElement(container)) { if (isArrowElement(container)) {
const containerHeight = height - BOUND_TEXT_PADDING * 8 * 2; const containerHeight = height - BOUND_TEXT_PADDING * 8 * 2;
if (containerHeight <= 0) { if (containerHeight <= 0) {
const boundText = getBoundTextElement(container); return boundTextElement.height;
if (boundText) {
return boundText.height;
}
return BOUND_TEXT_PADDING * 8 * 2;
} }
return height; return height;
} }
+13 -13
View File
@@ -32,8 +32,8 @@ import {
normalizeText, normalizeText,
redrawTextBoundingBox, redrawTextBoundingBox,
wrapText, wrapText,
getMaxContainerHeight, getBoundTextMaxHeight,
getMaxContainerWidth, getBoundTextMaxWidth,
computeContainerDimensionForBoundText, computeContainerDimensionForBoundText,
detectLineHeight, detectLineHeight,
} from "./textElement"; } from "./textElement";
@@ -205,8 +205,11 @@ export const textWysiwyg = ({
} }
} }
maxWidth = getMaxContainerWidth(container); maxWidth = getBoundTextMaxWidth(container);
maxHeight = getMaxContainerHeight(container); maxHeight = getBoundTextMaxHeight(
container,
updatedTextElement as ExcalidrawTextElementWithContainer,
);
// autogrow container height if text exceeds // autogrow container height if text exceeds
if (!isArrowElement(container) && textElementHeight > maxHeight) { if (!isArrowElement(container) && textElementHeight > maxHeight) {
@@ -230,17 +233,14 @@ export const textWysiwyg = ({
); );
mutateElement(container, { height: targetContainerHeight }); mutateElement(container, { height: targetContainerHeight });
} }
// Start pushing text upward until a diff of 30px (padding) // update y coord as you type, not needed for arrow as we calculate
// is reached // position from the container element for editor and canvas when rendering labelled arrows
else { else if (!isArrowElement(container)) {
const containerCoords = getContainerCoords(container); const containerCoords = getContainerCoords(container);
// vertically center align the text // vertically center align the text
if (verticalAlign === VERTICAL_ALIGN.MIDDLE) { if (verticalAlign === VERTICAL_ALIGN.MIDDLE) {
if (!isArrowElement(container)) { coordY = containerCoords.y + maxHeight / 2 - textElementHeight / 2;
coordY =
containerCoords.y + maxHeight / 2 - textElementHeight / 2;
}
} }
if (verticalAlign === VERTICAL_ALIGN.BOTTOM) { if (verticalAlign === VERTICAL_ALIGN.BOTTOM) {
coordY = containerCoords.y + (maxHeight - textElementHeight); coordY = containerCoords.y + (maxHeight - textElementHeight);
@@ -377,7 +377,7 @@ export const textWysiwyg = ({
const wrappedText = wrapText( const wrappedText = wrapText(
`${editable.value}${data}`, `${editable.value}${data}`,
font, font,
getMaxContainerWidth(container), getBoundTextMaxWidth(container),
); );
const width = getTextWidth(wrappedText, font); const width = getTextWidth(wrappedText, font);
editable.style.width = `${width}px`; editable.style.width = `${width}px`;
@@ -394,7 +394,7 @@ export const textWysiwyg = ({
const wrappedText = wrapText( const wrappedText = wrapText(
normalizeText(editable.value), normalizeText(editable.value),
font, font,
getMaxContainerWidth(container!), getBoundTextMaxWidth(container!),
); );
const { width, height } = measureText( const { width, height } = measureText(
wrappedText, wrappedText,
+1 -1
View File
@@ -263,7 +263,7 @@ export const loadScene = async (
await importFromBackend(id, privateKey), await importFromBackend(id, privateKey),
localDataState?.appState, localDataState?.appState,
localDataState?.elements, localDataState?.elements,
{ repairBindings: true, refreshDimensions: true }, { repairBindings: true, refreshDimensions: false },
); );
} else { } else {
data = restore(localDataState || null, null, null, { data = restore(localDataState || null, null, null, {
+1 -9
View File
@@ -220,15 +220,7 @@ export const exportToClipboard = async (
} else if (opts.type === "png") { } else if (opts.type === "png") {
await copyBlobToClipboardAsPng(exportToBlob(opts)); await copyBlobToClipboardAsPng(exportToBlob(opts));
} else if (opts.type === "json") { } else if (opts.type === "json") {
const appState = { await copyToClipboard(opts.elements, opts.files);
offsetTop: 0,
offsetLeft: 0,
width: 0,
height: 0,
...getDefaultAppState(),
...opts.appState,
};
await copyToClipboard(opts.elements, appState, opts.files);
} else { } else {
throw new Error("Invalid export type"); throw new Error("Invalid export type");
} }
+7 -4
View File
@@ -44,8 +44,8 @@ import {
getContainerCoords, getContainerCoords,
getContainerElement, getContainerElement,
getLineHeightInPx, getLineHeightInPx,
getMaxContainerHeight, getBoundTextMaxHeight,
getMaxContainerWidth, getBoundTextMaxWidth,
} from "../element/textElement"; } from "../element/textElement";
import { LinearElementEditor } from "../element/linearElementEditor"; import { LinearElementEditor } from "../element/linearElementEditor";
@@ -868,14 +868,17 @@ const drawElementFromCanvas = (
"true" && "true" &&
hasBoundTextElement(element) hasBoundTextElement(element)
) { ) {
const textElement = getBoundTextElement(
element,
) as ExcalidrawTextElementWithContainer;
const coords = getContainerCoords(element); const coords = getContainerCoords(element);
context.strokeStyle = "#c92a2a"; context.strokeStyle = "#c92a2a";
context.lineWidth = 3; context.lineWidth = 3;
context.strokeRect( context.strokeRect(
(coords.x + renderConfig.scrollX) * window.devicePixelRatio, (coords.x + renderConfig.scrollX) * window.devicePixelRatio,
(coords.y + renderConfig.scrollY) * window.devicePixelRatio, (coords.y + renderConfig.scrollY) * window.devicePixelRatio,
getMaxContainerWidth(element) * window.devicePixelRatio, getBoundTextMaxWidth(element) * window.devicePixelRatio,
getMaxContainerHeight(element) * window.devicePixelRatio, getBoundTextMaxHeight(element, textElement) * window.devicePixelRatio,
); );
} }
} }
+2 -1
View File
@@ -1,5 +1,6 @@
import { isTextElement, refreshTextDimensions } from "../element"; import { isTextElement, refreshTextDimensions } from "../element";
import { newElementWith } from "../element/mutateElement"; import { newElementWith } from "../element/mutateElement";
import { isBoundToContainer } from "../element/typeChecks";
import { ExcalidrawElement, ExcalidrawTextElement } from "../element/types"; import { ExcalidrawElement, ExcalidrawTextElement } from "../element/types";
import { invalidateShapeForElement } from "../renderer/renderElement"; import { invalidateShapeForElement } from "../renderer/renderElement";
import { getFontString } from "../utils"; import { getFontString } from "../utils";
@@ -52,7 +53,7 @@ export class Fonts {
let didUpdate = false; let didUpdate = false;
this.scene.mapElements((element) => { this.scene.mapElements((element) => {
if (isTextElement(element)) { if (isTextElement(element) && !isBoundToContainer(element)) {
invalidateShapeForElement(element); invalidateShapeForElement(element);
didUpdate = true; didUpdate = true;
return newElementWith(element, { return newElementWith(element, {
+42 -19
View File
@@ -1,5 +1,10 @@
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import { render, waitFor, GlobalTestState } from "./test-utils"; import {
render,
waitFor,
GlobalTestState,
createPasteEvent,
} from "./test-utils";
import { Pointer, Keyboard } from "./helpers/ui"; import { Pointer, Keyboard } from "./helpers/ui";
import ExcalidrawApp from "../excalidraw-app"; import ExcalidrawApp from "../excalidraw-app";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
@@ -9,6 +14,8 @@ import {
} from "../element/textElement"; } from "../element/textElement";
import { getElementBounds } from "../element"; import { getElementBounds } from "../element";
import { NormalizedZoomValue } from "../types"; import { NormalizedZoomValue } from "../types";
import { API } from "./helpers/api";
import { copyToClipboard } from "../clipboard";
const { h } = window; const { h } = window;
@@ -35,38 +42,28 @@ const setClipboardText = (text: string) => {
}); });
}; };
const sendPasteEvent = () => { const sendPasteEvent = (text?: string) => {
const clipboardEvent = new Event("paste", { const clipboardEvent = createPasteEvent(
bubbles: true, text || (() => window.navigator.clipboard.readText()),
cancelable: true, );
composed: true,
});
// set `clipboardData` properties.
// @ts-ignore
clipboardEvent.clipboardData = {
getData: () => window.navigator.clipboard.readText(),
files: [],
};
document.dispatchEvent(clipboardEvent); document.dispatchEvent(clipboardEvent);
}; };
const pasteWithCtrlCmdShiftV = () => { const pasteWithCtrlCmdShiftV = (text?: string) => {
Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => { Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => {
//triggering keydown with an empty clipboard //triggering keydown with an empty clipboard
Keyboard.keyPress(KEYS.V); Keyboard.keyPress(KEYS.V);
//triggering paste event with faked clipboard //triggering paste event with faked clipboard
sendPasteEvent(); sendPasteEvent(text);
}); });
}; };
const pasteWithCtrlCmdV = () => { const pasteWithCtrlCmdV = (text?: string) => {
Keyboard.withModifierKeys({ ctrl: true }, () => { Keyboard.withModifierKeys({ ctrl: true }, () => {
//triggering keydown with an empty clipboard //triggering keydown with an empty clipboard
Keyboard.keyPress(KEYS.V); Keyboard.keyPress(KEYS.V);
//triggering paste event with faked clipboard //triggering paste event with faked clipboard
sendPasteEvent(); sendPasteEvent(text);
}); });
}; };
@@ -89,6 +86,32 @@ 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", () => { describe("paste text as single lines", () => {
it("should create an element for each line when copying with Ctrl/Cmd+V", async () => { it("should create an element for each line when copying with Ctrl/Cmd+V", async () => {
const text = "sajgfakfn\naaksfnknas\nakefnkasf"; const text = "sajgfakfn\naaksfnknas\nakefnkasf";
+7 -14
View File
@@ -1,5 +1,10 @@
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import { GlobalTestState, render, waitFor } from "./test-utils"; import {
createPasteEvent,
GlobalTestState,
render,
waitFor,
} from "./test-utils";
import { UI, Pointer } from "./helpers/ui"; import { UI, Pointer } from "./helpers/ui";
import { API } from "./helpers/api"; import { API } from "./helpers/api";
import { actionFlipHorizontal, actionFlipVertical } from "../actions"; import { actionFlipHorizontal, actionFlipVertical } from "../actions";
@@ -680,19 +685,7 @@ describe("freedraw", () => {
describe("image", () => { describe("image", () => {
const createImage = async () => { const createImage = async () => {
const sendPasteEvent = (file?: File) => { const sendPasteEvent = (file?: File) => {
const clipboardEvent = new Event("paste", { const clipboardEvent = createPasteEvent("", file ? [file] : []);
bubbles: true,
cancelable: true,
composed: true,
});
// set `clipboardData` properties.
// @ts-ignore
clipboardEvent.clipboardData = {
getData: () => window.navigator.clipboard.readText(),
files: [file],
};
document.dispatchEvent(clipboardEvent); document.dispatchEvent(clipboardEvent);
}; };
+16 -7
View File
@@ -20,7 +20,7 @@ import { resize, rotate } from "./utils";
import { import {
getBoundTextElementPosition, getBoundTextElementPosition,
wrapText, wrapText,
getMaxContainerWidth, getBoundTextMaxWidth,
} from "../element/textElement"; } from "../element/textElement";
import * as textElementUtils from "../element/textElement"; import * as textElementUtils from "../element/textElement";
import { ROUNDNESS, VERTICAL_ALIGN } from "../constants"; import { ROUNDNESS, VERTICAL_ALIGN } from "../constants";
@@ -729,10 +729,11 @@ describe("Test Linear Elements", () => {
type: "text", type: "text",
x: 0, x: 0,
y: 0, y: 0,
text: wrapText(text, font, getMaxContainerWidth(container)), text: wrapText(text, font, getBoundTextMaxWidth(container)),
containerId: container.id, containerId: container.id,
width: 30, width: 30,
height: 20, height: 20,
verticalAlign: VERTICAL_ALIGN.MIDDLE,
}) as ExcalidrawTextElementWithContainer; }) as ExcalidrawTextElementWithContainer;
container = { container = {
@@ -1108,9 +1109,17 @@ describe("Test Linear Elements", () => {
`); `);
}); });
it("should not render vertical align tool when element selected", () => { it("should render vertical align tool when element selected only for two pointer arrow", () => {
createTwoPointerLinearElement("arrow"); let arrow = createTwoPointerLinearElement("arrow");
const arrow = h.elements[0] as ExcalidrawLinearElement;
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");
createBoundTextElement(DEFAULT_TEXT, arrow); createBoundTextElement(DEFAULT_TEXT, arrow);
API.setSelectedElements([arrow]); API.setSelectedElements([arrow]);
@@ -1149,7 +1158,7 @@ describe("Test Linear Elements", () => {
expect(rect.x).toBe(400); expect(rect.x).toBe(400);
expect(rect.y).toBe(0); expect(rect.y).toBe(0);
expect( expect(
wrapText(textElement.originalText, font, getMaxContainerWidth(arrow)), wrapText(textElement.originalText, font, getBoundTextMaxWidth(arrow)),
).toMatchInlineSnapshot(` ).toMatchInlineSnapshot(`
"Online whiteboard collaboration "Online whiteboard collaboration
made easy" made easy"
@@ -1172,7 +1181,7 @@ describe("Test Linear Elements", () => {
false, false,
); );
expect( expect(
wrapText(textElement.originalText, font, getMaxContainerWidth(arrow)), wrapText(textElement.originalText, font, getBoundTextMaxWidth(arrow)),
).toMatchInlineSnapshot(` ).toMatchInlineSnapshot(`
"Online whiteboard "Online whiteboard
collaboration made collaboration made
+21
View File
@@ -190,3 +190,24 @@ export const toggleMenu = (container: HTMLElement) => {
// open menu // open menu
fireEvent.click(container.querySelector(".dropdown-menu-button")!); 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 { MaybeTransformHandleType } from "./element/transformHandles";
import Library from "./data/library"; import Library from "./data/library";
import type { FileSystemHandle } from "./data/filesystem"; import type { FileSystemHandle } from "./data/filesystem";
import type { ALLOWED_IMAGE_MIME_TYPES, MIME_TYPES } from "./constants"; import type { IMAGE_MIME_TYPES, MIME_TYPES } from "./constants";
import { ContextMenuItems } from "./components/ContextMenu"; import { ContextMenuItems } from "./components/ContextMenu";
import { Merge, ForwardRef } from "./utility-types"; import { Merge, ForwardRef, ValueOf } from "./utility-types";
import React from "react"; import React from "react";
export type Point = Readonly<RoughPoint>; export type Point = Readonly<RoughPoint>;
@@ -60,7 +60,7 @@ export type DataURL = string & { _brand: "DataURL" };
export type BinaryFileData = { export type BinaryFileData = {
mimeType: mimeType:
| typeof ALLOWED_IMAGE_MIME_TYPES[number] | ValueOf<typeof IMAGE_MIME_TYPES>
// future user or unknown file type // future user or unknown file type
| typeof MIME_TYPES.binary; | typeof MIME_TYPES.binary;
id: FileId; id: FileId;
@@ -419,7 +419,7 @@ export type AppClassProperties = {
FileId, FileId,
{ {
image: HTMLImageElement | Promise<HTMLImageElement>; image: HTMLImageElement | Promise<HTMLImageElement>;
mimeType: typeof ALLOWED_IMAGE_MIME_TYPES[number]; mimeType: ValueOf<typeof IMAGE_MIME_TYPES>;
} }
>; >;
files: BinaryFiles; files: BinaryFiles;