Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6d5813e9d2 | |||
| 1ef287027b | |||
| a51ed9ced6 | |||
| 4501d6d630 | |||
| 92a5936c7f | |||
| 50bd5fbae1 | |||
| 62bead66d7 | |||
| b3073984b3 | |||
| 3c9ee13979 | |||
| 228c8136cf | |||
| 324dd460c8 | |||
| d8ea085a94 | |||
| adbd486f32 | |||
| 0a89c4b0c8 | |||
| c03845bac3 | |||
| d5a6014076 | |||
| 74861b1398 | |||
| ac71ee7278 | |||
| 9088df8f5a | |||
| c5fe0cd446 | |||
| 9f8783c2dd | |||
| b475412199 | |||
| 5f1616f2c5 | |||
| cec92c1d17 | |||
| 5f476e09d4 | |||
| 9aa6a27252 |
+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_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries
|
||||
|
||||
REACT_APP_SOCKET_SERVER_URL=http://localhost:3000
|
||||
REACT_APP_SOCKET_SERVER_URL=http://localhost:3002
|
||||
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
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 14.x
|
||||
node-version: 16.x
|
||||
- name: Install and test
|
||||
run: |
|
||||
yarn --frozen-lockfile
|
||||
|
||||
@@ -118,6 +118,10 @@ yarn start
|
||||
|
||||
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
|
||||
|
||||
| Command | Description |
|
||||
|
||||
+4
-4
@@ -23,8 +23,8 @@
|
||||
"@sentry/integrations": "6.2.5",
|
||||
"@testing-library/jest-dom": "5.16.1",
|
||||
"@testing-library/react": "12.1.2",
|
||||
"@tldraw/vec": "1.4.0",
|
||||
"@types/jest": "27.0.3",
|
||||
"@tldraw/vec": "1.4.3",
|
||||
"@types/jest": "27.4.0",
|
||||
"@types/pica": "5.1.3",
|
||||
"@types/react": "17.0.38",
|
||||
"@types/react-dom": "17.0.11",
|
||||
@@ -50,7 +50,7 @@
|
||||
"react-dom": "17.0.2",
|
||||
"react-scripts": "4.0.3",
|
||||
"roughjs": "4.5.2",
|
||||
"sass": "1.45.2",
|
||||
"sass": "1.47.0",
|
||||
"socket.io-client": "2.3.1",
|
||||
"typescript": "4.5.4"
|
||||
},
|
||||
@@ -68,7 +68,7 @@
|
||||
"firebase-tools": "9.23.0",
|
||||
"husky": "7.0.4",
|
||||
"jest-canvas-mock": "2.3.1",
|
||||
"lint-staged": "12.1.4",
|
||||
"lint-staged": "12.1.7",
|
||||
"pepjs": "0.5.3",
|
||||
"prettier": "2.5.1",
|
||||
"rewire": "5.0.0"
|
||||
|
||||
@@ -41,8 +41,16 @@ import {
|
||||
isTextElement,
|
||||
redrawTextBoundingBox,
|
||||
} from "../element";
|
||||
import { newElementWith } from "../element/mutateElement";
|
||||
import { isLinearElement, isLinearElementType } from "../element/typeChecks";
|
||||
import { mutateElement, newElementWith } from "../element/mutateElement";
|
||||
import {
|
||||
getBoundTextElement,
|
||||
getContainerElement,
|
||||
} from "../element/textElement";
|
||||
import {
|
||||
isBoundToContainer,
|
||||
isLinearElement,
|
||||
isLinearElementType,
|
||||
} from "../element/typeChecks";
|
||||
import {
|
||||
Arrowhead,
|
||||
ExcalidrawElement,
|
||||
@@ -52,6 +60,7 @@ import {
|
||||
TextAlign,
|
||||
} from "../element/types";
|
||||
import { getLanguage, t } from "../i18n";
|
||||
import { KEYS } from "../keys";
|
||||
import { randomInteger } from "../random";
|
||||
import {
|
||||
canChangeSharpness,
|
||||
@@ -62,10 +71,11 @@ import {
|
||||
isSomeElementSelected,
|
||||
} from "../scene";
|
||||
import { hasStrokeColor } from "../scene/comparisons";
|
||||
import Scene from "../scene/Scene";
|
||||
import { arrayToMap } from "../utils";
|
||||
import { register } from "./register";
|
||||
|
||||
const FONT_SIZE_RELATIVE_INCREASE_STEP = 0.1;
|
||||
|
||||
const changeProperty = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
appState: AppState,
|
||||
@@ -107,18 +117,96 @@ 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({
|
||||
name: "changeStrokeColor",
|
||||
perform: (elements, appState, value) => {
|
||||
return {
|
||||
...(value.currentItemStrokeColor && {
|
||||
elements: changeProperty(elements, appState, (el) => {
|
||||
return hasStrokeColor(el.type)
|
||||
? newElementWith(el, {
|
||||
strokeColor: value.currentItemStrokeColor,
|
||||
})
|
||||
: el;
|
||||
}),
|
||||
elements: changeProperty(
|
||||
elements,
|
||||
appState,
|
||||
(el) => {
|
||||
return hasStrokeColor(el.type)
|
||||
? newElementWith(el, {
|
||||
strokeColor: value.currentItemStrokeColor,
|
||||
})
|
||||
: el;
|
||||
},
|
||||
true,
|
||||
),
|
||||
}),
|
||||
appState: {
|
||||
...appState,
|
||||
@@ -432,33 +520,7 @@ export const actionChangeOpacity = register({
|
||||
export const actionChangeFontSize = register({
|
||||
name: "changeFontSize",
|
||||
perform: (elements, appState, value) => {
|
||||
return {
|
||||
elements: changeProperty(
|
||||
elements,
|
||||
appState,
|
||||
(el) => {
|
||||
if (isTextElement(el)) {
|
||||
const element: ExcalidrawTextElement = newElementWith(el, {
|
||||
fontSize: value,
|
||||
});
|
||||
let container = null;
|
||||
if (el.containerId) {
|
||||
container = Scene.getScene(el)!.getElement(el.containerId);
|
||||
}
|
||||
redrawTextBoundingBox(element, container, appState);
|
||||
return element;
|
||||
}
|
||||
|
||||
return el;
|
||||
},
|
||||
true,
|
||||
),
|
||||
appState: {
|
||||
...appState,
|
||||
currentItemFontSize: value,
|
||||
},
|
||||
commitToHistory: true,
|
||||
};
|
||||
return changeFontSize(elements, appState, () => value);
|
||||
},
|
||||
PanelComponent: ({ elements, appState, updateData }) => (
|
||||
<fieldset>
|
||||
@@ -490,7 +552,16 @@ export const actionChangeFontSize = register({
|
||||
value={getFormValue(
|
||||
elements,
|
||||
appState,
|
||||
(element) => isTextElement(element) && element.fontSize,
|
||||
(element) => {
|
||||
if (isTextElement(element)) {
|
||||
return element.fontSize;
|
||||
}
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
if (boundTextElement) {
|
||||
return boundTextElement.fontSize;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
appState.currentItemFontSize || DEFAULT_FONT_SIZE,
|
||||
)}
|
||||
onChange={(value) => updateData(value)}
|
||||
@@ -499,6 +570,44 @@ 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({
|
||||
name: "changeFontFamily",
|
||||
perform: (elements, appState, value) => {
|
||||
@@ -506,20 +615,23 @@ export const actionChangeFontFamily = register({
|
||||
elements: changeProperty(
|
||||
elements,
|
||||
appState,
|
||||
(el) => {
|
||||
if (isTextElement(el)) {
|
||||
const element: ExcalidrawTextElement = newElementWith(el, {
|
||||
fontFamily: value,
|
||||
});
|
||||
let container = null;
|
||||
if (el.containerId) {
|
||||
container = Scene.getScene(el)!.getElement(el.containerId);
|
||||
}
|
||||
redrawTextBoundingBox(element, container, appState);
|
||||
return element;
|
||||
(oldElement) => {
|
||||
if (isTextElement(oldElement)) {
|
||||
const newElement: ExcalidrawTextElement = newElementWith(
|
||||
oldElement,
|
||||
{
|
||||
fontFamily: value,
|
||||
},
|
||||
);
|
||||
redrawTextBoundingBox(
|
||||
newElement,
|
||||
getContainerElement(oldElement),
|
||||
appState,
|
||||
);
|
||||
return newElement;
|
||||
}
|
||||
|
||||
return el;
|
||||
return oldElement;
|
||||
},
|
||||
true,
|
||||
),
|
||||
@@ -562,7 +674,16 @@ export const actionChangeFontFamily = register({
|
||||
value={getFormValue(
|
||||
elements,
|
||||
appState,
|
||||
(element) => isTextElement(element) && element.fontFamily,
|
||||
(element) => {
|
||||
if (isTextElement(element)) {
|
||||
return element.fontFamily;
|
||||
}
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
if (boundTextElement) {
|
||||
return boundTextElement.fontFamily;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
appState.currentItemFontFamily || DEFAULT_FONT_FAMILY,
|
||||
)}
|
||||
onChange={(value) => updateData(value)}
|
||||
@@ -579,20 +700,23 @@ export const actionChangeTextAlign = register({
|
||||
elements: changeProperty(
|
||||
elements,
|
||||
appState,
|
||||
(el) => {
|
||||
if (isTextElement(el)) {
|
||||
const element: ExcalidrawTextElement = newElementWith(el, {
|
||||
textAlign: value,
|
||||
});
|
||||
let container = null;
|
||||
if (el.containerId) {
|
||||
container = Scene.getScene(el)!.getElement(el.containerId);
|
||||
}
|
||||
redrawTextBoundingBox(element, container, appState);
|
||||
return element;
|
||||
(oldElement) => {
|
||||
if (isTextElement(oldElement)) {
|
||||
const newElement: ExcalidrawTextElement = newElementWith(
|
||||
oldElement,
|
||||
{
|
||||
textAlign: value,
|
||||
},
|
||||
);
|
||||
redrawTextBoundingBox(
|
||||
newElement,
|
||||
getContainerElement(oldElement),
|
||||
appState,
|
||||
);
|
||||
return newElement;
|
||||
}
|
||||
|
||||
return el;
|
||||
return oldElement;
|
||||
},
|
||||
true,
|
||||
),
|
||||
@@ -628,7 +752,16 @@ export const actionChangeTextAlign = register({
|
||||
value={getFormValue(
|
||||
elements,
|
||||
appState,
|
||||
(element) => isTextElement(element) && element.textAlign,
|
||||
(element) => {
|
||||
if (isTextElement(element)) {
|
||||
return element.textAlign;
|
||||
}
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
if (boundTextElement) {
|
||||
return boundTextElement.textAlign;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
appState.currentItemTextAlign,
|
||||
)}
|
||||
onChange={(value) => updateData(value)}
|
||||
|
||||
@@ -12,9 +12,7 @@ import {
|
||||
DEFAULT_FONT_FAMILY,
|
||||
DEFAULT_TEXT_ALIGN,
|
||||
} from "../constants";
|
||||
import Scene from "../scene/Scene";
|
||||
import { isBoundToContainer } from "../element/typeChecks";
|
||||
import { ExcalidrawTextElement } from "../element/types";
|
||||
import { getContainerElement } from "../element/textElement";
|
||||
|
||||
// `copiedStyles` is exported only for tests.
|
||||
export let copiedStyles: string = "{}";
|
||||
@@ -58,22 +56,16 @@ export const actionPasteStyles = register({
|
||||
opacity: pastedElement?.opacity,
|
||||
roughness: pastedElement?.roughness,
|
||||
});
|
||||
if (isTextElement(newElement)) {
|
||||
if (isTextElement(newElement) && isTextElement(element)) {
|
||||
mutateElement(newElement, {
|
||||
fontSize: pastedElement?.fontSize || DEFAULT_FONT_SIZE,
|
||||
fontFamily: pastedElement?.fontFamily || DEFAULT_FONT_FAMILY,
|
||||
textAlign: pastedElement?.textAlign || DEFAULT_TEXT_ALIGN,
|
||||
});
|
||||
let container = null;
|
||||
|
||||
if (isBoundToContainer(element)) {
|
||||
container = Scene.getScene(element)!.getElement(
|
||||
element.containerId,
|
||||
);
|
||||
}
|
||||
redrawTextBoundingBox(
|
||||
element as ExcalidrawTextElement,
|
||||
container,
|
||||
element,
|
||||
getContainerElement(element),
|
||||
appState,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -101,7 +101,9 @@ export type ActionName =
|
||||
| "flipVertical"
|
||||
| "viewMode"
|
||||
| "exportWithDarkMode"
|
||||
| "toggleTheme";
|
||||
| "toggleTheme"
|
||||
| "increaseFontSize"
|
||||
| "decreaseFontSize";
|
||||
|
||||
export type PanelComponentProps = {
|
||||
elements: readonly ExcalidrawElement[];
|
||||
|
||||
+30
-11
@@ -72,7 +72,12 @@ import {
|
||||
import { loadFromBlob } from "../data";
|
||||
import { isValidLibrary } from "../data/json";
|
||||
import Library from "../data/library";
|
||||
import { restore, restoreElements, restoreLibraryItems } from "../data/restore";
|
||||
import {
|
||||
restore,
|
||||
restoreAppState,
|
||||
restoreElements,
|
||||
restoreLibraryItems,
|
||||
} from "../data/restore";
|
||||
import {
|
||||
dragNewElement,
|
||||
dragSelectedElements,
|
||||
@@ -1306,7 +1311,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}
|
||||
}
|
||||
|
||||
if (isSupportedImageFile(file)) {
|
||||
// prefer spreadsheet data over image file (MS Office/Libre Office)
|
||||
if (isSupportedImageFile(file) && !data.spreadsheet) {
|
||||
const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords(
|
||||
{ clientX: cursorX, clientY: cursorY },
|
||||
this.state,
|
||||
@@ -1582,22 +1588,31 @@ class App extends React.Component<AppProps, AppState> {
|
||||
appState?: Pick<AppState, K> | null;
|
||||
collaborators?: SceneData["collaborators"];
|
||||
commitToHistory?: SceneData["commitToHistory"];
|
||||
libraryItems?: SceneData["libraryItems"];
|
||||
}) => {
|
||||
if (sceneData.commitToHistory) {
|
||||
this.history.resumeRecording();
|
||||
}
|
||||
|
||||
if (sceneData.appState) {
|
||||
this.setState(sceneData.appState);
|
||||
this.setState(restoreAppState(sceneData.appState, null));
|
||||
}
|
||||
|
||||
if (sceneData.elements) {
|
||||
this.scene.replaceAllElements(sceneData.elements);
|
||||
this.scene.replaceAllElements(
|
||||
restoreElements(sceneData.elements, null),
|
||||
);
|
||||
}
|
||||
|
||||
if (sceneData.collaborators) {
|
||||
this.setState({ collaborators: sceneData.collaborators });
|
||||
}
|
||||
|
||||
if (sceneData.libraryItems) {
|
||||
this.library.saveLibrary(
|
||||
restoreLibraryItems(sceneData.libraryItems, "unpublished"),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1641,7 +1656,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}
|
||||
|
||||
if (
|
||||
(isWritableElement(event.target) && event.key !== KEYS.ESCAPE) ||
|
||||
(isWritableElement(event.target) &&
|
||||
event.key !== KEYS.ESCAPE &&
|
||||
// handle cmd/ctrl-modifier shortcuts even inside inputs
|
||||
!event[KEYS.CTRL_OR_CMD]) ||
|
||||
// case: using arrows to move between buttons
|
||||
(isArrowKey(event.key) && isInputLike(event.target))
|
||||
) {
|
||||
@@ -1903,8 +1921,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||
const updateElement = (
|
||||
text: string,
|
||||
originalText: string,
|
||||
isDeleted = false,
|
||||
updateDimensions = false,
|
||||
isDeleted: boolean,
|
||||
isSubmit: boolean,
|
||||
) => {
|
||||
this.scene.replaceAllElements([
|
||||
...this.scene.getElementsIncludingDeleted().map((_element) => {
|
||||
@@ -1916,7 +1934,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
isDeleted,
|
||||
originalText,
|
||||
},
|
||||
updateDimensions,
|
||||
isSubmit,
|
||||
);
|
||||
}
|
||||
return _element;
|
||||
@@ -1942,7 +1960,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
];
|
||||
},
|
||||
onChange: withBatchedUpdates((text) => {
|
||||
updateElement(text, text, false, !element.containerId);
|
||||
updateElement(text, text, false, false);
|
||||
if (isNonDeletedElement(element)) {
|
||||
updateBoundElements(element);
|
||||
}
|
||||
@@ -1988,7 +2006,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
|
||||
// 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)
|
||||
updateElement(element.text, element.originalText);
|
||||
updateElement(element.text, element.originalText, false, false);
|
||||
}
|
||||
|
||||
private deselectElements() {
|
||||
@@ -2727,7 +2745,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||
(event.button === POINTER_BUTTON.WHEEL ||
|
||||
(event.button === POINTER_BUTTON.MAIN && isHoldingSpace) ||
|
||||
this.state.viewModeEnabled)
|
||||
)
|
||||
) ||
|
||||
isTextElement(this.state.editingElement)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
||||
<Shortcut label={t("toolBar.line")} shortcuts={["P", "6"]} />
|
||||
<Shortcut
|
||||
label={t("toolBar.freedraw")}
|
||||
shortcuts={["Shift+P", "7"]}
|
||||
shortcuts={["Shift + P", "X", "7"]}
|
||||
/>
|
||||
<Shortcut label={t("toolBar.text")} shortcuts={["T", "8"]} />
|
||||
<Shortcut label={t("toolBar.image")} shortcuts={["9"]} />
|
||||
@@ -394,6 +394,14 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
||||
label={t("labels.showBackground")}
|
||||
shortcuts={[getShortcutKey("G")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.decreaseFontSize")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+Shift+<")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.increaseFontSize")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+Shift+>")]}
|
||||
/>
|
||||
</ShortcutIsland>
|
||||
</Column>
|
||||
</Columns>
|
||||
|
||||
@@ -106,10 +106,6 @@ export const EXPORT_DATA_TYPES = {
|
||||
|
||||
export const EXPORT_SOURCE = window.location.origin;
|
||||
|
||||
export const STORAGE_KEYS = {
|
||||
LOCAL_STORAGE_LIBRARY: "excalidraw-library",
|
||||
} as const;
|
||||
|
||||
// time in milliseconds
|
||||
export const IMAGE_RENDER_TIMEOUT = 500;
|
||||
export const TAP_TWICE_TIMEOUT = 300;
|
||||
|
||||
+1
-1
@@ -136,7 +136,7 @@ const restoreElement = (
|
||||
textAlign: element.textAlign || DEFAULT_TEXT_ALIGN,
|
||||
verticalAlign: element.verticalAlign || DEFAULT_VERTICAL_ALIGN,
|
||||
containerId: element.containerId ?? null,
|
||||
originalText: element.originalText ?? "",
|
||||
originalText: element.originalText || element.text,
|
||||
});
|
||||
case "freedraw": {
|
||||
return restoreElementWithProperties(element, {
|
||||
|
||||
@@ -21,9 +21,8 @@ import { AppState } from "../types";
|
||||
import { getElementAbsoluteCoords } from ".";
|
||||
import { adjustXYWithRotation } from "../math";
|
||||
import { getResizedElementAbsoluteCoords } from "./bounds";
|
||||
import { measureText } from "./textElement";
|
||||
import { getContainerElement, measureText } from "./textElement";
|
||||
import { isBoundToContainer } from "./typeChecks";
|
||||
import Scene from "../scene/Scene";
|
||||
import { BOUND_TEXT_PADDING } from "../constants";
|
||||
|
||||
type ElementConstructorOpts = MarkOptional<
|
||||
@@ -158,7 +157,11 @@ const getAdjustedDimensions = (
|
||||
height: number;
|
||||
baseline: number;
|
||||
} => {
|
||||
const maxWidth = element.containerId ? element.width : null;
|
||||
let maxWidth = null;
|
||||
const container = getContainerElement(element);
|
||||
if (container) {
|
||||
maxWidth = container.width - BOUND_TEXT_PADDING * 2;
|
||||
}
|
||||
const {
|
||||
width: nextWidth,
|
||||
height: nextHeight,
|
||||
@@ -216,7 +219,7 @@ const getAdjustedDimensions = (
|
||||
// make sure container dimensions are set properly when
|
||||
// text editor overflows beyond viewport dimensions
|
||||
if (isBoundToContainer(element)) {
|
||||
const container = Scene.getScene(element)!.getElement(element.containerId)!;
|
||||
const container = getContainerElement(element)!;
|
||||
let height = container.height;
|
||||
let width = container.width;
|
||||
if (nextHeight > height - BOUND_TEXT_PADDING * 2) {
|
||||
@@ -246,11 +249,15 @@ export const updateTextElement = (
|
||||
originalText,
|
||||
}: { text: string; isDeleted?: boolean; originalText: string },
|
||||
|
||||
updateDimensions: boolean,
|
||||
isSubmit: boolean,
|
||||
): ExcalidrawTextElement => {
|
||||
const dimensions = updateDimensions
|
||||
? getAdjustedDimensions(element, text)
|
||||
: undefined;
|
||||
const boundToContainer = isBoundToContainer(element);
|
||||
|
||||
// Don't update dimensions and text value for bounded text unless submitted
|
||||
const dimensions =
|
||||
boundToContainer && !isSubmit
|
||||
? undefined
|
||||
: getAdjustedDimensions(element, text);
|
||||
return newElementWith(element, {
|
||||
text,
|
||||
originalText,
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
ExcalidrawBindableElement,
|
||||
ExcalidrawElement,
|
||||
ExcalidrawTextElement,
|
||||
ExcalidrawTextElementWithContainer,
|
||||
FontString,
|
||||
NonDeletedExcalidrawElement,
|
||||
} from "./types";
|
||||
@@ -408,3 +409,32 @@ export const getApproxCharsToFitInWidth = (font: FontString, width: number) => {
|
||||
export const getBoundTextElementId = (container: ExcalidrawElement | null) => {
|
||||
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;
|
||||
};
|
||||
|
||||
+381
-137
@@ -1,169 +1,413 @@
|
||||
import ReactDOM from "react-dom";
|
||||
import ExcalidrawApp from "../excalidraw-app";
|
||||
import { render } from "../tests/test-utils";
|
||||
import { Pointer, UI } from "../tests/helpers/ui";
|
||||
import { render, screen } from "../tests/test-utils";
|
||||
import { Keyboard, Pointer, UI } from "../tests/helpers/ui";
|
||||
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
|
||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||
|
||||
const tab = " ";
|
||||
const mouse = new Pointer("mouse");
|
||||
|
||||
describe("textWysiwyg", () => {
|
||||
let textarea: HTMLTextAreaElement;
|
||||
beforeEach(async () => {
|
||||
await render(<ExcalidrawApp />);
|
||||
describe("Test unbounded text", () => {
|
||||
let textarea: HTMLTextAreaElement;
|
||||
beforeEach(async () => {
|
||||
await render(<ExcalidrawApp />);
|
||||
|
||||
const element = UI.createElement("text");
|
||||
const element = UI.createElement("text");
|
||||
|
||||
new Pointer("mouse").clickOn(element);
|
||||
textarea = document.querySelector(
|
||||
".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,
|
||||
mouse.clickOn(element);
|
||||
textarea = document.querySelector(
|
||||
".excalidraw-textEditorContainer > textarea",
|
||||
)!;
|
||||
});
|
||||
textarea.value = `${tab}Line#1\nLine#2`;
|
||||
// cursor: "| Line#1\nLine#2"
|
||||
textarea.selectionStart = 0;
|
||||
textarea.selectionEnd = 0;
|
||||
|
||||
textarea.dispatchEvent(event);
|
||||
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(`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,
|
||||
expect(textarea.value).toEqual(`${tab}Line#1\nLine#2`);
|
||||
// cursor: " |Line#1\nLine#2"
|
||||
expect(textarea.selectionStart).toEqual(4);
|
||||
expect(textarea.selectionEnd).toEqual(4);
|
||||
});
|
||||
// cursor: "Line#1\n Lin|e#2"
|
||||
textarea.value = `Line#1\n${tab}Line#2`;
|
||||
textarea.selectionStart = 15;
|
||||
textarea.selectionEnd = 15;
|
||||
|
||||
textarea.dispatchEvent(event);
|
||||
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;
|
||||
|
||||
expect(textarea.value).toEqual(`Line#1\nLine#2`);
|
||||
// cursor: "Line#1\nLin|e#2"
|
||||
expect(textarea.selectionStart).toEqual(11);
|
||||
expect(textarea.selectionEnd).toEqual(11);
|
||||
});
|
||||
textarea.dispatchEvent(event);
|
||||
|
||||
it("should remove a tab at the start of the first and second line", () => {
|
||||
const event = new KeyboardEvent("keydown", {
|
||||
key: KEYS.TAB,
|
||||
shiftKey: true,
|
||||
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);
|
||||
});
|
||||
// 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);
|
||||
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;
|
||||
|
||||
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);
|
||||
});
|
||||
textarea.dispatchEvent(event);
|
||||
|
||||
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,
|
||||
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);
|
||||
});
|
||||
// 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 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;
|
||||
|
||||
it("should remove partial tabs", () => {
|
||||
const event = new KeyboardEvent("keydown", {
|
||||
key: KEYS.TAB,
|
||||
shiftKey: true,
|
||||
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);
|
||||
});
|
||||
// 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 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 remove nothing", () => {
|
||||
const event = new KeyboardEvent("keydown", {
|
||||
key: KEYS.TAB,
|
||||
shiftKey: true,
|
||||
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);
|
||||
});
|
||||
// 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`);
|
||||
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;
|
||||
const DUMMY_WIDTH = 160;
|
||||
const APPROX_LINE_HEIGHT = 25;
|
||||
const INITIAL_WIDTH = 10;
|
||||
|
||||
beforeAll(() => {
|
||||
jest
|
||||
.spyOn(textElementUtils, "getApproxLineHeight")
|
||||
.mockReturnValue(APPROX_LINE_HEIGHT);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await render(<ExcalidrawApp />);
|
||||
|
||||
rectangle = UI.createElement("rectangle", {
|
||||
x: 10,
|
||||
y: 20,
|
||||
width: 90,
|
||||
height: 75,
|
||||
});
|
||||
});
|
||||
|
||||
it("should bind text to container when double clicked on center", async () => {
|
||||
expect(h.elements.length).toBe(1);
|
||||
expect(h.elements[0].id).toBe(rectangle.id);
|
||||
|
||||
mouse.doubleClickAt(
|
||||
rectangle.x + rectangle.width / 2,
|
||||
rectangle.y + rectangle.height / 2,
|
||||
);
|
||||
expect(h.elements.length).toBe(2);
|
||||
|
||||
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||
expect(text.type).toBe("text");
|
||||
expect(text.containerId).toBe(rectangle.id);
|
||||
mouse.down();
|
||||
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" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should bind text to container when clicked on container and enter pressed", async () => {
|
||||
expect(h.elements.length).toBe(1);
|
||||
expect(h.elements[0].id).toBe(rectangle.id);
|
||||
|
||||
Keyboard.withModifierKeys({}, () => {
|
||||
Keyboard.keyPress(KEYS.ENTER);
|
||||
});
|
||||
|
||||
expect(h.elements.length).toBe(2);
|
||||
|
||||
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||
expect(text.type).toBe("text");
|
||||
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" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should update font family correctly on undo/redo by selecting bounded text when font family was updated", async () => {
|
||||
mouse.doubleClickAt(
|
||||
rectangle.x + rectangle.width / 2,
|
||||
rectangle.y + rectangle.height / 2,
|
||||
);
|
||||
mouse.down();
|
||||
|
||||
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||
let 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(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);
|
||||
});
|
||||
|
||||
it("should wrap text and vertcially center align once text submitted", async () => {
|
||||
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({}, () => {
|
||||
Keyboard.keyPress(KEYS.ENTER);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+36
-33
@@ -3,20 +3,18 @@ import {
|
||||
isWritableElement,
|
||||
getFontString,
|
||||
getFontFamilyString,
|
||||
isTestEnv,
|
||||
} from "../utils";
|
||||
import Scene from "../scene/Scene";
|
||||
import { isBoundToContainer, isTextElement } from "./typeChecks";
|
||||
import { CLASSES, BOUND_TEXT_PADDING } from "../constants";
|
||||
import {
|
||||
ExcalidrawBindableElement,
|
||||
ExcalidrawElement,
|
||||
ExcalidrawTextElement,
|
||||
} from "./types";
|
||||
import { ExcalidrawElement, ExcalidrawTextElement } from "./types";
|
||||
import { AppState } from "../types";
|
||||
import { mutateElement } from "./mutateElement";
|
||||
import {
|
||||
getApproxLineHeight,
|
||||
getBoundTextElementId,
|
||||
getContainerElement,
|
||||
wrapText,
|
||||
} from "./textElement";
|
||||
|
||||
@@ -72,7 +70,7 @@ export const textWysiwyg = ({
|
||||
originalText: string;
|
||||
}) => void;
|
||||
getViewportCoords: (x: number, y: number) => [number, number];
|
||||
element: ExcalidrawElement;
|
||||
element: ExcalidrawTextElement;
|
||||
canvas: HTMLCanvasElement | null;
|
||||
excalidrawContainer: HTMLDivElement | null;
|
||||
}) => {
|
||||
@@ -93,18 +91,15 @@ export const textWysiwyg = ({
|
||||
return false;
|
||||
};
|
||||
let originalContainerHeight: number;
|
||||
let approxLineHeight = isTextElement(element)
|
||||
? getApproxLineHeight(getFontString(element))
|
||||
: 0;
|
||||
let approxLineHeight = getApproxLineHeight(getFontString(element));
|
||||
|
||||
const initialText = element.originalText;
|
||||
const updateWysiwygStyle = () => {
|
||||
const updatedElement = Scene.getScene(element)?.getElement(id);
|
||||
if (updatedElement && isTextElement(updatedElement)) {
|
||||
let coordX = updatedElement.x;
|
||||
let coordY = updatedElement.y;
|
||||
const container = updatedElement?.containerId
|
||||
? Scene.getScene(updatedElement)!.getElement(updatedElement.containerId)
|
||||
: null;
|
||||
const container = getContainerElement(updatedElement);
|
||||
let maxWidth = updatedElement.width;
|
||||
|
||||
let maxHeight = updatedElement.height;
|
||||
@@ -123,9 +118,7 @@ export const textWysiwyg = ({
|
||||
height = editorHeight;
|
||||
}
|
||||
if (propertiesUpdated) {
|
||||
approxLineHeight = isTextElement(updatedElement)
|
||||
? getApproxLineHeight(getFontString(updatedElement))
|
||||
: 0;
|
||||
approxLineHeight = getApproxLineHeight(getFontString(updatedElement));
|
||||
|
||||
originalContainerHeight = container.height;
|
||||
|
||||
@@ -164,7 +157,7 @@ export const textWysiwyg = ({
|
||||
}
|
||||
const [viewportX, viewportY] = getViewportCoords(coordX, coordY);
|
||||
const { textAlign } = updatedElement;
|
||||
editable.value = updatedElement.originalText || updatedElement.text;
|
||||
editable.value = updatedElement.originalText;
|
||||
const lines = updatedElement.originalText.split("\n");
|
||||
const lineHeight = updatedElement.containerId
|
||||
? approxLineHeight
|
||||
@@ -217,6 +210,11 @@ export const textWysiwyg = ({
|
||||
maxWidth: `${maxWidth}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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -271,9 +269,7 @@ export const textWysiwyg = ({
|
||||
let height = "auto";
|
||||
|
||||
if (lines === 2) {
|
||||
const container = Scene.getScene(element)!.getElement(
|
||||
element.containerId,
|
||||
);
|
||||
const container = getContainerElement(element);
|
||||
const actualLineCount = wrapText(
|
||||
editable.value,
|
||||
getFontString(element),
|
||||
@@ -297,13 +293,16 @@ export const textWysiwyg = ({
|
||||
}
|
||||
|
||||
editable.onkeydown = (event) => {
|
||||
event.stopPropagation();
|
||||
if (!event[KEYS.CTRL_OR_CMD]) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
if (event.key === KEYS.ESCAPE) {
|
||||
event.preventDefault();
|
||||
submittedViaKeyboard = true;
|
||||
handleSubmit();
|
||||
} else if (event.key === KEYS.ENTER && event[KEYS.CTRL_OR_CMD]) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (event.isComposing || event.keyCode === 229) {
|
||||
return;
|
||||
}
|
||||
@@ -316,6 +315,7 @@ export const textWysiwyg = ({
|
||||
event.code === CODES.BRACKET_RIGHT))
|
||||
) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (event.shiftKey || event.code === CODES.BRACKET_LEFT) {
|
||||
outdent();
|
||||
} else {
|
||||
@@ -440,9 +440,7 @@ export const textWysiwyg = ({
|
||||
}
|
||||
let wrappedText = "";
|
||||
if (isTextElement(updateElement) && updateElement?.containerId) {
|
||||
const container = Scene.getScene(updateElement)!.getElement(
|
||||
updateElement.containerId,
|
||||
) as ExcalidrawBindableElement;
|
||||
const container = getContainerElement(updateElement);
|
||||
|
||||
if (container) {
|
||||
wrappedText = wrapText(
|
||||
@@ -450,18 +448,23 @@ export const textWysiwyg = ({
|
||||
getFontString(updateElement),
|
||||
container.width,
|
||||
);
|
||||
if (isTextElement(updateElement) && updateElement.containerId) {
|
||||
|
||||
if (updateElement.containerId) {
|
||||
const editorHeight = Number(editable.style.height.slice(0, -2));
|
||||
if (editable.value) {
|
||||
mutateElement(updateElement, {
|
||||
// vertically center align
|
||||
y: container.y + container.height / 2 - editorHeight / 2,
|
||||
height: editorHeight,
|
||||
width: Number(editable.style.width.slice(0, -2)),
|
||||
// preserve padding
|
||||
x: container.x + BOUND_TEXT_PADDING,
|
||||
angle: container.angle,
|
||||
});
|
||||
// Don't mutate if text is not updated
|
||||
if (initialText !== editable.value) {
|
||||
mutateElement(updateElement, {
|
||||
// vertically center align
|
||||
y: container.y + container.height / 2 - editorHeight / 2,
|
||||
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);
|
||||
if (!boundTextElementId || boundTextElementId !== element.id) {
|
||||
mutateElement(container, {
|
||||
|
||||
@@ -5,13 +5,13 @@ import {
|
||||
getDefaultAppState,
|
||||
} from "../../appState";
|
||||
import { clearElementsForLocalStorage } from "../../element";
|
||||
import { STORAGE_KEYS as APP_STORAGE_KEYS } from "../../constants";
|
||||
|
||||
export const STORAGE_KEYS = {
|
||||
LOCAL_STORAGE_ELEMENTS: "excalidraw",
|
||||
LOCAL_STORAGE_APP_STATE: "excalidraw-state",
|
||||
LOCAL_STORAGE_COLLAB: "excalidraw-collab",
|
||||
LOCAL_STORAGE_KEY_COLLAB_FORCE_FLAG: "collabLinkForceLoadFlag",
|
||||
LOCAL_STORAGE_LIBRARY: "excalidraw-library",
|
||||
};
|
||||
|
||||
export const saveUsernameToLocalStorage = (username: string) => {
|
||||
@@ -113,9 +113,7 @@ export const getTotalStorageSize = () => {
|
||||
try {
|
||||
const appState = localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_APP_STATE);
|
||||
const collab = localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_COLLAB);
|
||||
const library = localStorage.getItem(
|
||||
APP_STORAGE_KEYS.LOCAL_STORAGE_LIBRARY,
|
||||
);
|
||||
const library = localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY);
|
||||
|
||||
const appStateSize = appState?.length || 0;
|
||||
const collabSize = collab?.length || 0;
|
||||
|
||||
@@ -7,7 +7,6 @@ import { TopErrorBoundary } from "../components/TopErrorBoundary";
|
||||
import {
|
||||
APP_NAME,
|
||||
EVENT,
|
||||
STORAGE_KEYS,
|
||||
TITLE_TIMEOUT,
|
||||
URL_HASH_KEYS,
|
||||
VERSION_TIMEOUT,
|
||||
@@ -53,6 +52,7 @@ import { exportToBackend, getCollaborationLinkData, loadScene } from "./data";
|
||||
import {
|
||||
importFromLocalStorage,
|
||||
saveToLocalStorage,
|
||||
STORAGE_KEYS,
|
||||
} from "./data/localStorage";
|
||||
import CustomStats from "./CustomStats";
|
||||
import { restoreAppState, RestoredDataState } from "../data/restore";
|
||||
@@ -262,7 +262,7 @@ const PlusLinkJSX = (
|
||||
Introducing Excalidraw+
|
||||
<br />
|
||||
<a
|
||||
href="https://plus.excalidraw.com/?utm_source=excalidraw&utm_medium=banner&utm_campaign=launch"
|
||||
href="https://plus.excalidraw.com/plus?utm_source=excalidraw&utm_medium=banner&utm_campaign=launch"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
|
||||
@@ -40,6 +40,10 @@ export const KEYS = {
|
||||
QUESTION_MARK: "?",
|
||||
SPACE: " ",
|
||||
TAB: "Tab",
|
||||
CHEVRON_LEFT: "<",
|
||||
CHEVRON_RIGHT: ">",
|
||||
PERIOD: ".",
|
||||
COMMA: ",",
|
||||
|
||||
A: "a",
|
||||
D: "d",
|
||||
|
||||
+3
-1
@@ -102,7 +102,9 @@
|
||||
"showBackground": "Show background color picker",
|
||||
"toggleTheme": "Toggle theme",
|
||||
"personalLib": "Personal Library",
|
||||
"excalidrawLib": "Excalidraw Library"
|
||||
"excalidrawLib": "Excalidraw Library",
|
||||
"decreaseFontSize": "Decrease font size",
|
||||
"increaseFontSize": "Increase font size"
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "Reset the canvas",
|
||||
|
||||
@@ -19,6 +19,8 @@ Please add the latest change on the top under the correct section.
|
||||
|
||||
### 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.
|
||||
|
||||
- #### BREAKING CHANGE
|
||||
|
||||
@@ -509,6 +509,7 @@ 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. |
|
||||
| `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`. |
|
||||
| `libraryItems` | [LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L258) | The `libraryItems` to be update in the scene. |
|
||||
|
||||
### `addFiles`
|
||||
|
||||
|
||||
@@ -79,6 +79,67 @@ export default function App() {
|
||||
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",
|
||||
@@ -103,6 +164,24 @@ export default function App() {
|
||||
>
|
||||
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"
|
||||
|
||||
@@ -2,45 +2,149 @@ export default {
|
||||
elements: [
|
||||
{
|
||||
type: "rectangle",
|
||||
version: 141,
|
||||
versionNonce: 361174001,
|
||||
version: 75,
|
||||
versionNonce: 812169490,
|
||||
isDeleted: false,
|
||||
id: "oDVXy8D6rom3H1-LLH2-f",
|
||||
fillStyle: "hachure",
|
||||
id: "6sVDp9mCGQTomD9Cg5w1b",
|
||||
fillStyle: "solid",
|
||||
strokeWidth: 1,
|
||||
strokeStyle: "solid",
|
||||
roughness: 1,
|
||||
opacity: 100,
|
||||
angle: 0,
|
||||
x: 100.50390625,
|
||||
y: 93.67578125,
|
||||
x: 211.04296875,
|
||||
y: -575.68359375,
|
||||
strokeColor: "#000000",
|
||||
backgroundColor: "transparent",
|
||||
width: 186.47265625,
|
||||
height: 141.9765625,
|
||||
seed: 1968410350,
|
||||
backgroundColor: "#e64980",
|
||||
width: 163,
|
||||
height: 185,
|
||||
seed: 1640006454,
|
||||
groupIds: [],
|
||||
strokeSharpness: "sharp",
|
||||
boundElements: [
|
||||
{
|
||||
type: "text",
|
||||
id: "MB9CSH621UIKH8MEgOhaM",
|
||||
},
|
||||
],
|
||||
updated: 1642057378650,
|
||||
},
|
||||
{
|
||||
id: "-xMIs_0jIFqvpx-R9UnaG",
|
||||
type: "ellipse",
|
||||
x: 300.5703125,
|
||||
y: 190.69140625,
|
||||
width: 198.21875,
|
||||
height: 129.51171875,
|
||||
type: "text",
|
||||
version: 81,
|
||||
versionNonce: 2146024018,
|
||||
isDeleted: false,
|
||||
id: "MB9CSH621UIKH8MEgOhaM",
|
||||
fillStyle: "solid",
|
||||
strokeWidth: 1,
|
||||
strokeStyle: "solid",
|
||||
roughness: 1,
|
||||
opacity: 100,
|
||||
angle: 0,
|
||||
x: 216.04296875,
|
||||
y: -520.68359375,
|
||||
strokeColor: "#000000",
|
||||
backgroundColor: "#e64980",
|
||||
width: 153,
|
||||
height: 75,
|
||||
seed: 802336758,
|
||||
groupIds: [],
|
||||
strokeSharpness: "sharp",
|
||||
boundElements: [],
|
||||
updated: 1642057383302,
|
||||
fontSize: 20,
|
||||
fontFamily: 1,
|
||||
text: "Hello World!!!\n\nSticky notes:)",
|
||||
baseline: 68,
|
||||
textAlign: "center",
|
||||
verticalAlign: "middle",
|
||||
containerId: "6sVDp9mCGQTomD9Cg5w1b",
|
||||
originalText: "Hello World!!!\n\nSticky notes:)",
|
||||
},
|
||||
{
|
||||
type: "diamond",
|
||||
version: 49,
|
||||
versionNonce: 2072814570,
|
||||
isDeleted: false,
|
||||
id: "lpW0Dxf_d5ZH4KencU57Q",
|
||||
fillStyle: "solid",
|
||||
strokeWidth: 1,
|
||||
strokeStyle: "solid",
|
||||
roughness: 1,
|
||||
opacity: 100,
|
||||
angle: 0,
|
||||
x: 439.9140625,
|
||||
y: -667.89453125,
|
||||
strokeColor: "#000000",
|
||||
backgroundColor: "#15aabf",
|
||||
width: 172.76953125,
|
||||
height: 194.109375,
|
||||
seed: 1339998698,
|
||||
groupIds: [],
|
||||
strokeSharpness: "sharp",
|
||||
boundElements: [],
|
||||
updated: 1639729533735,
|
||||
},
|
||||
{
|
||||
id: "Ioiq-J6pE6vMftvReHcq7",
|
||||
type: "rectangle",
|
||||
x: 643.01171875,
|
||||
y: -500.31640625,
|
||||
width: 158.34375,
|
||||
height: 175.52734375,
|
||||
angle: 0,
|
||||
strokeColor: "#000000",
|
||||
backgroundColor: "transparent",
|
||||
fillStyle: "hachure",
|
||||
backgroundColor: "#e64980",
|
||||
fillStyle: "solid",
|
||||
strokeWidth: 1,
|
||||
strokeStyle: "solid",
|
||||
roughness: 1,
|
||||
opacity: 100,
|
||||
groupIds: [],
|
||||
seed: 957947807,
|
||||
version: 47,
|
||||
versionNonce: 1128618623,
|
||||
strokeSharpness: "sharp",
|
||||
seed: 1752793487,
|
||||
version: 243,
|
||||
versionNonce: 1367252367,
|
||||
isDeleted: false,
|
||||
boundElementIds: ["QEIIo0aX3LIkipph5r9Vc"],
|
||||
updated: 1642057965561,
|
||||
},
|
||||
{
|
||||
id: "QEIIo0aX3LIkipph5r9Vc",
|
||||
type: "arrow",
|
||||
x: 402.73046875,
|
||||
y: -400.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: "#AFEEEE", currentItemFontFamily: 1 },
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
"react-dom": "^17.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.16.0",
|
||||
"@babel/core": "7.16.7",
|
||||
"@babel/plugin-transform-arrow-functions": "7.16.7",
|
||||
"@babel/plugin-transform-async-to-generator": "7.16.0",
|
||||
"@babel/plugin-transform-runtime": "7.16.4",
|
||||
@@ -52,21 +52,21 @@
|
||||
"@babel/preset-env": "7.16.7",
|
||||
"@babel/preset-react": "7.16.7",
|
||||
"@babel/preset-typescript": "7.16.7",
|
||||
"autoprefixer": "10.4.0",
|
||||
"autoprefixer": "10.4.2",
|
||||
"babel-loader": "8.2.3",
|
||||
"babel-plugin-transform-class-properties": "6.24.1",
|
||||
"cross-env": "7.0.3",
|
||||
"css-loader": "6.5.1",
|
||||
"mini-css-extract-plugin": "2.4.5",
|
||||
"mini-css-extract-plugin": "2.4.6",
|
||||
"postcss-loader": "6.2.1",
|
||||
"sass-loader": "12.4.0",
|
||||
"terser-webpack-plugin": "5.3.0",
|
||||
"ts-loader": "9.2.6",
|
||||
"typescript": "4.5.3",
|
||||
"typescript": "4.5.4",
|
||||
"webpack": "5.65.0",
|
||||
"webpack-bundle-analyzer": "4.5.0",
|
||||
"webpack-cli": "4.9.1",
|
||||
"webpack-dev-server": "4.7.1",
|
||||
"webpack-dev-server": "4.7.2",
|
||||
"webpack-merge": "5.8.0"
|
||||
},
|
||||
"bugs": "https://github.com/excalidraw/excalidraw/issues",
|
||||
|
||||
@@ -2,14 +2,7 @@
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.16.0":
|
||||
version "7.16.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.16.0.tgz#0dfc80309beec8411e65e706461c408b0bb9b431"
|
||||
integrity sha512-IF4EOMEV+bfYwOmNxGzSnjR2EmQod7f1UXOpZM3l4i4o4QNwzjtJAu/HxdjHq0aYBvdqMuQEY1eg0nqW9ZPORA==
|
||||
dependencies:
|
||||
"@babel/highlight" "^7.16.0"
|
||||
|
||||
"@babel/code-frame@^7.16.7":
|
||||
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.16.7":
|
||||
version "7.16.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.16.7.tgz#44416b6bd7624b998f5b1af5d470856c40138789"
|
||||
integrity sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==
|
||||
@@ -21,20 +14,20 @@
|
||||
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.16.4.tgz#081d6bbc336ec5c2435c6346b2ae1fb98b5ac68e"
|
||||
integrity sha512-1o/jo7D+kC9ZjHX5v+EHrdjl3PhxMrLSOTGsOdHJ+KL8HCaEK6ehrVL2RS6oHDZp+L7xLirLrPmQtEng769J/Q==
|
||||
|
||||
"@babel/core@7.16.0":
|
||||
version "7.16.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.16.0.tgz#c4ff44046f5fe310525cc9eb4ef5147f0c5374d4"
|
||||
integrity sha512-mYZEvshBRHGsIAiyH5PzCFTCfbWfoYbO/jcSdXQSUQu1/pW0xDZAUP7KEc32heqWTAfAHhV9j1vH8Sav7l+JNQ==
|
||||
"@babel/core@7.16.7":
|
||||
version "7.16.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.16.7.tgz#db990f931f6d40cb9b87a0dc7d2adc749f1dcbcf"
|
||||
integrity sha512-aeLaqcqThRNZYmbMqtulsetOQZ/5gbR/dWruUCJcpas4Qoyy+QeagfDsPdMrqwsPRDNxJvBlRiZxxX7THO7qtA==
|
||||
dependencies:
|
||||
"@babel/code-frame" "^7.16.0"
|
||||
"@babel/generator" "^7.16.0"
|
||||
"@babel/helper-compilation-targets" "^7.16.0"
|
||||
"@babel/helper-module-transforms" "^7.16.0"
|
||||
"@babel/helpers" "^7.16.0"
|
||||
"@babel/parser" "^7.16.0"
|
||||
"@babel/template" "^7.16.0"
|
||||
"@babel/traverse" "^7.16.0"
|
||||
"@babel/types" "^7.16.0"
|
||||
"@babel/code-frame" "^7.16.7"
|
||||
"@babel/generator" "^7.16.7"
|
||||
"@babel/helper-compilation-targets" "^7.16.7"
|
||||
"@babel/helper-module-transforms" "^7.16.7"
|
||||
"@babel/helpers" "^7.16.7"
|
||||
"@babel/parser" "^7.16.7"
|
||||
"@babel/template" "^7.16.7"
|
||||
"@babel/traverse" "^7.16.7"
|
||||
"@babel/types" "^7.16.7"
|
||||
convert-source-map "^1.7.0"
|
||||
debug "^4.1.0"
|
||||
gensync "^1.0.0-beta.2"
|
||||
@@ -42,15 +35,6 @@
|
||||
semver "^6.3.0"
|
||||
source-map "^0.5.0"
|
||||
|
||||
"@babel/generator@^7.16.0":
|
||||
version "7.16.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.16.0.tgz#d40f3d1d5075e62d3500bccb67f3daa8a95265b2"
|
||||
integrity sha512-RR8hUCfRQn9j9RPKEVXo9LiwoxLPYn6hNZlvUOR8tSnaxlD0p0+la00ZP9/SnRt6HchKr+X0fO2r8vrETiJGew==
|
||||
dependencies:
|
||||
"@babel/types" "^7.16.0"
|
||||
jsesc "^2.5.1"
|
||||
source-map "^0.5.0"
|
||||
|
||||
"@babel/generator@^7.16.7":
|
||||
version "7.16.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.16.7.tgz#b42bf46a3079fa65e1544135f32e7958f048adbb"
|
||||
@@ -82,7 +66,7 @@
|
||||
"@babel/helper-explode-assignable-expression" "^7.16.7"
|
||||
"@babel/types" "^7.16.7"
|
||||
|
||||
"@babel/helper-compilation-targets@^7.13.0", "@babel/helper-compilation-targets@^7.16.0", "@babel/helper-compilation-targets@^7.16.7":
|
||||
"@babel/helper-compilation-targets@^7.13.0", "@babel/helper-compilation-targets@^7.16.7":
|
||||
version "7.16.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.16.7.tgz#06e66c5f299601e6c7da350049315e83209d551b"
|
||||
integrity sha512-mGojBwIWcwGD6rfqgRXVlVYmPAv7eOpIemUG3dGnDdCY4Pae70ROij3XmfrH6Fa1h1aiDylpglbZyktfzyo/hA==
|
||||
@@ -185,13 +169,6 @@
|
||||
dependencies:
|
||||
"@babel/types" "^7.16.7"
|
||||
|
||||
"@babel/helper-hoist-variables@^7.16.0":
|
||||
version "7.16.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.0.tgz#4c9023c2f1def7e28ff46fc1dbcd36a39beaa81a"
|
||||
integrity sha512-1AZlpazjUR0EQZQv3sgRNfM9mEVWPK3M6vlalczA+EECcPz3XPh6VplbErL5UoMpChhSck5wAJHthlj1bYpcmg==
|
||||
dependencies:
|
||||
"@babel/types" "^7.16.0"
|
||||
|
||||
"@babel/helper-hoist-variables@^7.16.7":
|
||||
version "7.16.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz#86bcb19a77a509c7b77d0e22323ef588fa58c246"
|
||||
@@ -227,20 +204,6 @@
|
||||
dependencies:
|
||||
"@babel/types" "^7.16.7"
|
||||
|
||||
"@babel/helper-module-transforms@^7.16.0":
|
||||
version "7.16.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.16.0.tgz#1c82a8dd4cb34577502ebd2909699b194c3e9bb5"
|
||||
integrity sha512-My4cr9ATcaBbmaEa8M0dZNA74cfI6gitvUAskgDtAFmAqyFKDSHQo5YstxPbN+lzHl2D9l/YOEFqb2mtUh4gfA==
|
||||
dependencies:
|
||||
"@babel/helper-module-imports" "^7.16.0"
|
||||
"@babel/helper-replace-supers" "^7.16.0"
|
||||
"@babel/helper-simple-access" "^7.16.0"
|
||||
"@babel/helper-split-export-declaration" "^7.16.0"
|
||||
"@babel/helper-validator-identifier" "^7.15.7"
|
||||
"@babel/template" "^7.16.0"
|
||||
"@babel/traverse" "^7.16.0"
|
||||
"@babel/types" "^7.16.0"
|
||||
|
||||
"@babel/helper-module-transforms@^7.16.7":
|
||||
version "7.16.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.16.7.tgz#7665faeb721a01ca5327ddc6bba15a5cb34b6a41"
|
||||
@@ -313,13 +276,6 @@
|
||||
"@babel/traverse" "^7.16.7"
|
||||
"@babel/types" "^7.16.7"
|
||||
|
||||
"@babel/helper-simple-access@^7.16.0":
|
||||
version "7.16.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.16.0.tgz#21d6a27620e383e37534cf6c10bba019a6f90517"
|
||||
integrity sha512-o1rjBT/gppAqKsYfUdfHq5Rk03lMQrkPHG1OWzHWpLgVXRH4HnMM9Et9CVdIqwkCQlobnGHEJMsgWP/jE1zUiw==
|
||||
dependencies:
|
||||
"@babel/types" "^7.16.0"
|
||||
|
||||
"@babel/helper-simple-access@^7.16.7":
|
||||
version "7.16.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.16.7.tgz#d656654b9ea08dbb9659b69d61063ccd343ff0f7"
|
||||
@@ -348,11 +304,6 @@
|
||||
dependencies:
|
||||
"@babel/types" "^7.16.7"
|
||||
|
||||
"@babel/helper-validator-identifier@^7.15.7":
|
||||
version "7.15.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz#220df993bfe904a4a6b02ab4f3385a5ebf6e2389"
|
||||
integrity sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==
|
||||
|
||||
"@babel/helper-validator-identifier@^7.16.7":
|
||||
version "7.16.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz#e8c602438c4a8195751243da9031d1607d247cad"
|
||||
@@ -383,23 +334,14 @@
|
||||
"@babel/traverse" "^7.16.7"
|
||||
"@babel/types" "^7.16.7"
|
||||
|
||||
"@babel/helpers@^7.16.0":
|
||||
version "7.16.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.16.0.tgz#875519c979c232f41adfbd43a3b0398c2e388183"
|
||||
integrity sha512-dVRM0StFMdKlkt7cVcGgwD8UMaBfWJHl3A83Yfs8GQ3MO0LHIIIMvK7Fa0RGOGUQ10qikLaX6D7o5htcQWgTMQ==
|
||||
"@babel/helpers@^7.16.7":
|
||||
version "7.16.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.16.7.tgz#7e3504d708d50344112767c3542fc5e357fffefc"
|
||||
integrity sha512-9ZDoqtfY7AuEOt3cxchfii6C7GDyyMBffktR5B2jvWv8u2+efwvpnVKXMWzNehqy68tKgAfSwfdw/lWpthS2bw==
|
||||
dependencies:
|
||||
"@babel/template" "^7.16.0"
|
||||
"@babel/traverse" "^7.16.0"
|
||||
"@babel/types" "^7.16.0"
|
||||
|
||||
"@babel/highlight@^7.16.0":
|
||||
version "7.16.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.16.0.tgz#6ceb32b2ca4b8f5f361fb7fd821e3fddf4a1725a"
|
||||
integrity sha512-t8MH41kUQylBtu2+4IQA3atqevA2lRgqA2wyVB/YiWmsDSuylZZuXOUy9ric30hfzauEFfdsuk/eXTRrGrfd0g==
|
||||
dependencies:
|
||||
"@babel/helper-validator-identifier" "^7.15.7"
|
||||
chalk "^2.0.0"
|
||||
js-tokens "^4.0.0"
|
||||
"@babel/template" "^7.16.7"
|
||||
"@babel/traverse" "^7.16.7"
|
||||
"@babel/types" "^7.16.7"
|
||||
|
||||
"@babel/highlight@^7.16.7":
|
||||
version "7.16.7"
|
||||
@@ -410,11 +352,6 @@
|
||||
chalk "^2.0.0"
|
||||
js-tokens "^4.0.0"
|
||||
|
||||
"@babel/parser@^7.16.0":
|
||||
version "7.16.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.16.0.tgz#cf147d7ada0a3655e79bf4b08ee846f00a00a295"
|
||||
integrity sha512-TEHWXf0xxpi9wKVyBCmRcSSDjbJ/cl6LUdlbYUHEaNQUJGhreJbZrXT6sR4+fZLxVUJqNRB4KyOvjuy/D9009A==
|
||||
|
||||
"@babel/parser@^7.16.7":
|
||||
version "7.16.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.16.7.tgz#d372dda9c89fcec340a82630a9f533f2fe15877e"
|
||||
@@ -1124,16 +1061,7 @@
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/template@^7.16.0":
|
||||
version "7.16.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.0.tgz#d16a35ebf4cd74e202083356fab21dd89363ddd6"
|
||||
integrity sha512-MnZdpFD/ZdYhXwiunMqqgyZyucaYsbL0IrjoGjaVhGilz+x8YB++kRfygSOIj1yOtWKPlx7NBp+9I1RQSgsd5A==
|
||||
dependencies:
|
||||
"@babel/code-frame" "^7.16.0"
|
||||
"@babel/parser" "^7.16.0"
|
||||
"@babel/types" "^7.16.0"
|
||||
|
||||
"@babel/template@^7.16.7":
|
||||
"@babel/template@^7.16.0", "@babel/template@^7.16.7":
|
||||
version "7.16.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.7.tgz#8d126c8701fde4d66b264b3eba3d96f07666d155"
|
||||
integrity sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==
|
||||
@@ -1142,22 +1070,7 @@
|
||||
"@babel/parser" "^7.16.7"
|
||||
"@babel/types" "^7.16.7"
|
||||
|
||||
"@babel/traverse@^7.13.0", "@babel/traverse@^7.16.0":
|
||||
version "7.16.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.16.0.tgz#965df6c6bfc0a958c1e739284d3c9fa4a6e3c45b"
|
||||
integrity sha512-qQ84jIs1aRQxaGaxSysII9TuDaguZ5yVrEuC0BN2vcPlalwfLovVmCjbFDPECPXcYM/wLvNFfp8uDOliLxIoUQ==
|
||||
dependencies:
|
||||
"@babel/code-frame" "^7.16.0"
|
||||
"@babel/generator" "^7.16.0"
|
||||
"@babel/helper-function-name" "^7.16.0"
|
||||
"@babel/helper-hoist-variables" "^7.16.0"
|
||||
"@babel/helper-split-export-declaration" "^7.16.0"
|
||||
"@babel/parser" "^7.16.0"
|
||||
"@babel/types" "^7.16.0"
|
||||
debug "^4.1.0"
|
||||
globals "^11.1.0"
|
||||
|
||||
"@babel/traverse@^7.16.7":
|
||||
"@babel/traverse@^7.13.0", "@babel/traverse@^7.16.0", "@babel/traverse@^7.16.7":
|
||||
version "7.16.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.16.7.tgz#dac01236a72c2560073658dd1a285fe4e0865d76"
|
||||
integrity sha512-8KWJPIb8c2VvY8AJrydh6+fVRo2ODx1wYBU2398xJVq0JomuLBZmVQzLPBblJgHIGYG4znCpUZUZ0Pt2vdmVYQ==
|
||||
@@ -1645,17 +1558,17 @@ async@^2.6.2:
|
||||
dependencies:
|
||||
lodash "^4.17.14"
|
||||
|
||||
autoprefixer@10.4.0:
|
||||
version "10.4.0"
|
||||
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.0.tgz#c3577eb32a1079a440ec253e404eaf1eb21388c8"
|
||||
integrity sha512-7FdJ1ONtwzV1G43GDD0kpVMn/qbiNqyOPMFTX5nRffI+7vgWoFEc6DcXOxHJxrWNDXrZh18eDsZjvZGUljSRGA==
|
||||
autoprefixer@10.4.2:
|
||||
version "10.4.2"
|
||||
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.2.tgz#25e1df09a31a9fba5c40b578936b90d35c9d4d3b"
|
||||
integrity sha512-9fOPpHKuDW1w/0EKfRmVnxTDt8166MAnLI3mgZ1JCnhNtYWxcJ6Ud5CO/AVOZi/AvFa8DY9RTy3h3+tFBlrrdQ==
|
||||
dependencies:
|
||||
browserslist "^4.17.5"
|
||||
caniuse-lite "^1.0.30001272"
|
||||
fraction.js "^4.1.1"
|
||||
browserslist "^4.19.1"
|
||||
caniuse-lite "^1.0.30001297"
|
||||
fraction.js "^4.1.2"
|
||||
normalize-range "^0.1.2"
|
||||
picocolors "^1.0.0"
|
||||
postcss-value-parser "^4.1.0"
|
||||
postcss-value-parser "^4.2.0"
|
||||
|
||||
babel-code-frame@^6.26.0:
|
||||
version "6.26.0"
|
||||
@@ -1860,13 +1773,13 @@ braces@^3.0.1, braces@~3.0.2:
|
||||
dependencies:
|
||||
fill-range "^7.0.1"
|
||||
|
||||
browserslist@^4.14.5, browserslist@^4.17.5, browserslist@^4.17.6:
|
||||
version "4.18.1"
|
||||
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.18.1.tgz#60d3920f25b6860eb917c6c7b185576f4d8b017f"
|
||||
integrity sha512-8ScCzdpPwR2wQh8IT82CA2VgDwjHyqMovPBZSNH54+tm4Jk2pCuv90gmAdH6J84OCRWi0b4gMe6O6XPXuJnjgQ==
|
||||
browserslist@^4.14.5, browserslist@^4.17.5, browserslist@^4.17.6, browserslist@^4.19.1:
|
||||
version "4.19.1"
|
||||
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.19.1.tgz#4ac0435b35ab655896c31d53018b6dd5e9e4c9a3"
|
||||
integrity sha512-u2tbbG5PdKRTUoctO3NBD8FQ5HdPh1ZXPHzp1rwaa5jTc+RV9/+RlWiAIKmjRPQF+xbGM9Kklj5bZQFa2s/38A==
|
||||
dependencies:
|
||||
caniuse-lite "^1.0.30001280"
|
||||
electron-to-chromium "^1.3.896"
|
||||
caniuse-lite "^1.0.30001286"
|
||||
electron-to-chromium "^1.4.17"
|
||||
escalade "^3.1.1"
|
||||
node-releases "^2.0.1"
|
||||
picocolors "^1.0.0"
|
||||
@@ -1904,15 +1817,10 @@ callsites@^3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
|
||||
integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
|
||||
|
||||
caniuse-lite@^1.0.30001272:
|
||||
version "1.0.30001272"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001272.tgz#8e9790ff995e9eb6e1f4c45cd07ddaa87cddbb14"
|
||||
integrity sha512-DV1j9Oot5dydyH1v28g25KoVm7l8MTxazwuiH3utWiAS6iL/9Nh//TGwqFEeqqN8nnWYQ8HHhUq+o4QPt9kvYw==
|
||||
|
||||
caniuse-lite@^1.0.30001280:
|
||||
version "1.0.30001282"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001282.tgz#38c781ee0a90ccfe1fe7fefd00e43f5ffdcb96fd"
|
||||
integrity sha512-YhF/hG6nqBEllymSIjLtR2iWDDnChvhnVJqp+vloyt2tEHFG1yBR+ac2B/rOw0qOK0m0lEXU2dv4E/sMk5P9Kg==
|
||||
caniuse-lite@^1.0.30001286, caniuse-lite@^1.0.30001297:
|
||||
version "1.0.30001298"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001298.tgz#0e690039f62e91c3ea581673d716890512e7ec52"
|
||||
integrity sha512-AcKqikjMLlvghZL/vfTHorlQsLDhGRalYf1+GmWCf5SCMziSGjRYQW/JEksj14NaYHIR6KIhrFAy0HV5C25UzQ==
|
||||
|
||||
chalk@^1.1.3:
|
||||
version "1.1.3"
|
||||
@@ -2273,10 +2181,10 @@ ee-first@1.1.1:
|
||||
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
|
||||
integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
|
||||
|
||||
electron-to-chromium@^1.3.896:
|
||||
version "1.3.904"
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.904.tgz#52a353994faeb0f2a9fab3606b4e0614d1af7b58"
|
||||
integrity sha512-x5uZWXcVNYkTh4JubD7KSC1VMKz0vZwJUqVwY3ihsW0bst1BXDe494Uqbg3Y0fDGVjJqA8vEeGuvO5foyH2+qw==
|
||||
electron-to-chromium@^1.4.17:
|
||||
version "1.4.38"
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.38.tgz#10ea58d73d36b13e78d5024f3b74a352d3958d01"
|
||||
integrity sha512-WhHt3sZazKj0KK/UpgsbGQnUUoFeAHVishzHFExMxagpZgjiGYSC9S0ZlbhCfSH2L2i+2A1yyqOIliTctMx7KQ==
|
||||
|
||||
emojis-list@^3.0.0:
|
||||
version "3.0.0"
|
||||
@@ -2511,10 +2419,10 @@ forwarded@0.2.0:
|
||||
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
|
||||
integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==
|
||||
|
||||
fraction.js@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.1.1.tgz#ac4e520473dae67012d618aab91eda09bcb400ff"
|
||||
integrity sha512-MHOhvvxHTfRFpF1geTK9czMIZ6xclsEor2wkIGYYq+PxcQqT7vStJqjhe6S1TenZrMZzo+wlqOufBDVepUEgPg==
|
||||
fraction.js@^4.1.2:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.1.2.tgz#13e420a92422b6cf244dff8690ed89401029fbe8"
|
||||
integrity sha512-o2RiJQ6DZaR/5+Si0qJUIy637QMRudSi9kU/FFzx9EZazrIdnBgpU+3sEWCxAVhH2RtxW2Oz+T4p2o8uOPVcgA==
|
||||
|
||||
fresh@0.5.2:
|
||||
version "0.5.2"
|
||||
@@ -2606,12 +2514,7 @@ globby@^11.0.1:
|
||||
merge2 "^1.3.0"
|
||||
slash "^3.0.0"
|
||||
|
||||
graceful-fs@^4.1.2, graceful-fs@^4.2.4:
|
||||
version "4.2.6"
|
||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee"
|
||||
integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==
|
||||
|
||||
graceful-fs@^4.2.6:
|
||||
graceful-fs@^4.1.2, graceful-fs@^4.2.4, graceful-fs@^4.2.6:
|
||||
version "4.2.8"
|
||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a"
|
||||
integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==
|
||||
@@ -3158,10 +3061,10 @@ mimic-fn@^2.1.0:
|
||||
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
|
||||
integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
|
||||
|
||||
mini-css-extract-plugin@2.4.5:
|
||||
version "2.4.5"
|
||||
resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.4.5.tgz#191d6c170226037212c483af1180b4010b7b9eef"
|
||||
integrity sha512-oEIhRucyn1JbT/1tU2BhnwO6ft1jjH1iCX9Gc59WFMg0n5773rQU0oyQ0zzeYFFuBfONaRbQJyGoPtuNseMxjA==
|
||||
mini-css-extract-plugin@2.4.6:
|
||||
version "2.4.6"
|
||||
resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.4.6.tgz#0f925aaa02dd26513bac40802062a87ebe32e7cc"
|
||||
integrity sha512-khHpc29bdsE9EQiGSLqQieLyMbGca+bkC42/BBL1gXC8yAS0nHpOTUCBYUK6En1FuRdfE9wKXhGtsab8vmsugg==
|
||||
dependencies:
|
||||
schema-utils "^4.0.0"
|
||||
|
||||
@@ -3491,10 +3394,10 @@ postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4:
|
||||
uniq "^1.0.1"
|
||||
util-deprecate "^1.0.2"
|
||||
|
||||
postcss-value-parser@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb"
|
||||
integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==
|
||||
postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
|
||||
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
|
||||
|
||||
postcss@^8.2.15:
|
||||
version "8.3.0"
|
||||
@@ -4092,10 +3995,10 @@ type-is@~1.6.18:
|
||||
media-typer "0.3.0"
|
||||
mime-types "~2.1.24"
|
||||
|
||||
typescript@4.5.3:
|
||||
version "4.5.3"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.3.tgz#afaa858e68c7103317d89eb90c5d8906268d353c"
|
||||
integrity sha512-eVYaEHALSt+s9LbvgEv4Ef+Tdq7hBiIZgii12xXJnukryt3pMgJf6aKhoCZ3FWQsu6sydEnkg11fYXLzhLBjeQ==
|
||||
typescript@4.5.4:
|
||||
version "4.5.4"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.4.tgz#a17d3a0263bf5c8723b9c52f43c5084edf13c2e8"
|
||||
integrity sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg==
|
||||
|
||||
unicode-canonical-property-names-ecmascript@^1.0.4:
|
||||
version "1.0.4"
|
||||
@@ -4216,10 +4119,10 @@ webpack-dev-middleware@^5.3.0:
|
||||
range-parser "^1.2.1"
|
||||
schema-utils "^4.0.0"
|
||||
|
||||
webpack-dev-server@4.7.1:
|
||||
version "4.7.1"
|
||||
resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.7.1.tgz#4fe8f7435843dd2e676d687846f7a3d53f758c0d"
|
||||
integrity sha512-bkoNgFyqlF/CT726Axtf/ELHHYsTZJWz3QJ6HqstWPbalhjAPunlPH9bwt/Lr5cLb+uoLmsta6svVplVzq8beA==
|
||||
webpack-dev-server@4.7.2:
|
||||
version "4.7.2"
|
||||
resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.7.2.tgz#324b79046491f2cf0b9d9e275381198c8246b380"
|
||||
integrity sha512-s6yEOSfPpB6g1T2+C5ZOUt5cQOMhjI98IVmmvMNb5cdiqHoxSUfACISHqU/wZy+q4ar/A9jW0pbNj7sa50XRVA==
|
||||
dependencies:
|
||||
"@types/bonjour" "^3.5.9"
|
||||
"@types/connect-history-api-fallback" "^1.3.5"
|
||||
|
||||
@@ -34,13 +34,13 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.16.5",
|
||||
"@babel/core": "7.16.7",
|
||||
"@babel/plugin-transform-arrow-functions": "7.16.0",
|
||||
"@babel/plugin-transform-async-to-generator": "7.16.5",
|
||||
"@babel/plugin-transform-runtime": "^7.14.5",
|
||||
"@babel/plugin-transform-typescript": "7.16.1",
|
||||
"@babel/preset-env": "7.16.4",
|
||||
"@babel/preset-typescript": "7.16.5",
|
||||
"@babel/plugin-transform-typescript": "7.16.7",
|
||||
"@babel/preset-env": "7.16.7",
|
||||
"@babel/preset-typescript": "7.16.7",
|
||||
"babel-loader": "8.2.3",
|
||||
"babel-plugin-transform-class-properties": "6.24.1",
|
||||
"cross-env": "7.0.3",
|
||||
|
||||
+549
-500
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -80,7 +80,7 @@ export const SHAPES = [
|
||||
</svg>
|
||||
),
|
||||
value: "freedraw",
|
||||
key: KEYS.X,
|
||||
key: [KEYS.X, KEYS.P.toUpperCase()],
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
|
||||
@@ -255,6 +255,7 @@ export type SceneData = {
|
||||
appState?: ImportedDataState["appState"];
|
||||
collaborators?: Map<string, Collaborator>;
|
||||
commitToHistory?: boolean;
|
||||
libraryItems?: LibraryItems | LibraryItems_v1;
|
||||
};
|
||||
|
||||
export enum UserIdleState {
|
||||
|
||||
@@ -2159,10 +2159,10 @@
|
||||
"@babel/runtime" "^7.12.5"
|
||||
"@testing-library/dom" "^8.0.0"
|
||||
|
||||
"@tldraw/vec@1.4.0":
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@tldraw/vec/-/vec-1.4.0.tgz#91a6d7c852c680a3288fe8bf79623252ef917281"
|
||||
integrity sha512-YWkGe/IXdFB/Kts982fmtis2nJHB8KWVbkf0PlaZtQ1CL0TZKH0xdpIA/QJMk/WCj3bN4AOYuZKwEAegmWAnOA==
|
||||
"@tldraw/vec@1.4.3":
|
||||
version "1.4.3"
|
||||
resolved "https://registry.yarnpkg.com/@tldraw/vec/-/vec-1.4.3.tgz#c1460da019063b8eb9c62d715c61dff44dd23b02"
|
||||
integrity sha512-p7Nu9OWqorQ+nWPlvkN6Zn1oDbRgr4sheLzRif/+xB7BzMQFJh1T8syX6Jj/S88GPvn5PwZgYUKMnMFvvL5t/w==
|
||||
|
||||
"@tootallnate/once@1":
|
||||
version "1.1.2"
|
||||
@@ -2283,10 +2283,10 @@
|
||||
dependencies:
|
||||
"@types/istanbul-lib-report" "*"
|
||||
|
||||
"@types/jest@*", "@types/jest@27.0.3":
|
||||
version "27.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-27.0.3.tgz#0cf9dfe9009e467f70a342f0f94ead19842a783a"
|
||||
integrity sha512-cmmwv9t7gBYt7hNKH5Spu7Kuu/DotGa+Ff+JGRKZ4db5eh8PnKS4LuebJ3YLUoyOyIHraTGyULn23YtEAm0VSg==
|
||||
"@types/jest@*", "@types/jest@27.4.0":
|
||||
version "27.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-27.4.0.tgz#037ab8b872067cae842a320841693080f9cb84ed"
|
||||
integrity sha512-gHl8XuC1RZ8H2j5sHv/JqsaxXkDDM9iDOgu0Wp8sjs4u/snb2PVehyWXJPr+ORA0RPpgw231mnutWI1+0hgjIQ==
|
||||
dependencies:
|
||||
jest-diff "^27.0.0"
|
||||
pretty-format "^27.0.0"
|
||||
@@ -9276,10 +9276,10 @@ lines-and-columns@^1.1.6:
|
||||
resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz"
|
||||
integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=
|
||||
|
||||
lint-staged@12.1.4:
|
||||
version "12.1.4"
|
||||
resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-12.1.4.tgz#a92ec8509f13018caaafade61d515c2d5873316e"
|
||||
integrity sha512-RgDz9nsFsE0/5eL9Vat0AvCuk0+j5mEuzBIVfrRH5FRtt5wibYe8zTjZs2nuqLFrLAGQGYnj8+HJxolcj08i/A==
|
||||
lint-staged@12.1.7:
|
||||
version "12.1.7"
|
||||
resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-12.1.7.tgz#fe9137992ac18a456422bb8484dd30be0140629f"
|
||||
integrity sha512-bltv/ejiLWtowExpjU+s5z8j1Byjg9AlmaAjMmqNbIicY69u6sYIwXGg0dCn0TlkrrY2CphtHIXAkbZ+1VoWQQ==
|
||||
dependencies:
|
||||
cli-truncate "^3.1.0"
|
||||
colorette "^2.0.16"
|
||||
@@ -12915,10 +12915,10 @@ sass-loader@^10.0.5:
|
||||
schema-utils "^3.0.0"
|
||||
semver "^7.3.2"
|
||||
|
||||
sass@1.45.2:
|
||||
version "1.45.2"
|
||||
resolved "https://registry.yarnpkg.com/sass/-/sass-1.45.2.tgz#130b428c1692201cfa181139835d6fc378a33323"
|
||||
integrity sha512-cKfs+F9AMPAFlbbTXNsbGvg3y58nV0mXA3E94jqaySKcC8Kq3/8983zVKQ0TLMUrHw7hF9Tnd3Bz9z5Xgtrl9g==
|
||||
sass@1.47.0:
|
||||
version "1.47.0"
|
||||
resolved "https://registry.yarnpkg.com/sass/-/sass-1.47.0.tgz#c22dd0eed2e4a991430dae0b03c8e694bc41c2b4"
|
||||
integrity sha512-GtXwvwgD7/6MLUZPnlA5/8cdRgC9SzT5kAnnJMRmEZQFRE3J56Foswig4NyyyQGsnmNvg6EUM/FP0Pe9Y2zywQ==
|
||||
dependencies:
|
||||
chokidar ">=3.0.0 <4.0.0"
|
||||
immutable "^4.0.0"
|
||||
|
||||
Reference in New Issue
Block a user