Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3c83a322b6 |
+1
-1
@@ -4,5 +4,5 @@ REACT_APP_BACKEND_V2_POST_URL=https://json-dev.excalidraw.com/api/v2/post/
|
|||||||
REACT_APP_LIBRARY_URL=https://libraries.excalidraw.com
|
REACT_APP_LIBRARY_URL=https://libraries.excalidraw.com
|
||||||
REACT_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries
|
REACT_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries
|
||||||
|
|
||||||
REACT_APP_SOCKET_SERVER_URL=http://localhost:3002
|
REACT_APP_SOCKET_SERVER_URL=http://localhost:3000
|
||||||
REACT_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyCMkxA60XIW8KbqMYL7edC4qT5l4qHX2h8","authDomain":"excalidraw-oss-dev.firebaseapp.com","projectId":"excalidraw-oss-dev","storageBucket":"excalidraw-oss-dev.appspot.com","messagingSenderId":"664559512677","appId":"1:664559512677:web:a385181f2928d328a7aa8c"}'
|
REACT_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyCMkxA60XIW8KbqMYL7edC4qT5l4qHX2h8","authDomain":"excalidraw-oss-dev.firebaseapp.com","projectId":"excalidraw-oss-dev","storageBucket":"excalidraw-oss-dev.appspot.com","messagingSenderId":"664559512677","appId":"1:664559512677:web:a385181f2928d328a7aa8c"}'
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ jobs:
|
|||||||
- name: Setup Node.js 14.x
|
- name: Setup Node.js 14.x
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: 16.x
|
node-version: 14.x
|
||||||
- name: Install and test
|
- name: Install and test
|
||||||
run: |
|
run: |
|
||||||
yarn --frozen-lockfile
|
yarn --frozen-lockfile
|
||||||
|
|||||||
@@ -118,10 +118,6 @@ yarn start
|
|||||||
|
|
||||||
Now you can open [http://localhost:3000](http://localhost:3000) and start coding in your favorite code editor.
|
Now you can open [http://localhost:3000](http://localhost:3000) and start coding in your favorite code editor.
|
||||||
|
|
||||||
#### Collaboration
|
|
||||||
|
|
||||||
For collaboration, you will need to set up [collab server](https://github.com/excalidraw/excalidraw-room) in local.
|
|
||||||
|
|
||||||
#### Commands
|
#### Commands
|
||||||
|
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
|
|||||||
+10
-10
@@ -21,12 +21,12 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sentry/browser": "6.2.5",
|
"@sentry/browser": "6.2.5",
|
||||||
"@sentry/integrations": "6.2.5",
|
"@sentry/integrations": "6.2.5",
|
||||||
"@testing-library/jest-dom": "5.16.1",
|
"@testing-library/jest-dom": "5.15.1",
|
||||||
"@testing-library/react": "12.1.2",
|
"@testing-library/react": "12.1.2",
|
||||||
"@tldraw/vec": "1.4.3",
|
"@tldraw/vec": "1.1.5",
|
||||||
"@types/jest": "27.4.0",
|
"@types/jest": "27.0.3",
|
||||||
"@types/pica": "5.1.3",
|
"@types/pica": "5.1.3",
|
||||||
"@types/react": "17.0.38",
|
"@types/react": "17.0.37",
|
||||||
"@types/react-dom": "17.0.11",
|
"@types/react-dom": "17.0.11",
|
||||||
"@types/socket.io-client": "1.4.36",
|
"@types/socket.io-client": "1.4.36",
|
||||||
"browser-fs-access": "0.23.0",
|
"browser-fs-access": "0.23.0",
|
||||||
@@ -50,16 +50,16 @@
|
|||||||
"react-dom": "17.0.2",
|
"react-dom": "17.0.2",
|
||||||
"react-scripts": "4.0.3",
|
"react-scripts": "4.0.3",
|
||||||
"roughjs": "4.5.2",
|
"roughjs": "4.5.2",
|
||||||
"sass": "1.47.0",
|
"sass": "1.43.5",
|
||||||
"socket.io-client": "2.3.1",
|
"socket.io-client": "2.3.1",
|
||||||
"typescript": "4.5.4"
|
"typescript": "4.5.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@excalidraw/eslint-config": "1.0.0",
|
"@excalidraw/eslint-config": "1.0.0",
|
||||||
"@excalidraw/prettier-config": "1.0.2",
|
"@excalidraw/prettier-config": "1.0.2",
|
||||||
"@types/chai": "4.3.0",
|
"@types/chai": "4.2.22",
|
||||||
"@types/lodash.throttle": "4.1.6",
|
"@types/lodash.throttle": "4.1.6",
|
||||||
"@types/pako": "1.0.3",
|
"@types/pako": "1.0.2",
|
||||||
"@types/resize-observer-browser": "0.1.6",
|
"@types/resize-observer-browser": "0.1.6",
|
||||||
"chai": "4.3.4",
|
"chai": "4.3.4",
|
||||||
"dotenv": "10.0.0",
|
"dotenv": "10.0.0",
|
||||||
@@ -68,9 +68,9 @@
|
|||||||
"firebase-tools": "9.23.0",
|
"firebase-tools": "9.23.0",
|
||||||
"husky": "7.0.4",
|
"husky": "7.0.4",
|
||||||
"jest-canvas-mock": "2.3.1",
|
"jest-canvas-mock": "2.3.1",
|
||||||
"lint-staged": "12.1.7",
|
"lint-staged": "12.1.2",
|
||||||
"pepjs": "0.5.3",
|
"pepjs": "0.5.3",
|
||||||
"prettier": "2.5.1",
|
"prettier": "2.5.0",
|
||||||
"rewire": "5.0.0"
|
"rewire": "5.0.0"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
|
|||||||
@@ -14,10 +14,60 @@ import {
|
|||||||
bindOrUnbindLinearElement,
|
bindOrUnbindLinearElement,
|
||||||
} from "../element/binding";
|
} from "../element/binding";
|
||||||
import { isBindingElement } from "../element/typeChecks";
|
import { isBindingElement } from "../element/typeChecks";
|
||||||
|
import { ExcalidrawImageElement } from "../element/types";
|
||||||
|
import { imageFromImageData } from "../element/image";
|
||||||
|
|
||||||
export const actionFinalize = register({
|
export const actionFinalize = register({
|
||||||
name: "finalize",
|
name: "finalize",
|
||||||
perform: (elements, appState, _, { canvas, focusContainer }) => {
|
perform: (
|
||||||
|
elements,
|
||||||
|
appState,
|
||||||
|
_,
|
||||||
|
{ canvas, focusContainer, imageCache, addFiles },
|
||||||
|
) => {
|
||||||
|
if (appState.editingImageElement) {
|
||||||
|
const { elementId, imageData } = appState.editingImageElement;
|
||||||
|
const editingImageElement = elements.find((el) => el.id === elementId) as
|
||||||
|
| ExcalidrawImageElement
|
||||||
|
| undefined;
|
||||||
|
if (editingImageElement?.fileId) {
|
||||||
|
const cachedImageData = imageCache.get(editingImageElement.fileId);
|
||||||
|
if (cachedImageData) {
|
||||||
|
const { image, dataURL } = imageFromImageData(imageData);
|
||||||
|
|
||||||
|
imageCache.set(editingImageElement.fileId, {
|
||||||
|
...cachedImageData,
|
||||||
|
image,
|
||||||
|
});
|
||||||
|
|
||||||
|
addFiles([
|
||||||
|
{
|
||||||
|
id: editingImageElement.fileId,
|
||||||
|
dataURL,
|
||||||
|
mimeType: cachedImageData.mimeType,
|
||||||
|
created: Date.now(),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
appState: {
|
||||||
|
...appState,
|
||||||
|
editingImageElement: null,
|
||||||
|
},
|
||||||
|
commitToHistory: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
appState: {
|
||||||
|
...appState,
|
||||||
|
editingImageElement: null,
|
||||||
|
},
|
||||||
|
commitToHistory: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (appState.editingLinearElement) {
|
if (appState.editingLinearElement) {
|
||||||
const { elementId, startBindingElement, endBindingElement } =
|
const { elementId, startBindingElement, endBindingElement } =
|
||||||
appState.editingLinearElement;
|
appState.editingLinearElement;
|
||||||
@@ -162,6 +212,7 @@ export const actionFinalize = register({
|
|||||||
keyTest: (event, appState) =>
|
keyTest: (event, appState) =>
|
||||||
(event.key === KEYS.ESCAPE &&
|
(event.key === KEYS.ESCAPE &&
|
||||||
(appState.editingLinearElement !== null ||
|
(appState.editingLinearElement !== null ||
|
||||||
|
appState.editingImageElement !== null ||
|
||||||
(!appState.draggingElement && appState.multiElement === null))) ||
|
(!appState.draggingElement && appState.multiElement === null))) ||
|
||||||
((event.key === KEYS.ESCAPE || event.key === KEYS.ENTER) &&
|
((event.key === KEYS.ESCAPE || event.key === KEYS.ENTER) &&
|
||||||
appState.multiElement !== null),
|
appState.multiElement !== null),
|
||||||
|
|||||||
@@ -17,9 +17,8 @@ import {
|
|||||||
import { getNonDeletedElements } from "../element";
|
import { getNonDeletedElements } from "../element";
|
||||||
import { randomId } from "../random";
|
import { randomId } from "../random";
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import { ExcalidrawElement, ExcalidrawTextElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { isBoundToContainer } from "../element/typeChecks";
|
|
||||||
|
|
||||||
const allElementsInSameGroup = (elements: readonly ExcalidrawElement[]) => {
|
const allElementsInSameGroup = (elements: readonly ExcalidrawElement[]) => {
|
||||||
if (elements.length >= 2) {
|
if (elements.length >= 2) {
|
||||||
@@ -152,12 +151,7 @@ export const actionUngroup = register({
|
|||||||
if (groupIds.length === 0) {
|
if (groupIds.length === 0) {
|
||||||
return { appState, elements, commitToHistory: false };
|
return { appState, elements, commitToHistory: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
const boundTextElementIds: ExcalidrawTextElement["id"][] = [];
|
|
||||||
const nextElements = elements.map((element) => {
|
const nextElements = elements.map((element) => {
|
||||||
if (isBoundToContainer(element)) {
|
|
||||||
boundTextElementIds.push(element.id);
|
|
||||||
}
|
|
||||||
const nextGroupIds = removeFromSelectedGroups(
|
const nextGroupIds = removeFromSelectedGroups(
|
||||||
element.groupIds,
|
element.groupIds,
|
||||||
appState.selectedGroupIds,
|
appState.selectedGroupIds,
|
||||||
@@ -169,19 +163,11 @@ export const actionUngroup = register({
|
|||||||
groupIds: nextGroupIds,
|
groupIds: nextGroupIds,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateAppState = selectGroupsForSelectedElements(
|
|
||||||
{ ...appState, selectedGroupIds: {} },
|
|
||||||
getNonDeletedElements(nextElements),
|
|
||||||
);
|
|
||||||
|
|
||||||
// remove binded text elements from selection
|
|
||||||
boundTextElementIds.forEach(
|
|
||||||
(id) => (updateAppState.selectedElementIds[id] = false),
|
|
||||||
);
|
|
||||||
return {
|
return {
|
||||||
appState: updateAppState,
|
appState: selectGroupsForSelectedElements(
|
||||||
|
{ ...appState, selectedGroupIds: {} },
|
||||||
|
getNonDeletedElements(nextElements),
|
||||||
|
),
|
||||||
elements: nextElements,
|
elements: nextElements,
|
||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
||||||
|
import { ToolButton } from "../components/ToolButton";
|
||||||
|
import { backgroundIcon } from "../components/icons";
|
||||||
|
import { register } from "./register";
|
||||||
|
import { getNonDeletedElements } from "../element";
|
||||||
|
import { isInitializedImageElement } from "../element/typeChecks";
|
||||||
|
import Scene from "../scene/Scene";
|
||||||
|
|
||||||
|
export const actionEditImageAlpha = register({
|
||||||
|
name: "editImageAlpha",
|
||||||
|
perform: async (elements, appState, _, app) => {
|
||||||
|
if (appState.editingImageElement) {
|
||||||
|
return {
|
||||||
|
appState: {
|
||||||
|
...appState,
|
||||||
|
editingImageElement: null,
|
||||||
|
},
|
||||||
|
commitToHistory: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedElements = getSelectedElements(elements, appState);
|
||||||
|
const selectedElement = selectedElements[0];
|
||||||
|
if (
|
||||||
|
selectedElements.length === 1 &&
|
||||||
|
isInitializedImageElement(selectedElement)
|
||||||
|
) {
|
||||||
|
const imgData = app.imageCache.get(selectedElement.fileId);
|
||||||
|
if (!imgData) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const image = await imgData.image;
|
||||||
|
const { width, height } = image;
|
||||||
|
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
canvas.height = height;
|
||||||
|
canvas.width = width;
|
||||||
|
const context = canvas.getContext("2d")!;
|
||||||
|
|
||||||
|
context.drawImage(image, 0, 0, width, height);
|
||||||
|
|
||||||
|
const imageData = context.getImageData(0, 0, width, height);
|
||||||
|
|
||||||
|
Scene.mapElementToScene(selectedElement.id, app.scene);
|
||||||
|
|
||||||
|
return {
|
||||||
|
appState: {
|
||||||
|
...appState,
|
||||||
|
editingImageElement: {
|
||||||
|
editorType: "alpha",
|
||||||
|
elementId: selectedElement.id,
|
||||||
|
origImageData: imageData,
|
||||||
|
imageData,
|
||||||
|
pointerDownState: { screenX: 0, screenY: 0, sampledPixel: null },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
commitToHistory: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
PanelComponent: ({ elements, appState, updateData }) => (
|
||||||
|
<ToolButton
|
||||||
|
type="button"
|
||||||
|
icon={backgroundIcon}
|
||||||
|
label="Edit Image Alpha"
|
||||||
|
className={appState.editingImageElement ? "active" : ""}
|
||||||
|
title={"Edit image alpha"}
|
||||||
|
aria-label={"Edit image alpha"}
|
||||||
|
onClick={() => updateData(null)}
|
||||||
|
visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
});
|
||||||
@@ -41,16 +41,8 @@ import {
|
|||||||
isTextElement,
|
isTextElement,
|
||||||
redrawTextBoundingBox,
|
redrawTextBoundingBox,
|
||||||
} from "../element";
|
} from "../element";
|
||||||
import { mutateElement, newElementWith } from "../element/mutateElement";
|
import { newElementWith } from "../element/mutateElement";
|
||||||
import {
|
import { isLinearElement, isLinearElementType } from "../element/typeChecks";
|
||||||
getBoundTextElement,
|
|
||||||
getContainerElement,
|
|
||||||
} from "../element/textElement";
|
|
||||||
import {
|
|
||||||
isBoundToContainer,
|
|
||||||
isLinearElement,
|
|
||||||
isLinearElementType,
|
|
||||||
} from "../element/typeChecks";
|
|
||||||
import {
|
import {
|
||||||
Arrowhead,
|
Arrowhead,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
@@ -60,34 +52,25 @@ import {
|
|||||||
TextAlign,
|
TextAlign,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import { getLanguage, t } from "../i18n";
|
import { getLanguage, t } from "../i18n";
|
||||||
import { KEYS } from "../keys";
|
|
||||||
import { randomInteger } from "../random";
|
import { randomInteger } from "../random";
|
||||||
import {
|
import {
|
||||||
canChangeSharpness,
|
canChangeSharpness,
|
||||||
canHaveArrowheads,
|
canHaveArrowheads,
|
||||||
getCommonAttributeOfSelectedElements,
|
getCommonAttributeOfSelectedElements,
|
||||||
getSelectedElements,
|
|
||||||
getTargetElements,
|
getTargetElements,
|
||||||
isSomeElementSelected,
|
isSomeElementSelected,
|
||||||
} from "../scene";
|
} from "../scene";
|
||||||
import { hasStrokeColor } from "../scene/comparisons";
|
import { hasStrokeColor } from "../scene/comparisons";
|
||||||
import { arrayToMap } from "../utils";
|
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
const FONT_SIZE_RELATIVE_INCREASE_STEP = 0.1;
|
|
||||||
|
|
||||||
const changeProperty = (
|
const changeProperty = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
callback: (element: ExcalidrawElement) => ExcalidrawElement,
|
callback: (element: ExcalidrawElement) => ExcalidrawElement,
|
||||||
includeBoundText = false,
|
|
||||||
) => {
|
) => {
|
||||||
const selectedElementIds = arrayToMap(
|
|
||||||
getSelectedElements(elements, appState, includeBoundText),
|
|
||||||
);
|
|
||||||
return elements.map((element) => {
|
return elements.map((element) => {
|
||||||
if (
|
if (
|
||||||
selectedElementIds.get(element.id) ||
|
appState.selectedElementIds[element.id] ||
|
||||||
element.id === appState.editingElement?.id
|
element.id === appState.editingElement?.id
|
||||||
) {
|
) {
|
||||||
return callback(element);
|
return callback(element);
|
||||||
@@ -117,96 +100,18 @@ const getFormValue = function <T>(
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const offsetElementAfterFontResize = (
|
|
||||||
prevElement: ExcalidrawTextElement,
|
|
||||||
nextElement: ExcalidrawTextElement,
|
|
||||||
) => {
|
|
||||||
if (isBoundToContainer(nextElement)) {
|
|
||||||
return nextElement;
|
|
||||||
}
|
|
||||||
return mutateElement(
|
|
||||||
nextElement,
|
|
||||||
{
|
|
||||||
x:
|
|
||||||
prevElement.textAlign === "left"
|
|
||||||
? prevElement.x
|
|
||||||
: prevElement.x +
|
|
||||||
(prevElement.width - nextElement.width) /
|
|
||||||
(prevElement.textAlign === "center" ? 2 : 1),
|
|
||||||
// centering vertically is non-standard, but for Excalidraw I think
|
|
||||||
// it makes sense
|
|
||||||
y: prevElement.y + (prevElement.height - nextElement.height) / 2,
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const changeFontSize = (
|
|
||||||
elements: readonly ExcalidrawElement[],
|
|
||||||
appState: AppState,
|
|
||||||
getNewFontSize: (element: ExcalidrawTextElement) => number,
|
|
||||||
) => {
|
|
||||||
const newFontSizes = new Set<number>();
|
|
||||||
|
|
||||||
return {
|
|
||||||
elements: changeProperty(
|
|
||||||
elements,
|
|
||||||
appState,
|
|
||||||
(oldElement) => {
|
|
||||||
if (isTextElement(oldElement)) {
|
|
||||||
const newFontSize = getNewFontSize(oldElement);
|
|
||||||
newFontSizes.add(newFontSize);
|
|
||||||
|
|
||||||
let newElement: ExcalidrawTextElement = newElementWith(oldElement, {
|
|
||||||
fontSize: newFontSize,
|
|
||||||
});
|
|
||||||
redrawTextBoundingBox(
|
|
||||||
newElement,
|
|
||||||
getContainerElement(oldElement),
|
|
||||||
appState,
|
|
||||||
);
|
|
||||||
|
|
||||||
newElement = offsetElementAfterFontResize(oldElement, newElement);
|
|
||||||
|
|
||||||
return newElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
return oldElement;
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
),
|
|
||||||
appState: {
|
|
||||||
...appState,
|
|
||||||
// update state only if we've set all select text elements to
|
|
||||||
// the same font size
|
|
||||||
currentItemFontSize:
|
|
||||||
newFontSizes.size === 1
|
|
||||||
? [...newFontSizes][0]
|
|
||||||
: appState.currentItemFontSize,
|
|
||||||
},
|
|
||||||
commitToHistory: true,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
export const actionChangeStrokeColor = register({
|
export const actionChangeStrokeColor = register({
|
||||||
name: "changeStrokeColor",
|
name: "changeStrokeColor",
|
||||||
perform: (elements, appState, value) => {
|
perform: (elements, appState, value) => {
|
||||||
return {
|
return {
|
||||||
...(value.currentItemStrokeColor && {
|
...(value.currentItemStrokeColor && {
|
||||||
elements: changeProperty(
|
elements: changeProperty(elements, appState, (el) => {
|
||||||
elements,
|
return hasStrokeColor(el.type)
|
||||||
appState,
|
? newElementWith(el, {
|
||||||
(el) => {
|
strokeColor: value.currentItemStrokeColor,
|
||||||
return hasStrokeColor(el.type)
|
})
|
||||||
? newElementWith(el, {
|
: el;
|
||||||
strokeColor: value.currentItemStrokeColor,
|
}),
|
||||||
})
|
|
||||||
: el;
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
),
|
|
||||||
}),
|
}),
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
@@ -520,7 +425,24 @@ export const actionChangeOpacity = register({
|
|||||||
export const actionChangeFontSize = register({
|
export const actionChangeFontSize = register({
|
||||||
name: "changeFontSize",
|
name: "changeFontSize",
|
||||||
perform: (elements, appState, value) => {
|
perform: (elements, appState, value) => {
|
||||||
return changeFontSize(elements, appState, () => value);
|
return {
|
||||||
|
elements: changeProperty(elements, appState, (el) => {
|
||||||
|
if (isTextElement(el)) {
|
||||||
|
const element: ExcalidrawTextElement = newElementWith(el, {
|
||||||
|
fontSize: value,
|
||||||
|
});
|
||||||
|
redrawTextBoundingBox(element);
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
return el;
|
||||||
|
}),
|
||||||
|
appState: {
|
||||||
|
...appState,
|
||||||
|
currentItemFontSize: value,
|
||||||
|
},
|
||||||
|
commitToHistory: true,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData }) => (
|
||||||
<fieldset>
|
<fieldset>
|
||||||
@@ -552,16 +474,7 @@ export const actionChangeFontSize = register({
|
|||||||
value={getFormValue(
|
value={getFormValue(
|
||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
(element) => {
|
(element) => isTextElement(element) && element.fontSize,
|
||||||
if (isTextElement(element)) {
|
|
||||||
return element.fontSize;
|
|
||||||
}
|
|
||||||
const boundTextElement = getBoundTextElement(element);
|
|
||||||
if (boundTextElement) {
|
|
||||||
return boundTextElement.fontSize;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
appState.currentItemFontSize || DEFAULT_FONT_SIZE,
|
appState.currentItemFontSize || DEFAULT_FONT_SIZE,
|
||||||
)}
|
)}
|
||||||
onChange={(value) => updateData(value)}
|
onChange={(value) => updateData(value)}
|
||||||
@@ -570,71 +483,21 @@ export const actionChangeFontSize = register({
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionDecreaseFontSize = register({
|
|
||||||
name: "decreaseFontSize",
|
|
||||||
perform: (elements, appState, value) => {
|
|
||||||
return changeFontSize(elements, appState, (element) =>
|
|
||||||
Math.round(
|
|
||||||
// get previous value before relative increase (doesn't work fully
|
|
||||||
// due to rounding and float precision issues)
|
|
||||||
(1 / (1 + FONT_SIZE_RELATIVE_INCREASE_STEP)) * element.fontSize,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
keyTest: (event) => {
|
|
||||||
return (
|
|
||||||
event[KEYS.CTRL_OR_CMD] &&
|
|
||||||
event.shiftKey &&
|
|
||||||
// KEYS.COMMA needed for MacOS
|
|
||||||
(event.key === KEYS.CHEVRON_LEFT || event.key === KEYS.COMMA)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const actionIncreaseFontSize = register({
|
|
||||||
name: "increaseFontSize",
|
|
||||||
perform: (elements, appState, value) => {
|
|
||||||
return changeFontSize(elements, appState, (element) =>
|
|
||||||
Math.round(element.fontSize * (1 + FONT_SIZE_RELATIVE_INCREASE_STEP)),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
keyTest: (event) => {
|
|
||||||
return (
|
|
||||||
event[KEYS.CTRL_OR_CMD] &&
|
|
||||||
event.shiftKey &&
|
|
||||||
// KEYS.PERIOD needed for MacOS
|
|
||||||
(event.key === KEYS.CHEVRON_RIGHT || event.key === KEYS.PERIOD)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const actionChangeFontFamily = register({
|
export const actionChangeFontFamily = register({
|
||||||
name: "changeFontFamily",
|
name: "changeFontFamily",
|
||||||
perform: (elements, appState, value) => {
|
perform: (elements, appState, value) => {
|
||||||
return {
|
return {
|
||||||
elements: changeProperty(
|
elements: changeProperty(elements, appState, (el) => {
|
||||||
elements,
|
if (isTextElement(el)) {
|
||||||
appState,
|
const element: ExcalidrawTextElement = newElementWith(el, {
|
||||||
(oldElement) => {
|
fontFamily: value,
|
||||||
if (isTextElement(oldElement)) {
|
});
|
||||||
const newElement: ExcalidrawTextElement = newElementWith(
|
redrawTextBoundingBox(element);
|
||||||
oldElement,
|
return element;
|
||||||
{
|
}
|
||||||
fontFamily: value,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
redrawTextBoundingBox(
|
|
||||||
newElement,
|
|
||||||
getContainerElement(oldElement),
|
|
||||||
appState,
|
|
||||||
);
|
|
||||||
return newElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
return oldElement;
|
return el;
|
||||||
},
|
}),
|
||||||
true,
|
|
||||||
),
|
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
currentItemFontFamily: value,
|
currentItemFontFamily: value,
|
||||||
@@ -674,16 +537,7 @@ export const actionChangeFontFamily = register({
|
|||||||
value={getFormValue(
|
value={getFormValue(
|
||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
(element) => {
|
(element) => isTextElement(element) && element.fontFamily,
|
||||||
if (isTextElement(element)) {
|
|
||||||
return element.fontFamily;
|
|
||||||
}
|
|
||||||
const boundTextElement = getBoundTextElement(element);
|
|
||||||
if (boundTextElement) {
|
|
||||||
return boundTextElement.fontFamily;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
appState.currentItemFontFamily || DEFAULT_FONT_FAMILY,
|
appState.currentItemFontFamily || DEFAULT_FONT_FAMILY,
|
||||||
)}
|
)}
|
||||||
onChange={(value) => updateData(value)}
|
onChange={(value) => updateData(value)}
|
||||||
@@ -697,29 +551,17 @@ export const actionChangeTextAlign = register({
|
|||||||
name: "changeTextAlign",
|
name: "changeTextAlign",
|
||||||
perform: (elements, appState, value) => {
|
perform: (elements, appState, value) => {
|
||||||
return {
|
return {
|
||||||
elements: changeProperty(
|
elements: changeProperty(elements, appState, (el) => {
|
||||||
elements,
|
if (isTextElement(el)) {
|
||||||
appState,
|
const element: ExcalidrawTextElement = newElementWith(el, {
|
||||||
(oldElement) => {
|
textAlign: value,
|
||||||
if (isTextElement(oldElement)) {
|
});
|
||||||
const newElement: ExcalidrawTextElement = newElementWith(
|
redrawTextBoundingBox(element);
|
||||||
oldElement,
|
return element;
|
||||||
{
|
}
|
||||||
textAlign: value,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
redrawTextBoundingBox(
|
|
||||||
newElement,
|
|
||||||
getContainerElement(oldElement),
|
|
||||||
appState,
|
|
||||||
);
|
|
||||||
return newElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
return oldElement;
|
return el;
|
||||||
},
|
}),
|
||||||
true,
|
|
||||||
),
|
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
currentItemTextAlign: value,
|
currentItemTextAlign: value,
|
||||||
@@ -752,16 +594,7 @@ export const actionChangeTextAlign = register({
|
|||||||
value={getFormValue(
|
value={getFormValue(
|
||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
(element) => {
|
(element) => isTextElement(element) && element.textAlign,
|
||||||
if (isTextElement(element)) {
|
|
||||||
return element.textAlign;
|
|
||||||
}
|
|
||||||
const boundTextElement = getBoundTextElement(element);
|
|
||||||
if (boundTextElement) {
|
|
||||||
return boundTextElement.textAlign;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
appState.currentItemTextAlign,
|
appState.currentItemTextAlign,
|
||||||
)}
|
)}
|
||||||
onChange={(value) => updateData(value)}
|
onChange={(value) => updateData(value)}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
DEFAULT_FONT_FAMILY,
|
DEFAULT_FONT_FAMILY,
|
||||||
DEFAULT_TEXT_ALIGN,
|
DEFAULT_TEXT_ALIGN,
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
import { getContainerElement } from "../element/textElement";
|
|
||||||
|
|
||||||
// `copiedStyles` is exported only for tests.
|
// `copiedStyles` is exported only for tests.
|
||||||
export let copiedStyles: string = "{}";
|
export let copiedStyles: string = "{}";
|
||||||
@@ -56,18 +55,13 @@ export const actionPasteStyles = register({
|
|||||||
opacity: pastedElement?.opacity,
|
opacity: pastedElement?.opacity,
|
||||||
roughness: pastedElement?.roughness,
|
roughness: pastedElement?.roughness,
|
||||||
});
|
});
|
||||||
if (isTextElement(newElement) && isTextElement(element)) {
|
if (isTextElement(newElement)) {
|
||||||
mutateElement(newElement, {
|
mutateElement(newElement, {
|
||||||
fontSize: pastedElement?.fontSize || DEFAULT_FONT_SIZE,
|
fontSize: pastedElement?.fontSize || DEFAULT_FONT_SIZE,
|
||||||
fontFamily: pastedElement?.fontFamily || DEFAULT_FONT_FAMILY,
|
fontFamily: pastedElement?.fontFamily || DEFAULT_FONT_FAMILY,
|
||||||
textAlign: pastedElement?.textAlign || DEFAULT_TEXT_ALIGN,
|
textAlign: pastedElement?.textAlign || DEFAULT_TEXT_ALIGN,
|
||||||
});
|
});
|
||||||
|
redrawTextBoundingBox(newElement);
|
||||||
redrawTextBoundingBox(
|
|
||||||
element,
|
|
||||||
getContainerElement(element),
|
|
||||||
appState,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return newElement;
|
return newElement;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,3 +80,4 @@ export { actionToggleGridMode } from "./actionToggleGridMode";
|
|||||||
export { actionToggleZenMode } from "./actionToggleZenMode";
|
export { actionToggleZenMode } from "./actionToggleZenMode";
|
||||||
|
|
||||||
export { actionToggleStats } from "./actionToggleStats";
|
export { actionToggleStats } from "./actionToggleStats";
|
||||||
|
export { actionEditImageAlpha } from "./actionImageEditing";
|
||||||
|
|||||||
@@ -102,8 +102,7 @@ export type ActionName =
|
|||||||
| "viewMode"
|
| "viewMode"
|
||||||
| "exportWithDarkMode"
|
| "exportWithDarkMode"
|
||||||
| "toggleTheme"
|
| "toggleTheme"
|
||||||
| "increaseFontSize"
|
| "editImageAlpha";
|
||||||
| "decreaseFontSize";
|
|
||||||
|
|
||||||
export type PanelComponentProps = {
|
export type PanelComponentProps = {
|
||||||
elements: readonly ExcalidrawElement[];
|
elements: readonly ExcalidrawElement[];
|
||||||
|
|||||||
+22
-1
@@ -1,7 +1,6 @@
|
|||||||
import { ExcalidrawElement } from "./element/types";
|
import { ExcalidrawElement } from "./element/types";
|
||||||
import { newElementWith } from "./element/mutateElement";
|
import { newElementWith } from "./element/mutateElement";
|
||||||
import { Box, getCommonBoundingBox } from "./element/bounds";
|
import { Box, getCommonBoundingBox } from "./element/bounds";
|
||||||
import { getMaximumGroups } from "./groups";
|
|
||||||
|
|
||||||
export interface Alignment {
|
export interface Alignment {
|
||||||
position: "start" | "center" | "end";
|
position: "start" | "center" | "end";
|
||||||
@@ -31,6 +30,28 @@ export const alignElements = (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getMaximumGroups = (
|
||||||
|
elements: ExcalidrawElement[],
|
||||||
|
): ExcalidrawElement[][] => {
|
||||||
|
const groups: Map<String, ExcalidrawElement[]> = new Map<
|
||||||
|
String,
|
||||||
|
ExcalidrawElement[]
|
||||||
|
>();
|
||||||
|
|
||||||
|
elements.forEach((element: ExcalidrawElement) => {
|
||||||
|
const groupId =
|
||||||
|
element.groupIds.length === 0
|
||||||
|
? element.id
|
||||||
|
: element.groupIds[element.groupIds.length - 1];
|
||||||
|
|
||||||
|
const currentGroupMembers = groups.get(groupId) || [];
|
||||||
|
|
||||||
|
groups.set(groupId, [...currentGroupMembers, element]);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Array.from(groups.values());
|
||||||
|
};
|
||||||
|
|
||||||
const calculateTranslation = (
|
const calculateTranslation = (
|
||||||
group: ExcalidrawElement[],
|
group: ExcalidrawElement[],
|
||||||
selectionBoundingBox: Box,
|
selectionBoundingBox: Box,
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export const getDefaultAppState = (): Omit<
|
|||||||
editingElement: null,
|
editingElement: null,
|
||||||
editingGroupId: null,
|
editingGroupId: null,
|
||||||
editingLinearElement: null,
|
editingLinearElement: null,
|
||||||
|
editingImageElement: null,
|
||||||
elementLocked: false,
|
elementLocked: false,
|
||||||
elementType: "selection",
|
elementType: "selection",
|
||||||
errorMessage: null,
|
errorMessage: null,
|
||||||
@@ -125,6 +126,7 @@ const APP_STATE_STORAGE_CONF = (<
|
|||||||
editingElement: { browser: false, export: false, server: false },
|
editingElement: { browser: false, export: false, server: false },
|
||||||
editingGroupId: { browser: true, export: false, server: false },
|
editingGroupId: { browser: true, export: false, server: false },
|
||||||
editingLinearElement: { browser: false, export: false, server: false },
|
editingLinearElement: { browser: false, export: false, server: false },
|
||||||
|
editingImageElement: { browser: false, export: false, server: false },
|
||||||
elementLocked: { browser: true, export: false, server: false },
|
elementLocked: { browser: true, export: false, server: false },
|
||||||
elementType: { browser: true, export: false, server: false },
|
elementType: { browser: true, export: false, server: false },
|
||||||
errorMessage: { browser: false, export: false, server: false },
|
errorMessage: { browser: false, export: false, server: false },
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { capitalizeString, isTransparent, setCursorForShape } from "../utils";
|
|||||||
import Stack from "./Stack";
|
import Stack from "./Stack";
|
||||||
import { ToolButton } from "./ToolButton";
|
import { ToolButton } from "./ToolButton";
|
||||||
import { hasStrokeColor } from "../scene/comparisons";
|
import { hasStrokeColor } from "../scene/comparisons";
|
||||||
|
import { isImageElement } from "../element/typeChecks";
|
||||||
|
|
||||||
export const SelectedShapeActions = ({
|
export const SelectedShapeActions = ({
|
||||||
appState,
|
appState,
|
||||||
@@ -105,6 +106,13 @@ export const SelectedShapeActions = ({
|
|||||||
<>{renderAction("changeArrowhead")}</>
|
<>{renderAction("changeArrowhead")}</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<div className="buttonList">
|
||||||
|
{targetElements.some((element) => isImageElement(element)) &&
|
||||||
|
renderAction("editImageAlpha")}
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
{renderAction("changeOpacity")}
|
{renderAction("changeOpacity")}
|
||||||
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
|
|||||||
+65
-75
@@ -72,12 +72,7 @@ import {
|
|||||||
import { loadFromBlob } from "../data";
|
import { loadFromBlob } from "../data";
|
||||||
import { isValidLibrary } from "../data/json";
|
import { isValidLibrary } from "../data/json";
|
||||||
import Library from "../data/library";
|
import Library from "../data/library";
|
||||||
import {
|
import { restore, restoreElements, restoreLibraryItems } from "../data/restore";
|
||||||
restore,
|
|
||||||
restoreAppState,
|
|
||||||
restoreElements,
|
|
||||||
restoreLibraryItems,
|
|
||||||
} from "../data/restore";
|
|
||||||
import {
|
import {
|
||||||
dragNewElement,
|
dragNewElement,
|
||||||
dragSelectedElements,
|
dragSelectedElements,
|
||||||
@@ -128,7 +123,6 @@ import {
|
|||||||
hasBoundTextElement,
|
hasBoundTextElement,
|
||||||
isBindingElement,
|
isBindingElement,
|
||||||
isBindingElementType,
|
isBindingElementType,
|
||||||
isBoundToContainer,
|
|
||||||
isImageElement,
|
isImageElement,
|
||||||
isInitializedImageElement,
|
isInitializedImageElement,
|
||||||
isLinearElement,
|
isLinearElement,
|
||||||
@@ -243,6 +237,7 @@ import {
|
|||||||
getBoundTextElementId,
|
getBoundTextElementId,
|
||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
import { isHittingElementNotConsideringBoundingBox } from "../element/collision";
|
import { isHittingElementNotConsideringBoundingBox } from "../element/collision";
|
||||||
|
import { ImageEditor } from "../element/imageEditor";
|
||||||
|
|
||||||
const IsMobileContext = React.createContext(false);
|
const IsMobileContext = React.createContext(false);
|
||||||
export const useIsMobile = () => useContext(IsMobileContext);
|
export const useIsMobile = () => useContext(IsMobileContext);
|
||||||
@@ -287,7 +282,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
UIOptions: DEFAULT_UI_OPTIONS,
|
UIOptions: DEFAULT_UI_OPTIONS,
|
||||||
};
|
};
|
||||||
|
|
||||||
private scene: Scene;
|
public scene: Scene;
|
||||||
private resizeObserver: ResizeObserver | undefined;
|
private resizeObserver: ResizeObserver | undefined;
|
||||||
private nearestScrollableContainer: HTMLElement | Document | undefined;
|
private nearestScrollableContainer: HTMLElement | Document | undefined;
|
||||||
public library: AppClassProperties["library"];
|
public library: AppClassProperties["library"];
|
||||||
@@ -923,16 +918,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
window.removeEventListener(EVENT.RESIZE, this.onResize, false);
|
window.removeEventListener(EVENT.RESIZE, this.onResize, false);
|
||||||
window.removeEventListener(EVENT.UNLOAD, this.onUnload, false);
|
window.removeEventListener(EVENT.UNLOAD, this.onUnload, false);
|
||||||
window.removeEventListener(EVENT.BLUR, this.onBlur, false);
|
window.removeEventListener(EVENT.BLUR, this.onBlur, false);
|
||||||
this.excalidrawContainerRef.current?.removeEventListener(
|
window.removeEventListener(EVENT.DRAG_OVER, this.disableEvent, false);
|
||||||
EVENT.DRAG_OVER,
|
window.removeEventListener(EVENT.DROP, this.disableEvent, false);
|
||||||
this.disableEvent,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
this.excalidrawContainerRef.current?.removeEventListener(
|
|
||||||
EVENT.DROP,
|
|
||||||
this.disableEvent,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
document.removeEventListener(
|
document.removeEventListener(
|
||||||
EVENT.GESTURE_START,
|
EVENT.GESTURE_START,
|
||||||
@@ -1001,16 +988,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
window.addEventListener(EVENT.RESIZE, this.onResize, false);
|
window.addEventListener(EVENT.RESIZE, this.onResize, false);
|
||||||
window.addEventListener(EVENT.UNLOAD, this.onUnload, false);
|
window.addEventListener(EVENT.UNLOAD, this.onUnload, false);
|
||||||
window.addEventListener(EVENT.BLUR, this.onBlur, false);
|
window.addEventListener(EVENT.BLUR, this.onBlur, false);
|
||||||
this.excalidrawContainerRef.current?.addEventListener(
|
window.addEventListener(EVENT.DRAG_OVER, this.disableEvent, false);
|
||||||
EVENT.DRAG_OVER,
|
window.addEventListener(EVENT.DROP, this.disableEvent, false);
|
||||||
this.disableEvent,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
this.excalidrawContainerRef.current?.addEventListener(
|
|
||||||
EVENT.DROP,
|
|
||||||
this.disableEvent,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps: AppProps, prevState: AppState) {
|
componentDidUpdate(prevProps: AppProps, prevState: AppState) {
|
||||||
@@ -1053,8 +1032,14 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.state.editingLinearElement &&
|
(this.state.editingLinearElement &&
|
||||||
!this.state.selectedElementIds[this.state.editingLinearElement.elementId]
|
!this.state.selectedElementIds[
|
||||||
|
this.state.editingLinearElement.elementId
|
||||||
|
]) ||
|
||||||
|
(this.state.editingImageElement &&
|
||||||
|
!this.state.selectedElementIds[
|
||||||
|
this.state.editingImageElement.elementId
|
||||||
|
])
|
||||||
) {
|
) {
|
||||||
// defer so that the commitToHistory flag isn't reset via current update
|
// defer so that the commitToHistory flag isn't reset via current update
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -1157,6 +1142,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
imageCache: this.imageCache,
|
imageCache: this.imageCache,
|
||||||
isExporting: false,
|
isExporting: false,
|
||||||
renderScrollbars: !this.isMobile,
|
renderScrollbars: !this.isMobile,
|
||||||
|
editingImageElement: this.state.editingImageElement,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (scrollBars) {
|
if (scrollBars) {
|
||||||
@@ -1311,8 +1297,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// prefer spreadsheet data over image file (MS Office/Libre Office)
|
if (isSupportedImageFile(file)) {
|
||||||
if (isSupportedImageFile(file) && !data.spreadsheet) {
|
|
||||||
const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords(
|
const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords(
|
||||||
{ clientX: cursorX, clientY: cursorY },
|
{ clientX: cursorX, clientY: cursorY },
|
||||||
this.state,
|
this.state,
|
||||||
@@ -1426,7 +1411,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
...this.state,
|
...this.state,
|
||||||
isLibraryOpen: false,
|
isLibraryOpen: false,
|
||||||
selectedElementIds: newElements.reduce((map, element) => {
|
selectedElementIds: newElements.reduce((map, element) => {
|
||||||
if (!isBoundToContainer(element)) {
|
if (isTextElement(element) && !element.containerId) {
|
||||||
map[element.id] = true;
|
map[element.id] = true;
|
||||||
}
|
}
|
||||||
return map;
|
return map;
|
||||||
@@ -1588,31 +1573,22 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
appState?: Pick<AppState, K> | null;
|
appState?: Pick<AppState, K> | null;
|
||||||
collaborators?: SceneData["collaborators"];
|
collaborators?: SceneData["collaborators"];
|
||||||
commitToHistory?: SceneData["commitToHistory"];
|
commitToHistory?: SceneData["commitToHistory"];
|
||||||
libraryItems?: SceneData["libraryItems"];
|
|
||||||
}) => {
|
}) => {
|
||||||
if (sceneData.commitToHistory) {
|
if (sceneData.commitToHistory) {
|
||||||
this.history.resumeRecording();
|
this.history.resumeRecording();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sceneData.appState) {
|
if (sceneData.appState) {
|
||||||
this.setState(restoreAppState(sceneData.appState, null));
|
this.setState(sceneData.appState);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sceneData.elements) {
|
if (sceneData.elements) {
|
||||||
this.scene.replaceAllElements(
|
this.scene.replaceAllElements(sceneData.elements);
|
||||||
restoreElements(sceneData.elements, null),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sceneData.collaborators) {
|
if (sceneData.collaborators) {
|
||||||
this.setState({ collaborators: sceneData.collaborators });
|
this.setState({ collaborators: sceneData.collaborators });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sceneData.libraryItems) {
|
|
||||||
this.library.saveLibrary(
|
|
||||||
restoreLibraryItems(sceneData.libraryItems, "unpublished"),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1656,10 +1632,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(isWritableElement(event.target) &&
|
(isWritableElement(event.target) && event.key !== KEYS.ESCAPE) ||
|
||||||
event.key !== KEYS.ESCAPE &&
|
|
||||||
// handle cmd/ctrl-modifier shortcuts even inside inputs
|
|
||||||
!event[KEYS.CTRL_OR_CMD]) ||
|
|
||||||
// case: using arrows to move between buttons
|
// case: using arrows to move between buttons
|
||||||
(isArrowKey(event.key) && isInputLike(event.target))
|
(isArrowKey(event.key) && isInputLike(event.target))
|
||||||
) {
|
) {
|
||||||
@@ -1698,11 +1671,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
? ELEMENT_SHIFT_TRANSLATE_AMOUNT
|
? ELEMENT_SHIFT_TRANSLATE_AMOUNT
|
||||||
: ELEMENT_TRANSLATE_AMOUNT);
|
: ELEMENT_TRANSLATE_AMOUNT);
|
||||||
|
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = this.scene
|
||||||
this.scene.getElements(),
|
.getElements()
|
||||||
this.state,
|
.filter((element) => this.state.selectedElementIds[element.id]);
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
let offsetX = 0;
|
let offsetX = 0;
|
||||||
let offsetY = 0;
|
let offsetY = 0;
|
||||||
@@ -1783,7 +1754,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
if (event.key === KEYS.SPACE && gesture.pointers.size === 0) {
|
if (event.key === KEYS.SPACE && gesture.pointers.size === 0) {
|
||||||
isHoldingSpace = true;
|
isHoldingSpace = true;
|
||||||
setCursor(this.canvas, CURSOR_TYPE.GRABBING);
|
setCursor(this.canvas, CURSOR_TYPE.GRABBING);
|
||||||
event.preventDefault();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.key === KEYS.G || event.key === KEYS.S) {
|
if (event.key === KEYS.G || event.key === KEYS.S) {
|
||||||
@@ -1921,8 +1891,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
const updateElement = (
|
const updateElement = (
|
||||||
text: string,
|
text: string,
|
||||||
originalText: string,
|
originalText: string,
|
||||||
isDeleted: boolean,
|
isDeleted = false,
|
||||||
isSubmit: boolean,
|
updateDimensions = false,
|
||||||
) => {
|
) => {
|
||||||
this.scene.replaceAllElements([
|
this.scene.replaceAllElements([
|
||||||
...this.scene.getElementsIncludingDeleted().map((_element) => {
|
...this.scene.getElementsIncludingDeleted().map((_element) => {
|
||||||
@@ -1934,7 +1904,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
isDeleted,
|
isDeleted,
|
||||||
originalText,
|
originalText,
|
||||||
},
|
},
|
||||||
isSubmit,
|
updateDimensions,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return _element;
|
return _element;
|
||||||
@@ -1960,7 +1930,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
];
|
];
|
||||||
},
|
},
|
||||||
onChange: withBatchedUpdates((text) => {
|
onChange: withBatchedUpdates((text) => {
|
||||||
updateElement(text, text, false, false);
|
updateElement(text, text, false, !element.containerId);
|
||||||
if (isNonDeletedElement(element)) {
|
if (isNonDeletedElement(element)) {
|
||||||
updateBoundElements(element);
|
updateBoundElements(element);
|
||||||
}
|
}
|
||||||
@@ -2006,7 +1976,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
// do an initial update to re-initialize element position since we were
|
// do an initial update to re-initialize element position since we were
|
||||||
// modifying element's x/y for sake of editor (case: syncing to remote)
|
// modifying element's x/y for sake of editor (case: syncing to remote)
|
||||||
updateElement(element.text, element.originalText, false, false);
|
updateElement(element.text, element.originalText);
|
||||||
}
|
}
|
||||||
|
|
||||||
private deselectElements() {
|
private deselectElements() {
|
||||||
@@ -2104,7 +2074,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
/** whether to attempt to insert at element center if applicable */
|
/** whether to attempt to insert at element center if applicable */
|
||||||
insertAtParentCenter?: boolean;
|
insertAtParentCenter?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
let parentCenterPosition =
|
const parentCenterPosition =
|
||||||
insertAtParentCenter &&
|
insertAtParentCenter &&
|
||||||
this.getTextWysiwygSnappedToCenterPosition(
|
this.getTextWysiwygSnappedToCenterPosition(
|
||||||
sceneX,
|
sceneX,
|
||||||
@@ -2119,9 +2089,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
const container =
|
const container =
|
||||||
shouldBind || parentCenterPosition
|
shouldBind || parentCenterPosition
|
||||||
? getElementContainingPosition(
|
? getElementContainingPosition(
|
||||||
this.scene.getElements().filter((ele) => !isTextElement(ele)),
|
this.scene.getElements(),
|
||||||
sceneX,
|
sceneX,
|
||||||
sceneY,
|
sceneY,
|
||||||
|
"text",
|
||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
@@ -2148,15 +2119,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
mutateElement(container, { height: newHeight, width: newWidth });
|
mutateElement(container, { height: newHeight, width: newWidth });
|
||||||
sceneX = container.x + newWidth / 2;
|
sceneX = container.x + newWidth / 2;
|
||||||
sceneY = container.y + newHeight / 2;
|
sceneY = container.y + newHeight / 2;
|
||||||
if (parentCenterPosition) {
|
|
||||||
parentCenterPosition = this.getTextWysiwygSnappedToCenterPosition(
|
|
||||||
sceneX,
|
|
||||||
sceneY,
|
|
||||||
this.state,
|
|
||||||
this.canvas,
|
|
||||||
window.devicePixelRatio,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const element = existingTextElement
|
const element = existingTextElement
|
||||||
@@ -2186,7 +2148,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
? "middle"
|
? "middle"
|
||||||
: DEFAULT_VERTICAL_ALIGN,
|
: DEFAULT_VERTICAL_ALIGN,
|
||||||
containerId: container?.id ?? undefined,
|
containerId: container?.id ?? undefined,
|
||||||
groupIds: container?.groupIds ?? [],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.setState({ editingElement: element });
|
this.setState({ editingElement: element });
|
||||||
@@ -2377,6 +2338,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
const scenePointer = viewportCoordsToSceneCoords(event, this.state);
|
const scenePointer = viewportCoordsToSceneCoords(event, this.state);
|
||||||
const { x: scenePointerX, y: scenePointerY } = scenePointer;
|
const { x: scenePointerX, y: scenePointerY } = scenePointer;
|
||||||
|
|
||||||
|
if (this.state.editingImageElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.state.editingLinearElement &&
|
this.state.editingLinearElement &&
|
||||||
!this.state.editingLinearElement.isDragging
|
!this.state.editingLinearElement.isDragging
|
||||||
@@ -2745,13 +2710,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
(event.button === POINTER_BUTTON.WHEEL ||
|
(event.button === POINTER_BUTTON.WHEEL ||
|
||||||
(event.button === POINTER_BUTTON.MAIN && isHoldingSpace) ||
|
(event.button === POINTER_BUTTON.MAIN && isHoldingSpace) ||
|
||||||
this.state.viewModeEnabled)
|
this.state.viewModeEnabled)
|
||||||
) ||
|
)
|
||||||
isTextElement(this.state.editingElement)
|
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
isPanning = true;
|
isPanning = true;
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
let nextPastePrevented = false;
|
let nextPastePrevented = false;
|
||||||
const isLinux = /Linux/.test(window.navigator.platform);
|
const isLinux = /Linux/.test(window.navigator.platform);
|
||||||
@@ -2969,6 +2932,14 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
pointerDownState: PointerDownState,
|
pointerDownState: PointerDownState,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
if (this.state.elementType === "selection") {
|
if (this.state.elementType === "selection") {
|
||||||
|
if (this.state.editingImageElement) {
|
||||||
|
ImageEditor.handlePointerDown(
|
||||||
|
this.state.editingImageElement,
|
||||||
|
pointerDownState.origin,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const elements = this.scene.getElements();
|
const elements = this.scene.getElements();
|
||||||
const selectedElements = getSelectedElements(elements, this.state);
|
const selectedElements = getSelectedElements(elements, this.state);
|
||||||
if (selectedElements.length === 1 && !this.state.editingLinearElement) {
|
if (selectedElements.length === 1 && !this.state.editingLinearElement) {
|
||||||
@@ -3529,6 +3500,22 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.state.editingImageElement) {
|
||||||
|
const newImageData = ImageEditor.handlePointerMove(
|
||||||
|
this.state.editingImageElement,
|
||||||
|
pointerCoords,
|
||||||
|
);
|
||||||
|
if (newImageData) {
|
||||||
|
this.setState({
|
||||||
|
editingImageElement: {
|
||||||
|
...this.state.editingImageElement,
|
||||||
|
imageData: newImageData,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.state.editingLinearElement) {
|
if (this.state.editingLinearElement) {
|
||||||
const didDrag = LinearElementEditor.handlePointDragging(
|
const didDrag = LinearElementEditor.handlePointDragging(
|
||||||
this.state,
|
this.state,
|
||||||
@@ -3598,7 +3585,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
lockDirection,
|
lockDirection,
|
||||||
dragDistanceX,
|
dragDistanceX,
|
||||||
dragDistanceY,
|
dragDistanceY,
|
||||||
this.state,
|
|
||||||
);
|
);
|
||||||
this.maybeSuggestBindingForAll(selectedElements);
|
this.maybeSuggestBindingForAll(selectedElements);
|
||||||
|
|
||||||
@@ -3852,6 +3838,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
this.savePointer(childEvent.clientX, childEvent.clientY, "up");
|
this.savePointer(childEvent.clientX, childEvent.clientY, "up");
|
||||||
|
|
||||||
|
if (this.state.editingImageElement) {
|
||||||
|
ImageEditor.handlePointerUp(this.state.editingImageElement);
|
||||||
|
}
|
||||||
|
|
||||||
// Handle end of dragging a point of a linear element, might close a loop
|
// Handle end of dragging a point of a linear element, might close a loop
|
||||||
// and sets binding element
|
// and sets binding element
|
||||||
if (this.state.editingLinearElement) {
|
if (this.state.editingLinearElement) {
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
|||||||
<Shortcut label={t("toolBar.line")} shortcuts={["P", "6"]} />
|
<Shortcut label={t("toolBar.line")} shortcuts={["P", "6"]} />
|
||||||
<Shortcut
|
<Shortcut
|
||||||
label={t("toolBar.freedraw")}
|
label={t("toolBar.freedraw")}
|
||||||
shortcuts={["Shift + P", "X", "7"]}
|
shortcuts={["Shift+P", "7"]}
|
||||||
/>
|
/>
|
||||||
<Shortcut label={t("toolBar.text")} shortcuts={["T", "8"]} />
|
<Shortcut label={t("toolBar.text")} shortcuts={["T", "8"]} />
|
||||||
<Shortcut label={t("toolBar.image")} shortcuts={["9"]} />
|
<Shortcut label={t("toolBar.image")} shortcuts={["9"]} />
|
||||||
@@ -260,18 +260,6 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
|||||||
label={t("labels.multiSelect")}
|
label={t("labels.multiSelect")}
|
||||||
shortcuts={[getShortcutKey(`Shift+${t("helpDialog.click")}`)]}
|
shortcuts={[getShortcutKey(`Shift+${t("helpDialog.click")}`)]}
|
||||||
/>
|
/>
|
||||||
<Shortcut
|
|
||||||
label={t("helpDialog.deepSelect")}
|
|
||||||
shortcuts={[
|
|
||||||
getShortcutKey(`CtrlOrCmd+${t("helpDialog.click")}`),
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<Shortcut
|
|
||||||
label={t("helpDialog.deepBoxSelect")}
|
|
||||||
shortcuts={[
|
|
||||||
getShortcutKey(`CtrlOrCmd+${t("helpDialog.drag")}`),
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<Shortcut
|
<Shortcut
|
||||||
label={t("labels.moveCanvas")}
|
label={t("labels.moveCanvas")}
|
||||||
shortcuts={[
|
shortcuts={[
|
||||||
@@ -394,14 +382,6 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
|||||||
label={t("labels.showBackground")}
|
label={t("labels.showBackground")}
|
||||||
shortcuts={[getShortcutKey("G")]}
|
shortcuts={[getShortcutKey("G")]}
|
||||||
/>
|
/>
|
||||||
<Shortcut
|
|
||||||
label={t("labels.decreaseFontSize")}
|
|
||||||
shortcuts={[getShortcutKey("CtrlOrCmd+Shift+<")]}
|
|
||||||
/>
|
|
||||||
<Shortcut
|
|
||||||
label={t("labels.increaseFontSize")}
|
|
||||||
shortcuts={[getShortcutKey("CtrlOrCmd+Shift+>")]}
|
|
||||||
/>
|
|
||||||
</ShortcutIsland>
|
</ShortcutIsland>
|
||||||
</Column>
|
</Column>
|
||||||
</Columns>
|
</Columns>
|
||||||
|
|||||||
@@ -61,27 +61,6 @@ const getHints = ({ appState, elements, isMobile }: HintViewerProps) => {
|
|||||||
return t("hints.rotate");
|
return t("hints.rotate");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedElements.length === 1 && isTextElement(selectedElements[0])) {
|
|
||||||
return t("hints.text_selected");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (appState.editingElement && isTextElement(appState.editingElement)) {
|
|
||||||
return t("hints.text_editing");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (elementType === "selection") {
|
|
||||||
if (
|
|
||||||
appState.draggingElement?.type === "selection" &&
|
|
||||||
!appState.editingElement &&
|
|
||||||
!appState.editingLinearElement
|
|
||||||
) {
|
|
||||||
return t("hints.deepBoxSelect");
|
|
||||||
}
|
|
||||||
if (!selectedElements.length && !isMobile) {
|
|
||||||
return t("hints.canvasPanning");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedElements.length === 1) {
|
if (selectedElements.length === 1) {
|
||||||
if (isLinearElement(selectedElements[0])) {
|
if (isLinearElement(selectedElements[0])) {
|
||||||
if (appState.editingLinearElement) {
|
if (appState.editingLinearElement) {
|
||||||
@@ -96,6 +75,18 @@ const getHints = ({ appState, elements, isMobile }: HintViewerProps) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (selectedElements.length === 1 && isTextElement(selectedElements[0])) {
|
||||||
|
return t("hints.text_selected");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (appState.editingElement && isTextElement(appState.editingElement)) {
|
||||||
|
return t("hints.text_editing");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elementType === "selection" && !selectedElements.length && !isMobile) {
|
||||||
|
return t("hints.canvasPanning");
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -27,8 +27,6 @@
|
|||||||
|
|
||||||
.library-unit__dragger {
|
.library-unit__dragger {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,6 +89,14 @@ export const trash = createIcon(
|
|||||||
|
|
||||||
{ width: 448, height: 512 },
|
{ width: 448, height: 512 },
|
||||||
);
|
);
|
||||||
|
export const backgroundIcon = createIcon(
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M512 320s-64 92.65-64 128c0 35.35 28.66 64 64 64s64-28.65 64-64-64-128-64-128zm-9.37-102.94L294.94 9.37C288.69 3.12 280.5 0 272.31 0s-16.38 3.12-22.62 9.37l-81.58 81.58L81.93 4.76c-6.25-6.25-16.38-6.25-22.62 0L36.69 27.38c-6.24 6.25-6.24 16.38 0 22.62l86.19 86.18-94.76 94.76c-37.49 37.48-37.49 98.26 0 135.75l117.19 117.19c18.74 18.74 43.31 28.12 67.87 28.12 24.57 0 49.13-9.37 67.87-28.12l221.57-221.57c12.5-12.5 12.5-32.75.01-45.25zm-116.22 70.97H65.93c1.36-3.84 3.57-7.98 7.43-11.83l13.15-13.15 81.61-81.61 58.6 58.6c12.49 12.49 32.75 12.49 45.24 0s12.49-32.75 0-45.24l-58.6-58.6 58.95-58.95 162.44 162.44-48.34 48.34z"
|
||||||
|
></path>,
|
||||||
|
|
||||||
|
{ width: 576, height: 512 },
|
||||||
|
);
|
||||||
|
|
||||||
export const palette = createIcon(
|
export const palette = createIcon(
|
||||||
"M204.3 5C104.9 24.4 24.8 104.3 5.2 203.4c-37 187 131.7 326.4 258.8 306.7 41.2-6.4 61.4-54.6 42.5-91.7-23.1-45.4 9.9-98.4 60.9-98.4h79.7c35.8 0 64.8-29.6 64.9-65.3C511.5 97.1 368.1-26.9 204.3 5zM96 320c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm32-128c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm128-64c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm128 64c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32z",
|
"M204.3 5C104.9 24.4 24.8 104.3 5.2 203.4c-37 187 131.7 326.4 258.8 306.7 41.2-6.4 61.4-54.6 42.5-91.7-23.1-45.4 9.9-98.4 60.9-98.4h79.7c35.8 0 64.8-29.6 64.9-65.3C511.5 97.1 368.1-26.9 204.3 5zM96 320c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm32-128c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm128-64c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm128 64c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32z",
|
||||||
|
|||||||
+5
-1
@@ -106,6 +106,10 @@ export const EXPORT_DATA_TYPES = {
|
|||||||
|
|
||||||
export const EXPORT_SOURCE = window.location.origin;
|
export const EXPORT_SOURCE = window.location.origin;
|
||||||
|
|
||||||
|
export const STORAGE_KEYS = {
|
||||||
|
LOCAL_STORAGE_LIBRARY: "excalidraw-library",
|
||||||
|
} as const;
|
||||||
|
|
||||||
// time in milliseconds
|
// time in milliseconds
|
||||||
export const IMAGE_RENDER_TIMEOUT = 500;
|
export const IMAGE_RENDER_TIMEOUT = 500;
|
||||||
export const TAP_TWICE_TIMEOUT = 300;
|
export const TAP_TWICE_TIMEOUT = 300;
|
||||||
@@ -178,4 +182,4 @@ export const VERSIONS = {
|
|||||||
excalidrawLibrary: 2,
|
excalidrawLibrary: 2,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const BOUND_TEXT_PADDING = 5;
|
export const PADDING = 30;
|
||||||
|
|||||||
+1
-1
@@ -136,7 +136,7 @@ const restoreElement = (
|
|||||||
textAlign: element.textAlign || DEFAULT_TEXT_ALIGN,
|
textAlign: element.textAlign || DEFAULT_TEXT_ALIGN,
|
||||||
verticalAlign: element.verticalAlign || DEFAULT_VERTICAL_ALIGN,
|
verticalAlign: element.verticalAlign || DEFAULT_VERTICAL_ALIGN,
|
||||||
containerId: element.containerId ?? null,
|
containerId: element.containerId ?? null,
|
||||||
originalText: element.originalText || element.text,
|
originalText: element.originalText ?? "",
|
||||||
});
|
});
|
||||||
case "freedraw": {
|
case "freedraw": {
|
||||||
return restoreElementWithProperties(element, {
|
return restoreElementWithProperties(element, {
|
||||||
|
|||||||
+48
-2
@@ -1,7 +1,17 @@
|
|||||||
import { ExcalidrawElement } from "./element/types";
|
import { ExcalidrawElement } from "./element/types";
|
||||||
import { newElementWith } from "./element/mutateElement";
|
import { newElementWith } from "./element/mutateElement";
|
||||||
import { getMaximumGroups } from "./groups";
|
import { getCommonBounds } from "./element";
|
||||||
import { getCommonBoundingBox } from "./element/bounds";
|
|
||||||
|
interface Box {
|
||||||
|
minX: number;
|
||||||
|
minY: number;
|
||||||
|
maxX: number;
|
||||||
|
maxY: number;
|
||||||
|
midX: number;
|
||||||
|
midY: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Distribution {
|
export interface Distribution {
|
||||||
space: "between";
|
space: "between";
|
||||||
@@ -88,3 +98,39 @@ export const distributeElements = (
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getMaximumGroups = (
|
||||||
|
elements: ExcalidrawElement[],
|
||||||
|
): ExcalidrawElement[][] => {
|
||||||
|
const groups: Map<String, ExcalidrawElement[]> = new Map<
|
||||||
|
String,
|
||||||
|
ExcalidrawElement[]
|
||||||
|
>();
|
||||||
|
|
||||||
|
elements.forEach((element: ExcalidrawElement) => {
|
||||||
|
const groupId =
|
||||||
|
element.groupIds.length === 0
|
||||||
|
? element.id
|
||||||
|
: element.groupIds[element.groupIds.length - 1];
|
||||||
|
|
||||||
|
const currentGroupMembers = groups.get(groupId) || [];
|
||||||
|
|
||||||
|
groups.set(groupId, [...currentGroupMembers, element]);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Array.from(groups.values());
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCommonBoundingBox = (elements: ExcalidrawElement[]): Box => {
|
||||||
|
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
|
||||||
|
return {
|
||||||
|
minX,
|
||||||
|
minY,
|
||||||
|
maxX,
|
||||||
|
maxY,
|
||||||
|
width: maxX - minX,
|
||||||
|
height: maxY - minY,
|
||||||
|
midX: (minX + maxX) / 2,
|
||||||
|
midY: (minY + maxY) / 2,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
+1
-14
@@ -520,24 +520,11 @@ export interface Box {
|
|||||||
minY: number;
|
minY: number;
|
||||||
maxX: number;
|
maxX: number;
|
||||||
maxY: number;
|
maxY: number;
|
||||||
midX: number;
|
|
||||||
midY: number;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getCommonBoundingBox = (
|
export const getCommonBoundingBox = (
|
||||||
elements: ExcalidrawElement[] | readonly NonDeleted<ExcalidrawElement>[],
|
elements: ExcalidrawElement[] | readonly NonDeleted<ExcalidrawElement>[],
|
||||||
): Box => {
|
): Box => {
|
||||||
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
|
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
|
||||||
return {
|
return { minX, minY, maxX, maxY };
|
||||||
minX,
|
|
||||||
minY,
|
|
||||||
maxX,
|
|
||||||
maxY,
|
|
||||||
width: maxX - minX,
|
|
||||||
height: maxY - minY,
|
|
||||||
midX: (minX + maxX) / 2,
|
|
||||||
midY: (minY + maxY) / 2,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -46,7 +46,8 @@ const isElementDraggableFromInside = (
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const isDraggableFromInside =
|
const isDraggableFromInside =
|
||||||
!isTransparent(element.backgroundColor) || hasBoundTextElement(element);
|
!isTransparent(element.backgroundColor) ||
|
||||||
|
(isTransparent(element.backgroundColor) && hasBoundTextElement(element));
|
||||||
if (element.type === "line") {
|
if (element.type === "line") {
|
||||||
return isDraggableFromInside && isPathALoop(element.points);
|
return isDraggableFromInside && isPathALoop(element.points);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,8 @@ import { mutateElement } from "./mutateElement";
|
|||||||
import { getPerfectElementSize } from "./sizeHelpers";
|
import { getPerfectElementSize } from "./sizeHelpers";
|
||||||
import Scene from "../scene/Scene";
|
import Scene from "../scene/Scene";
|
||||||
import { NonDeletedExcalidrawElement } from "./types";
|
import { NonDeletedExcalidrawElement } from "./types";
|
||||||
import { AppState, PointerDownState } from "../types";
|
import { PointerDownState } from "../types";
|
||||||
import { getBoundTextElementId } from "./textElement";
|
import { getBoundTextElementId } from "./textElement";
|
||||||
import { isSelectedViaGroup } from "../groups";
|
|
||||||
|
|
||||||
export const dragSelectedElements = (
|
export const dragSelectedElements = (
|
||||||
pointerDownState: PointerDownState,
|
pointerDownState: PointerDownState,
|
||||||
@@ -17,7 +16,6 @@ export const dragSelectedElements = (
|
|||||||
lockDirection: boolean = false,
|
lockDirection: boolean = false,
|
||||||
distanceX: number = 0,
|
distanceX: number = 0,
|
||||||
distanceY: number = 0,
|
distanceY: number = 0,
|
||||||
appState: AppState,
|
|
||||||
) => {
|
) => {
|
||||||
const [x1, y1] = getCommonBounds(selectedElements);
|
const [x1, y1] = getCommonBounds(selectedElements);
|
||||||
const offset = { x: pointerX - x1, y: pointerY - y1 };
|
const offset = { x: pointerX - x1, y: pointerY - y1 };
|
||||||
@@ -30,15 +28,7 @@ export const dragSelectedElements = (
|
|||||||
element,
|
element,
|
||||||
offset,
|
offset,
|
||||||
);
|
);
|
||||||
// update coords of bound text only if we're dragging the container directly
|
if (!element.groupIds.length) {
|
||||||
// (we don't drag the group that it's part of)
|
|
||||||
if (
|
|
||||||
// container isn't part of any group
|
|
||||||
// (perf optim so we don't check `isSelectedViaGroup()` in every case)
|
|
||||||
!element.groupIds.length ||
|
|
||||||
// container is part of a group, but we're dragging the container directly
|
|
||||||
(appState.editingGroupId && !isSelectedViaGroup(appState, element))
|
|
||||||
) {
|
|
||||||
const boundTextElementId = getBoundTextElementId(element);
|
const boundTextElementId = getBoundTextElementId(element);
|
||||||
if (boundTextElementId) {
|
if (boundTextElementId) {
|
||||||
const textElement =
|
const textElement =
|
||||||
|
|||||||
@@ -109,3 +109,16 @@ export const normalizeSVG = async (SVGString: string) => {
|
|||||||
return svg.outerHTML;
|
return svg.outerHTML;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const imageFromImageData = (imagedata: ImageData) => {
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
const ctx = canvas.getContext("2d")!;
|
||||||
|
canvas.width = imagedata.width;
|
||||||
|
canvas.height = imagedata.height;
|
||||||
|
ctx.putImageData(imagedata, 0, 0);
|
||||||
|
|
||||||
|
const image = new Image();
|
||||||
|
const dataURL = canvas.toDataURL() as DataURL;
|
||||||
|
image.src = dataURL;
|
||||||
|
return { image, dataURL };
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
import { distance2d } from "../math";
|
||||||
|
import Scene from "../scene/Scene";
|
||||||
|
import {
|
||||||
|
ExcalidrawImageElement,
|
||||||
|
InitializedExcalidrawImageElement,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
export type EditingImageElement = {
|
||||||
|
editorType: "alpha";
|
||||||
|
elementId: ExcalidrawImageElement["id"];
|
||||||
|
origImageData: Readonly<ImageData>;
|
||||||
|
imageData: ImageData;
|
||||||
|
pointerDownState: {
|
||||||
|
screenX: number;
|
||||||
|
screenY: number;
|
||||||
|
sampledPixel: readonly [number, number, number, number] | null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getElement = (id: EditingImageElement["elementId"]) => {
|
||||||
|
const element = Scene.getScene(id)?.getNonDeletedElement(id);
|
||||||
|
if (element) {
|
||||||
|
return element as InitializedExcalidrawImageElement;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class ImageEditor {
|
||||||
|
static handlePointerDown(
|
||||||
|
editingElement: EditingImageElement,
|
||||||
|
scenePointer: { x: number; y: number },
|
||||||
|
) {
|
||||||
|
const imageElement = getElement(editingElement.elementId);
|
||||||
|
|
||||||
|
if (imageElement) {
|
||||||
|
if (
|
||||||
|
scenePointer.x >= imageElement.x &&
|
||||||
|
scenePointer.x <= imageElement.x + imageElement.width &&
|
||||||
|
scenePointer.y >= imageElement.y &&
|
||||||
|
scenePointer.y <= imageElement.y + imageElement.height
|
||||||
|
) {
|
||||||
|
editingElement.pointerDownState.screenX = scenePointer.x;
|
||||||
|
editingElement.pointerDownState.screenY = scenePointer.y;
|
||||||
|
|
||||||
|
const { width, height, data } = editingElement.origImageData;
|
||||||
|
|
||||||
|
const imageOffsetX = Math.round(
|
||||||
|
(scenePointer.x - imageElement.x) * (width / imageElement.width),
|
||||||
|
);
|
||||||
|
const imageOffsetY = Math.round(
|
||||||
|
(scenePointer.y - imageElement.y) * (height / imageElement.height),
|
||||||
|
);
|
||||||
|
|
||||||
|
const sampledPixel = [
|
||||||
|
data[(imageOffsetY * width + imageOffsetX) * 4 + 0],
|
||||||
|
data[(imageOffsetY * width + imageOffsetX) * 4 + 1],
|
||||||
|
data[(imageOffsetY * width + imageOffsetX) * 4 + 2],
|
||||||
|
data[(imageOffsetY * width + imageOffsetX) * 4 + 3],
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
editingElement.pointerDownState.sampledPixel = sampledPixel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static handlePointerMove(
|
||||||
|
editingElement: EditingImageElement,
|
||||||
|
scenePointer: { x: number; y: number },
|
||||||
|
) {
|
||||||
|
const { sampledPixel } = editingElement.pointerDownState;
|
||||||
|
if (sampledPixel) {
|
||||||
|
const { screenX, screenY } = editingElement.pointerDownState;
|
||||||
|
const distance = distance2d(
|
||||||
|
scenePointer.x,
|
||||||
|
scenePointer.y,
|
||||||
|
screenX,
|
||||||
|
screenY,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { width, height, data } = editingElement.origImageData;
|
||||||
|
const newImageData = new ImageData(width, height);
|
||||||
|
|
||||||
|
for (let x = 0; x < width; ++x) {
|
||||||
|
for (let y = 0; y < height; ++y) {
|
||||||
|
if (
|
||||||
|
Math.abs(sampledPixel[0] - data[(y * width + x) * 4 + 0]) +
|
||||||
|
Math.abs(sampledPixel[1] - data[(y * width + x) * 4 + 1]) +
|
||||||
|
Math.abs(sampledPixel[2] - data[(y * width + x) * 4 + 2]) <
|
||||||
|
distance
|
||||||
|
) {
|
||||||
|
newImageData.data[(y * width + x) * 4 + 0] = 0;
|
||||||
|
newImageData.data[(y * width + x) * 4 + 1] = 255;
|
||||||
|
newImageData.data[(y * width + x) * 4 + 2] = 0;
|
||||||
|
newImageData.data[(y * width + x) * 4 + 3] = 0;
|
||||||
|
} else {
|
||||||
|
for (let p = 0; p < 4; ++p) {
|
||||||
|
newImageData.data[(y * width + x) * 4 + p] =
|
||||||
|
data[(y * width + x) * 4 + p];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newImageData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static handlePointerUp(editingElement: EditingImageElement) {
|
||||||
|
editingElement.pointerDownState.sampledPixel = null;
|
||||||
|
editingElement.origImageData = editingElement.imageData;
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
-22
@@ -13,7 +13,7 @@ import {
|
|||||||
FontFamilyValues,
|
FontFamilyValues,
|
||||||
ExcalidrawRectangleElement,
|
ExcalidrawRectangleElement,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import { getFontString, getUpdatedTimestamp, isTestEnv } from "../utils";
|
import { getFontString, getUpdatedTimestamp } from "../utils";
|
||||||
import { randomInteger, randomId } from "../random";
|
import { randomInteger, randomId } from "../random";
|
||||||
import { mutateElement, newElementWith } from "./mutateElement";
|
import { mutateElement, newElementWith } from "./mutateElement";
|
||||||
import { getNewGroupIdsForDuplication } from "../groups";
|
import { getNewGroupIdsForDuplication } from "../groups";
|
||||||
@@ -21,9 +21,10 @@ import { AppState } from "../types";
|
|||||||
import { getElementAbsoluteCoords } from ".";
|
import { getElementAbsoluteCoords } from ".";
|
||||||
import { adjustXYWithRotation } from "../math";
|
import { adjustXYWithRotation } from "../math";
|
||||||
import { getResizedElementAbsoluteCoords } from "./bounds";
|
import { getResizedElementAbsoluteCoords } from "./bounds";
|
||||||
import { getContainerElement, measureText } from "./textElement";
|
import { measureText } from "./textElement";
|
||||||
import { isBoundToContainer } from "./typeChecks";
|
import { isBoundToContainer } from "./typeChecks";
|
||||||
import { BOUND_TEXT_PADDING } from "../constants";
|
import Scene from "../scene/Scene";
|
||||||
|
import { PADDING } from "../constants";
|
||||||
|
|
||||||
type ElementConstructorOpts = MarkOptional<
|
type ElementConstructorOpts = MarkOptional<
|
||||||
Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted" | "updated">,
|
Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted" | "updated">,
|
||||||
@@ -157,11 +158,7 @@ const getAdjustedDimensions = (
|
|||||||
height: number;
|
height: number;
|
||||||
baseline: number;
|
baseline: number;
|
||||||
} => {
|
} => {
|
||||||
let maxWidth = null;
|
const maxWidth = element.containerId ? element.width : null;
|
||||||
const container = getContainerElement(element);
|
|
||||||
if (container) {
|
|
||||||
maxWidth = container.width - BOUND_TEXT_PADDING * 2;
|
|
||||||
}
|
|
||||||
const {
|
const {
|
||||||
width: nextWidth,
|
width: nextWidth,
|
||||||
height: nextHeight,
|
height: nextHeight,
|
||||||
@@ -219,14 +216,14 @@ const getAdjustedDimensions = (
|
|||||||
// make sure container dimensions are set properly when
|
// make sure container dimensions are set properly when
|
||||||
// text editor overflows beyond viewport dimensions
|
// text editor overflows beyond viewport dimensions
|
||||||
if (isBoundToContainer(element)) {
|
if (isBoundToContainer(element)) {
|
||||||
const container = getContainerElement(element)!;
|
const container = Scene.getScene(element)!.getElement(element.containerId)!;
|
||||||
let height = container.height;
|
let height = container.height;
|
||||||
let width = container.width;
|
let width = container.width;
|
||||||
if (nextHeight > height - BOUND_TEXT_PADDING * 2) {
|
if (nextHeight > height - PADDING * 2) {
|
||||||
height = nextHeight + BOUND_TEXT_PADDING * 2;
|
height = nextHeight + PADDING * 2;
|
||||||
}
|
}
|
||||||
if (nextWidth > width - BOUND_TEXT_PADDING * 2) {
|
if (nextWidth > width - PADDING * 2) {
|
||||||
width = nextWidth + BOUND_TEXT_PADDING * 2;
|
width = nextWidth + PADDING * 2;
|
||||||
}
|
}
|
||||||
if (height !== container.height || width !== container.width) {
|
if (height !== container.height || width !== container.width) {
|
||||||
mutateElement(container, { height, width });
|
mutateElement(container, { height, width });
|
||||||
@@ -249,15 +246,11 @@ export const updateTextElement = (
|
|||||||
originalText,
|
originalText,
|
||||||
}: { text: string; isDeleted?: boolean; originalText: string },
|
}: { text: string; isDeleted?: boolean; originalText: string },
|
||||||
|
|
||||||
isSubmit: boolean,
|
updateDimensions: boolean,
|
||||||
): ExcalidrawTextElement => {
|
): ExcalidrawTextElement => {
|
||||||
const boundToContainer = isBoundToContainer(element);
|
const dimensions = updateDimensions
|
||||||
|
? getAdjustedDimensions(element, text)
|
||||||
// Don't update dimensions and text value for bounded text unless submitted
|
: undefined;
|
||||||
const dimensions =
|
|
||||||
boundToContainer && !isSubmit
|
|
||||||
? undefined
|
|
||||||
: getAdjustedDimensions(element, text);
|
|
||||||
return newElementWith(element, {
|
return newElementWith(element, {
|
||||||
text,
|
text,
|
||||||
originalText,
|
originalText,
|
||||||
@@ -376,7 +369,7 @@ export const duplicateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
|||||||
overrides?: Partial<TElement>,
|
overrides?: Partial<TElement>,
|
||||||
): TElement => {
|
): TElement => {
|
||||||
let copy: TElement = deepCopyElement(element);
|
let copy: TElement = deepCopyElement(element);
|
||||||
if (isTestEnv()) {
|
if (process.env.NODE_ENV === "test") {
|
||||||
copy.id = `${copy.id}_copy`;
|
copy.id = `${copy.id}_copy`;
|
||||||
// `window.h` may not be defined in some unit tests
|
// `window.h` may not be defined in some unit tests
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -1,140 +0,0 @@
|
|||||||
import { wrapText } from "./textElement";
|
|
||||||
import { FontString } from "./types";
|
|
||||||
|
|
||||||
describe("Test wrapText", () => {
|
|
||||||
const font = "20px Cascadia, width: Segoe UI Emoji" as FontString;
|
|
||||||
|
|
||||||
describe("When text doesn't contain new lines", () => {
|
|
||||||
const text = "Hello whats up";
|
|
||||||
[
|
|
||||||
{
|
|
||||||
desc: "break all words when width of each word is less than container width",
|
|
||||||
width: 90,
|
|
||||||
res: `Hello
|
|
||||||
whats
|
|
||||||
up`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "break all characters when width of each character is less than container width",
|
|
||||||
width: 25,
|
|
||||||
res: `H
|
|
||||||
e
|
|
||||||
l
|
|
||||||
l
|
|
||||||
o
|
|
||||||
w
|
|
||||||
h
|
|
||||||
a
|
|
||||||
t
|
|
||||||
s
|
|
||||||
u
|
|
||||||
p`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "break words as per the width",
|
|
||||||
|
|
||||||
width: 150,
|
|
||||||
res: `Hello whats
|
|
||||||
up`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "fit the container",
|
|
||||||
|
|
||||||
width: 250,
|
|
||||||
res: "Hello whats up",
|
|
||||||
},
|
|
||||||
].forEach((data) => {
|
|
||||||
it(`should ${data.desc}`, () => {
|
|
||||||
const res = wrapText(text, font, data.width);
|
|
||||||
expect(res).toEqual(data.res);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe("When text contain new lines", () => {
|
|
||||||
const text = `Hello
|
|
||||||
whats up`;
|
|
||||||
[
|
|
||||||
{
|
|
||||||
desc: "break all words when width of each word is less than container width",
|
|
||||||
width: 90,
|
|
||||||
res: `Hello
|
|
||||||
whats
|
|
||||||
up`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "break all characters when width of each character is less than container width",
|
|
||||||
width: 25,
|
|
||||||
res: `H
|
|
||||||
e
|
|
||||||
l
|
|
||||||
l
|
|
||||||
o
|
|
||||||
w
|
|
||||||
h
|
|
||||||
a
|
|
||||||
t
|
|
||||||
s
|
|
||||||
u
|
|
||||||
p`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "break words as per the width",
|
|
||||||
|
|
||||||
width: 150,
|
|
||||||
res: `Hello
|
|
||||||
whats up`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "fit the container",
|
|
||||||
|
|
||||||
width: 250,
|
|
||||||
res: `Hello
|
|
||||||
whats up`,
|
|
||||||
},
|
|
||||||
].forEach((data) => {
|
|
||||||
it(`should respect new lines and ${data.desc}`, () => {
|
|
||||||
const res = wrapText(text, font, data.width);
|
|
||||||
expect(res).toEqual(data.res);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe("When text is long", () => {
|
|
||||||
const text = `hellolongtextthisiswhatsupwithyouIamtypingggggandtypinggg break it now`;
|
|
||||||
[
|
|
||||||
{
|
|
||||||
desc: "fit characters of long string as per container width",
|
|
||||||
width: 170,
|
|
||||||
res: `hellolongtextth
|
|
||||||
isiswhatsupwith
|
|
||||||
youIamtypingggg
|
|
||||||
gandtypinggg
|
|
||||||
break it now`,
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
desc: "fit characters of long string as per container width and break words as per the width",
|
|
||||||
|
|
||||||
width: 130,
|
|
||||||
res: `hellolongte
|
|
||||||
xtthisiswha
|
|
||||||
tsupwithyou
|
|
||||||
Iamtypinggg
|
|
||||||
ggandtyping
|
|
||||||
gg break it
|
|
||||||
now`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "fit the long text when container width is greater than text length and move the rest to next line",
|
|
||||||
|
|
||||||
width: 600,
|
|
||||||
res: `hellolongtextthisiswhatsupwithyouIamtypingggggandtypinggg
|
|
||||||
break it now`,
|
|
||||||
},
|
|
||||||
].forEach((data) => {
|
|
||||||
it(`should ${data.desc}`, () => {
|
|
||||||
const res = wrapText(text, font, data.width);
|
|
||||||
expect(res).toEqual(data.res);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
+42
-93
@@ -1,34 +1,20 @@
|
|||||||
import { getFontString, arrayToMap, isTestEnv } from "../utils";
|
import { getFontString, arrayToMap } from "../utils";
|
||||||
import {
|
import {
|
||||||
ExcalidrawBindableElement,
|
ExcalidrawBindableElement,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
ExcalidrawTextElementWithContainer,
|
|
||||||
FontString,
|
FontString,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
import { BOUND_TEXT_PADDING } from "../constants";
|
import { PADDING } from "../constants";
|
||||||
import { MaybeTransformHandleType } from "./transformHandles";
|
import { MaybeTransformHandleType } from "./transformHandles";
|
||||||
import Scene from "../scene/Scene";
|
import Scene from "../scene/Scene";
|
||||||
import { AppState } from "../types";
|
|
||||||
|
|
||||||
export const redrawTextBoundingBox = (
|
export const redrawTextBoundingBox = (element: ExcalidrawTextElement) => {
|
||||||
element: ExcalidrawTextElement,
|
let maxWidth;
|
||||||
container: ExcalidrawElement | null,
|
if (element.containerId) {
|
||||||
appState: AppState,
|
maxWidth = element.width;
|
||||||
) => {
|
|
||||||
const maxWidth = container
|
|
||||||
? container.width - BOUND_TEXT_PADDING * 2
|
|
||||||
: undefined;
|
|
||||||
let text = element.text;
|
|
||||||
|
|
||||||
if (container) {
|
|
||||||
text = wrapText(
|
|
||||||
element.originalText,
|
|
||||||
getFontString(element),
|
|
||||||
container.width,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
const metrics = measureText(
|
const metrics = measureText(
|
||||||
element.originalText,
|
element.originalText,
|
||||||
@@ -36,24 +22,10 @@ export const redrawTextBoundingBox = (
|
|||||||
maxWidth,
|
maxWidth,
|
||||||
);
|
);
|
||||||
|
|
||||||
let coordY = element.y;
|
|
||||||
// Resize container and vertically center align the text
|
|
||||||
if (container) {
|
|
||||||
coordY = container.y + container.height / 2 - metrics.height / 2;
|
|
||||||
let nextHeight = container.height;
|
|
||||||
if (metrics.height > container.height - BOUND_TEXT_PADDING * 2) {
|
|
||||||
nextHeight = metrics.height + BOUND_TEXT_PADDING * 2;
|
|
||||||
coordY = container.y + nextHeight / 2 - metrics.height / 2;
|
|
||||||
}
|
|
||||||
mutateElement(container, { height: nextHeight });
|
|
||||||
}
|
|
||||||
|
|
||||||
mutateElement(element, {
|
mutateElement(element, {
|
||||||
width: metrics.width,
|
width: metrics.width,
|
||||||
height: metrics.height,
|
height: metrics.height,
|
||||||
baseline: metrics.baseline,
|
baseline: metrics.baseline,
|
||||||
y: coordY,
|
|
||||||
text,
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -110,12 +82,20 @@ export const handleBindTextResize = (
|
|||||||
let containerHeight = element.height;
|
let containerHeight = element.height;
|
||||||
let nextBaseLine = textElement.baseline;
|
let nextBaseLine = textElement.baseline;
|
||||||
if (transformHandleType !== "n" && transformHandleType !== "s") {
|
if (transformHandleType !== "n" && transformHandleType !== "s") {
|
||||||
|
let minCharWidthTillNow = 0;
|
||||||
if (text) {
|
if (text) {
|
||||||
text = wrapText(
|
minCharWidthTillNow = getMinCharWidth(getFontString(textElement));
|
||||||
textElement.originalText,
|
// check if the diff has exceeded min char width needed
|
||||||
getFontString(textElement),
|
const diff = Math.abs(
|
||||||
element.width,
|
element.width - textElement.width + PADDING * 2,
|
||||||
);
|
);
|
||||||
|
if (diff >= minCharWidthTillNow) {
|
||||||
|
text = wrapText(
|
||||||
|
textElement.originalText,
|
||||||
|
getFontString(textElement),
|
||||||
|
element.width,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const dimensions = measureText(
|
const dimensions = measureText(
|
||||||
@@ -127,8 +107,8 @@ export const handleBindTextResize = (
|
|||||||
nextBaseLine = dimensions.baseline;
|
nextBaseLine = dimensions.baseline;
|
||||||
}
|
}
|
||||||
// increase height in case text element height exceeds
|
// increase height in case text element height exceeds
|
||||||
if (nextHeight > element.height - BOUND_TEXT_PADDING * 2) {
|
if (nextHeight > element.height - PADDING * 2) {
|
||||||
containerHeight = nextHeight + BOUND_TEXT_PADDING * 2;
|
containerHeight = nextHeight + PADDING * 2;
|
||||||
const diff = containerHeight - element.height;
|
const diff = containerHeight - element.height;
|
||||||
// fix the y coord when resizing from ne/nw/n
|
// fix the y coord when resizing from ne/nw/n
|
||||||
const updatedY =
|
const updatedY =
|
||||||
@@ -147,9 +127,9 @@ export const handleBindTextResize = (
|
|||||||
mutateElement(textElement, {
|
mutateElement(textElement, {
|
||||||
text,
|
text,
|
||||||
// preserve padding and set width correctly
|
// preserve padding and set width correctly
|
||||||
width: element.width - BOUND_TEXT_PADDING * 2,
|
width: element.width - PADDING * 2,
|
||||||
height: nextHeight,
|
height: nextHeight,
|
||||||
x: element.x + BOUND_TEXT_PADDING,
|
x: element.x + PADDING,
|
||||||
y: updatedY,
|
y: updatedY,
|
||||||
baseline: nextBaseLine,
|
baseline: nextBaseLine,
|
||||||
});
|
});
|
||||||
@@ -218,12 +198,6 @@ const getTextWidth = (text: string, font: FontString) => {
|
|||||||
canvas2dContext.font = font;
|
canvas2dContext.font = font;
|
||||||
|
|
||||||
const metrics = canvas2dContext.measureText(text);
|
const metrics = canvas2dContext.measureText(text);
|
||||||
// since in test env the canvas measureText algo
|
|
||||||
// doesn't measure text and instead just returns number of
|
|
||||||
// characters hence we assume that each letteris 10px
|
|
||||||
if (isTestEnv()) {
|
|
||||||
return metrics.width * 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
return metrics.width;
|
return metrics.width;
|
||||||
};
|
};
|
||||||
@@ -233,7 +207,7 @@ export const wrapText = (
|
|||||||
font: FontString,
|
font: FontString,
|
||||||
containerWidth: number,
|
containerWidth: number,
|
||||||
) => {
|
) => {
|
||||||
const maxWidth = containerWidth - BOUND_TEXT_PADDING * 2;
|
const maxWidth = containerWidth - PADDING * 2;
|
||||||
|
|
||||||
const lines: Array<string> = [];
|
const lines: Array<string> = [];
|
||||||
const originalLines = text.split("\n");
|
const originalLines = text.split("\n");
|
||||||
@@ -252,7 +226,7 @@ export const wrapText = (
|
|||||||
const currentWordWidth = getTextWidth(words[index], font);
|
const currentWordWidth = getTextWidth(words[index], font);
|
||||||
|
|
||||||
// Start breaking longer words exceeding max width
|
// Start breaking longer words exceeding max width
|
||||||
if (currentWordWidth >= maxWidth) {
|
if (currentWordWidth > maxWidth) {
|
||||||
// push current line since the current word exceeds the max width
|
// push current line since the current word exceeds the max width
|
||||||
// so will be appended in next line
|
// so will be appended in next line
|
||||||
if (currentLine) {
|
if (currentLine) {
|
||||||
@@ -283,7 +257,7 @@ export const wrapText = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// push current line if appending space exceeds max width
|
// push current line if appending space exceeds max width
|
||||||
if (currentLineWidthTillNow + spaceWidth >= maxWidth) {
|
if (currentLineWidthTillNow + spaceWidth > maxWidth) {
|
||||||
lines.push(currentLine);
|
lines.push(currentLine);
|
||||||
currentLine = "";
|
currentLine = "";
|
||||||
currentLineWidthTillNow = 0;
|
currentLineWidthTillNow = 0;
|
||||||
@@ -311,15 +285,8 @@ export const wrapText = (
|
|||||||
}
|
}
|
||||||
index++;
|
index++;
|
||||||
currentLine += `${word} `;
|
currentLine += `${word} `;
|
||||||
|
|
||||||
// Push the word if appending space exceeds max width
|
|
||||||
if (currentLineWidthTillNow + spaceWidth >= maxWidth) {
|
|
||||||
lines.push(currentLine.slice(0, -1));
|
|
||||||
currentLine = "";
|
|
||||||
currentLineWidthTillNow = 0;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentLineWidthTillNow === maxWidth) {
|
if (currentLineWidthTillNow === maxWidth) {
|
||||||
currentLine = "";
|
currentLine = "";
|
||||||
currentLineWidthTillNow = 0;
|
currentLineWidthTillNow = 0;
|
||||||
@@ -353,23 +320,34 @@ export const charWidth = (() => {
|
|||||||
return cachedCharWidth[font][ascii];
|
return cachedCharWidth[font][ascii];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateCache = (char: string, font: FontString) => {
|
||||||
|
const ascii = char.charCodeAt(0);
|
||||||
|
|
||||||
|
if (!cachedCharWidth[font][ascii]) {
|
||||||
|
cachedCharWidth[font][ascii] = calculate(char, font);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearCacheforFont = (font: FontString) => {
|
||||||
|
cachedCharWidth[font] = [];
|
||||||
|
};
|
||||||
|
|
||||||
const getCache = (font: FontString) => {
|
const getCache = (font: FontString) => {
|
||||||
return cachedCharWidth[font];
|
return cachedCharWidth[font];
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
calculate,
|
calculate,
|
||||||
|
updateCache,
|
||||||
|
clearCacheforFont,
|
||||||
getCache,
|
getCache,
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
export const getApproxMinLineWidth = (font: FontString) => {
|
export const getApproxMinLineWidth = (font: FontString) => {
|
||||||
return (
|
return measureText(DUMMY_TEXT.split("").join("\n"), font).width + PADDING * 2;
|
||||||
measureText(DUMMY_TEXT.split("").join("\n"), font).width +
|
|
||||||
BOUND_TEXT_PADDING * 2
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getApproxMinLineHeight = (font: FontString) => {
|
export const getApproxMinLineHeight = (font: FontString) => {
|
||||||
return getApproxLineHeight(font) + BOUND_TEXT_PADDING * 2;
|
return getApproxLineHeight(font) + PADDING * 2;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getMinCharWidth = (font: FontString) => {
|
export const getMinCharWidth = (font: FontString) => {
|
||||||
@@ -409,32 +387,3 @@ export const getApproxCharsToFitInWidth = (font: FontString, width: number) => {
|
|||||||
export const getBoundTextElementId = (container: ExcalidrawElement | null) => {
|
export const getBoundTextElementId = (container: ExcalidrawElement | null) => {
|
||||||
return container?.boundElements?.filter((ele) => ele.type === "text")[0]?.id;
|
return container?.boundElements?.filter((ele) => ele.type === "text")[0]?.id;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getBoundTextElement = (element: ExcalidrawElement | null) => {
|
|
||||||
if (!element) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const boundTextElementId = getBoundTextElementId(element);
|
|
||||||
if (boundTextElementId) {
|
|
||||||
return (
|
|
||||||
(Scene.getScene(element)?.getElement(
|
|
||||||
boundTextElementId,
|
|
||||||
) as ExcalidrawTextElementWithContainer) || null
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getContainerElement = (
|
|
||||||
element:
|
|
||||||
| (ExcalidrawElement & { containerId: ExcalidrawElement["id"] | null })
|
|
||||||
| null,
|
|
||||||
) => {
|
|
||||||
if (!element) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (element.containerId) {
|
|
||||||
return Scene.getScene(element)?.getElement(element.containerId) || null;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|||||||
+135
-379
@@ -1,413 +1,169 @@
|
|||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import ExcalidrawApp from "../excalidraw-app";
|
import ExcalidrawApp from "../excalidraw-app";
|
||||||
import { render, screen } from "../tests/test-utils";
|
import { render } from "../tests/test-utils";
|
||||||
import { Keyboard, Pointer, UI } from "../tests/helpers/ui";
|
import { Pointer, UI } from "../tests/helpers/ui";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { fireEvent } from "../tests/test-utils";
|
|
||||||
import { BOUND_TEXT_PADDING, FONT_FAMILY } from "../constants";
|
|
||||||
import { ExcalidrawTextElementWithContainer } from "./types";
|
|
||||||
import * as textElementUtils from "./textElement";
|
|
||||||
// Unmount ReactDOM from root
|
// Unmount ReactDOM from root
|
||||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||||
|
|
||||||
const tab = " ";
|
const tab = " ";
|
||||||
const mouse = new Pointer("mouse");
|
|
||||||
|
|
||||||
describe("textWysiwyg", () => {
|
describe("textWysiwyg", () => {
|
||||||
describe("Test unbounded text", () => {
|
let textarea: HTMLTextAreaElement;
|
||||||
let textarea: HTMLTextAreaElement;
|
beforeEach(async () => {
|
||||||
beforeEach(async () => {
|
await render(<ExcalidrawApp />);
|
||||||
await render(<ExcalidrawApp />);
|
|
||||||
|
|
||||||
const element = UI.createElement("text");
|
const element = UI.createElement("text");
|
||||||
|
|
||||||
mouse.clickOn(element);
|
new Pointer("mouse").clickOn(element);
|
||||||
textarea = document.querySelector(
|
textarea = document.querySelector(
|
||||||
".excalidraw-textEditorContainer > textarea",
|
".excalidraw-textEditorContainer > textarea",
|
||||||
)!;
|
)!;
|
||||||
});
|
|
||||||
|
|
||||||
it("should add a tab at the start of the first line", () => {
|
|
||||||
const event = new KeyboardEvent("keydown", { key: KEYS.TAB });
|
|
||||||
textarea.value = "Line#1\nLine#2";
|
|
||||||
// cursor: "|Line#1\nLine#2"
|
|
||||||
textarea.selectionStart = 0;
|
|
||||||
textarea.selectionEnd = 0;
|
|
||||||
textarea.dispatchEvent(event);
|
|
||||||
|
|
||||||
expect(textarea.value).toEqual(`${tab}Line#1\nLine#2`);
|
|
||||||
// cursor: " |Line#1\nLine#2"
|
|
||||||
expect(textarea.selectionStart).toEqual(4);
|
|
||||||
expect(textarea.selectionEnd).toEqual(4);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should add a tab at the start of the second line", () => {
|
|
||||||
const event = new KeyboardEvent("keydown", { key: KEYS.TAB });
|
|
||||||
textarea.value = "Line#1\nLine#2";
|
|
||||||
// cursor: "Line#1\nLin|e#2"
|
|
||||||
textarea.selectionStart = 10;
|
|
||||||
textarea.selectionEnd = 10;
|
|
||||||
|
|
||||||
textarea.dispatchEvent(event);
|
|
||||||
|
|
||||||
expect(textarea.value).toEqual(`Line#1\n${tab}Line#2`);
|
|
||||||
|
|
||||||
// cursor: "Line#1\n Lin|e#2"
|
|
||||||
expect(textarea.selectionStart).toEqual(14);
|
|
||||||
expect(textarea.selectionEnd).toEqual(14);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should add a tab at the start of the first and second line", () => {
|
|
||||||
const event = new KeyboardEvent("keydown", { key: KEYS.TAB });
|
|
||||||
textarea.value = "Line#1\nLine#2\nLine#3";
|
|
||||||
// cursor: "Li|ne#1\nLi|ne#2\nLine#3"
|
|
||||||
textarea.selectionStart = 2;
|
|
||||||
textarea.selectionEnd = 9;
|
|
||||||
|
|
||||||
textarea.dispatchEvent(event);
|
|
||||||
|
|
||||||
expect(textarea.value).toEqual(`${tab}Line#1\n${tab}Line#2\nLine#3`);
|
|
||||||
|
|
||||||
// cursor: " Li|ne#1\n Li|ne#2\nLine#3"
|
|
||||||
expect(textarea.selectionStart).toEqual(6);
|
|
||||||
expect(textarea.selectionEnd).toEqual(17);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should remove a tab at the start of the first line", () => {
|
|
||||||
const event = new KeyboardEvent("keydown", {
|
|
||||||
key: KEYS.TAB,
|
|
||||||
shiftKey: true,
|
|
||||||
});
|
|
||||||
textarea.value = `${tab}Line#1\nLine#2`;
|
|
||||||
// cursor: "| Line#1\nLine#2"
|
|
||||||
textarea.selectionStart = 0;
|
|
||||||
textarea.selectionEnd = 0;
|
|
||||||
|
|
||||||
textarea.dispatchEvent(event);
|
|
||||||
|
|
||||||
expect(textarea.value).toEqual(`Line#1\nLine#2`);
|
|
||||||
|
|
||||||
// cursor: "|Line#1\nLine#2"
|
|
||||||
expect(textarea.selectionStart).toEqual(0);
|
|
||||||
expect(textarea.selectionEnd).toEqual(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should remove a tab at the start of the second line", () => {
|
|
||||||
const event = new KeyboardEvent("keydown", {
|
|
||||||
key: KEYS.TAB,
|
|
||||||
shiftKey: true,
|
|
||||||
});
|
|
||||||
// cursor: "Line#1\n Lin|e#2"
|
|
||||||
textarea.value = `Line#1\n${tab}Line#2`;
|
|
||||||
textarea.selectionStart = 15;
|
|
||||||
textarea.selectionEnd = 15;
|
|
||||||
|
|
||||||
textarea.dispatchEvent(event);
|
|
||||||
|
|
||||||
expect(textarea.value).toEqual(`Line#1\nLine#2`);
|
|
||||||
// cursor: "Line#1\nLin|e#2"
|
|
||||||
expect(textarea.selectionStart).toEqual(11);
|
|
||||||
expect(textarea.selectionEnd).toEqual(11);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should remove a tab at the start of the first and second line", () => {
|
|
||||||
const event = new KeyboardEvent("keydown", {
|
|
||||||
key: KEYS.TAB,
|
|
||||||
shiftKey: true,
|
|
||||||
});
|
|
||||||
// cursor: " Li|ne#1\n Li|ne#2\nLine#3"
|
|
||||||
textarea.value = `${tab}Line#1\n${tab}Line#2\nLine#3`;
|
|
||||||
textarea.selectionStart = 6;
|
|
||||||
textarea.selectionEnd = 17;
|
|
||||||
|
|
||||||
textarea.dispatchEvent(event);
|
|
||||||
|
|
||||||
expect(textarea.value).toEqual(`Line#1\nLine#2\nLine#3`);
|
|
||||||
// cursor: "Li|ne#1\nLi|ne#2\nLine#3"
|
|
||||||
expect(textarea.selectionStart).toEqual(2);
|
|
||||||
expect(textarea.selectionEnd).toEqual(9);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should remove a tab at the start of the second line and cursor stay on this line", () => {
|
|
||||||
const event = new KeyboardEvent("keydown", {
|
|
||||||
key: KEYS.TAB,
|
|
||||||
shiftKey: true,
|
|
||||||
});
|
|
||||||
// cursor: "Line#1\n | Line#2"
|
|
||||||
textarea.value = `Line#1\n${tab}Line#2`;
|
|
||||||
textarea.selectionStart = 9;
|
|
||||||
textarea.selectionEnd = 9;
|
|
||||||
textarea.dispatchEvent(event);
|
|
||||||
|
|
||||||
// cursor: "Line#1\n|Line#2"
|
|
||||||
expect(textarea.selectionStart).toEqual(7);
|
|
||||||
// expect(textarea.selectionEnd).toEqual(7);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should remove partial tabs", () => {
|
|
||||||
const event = new KeyboardEvent("keydown", {
|
|
||||||
key: KEYS.TAB,
|
|
||||||
shiftKey: true,
|
|
||||||
});
|
|
||||||
// cursor: "Line#1\n Line#|2"
|
|
||||||
textarea.value = `Line#1\n Line#2`;
|
|
||||||
textarea.selectionStart = 15;
|
|
||||||
textarea.selectionEnd = 15;
|
|
||||||
textarea.dispatchEvent(event);
|
|
||||||
|
|
||||||
expect(textarea.value).toEqual(`Line#1\nLine#2`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should remove nothing", () => {
|
|
||||||
const event = new KeyboardEvent("keydown", {
|
|
||||||
key: KEYS.TAB,
|
|
||||||
shiftKey: true,
|
|
||||||
});
|
|
||||||
// cursor: "Line#1\n Li|ne#2"
|
|
||||||
textarea.value = `Line#1\nLine#2`;
|
|
||||||
textarea.selectionStart = 9;
|
|
||||||
textarea.selectionEnd = 9;
|
|
||||||
textarea.dispatchEvent(event);
|
|
||||||
|
|
||||||
expect(textarea.value).toEqual(`Line#1\nLine#2`);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
describe("Test bounded text", () => {
|
|
||||||
let rectangle: any;
|
|
||||||
const {
|
|
||||||
h,
|
|
||||||
}: {
|
|
||||||
h: {
|
|
||||||
elements: any;
|
|
||||||
};
|
|
||||||
} = window;
|
|
||||||
|
|
||||||
const DUMMY_HEIGHT = 240;
|
it("should add a tab at the start of the first line", () => {
|
||||||
const DUMMY_WIDTH = 160;
|
const event = new KeyboardEvent("keydown", { key: KEYS.TAB });
|
||||||
const APPROX_LINE_HEIGHT = 25;
|
textarea.value = "Line#1\nLine#2";
|
||||||
const INITIAL_WIDTH = 10;
|
// cursor: "|Line#1\nLine#2"
|
||||||
|
textarea.selectionStart = 0;
|
||||||
|
textarea.selectionEnd = 0;
|
||||||
|
textarea.dispatchEvent(event);
|
||||||
|
|
||||||
beforeAll(() => {
|
expect(textarea.value).toEqual(`${tab}Line#1\nLine#2`);
|
||||||
jest
|
// cursor: " |Line#1\nLine#2"
|
||||||
.spyOn(textElementUtils, "getApproxLineHeight")
|
expect(textarea.selectionStart).toEqual(4);
|
||||||
.mockReturnValue(APPROX_LINE_HEIGHT);
|
expect(textarea.selectionEnd).toEqual(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should add a tab at the start of the second line", () => {
|
||||||
|
const event = new KeyboardEvent("keydown", { key: KEYS.TAB });
|
||||||
|
textarea.value = "Line#1\nLine#2";
|
||||||
|
// cursor: "Line#1\nLin|e#2"
|
||||||
|
textarea.selectionStart = 10;
|
||||||
|
textarea.selectionEnd = 10;
|
||||||
|
|
||||||
|
textarea.dispatchEvent(event);
|
||||||
|
|
||||||
|
expect(textarea.value).toEqual(`Line#1\n${tab}Line#2`);
|
||||||
|
|
||||||
|
// cursor: "Line#1\n Lin|e#2"
|
||||||
|
expect(textarea.selectionStart).toEqual(14);
|
||||||
|
expect(textarea.selectionEnd).toEqual(14);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should add a tab at the start of the first and second line", () => {
|
||||||
|
const event = new KeyboardEvent("keydown", { key: KEYS.TAB });
|
||||||
|
textarea.value = "Line#1\nLine#2\nLine#3";
|
||||||
|
// cursor: "Li|ne#1\nLi|ne#2\nLine#3"
|
||||||
|
textarea.selectionStart = 2;
|
||||||
|
textarea.selectionEnd = 9;
|
||||||
|
|
||||||
|
textarea.dispatchEvent(event);
|
||||||
|
|
||||||
|
expect(textarea.value).toEqual(`${tab}Line#1\n${tab}Line#2\nLine#3`);
|
||||||
|
|
||||||
|
// cursor: " Li|ne#1\n Li|ne#2\nLine#3"
|
||||||
|
expect(textarea.selectionStart).toEqual(6);
|
||||||
|
expect(textarea.selectionEnd).toEqual(17);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should remove a tab at the start of the first line", () => {
|
||||||
|
const event = new KeyboardEvent("keydown", {
|
||||||
|
key: KEYS.TAB,
|
||||||
|
shiftKey: true,
|
||||||
});
|
});
|
||||||
|
textarea.value = `${tab}Line#1\nLine#2`;
|
||||||
|
// cursor: "| Line#1\nLine#2"
|
||||||
|
textarea.selectionStart = 0;
|
||||||
|
textarea.selectionEnd = 0;
|
||||||
|
|
||||||
beforeEach(async () => {
|
textarea.dispatchEvent(event);
|
||||||
await render(<ExcalidrawApp />);
|
|
||||||
|
|
||||||
rectangle = UI.createElement("rectangle", {
|
expect(textarea.value).toEqual(`Line#1\nLine#2`);
|
||||||
x: 10,
|
|
||||||
y: 20,
|
// cursor: "|Line#1\nLine#2"
|
||||||
width: 90,
|
expect(textarea.selectionStart).toEqual(0);
|
||||||
height: 75,
|
expect(textarea.selectionEnd).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should remove a tab at the start of the second line", () => {
|
||||||
|
const event = new KeyboardEvent("keydown", {
|
||||||
|
key: KEYS.TAB,
|
||||||
|
shiftKey: true,
|
||||||
});
|
});
|
||||||
|
// cursor: "Line#1\n Lin|e#2"
|
||||||
|
textarea.value = `Line#1\n${tab}Line#2`;
|
||||||
|
textarea.selectionStart = 15;
|
||||||
|
textarea.selectionEnd = 15;
|
||||||
|
|
||||||
it("should bind text to container when double clicked on center", async () => {
|
textarea.dispatchEvent(event);
|
||||||
expect(h.elements.length).toBe(1);
|
|
||||||
expect(h.elements[0].id).toBe(rectangle.id);
|
|
||||||
|
|
||||||
mouse.doubleClickAt(
|
expect(textarea.value).toEqual(`Line#1\nLine#2`);
|
||||||
rectangle.x + rectangle.width / 2,
|
// cursor: "Line#1\nLin|e#2"
|
||||||
rectangle.y + rectangle.height / 2,
|
expect(textarea.selectionStart).toEqual(11);
|
||||||
);
|
expect(textarea.selectionEnd).toEqual(11);
|
||||||
expect(h.elements.length).toBe(2);
|
});
|
||||||
|
|
||||||
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
it("should remove a tab at the start of the first and second line", () => {
|
||||||
expect(text.type).toBe("text");
|
const event = new KeyboardEvent("keydown", {
|
||||||
expect(text.containerId).toBe(rectangle.id);
|
key: KEYS.TAB,
|
||||||
mouse.down();
|
shiftKey: true,
|
||||||
const editor = document.querySelector(
|
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
|
||||||
|
|
||||||
fireEvent.change(editor, { target: { value: "Hello World!" } });
|
|
||||||
editor.blur();
|
|
||||||
expect(rectangle.boundElements).toStrictEqual([
|
|
||||||
{ id: text.id, type: "text" },
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
// cursor: " Li|ne#1\n Li|ne#2\nLine#3"
|
||||||
|
textarea.value = `${tab}Line#1\n${tab}Line#2\nLine#3`;
|
||||||
|
textarea.selectionStart = 6;
|
||||||
|
textarea.selectionEnd = 17;
|
||||||
|
|
||||||
it("should bind text to container when clicked on container and enter pressed", async () => {
|
textarea.dispatchEvent(event);
|
||||||
expect(h.elements.length).toBe(1);
|
|
||||||
expect(h.elements[0].id).toBe(rectangle.id);
|
|
||||||
|
|
||||||
Keyboard.withModifierKeys({}, () => {
|
expect(textarea.value).toEqual(`Line#1\nLine#2\nLine#3`);
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
// cursor: "Li|ne#1\nLi|ne#2\nLine#3"
|
||||||
});
|
expect(textarea.selectionStart).toEqual(2);
|
||||||
|
expect(textarea.selectionEnd).toEqual(9);
|
||||||
|
});
|
||||||
|
|
||||||
expect(h.elements.length).toBe(2);
|
it("should remove a tab at the start of the second line and cursor stay on this line", () => {
|
||||||
|
const event = new KeyboardEvent("keydown", {
|
||||||
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
key: KEYS.TAB,
|
||||||
expect(text.type).toBe("text");
|
shiftKey: true,
|
||||||
expect(text.containerId).toBe(rectangle.id);
|
|
||||||
const editor = document.querySelector(
|
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
|
||||||
|
|
||||||
fireEvent.change(editor, { target: { value: "Hello World!" } });
|
|
||||||
editor.blur();
|
|
||||||
expect(rectangle.boundElements).toStrictEqual([
|
|
||||||
{ id: text.id, type: "text" },
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
// cursor: "Line#1\n | Line#2"
|
||||||
|
textarea.value = `Line#1\n${tab}Line#2`;
|
||||||
|
textarea.selectionStart = 9;
|
||||||
|
textarea.selectionEnd = 9;
|
||||||
|
textarea.dispatchEvent(event);
|
||||||
|
|
||||||
it("should update font family correctly on undo/redo by selecting bounded text when font family was updated", async () => {
|
// cursor: "Line#1\n|Line#2"
|
||||||
mouse.doubleClickAt(
|
expect(textarea.selectionStart).toEqual(7);
|
||||||
rectangle.x + rectangle.width / 2,
|
// expect(textarea.selectionEnd).toEqual(7);
|
||||||
rectangle.y + rectangle.height / 2,
|
});
|
||||||
);
|
|
||||||
mouse.down();
|
|
||||||
|
|
||||||
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
it("should remove partial tabs", () => {
|
||||||
let editor = document.querySelector(
|
const event = new KeyboardEvent("keydown", {
|
||||||
".excalidraw-textEditorContainer > textarea",
|
key: KEYS.TAB,
|
||||||
) as HTMLTextAreaElement;
|
shiftKey: true,
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
|
||||||
fireEvent.change(editor, { target: { value: "Hello World!" } });
|
|
||||||
editor.blur();
|
|
||||||
expect(text.fontFamily).toEqual(FONT_FAMILY.Virgil);
|
|
||||||
UI.clickTool("text");
|
|
||||||
|
|
||||||
mouse.clickAt(
|
|
||||||
rectangle.x + rectangle.width / 2,
|
|
||||||
rectangle.y + rectangle.height / 2,
|
|
||||||
);
|
|
||||||
mouse.down();
|
|
||||||
editor = document.querySelector(
|
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
|
|
||||||
editor.select();
|
|
||||||
fireEvent.click(screen.getByTitle(/code/i));
|
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
|
||||||
editor.blur();
|
|
||||||
expect(h.elements[1].fontFamily).toEqual(FONT_FAMILY.Cascadia);
|
|
||||||
|
|
||||||
//undo
|
|
||||||
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
|
||||||
Keyboard.keyPress(KEYS.Z);
|
|
||||||
});
|
|
||||||
expect(h.elements[1].fontFamily).toEqual(FONT_FAMILY.Virgil);
|
|
||||||
|
|
||||||
//redo
|
|
||||||
Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => {
|
|
||||||
Keyboard.keyPress(KEYS.Z);
|
|
||||||
});
|
|
||||||
expect(h.elements[1].fontFamily).toEqual(FONT_FAMILY.Cascadia);
|
|
||||||
});
|
});
|
||||||
|
// cursor: "Line#1\n Line#|2"
|
||||||
|
textarea.value = `Line#1\n Line#2`;
|
||||||
|
textarea.selectionStart = 15;
|
||||||
|
textarea.selectionEnd = 15;
|
||||||
|
textarea.dispatchEvent(event);
|
||||||
|
|
||||||
it("should wrap text and vertcially center align once text submitted", async () => {
|
expect(textarea.value).toEqual(`Line#1\nLine#2`);
|
||||||
jest
|
});
|
||||||
.spyOn(textElementUtils, "measureText")
|
|
||||||
.mockImplementation((text, font, maxWidth) => {
|
|
||||||
let width = INITIAL_WIDTH;
|
|
||||||
let height = APPROX_LINE_HEIGHT;
|
|
||||||
let baseline = 10;
|
|
||||||
if (!text) {
|
|
||||||
return {
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
baseline,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
baseline = 30;
|
|
||||||
width = DUMMY_WIDTH;
|
|
||||||
if (text === "Hello \nWorld!") {
|
|
||||||
height = APPROX_LINE_HEIGHT * 2;
|
|
||||||
}
|
|
||||||
if (maxWidth) {
|
|
||||||
width = maxWidth;
|
|
||||||
// To capture cases where maxWidth passed is initial width
|
|
||||||
// due to which the text is not wrapped correctly
|
|
||||||
if (maxWidth === INITIAL_WIDTH) {
|
|
||||||
height = DUMMY_HEIGHT;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
baseline,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
Keyboard.withModifierKeys({}, () => {
|
it("should remove nothing", () => {
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
const event = new KeyboardEvent("keydown", {
|
||||||
});
|
key: KEYS.TAB,
|
||||||
|
shiftKey: true,
|
||||||
let text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
|
||||||
let editor = document.querySelector(
|
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
|
|
||||||
// mock scroll height
|
|
||||||
jest
|
|
||||||
.spyOn(editor, "scrollHeight", "get")
|
|
||||||
.mockImplementation(() => APPROX_LINE_HEIGHT * 2);
|
|
||||||
|
|
||||||
fireEvent.change(editor, {
|
|
||||||
target: {
|
|
||||||
value: "Hello World!",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
editor.dispatchEvent(new Event("input"));
|
|
||||||
|
|
||||||
await new Promise((cb) => setTimeout(cb, 0));
|
|
||||||
editor.blur();
|
|
||||||
text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
|
||||||
expect(text.text).toBe("Hello \nWorld!");
|
|
||||||
expect(text.originalText).toBe("Hello World!");
|
|
||||||
expect(text.y).toBe(
|
|
||||||
rectangle.y + rectangle.height / 2 - (APPROX_LINE_HEIGHT * 2) / 2,
|
|
||||||
);
|
|
||||||
expect(text.x).toBe(rectangle.x + BOUND_TEXT_PADDING);
|
|
||||||
expect(text.height).toBe(APPROX_LINE_HEIGHT * 2);
|
|
||||||
expect(text.width).toBe(rectangle.width - BOUND_TEXT_PADDING * 2);
|
|
||||||
|
|
||||||
// Edit and text by removing second line and it should
|
|
||||||
// still vertically align correctly
|
|
||||||
mouse.select(rectangle);
|
|
||||||
Keyboard.withModifierKeys({}, () => {
|
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
|
||||||
});
|
|
||||||
editor = document.querySelector(
|
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
|
|
||||||
fireEvent.change(editor, {
|
|
||||||
target: {
|
|
||||||
value: "Hello",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// mock scroll height
|
|
||||||
jest
|
|
||||||
.spyOn(editor, "scrollHeight", "get")
|
|
||||||
.mockImplementation(() => APPROX_LINE_HEIGHT);
|
|
||||||
editor.style.height = "25px";
|
|
||||||
editor.dispatchEvent(new Event("input"));
|
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
|
||||||
|
|
||||||
editor.blur();
|
|
||||||
text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
|
||||||
|
|
||||||
expect(text.text).toBe("Hello");
|
|
||||||
expect(text.originalText).toBe("Hello");
|
|
||||||
expect(text.y).toBe(
|
|
||||||
rectangle.y + rectangle.height / 2 - APPROX_LINE_HEIGHT / 2,
|
|
||||||
);
|
|
||||||
expect(text.x).toBe(rectangle.x + BOUND_TEXT_PADDING);
|
|
||||||
expect(text.height).toBe(APPROX_LINE_HEIGHT);
|
|
||||||
expect(text.width).toBe(rectangle.width - BOUND_TEXT_PADDING * 2);
|
|
||||||
});
|
});
|
||||||
|
// cursor: "Line#1\n Li|ne#2"
|
||||||
|
textarea.value = `Line#1\nLine#2`;
|
||||||
|
textarea.selectionStart = 9;
|
||||||
|
textarea.selectionEnd = 9;
|
||||||
|
textarea.dispatchEvent(event);
|
||||||
|
|
||||||
|
expect(textarea.value).toEqual(`Line#1\nLine#2`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+79
-113
@@ -2,19 +2,22 @@ import { CODES, KEYS } from "../keys";
|
|||||||
import {
|
import {
|
||||||
isWritableElement,
|
isWritableElement,
|
||||||
getFontString,
|
getFontString,
|
||||||
|
viewportCoordsToSceneCoords,
|
||||||
getFontFamilyString,
|
getFontFamilyString,
|
||||||
isTestEnv,
|
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
import Scene from "../scene/Scene";
|
import Scene from "../scene/Scene";
|
||||||
import { isBoundToContainer, isTextElement } from "./typeChecks";
|
import { isBoundToContainer, isTextElement } from "./typeChecks";
|
||||||
import { CLASSES, BOUND_TEXT_PADDING } from "../constants";
|
import { CLASSES, PADDING } from "../constants";
|
||||||
import { ExcalidrawElement, ExcalidrawTextElement } from "./types";
|
import {
|
||||||
|
ExcalidrawBindableElement,
|
||||||
|
ExcalidrawElement,
|
||||||
|
ExcalidrawTextElement,
|
||||||
|
} from "./types";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
import {
|
import {
|
||||||
getApproxLineHeight,
|
getApproxLineHeight,
|
||||||
getBoundTextElementId,
|
getBoundTextElementId,
|
||||||
getContainerElement,
|
|
||||||
wrapText,
|
wrapText,
|
||||||
} from "./textElement";
|
} from "./textElement";
|
||||||
|
|
||||||
@@ -34,20 +37,16 @@ const getTransform = (
|
|||||||
angle: number,
|
angle: number,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
maxWidth: number,
|
maxWidth: number,
|
||||||
maxHeight: number,
|
|
||||||
) => {
|
) => {
|
||||||
const { zoom, offsetTop, offsetLeft } = appState;
|
const { zoom, offsetTop, offsetLeft } = appState;
|
||||||
const degree = (180 * angle) / Math.PI;
|
const degree = (180 * angle) / Math.PI;
|
||||||
// offsets must be multiplied by 2 to account for the division by 2 of
|
// offsets must be multiplied by 2 to account for the division by 2 of
|
||||||
// the whole expression afterwards
|
// the whole expression afterwards
|
||||||
let translateX = ((width - offsetLeft * 2) * (zoom.value - 1)) / 2;
|
let translateX = ((width - offsetLeft * 2) * (zoom.value - 1)) / 2;
|
||||||
let translateY = ((height - offsetTop * 2) * (zoom.value - 1)) / 2;
|
const translateY = ((height - offsetTop * 2) * (zoom.value - 1)) / 2;
|
||||||
if (width > maxWidth && zoom.value !== 1) {
|
if (width > maxWidth && zoom.value !== 1) {
|
||||||
translateX = (maxWidth / 2) * (zoom.value - 1);
|
translateX = (maxWidth / 2) * (zoom.value - 1);
|
||||||
}
|
}
|
||||||
if (height > maxHeight && zoom.value !== 1) {
|
|
||||||
translateY = ((maxHeight - offsetTop * 2) * (zoom.value - 1)) / 2;
|
|
||||||
}
|
|
||||||
return `translate(${translateX}px, ${translateY}px) scale(${zoom.value}) rotate(${degree}deg)`;
|
return `translate(${translateX}px, ${translateY}px) scale(${zoom.value}) rotate(${degree}deg)`;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -70,7 +69,7 @@ export const textWysiwyg = ({
|
|||||||
originalText: string;
|
originalText: string;
|
||||||
}) => void;
|
}) => void;
|
||||||
getViewportCoords: (x: number, y: number) => [number, number];
|
getViewportCoords: (x: number, y: number) => [number, number];
|
||||||
element: ExcalidrawTextElement;
|
element: ExcalidrawElement;
|
||||||
canvas: HTMLCanvasElement | null;
|
canvas: HTMLCanvasElement | null;
|
||||||
excalidrawContainer: HTMLDivElement | null;
|
excalidrawContainer: HTMLDivElement | null;
|
||||||
}) => {
|
}) => {
|
||||||
@@ -91,73 +90,93 @@ export const textWysiwyg = ({
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
let originalContainerHeight: number;
|
let originalContainerHeight: number;
|
||||||
let approxLineHeight = getApproxLineHeight(getFontString(element));
|
let approxLineHeight = isTextElement(element)
|
||||||
|
? getApproxLineHeight(getFontString(element))
|
||||||
|
: 0;
|
||||||
|
|
||||||
const initialText = element.originalText;
|
|
||||||
const updateWysiwygStyle = () => {
|
const updateWysiwygStyle = () => {
|
||||||
const updatedElement = Scene.getScene(element)?.getElement(id);
|
const updatedElement = Scene.getScene(element)?.getElement(id);
|
||||||
if (updatedElement && isTextElement(updatedElement)) {
|
if (updatedElement && isTextElement(updatedElement)) {
|
||||||
let coordX = updatedElement.x;
|
let coordX = updatedElement.x;
|
||||||
let coordY = updatedElement.y;
|
let coordY = updatedElement.y;
|
||||||
const container = getContainerElement(updatedElement);
|
let container = updatedElement?.containerId
|
||||||
|
? Scene.getScene(updatedElement)!.getElement(updatedElement.containerId)
|
||||||
|
: null;
|
||||||
let maxWidth = updatedElement.width;
|
let maxWidth = updatedElement.width;
|
||||||
|
|
||||||
let maxHeight = updatedElement.height;
|
let maxHeight = updatedElement.height;
|
||||||
let width = updatedElement.width;
|
let width = updatedElement.width;
|
||||||
// Set to element height by default since thats
|
|
||||||
// what is going to be used for unbounded text
|
|
||||||
let height = updatedElement.height;
|
let height = updatedElement.height;
|
||||||
if (container && updatedElement.containerId) {
|
if (container && updatedElement.containerId) {
|
||||||
const propertiesUpdated = textPropertiesUpdated(
|
const propertiesUpdated = textPropertiesUpdated(
|
||||||
updatedElement,
|
updatedElement,
|
||||||
editable,
|
editable,
|
||||||
);
|
);
|
||||||
// using editor.style.height to get the accurate height of text editor
|
|
||||||
const editorHeight = Number(editable.style.height.slice(0, -2));
|
|
||||||
if (editorHeight > 0) {
|
|
||||||
height = editorHeight;
|
|
||||||
}
|
|
||||||
if (propertiesUpdated) {
|
if (propertiesUpdated) {
|
||||||
approxLineHeight = getApproxLineHeight(getFontString(updatedElement));
|
const currentContainer = Scene.getScene(updatedElement)?.getElement(
|
||||||
|
updatedElement.containerId,
|
||||||
originalContainerHeight = container.height;
|
) as ExcalidrawBindableElement;
|
||||||
|
approxLineHeight = isTextElement(updatedElement)
|
||||||
// update height of the editor after properties updated
|
? getApproxLineHeight(getFontString(updatedElement))
|
||||||
height = updatedElement.height;
|
: 0;
|
||||||
|
if (updatedElement.height > currentContainer.height - PADDING * 2) {
|
||||||
|
const nextHeight = updatedElement.height + PADDING * 2;
|
||||||
|
originalContainerHeight = nextHeight;
|
||||||
|
mutateElement(container, { height: nextHeight });
|
||||||
|
container = { ...container, height: nextHeight };
|
||||||
|
}
|
||||||
|
editable.style.height = `${updatedElement.height}px`;
|
||||||
}
|
}
|
||||||
if (!originalContainerHeight) {
|
if (!originalContainerHeight) {
|
||||||
originalContainerHeight = container.height;
|
originalContainerHeight = container.height;
|
||||||
}
|
}
|
||||||
maxWidth = container.width - BOUND_TEXT_PADDING * 2;
|
maxWidth = container.width - PADDING * 2;
|
||||||
maxHeight = container.height - BOUND_TEXT_PADDING * 2;
|
maxHeight = container.height - PADDING * 2;
|
||||||
width = maxWidth;
|
width = maxWidth;
|
||||||
|
height = Math.min(height, maxHeight);
|
||||||
// The coordinates of text box set a distance of
|
// The coordinates of text box set a distance of
|
||||||
// 30px to preserve padding
|
// 30px to preserve padding
|
||||||
coordX = container.x + BOUND_TEXT_PADDING;
|
coordX = container.x + PADDING;
|
||||||
|
|
||||||
// autogrow container height if text exceeds
|
// autogrow container height if text exceeds
|
||||||
if (height > maxHeight) {
|
if (editable.clientHeight > maxHeight) {
|
||||||
const diff = Math.min(height - maxHeight, approxLineHeight);
|
const diff = Math.min(
|
||||||
|
editable.clientHeight - maxHeight,
|
||||||
|
approxLineHeight,
|
||||||
|
);
|
||||||
mutateElement(container, { height: container.height + diff });
|
mutateElement(container, { height: container.height + diff });
|
||||||
return;
|
return;
|
||||||
} else if (
|
} else if (
|
||||||
// autoshrink container height until original container height
|
// autoshrink container height until original container height
|
||||||
// is reached when text is removed
|
// is reached when text is removed
|
||||||
container.height > originalContainerHeight &&
|
container.height > originalContainerHeight &&
|
||||||
height < maxHeight
|
editable.clientHeight < maxHeight
|
||||||
) {
|
) {
|
||||||
const diff = Math.min(maxHeight - height, approxLineHeight);
|
const diff = Math.min(
|
||||||
|
maxHeight - editable.clientHeight,
|
||||||
|
approxLineHeight,
|
||||||
|
);
|
||||||
mutateElement(container, { height: container.height - diff });
|
mutateElement(container, { height: container.height - diff });
|
||||||
}
|
}
|
||||||
// Start pushing text upward until a diff of 30px (padding)
|
// Start pushing text upward until a diff of 30px (padding)
|
||||||
// is reached
|
// is reached
|
||||||
else {
|
else {
|
||||||
// vertically center align the text
|
const lines = editable.clientHeight / approxLineHeight;
|
||||||
coordY = container.y + container.height / 2 - height / 2;
|
// For some reason the scrollHeight gets set to twice the lineHeight
|
||||||
|
// when you start typing for first time and thus line count is 2
|
||||||
|
// hence this check
|
||||||
|
if (lines > 2 || propertiesUpdated) {
|
||||||
|
// vertically center align the text
|
||||||
|
coordY =
|
||||||
|
container.y + container.height / 2 - editable.clientHeight / 2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const [viewportX, viewportY] = getViewportCoords(coordX, coordY);
|
const [viewportX, viewportY] = getViewportCoords(coordX, coordY);
|
||||||
const { textAlign } = updatedElement;
|
const { textAlign, angle } = updatedElement;
|
||||||
editable.value = updatedElement.originalText;
|
|
||||||
|
editable.value = updatedElement.originalText || updatedElement.text;
|
||||||
const lines = updatedElement.originalText.split("\n");
|
const lines = updatedElement.originalText.split("\n");
|
||||||
const lineHeight = updatedElement.containerId
|
const lineHeight = updatedElement.containerId
|
||||||
? approxLineHeight
|
? approxLineHeight
|
||||||
@@ -173,36 +192,19 @@ export const textWysiwyg = ({
|
|||||||
).marginRight.slice(0, -2),
|
).marginRight.slice(0, -2),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure text editor height doesn't go beyond viewport
|
// Make sure text editor height doesn't go beyond viewport
|
||||||
const editorMaxHeight =
|
const editorMaxHeight =
|
||||||
(appState.height -
|
(appState.offsetTop + appState.height - viewportY) /
|
||||||
viewportY -
|
|
||||||
// There is a ~14px difference which keeps on increasing
|
|
||||||
// with every zoom step when offset present hence I am subtracting it here
|
|
||||||
// However this is not the best fix and breaks in
|
|
||||||
// few scenarios
|
|
||||||
(appState.offsetTop
|
|
||||||
? ((appState.zoom.value * 100 - 100) / 10) * 14
|
|
||||||
: 0)) /
|
|
||||||
appState.zoom.value;
|
appState.zoom.value;
|
||||||
const angle = container ? container.angle : updatedElement.angle;
|
|
||||||
Object.assign(editable.style, {
|
Object.assign(editable.style, {
|
||||||
font: getFontString(updatedElement),
|
font: getFontString(updatedElement),
|
||||||
// must be defined *after* font ¯\_(ツ)_/¯
|
// must be defined *after* font ¯\_(ツ)_/¯
|
||||||
lineHeight: `${lineHeight}px`,
|
lineHeight: `${lineHeight}px`,
|
||||||
width: `${width}px`,
|
width: `${width}px`,
|
||||||
height: `${height}px`,
|
height: `${Math.max(editable.clientHeight, updatedElement.height)}px`,
|
||||||
left: `${viewportX}px`,
|
left: `${viewportX}px`,
|
||||||
top: `${viewportY}px`,
|
top: `${viewportY}px`,
|
||||||
transform: getTransform(
|
transform: getTransform(width, height, angle, appState, maxWidth),
|
||||||
width,
|
|
||||||
height,
|
|
||||||
angle,
|
|
||||||
appState,
|
|
||||||
maxWidth,
|
|
||||||
editorMaxHeight,
|
|
||||||
),
|
|
||||||
textAlign,
|
textAlign,
|
||||||
color: updatedElement.strokeColor,
|
color: updatedElement.strokeColor,
|
||||||
opacity: updatedElement.opacity / 100,
|
opacity: updatedElement.opacity / 100,
|
||||||
@@ -210,11 +212,6 @@ export const textWysiwyg = ({
|
|||||||
maxWidth: `${maxWidth}px`,
|
maxWidth: `${maxWidth}px`,
|
||||||
maxHeight: `${editorMaxHeight}px`,
|
maxHeight: `${editorMaxHeight}px`,
|
||||||
});
|
});
|
||||||
// For some reason updating font attribute doesn't set font family
|
|
||||||
// hence updating font family explicitly for test environment
|
|
||||||
if (isTestEnv()) {
|
|
||||||
editable.style.fontFamily = getFontFamilyString(updatedElement);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -257,35 +254,8 @@ export const textWysiwyg = ({
|
|||||||
|
|
||||||
if (onChange) {
|
if (onChange) {
|
||||||
editable.oninput = () => {
|
editable.oninput = () => {
|
||||||
// using scrollHeight here since we need to calculate
|
if (isBoundToContainer(element)) {
|
||||||
// number of lines so cannot use editable.style.height
|
editable.style.height = "auto";
|
||||||
// as that gets updated below
|
|
||||||
const lines = editable.scrollHeight / approxLineHeight;
|
|
||||||
// auto increase height only when lines > 1 so its
|
|
||||||
// measured correctly and vertically alignes for
|
|
||||||
// first line as well as setting height to "auto"
|
|
||||||
// doubles the height as soon as user starts typing
|
|
||||||
if (isBoundToContainer(element) && lines > 1) {
|
|
||||||
let height = "auto";
|
|
||||||
|
|
||||||
if (lines === 2) {
|
|
||||||
const container = getContainerElement(element);
|
|
||||||
const actualLineCount = wrapText(
|
|
||||||
editable.value,
|
|
||||||
getFontString(element),
|
|
||||||
container!.width,
|
|
||||||
).split("\n").length;
|
|
||||||
|
|
||||||
// This is browser behaviour when setting height to "auto"
|
|
||||||
// It sets the height needed for 2 lines even if actual
|
|
||||||
// line count is 1 as mentioned above as well
|
|
||||||
// hence reducing the height by half if actual line count is 1
|
|
||||||
// so single line aligns vertically when deleting
|
|
||||||
if (actualLineCount === 1) {
|
|
||||||
height = `${editable.scrollHeight / 2}px`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
editable.style.height = height;
|
|
||||||
editable.style.height = `${editable.scrollHeight}px`;
|
editable.style.height = `${editable.scrollHeight}px`;
|
||||||
}
|
}
|
||||||
onChange(normalizeText(editable.value));
|
onChange(normalizeText(editable.value));
|
||||||
@@ -293,16 +263,13 @@ export const textWysiwyg = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
editable.onkeydown = (event) => {
|
editable.onkeydown = (event) => {
|
||||||
if (!event[KEYS.CTRL_OR_CMD]) {
|
event.stopPropagation();
|
||||||
event.stopPropagation();
|
|
||||||
}
|
|
||||||
if (event.key === KEYS.ESCAPE) {
|
if (event.key === KEYS.ESCAPE) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
submittedViaKeyboard = true;
|
submittedViaKeyboard = true;
|
||||||
handleSubmit();
|
handleSubmit();
|
||||||
} else if (event.key === KEYS.ENTER && event[KEYS.CTRL_OR_CMD]) {
|
} else if (event.key === KEYS.ENTER && event[KEYS.CTRL_OR_CMD]) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
|
||||||
if (event.isComposing || event.keyCode === 229) {
|
if (event.isComposing || event.keyCode === 229) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -315,7 +282,6 @@ export const textWysiwyg = ({
|
|||||||
event.code === CODES.BRACKET_RIGHT))
|
event.code === CODES.BRACKET_RIGHT))
|
||||||
) {
|
) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
|
||||||
if (event.shiftKey || event.code === CODES.BRACKET_LEFT) {
|
if (event.shiftKey || event.code === CODES.BRACKET_LEFT) {
|
||||||
outdent();
|
outdent();
|
||||||
} else {
|
} else {
|
||||||
@@ -440,7 +406,9 @@ export const textWysiwyg = ({
|
|||||||
}
|
}
|
||||||
let wrappedText = "";
|
let wrappedText = "";
|
||||||
if (isTextElement(updateElement) && updateElement?.containerId) {
|
if (isTextElement(updateElement) && updateElement?.containerId) {
|
||||||
const container = getContainerElement(updateElement);
|
const container = Scene.getScene(updateElement)!.getElement(
|
||||||
|
updateElement.containerId,
|
||||||
|
) as ExcalidrawBindableElement;
|
||||||
|
|
||||||
if (container) {
|
if (container) {
|
||||||
wrappedText = wrapText(
|
wrappedText = wrapText(
|
||||||
@@ -448,23 +416,21 @@ export const textWysiwyg = ({
|
|||||||
getFontString(updateElement),
|
getFontString(updateElement),
|
||||||
container.width,
|
container.width,
|
||||||
);
|
);
|
||||||
|
const { x, y } = viewportCoordsToSceneCoords(
|
||||||
if (updateElement.containerId) {
|
{
|
||||||
const editorHeight = Number(editable.style.height.slice(0, -2));
|
clientX: Number(editable.style.left.slice(0, -2)),
|
||||||
|
clientY: Number(editable.style.top.slice(0, -2)),
|
||||||
|
},
|
||||||
|
appState,
|
||||||
|
);
|
||||||
|
if (isTextElement(updateElement) && updateElement.containerId) {
|
||||||
if (editable.value) {
|
if (editable.value) {
|
||||||
// Don't mutate if text is not updated
|
mutateElement(updateElement, {
|
||||||
if (initialText !== editable.value) {
|
y: y + appState.offsetTop,
|
||||||
mutateElement(updateElement, {
|
height: Number(editable.style.height.slice(0, -2)),
|
||||||
// vertically center align
|
width: Number(editable.style.width.slice(0, -2)),
|
||||||
y: container.y + container.height / 2 - editorHeight / 2,
|
x: x + appState.offsetLeft,
|
||||||
height: editorHeight,
|
});
|
||||||
width: Number(editable.style.width.slice(0, -2)),
|
|
||||||
// preserve padding
|
|
||||||
x: container.x + BOUND_TEXT_PADDING,
|
|
||||||
angle: container.angle,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const boundTextElementId = getBoundTextElementId(container);
|
const boundTextElementId = getBoundTextElementId(container);
|
||||||
if (!boundTextElementId || boundTextElementId !== element.id) {
|
if (!boundTextElementId || boundTextElementId !== element.id) {
|
||||||
mutateElement(container, {
|
mutateElement(container, {
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ import {
|
|||||||
getDefaultAppState,
|
getDefaultAppState,
|
||||||
} from "../../appState";
|
} from "../../appState";
|
||||||
import { clearElementsForLocalStorage } from "../../element";
|
import { clearElementsForLocalStorage } from "../../element";
|
||||||
|
import { STORAGE_KEYS as APP_STORAGE_KEYS } from "../../constants";
|
||||||
|
|
||||||
export const STORAGE_KEYS = {
|
export const STORAGE_KEYS = {
|
||||||
LOCAL_STORAGE_ELEMENTS: "excalidraw",
|
LOCAL_STORAGE_ELEMENTS: "excalidraw",
|
||||||
LOCAL_STORAGE_APP_STATE: "excalidraw-state",
|
LOCAL_STORAGE_APP_STATE: "excalidraw-state",
|
||||||
LOCAL_STORAGE_COLLAB: "excalidraw-collab",
|
LOCAL_STORAGE_COLLAB: "excalidraw-collab",
|
||||||
LOCAL_STORAGE_KEY_COLLAB_FORCE_FLAG: "collabLinkForceLoadFlag",
|
LOCAL_STORAGE_KEY_COLLAB_FORCE_FLAG: "collabLinkForceLoadFlag",
|
||||||
LOCAL_STORAGE_LIBRARY: "excalidraw-library",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const saveUsernameToLocalStorage = (username: string) => {
|
export const saveUsernameToLocalStorage = (username: string) => {
|
||||||
@@ -113,7 +113,9 @@ export const getTotalStorageSize = () => {
|
|||||||
try {
|
try {
|
||||||
const appState = localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_APP_STATE);
|
const appState = localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_APP_STATE);
|
||||||
const collab = localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_COLLAB);
|
const collab = localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_COLLAB);
|
||||||
const library = localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY);
|
const library = localStorage.getItem(
|
||||||
|
APP_STORAGE_KEYS.LOCAL_STORAGE_LIBRARY,
|
||||||
|
);
|
||||||
|
|
||||||
const appStateSize = appState?.length || 0;
|
const appStateSize = appState?.length || 0;
|
||||||
const collabSize = collab?.length || 0;
|
const collabSize = collab?.length || 0;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { TopErrorBoundary } from "../components/TopErrorBoundary";
|
|||||||
import {
|
import {
|
||||||
APP_NAME,
|
APP_NAME,
|
||||||
EVENT,
|
EVENT,
|
||||||
|
STORAGE_KEYS,
|
||||||
TITLE_TIMEOUT,
|
TITLE_TIMEOUT,
|
||||||
URL_HASH_KEYS,
|
URL_HASH_KEYS,
|
||||||
VERSION_TIMEOUT,
|
VERSION_TIMEOUT,
|
||||||
@@ -52,7 +53,6 @@ import { exportToBackend, getCollaborationLinkData, loadScene } from "./data";
|
|||||||
import {
|
import {
|
||||||
importFromLocalStorage,
|
importFromLocalStorage,
|
||||||
saveToLocalStorage,
|
saveToLocalStorage,
|
||||||
STORAGE_KEYS,
|
|
||||||
} from "./data/localStorage";
|
} from "./data/localStorage";
|
||||||
import CustomStats from "./CustomStats";
|
import CustomStats from "./CustomStats";
|
||||||
import { restoreAppState, RestoredDataState } from "../data/restore";
|
import { restoreAppState, RestoredDataState } from "../data/restore";
|
||||||
@@ -262,7 +262,7 @@ const PlusLinkJSX = (
|
|||||||
Introducing Excalidraw+
|
Introducing Excalidraw+
|
||||||
<br />
|
<br />
|
||||||
<a
|
<a
|
||||||
href="https://plus.excalidraw.com/plus?utm_source=excalidraw&utm_medium=banner&utm_campaign=launch"
|
href="https://plus.excalidraw.com/?utm_source=excalidraw&utm_medium=banner&utm_campaign=launch"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
|
|||||||
+1
-38
@@ -1,13 +1,6 @@
|
|||||||
import {
|
import { GroupId, ExcalidrawElement, NonDeleted } from "./element/types";
|
||||||
GroupId,
|
|
||||||
ExcalidrawElement,
|
|
||||||
NonDeleted,
|
|
||||||
ExcalidrawTextElementWithContainer,
|
|
||||||
} from "./element/types";
|
|
||||||
import { AppState } from "./types";
|
import { AppState } from "./types";
|
||||||
import { getSelectedElements } from "./scene";
|
import { getSelectedElements } from "./scene";
|
||||||
import { getBoundTextElementId } from "./element/textElement";
|
|
||||||
import Scene from "./scene/Scene";
|
|
||||||
|
|
||||||
export const selectGroup = (
|
export const selectGroup = (
|
||||||
groupId: GroupId,
|
groupId: GroupId,
|
||||||
@@ -165,33 +158,3 @@ export const removeFromSelectedGroups = (
|
|||||||
groupIds: ExcalidrawElement["groupIds"],
|
groupIds: ExcalidrawElement["groupIds"],
|
||||||
selectedGroupIds: { [groupId: string]: boolean },
|
selectedGroupIds: { [groupId: string]: boolean },
|
||||||
) => groupIds.filter((groupId) => !selectedGroupIds[groupId]);
|
) => groupIds.filter((groupId) => !selectedGroupIds[groupId]);
|
||||||
|
|
||||||
export const getMaximumGroups = (
|
|
||||||
elements: ExcalidrawElement[],
|
|
||||||
): ExcalidrawElement[][] => {
|
|
||||||
const groups: Map<String, ExcalidrawElement[]> = new Map<
|
|
||||||
String,
|
|
||||||
ExcalidrawElement[]
|
|
||||||
>();
|
|
||||||
|
|
||||||
elements.forEach((element: ExcalidrawElement) => {
|
|
||||||
const groupId =
|
|
||||||
element.groupIds.length === 0
|
|
||||||
? element.id
|
|
||||||
: element.groupIds[element.groupIds.length - 1];
|
|
||||||
|
|
||||||
const currentGroupMembers = groups.get(groupId) || [];
|
|
||||||
|
|
||||||
// Include bounded text if present when grouping
|
|
||||||
const boundTextElementId = getBoundTextElementId(element);
|
|
||||||
if (boundTextElementId) {
|
|
||||||
const textElement = Scene.getScene(element)!.getElement(
|
|
||||||
boundTextElementId,
|
|
||||||
) as ExcalidrawTextElementWithContainer;
|
|
||||||
currentGroupMembers.push(textElement);
|
|
||||||
}
|
|
||||||
groups.set(groupId, [...currentGroupMembers, element]);
|
|
||||||
});
|
|
||||||
|
|
||||||
return Array.from(groups.values());
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -40,10 +40,6 @@ export const KEYS = {
|
|||||||
QUESTION_MARK: "?",
|
QUESTION_MARK: "?",
|
||||||
SPACE: " ",
|
SPACE: " ",
|
||||||
TAB: "Tab",
|
TAB: "Tab",
|
||||||
CHEVRON_LEFT: "<",
|
|
||||||
CHEVRON_RIGHT: ">",
|
|
||||||
PERIOD: ".",
|
|
||||||
COMMA: ",",
|
|
||||||
|
|
||||||
A: "a",
|
A: "a",
|
||||||
D: "d",
|
D: "d",
|
||||||
|
|||||||
+2
-7
@@ -102,9 +102,7 @@
|
|||||||
"showBackground": "Show background color picker",
|
"showBackground": "Show background color picker",
|
||||||
"toggleTheme": "Toggle theme",
|
"toggleTheme": "Toggle theme",
|
||||||
"personalLib": "Personal Library",
|
"personalLib": "Personal Library",
|
||||||
"excalidrawLib": "Excalidraw Library",
|
"excalidrawLib": "Excalidraw Library"
|
||||||
"decreaseFontSize": "Decrease font size",
|
|
||||||
"increaseFontSize": "Increase font size"
|
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "Reset the canvas",
|
"clearReset": "Reset the canvas",
|
||||||
@@ -210,8 +208,7 @@
|
|||||||
"lineEditor_nothingSelected": "Select a point to edit (hold SHIFT to select multiple),\nor hold Alt and click to add new points",
|
"lineEditor_nothingSelected": "Select a point to edit (hold SHIFT to select multiple),\nor hold Alt and click to add new points",
|
||||||
"placeImage": "Click to place the image, or click and drag to set its size manually",
|
"placeImage": "Click to place the image, or click and drag to set its size manually",
|
||||||
"publishLibrary": "Publish your own library",
|
"publishLibrary": "Publish your own library",
|
||||||
"bindTextToElement": "Press enter to add text",
|
"bindTextToElement": "Press enter to add text"
|
||||||
"deepBoxSelect": "Hold CtrlOrCmd to deep select, and to prevent dragging"
|
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "Cannot show preview",
|
"cannotShowPreview": "Cannot show preview",
|
||||||
@@ -258,8 +255,6 @@
|
|||||||
"helpDialog": {
|
"helpDialog": {
|
||||||
"blog": "Read our blog",
|
"blog": "Read our blog",
|
||||||
"click": "click",
|
"click": "click",
|
||||||
"deepSelect": "Deep select",
|
|
||||||
"deepBoxSelect": "Deep select within box, and prevent dragging",
|
|
||||||
"curvedArrow": "Curved arrow",
|
"curvedArrow": "Curved arrow",
|
||||||
"curvedLine": "Curved line",
|
"curvedLine": "Curved line",
|
||||||
"documentation": "Documentation",
|
"documentation": "Documentation",
|
||||||
|
|||||||
@@ -19,8 +19,6 @@ Please add the latest change on the top under the correct section.
|
|||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
- Support updating library using [`updateScene`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#updateScene) API [#4546](https://github.com/excalidraw/excalidraw/pull/4546).
|
|
||||||
|
|
||||||
- Introduced primary colors to the app. The colors can be overriden. Check [readme](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#customizing-styles) on how to do so.
|
- Introduced primary colors to the app. The colors can be overriden. Check [readme](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#customizing-styles) on how to do so.
|
||||||
|
|
||||||
- #### BREAKING CHANGE
|
- #### BREAKING CHANGE
|
||||||
@@ -66,15 +64,8 @@ Please add the latest change on the top under the correct section.
|
|||||||
|
|
||||||
The `Appearance` type is now removed and renamed to `Theme` so `Theme` type needs to be used.
|
The `Appearance` type is now removed and renamed to `Theme` so `Theme` type needs to be used.
|
||||||
|
|
||||||
### Fixes
|
|
||||||
|
|
||||||
- Panning the canvas using `mousewheel-drag` and `space-drag` now prevents the browser from scrolling the container/page [#4489](https://github.com/excalidraw/excalidraw/pull/4489).
|
|
||||||
- Scope drag and drop events to Excalidraw container to prevent overriding host application drag and drop events.
|
|
||||||
|
|
||||||
### Build
|
### Build
|
||||||
|
|
||||||
- Added an example to test and develop the package [locally](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#Development) using `yarn start`
|
|
||||||
|
|
||||||
- Remove `file-loader` so font assets are not duplicated by webpack and use webpack asset modules for font generation [#4380](https://github.com/excalidraw/excalidraw/pull/4380)
|
- Remove `file-loader` so font assets are not duplicated by webpack and use webpack asset modules for font generation [#4380](https://github.com/excalidraw/excalidraw/pull/4380)
|
||||||
|
|
||||||
- We're now compiling to `es2017` target. Notably, `async/await` is not compiled down to generators. [#4341](https://github.com/excalidraw/excalidraw/pull/4341)
|
- We're now compiling to `es2017` target. Notably, `async/await` is not compiled down to generators. [#4341](https://github.com/excalidraw/excalidraw/pull/4341)
|
||||||
|
|||||||
@@ -509,7 +509,6 @@ You can use this function to update the scene with the sceneData. It accepts the
|
|||||||
| `appState` | [`ImportedDataState["appState"]`](https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L18) | The `appState` to be updated in the scene. |
|
| `appState` | [`ImportedDataState["appState"]`](https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L18) | The `appState` to be updated in the scene. |
|
||||||
| `collaborators` | <pre>Map<string, <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L29">Collaborator></a></pre> | The list of collaborators to be updated in the scene. |
|
| `collaborators` | <pre>Map<string, <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L29">Collaborator></a></pre> | The list of collaborators to be updated in the scene. |
|
||||||
| `commitToHistory` | `boolean` | Implies if the `history (undo/redo)` should be recorded. Defaults to `false`. |
|
| `commitToHistory` | `boolean` | Implies if the `history (undo/redo)` should be recorded. Defaults to `false`. |
|
||||||
| `libraryItems` | [LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L258) | The `libraryItems` to be update in the scene. |
|
|
||||||
|
|
||||||
### `addFiles`
|
### `addFiles`
|
||||||
|
|
||||||
@@ -1009,21 +1008,3 @@ Defaults to `THEME.LIGHT` unless passed in `initialData.appState.theme`
|
|||||||
## Need help?
|
## Need help?
|
||||||
|
|
||||||
Check out the existing [Q&A](https://github.com/excalidraw/excalidraw/discussions?discussions_q=label%3Apackage%3Aexcalidraw). If you have any queries or need help, ask us [here](https://github.com/excalidraw/excalidraw/discussions?discussions_q=label%3Apackage%3Aexcalidraw).
|
Check out the existing [Q&A](https://github.com/excalidraw/excalidraw/discussions?discussions_q=label%3Apackage%3Aexcalidraw). If you have any queries or need help, ask us [here](https://github.com/excalidraw/excalidraw/discussions?discussions_q=label%3Apackage%3Aexcalidraw).
|
||||||
|
|
||||||
### Development
|
|
||||||
|
|
||||||
#### Install the dependencies
|
|
||||||
|
|
||||||
```bash
|
|
||||||
yarn
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Start the server
|
|
||||||
|
|
||||||
```bash
|
|
||||||
yarn start
|
|
||||||
```
|
|
||||||
|
|
||||||
[http://localhost:3001](http://localhost:3001) will open in your default browser.
|
|
||||||
|
|
||||||
The example is same as the [codesandbox example](https://ehlz3.csb.app/)
|
|
||||||
|
|||||||
@@ -1,328 +0,0 @@
|
|||||||
import { useEffect, useState, useRef } from "react";
|
|
||||||
|
|
||||||
import InitialData from "./initialData";
|
|
||||||
import Sidebar from "./sidebar/Sidebar";
|
|
||||||
|
|
||||||
import "./App.scss";
|
|
||||||
import initialData from "./initialData";
|
|
||||||
|
|
||||||
// This is so that we use the bundled excalidraw.developement.js file instead
|
|
||||||
// of the actual source code
|
|
||||||
const { exportToCanvas, exportToSvg, exportToBlob } = window.Excalidraw;
|
|
||||||
const Excalidraw = window.Excalidraw.default;
|
|
||||||
|
|
||||||
const renderTopRightUI = () => {
|
|
||||||
return (
|
|
||||||
<button onClick={() => alert("This is dummy top right UI")}>
|
|
||||||
{" "}
|
|
||||||
Click me{" "}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderFooter = () => {
|
|
||||||
return (
|
|
||||||
<button onClick={() => alert("This is dummy footer")}>
|
|
||||||
{" "}
|
|
||||||
custom footer{" "}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function App() {
|
|
||||||
const excalidrawRef = useRef(null);
|
|
||||||
|
|
||||||
const [viewModeEnabled, setViewModeEnabled] = useState(false);
|
|
||||||
const [zenModeEnabled, setZenModeEnabled] = useState(false);
|
|
||||||
const [gridModeEnabled, setGridModeEnabled] = useState(false);
|
|
||||||
const [blobUrl, setBlobUrl] = useState(null);
|
|
||||||
const [canvasUrl, setCanvasUrl] = useState(null);
|
|
||||||
const [exportWithDarkMode, setExportWithDarkMode] = useState(false);
|
|
||||||
const [shouldAddWatermark, setShouldAddWatermark] = useState(false);
|
|
||||||
const [theme, setTheme] = useState("light");
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const onHashChange = () => {
|
|
||||||
const hash = new URLSearchParams(window.location.hash.slice(1));
|
|
||||||
const libraryUrl = hash.get("addLibrary");
|
|
||||||
if (libraryUrl) {
|
|
||||||
excalidrawRef.current.importLibrary(libraryUrl, hash.get("token"));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
window.addEventListener("hashchange", onHashChange, false);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener("hashchange", onHashChange);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const updateScene = () => {
|
|
||||||
const sceneData = {
|
|
||||||
elements: [
|
|
||||||
{
|
|
||||||
type: "rectangle",
|
|
||||||
version: 141,
|
|
||||||
versionNonce: 361174001,
|
|
||||||
isDeleted: false,
|
|
||||||
id: "oDVXy8D6rom3H1-LLH2-f",
|
|
||||||
fillStyle: "hachure",
|
|
||||||
strokeWidth: 1,
|
|
||||||
strokeStyle: "solid",
|
|
||||||
roughness: 1,
|
|
||||||
opacity: 100,
|
|
||||||
angle: 0,
|
|
||||||
x: 100.50390625,
|
|
||||||
y: 93.67578125,
|
|
||||||
strokeColor: "#c92a2a",
|
|
||||||
backgroundColor: "transparent",
|
|
||||||
width: 186.47265625,
|
|
||||||
height: 141.9765625,
|
|
||||||
seed: 1968410350,
|
|
||||||
groupIds: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "Ioiq-J6pE6vMftvReHcq7",
|
|
||||||
type: "rectangle",
|
|
||||||
x: 530.01171875,
|
|
||||||
y: 120.31640625,
|
|
||||||
width: 158.34375,
|
|
||||||
height: 175.52734375,
|
|
||||||
angle: 0,
|
|
||||||
strokeColor: "#000000",
|
|
||||||
backgroundColor: "#e64980",
|
|
||||||
fillStyle: "solid",
|
|
||||||
strokeWidth: 1,
|
|
||||||
strokeStyle: "solid",
|
|
||||||
roughness: 1,
|
|
||||||
opacity: 100,
|
|
||||||
groupIds: [],
|
|
||||||
strokeSharpness: "sharp",
|
|
||||||
seed: 1752793487,
|
|
||||||
version: 243,
|
|
||||||
versionNonce: 1367252367,
|
|
||||||
isDeleted: false,
|
|
||||||
boundElementIds: ["QEIIo0aX3LIkipph5r9Vc"],
|
|
||||||
updated: 1642057965561,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "QEIIo0aX3LIkipph5r9Vc",
|
|
||||||
type: "arrow",
|
|
||||||
x: 300.73046875,
|
|
||||||
y: 200.47265625,
|
|
||||||
width: 231.7578125,
|
|
||||||
height: 9.823413575122089,
|
|
||||||
angle: 0,
|
|
||||||
strokeColor: "#000000",
|
|
||||||
backgroundColor: "#e64980",
|
|
||||||
fillStyle: "solid",
|
|
||||||
strokeWidth: 1,
|
|
||||||
strokeStyle: "solid",
|
|
||||||
roughness: 1,
|
|
||||||
opacity: 100,
|
|
||||||
groupIds: [],
|
|
||||||
strokeSharpness: "round",
|
|
||||||
seed: 1335966913,
|
|
||||||
version: 180,
|
|
||||||
versionNonce: 24053473,
|
|
||||||
isDeleted: false,
|
|
||||||
boundElementIds: null,
|
|
||||||
updated: 1642057965562,
|
|
||||||
points: [
|
|
||||||
[0, 0],
|
|
||||||
[231.7578125, -9.823413575122089],
|
|
||||||
],
|
|
||||||
lastCommittedPoint: null,
|
|
||||||
startBinding: null,
|
|
||||||
endBinding: {
|
|
||||||
elementId: "Ioiq-J6pE6vMftvReHcq7",
|
|
||||||
focus: 0.012598149333175906,
|
|
||||||
gap: 8.5234375,
|
|
||||||
},
|
|
||||||
startArrowhead: null,
|
|
||||||
endArrowhead: "arrow",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
appState: {
|
|
||||||
viewBackgroundColor: "#edf2ff",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
excalidrawRef.current.updateScene(sceneData);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="App">
|
|
||||||
<h1> Excalidraw Example</h1>
|
|
||||||
<Sidebar>
|
|
||||||
<div className="button-wrapper">
|
|
||||||
<button className="update-scene" onClick={updateScene}>
|
|
||||||
Update Scene
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="reset-scene"
|
|
||||||
onClick={() => {
|
|
||||||
excalidrawRef.current.resetScene();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Reset Scene
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
excalidrawRef.current.updateScene({
|
|
||||||
libraryItems: [
|
|
||||||
{
|
|
||||||
status: "published",
|
|
||||||
elements: initialData.libraryItems[0],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
status: "unpublished",
|
|
||||||
elements: initialData.libraryItems[1],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Update Library
|
|
||||||
</button>
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={viewModeEnabled}
|
|
||||||
onChange={() => setViewModeEnabled(!viewModeEnabled)}
|
|
||||||
/>
|
|
||||||
View mode
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={zenModeEnabled}
|
|
||||||
onChange={() => setZenModeEnabled(!zenModeEnabled)}
|
|
||||||
/>
|
|
||||||
Zen mode
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={gridModeEnabled}
|
|
||||||
onChange={() => setGridModeEnabled(!gridModeEnabled)}
|
|
||||||
/>
|
|
||||||
Grid mode
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={theme === "dark"}
|
|
||||||
onChange={() => {
|
|
||||||
let newTheme = "light";
|
|
||||||
if (theme === "light") {
|
|
||||||
newTheme = "dark";
|
|
||||||
}
|
|
||||||
setTheme(newTheme);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
Switch to Dark Theme
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className="excalidraw-wrapper">
|
|
||||||
<Excalidraw
|
|
||||||
ref={excalidrawRef}
|
|
||||||
initialData={InitialData}
|
|
||||||
onChange={(elements, state) =>
|
|
||||||
console.info("Elements :", elements, "State : ", state)
|
|
||||||
}
|
|
||||||
onPointerUpdate={(payload) => console.info(payload)}
|
|
||||||
onCollabButtonClick={() =>
|
|
||||||
window.alert("You clicked on collab button")
|
|
||||||
}
|
|
||||||
viewModeEnabled={viewModeEnabled}
|
|
||||||
zenModeEnabled={zenModeEnabled}
|
|
||||||
gridModeEnabled={gridModeEnabled}
|
|
||||||
theme={theme}
|
|
||||||
name="Custom name of drawing"
|
|
||||||
UIOptions={{ canvasActions: { loadScene: false } }}
|
|
||||||
renderTopRightUI={renderTopRightUI}
|
|
||||||
renderFooter={renderFooter}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="export-wrapper button-wrapper">
|
|
||||||
<label className="export-wrapper__checkbox">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={exportWithDarkMode}
|
|
||||||
onChange={() => setExportWithDarkMode(!exportWithDarkMode)}
|
|
||||||
/>
|
|
||||||
Export with dark mode
|
|
||||||
</label>
|
|
||||||
<label className="export-wrapper__checkbox">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={shouldAddWatermark}
|
|
||||||
onChange={() => setShouldAddWatermark(!shouldAddWatermark)}
|
|
||||||
/>
|
|
||||||
Add Watermark
|
|
||||||
</label>
|
|
||||||
<button
|
|
||||||
onClick={async () => {
|
|
||||||
const svg = await exportToSvg({
|
|
||||||
elements: excalidrawRef.current.getSceneElements(),
|
|
||||||
appState: {
|
|
||||||
...initialData.appState,
|
|
||||||
exportWithDarkMode,
|
|
||||||
shouldAddWatermark,
|
|
||||||
width: 300,
|
|
||||||
height: 100,
|
|
||||||
},
|
|
||||||
embedScene: true,
|
|
||||||
});
|
|
||||||
document.querySelector(".export-svg").innerHTML = svg.outerHTML;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Export to SVG
|
|
||||||
</button>
|
|
||||||
<div className="export export-svg"></div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={async () => {
|
|
||||||
const blob = await exportToBlob({
|
|
||||||
elements: excalidrawRef.current.getSceneElements(),
|
|
||||||
mimeType: "image/png",
|
|
||||||
appState: {
|
|
||||||
...initialData.appState,
|
|
||||||
exportWithDarkMode,
|
|
||||||
shouldAddWatermark,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
setBlobUrl(window.URL.createObjectURL(blob));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Export to Blob
|
|
||||||
</button>
|
|
||||||
<div className="export export-blob">
|
|
||||||
<img src={blobUrl} alt="" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
const canvas = exportToCanvas({
|
|
||||||
elements: excalidrawRef.current.getSceneElements(),
|
|
||||||
appState: {
|
|
||||||
...initialData.appState,
|
|
||||||
exportWithDarkMode,
|
|
||||||
shouldAddWatermark,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const ctx = canvas.getContext("2d");
|
|
||||||
ctx.font = "30px Virgil";
|
|
||||||
ctx.strokeText("My custom text", 50, 60);
|
|
||||||
setCanvasUrl(canvas.toDataURL());
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Export to Canvas
|
|
||||||
</button>
|
|
||||||
<div className="export export-canvas">
|
|
||||||
<img src={canvasUrl} alt="" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Sidebar>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
.App {
|
|
||||||
font-family: sans-serif;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-wrapper button {
|
|
||||||
z-index: 1;
|
|
||||||
height: 40px;
|
|
||||||
max-width: 200px;
|
|
||||||
margin: 10px;
|
|
||||||
padding: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.excalidraw .App-menu_top .buttonList {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.excalidraw-wrapper {
|
|
||||||
height: 800px;
|
|
||||||
margin: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[dir="ltr"]
|
|
||||||
.excalidraw
|
|
||||||
.layer-ui__wrapper
|
|
||||||
.zen-mode-transition.App-menu_bottom--transition-left {
|
|
||||||
transform: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.excalidraw .panelColumn {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.export-wrapper {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
margin: 50px;
|
|
||||||
|
|
||||||
&__checkbox {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta
|
|
||||||
name="viewport"
|
|
||||||
content="width=device-width, initial-scale=1, shrink-to-fit=no"
|
|
||||||
/>
|
|
||||||
<meta name="theme-color" content="#000000" />
|
|
||||||
|
|
||||||
<title>React App</title>
|
|
||||||
<script>
|
|
||||||
window.name = "codesandbox";
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<noscript> You need to enable JavaScript to run this app. </noscript>
|
|
||||||
<div id="root"></div>
|
|
||||||
<script src="https://unpkg.com/react@17.0.2/umd/react.development.js"></script>
|
|
||||||
<script src="https://unpkg.com/react-dom@17.0.2/umd/react-dom.development.js"></script>
|
|
||||||
|
|
||||||
<!-- This is so that we use the bundled excalidraw.developement.js file instead
|
|
||||||
of the actual source code -->
|
|
||||||
<script src="./excalidraw.development.js"></script>
|
|
||||||
|
|
||||||
<script src="./bundle.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import ReactDOM from "react-dom";
|
|
||||||
|
|
||||||
import App from "./App";
|
|
||||||
|
|
||||||
const rootElement = document.getElementById("root");
|
|
||||||
ReactDOM.render(
|
|
||||||
<React.StrictMode>
|
|
||||||
<App />
|
|
||||||
</React.StrictMode>,
|
|
||||||
rootElement,
|
|
||||||
);
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,30 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import "./Sidebar.scss";
|
|
||||||
export default function Sidebar(props) {
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div id="mySidebar" className={`sidebar ${open ? "open" : ""}`}>
|
|
||||||
<button className="closebtn" onClick={() => setOpen(false)}>
|
|
||||||
x
|
|
||||||
</button>
|
|
||||||
<div className="sidebar-links">
|
|
||||||
<button>Dummy Home</button>
|
|
||||||
<button>Dummy About</button>{" "}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={`${open ? "sidebar-open" : ""}`}>
|
|
||||||
<button
|
|
||||||
className="openbtn"
|
|
||||||
onClick={() => {
|
|
||||||
setOpen(!open);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Open Sidebar
|
|
||||||
</button>
|
|
||||||
{props.children}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
.sidebar {
|
|
||||||
height: 100%;
|
|
||||||
width: 0;
|
|
||||||
position: absolute;
|
|
||||||
z-index: 1;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
background-color: #111;
|
|
||||||
overflow-x: hidden;
|
|
||||||
transition: 0.5s;
|
|
||||||
padding-top: 60px;
|
|
||||||
|
|
||||||
&.open {
|
|
||||||
width: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-links {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 20px;
|
|
||||||
|
|
||||||
button {
|
|
||||||
padding: 10px;
|
|
||||||
margin: 10px;
|
|
||||||
background: #faa2c1;
|
|
||||||
color: #fff;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar a {
|
|
||||||
padding: 8px 8px 8px 32px;
|
|
||||||
text-decoration: none;
|
|
||||||
font-size: 25px;
|
|
||||||
color: #818181;
|
|
||||||
display: block;
|
|
||||||
transition: 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar a:hover {
|
|
||||||
color: #f1f1f1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar .closebtn {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
font-size: 36px;
|
|
||||||
margin-left: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.openbtn {
|
|
||||||
font-size: 20px;
|
|
||||||
cursor: pointer;
|
|
||||||
background-color: #111;
|
|
||||||
color: white;
|
|
||||||
padding: 10px 15px;
|
|
||||||
border: none;
|
|
||||||
display: flex;
|
|
||||||
margin-left: 50px;
|
|
||||||
}
|
|
||||||
.sidebar-open {
|
|
||||||
margin-left: 300px;
|
|
||||||
}
|
|
||||||
@@ -44,30 +44,28 @@
|
|||||||
"react-dom": "^17.0.2"
|
"react-dom": "^17.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.16.7",
|
"@babel/core": "7.16.0",
|
||||||
"@babel/plugin-transform-arrow-functions": "7.16.7",
|
"@babel/plugin-transform-arrow-functions": "7.16.0",
|
||||||
"@babel/plugin-transform-async-to-generator": "7.16.0",
|
"@babel/plugin-transform-async-to-generator": "7.16.0",
|
||||||
"@babel/plugin-transform-runtime": "7.16.4",
|
"@babel/plugin-transform-runtime": "7.16.4",
|
||||||
"@babel/plugin-transform-typescript": "7.16.1",
|
"@babel/plugin-transform-typescript": "7.16.1",
|
||||||
"@babel/preset-env": "7.16.7",
|
"@babel/preset-env": "7.16.4",
|
||||||
"@babel/preset-react": "7.16.7",
|
"@babel/preset-react": "7.16.0",
|
||||||
"@babel/preset-typescript": "7.16.7",
|
"@babel/preset-typescript": "7.16.0",
|
||||||
"autoprefixer": "10.4.2",
|
"autoprefixer": "10.4.0",
|
||||||
"babel-loader": "8.2.3",
|
"babel-loader": "8.2.3",
|
||||||
"babel-plugin-transform-class-properties": "6.24.1",
|
"babel-plugin-transform-class-properties": "6.24.1",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"css-loader": "6.5.1",
|
"css-loader": "6.5.1",
|
||||||
"mini-css-extract-plugin": "2.4.6",
|
"mini-css-extract-plugin": "2.4.5",
|
||||||
"postcss-loader": "6.2.1",
|
"postcss-loader": "6.2.1",
|
||||||
"sass-loader": "12.4.0",
|
"sass-loader": "12.4.0",
|
||||||
"terser-webpack-plugin": "5.3.0",
|
"terser-webpack-plugin": "5.2.5",
|
||||||
"ts-loader": "9.2.6",
|
"ts-loader": "9.2.6",
|
||||||
"typescript": "4.5.4",
|
"typescript": "4.5.3",
|
||||||
"webpack": "5.65.0",
|
"webpack": "5.65.0",
|
||||||
"webpack-bundle-analyzer": "4.5.0",
|
"webpack-bundle-analyzer": "4.5.0",
|
||||||
"webpack-cli": "4.9.1",
|
"webpack-cli": "4.9.1"
|
||||||
"webpack-dev-server": "4.7.2",
|
|
||||||
"webpack-merge": "5.8.0"
|
|
||||||
},
|
},
|
||||||
"bugs": "https://github.com/excalidraw/excalidraw/issues",
|
"bugs": "https://github.com/excalidraw/excalidraw/issues",
|
||||||
"homepage": "https://github.com/excalidraw/excalidraw/tree/master/src/packages/excalidraw",
|
"homepage": "https://github.com/excalidraw/excalidraw/tree/master/src/packages/excalidraw",
|
||||||
@@ -75,8 +73,7 @@
|
|||||||
"gen:types": "tsc --project ../../../tsconfig-types.json",
|
"gen:types": "tsc --project ../../../tsconfig-types.json",
|
||||||
"build:umd": "rm -rf dist && cross-env NODE_ENV=production webpack --config webpack.prod.config.js && cross-env NODE_ENV=development webpack --config webpack.dev.config.js && yarn gen:types",
|
"build:umd": "rm -rf dist && cross-env NODE_ENV=production webpack --config webpack.prod.config.js && cross-env NODE_ENV=development webpack --config webpack.dev.config.js && yarn gen:types",
|
||||||
"build:umd:withAnalyzer": "cross-env NODE_ENV=production ANALYZER=true webpack --config webpack.prod.config.js",
|
"build:umd:withAnalyzer": "cross-env NODE_ENV=production ANALYZER=true webpack --config webpack.prod.config.js",
|
||||||
"pack": "yarn build:umd && yarn pack",
|
"pack": "yarn build:umd && yarn pack"
|
||||||
"start": "webpack serve --config webpack.dev-server.config.js "
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dotenv": "10.0.0"
|
"dotenv": "10.0.0"
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
const path = require("path");
|
|
||||||
const { merge } = require("webpack-merge");
|
|
||||||
|
|
||||||
const devConfig = require("./webpack.dev.config");
|
|
||||||
|
|
||||||
const devServerConfig = {
|
|
||||||
entry: {
|
|
||||||
bundle: "./example/index.js",
|
|
||||||
},
|
|
||||||
// Server Configuration options
|
|
||||||
devServer: {
|
|
||||||
port: 3001,
|
|
||||||
host: "localhost",
|
|
||||||
hot: true,
|
|
||||||
compress: true,
|
|
||||||
static: {
|
|
||||||
directory: path.join(__dirname, "example"),
|
|
||||||
},
|
|
||||||
client: {
|
|
||||||
progress: true,
|
|
||||||
logging: "info",
|
|
||||||
overlay: true, //Shows a full-screen overlay in the browser when there are compiler errors or warnings.
|
|
||||||
},
|
|
||||||
open: ["./"],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = merge(devServerConfig, devConfig);
|
|
||||||
+566
-2008
File diff suppressed because it is too large
Load Diff
@@ -34,13 +34,13 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.16.7",
|
"@babel/core": "7.16.0",
|
||||||
"@babel/plugin-transform-arrow-functions": "7.16.0",
|
"@babel/plugin-transform-arrow-functions": "7.16.0",
|
||||||
"@babel/plugin-transform-async-to-generator": "7.16.5",
|
"@babel/plugin-transform-async-to-generator": "7.16.0",
|
||||||
"@babel/plugin-transform-runtime": "^7.14.5",
|
"@babel/plugin-transform-runtime": "^7.14.5",
|
||||||
"@babel/plugin-transform-typescript": "7.16.7",
|
"@babel/plugin-transform-typescript": "7.16.1",
|
||||||
"@babel/preset-env": "7.16.7",
|
"@babel/preset-env": "7.16.4",
|
||||||
"@babel/preset-typescript": "7.16.7",
|
"@babel/preset-typescript": "7.16.0",
|
||||||
"babel-loader": "8.2.3",
|
"babel-loader": "8.2.3",
|
||||||
"babel-plugin-transform-class-properties": "6.24.1",
|
"babel-plugin-transform-class-properties": "6.24.1",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
|
|||||||
+536
-597
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -1,6 +1,5 @@
|
|||||||
import { Random } from "roughjs/bin/math";
|
import { Random } from "roughjs/bin/math";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { isTestEnv } from "./utils";
|
|
||||||
|
|
||||||
let random = new Random(Date.now());
|
let random = new Random(Date.now());
|
||||||
let testIdBase = 0;
|
let testIdBase = 0;
|
||||||
@@ -12,4 +11,5 @@ export const reseed = (seed: number) => {
|
|||||||
testIdBase = 0;
|
testIdBase = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const randomId = () => (isTestEnv() ? `id${testIdBase++}` : nanoid());
|
export const randomId = () =>
|
||||||
|
process.env.NODE_ENV === "test" ? `id${testIdBase++}` : nanoid();
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
isLinearElement,
|
isLinearElement,
|
||||||
isFreeDrawElement,
|
isFreeDrawElement,
|
||||||
isInitializedImageElement,
|
isInitializedImageElement,
|
||||||
|
isImageElement,
|
||||||
} from "../element/typeChecks";
|
} from "../element/typeChecks";
|
||||||
import {
|
import {
|
||||||
getDiamondPoints,
|
getDiamondPoints,
|
||||||
@@ -221,19 +222,31 @@ const drawElementOnCanvas = (
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "image": {
|
case "image": {
|
||||||
const img = isInitializedImageElement(element)
|
if (renderConfig.editingImageElement) {
|
||||||
? renderConfig.imageCache.get(element.fileId)?.image
|
const { imageData } = renderConfig.editingImageElement;
|
||||||
: undefined;
|
|
||||||
if (img != null && !(img instanceof Promise)) {
|
const imgCanvas = document.createElement("canvas");
|
||||||
context.drawImage(
|
imgCanvas.width = imageData.width;
|
||||||
img,
|
imgCanvas.height = imageData.height;
|
||||||
0 /* hardcoded for the selection box*/,
|
const imgContext = imgCanvas.getContext("2d")!;
|
||||||
0,
|
imgContext.putImageData(imageData, 0, 0);
|
||||||
element.width,
|
|
||||||
element.height,
|
context.drawImage(imgCanvas, 0, 0, element.width, element.height);
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
drawImagePlaceholder(element, context, renderConfig.zoom.value);
|
const img = isInitializedImageElement(element)
|
||||||
|
? renderConfig.imageCache.get(element.fileId)?.image
|
||||||
|
: undefined;
|
||||||
|
if (img != null && !(img instanceof Promise)) {
|
||||||
|
context.drawImage(
|
||||||
|
img,
|
||||||
|
0 /* hardcoded for the selection box*/,
|
||||||
|
0,
|
||||||
|
element.width,
|
||||||
|
element.height,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
drawImagePlaceholder(element, context, renderConfig.zoom.value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -410,23 +423,23 @@ const generateElementShape = (
|
|||||||
topY + (rightY - topY) * 0.25
|
topY + (rightY - topY) * 0.25
|
||||||
} L ${rightX - (rightX - topX) * 0.25} ${
|
} L ${rightX - (rightX - topX) * 0.25} ${
|
||||||
rightY - (rightY - topY) * 0.25
|
rightY - (rightY - topY) * 0.25
|
||||||
}
|
}
|
||||||
C ${rightX} ${rightY}, ${rightX} ${rightY}, ${
|
C ${rightX} ${rightY}, ${rightX} ${rightY}, ${
|
||||||
rightX - (rightX - bottomX) * 0.25
|
rightX - (rightX - bottomX) * 0.25
|
||||||
} ${rightY + (bottomY - rightY) * 0.25}
|
} ${rightY + (bottomY - rightY) * 0.25}
|
||||||
L ${bottomX + (rightX - bottomX) * 0.25} ${
|
L ${bottomX + (rightX - bottomX) * 0.25} ${
|
||||||
bottomY - (bottomY - rightY) * 0.25
|
bottomY - (bottomY - rightY) * 0.25
|
||||||
}
|
}
|
||||||
C ${bottomX} ${bottomY}, ${bottomX} ${bottomY}, ${
|
C ${bottomX} ${bottomY}, ${bottomX} ${bottomY}, ${
|
||||||
bottomX - (bottomX - leftX) * 0.25
|
bottomX - (bottomX - leftX) * 0.25
|
||||||
} ${bottomY - (bottomY - leftY) * 0.25}
|
} ${bottomY - (bottomY - leftY) * 0.25}
|
||||||
L ${leftX + (bottomX - leftX) * 0.25} ${
|
L ${leftX + (bottomX - leftX) * 0.25} ${
|
||||||
leftY + (bottomY - leftY) * 0.25
|
leftY + (bottomY - leftY) * 0.25
|
||||||
}
|
}
|
||||||
C ${leftX} ${leftY}, ${leftX} ${leftY}, ${
|
C ${leftX} ${leftY}, ${leftX} ${leftY}, ${
|
||||||
leftX + (topX - leftX) * 0.25
|
leftX + (topX - leftX) * 0.25
|
||||||
} ${leftY - (leftY - topY) * 0.25}
|
} ${leftY - (leftY - topY) * 0.25}
|
||||||
L ${topX - (topX - leftX) * 0.25} ${topY + (leftY - topY) * 0.25}
|
L ${topX - (topX - leftX) * 0.25} ${topY + (leftY - topY) * 0.25}
|
||||||
C ${topX} ${topY}, ${topX} ${topY}, ${
|
C ${topX} ${topY}, ${topX} ${topY}, ${
|
||||||
topX + (rightX - topX) * 0.25
|
topX + (rightX - topX) * 0.25
|
||||||
} ${topY + (rightY - topY) * 0.25}`,
|
} ${topY + (rightY - topY) * 0.25}`,
|
||||||
@@ -608,7 +621,10 @@ const generateElementWithCanvas = (
|
|||||||
if (
|
if (
|
||||||
!prevElementWithCanvas ||
|
!prevElementWithCanvas ||
|
||||||
shouldRegenerateBecauseZoom ||
|
shouldRegenerateBecauseZoom ||
|
||||||
prevElementWithCanvas.theme !== renderConfig.theme
|
prevElementWithCanvas.theme !== renderConfig.theme ||
|
||||||
|
(renderConfig.editingImageElement &&
|
||||||
|
isImageElement(element) &&
|
||||||
|
element.id === renderConfig.editingImageElement.elementId)
|
||||||
) {
|
) {
|
||||||
const elementWithCanvas = generateElementCanvas(
|
const elementWithCanvas = generateElementCanvas(
|
||||||
element,
|
element,
|
||||||
|
|||||||
+2
-4
@@ -4,15 +4,13 @@ import {
|
|||||||
NonDeleted,
|
NonDeleted,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import { getNonDeletedElements, isNonDeletedElement } from "../element";
|
import { getNonDeletedElements, isNonDeletedElement } from "../element";
|
||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
|
||||||
|
|
||||||
type ElementIdKey = InstanceType<typeof LinearElementEditor>["elementId"];
|
type ElementKey = ExcalidrawElement | ExcalidrawElement["id"];
|
||||||
type ElementKey = ExcalidrawElement | ElementIdKey;
|
|
||||||
|
|
||||||
type SceneStateCallback = () => void;
|
type SceneStateCallback = () => void;
|
||||||
type SceneStateCallbackRemover = () => void;
|
type SceneStateCallbackRemover = () => void;
|
||||||
|
|
||||||
const isIdKey = (elementKey: ElementKey): elementKey is ElementIdKey => {
|
const isIdKey = (elementKey: ElementKey): elementKey is string => {
|
||||||
if (typeof elementKey === "string") {
|
if (typeof elementKey === "string") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ export const getElementContainingPosition = (
|
|||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
x: number,
|
x: number,
|
||||||
y: number,
|
y: number,
|
||||||
|
excludedType?: ExcalidrawElement["type"],
|
||||||
) => {
|
) => {
|
||||||
let hitElement = null;
|
let hitElement = null;
|
||||||
// We need to to hit testing from front (end of the array) to back (beginning of the array)
|
// We need to to hit testing from front (end of the array) to back (beginning of the array)
|
||||||
@@ -83,7 +84,13 @@ export const getElementContainingPosition = (
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(elements[index]);
|
const [x1, y1, x2, y2] = getElementAbsoluteCoords(elements[index]);
|
||||||
if (x1 < x && x < x2 && y1 < y && y < y2) {
|
if (
|
||||||
|
x1 < x &&
|
||||||
|
x < x2 &&
|
||||||
|
y1 < y &&
|
||||||
|
y < y2 &&
|
||||||
|
elements[index].type !== excludedType
|
||||||
|
) {
|
||||||
hitElement = elements[index];
|
hitElement = elements[index];
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ export const exportToCanvas = async (
|
|||||||
renderSelection: false,
|
renderSelection: false,
|
||||||
renderGrid: false,
|
renderGrid: false,
|
||||||
isExporting: true,
|
isExporting: true,
|
||||||
|
editingImageElement: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
return canvas;
|
return canvas;
|
||||||
|
|||||||
@@ -77,4 +77,4 @@ export const getTargetElements = (
|
|||||||
) =>
|
) =>
|
||||||
appState.editingElement
|
appState.editingElement
|
||||||
? [appState.editingElement]
|
? [appState.editingElement]
|
||||||
: getSelectedElements(elements, appState, true);
|
: getSelectedElements(elements, appState);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export type RenderConfig = {
|
|||||||
zoom: AppState["zoom"];
|
zoom: AppState["zoom"];
|
||||||
shouldCacheIgnoreZoom: AppState["shouldCacheIgnoreZoom"];
|
shouldCacheIgnoreZoom: AppState["shouldCacheIgnoreZoom"];
|
||||||
theme: AppState["theme"];
|
theme: AppState["theme"];
|
||||||
|
editingImageElement: AppState["editingImageElement"];
|
||||||
// collab-related state
|
// collab-related state
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
remotePointerViewportCoords: { [id: string]: { x: number; y: number } };
|
remotePointerViewportCoords: { [id: string]: { x: number; y: number } };
|
||||||
|
|||||||
+1
-1
@@ -80,7 +80,7 @@ export const SHAPES = [
|
|||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
value: "freedraw",
|
value: "freedraw",
|
||||||
key: [KEYS.X, KEYS.P.toUpperCase()],
|
key: KEYS.X,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: (
|
icon: (
|
||||||
|
|||||||
+5
-1
@@ -29,6 +29,8 @@ 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 { ALLOWED_IMAGE_MIME_TYPES, MIME_TYPES } from "./constants";
|
||||||
|
import { EditingImageElement } from "./element/imageEditor";
|
||||||
|
import Scene from "./scene/Scene";
|
||||||
|
|
||||||
export type Point = Readonly<RoughPoint>;
|
export type Point = Readonly<RoughPoint>;
|
||||||
|
|
||||||
@@ -77,6 +79,7 @@ export type AppState = {
|
|||||||
// (e.g. text element when typing into the input)
|
// (e.g. text element when typing into the input)
|
||||||
editingElement: NonDeletedExcalidrawElement | null;
|
editingElement: NonDeletedExcalidrawElement | null;
|
||||||
editingLinearElement: LinearElementEditor | null;
|
editingLinearElement: LinearElementEditor | null;
|
||||||
|
editingImageElement: EditingImageElement | null;
|
||||||
elementType: typeof SHAPES[number]["value"];
|
elementType: typeof SHAPES[number]["value"];
|
||||||
elementLocked: boolean;
|
elementLocked: boolean;
|
||||||
exportBackground: boolean;
|
exportBackground: boolean;
|
||||||
@@ -255,7 +258,6 @@ export type SceneData = {
|
|||||||
appState?: ImportedDataState["appState"];
|
appState?: ImportedDataState["appState"];
|
||||||
collaborators?: Map<string, Collaborator>;
|
collaborators?: Map<string, Collaborator>;
|
||||||
commitToHistory?: boolean;
|
commitToHistory?: boolean;
|
||||||
libraryItems?: LibraryItems | LibraryItems_v1;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum UserIdleState {
|
export enum UserIdleState {
|
||||||
@@ -317,6 +319,8 @@ export type AppClassProperties = {
|
|||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
files: BinaryFiles;
|
files: BinaryFiles;
|
||||||
|
scene: Scene;
|
||||||
|
addFiles: App["addFiles"];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PointerDownState = Readonly<{
|
export type PointerDownState = Readonly<{
|
||||||
|
|||||||
+2
-4
@@ -443,7 +443,8 @@ export const bytesToHexString = (bytes: Uint8Array) => {
|
|||||||
.join("");
|
.join("");
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getUpdatedTimestamp = () => (isTestEnv() ? 1 : Date.now());
|
export const getUpdatedTimestamp = () =>
|
||||||
|
process.env.NODE_ENV === "test" ? 1 : Date.now();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transforms array of objects containing `id` attribute,
|
* Transforms array of objects containing `id` attribute,
|
||||||
@@ -457,6 +458,3 @@ export const arrayToMap = <T extends { id: string } | string>(
|
|||||||
return acc;
|
return acc;
|
||||||
}, new Map());
|
}, new Map());
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isTestEnv = () =>
|
|
||||||
typeof process !== "undefined" && process.env?.NODE_ENV === "test";
|
|
||||||
|
|||||||
@@ -2136,14 +2136,14 @@
|
|||||||
lz-string "^1.4.4"
|
lz-string "^1.4.4"
|
||||||
pretty-format "^27.0.2"
|
pretty-format "^27.0.2"
|
||||||
|
|
||||||
"@testing-library/jest-dom@5.16.1":
|
"@testing-library/jest-dom@5.15.1":
|
||||||
version "5.16.1"
|
version "5.15.1"
|
||||||
resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-5.16.1.tgz#3db7df5ae97596264a7da9696fe14695ba02e51f"
|
resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-5.15.1.tgz#4c49ba4d244f235aec53f0a83498daeb4ee06c33"
|
||||||
integrity sha512-ajUJdfDIuTCadB79ukO+0l8O+QwN0LiSxDaYUTI4LndbbUsGi6rWU1SCexXzBA2NSjlVB9/vbkasQIL3tmPBjw==
|
integrity sha512-kmj8opVDRE1E4GXyLlESsQthCXK7An28dFWxhiMwD7ZUI7ZxA6sjdJRxLerD9Jd8cHX4BDc1jzXaaZKqzlUkvg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.9.2"
|
"@babel/runtime" "^7.9.2"
|
||||||
"@types/testing-library__jest-dom" "^5.9.1"
|
"@types/testing-library__jest-dom" "^5.9.1"
|
||||||
aria-query "^5.0.0"
|
aria-query "^4.2.2"
|
||||||
chalk "^3.0.0"
|
chalk "^3.0.0"
|
||||||
css "^3.0.0"
|
css "^3.0.0"
|
||||||
css.escape "^1.5.1"
|
css.escape "^1.5.1"
|
||||||
@@ -2159,10 +2159,10 @@
|
|||||||
"@babel/runtime" "^7.12.5"
|
"@babel/runtime" "^7.12.5"
|
||||||
"@testing-library/dom" "^8.0.0"
|
"@testing-library/dom" "^8.0.0"
|
||||||
|
|
||||||
"@tldraw/vec@1.4.3":
|
"@tldraw/vec@1.1.5":
|
||||||
version "1.4.3"
|
version "1.1.5"
|
||||||
resolved "https://registry.yarnpkg.com/@tldraw/vec/-/vec-1.4.3.tgz#c1460da019063b8eb9c62d715c61dff44dd23b02"
|
resolved "https://registry.yarnpkg.com/@tldraw/vec/-/vec-1.1.5.tgz#a0ec75742c20da43e3328f824ef7fca0f982b1fc"
|
||||||
integrity sha512-p7Nu9OWqorQ+nWPlvkN6Zn1oDbRgr4sheLzRif/+xB7BzMQFJh1T8syX6Jj/S88GPvn5PwZgYUKMnMFvvL5t/w==
|
integrity sha512-reEos3gJ9OiNvAYFtJzHbYWNjPTvWPmpjAY70HWHMjfhyNk6lHwwzDjwSgej4/KxVBbxB3ppNfLD9mEMq9yCOQ==
|
||||||
|
|
||||||
"@tootallnate/once@1":
|
"@tootallnate/once@1":
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
@@ -2219,10 +2219,10 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/types" "^7.3.0"
|
"@babel/types" "^7.3.0"
|
||||||
|
|
||||||
"@types/chai@4.3.0":
|
"@types/chai@4.2.22":
|
||||||
version "4.3.0"
|
version "4.2.22"
|
||||||
resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.0.tgz#23509ebc1fa32f1b4d50d6a66c4032d5b8eaabdc"
|
resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.22.tgz#47020d7e4cf19194d43b5202f35f75bd2ad35ce7"
|
||||||
integrity sha512-/ceqdqeRraGolFTcfoXNiqjyQhZzbINDngeoAq9GoHa8PPK1yNzTaxWjA6BFWp5Ua9JpXEMSS4s5i9tS0hOJtw==
|
integrity sha512-tFfcE+DSTzWAgifkjik9AySNqIyNoYwmR+uecPwwD/XRNfvOjmC/FjCxpiUGDkDVDphPfCUecSQVFw+lN3M3kQ==
|
||||||
|
|
||||||
"@types/duplexify@^3.6.0":
|
"@types/duplexify@^3.6.0":
|
||||||
version "3.6.0"
|
version "3.6.0"
|
||||||
@@ -2283,10 +2283,10 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/istanbul-lib-report" "*"
|
"@types/istanbul-lib-report" "*"
|
||||||
|
|
||||||
"@types/jest@*", "@types/jest@27.4.0":
|
"@types/jest@*", "@types/jest@27.0.3":
|
||||||
version "27.4.0"
|
version "27.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-27.4.0.tgz#037ab8b872067cae842a320841693080f9cb84ed"
|
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-27.0.3.tgz#0cf9dfe9009e467f70a342f0f94ead19842a783a"
|
||||||
integrity sha512-gHl8XuC1RZ8H2j5sHv/JqsaxXkDDM9iDOgu0Wp8sjs4u/snb2PVehyWXJPr+ORA0RPpgw231mnutWI1+0hgjIQ==
|
integrity sha512-cmmwv9t7gBYt7hNKH5Spu7Kuu/DotGa+Ff+JGRKZ4db5eh8PnKS4LuebJ3YLUoyOyIHraTGyULn23YtEAm0VSg==
|
||||||
dependencies:
|
dependencies:
|
||||||
jest-diff "^27.0.0"
|
jest-diff "^27.0.0"
|
||||||
pretty-format "^27.0.0"
|
pretty-format "^27.0.0"
|
||||||
@@ -2338,10 +2338,10 @@
|
|||||||
resolved "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz"
|
resolved "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz"
|
||||||
integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==
|
integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==
|
||||||
|
|
||||||
"@types/pako@1.0.3":
|
"@types/pako@1.0.2":
|
||||||
version "1.0.3"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/@types/pako/-/pako-1.0.3.tgz#2e61c2b02020b5f44e2e5e946dfac74f4ec33c58"
|
resolved "https://registry.yarnpkg.com/@types/pako/-/pako-1.0.2.tgz#17c9b136877f33d9ecc8e73cd26944f1f6dd39a1"
|
||||||
integrity sha512-EDxOsHAD5dqjbjEUM1xwa7rpKPFb8ECBE5irONTQU7/OsO3thI5YrNEWSPNMvYmvFM0l/OLQJ6Mgw7PEdXSjhg==
|
integrity sha512-8UJl2MjkqqS6ncpLZqRZ5LmGiFBkbYxocD4e4jmBqGvfRG1RS23gKsBQbdtV9O9GvRyjFTiRHRByjSlKCLlmZw==
|
||||||
|
|
||||||
"@types/parse-json@^4.0.0":
|
"@types/parse-json@^4.0.0":
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
@@ -2375,10 +2375,10 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/react" "*"
|
"@types/react" "*"
|
||||||
|
|
||||||
"@types/react@*", "@types/react@17.0.38":
|
"@types/react@*", "@types/react@17.0.37":
|
||||||
version "17.0.38"
|
version "17.0.37"
|
||||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.38.tgz#f24249fefd89357d5fa71f739a686b8d7c7202bd"
|
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.37.tgz#6884d0aa402605935c397ae689deed115caad959"
|
||||||
integrity sha512-SI92X1IA+FMnP3qM5m4QReluXzhcmovhZnLNm3pyeQlooi02qI7sLiepEYqT678uNiyc25XfCqxREFpy3W7YhQ==
|
integrity sha512-2FS1oTqBGcH/s0E+CjrCCR9+JMpsu9b69RTFO+40ua43ZqP5MmQ4iUde/dMjWR909KxZwmOQIFq6AV6NjEG5xg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/prop-types" "*"
|
"@types/prop-types" "*"
|
||||||
"@types/scheduler" "*"
|
"@types/scheduler" "*"
|
||||||
@@ -4379,6 +4379,11 @@ clone@^1.0.2:
|
|||||||
resolved "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz"
|
resolved "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz"
|
||||||
integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4=
|
integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4=
|
||||||
|
|
||||||
|
clone@^2.1.2:
|
||||||
|
version "2.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f"
|
||||||
|
integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=
|
||||||
|
|
||||||
clsx@1.1.1:
|
clsx@1.1.1:
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz"
|
resolved "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz"
|
||||||
@@ -5158,10 +5163,10 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.9:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms "2.0.0"
|
ms "2.0.0"
|
||||||
|
|
||||||
debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3:
|
debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2:
|
||||||
version "4.3.3"
|
version "4.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664"
|
resolved "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz"
|
||||||
integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==
|
integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==
|
||||||
dependencies:
|
dependencies:
|
||||||
ms "2.1.2"
|
ms "2.1.2"
|
||||||
|
|
||||||
@@ -5718,7 +5723,7 @@ enhanced-resolve@^4.3.0:
|
|||||||
memory-fs "^0.5.0"
|
memory-fs "^0.5.0"
|
||||||
tapable "^1.0.0"
|
tapable "^1.0.0"
|
||||||
|
|
||||||
enquirer@^2.3.5:
|
enquirer@^2.3.5, enquirer@^2.3.6:
|
||||||
version "2.3.6"
|
version "2.3.6"
|
||||||
resolved "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz"
|
resolved "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz"
|
||||||
integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==
|
integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==
|
||||||
@@ -7750,11 +7755,6 @@ immer@8.0.1:
|
|||||||
resolved "https://registry.npmjs.org/immer/-/immer-8.0.1.tgz"
|
resolved "https://registry.npmjs.org/immer/-/immer-8.0.1.tgz"
|
||||||
integrity sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA==
|
integrity sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA==
|
||||||
|
|
||||||
immutable@^4.0.0:
|
|
||||||
version "4.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.0.0.tgz#b86f78de6adef3608395efb269a91462797e2c23"
|
|
||||||
integrity sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw==
|
|
||||||
|
|
||||||
import-cwd@^2.0.0:
|
import-cwd@^2.0.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz"
|
resolved "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz"
|
||||||
@@ -9276,23 +9276,24 @@ lines-and-columns@^1.1.6:
|
|||||||
resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz"
|
resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz"
|
||||||
integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=
|
integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=
|
||||||
|
|
||||||
lint-staged@12.1.7:
|
lint-staged@12.1.2:
|
||||||
version "12.1.7"
|
version "12.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-12.1.7.tgz#fe9137992ac18a456422bb8484dd30be0140629f"
|
resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-12.1.2.tgz#90c571927e1371fc133e720671dd7989eab53f74"
|
||||||
integrity sha512-bltv/ejiLWtowExpjU+s5z8j1Byjg9AlmaAjMmqNbIicY69u6sYIwXGg0dCn0TlkrrY2CphtHIXAkbZ+1VoWQQ==
|
integrity sha512-bSMcQVqMW98HLLLR2c2tZ+vnDCnx4fd+0QJBQgN/4XkdspGRPc8DGp7UuOEBe1ApCfJ+wXXumYnJmU+wDo7j9A==
|
||||||
dependencies:
|
dependencies:
|
||||||
cli-truncate "^3.1.0"
|
cli-truncate "^3.1.0"
|
||||||
colorette "^2.0.16"
|
colorette "^2.0.16"
|
||||||
commander "^8.3.0"
|
commander "^8.3.0"
|
||||||
debug "^4.3.3"
|
debug "^4.3.2"
|
||||||
|
enquirer "^2.3.6"
|
||||||
execa "^5.1.1"
|
execa "^5.1.1"
|
||||||
lilconfig "2.0.4"
|
lilconfig "2.0.4"
|
||||||
listr2 "^3.13.5"
|
listr2 "^3.13.3"
|
||||||
micromatch "^4.0.4"
|
micromatch "^4.0.4"
|
||||||
normalize-path "^3.0.0"
|
normalize-path "^3.0.0"
|
||||||
object-inspect "^1.11.1"
|
object-inspect "^1.11.0"
|
||||||
string-argv "^0.3.1"
|
string-argv "^0.3.1"
|
||||||
supports-color "^9.2.1"
|
supports-color "^9.0.2"
|
||||||
yaml "^1.10.2"
|
yaml "^1.10.2"
|
||||||
|
|
||||||
listenercount@~1.0.1:
|
listenercount@~1.0.1:
|
||||||
@@ -9300,16 +9301,16 @@ listenercount@~1.0.1:
|
|||||||
resolved "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz"
|
resolved "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz"
|
||||||
integrity sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc=
|
integrity sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc=
|
||||||
|
|
||||||
listr2@^3.13.5:
|
listr2@^3.13.3:
|
||||||
version "3.13.5"
|
version "3.13.3"
|
||||||
resolved "https://registry.yarnpkg.com/listr2/-/listr2-3.13.5.tgz#105a813f2eb2329c4aae27373a281d610ee4985f"
|
resolved "https://registry.yarnpkg.com/listr2/-/listr2-3.13.3.tgz#d8f6095c9371b382c9b1c2bc33c5941d8e177f11"
|
||||||
integrity sha512-3n8heFQDSk+NcwBn3CgxEibZGaRzx+pC64n3YjpMD1qguV4nWus3Al+Oo3KooqFKTQEJ1v7MmnbnyyNspgx3NA==
|
integrity sha512-VqAgN+XVfyaEjSaFewGPcDs5/3hBbWVaX1VgWv2f52MF7US45JuARlArULctiB44IIcEk3JF7GtoFCLqEdeuPA==
|
||||||
dependencies:
|
dependencies:
|
||||||
cli-truncate "^2.1.0"
|
cli-truncate "^2.1.0"
|
||||||
|
clone "^2.1.2"
|
||||||
colorette "^2.0.16"
|
colorette "^2.0.16"
|
||||||
log-update "^4.0.0"
|
log-update "^4.0.0"
|
||||||
p-map "^4.0.0"
|
p-map "^4.0.0"
|
||||||
rfdc "^1.3.0"
|
|
||||||
rxjs "^7.4.0"
|
rxjs "^7.4.0"
|
||||||
through "^2.3.8"
|
through "^2.3.8"
|
||||||
wrap-ansi "^7.0.0"
|
wrap-ansi "^7.0.0"
|
||||||
@@ -10403,10 +10404,10 @@ object-copy@^0.1.0:
|
|||||||
define-property "^0.2.5"
|
define-property "^0.2.5"
|
||||||
kind-of "^3.0.3"
|
kind-of "^3.0.3"
|
||||||
|
|
||||||
object-inspect@^1.11.1, object-inspect@^1.9.0:
|
object-inspect@^1.11.0, object-inspect@^1.9.0:
|
||||||
version "1.12.0"
|
version "1.11.0"
|
||||||
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.0.tgz#6e2c120e868fd1fd18cb4f18c31741d0d6e776f0"
|
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.11.0.tgz#9dceb146cedd4148a0d9e51ab88d34cf509922b1"
|
||||||
integrity sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==
|
integrity sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==
|
||||||
|
|
||||||
object-is@^1.0.1:
|
object-is@^1.0.1:
|
||||||
version "1.1.5"
|
version "1.1.5"
|
||||||
@@ -11787,10 +11788,10 @@ prettier-linter-helpers@^1.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
fast-diff "^1.1.2"
|
fast-diff "^1.1.2"
|
||||||
|
|
||||||
prettier@2.5.1:
|
prettier@2.5.0:
|
||||||
version "2.5.1"
|
version "2.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.5.1.tgz#fff75fa9d519c54cf0fce328c1017d94546bc56a"
|
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.5.0.tgz#a6370e2d4594e093270419d9cc47f7670488f893"
|
||||||
integrity sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==
|
integrity sha512-FM/zAKgWTxj40rH03VxzIPdXmj39SwSjwG0heUcNFwI+EMZJnY93yAiKXM3dObIKAM5TA88werc8T/EwhB45eg==
|
||||||
|
|
||||||
pretty-bytes@^5.3.0:
|
pretty-bytes@^5.3.0:
|
||||||
version "5.6.0"
|
version "5.6.0"
|
||||||
@@ -12718,11 +12719,6 @@ rework@1.0.1:
|
|||||||
convert-source-map "^0.3.3"
|
convert-source-map "^0.3.3"
|
||||||
css "^2.0.0"
|
css "^2.0.0"
|
||||||
|
|
||||||
rfdc@^1.3.0:
|
|
||||||
version "1.3.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b"
|
|
||||||
integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==
|
|
||||||
|
|
||||||
rgb-regex@^1.0.1:
|
rgb-regex@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.npmjs.org/rgb-regex/-/rgb-regex-1.0.1.tgz"
|
resolved "https://registry.npmjs.org/rgb-regex/-/rgb-regex-1.0.1.tgz"
|
||||||
@@ -12915,14 +12911,12 @@ sass-loader@^10.0.5:
|
|||||||
schema-utils "^3.0.0"
|
schema-utils "^3.0.0"
|
||||||
semver "^7.3.2"
|
semver "^7.3.2"
|
||||||
|
|
||||||
sass@1.47.0:
|
sass@1.43.5:
|
||||||
version "1.47.0"
|
version "1.43.5"
|
||||||
resolved "https://registry.yarnpkg.com/sass/-/sass-1.47.0.tgz#c22dd0eed2e4a991430dae0b03c8e694bc41c2b4"
|
resolved "https://registry.yarnpkg.com/sass/-/sass-1.43.5.tgz#25a9d91dd098793ef7229d7b04dd3daae2fc4a65"
|
||||||
integrity sha512-GtXwvwgD7/6MLUZPnlA5/8cdRgC9SzT5kAnnJMRmEZQFRE3J56Foswig4NyyyQGsnmNvg6EUM/FP0Pe9Y2zywQ==
|
integrity sha512-WuNm+eAryMgQluL7Mbq9M4EruyGGMyal7Lu58FfnRMVWxgUzIvI7aSn60iNt3kn5yZBMR7G84fAGDcwqOF5JOg==
|
||||||
dependencies:
|
dependencies:
|
||||||
chokidar ">=3.0.0 <4.0.0"
|
chokidar ">=3.0.0 <4.0.0"
|
||||||
immutable "^4.0.0"
|
|
||||||
source-map-js ">=0.6.2 <2.0.0"
|
|
||||||
|
|
||||||
sax@~1.2.4:
|
sax@~1.2.4:
|
||||||
version "1.2.4"
|
version "1.2.4"
|
||||||
@@ -13354,11 +13348,6 @@ source-list-map@^2.0.0:
|
|||||||
resolved "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz"
|
resolved "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz"
|
||||||
integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==
|
integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==
|
||||||
|
|
||||||
"source-map-js@>=0.6.2 <2.0.0":
|
|
||||||
version "1.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.1.tgz#a1741c131e3c77d048252adfa24e23b908670caf"
|
|
||||||
integrity sha512-4+TN2b3tqOCd/kaGRJ/sTYA0tR0mdXx26ipdolxcwtJVqEnqNYvlCAt1q3ypy4QMlYus+Zh34RNtYLoq2oQ4IA==
|
|
||||||
|
|
||||||
source-map-resolve@^0.5.0, source-map-resolve@^0.5.2:
|
source-map-resolve@^0.5.0, source-map-resolve@^0.5.2:
|
||||||
version "0.5.3"
|
version "0.5.3"
|
||||||
resolved "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz"
|
resolved "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz"
|
||||||
@@ -13875,10 +13864,10 @@ supports-color@^7.0.0, supports-color@^7.1.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
has-flag "^4.0.0"
|
has-flag "^4.0.0"
|
||||||
|
|
||||||
supports-color@^9.2.1:
|
supports-color@^9.0.2:
|
||||||
version "9.2.1"
|
version "9.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-9.2.1.tgz#599dc9d45acf74c6176e0d880bab1d7d718fe891"
|
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-9.1.0.tgz#558963681dafeff41ed68220488cbf438d29f351"
|
||||||
integrity sha512-Obv7ycoCTG51N7y175StI9BlAXrmgZrFhZOb0/PyjHBher/NmsdBgbbQ1Inhq+gIhz6+7Gb+jWF2Vqi7Mf1xnQ==
|
integrity sha512-lOCGOTmBSN54zKAoPWhHkjoqVQ0MqgzPE5iirtoSixhr0ZieR/6l7WZ32V53cvy9+1qghFnIk7k52p991lKd6g==
|
||||||
|
|
||||||
supports-hyperlinks@^1.0.1:
|
supports-hyperlinks@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
@@ -14392,10 +14381,10 @@ typedarray@^0.0.6:
|
|||||||
resolved "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz"
|
resolved "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz"
|
||||||
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
|
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
|
||||||
|
|
||||||
typescript@4.5.4:
|
typescript@4.5.2:
|
||||||
version "4.5.4"
|
version "4.5.2"
|
||||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.4.tgz#a17d3a0263bf5c8723b9c52f43c5084edf13c2e8"
|
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.2.tgz#8ac1fba9f52256fdb06fb89e4122fa6a346c2998"
|
||||||
integrity sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg==
|
integrity sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==
|
||||||
|
|
||||||
typeson-registry@^1.0.0-alpha.20:
|
typeson-registry@^1.0.0-alpha.20:
|
||||||
version "1.0.0-alpha.39"
|
version "1.0.0-alpha.39"
|
||||||
|
|||||||
Reference in New Issue
Block a user