Compare commits

...

25 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 Doshi 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 Doshi 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 Doshi dae81c0a2c fix: cleanup redrawTextBoundingBox (#6518)
* chore: cleanup redrawTextBoundingBox

* fix
2023-04-25 17:57:53 +05:30
suwalkanishka 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
Nainterceptor 1815cf3213 build: Add version tags to Docker build (#6508)
ci: Add version tags
2023-04-25 16:51:25 +05:30
David Luzar d35386755f feat: retain seed on shift-paste (#6509)
thanks for the review 👍
2023-04-24 10:26:21 +02:00
zsviczian 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 Luzar 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 Doshi 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
Aakansha Doshi c4445c181b docs: release @excalidraw/excalidraw@0.15.2 🎉 (#6495) 2023-04-20 17:34:06 +05:30
dependabot[bot] d12a9fdd40 build(deps): bump ua-parser-js from 0.7.31 to 0.7.33 in /dev-docs (#6164)
Bumps [ua-parser-js](https://github.com/faisalman/ua-parser-js) from 0.7.31 to 0.7.33.
- [Release notes](https://github.com/faisalman/ua-parser-js/releases)
- [Changelog](https://github.com/faisalman/ua-parser-js/blob/master/changelog.md)
- [Commits](https://github.com/faisalman/ua-parser-js/compare/0.7.31...0.7.33)

---
updated-dependencies:
- dependency-name: ua-parser-js
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-20 17:07:56 +05:30
dependabot[bot] 9368a9ce3e build(deps): bump webpack from 5.75.0 to 5.76.1 (#6357)
Bumps [webpack](https://github.com/webpack/webpack) from 5.75.0 to 5.76.1.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.75.0...v5.76.1)

---
updated-dependencies:
- dependency-name: webpack
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-20 17:05:03 +05:30
siddhant 851b9b7aec fix: rotate the text element when binding to a rotated container (#6477)
* Updated logic to update the bound child angle from the parent

* update angle when generating text element

* add test

* remove

* fix

---------

Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
2023-04-20 16:34:39 +05:30
Aakansha Doshi 5ddb28d378 fix: support breaking words containing hyphen - (#6014)
* fix: support breaking words containing hyphen -

* fix

* add spec

* fix

* fix

* fix

* fix and add spec

* improve code and add more tests
2023-04-20 11:10:46 +05:30
Max Kovalenko 404a79e241 chore: typo (collab) - reconciliation.ts (#6447) 2023-04-19 19:18:03 +02:00
David Luzar eea30da05a fix: incorrect background fill button active state (#6491) 2023-04-19 16:23:24 +02:00
Aakansha Doshi 98a77d7426 chore: show bounding box only when flag is true (#6490) 2023-04-19 17:02:20 +05:30
Aakansha Doshi ff3c2e5a16 docs: fix docs link in readme (#6486)
* docs: fix docs link in readme

* update changelog
2023-04-18 21:52:08 +05:30
Aakansha Doshi b64beaf5ba docs: release @excalidraw/excalidraw@0.15.1 🎉 (#6485) 2023-04-18 21:32:33 +05:30
Aakansha Doshi 89304c9f66 fix: add readme back to npm package which was mistakenly removed (#6484)
* fix: remove update readme script from release

* update docs

* remove

* fix
2023-04-18 21:23:47 +05:30
Aakansha Doshi 1d0653ce50 docs: update docs for next version (#6251)
* update docs for opts param inr estore utils

* docs: add docs for `useI18n` hook (#6269)

Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>

* upgrade excal

---------

Co-authored-by: David Luzar <luzar.david@gmail.com>
2023-04-18 21:03:17 +05:30
41 changed files with 565 additions and 241 deletions
+12 -2
View File
@@ -12,14 +12,24 @@ 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@v3
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: excalidraw/excalidraw:latest
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
@@ -31,10 +31,29 @@ You can pass `null` / `undefined` if not applicable.
restoreElements(
elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ImportedDataState["elements"]</a>,<br/>&nbsp;
localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a> | null | undefined): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a>,<br/>&nbsp;
refreshDimensions?: boolean<br/>
opts: &#123; refreshDimensions?: boolean, repairBindings?: boolean }<br/>
)
</pre>
| Prop | Type | Description |
| ---- | ---- | ---- |
| `elements` | <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ImportedDataState["elements"]</a> | The `elements` to be restored |
| [`localElements`](#localelements) | <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a> &#124; null &#124; undefined | When `localElements` are supplied, they are used to ensure that existing restored elements reuse `version` (and increment it), and regenerate `versionNonce`. |
| [`opts`](#opts) | `Object` | The extra optional parameter to configure restored elements
#### localElements
When `localElements` are supplied, they are used to ensure that existing restored elements reuse `version` (and increment it), and regenerate `versionNonce`.
Use this when you `import` elements which may already be present in the scene to ensure that you do not disregard the newly imported elements if you're using element version to detect the update
#### opts
The extra optional parameter to configure restored elements. It has the following attributes
| Prop | Type | Description|
| --- | --- | ------|
| `refreshDimensions` | `boolean` | Indicates whether we should also `recalculate` text element dimensions. Since this is a potentially costly operation, you may want to disable it if you restore elements in tight loops, such as during collaboration. |
| `repairBindings` |`boolean` | Indicates whether the `bindings` for the elements should be repaired. This is to make sure there are no containers with non existent bound text element id and no bound text elements with non existent container id. |
**_How to use_**
```js
@@ -43,9 +62,6 @@ import { restoreElements } from "@excalidraw/excalidraw";
This function will make sure all properties of element is correctly set and if any attribute is missing, it will be set to its default value.
When `localElements` are supplied, they are used to ensure that existing restored elements reuse `version` (and increment it), and regenerate `versionNonce`.
Use this when you import elements which may already be present in the scene to ensure that you do not disregard the newly imported elements if you're using element version to detect the updates.
Parameter `refreshDimensions` indicates whether we should also `recalculate` text element dimensions. Defaults to `false`. Since this is a potentially costly operation, you may want to disable it if you restore elements in tight loops, such as during collaboration.
### restore
@@ -56,7 +72,9 @@ Parameter `refreshDimensions` indicates whether we should also `recalculate` tex
restore(
data: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L34">ImportedDataState</a>,<br/>&nbsp;
localAppState: Partial&lt;<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a>> | null | undefined,<br/>&nbsp;
localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a> | null | undefined<br/>): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L4">DataState</a>
localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a> | null | undefined<br/>): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L4">DataState</a><br/>
opts: &#123; refreshDimensions?: boolean, repairBindings?: boolean }<br/>
)
</pre>
@@ -339,3 +339,47 @@ The `device` has the following `attributes`
| `isMobile` | `boolean` | Set to `true` when the device is `mobile` |
| `isTouchScreen` | `boolean` | Set to `true` for `touch` devices |
| `canDeviceFitSidebar` | `boolean` | Implies whether there is enough space to fit the `sidebar` |
### i18n
To help with localization, we export the following.
| name | type |
| --- | --- |
| `defaultLang` | `string` |
| `languages` | [`Language[]`](https://github.com/excalidraw/excalidraw/blob/master/src/i18n.ts#L15) |
| `useI18n` | [`() => { langCode, t }`](https://github.com/excalidraw/excalidraw/blob/master/src/i18n.ts#L15) |
```js
import { defaultLang, languages, useI18n } from "@excalidraw/excalidraw";
```
#### defaultLang
Default language code, `en`.
#### languages
List of supported language codes. You can pass any of these to `Excalidraw`'s [`langCode` prop](/docs/@excalidraw/excalidraw/api/props/#langcode).
#### useI18n
A hook that returns the current language code and translation helper function. You can use this to translate strings in the components you render as children of `<Excalidraw>`.
```jsx live
function App() {
const { t } = useI18n();
return (
<div style={{ height: "500px" }}>
<Excalidraw>
<button
style={{ position: "absolute", zIndex: 10, height: "2rem" }}
onClick={() => window.alert(t("labels.madeWithExcalidraw"))}
>
{t("buttons.confirm")}
</button>
</Excalidraw>
</div>
);
}
```
+1 -1
View File
@@ -18,7 +18,7 @@
"@docusaurus/core": "2.2.0",
"@docusaurus/preset-classic": "2.2.0",
"@docusaurus/theme-live-codeblock": "2.2.0",
"@excalidraw/excalidraw": "0.14.2",
"@excalidraw/excalidraw": "0.15.2",
"@mdx-js/react": "^1.6.22",
"clsx": "^1.2.1",
"docusaurus-plugin-sass": "0.2.3",
@@ -24,6 +24,7 @@ const ExcalidrawScope = {
Sidebar: ExcalidrawComp.Sidebar,
exportToCanvas: ExcalidrawComp.exportToCanvas,
initialData,
useI18n: ExcalidrawComp.useI18n,
};
export default ExcalidrawScope;
+7 -7
View File
@@ -1631,10 +1631,10 @@
url-loader "^4.1.1"
webpack "^5.73.0"
"@excalidraw/excalidraw@0.14.2":
version "0.14.2"
resolved "https://registry.yarnpkg.com/@excalidraw/excalidraw/-/excalidraw-0.14.2.tgz#150cb4b7a1bf0d11cd64295936c930e7e0db8375"
integrity sha512-8LdjpTBWEK5waDWB7Bt/G9YBI4j0OxkstUhvaDGz7dwQGfzF6FW5CXBoYHNEoX0qmb+Fg/NPOlZ7FrKsrSVCqg==
"@excalidraw/excalidraw@0.15.2":
version "0.15.2"
resolved "https://registry.yarnpkg.com/@excalidraw/excalidraw/-/excalidraw-0.15.2.tgz#7dba4f6e10c52015a007efb75a9fc1afe598574c"
integrity sha512-rTI02kgWSTXiUdIkBxt9u/581F3eXcqQgJdIxmz54TFtG3ughoxO5fr4t7Fr2LZIturBPqfocQHGKZ0t2KLKgw==
"@hapi/hoek@^9.0.0":
version "9.3.0"
@@ -7159,9 +7159,9 @@ typescript@^4.7.4:
integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==
ua-parser-js@^0.7.30:
version "0.7.31"
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.31.tgz#649a656b191dffab4f21d5e053e27ca17cbff5c6"
integrity sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ==
version "0.7.33"
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.33.tgz#1d04acb4ccef9293df6f70f2c3d22f3030d8b532"
integrity sha512-s8ax/CeZdK9R/56Sui0WM6y9OFREJarMRHqLB2EwkovemBxNQ+Bqu8GAsUnVcXKgphb++ghr/B2BZx4mahujPw==
unescape@^1.0.1:
version "1.0.1"
-20
View File
@@ -1,22 +1,9 @@
const fs = require("fs");
const { execSync } = require("child_process");
const excalidrawDir = `${__dirname}/../src/packages/excalidraw`;
const excalidrawPackage = `${excalidrawDir}/package.json`;
const pkg = require(excalidrawPackage);
const originalReadMe = fs.readFileSync(`${excalidrawDir}/README.md`, "utf8");
const updateReadme = () => {
const excalidrawIndex = originalReadMe.indexOf("### Excalidraw");
// remove note for stable readme
const data = originalReadMe.slice(excalidrawIndex);
// update readme
fs.writeFileSync(`${excalidrawDir}/README.md`, data, "utf8");
};
const publish = () => {
try {
execSync(`yarn --frozen-lockfile`);
@@ -30,15 +17,8 @@ const publish = () => {
};
const release = () => {
updateReadme();
console.info("Note for stable readme removed");
publish();
console.info(`Published ${pkg.version}!`);
// revert readme after release
fs.writeFileSync(`${excalidrawDir}/README.md`, originalReadMe, "utf8");
console.info("Readme reverted");
};
release();
+1 -1
View File
@@ -18,7 +18,7 @@ export const actionCopy = register({
perform: (elements, appState, _, app) => {
const selectedElements = getSelectedElements(elements, appState, true);
copyToClipboard(selectedElements, appState, app.files);
copyToClipboard(selectedElements, app.files);
return {
commitToHistory: false,
+7 -5
View File
@@ -84,7 +84,7 @@ import {
isSomeElementSelected,
} from "../scene";
import { hasStrokeColor } from "../scene/comparisons";
import { arrayToMap } from "../utils";
import { arrayToMap, getShortcutKey } from "../utils";
import { register } from "./register";
const FONT_SIZE_RELATIVE_INCREASE_STEP = 0.1;
@@ -314,9 +314,9 @@ export const actionChangeFillStyle = register({
},
PanelComponent: ({ elements, appState, updateData }) => {
const selectedElements = getSelectedElements(elements, appState);
const allElementsZigZag = selectedElements.every(
(el) => el.fillStyle === "zigzag",
);
const allElementsZigZag =
selectedElements.length > 0 &&
selectedElements.every((el) => el.fillStyle === "zigzag");
return (
<fieldset>
@@ -326,7 +326,9 @@ export const actionChangeFillStyle = register({
options={[
{
value: "hachure",
text: t("labels.hachure"),
text: `${
allElementsZigZag ? t("labels.zigzag") : t("labels.hachure")
} (${getShortcutKey("Alt-Click")})`,
icon: allElementsZigZag ? FillZigZagIcon : FillHachureIcon,
active: allElementsZigZag ? true : undefined,
},
+27 -11
View File
@@ -2,12 +2,12 @@ import {
ExcalidrawElement,
NonDeletedExcalidrawElement,
} from "./element/types";
import { AppState, BinaryFiles } from "./types";
import { 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 } from "./utils";
import { isPromiseLike, isTestEnv } from "./utils";
type ElementsClipboard = {
type: typeof EXPORT_DATA_TYPES.excalidrawClipboard;
@@ -55,24 +55,40 @@ 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
? elements.reduce((acc, element) => {
if (isInitializedImageElement(element) && files[element.fileId]) {
acc[element.fileId] = files[element.fileId];
}
return acc;
}, {} as BinaryFiles)
: undefined,
files: files ? _files : undefined,
};
const json = JSON.stringify(contents);
if (isTestEnv()) {
return json;
}
CLIPBOARD = json;
try {
PREFER_APP_CLIPBOARD = false;
await copyTextToSystemClipboard(json);
+16 -2
View File
@@ -60,6 +60,7 @@ import {
ENV,
EVENT,
GRID_SIZE,
IMAGE_MIME_TYPES,
IMAGE_RENDER_TIMEOUT,
isAndroid,
isBrave,
@@ -1589,6 +1590,7 @@ 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);
@@ -1602,6 +1604,7 @@ 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);
@@ -1639,6 +1642,9 @@ class App extends React.Component<AppProps, AppState> {
y: element.y + gridY - minY,
});
}),
{
randomizeSeed: !opts.retainSeed,
},
);
const nextElements = [
@@ -2744,6 +2750,7 @@ class App extends React.Component<AppProps, AppState> {
containerId: shouldBindToContainer ? container?.id : undefined,
groupIds: container?.groupIds ?? [],
lineHeight,
angle: container?.angle ?? 0,
});
if (!existingTextElement && shouldBindToContainer && container) {
@@ -4719,7 +4726,12 @@ 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)
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(
pointerCoords.x - pointerDownState.drag.offset.x,
pointerCoords.y - pointerDownState.drag.offset.y,
@@ -5742,7 +5754,9 @@ class App extends React.Component<AppProps, AppState> {
const imageFile = await fileOpen({
description: "Image",
extensions: ["jpg", "png", "svg", "gif"],
extensions: Object.keys(
IMAGE_MIME_TYPES,
) as (keyof typeof IMAGE_MIME_TYPES)[],
});
const imageElement = this.createImageElement({
+1
View File
@@ -183,6 +183,7 @@
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,6 +30,7 @@
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),
elements: duplicateElements(item.elements, { randomizeSeed: true }),
};
});
};
+3
View File
@@ -2,6 +2,9 @@
// 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,20 +105,30 @@ export const CANVAS_ONLY_ACTIONS = ["selectAll"];
export const GRID_SIZE = 20; // TODO make it configurable?
export const MIME_TYPES = {
excalidraw: "application/vnd.excalidraw+json",
excalidrawlib: "application/vnd.excalidrawlib+json",
json: "application/json",
export const IMAGE_MIME_TYPES = {
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 = {
@@ -189,16 +199,6 @@ 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,6 +354,7 @@
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);
@@ -413,6 +414,7 @@
bottom: 30px;
transform: translateX(-50%);
pointer-events: all;
font-family: inherit;
&:hover {
background-color: var(--button-hover-bg);
+4 -6
View File
@@ -1,6 +1,6 @@
import { nanoid } from "nanoid";
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 { ExcalidrawElement, FileId } from "../element/types";
import { CanvasError } from "../errors";
@@ -117,11 +117,9 @@ export const isImageFileHandle = (handle: FileSystemHandle | null) => {
export const isSupportedImageFile = (
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 || {};
return (
!!type && (ALLOWED_IMAGE_MIME_TYPES as readonly string[]).includes(type)
);
return !!type && (Object.values(IMAGE_MIME_TYPES) as string[]).includes(type);
};
export const loadSceneOrLibraryFromBlob = async (
@@ -157,7 +155,7 @@ export const loadSceneOrLibraryFromBlob = async (
},
localAppState,
localElements,
{ repairBindings: true, refreshDimensions: true },
{ repairBindings: true, refreshDimensions: false },
),
};
} else if (isValidLibrary(data)) {
+1 -10
View File
@@ -8,16 +8,7 @@ import { EVENT, MIME_TYPES } from "../constants";
import { AbortError } from "../errors";
import { debounce } from "../utils";
type FILE_EXTENSION =
| "gif"
| "jpg"
| "png"
| "excalidraw.png"
| "svg"
| "excalidraw.svg"
| "json"
| "excalidraw"
| "excalidrawlib";
type FILE_EXTENSION = Exclude<keyof typeof MIME_TYPES, "binary">;
const INPUT_CHANGE_INTERVAL_MS = 500;
+15 -1
View File
@@ -40,7 +40,11 @@ import { isBindingElement } from "./typeChecks";
import { shouldRotateWithDiscreteAngle } from "../keys";
import { getBoundTextElement, handleBindTextResize } from "./textElement";
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";
const editorMidPointsCache: {
@@ -1304,6 +1308,16 @@ 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 };
};
+17 -4
View File
@@ -20,7 +20,7 @@ import {
isTestEnv,
} from "../utils";
import { randomInteger, randomId } from "../random";
import { mutateElement, newElementWith } from "./mutateElement";
import { bumpVersion, mutateElement, newElementWith } from "./mutateElement";
import { getNewGroupIdsForDuplication } from "../groups";
import { AppState } from "../types";
import { getElementAbsoluteCoords } from ".";
@@ -33,7 +33,7 @@ import {
measureText,
normalizeText,
wrapText,
getMaxContainerWidth,
getBoundTextMaxWidth,
getDefaultLineHeight,
} from "./textElement";
import {
@@ -310,7 +310,7 @@ export const refreshTextDimensions = (
text = wrapText(
text,
getFontString(textElement),
getMaxContainerWidth(container),
getBoundTextMaxWidth(container),
);
}
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
* 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[]) => {
export const duplicateElements = (
elements: readonly ExcalidrawElement[],
opts?: {
/** NOTE also updates version flags and `updated` */
randomizeSeed: boolean;
},
) => {
const clonedElements: ExcalidrawElement[] = [];
const origElementsMap = arrayToMap(elements);
@@ -574,6 +582,11 @@ export const duplicateElements = (elements: readonly ExcalidrawElement[]) => {
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,
getMaxContainerWidth,
getBoundTextMaxWidth,
getApproxMinLineHeight,
measureText,
getMaxContainerHeight,
getBoundTextMaxHeight,
} from "./textElement";
export const normalizeAngle = (angle: number): number => {
@@ -204,7 +204,7 @@ const measureFontSizeFromWidth = (
if (hasContainer) {
const container = getContainerElement(element);
if (container) {
width = getMaxContainerWidth(container);
width = getBoundTextMaxWidth(container);
}
}
const nextFontSize = element.fontSize * (nextWidth / width);
@@ -435,8 +435,8 @@ export const resizeSingleElement = (
const nextFont = measureFontSizeFromWidth(
boundTextElement,
getMaxContainerWidth(updatedElement),
getMaxContainerHeight(updatedElement),
getBoundTextMaxWidth(updatedElement),
getBoundTextMaxHeight(updatedElement, boundTextElement),
);
if (nextFont === null) {
return;
@@ -718,10 +718,10 @@ const resizeMultipleElements = (
const metrics = measureFontSizeFromWidth(
boundTextElement ?? (element.orig as ExcalidrawTextElement),
boundTextElement
? getMaxContainerWidth(updatedElement)
? getBoundTextMaxWidth(updatedElement)
: updatedElement.width,
boundTextElement
? getMaxContainerHeight(updatedElement)
? getBoundTextMaxHeight(updatedElement, boundTextElement)
: updatedElement.height,
);
+99 -11
View File
@@ -3,14 +3,15 @@ import { API } from "../tests/helpers/api";
import {
computeContainerDimensionForBoundText,
getContainerCoords,
getMaxContainerWidth,
getMaxContainerHeight,
getBoundTextMaxWidth,
getBoundTextMaxHeight,
wrapText,
detectLineHeight,
getLineHeightInPx,
getDefaultLineHeight,
parseTokens,
} from "./textElement";
import { FontString } from "./types";
import { ExcalidrawTextElementWithContainer, FontString } from "./types";
describe("Test wrapText", () => {
const font = "20px Cascadia, width: Segoe UI Emoji" as FontString;
@@ -183,6 +184,56 @@ now`,
expect(wrapText(text, font, -1)).toEqual(text);
expect(wrapText(text, font, Infinity)).toEqual(text);
});
it("should wrap the text correctly when text contains hyphen", () => {
let text =
"Wikipedia is hosted by Wikimedia- Foundation, a non-profit organization that also hosts a range-of other projects";
const res = wrapText(text, font, 110);
expect(res).toBe(
`Wikipedia \nis hosted \nby \nWikimedia-\nFoundation,\na non-\nprofit \norganizati\non that \nalso hosts\na range-of\nother \nprojects`,
);
text = "Hello thereusing-now";
expect(wrapText(text, font, 100)).toEqual("Hello \nthereusin\ng-now");
});
});
describe("Test parseTokens", () => {
it("should split into tokens correctly", () => {
let text = "Excalidraw is a virtual collaborative whiteboard";
expect(parseTokens(text)).toEqual([
"Excalidraw",
"is",
"a",
"virtual",
"collaborative",
"whiteboard",
]);
text =
"Wikipedia is hosted by Wikimedia- Foundation, a non-profit organization that also hosts a range-of other projects";
expect(parseTokens(text)).toEqual([
"Wikipedia",
"is",
"hosted",
"by",
"Wikimedia-",
"",
"Foundation,",
"a",
"non-",
"profit",
"organization",
"that",
"also",
"hosts",
"a",
"range-",
"of",
"other",
"projects",
]);
});
});
describe("Test measureText", () => {
@@ -260,7 +311,7 @@ describe("Test measureText", () => {
});
});
describe("Test getMaxContainerWidth", () => {
describe("Test getBoundTextMaxWidth", () => {
const params = {
width: 178,
height: 194,
@@ -268,39 +319,76 @@ describe("Test measureText", () => {
it("should return max width when container is rectangle", () => {
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", () => {
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", () => {
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 = {
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(getMaxContainerHeight(container)).toBe(184);
expect(getBoundTextMaxHeight(container, boundTextElement)).toBe(184);
});
it("should return max height when container is ellipse", () => {
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", () => {
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,
);
});
});
});
+73 -51
View File
@@ -65,7 +65,7 @@ export const redrawTextBoundingBox = (
boundTextUpdates.text = textElement.text;
if (container) {
maxWidth = getMaxContainerWidth(container);
maxWidth = getBoundTextMaxWidth(container);
boundTextUpdates.text = wrapText(
textElement.originalText,
getFontString(textElement),
@@ -83,27 +83,22 @@ export const redrawTextBoundingBox = (
boundTextUpdates.baseline = metrics.baseline;
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);
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 });
maxContainerHeight = getMaxContainerHeight(container);
updateOriginalContainerCache(container.id, nextHeight);
}
let nextHeight = containerDims.height;
if (metrics.height > maxContainerHeight) {
nextHeight = computeContainerDimensionForBoundText(
metrics.height,
container.type,
);
mutateElement(container, { height: nextHeight });
updateOriginalContainerCache(container.id, nextHeight);
}
if (!isArrowElement(container)) {
const updatedTextElement = {
...textElement,
...boundTextUpdates,
@@ -183,8 +178,11 @@ export const handleBindTextResize = (
let nextHeight = textElement.height;
let nextWidth = textElement.width;
const containerDims = getContainerDims(container);
const maxWidth = getMaxContainerWidth(container);
const maxHeight = getMaxContainerHeight(container);
const maxWidth = getBoundTextMaxWidth(container);
const maxHeight = getBoundTextMaxHeight(
container,
textElement as ExcalidrawTextElementWithContainer,
);
let containerHeight = containerDims.height;
let nextBaseLine = textElement.baseline;
if (transformHandleType !== "n" && transformHandleType !== "s") {
@@ -256,8 +254,8 @@ export const computeBoundTextPosition = (
);
}
const containerCoords = getContainerCoords(container);
const maxContainerHeight = getMaxContainerHeight(container);
const maxContainerWidth = getMaxContainerWidth(container);
const maxContainerHeight = getBoundTextMaxHeight(container, boundTextElement);
const maxContainerWidth = getBoundTextMaxWidth(container);
let x;
let y;
@@ -419,6 +417,24 @@ export const getTextHeight = (
return getLineHeightInPx(fontSize, lineHeight) * lineCount;
};
export const parseTokens = (text: string) => {
// Splitting words containing "-" as those are treated as separate words
// by css wrapping algorithm eg non-profit => non-, profit
const words = text.split("-");
if (words.length > 1) {
// non-proft org => ['non-', 'profit org']
words.forEach((word, index) => {
if (index !== words.length - 1) {
words[index] = word += "-";
}
});
}
// Joining the words with space and splitting them again with space to get the
// final list of tokens
// ['non-', 'profit org'] =>,'non- proft org' => ['non-','profit','org']
return words.join(" ").split(" ");
};
export const wrapText = (text: string, font: FontString, maxWidth: number) => {
// if maxWidth is not finite or NaN which can happen in case of bugs in
// computation, we need to make sure we don't continue as we'll end up
@@ -444,17 +460,16 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
currentLine = "";
currentLineWidthTillNow = 0;
};
originalLines.forEach((originalLine) => {
const currentLineWidth = getTextWidth(originalLine, font);
//Push the line if its <= maxWidth
// Push the line if its <= maxWidth
if (currentLineWidth <= maxWidth) {
lines.push(originalLine);
return; // continue
}
const words = originalLine.split(" ");
const words = parseTokens(originalLine);
resetParams();
let index = 0;
@@ -472,6 +487,7 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
else if (currentWordWidth > maxWidth) {
// push current line since the current word exceeds the max width
// so will be appended in next line
push(currentLine);
resetParams();
@@ -492,15 +508,15 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
currentLine += currentChar;
}
}
// push current line if appending space exceeds max width
if (currentLineWidthTillNow + spaceWidth >= maxWidth) {
push(currentLine);
resetParams();
} else {
// space needs to be appended before next word
// as currentLine contains chars which couldn't be appended
// to previous line
// to previous line unless the line ends with hyphen to sync
// with css word-wrap
} else if (!currentLine.endsWith("-")) {
currentLine += " ";
currentLineWidthTillNow += spaceWidth;
}
@@ -518,12 +534,23 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
break;
}
index++;
currentLine += `${word} `;
// if word ends with "-" then we don't need to add space
// to sync with css word-wrap
const shouldAppendSpace = !word.endsWith("-");
currentLine += word;
if (shouldAppendSpace) {
currentLine += " ";
}
// Push the word if appending space exceeds max width
if (currentLineWidthTillNow + spaceWidth >= maxWidth) {
const word = currentLine.slice(0, -1);
push(word);
if (shouldAppendSpace) {
lines.push(currentLine.slice(0, -1));
} else {
lines.push(currentLine);
}
resetParams();
break;
}
@@ -768,7 +795,11 @@ export const shouldAllowVerticalAlign = (
const hasBoundContainer = isBoundToContainer(element);
if (hasBoundContainer) {
const container = getContainerElement(element);
if (isTextElement(element) && isArrowElement(container)) {
if (
isTextElement(element) &&
isArrowElement(container) &&
container.points.length > 2
) {
return false;
}
return true;
@@ -861,18 +892,10 @@ export const computeContainerDimensionForBoundText = (
return dimension + padding;
};
export const getMaxContainerWidth = (container: ExcalidrawElement) => {
export const getBoundTextMaxWidth = (container: ExcalidrawElement) => {
const width = getContainerDims(container).width;
if (isArrowElement(container)) {
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;
return width - BOUND_TEXT_PADDING * 8 * 2;
}
if (container.type === "ellipse") {
@@ -889,16 +912,15 @@ export const getMaxContainerWidth = (container: ExcalidrawElement) => {
return width - BOUND_TEXT_PADDING * 2;
};
export const getMaxContainerHeight = (container: ExcalidrawElement) => {
export const getBoundTextMaxHeight = (
container: ExcalidrawElement,
boundTextElement: ExcalidrawTextElementWithContainer,
) => {
const height = getContainerDims(container).height;
if (isArrowElement(container)) {
const containerHeight = height - BOUND_TEXT_PADDING * 8 * 2;
if (containerHeight <= 0) {
const boundText = getBoundTextElement(container);
if (boundText) {
return boundText.height;
}
return BOUND_TEXT_PADDING * 8 * 2;
return boundTextElement.height;
}
return height;
}
+30
View File
@@ -526,6 +526,36 @@ describe("textWysiwyg", () => {
]);
});
it("should set the text element angle to same as container angle when binding to rotated container", async () => {
const rectangle = API.createElement({
type: "rectangle",
width: 90,
height: 75,
angle: 45,
});
h.elements = [rectangle];
mouse.doubleClickAt(rectangle.x + 10, rectangle.y + 10);
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
expect(text.type).toBe("text");
expect(text.containerId).toBe(rectangle.id);
expect(rectangle.boundElements).toStrictEqual([
{ id: text.id, type: "text" },
]);
expect(text.angle).toBe(rectangle.angle);
mouse.down();
const editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
fireEvent.change(editor, { target: { value: "Hello World!" } });
await new Promise((r) => setTimeout(r, 0));
editor.blur();
expect(rectangle.boundElements).toStrictEqual([
{ id: text.id, type: "text" },
]);
});
it("should compute the container height correctly and not throw error when height is updated while editing the text", async () => {
const diamond = API.createElement({
type: "diamond",
+13 -13
View File
@@ -32,8 +32,8 @@ import {
normalizeText,
redrawTextBoundingBox,
wrapText,
getMaxContainerHeight,
getMaxContainerWidth,
getBoundTextMaxHeight,
getBoundTextMaxWidth,
computeContainerDimensionForBoundText,
detectLineHeight,
} from "./textElement";
@@ -205,8 +205,11 @@ export const textWysiwyg = ({
}
}
maxWidth = getMaxContainerWidth(container);
maxHeight = getMaxContainerHeight(container);
maxWidth = getBoundTextMaxWidth(container);
maxHeight = getBoundTextMaxHeight(
container,
updatedTextElement as ExcalidrawTextElementWithContainer,
);
// autogrow container height if text exceeds
if (!isArrowElement(container) && textElementHeight > maxHeight) {
@@ -230,17 +233,14 @@ export const textWysiwyg = ({
);
mutateElement(container, { height: targetContainerHeight });
}
// Start pushing text upward until a diff of 30px (padding)
// is reached
else {
// 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)) {
const containerCoords = getContainerCoords(container);
// vertically center align the text
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) {
coordY = containerCoords.y + (maxHeight - textElementHeight);
@@ -377,7 +377,7 @@ export const textWysiwyg = ({
const wrappedText = wrapText(
`${editable.value}${data}`,
font,
getMaxContainerWidth(container),
getBoundTextMaxWidth(container),
);
const width = getTextWidth(wrappedText, font);
editable.style.width = `${width}px`;
@@ -394,7 +394,7 @@ export const textWysiwyg = ({
const wrappedText = wrapText(
normalizeText(editable.value),
font,
getMaxContainerWidth(container!),
getBoundTextMaxWidth(container!),
);
const { width, height } = measureText(
wrappedText,
+1 -1
View File
@@ -65,7 +65,7 @@ export const reconcileElements = (
// Mark duplicate for removal as it'll be replaced with the remote element
if (local) {
// Unless the ramote and local elements are the same element in which case
// Unless the remote and local elements are the same element in which case
// we need to keep it as we'd otherwise discard it from the resulting
// array.
if (local[0] === remoteElement) {
+1 -1
View File
@@ -263,7 +263,7 @@ export const loadScene = async (
await importFromBackend(id, privateKey),
localDataState?.appState,
localDataState?.elements,
{ repairBindings: true, refreshDimensions: true },
{ repairBindings: true, refreshDimensions: false },
);
} else {
data = restore(localDataState || null, null, null, {
+1
View File
@@ -54,6 +54,7 @@
"veryLarge": "Very large",
"solid": "Solid",
"hachure": "Hachure",
"zigzag": "Zigzag",
"crossHatch": "Cross-hatch",
"thin": "Thin",
"bold": "Bold",
+32
View File
@@ -11,6 +11,38 @@ The change should be grouped under one of the below section and must contain PR
Please add the latest change on the top under the correct section.
-->
## 0.15.2 (2023-04-20)
### Docs
- Fix docs link in readme [#6486](https://github.com/excalidraw/excalidraw/pull/6486)
## Excalidraw Library
**_This section lists the updates made to the excalidraw library and will not affect the integration._**
### Fixes
- Rotate the text element when binding to a rotated container [#6477](https://github.com/excalidraw/excalidraw/pull/6477)
- Support breaking words containing hyphen - [#6014](https://github.com/excalidraw/excalidraw/pull/6014)
- Incorrect background fill button active state [#6491](https://github.com/excalidraw/excalidraw/pull/6491)
---
## 0.15.1 (2023-04-18)
### Docs
- Add the readme back to the package which was mistakenly removed [#6484](https://github.com/excalidraw/excalidraw/pull/6484)
## Excalidraw Library
**_This section lists the updates made to the excalidraw library and will not affect the integration._**
---
## 0.15.0 (2023-04-18)
### Features
+2 -2
View File
@@ -38,8 +38,8 @@ Excalidraw takes _100%_ of `width` and `height` of the containing block so make
## Integration
Head over to the [docs](https://docs.excalidraw.com/docs/package/integration)
Head over to the [docs](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/integration)
## API
Head over to the [docs](https://docs.excalidraw.com/docs/package/api)
Head over to the [docs](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@excalidraw/excalidraw",
"version": "0.15.0",
"version": "0.15.2",
"main": "main.js",
"types": "types/packages/excalidraw/index.d.ts",
"files": [
+1 -9
View File
@@ -220,15 +220,7 @@ export const exportToClipboard = async (
} else if (opts.type === "png") {
await copyBlobToClipboardAsPng(exportToBlob(opts));
} else if (opts.type === "json") {
const appState = {
offsetTop: 0,
offsetLeft: 0,
width: 0,
height: 0,
...getDefaultAppState(),
...opts.appState,
};
await copyToClipboard(opts.elements, appState, opts.files);
await copyToClipboard(opts.elements, opts.files);
} else {
throw new Error("Invalid export type");
}
+9 -5
View File
@@ -44,8 +44,8 @@ import {
getContainerCoords,
getContainerElement,
getLineHeightInPx,
getMaxContainerHeight,
getMaxContainerWidth,
getBoundTextMaxHeight,
getBoundTextMaxWidth,
} from "../element/textElement";
import { LinearElementEditor } from "../element/linearElementEditor";
@@ -864,17 +864,21 @@ const drawElementFromCanvas = (
);
if (
process.env.REACT_APP_DEBUG_ENABLE_TEXT_CONTAINER_BOUNDING_BOX &&
process.env.REACT_APP_DEBUG_ENABLE_TEXT_CONTAINER_BOUNDING_BOX ===
"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,
getMaxContainerWidth(element) * window.devicePixelRatio,
getMaxContainerHeight(element) * window.devicePixelRatio,
getBoundTextMaxWidth(element) * window.devicePixelRatio,
getBoundTextMaxHeight(element, textElement) * window.devicePixelRatio,
);
}
}
+2 -1
View File
@@ -1,5 +1,6 @@
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";
@@ -52,7 +53,7 @@ export class Fonts {
let didUpdate = false;
this.scene.mapElements((element) => {
if (isTextElement(element)) {
if (isTextElement(element) && !isBoundToContainer(element)) {
invalidateShapeForElement(element);
didUpdate = true;
return newElementWith(element, {
+42 -19
View File
@@ -1,5 +1,10 @@
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 ExcalidrawApp from "../excalidraw-app";
import { KEYS } from "../keys";
@@ -9,6 +14,8 @@ import {
} from "../element/textElement";
import { getElementBounds } from "../element";
import { NormalizedZoomValue } from "../types";
import { API } from "./helpers/api";
import { copyToClipboard } from "../clipboard";
const { h } = window;
@@ -35,38 +42,28 @@ const setClipboardText = (text: string) => {
});
};
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: [],
};
const sendPasteEvent = (text?: string) => {
const clipboardEvent = createPasteEvent(
text || (() => window.navigator.clipboard.readText()),
);
document.dispatchEvent(clipboardEvent);
};
const pasteWithCtrlCmdShiftV = () => {
const pasteWithCtrlCmdShiftV = (text?: string) => {
Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => {
//triggering keydown with an empty clipboard
Keyboard.keyPress(KEYS.V);
//triggering paste event with faked clipboard
sendPasteEvent();
sendPasteEvent(text);
});
};
const pasteWithCtrlCmdV = () => {
const pasteWithCtrlCmdV = (text?: string) => {
Keyboard.withModifierKeys({ ctrl: true }, () => {
//triggering keydown with an empty clipboard
Keyboard.keyPress(KEYS.V);
//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", () => {
it("should create an element for each line when copying with Ctrl/Cmd+V", async () => {
const text = "sajgfakfn\naaksfnknas\nakefnkasf";
+7 -14
View File
@@ -1,5 +1,10 @@
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 { API } from "./helpers/api";
import { actionFlipHorizontal, actionFlipVertical } from "../actions";
@@ -680,19 +685,7 @@ describe("freedraw", () => {
describe("image", () => {
const createImage = async () => {
const sendPasteEvent = (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],
};
const clipboardEvent = createPasteEvent("", file ? [file] : []);
document.dispatchEvent(clipboardEvent);
};
+16 -7
View File
@@ -20,7 +20,7 @@ import { resize, rotate } from "./utils";
import {
getBoundTextElementPosition,
wrapText,
getMaxContainerWidth,
getBoundTextMaxWidth,
} from "../element/textElement";
import * as textElementUtils from "../element/textElement";
import { ROUNDNESS, VERTICAL_ALIGN } from "../constants";
@@ -729,10 +729,11 @@ describe("Test Linear Elements", () => {
type: "text",
x: 0,
y: 0,
text: wrapText(text, font, getMaxContainerWidth(container)),
text: wrapText(text, font, getBoundTextMaxWidth(container)),
containerId: container.id,
width: 30,
height: 20,
verticalAlign: VERTICAL_ALIGN.MIDDLE,
}) as ExcalidrawTextElementWithContainer;
container = {
@@ -1108,9 +1109,17 @@ describe("Test Linear Elements", () => {
`);
});
it("should not render vertical align tool when element selected", () => {
createTwoPointerLinearElement("arrow");
const arrow = h.elements[0] as ExcalidrawLinearElement;
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");
createBoundTextElement(DEFAULT_TEXT, arrow);
API.setSelectedElements([arrow]);
@@ -1149,7 +1158,7 @@ describe("Test Linear Elements", () => {
expect(rect.x).toBe(400);
expect(rect.y).toBe(0);
expect(
wrapText(textElement.originalText, font, getMaxContainerWidth(arrow)),
wrapText(textElement.originalText, font, getBoundTextMaxWidth(arrow)),
).toMatchInlineSnapshot(`
"Online whiteboard collaboration
made easy"
@@ -1172,7 +1181,7 @@ describe("Test Linear Elements", () => {
false,
);
expect(
wrapText(textElement.originalText, font, getMaxContainerWidth(arrow)),
wrapText(textElement.originalText, font, getBoundTextMaxWidth(arrow)),
).toMatchInlineSnapshot(`
"Online whiteboard
collaboration made
+21
View File
@@ -190,3 +190,24 @@ 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 { ALLOWED_IMAGE_MIME_TYPES, MIME_TYPES } from "./constants";
import type { IMAGE_MIME_TYPES, MIME_TYPES } from "./constants";
import { ContextMenuItems } from "./components/ContextMenu";
import { Merge, ForwardRef } from "./utility-types";
import { Merge, ForwardRef, ValueOf } 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:
| typeof ALLOWED_IMAGE_MIME_TYPES[number]
| ValueOf<typeof IMAGE_MIME_TYPES>
// 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: typeof ALLOWED_IMAGE_MIME_TYPES[number];
mimeType: ValueOf<typeof IMAGE_MIME_TYPES>;
}
>;
files: BinaryFiles;
+3 -3
View File
@@ -10601,9 +10601,9 @@ webpack-sources@^3.2.3:
integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
webpack@^5.64.4:
version "5.75.0"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.75.0.tgz#1e440468647b2505860e94c9ff3e44d5b582c152"
integrity sha512-piaIaoVJlqMsPtX/+3KTTO6jfvrSYgauFVdt8cr9LTHKmcq/AMd4mhzsiP7ZF/PGRNPGA8336jldh9l2Kt2ogQ==
version "5.76.1"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.76.1.tgz#7773de017e988bccb0f13c7d75ec245f377d295c"
integrity sha512-4+YIK4Abzv8172/SGqObnUjaIHjLEuUasz9EwQj/9xmPPkYJy2Mh03Q/lJfSD3YLzbxy5FeTq5Uw0323Oh6SJQ==
dependencies:
"@types/eslint-scope" "^3.7.3"
"@types/estree" "^0.0.51"