mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-11-13 01:04:32 +01:00
Compare commits
45 Commits
perf_debug
...
aakansha-h
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef8bcbe1f8 | ||
|
|
02d5cc4174 | ||
|
|
ec4b3d913e | ||
|
|
5390617c01 | ||
|
|
0d1058a596 | ||
|
|
c5869979c8 | ||
|
|
6a6b9c90a7 | ||
|
|
5c17751662 | ||
|
|
898789b979 | ||
|
|
7922ce129e | ||
|
|
59ec1c6cee | ||
|
|
933c6a2237 | ||
|
|
cd61f31116 | ||
|
|
2f4d051ea1 | ||
|
|
0b0846bd8e | ||
|
|
330b3a0530 | ||
|
|
cd195374bc | ||
|
|
8971d06655 | ||
|
|
b3052f0178 | ||
|
|
a271e42af1 | ||
|
|
836120c14b | ||
|
|
da4fa91ffc | ||
|
|
553b493f37 | ||
|
|
59a1d192d2 | ||
|
|
8b7302e89e | ||
|
|
f9b7cfd8aa | ||
|
|
1e4e37b87f | ||
|
|
189a557ed6 | ||
|
|
bff2f9178d | ||
|
|
9b5715623a | ||
|
|
2b4462c941 | ||
|
|
43b13d8e3a | ||
|
|
3487f0ab26 | ||
|
|
76910828c2 | ||
|
|
fde521ef4d | ||
|
|
bcb45f7cf6 | ||
|
|
720f468f39 | ||
|
|
33300d19f6 | ||
|
|
5aed159991 | ||
|
|
de1d221d1c | ||
|
|
9a68dbffe2 | ||
|
|
32d82219b1 | ||
|
|
ba2c86fe1b | ||
|
|
f1ae37c84b | ||
|
|
ec350ba8b2 |
43
.codesandbox/tasks.json
Normal file
43
.codesandbox/tasks.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
// These tasks will run in order when initializing your CodeSandbox project.
|
||||
"setupTasks": [
|
||||
{
|
||||
"name": "Install Dependencies",
|
||||
"command": "yarn install"
|
||||
}
|
||||
],
|
||||
|
||||
// These tasks can be run from CodeSandbox. Running one will open a log in the app.
|
||||
"tasks": {
|
||||
"build": {
|
||||
"name": "Build",
|
||||
"command": "yarn build",
|
||||
"runAtStart": false
|
||||
},
|
||||
"fix": {
|
||||
"name": "Fix",
|
||||
"command": "yarn fix",
|
||||
"runAtStart": false
|
||||
},
|
||||
"prettier": {
|
||||
"name": "Prettify",
|
||||
"command": "yarn prettier",
|
||||
"runAtStart": false
|
||||
},
|
||||
"start": {
|
||||
"name": "Start Excalidraw",
|
||||
"command": "yarn start",
|
||||
"runAtStart": true
|
||||
},
|
||||
"test": {
|
||||
"name": "Run Tests",
|
||||
"command": "yarn test",
|
||||
"runAtStart": false
|
||||
},
|
||||
"install-deps": {
|
||||
"name": "Install Dependencies",
|
||||
"command": "yarn install",
|
||||
"restartOn": { "files": ["yarn.lock"] }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -88,7 +88,7 @@ Try out [`@excalidraw/excalidraw`](https://www.npmjs.com/package/@excalidraw/exc
|
||||
|
||||
### Code Sandbox
|
||||
|
||||
- Go to https://codesandbox.io/s/github/excalidraw/excalidraw
|
||||
- Go to https://codesandbox.io/p/github/excalidraw/excalidraw
|
||||
- You may need to sign in with GitHub and reload the page
|
||||
- You can start coding instantly, and even send PRs from there!
|
||||
|
||||
|
||||
@@ -31,10 +31,8 @@
|
||||
"@types/socket.io-client": "1.4.36",
|
||||
"browser-fs-access": "0.29.1",
|
||||
"clsx": "1.1.1",
|
||||
"cross-env": "7.0.3",
|
||||
"fake-indexeddb": "3.1.7",
|
||||
"firebase": "8.3.3",
|
||||
"http-server": "14.1.1",
|
||||
"i18next-browser-languagedetector": "6.1.4",
|
||||
"idb-keyval": "6.0.3",
|
||||
"image-blob-reduce": "3.0.1",
|
||||
@@ -44,6 +42,7 @@
|
||||
"open-color": "1.9.1",
|
||||
"pako": "1.0.11",
|
||||
"perfect-freehand": "1.0.16",
|
||||
"pica": "7.1.1",
|
||||
"png-chunk-text": "1.0.0",
|
||||
"png-chunks-encode": "1.0.0",
|
||||
"png-chunks-extract": "1.0.0",
|
||||
@@ -93,8 +92,8 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build-node": "node ./scripts/build-node.js",
|
||||
"build:app:docker": "cross-env REACT_APP_DISABLE_SENTRY=true react-scripts build",
|
||||
"build:app": "cross-env REACT_APP_GIT_SHA=$VERCEL_GIT_COMMIT_SHA react-scripts build",
|
||||
"build:app:docker": "REACT_APP_DISABLE_SENTRY=true react-scripts build",
|
||||
"build:app": "REACT_APP_GIT_SHA=$VERCEL_GIT_COMMIT_SHA react-scripts build",
|
||||
"build:version": "node ./scripts/build-version.js",
|
||||
"build:prebuild": "node ./scripts/prebuild.js",
|
||||
"build": "yarn build:prebuild && yarn build:app && yarn build:version",
|
||||
@@ -107,7 +106,6 @@
|
||||
"prepare": "husky install",
|
||||
"prettier": "prettier \"**/*.{css,scss,json,md,html,yml}\" --ignore-path=.eslintignore",
|
||||
"start": "react-scripts start",
|
||||
"start:build": "npm run build && npx http-server build -a localhost -p 3001 -o",
|
||||
"test:all": "yarn test:typecheck && yarn test:code && yarn test:other && yarn test:app --watchAll=false",
|
||||
"test:app": "react-scripts test --passWithNoTests",
|
||||
"test:code": "eslint --max-warnings=0 --ext .js,.ts,.tsx .",
|
||||
|
||||
@@ -18,6 +18,4 @@ const moveServiceWorkerScript = () => {
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
if (process.env.CI) {
|
||||
moveServiceWorkerScript();
|
||||
}
|
||||
moveServiceWorkerScript();
|
||||
|
||||
@@ -244,7 +244,7 @@ export const actionLoadScene = register({
|
||||
}
|
||||
},
|
||||
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.O,
|
||||
PanelComponent: ({ updateData, appState }) => (
|
||||
PanelComponent: ({ updateData }) => (
|
||||
<ToolButton
|
||||
type="button"
|
||||
icon={load}
|
||||
|
||||
@@ -33,6 +33,9 @@ export const actionFinalize = register({
|
||||
endBindingElement,
|
||||
);
|
||||
}
|
||||
const selectedLinearElement = appState.selectedLinearElement
|
||||
? new LinearElementEditor(element, scene, appState)
|
||||
: null;
|
||||
return {
|
||||
elements:
|
||||
element.points.length < 2 || isInvisiblySmallElement(element)
|
||||
@@ -42,6 +45,7 @@ export const actionFinalize = register({
|
||||
...appState,
|
||||
cursorButton: "up",
|
||||
editingLinearElement: null,
|
||||
selectedLinearElement,
|
||||
},
|
||||
commitToHistory: true,
|
||||
};
|
||||
@@ -184,7 +188,7 @@ export const actionFinalize = register({
|
||||
// To select the linear element when user has finished mutipoint editing
|
||||
selectedLinearElement:
|
||||
multiPointElement && isLinearElement(multiPointElement)
|
||||
? new LinearElementEditor(multiPointElement, scene)
|
||||
? new LinearElementEditor(multiPointElement, scene, appState)
|
||||
: appState.selectedLinearElement,
|
||||
pendingImageElementId: null,
|
||||
},
|
||||
|
||||
@@ -35,7 +35,7 @@ export const actionSelectAll = register({
|
||||
// single linear element selected
|
||||
Object.keys(selectedElementIds).length === 1 &&
|
||||
isLinearElement(elements[0])
|
||||
? new LinearElementEditor(elements[0], app.scene)
|
||||
? new LinearElementEditor(elements[0], app.scene, appState)
|
||||
: null,
|
||||
editingGroupId: null,
|
||||
selectedElementIds,
|
||||
|
||||
@@ -147,6 +147,7 @@ export class ActionManager {
|
||||
) {
|
||||
const action = this.actions[name];
|
||||
const PanelComponent = action.PanelComponent!;
|
||||
PanelComponent.displayName = "PanelComponent";
|
||||
const elements = this.getElementsIncludingDeleted();
|
||||
const appState = this.getAppState();
|
||||
const updateData = (formState?: any) => {
|
||||
|
||||
@@ -26,17 +26,17 @@ import { ToolButton } from "./ToolButton";
|
||||
import { hasStrokeColor } from "../scene/comparisons";
|
||||
import { trackEvent } from "../analytics";
|
||||
import { hasBoundTextElement, isBoundToContainer } from "../element/typeChecks";
|
||||
import clsx from "clsx";
|
||||
import { actionToggleZenMode } from "../actions";
|
||||
|
||||
export const SelectedShapeActions = ({
|
||||
appState,
|
||||
elements,
|
||||
renderAction,
|
||||
activeTool,
|
||||
}: {
|
||||
appState: AppState;
|
||||
elements: readonly ExcalidrawElement[];
|
||||
renderAction: ActionManager["renderAction"];
|
||||
activeTool: AppState["activeTool"]["type"];
|
||||
}) => {
|
||||
const targetElements = getTargetElements(
|
||||
getNonDeletedElements(elements),
|
||||
@@ -56,13 +56,13 @@ export const SelectedShapeActions = ({
|
||||
const isRTL = document.documentElement.getAttribute("dir") === "rtl";
|
||||
|
||||
const showFillIcons =
|
||||
hasBackground(activeTool) ||
|
||||
hasBackground(appState.activeTool.type) ||
|
||||
targetElements.some(
|
||||
(element) =>
|
||||
hasBackground(element.type) && !isTransparent(element.backgroundColor),
|
||||
);
|
||||
const showChangeBackgroundIcons =
|
||||
hasBackground(activeTool) ||
|
||||
hasBackground(appState.activeTool.type) ||
|
||||
targetElements.some((element) => hasBackground(element.type));
|
||||
|
||||
const showLinkIcon =
|
||||
@@ -79,23 +79,23 @@ export const SelectedShapeActions = ({
|
||||
|
||||
return (
|
||||
<div className="panelColumn">
|
||||
{((hasStrokeColor(activeTool) &&
|
||||
activeTool !== "image" &&
|
||||
{((hasStrokeColor(appState.activeTool.type) &&
|
||||
appState.activeTool.type !== "image" &&
|
||||
commonSelectedType !== "image") ||
|
||||
targetElements.some((element) => hasStrokeColor(element.type))) &&
|
||||
renderAction("changeStrokeColor")}
|
||||
{showChangeBackgroundIcons && renderAction("changeBackgroundColor")}
|
||||
{showFillIcons && renderAction("changeFillStyle")}
|
||||
|
||||
{(hasStrokeWidth(activeTool) ||
|
||||
{(hasStrokeWidth(appState.activeTool.type) ||
|
||||
targetElements.some((element) => hasStrokeWidth(element.type))) &&
|
||||
renderAction("changeStrokeWidth")}
|
||||
|
||||
{(activeTool === "freedraw" ||
|
||||
{(appState.activeTool.type === "freedraw" ||
|
||||
targetElements.some((element) => element.type === "freedraw")) &&
|
||||
renderAction("changeStrokeShape")}
|
||||
|
||||
{(hasStrokeStyle(activeTool) ||
|
||||
{(hasStrokeStyle(appState.activeTool.type) ||
|
||||
targetElements.some((element) => hasStrokeStyle(element.type))) && (
|
||||
<>
|
||||
{renderAction("changeStrokeStyle")}
|
||||
@@ -103,12 +103,12 @@ export const SelectedShapeActions = ({
|
||||
</>
|
||||
)}
|
||||
|
||||
{(canChangeSharpness(activeTool) ||
|
||||
{(canChangeSharpness(appState.activeTool.type) ||
|
||||
targetElements.some((element) => canChangeSharpness(element.type))) && (
|
||||
<>{renderAction("changeSharpness")}</>
|
||||
)}
|
||||
|
||||
{(hasText(activeTool) ||
|
||||
{(hasText(appState.activeTool.type) ||
|
||||
targetElements.some((element) => hasText(element.type))) && (
|
||||
<>
|
||||
{renderAction("changeFontSize")}
|
||||
@@ -123,7 +123,7 @@ export const SelectedShapeActions = ({
|
||||
(element) =>
|
||||
hasBoundTextElement(element) || isBoundToContainer(element),
|
||||
) && renderAction("changeVerticalAlign")}
|
||||
{(canHaveArrowheads(activeTool) ||
|
||||
{(canHaveArrowheads(appState.activeTool.type) ||
|
||||
targetElements.some((element) => canHaveArrowheads(element.type))) && (
|
||||
<>{renderAction("changeArrowhead")}</>
|
||||
)}
|
||||
@@ -271,3 +271,45 @@ export const ZoomActions = ({
|
||||
</Stack.Row>
|
||||
</Stack.Col>
|
||||
);
|
||||
|
||||
export const UndoRedoActions = ({
|
||||
renderAction,
|
||||
className,
|
||||
}: {
|
||||
renderAction: ActionManager["renderAction"];
|
||||
className?: string;
|
||||
}) => (
|
||||
<div className={`undo-redo-buttons ${className}`}>
|
||||
{renderAction("undo", { size: "small" })}
|
||||
{renderAction("redo", { size: "small" })}
|
||||
</div>
|
||||
);
|
||||
|
||||
export const ExitZenModeAction = ({
|
||||
actionManager,
|
||||
showExitZenModeBtn,
|
||||
}: {
|
||||
actionManager: ActionManager;
|
||||
showExitZenModeBtn: boolean;
|
||||
}) => (
|
||||
<button
|
||||
className={clsx("disable-zen-mode", {
|
||||
"disable-zen-mode--visible": showExitZenModeBtn,
|
||||
})}
|
||||
onClick={() => actionManager.executeAction(actionToggleZenMode)}
|
||||
>
|
||||
{t("buttons.exitZenMode")}
|
||||
</button>
|
||||
);
|
||||
|
||||
export const FinalizeAction = ({
|
||||
renderAction,
|
||||
className,
|
||||
}: {
|
||||
renderAction: ActionManager["renderAction"];
|
||||
className?: string;
|
||||
}) => (
|
||||
<div className={`finalize-button ${className}`}>
|
||||
{renderAction("finalize", { size: "small" })}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -264,101 +264,6 @@ import {
|
||||
} from "../element/Hyperlink";
|
||||
import { shouldShowBoundingBox } from "../element/transformHandles";
|
||||
|
||||
let TIMES_AGGR: Record<string, { t: number; times: number[] }> = {};
|
||||
let TIMES_AVG: Record<
|
||||
string,
|
||||
{ t: number; times: number[]; avg: number | null }
|
||||
> = {};
|
||||
|
||||
window.DEBUG_LOG_TIMES = true;
|
||||
|
||||
let lastDebugLogCall = 0;
|
||||
let DEBUG_LOG_INTERVAL_ID: null | number = null;
|
||||
|
||||
const setupInterval = () => {
|
||||
if (DEBUG_LOG_INTERVAL_ID === null) {
|
||||
console.info("%c(starting perf recording)", "color: lime");
|
||||
DEBUG_LOG_INTERVAL_ID = window.setInterval(debugLogger, 1000);
|
||||
}
|
||||
lastDebugLogCall = Date.now();
|
||||
};
|
||||
|
||||
const lessPrecise = (num: number, precision = 5) =>
|
||||
parseFloat(num.toPrecision(precision));
|
||||
|
||||
const getAvgFrameTime = (times: number[]) =>
|
||||
lessPrecise(times.reduce((a, b) => a + b) / times.length);
|
||||
|
||||
const getFps = (frametime: number) => lessPrecise(1000 / frametime);
|
||||
|
||||
const debugLogger = () => {
|
||||
if (Date.now() - lastDebugLogCall > 600 && DEBUG_LOG_INTERVAL_ID !== null) {
|
||||
window.clearInterval(DEBUG_LOG_INTERVAL_ID);
|
||||
DEBUG_LOG_INTERVAL_ID = null;
|
||||
for (const [name, { avg }] of Object.entries(TIMES_AVG)) {
|
||||
if (avg != null) {
|
||||
console.info(
|
||||
`%c${name} run avg: ${avg}ms (${getFps(avg)} fps)`,
|
||||
"color: blue",
|
||||
);
|
||||
}
|
||||
}
|
||||
console.info("%c(stopping perf recording)", "color: red");
|
||||
TIMES_AGGR = {};
|
||||
TIMES_AVG = {};
|
||||
return;
|
||||
}
|
||||
if (window.DEBUG_LOG_TIMES) {
|
||||
for (const [name, { t, times }] of Object.entries(TIMES_AGGR)) {
|
||||
if (times.length) {
|
||||
console.info(
|
||||
name,
|
||||
lessPrecise(times.reduce((a, b) => a + b) / times.length),
|
||||
times.sort((a, b) => a - b).map((x) => lessPrecise(x)),
|
||||
);
|
||||
TIMES_AGGR[name] = { t, times: [] };
|
||||
}
|
||||
}
|
||||
for (const [name, { t, times, avg }] of Object.entries(TIMES_AVG)) {
|
||||
if (times.length) {
|
||||
const avgFrameTime = getAvgFrameTime(times);
|
||||
console.info(name, `${avgFrameTime}ms (${getFps(avgFrameTime)} fps)`);
|
||||
TIMES_AVG[name] = {
|
||||
t,
|
||||
times: [],
|
||||
avg:
|
||||
avg != null ? getAvgFrameTime([avg, avgFrameTime]) : avgFrameTime,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.logTime = (name: string, time?: number) => {
|
||||
setupInterval();
|
||||
const now = performance.now();
|
||||
const { t, times } = (TIMES_AGGR[name] = TIMES_AGGR[name] || {
|
||||
t: 0,
|
||||
times: [],
|
||||
});
|
||||
if (t) {
|
||||
times.push(time != null ? time : now - t);
|
||||
}
|
||||
TIMES_AGGR[name].t = now;
|
||||
};
|
||||
window.logTimeAverage = (name: string, time?: number) => {
|
||||
setupInterval();
|
||||
const now = performance.now();
|
||||
const { t, times } = (TIMES_AVG[name] = TIMES_AVG[name] || {
|
||||
t: 0,
|
||||
times: [],
|
||||
});
|
||||
if (t) {
|
||||
times.push(time != null ? time : now - t);
|
||||
}
|
||||
TIMES_AVG[name].t = now;
|
||||
};
|
||||
|
||||
const deviceContextInitialValue = {
|
||||
isSmScreen: false,
|
||||
isMobile: false,
|
||||
@@ -367,6 +272,7 @@ const deviceContextInitialValue = {
|
||||
};
|
||||
const DeviceContext = React.createContext<Device>(deviceContextInitialValue);
|
||||
export const useDevice = () => useContext<Device>(DeviceContext);
|
||||
|
||||
const ExcalidrawContainerContext = React.createContext<{
|
||||
container: HTMLDivElement | null;
|
||||
id: string | null;
|
||||
@@ -374,6 +280,22 @@ const ExcalidrawContainerContext = React.createContext<{
|
||||
export const useExcalidrawContainer = () =>
|
||||
useContext(ExcalidrawContainerContext);
|
||||
|
||||
const ExcalidrawElementsContext = React.createContext<
|
||||
readonly NonDeletedExcalidrawElement[]
|
||||
>([]);
|
||||
|
||||
const ExcalidrawAppStateContext = React.createContext<AppState>({
|
||||
...getDefaultAppState(),
|
||||
width: 0,
|
||||
height: 0,
|
||||
offsetLeft: 0,
|
||||
offsetTop: 0,
|
||||
});
|
||||
export const useExcalidrawElements = () =>
|
||||
useContext(ExcalidrawElementsContext);
|
||||
export const useExcalidrawAppState = () =>
|
||||
useContext(ExcalidrawAppStateContext);
|
||||
|
||||
let didTapTwice: boolean = false;
|
||||
let tappedTwiceTimer = 0;
|
||||
let cursorX = 0;
|
||||
@@ -571,26 +493,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
);
|
||||
}
|
||||
|
||||
private __renderUI = true;
|
||||
|
||||
private perfTest = (runs = 180, initial = true) => {
|
||||
if (initial) {
|
||||
console.time("perfTest");
|
||||
} else if (!runs) {
|
||||
console.timeEnd("perfTest");
|
||||
}
|
||||
if (runs) {
|
||||
requestAnimationFrame((id) => {
|
||||
for (const element of this.scene.getNonDeletedElements()) {
|
||||
mutateElement(element, {
|
||||
x: element.x + 1,
|
||||
});
|
||||
}
|
||||
this.perfTest(runs - 1, false);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
public render() {
|
||||
const selectedElement = getSelectedElements(
|
||||
this.scene.getNonDeletedElements(),
|
||||
@@ -620,65 +522,69 @@ class App extends React.Component<AppProps, AppState> {
|
||||
value={this.excalidrawContainerValue}
|
||||
>
|
||||
<DeviceContext.Provider value={this.device}>
|
||||
{this.__renderUI && (
|
||||
<LayerUI
|
||||
canvas={this.canvas}
|
||||
appState={this.state}
|
||||
files={this.files}
|
||||
setAppState={this.setAppState}
|
||||
actionManager={this.actionManager}
|
||||
elements={this.scene.getNonDeletedElements()}
|
||||
onCollabButtonClick={onCollabButtonClick}
|
||||
onLockToggle={this.toggleLock}
|
||||
onPenModeToggle={this.togglePenMode}
|
||||
onInsertElements={(elements) =>
|
||||
this.addElementsFromPasteOrLibrary({
|
||||
elements,
|
||||
position: "center",
|
||||
files: null,
|
||||
})
|
||||
}
|
||||
langCode={getLanguage().code}
|
||||
isCollaborating={this.props.isCollaborating}
|
||||
renderTopRightUI={renderTopRightUI}
|
||||
renderCustomFooter={renderFooter}
|
||||
renderCustomStats={renderCustomStats}
|
||||
showExitZenModeBtn={
|
||||
typeof this.props?.zenModeEnabled === "undefined" &&
|
||||
this.state.zenModeEnabled
|
||||
}
|
||||
showThemeBtn={
|
||||
typeof this.props?.theme === "undefined" &&
|
||||
this.props.UIOptions.canvasActions.theme
|
||||
}
|
||||
libraryReturnUrl={this.props.libraryReturnUrl}
|
||||
UIOptions={this.props.UIOptions}
|
||||
focusContainer={this.focusContainer}
|
||||
library={this.library}
|
||||
id={this.id}
|
||||
onImageAction={this.onImageAction}
|
||||
/>
|
||||
)}
|
||||
<div className="excalidraw-textEditorContainer" />
|
||||
<div className="excalidraw-contextMenuContainer" />
|
||||
{selectedElement.length === 1 && this.state.showHyperlinkPopup && (
|
||||
<Hyperlink
|
||||
key={selectedElement[0].id}
|
||||
element={selectedElement[0]}
|
||||
appState={this.state}
|
||||
setAppState={this.setAppState}
|
||||
onLinkOpen={this.props.onLinkOpen}
|
||||
/>
|
||||
)}
|
||||
{this.state.toast !== null && (
|
||||
<Toast
|
||||
message={this.state.toast.message}
|
||||
onClose={() => this.setToast(null)}
|
||||
duration={this.state.toast.duration}
|
||||
closable={this.state.toast.closable}
|
||||
/>
|
||||
)}
|
||||
<main>{this.renderCanvas()}</main>
|
||||
<ExcalidrawAppStateContext.Provider value={this.state}>
|
||||
<ExcalidrawElementsContext.Provider
|
||||
value={this.scene.getNonDeletedElements()}
|
||||
>
|
||||
<LayerUI
|
||||
canvas={this.canvas}
|
||||
appState={this.state}
|
||||
files={this.files}
|
||||
setAppState={this.setAppState}
|
||||
actionManager={this.actionManager}
|
||||
elements={this.scene.getNonDeletedElements()}
|
||||
onCollabButtonClick={onCollabButtonClick}
|
||||
onLockToggle={this.toggleLock}
|
||||
onPenModeToggle={this.togglePenMode}
|
||||
onInsertElements={(elements) =>
|
||||
this.addElementsFromPasteOrLibrary({
|
||||
elements,
|
||||
position: "center",
|
||||
files: null,
|
||||
})
|
||||
}
|
||||
langCode={getLanguage().code}
|
||||
isCollaborating={this.props.isCollaborating}
|
||||
renderTopRightUI={renderTopRightUI}
|
||||
renderCustomFooter={renderFooter}
|
||||
renderCustomStats={renderCustomStats}
|
||||
showExitZenModeBtn={
|
||||
typeof this.props?.zenModeEnabled === "undefined" &&
|
||||
this.state.zenModeEnabled
|
||||
}
|
||||
showThemeBtn={
|
||||
typeof this.props?.theme === "undefined" &&
|
||||
this.props.UIOptions.canvasActions.theme
|
||||
}
|
||||
libraryReturnUrl={this.props.libraryReturnUrl}
|
||||
UIOptions={this.props.UIOptions}
|
||||
focusContainer={this.focusContainer}
|
||||
library={this.library}
|
||||
id={this.id}
|
||||
onImageAction={this.onImageAction}
|
||||
/>
|
||||
<div className="excalidraw-textEditorContainer" />
|
||||
<div className="excalidraw-contextMenuContainer" />
|
||||
{selectedElement.length === 1 &&
|
||||
this.state.showHyperlinkPopup && (
|
||||
<Hyperlink
|
||||
key={selectedElement[0].id}
|
||||
element={selectedElement[0]}
|
||||
setAppState={this.setAppState}
|
||||
onLinkOpen={this.props.onLinkOpen}
|
||||
/>
|
||||
)}
|
||||
{this.state.toast !== null && (
|
||||
<Toast
|
||||
message={this.state.toast.message}
|
||||
onClose={() => this.setToast(null)}
|
||||
duration={this.state.toast.duration}
|
||||
closable={this.state.toast.closable}
|
||||
/>
|
||||
)}
|
||||
<main>{this.renderCanvas()}</main>
|
||||
</ExcalidrawElementsContext.Provider>{" "}
|
||||
</ExcalidrawAppStateContext.Provider>
|
||||
</DeviceContext.Provider>
|
||||
</ExcalidrawContainerContext.Provider>
|
||||
</div>
|
||||
@@ -937,8 +843,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
|
||||
if (
|
||||
process.env.NODE_ENV === ENV.TEST ||
|
||||
process.env.NODE_ENV === ENV.DEVELOPMENT ||
|
||||
process.env.REACT_APP_VERCEL_ENV === "preview"
|
||||
process.env.NODE_ENV === ENV.DEVELOPMENT
|
||||
) {
|
||||
const setState = this.setState.bind(this);
|
||||
Object.defineProperties(window.h, {
|
||||
@@ -2002,6 +1907,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||
editingLinearElement: new LinearElementEditor(
|
||||
selectedElements[0],
|
||||
this.scene,
|
||||
this.state,
|
||||
true,
|
||||
),
|
||||
});
|
||||
}
|
||||
@@ -2580,6 +2487,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||
editingLinearElement: new LinearElementEditor(
|
||||
selectedElements[0],
|
||||
this.scene,
|
||||
this.state,
|
||||
true,
|
||||
),
|
||||
});
|
||||
}
|
||||
@@ -2813,18 +2722,23 @@ class App extends React.Component<AppProps, AppState> {
|
||||
event,
|
||||
scenePointerX,
|
||||
scenePointerY,
|
||||
this.state.editingLinearElement,
|
||||
this.state.gridSize,
|
||||
this.state,
|
||||
);
|
||||
if (editingLinearElement !== this.state.editingLinearElement) {
|
||||
|
||||
if (
|
||||
editingLinearElement &&
|
||||
editingLinearElement !== this.state.editingLinearElement
|
||||
) {
|
||||
// Since we are reading from previous state which is not possible with
|
||||
// automatic batching in React 18 hence using flush sync to synchronously
|
||||
// update the state. Check https://github.com/excalidraw/excalidraw/pull/5508 for more details.
|
||||
flushSync(() => {
|
||||
this.setState({ editingLinearElement });
|
||||
this.setState({
|
||||
editingLinearElement,
|
||||
});
|
||||
});
|
||||
}
|
||||
if (editingLinearElement.lastUncommittedPoint != null) {
|
||||
if (editingLinearElement?.lastUncommittedPoint != null) {
|
||||
this.maybeSuggestBindingAtCursor(scenePointer);
|
||||
} else {
|
||||
this.setState({ suggestedBindings: [] });
|
||||
@@ -3153,7 +3067,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}
|
||||
if (this.state.selectedLinearElement) {
|
||||
let hoverPointIndex = -1;
|
||||
let midPointHovered = false;
|
||||
let segmentMidPointHoveredCoords = null;
|
||||
if (
|
||||
isHittingElementNotConsideringBoundingBox(element, this.state, [
|
||||
scenePointerX,
|
||||
@@ -3161,18 +3075,19 @@ class App extends React.Component<AppProps, AppState> {
|
||||
])
|
||||
) {
|
||||
hoverPointIndex = LinearElementEditor.getPointIndexUnderCursor(
|
||||
element,
|
||||
this.state.selectedLinearElement,
|
||||
this.state.zoom,
|
||||
scenePointerX,
|
||||
scenePointerY,
|
||||
);
|
||||
midPointHovered = LinearElementEditor.isHittingMidPoint(
|
||||
linearElementEditor,
|
||||
{ x: scenePointerX, y: scenePointerY },
|
||||
this.state,
|
||||
);
|
||||
segmentMidPointHoveredCoords =
|
||||
LinearElementEditor.getSegmentMidpointHitCoords(
|
||||
linearElementEditor,
|
||||
{ x: scenePointerX, y: scenePointerY },
|
||||
this.state,
|
||||
);
|
||||
|
||||
if (hoverPointIndex >= 0 || midPointHovered) {
|
||||
if (hoverPointIndex >= 0 || segmentMidPointHoveredCoords) {
|
||||
setCursor(this.canvas, CURSOR_TYPE.POINTER);
|
||||
} else {
|
||||
setCursor(this.canvas, CURSOR_TYPE.MOVE);
|
||||
@@ -3201,12 +3116,15 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}
|
||||
|
||||
if (
|
||||
this.state.selectedLinearElement.midPointHovered !== midPointHovered
|
||||
!LinearElementEditor.arePointsEqual(
|
||||
this.state.selectedLinearElement.segmentMidPointHoveredCoords,
|
||||
segmentMidPointHoveredCoords,
|
||||
)
|
||||
) {
|
||||
this.setState({
|
||||
selectedLinearElement: {
|
||||
...this.state.selectedLinearElement,
|
||||
midPointHovered,
|
||||
segmentMidPointHoveredCoords,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -4574,6 +4492,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
? new LinearElementEditor(
|
||||
elementsWithinSelection[0],
|
||||
this.scene,
|
||||
this.state,
|
||||
)
|
||||
: null,
|
||||
},
|
||||
@@ -4838,6 +4757,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
selectedLinearElement: new LinearElementEditor(
|
||||
draggingElement,
|
||||
this.scene,
|
||||
this.state,
|
||||
),
|
||||
}));
|
||||
} else {
|
||||
@@ -4905,6 +4825,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
selectedLinearElement: new LinearElementEditor(
|
||||
hitElement,
|
||||
this.scene,
|
||||
this.state,
|
||||
),
|
||||
});
|
||||
}
|
||||
@@ -5007,6 +4928,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
? new LinearElementEditor(
|
||||
newSelectedElements[0],
|
||||
this.scene,
|
||||
this.state,
|
||||
)
|
||||
: prevState.selectedLinearElement,
|
||||
},
|
||||
@@ -5035,7 +4957,11 @@ class App extends React.Component<AppProps, AppState> {
|
||||
// Don't set `selectedLinearElement` if its same as the hitElement, this is mainly to prevent resetting the `hoverPointIndex` to -1.
|
||||
// Future we should update the API to take care of setting the correct `hoverPointIndex` when initialized
|
||||
prevState.selectedLinearElement?.elementId !== hitElement.id
|
||||
? new LinearElementEditor(hitElement, this.scene)
|
||||
? new LinearElementEditor(
|
||||
hitElement,
|
||||
this.scene,
|
||||
this.state,
|
||||
)
|
||||
: prevState.selectedLinearElement,
|
||||
},
|
||||
this.scene.getNonDeletedElements(),
|
||||
@@ -5795,7 +5721,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
...this.state,
|
||||
selectedElementIds: { [element.id]: true },
|
||||
selectedLinearElement: isLinearElement(element)
|
||||
? new LinearElementEditor(element, this.scene)
|
||||
? new LinearElementEditor(element, this.scene, this.state)
|
||||
: null,
|
||||
},
|
||||
this.scene.getNonDeletedElements(),
|
||||
@@ -6321,8 +6247,7 @@ declare global {
|
||||
|
||||
if (
|
||||
process.env.NODE_ENV === ENV.TEST ||
|
||||
process.env.NODE_ENV === ENV.DEVELOPMENT ||
|
||||
process.env.REACT_APP_VERCEL_ENV === "preview"
|
||||
process.env.NODE_ENV === ENV.DEVELOPMENT
|
||||
) {
|
||||
window.h = window.h || ({} as Window["h"]);
|
||||
|
||||
|
||||
@@ -343,6 +343,8 @@ const ColorInput = React.forwardRef(
|
||||
},
|
||||
);
|
||||
|
||||
ColorInput.displayName = "ColorInput";
|
||||
|
||||
export const ColorPicker = ({
|
||||
type,
|
||||
color,
|
||||
|
||||
106
src/components/Footer.tsx
Normal file
106
src/components/Footer.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import clsx from "clsx";
|
||||
import { ActionManager } from "../actions/manager";
|
||||
import { AppState, ExcalidrawProps } from "../types";
|
||||
import {
|
||||
ExitZenModeAction,
|
||||
FinalizeAction,
|
||||
UndoRedoActions,
|
||||
ZoomActions,
|
||||
} from "./Actions";
|
||||
import { useDevice } from "./App";
|
||||
import { Island } from "./Island";
|
||||
import { Section } from "./Section";
|
||||
import Stack from "./Stack";
|
||||
|
||||
const Footer = ({
|
||||
appState,
|
||||
actionManager,
|
||||
renderCustomFooter,
|
||||
showExitZenModeBtn,
|
||||
}: {
|
||||
appState: AppState;
|
||||
actionManager: ActionManager;
|
||||
renderCustomFooter?: ExcalidrawProps["renderFooter"];
|
||||
showExitZenModeBtn: boolean;
|
||||
}) => {
|
||||
const device = useDevice();
|
||||
const showFinalize =
|
||||
!appState.viewModeEnabled && appState.multiElement && device.isTouchScreen;
|
||||
return (
|
||||
<footer
|
||||
role="contentinfo"
|
||||
className="layer-ui__wrapper__footer App-menu App-menu_bottom"
|
||||
>
|
||||
<div
|
||||
className={clsx("layer-ui__wrapper__footer-left zen-mode-transition", {
|
||||
"layer-ui__wrapper__footer-left--transition-left":
|
||||
appState.zenModeEnabled,
|
||||
})}
|
||||
>
|
||||
<Stack.Col gap={2}>
|
||||
<Section heading="canvasActions">
|
||||
<Island padding={1}>
|
||||
<ZoomActions
|
||||
renderAction={actionManager.renderAction}
|
||||
zoom={appState.zoom}
|
||||
/>
|
||||
</Island>
|
||||
{!appState.viewModeEnabled && (
|
||||
<>
|
||||
<UndoRedoActions
|
||||
renderAction={actionManager.renderAction}
|
||||
className={clsx("zen-mode-transition", {
|
||||
"layer-ui__wrapper__footer-left--transition-bottom":
|
||||
appState.zenModeEnabled,
|
||||
})}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={clsx("eraser-buttons zen-mode-transition", {
|
||||
"layer-ui__wrapper__footer-left--transition-left":
|
||||
appState.zenModeEnabled,
|
||||
})}
|
||||
>
|
||||
{actionManager.renderAction("eraser", { size: "small" })}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{showFinalize && (
|
||||
<FinalizeAction
|
||||
renderAction={actionManager.renderAction}
|
||||
className={clsx("zen-mode-transition", {
|
||||
"layer-ui__wrapper__footer-left--transition-left":
|
||||
appState.zenModeEnabled,
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</Section>
|
||||
</Stack.Col>
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
"layer-ui__wrapper__footer-center zen-mode-transition",
|
||||
{
|
||||
"layer-ui__wrapper__footer-left--transition-bottom":
|
||||
appState.zenModeEnabled,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{renderCustomFooter?.(false, appState)}
|
||||
</div>
|
||||
<div
|
||||
className={clsx("layer-ui__wrapper__footer-right zen-mode-transition", {
|
||||
"transition-right disable-pointerEvents": appState.zenModeEnabled,
|
||||
})}
|
||||
>
|
||||
{actionManager.renderAction("toggleShortcuts")}
|
||||
</div>
|
||||
<ExitZenModeAction
|
||||
actionManager={actionManager}
|
||||
showExitZenModeBtn={showExitZenModeBtn}
|
||||
/>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
@@ -10,7 +10,7 @@ import { calculateScrollCenter, getSelectedElements } from "../scene";
|
||||
import { ExportType } from "../scene/types";
|
||||
import { AppProps, AppState, ExcalidrawProps, BinaryFiles } from "../types";
|
||||
import { muteFSAbortError } from "../utils";
|
||||
import { SelectedShapeActions, ShapesSwitcher, ZoomActions } from "./Actions";
|
||||
import { SelectedShapeActions, ShapesSwitcher } from "./Actions";
|
||||
import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
|
||||
import CollabButton from "./CollabButton";
|
||||
import { ErrorDialog } from "./ErrorDialog";
|
||||
@@ -39,7 +39,7 @@ import { trackEvent } from "../analytics";
|
||||
import { useDevice } from "../components/App";
|
||||
import { Stats } from "./Stats";
|
||||
import { actionToggleStats } from "../actions/actionToggleStats";
|
||||
import { actionToggleZenMode } from "../actions";
|
||||
import Footer from "./Footer";
|
||||
|
||||
interface LayerUIProps {
|
||||
actionManager: ActionManager;
|
||||
@@ -71,8 +71,8 @@ const LayerUI = ({
|
||||
appState,
|
||||
files,
|
||||
setAppState,
|
||||
canvas,
|
||||
elements,
|
||||
canvas,
|
||||
onCollabButtonClick,
|
||||
onLockToggle,
|
||||
onPenModeToggle,
|
||||
@@ -210,8 +210,8 @@ const LayerUI = ({
|
||||
)}
|
||||
</Stack.Row>
|
||||
<BackgroundPickerAndDarkModeToggle
|
||||
actionManager={actionManager}
|
||||
appState={appState}
|
||||
actionManager={actionManager}
|
||||
setAppState={setAppState}
|
||||
showThemeBtn={showThemeBtn}
|
||||
/>
|
||||
@@ -244,7 +244,6 @@ const LayerUI = ({
|
||||
appState={appState}
|
||||
elements={elements}
|
||||
renderAction={actionManager.renderAction}
|
||||
activeTool={appState.activeTool.type}
|
||||
/>
|
||||
</Island>
|
||||
</Section>
|
||||
@@ -279,7 +278,6 @@ const LayerUI = ({
|
||||
libraryReturnUrl={libraryReturnUrl}
|
||||
focusContainer={focusContainer}
|
||||
library={library}
|
||||
theme={appState.theme}
|
||||
files={files}
|
||||
id={id}
|
||||
appState={appState}
|
||||
@@ -383,100 +381,7 @@ const LayerUI = ({
|
||||
);
|
||||
};
|
||||
|
||||
const renderBottomAppMenu = () => {
|
||||
return (
|
||||
<footer
|
||||
role="contentinfo"
|
||||
className="layer-ui__wrapper__footer App-menu App-menu_bottom"
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
"layer-ui__wrapper__footer-left zen-mode-transition",
|
||||
{
|
||||
"layer-ui__wrapper__footer-left--transition-left":
|
||||
appState.zenModeEnabled,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<Stack.Col gap={2}>
|
||||
<Section heading="canvasActions">
|
||||
<Island padding={1}>
|
||||
<ZoomActions
|
||||
renderAction={actionManager.renderAction}
|
||||
zoom={appState.zoom}
|
||||
/>
|
||||
</Island>
|
||||
{!appState.viewModeEnabled && (
|
||||
<>
|
||||
<div
|
||||
className={clsx("undo-redo-buttons zen-mode-transition", {
|
||||
"layer-ui__wrapper__footer-left--transition-bottom":
|
||||
appState.zenModeEnabled,
|
||||
})}
|
||||
>
|
||||
{actionManager.renderAction("undo", { size: "small" })}
|
||||
{actionManager.renderAction("redo", { size: "small" })}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={clsx("eraser-buttons zen-mode-transition", {
|
||||
"layer-ui__wrapper__footer-left--transition-left":
|
||||
appState.zenModeEnabled,
|
||||
})}
|
||||
>
|
||||
{actionManager.renderAction("eraser", { size: "small" })}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{!appState.viewModeEnabled &&
|
||||
appState.multiElement &&
|
||||
device.isTouchScreen && (
|
||||
<div
|
||||
className={clsx("finalize-button zen-mode-transition", {
|
||||
"layer-ui__wrapper__footer-left--transition-left":
|
||||
appState.zenModeEnabled,
|
||||
})}
|
||||
>
|
||||
{actionManager.renderAction("finalize", { size: "small" })}
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
</Stack.Col>
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
"layer-ui__wrapper__footer-center zen-mode-transition",
|
||||
{
|
||||
"layer-ui__wrapper__footer-left--transition-bottom":
|
||||
appState.zenModeEnabled,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{renderCustomFooter?.(false, appState)}
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
"layer-ui__wrapper__footer-right zen-mode-transition",
|
||||
{
|
||||
"transition-right disable-pointerEvents": appState.zenModeEnabled,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{actionManager.renderAction("toggleShortcuts")}
|
||||
</div>
|
||||
<button
|
||||
className={clsx("disable-zen-mode", {
|
||||
"disable-zen-mode--visible": showExitZenModeBtn,
|
||||
})}
|
||||
onClick={() => actionManager.executeAction(actionToggleZenMode)}
|
||||
>
|
||||
{t("buttons.exitZenMode")}
|
||||
</button>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
const dialogs = (
|
||||
return (
|
||||
<>
|
||||
{appState.isLoading && <LoadingMessage delay={250} />}
|
||||
{appState.errorMessage && (
|
||||
@@ -504,86 +409,81 @@ const LayerUI = ({
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
{device.isMobile && (
|
||||
<MobileMenu
|
||||
appState={appState}
|
||||
elements={elements}
|
||||
actionManager={actionManager}
|
||||
libraryMenu={libraryMenu}
|
||||
renderJSONExportDialog={renderJSONExportDialog}
|
||||
renderImageExportDialog={renderImageExportDialog}
|
||||
setAppState={setAppState}
|
||||
onCollabButtonClick={onCollabButtonClick}
|
||||
onLockToggle={() => onLockToggle()}
|
||||
onPenModeToggle={onPenModeToggle}
|
||||
canvas={canvas}
|
||||
isCollaborating={isCollaborating}
|
||||
renderCustomFooter={renderCustomFooter}
|
||||
showThemeBtn={showThemeBtn}
|
||||
onImageAction={onImageAction}
|
||||
renderTopRightUI={renderTopRightUI}
|
||||
renderCustomStats={renderCustomStats}
|
||||
/>
|
||||
)}
|
||||
|
||||
const renderStats = () => {
|
||||
if (!appState.showStats) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Stats
|
||||
appState={appState}
|
||||
setAppState={setAppState}
|
||||
elements={elements}
|
||||
onClose={() => {
|
||||
actionManager.executeAction(actionToggleStats);
|
||||
}}
|
||||
renderCustomStats={renderCustomStats}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return device.isMobile ? (
|
||||
<>
|
||||
{dialogs}
|
||||
<MobileMenu
|
||||
appState={appState}
|
||||
elements={elements}
|
||||
actionManager={actionManager}
|
||||
libraryMenu={libraryMenu}
|
||||
renderJSONExportDialog={renderJSONExportDialog}
|
||||
renderImageExportDialog={renderImageExportDialog}
|
||||
setAppState={setAppState}
|
||||
onCollabButtonClick={onCollabButtonClick}
|
||||
onLockToggle={() => onLockToggle()}
|
||||
onPenModeToggle={onPenModeToggle}
|
||||
canvas={canvas}
|
||||
isCollaborating={isCollaborating}
|
||||
renderCustomFooter={renderCustomFooter}
|
||||
showThemeBtn={showThemeBtn}
|
||||
onImageAction={onImageAction}
|
||||
renderTopRightUI={renderTopRightUI}
|
||||
renderStats={renderStats}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div
|
||||
className={clsx("layer-ui__wrapper", {
|
||||
"disable-pointerEvents":
|
||||
appState.draggingElement ||
|
||||
appState.resizingElement ||
|
||||
(appState.editingElement &&
|
||||
!isTextElement(appState.editingElement)),
|
||||
})}
|
||||
style={
|
||||
appState.isLibraryOpen &&
|
||||
appState.isLibraryMenuDocked &&
|
||||
device.canDeviceFitSidebar
|
||||
? { width: `calc(100% - ${LIBRARY_SIDEBAR_WIDTH}px)` }
|
||||
: {}
|
||||
}
|
||||
>
|
||||
{dialogs}
|
||||
{renderFixedSideContainer()}
|
||||
{renderBottomAppMenu()}
|
||||
{renderStats()}
|
||||
{appState.scrolledOutside && (
|
||||
<button
|
||||
className="scroll-back-to-content"
|
||||
onClick={() => {
|
||||
setAppState({
|
||||
...calculateScrollCenter(elements, appState, canvas),
|
||||
});
|
||||
}}
|
||||
{!device.isMobile && (
|
||||
<>
|
||||
<div
|
||||
className={clsx("layer-ui__wrapper", {
|
||||
"disable-pointerEvents":
|
||||
appState.draggingElement ||
|
||||
appState.resizingElement ||
|
||||
(appState.editingElement &&
|
||||
!isTextElement(appState.editingElement)),
|
||||
})}
|
||||
style={
|
||||
appState.isLibraryOpen &&
|
||||
appState.isLibraryMenuDocked &&
|
||||
device.canDeviceFitSidebar
|
||||
? { width: `calc(100% - ${LIBRARY_SIDEBAR_WIDTH}px)` }
|
||||
: {}
|
||||
}
|
||||
>
|
||||
{t("buttons.scrollBackToContent")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{appState.isLibraryOpen && (
|
||||
<div className="layer-ui__sidebar">{libraryMenu}</div>
|
||||
{renderFixedSideContainer()}
|
||||
<Footer
|
||||
appState={appState}
|
||||
actionManager={actionManager}
|
||||
renderCustomFooter={renderCustomFooter}
|
||||
showExitZenModeBtn={showExitZenModeBtn}
|
||||
/>
|
||||
{appState.showStats && (
|
||||
<Stats
|
||||
appState={appState}
|
||||
setAppState={setAppState}
|
||||
elements={elements}
|
||||
onClose={() => {
|
||||
actionManager.executeAction(actionToggleStats);
|
||||
}}
|
||||
renderCustomStats={renderCustomStats}
|
||||
/>
|
||||
)}
|
||||
{appState.scrolledOutside && (
|
||||
<button
|
||||
className="scroll-back-to-content"
|
||||
onClick={() => {
|
||||
setAppState({
|
||||
...calculateScrollCenter(elements, appState, canvas),
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("buttons.scrollBackToContent")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{appState.isLibraryOpen && (
|
||||
<div className="layer-ui__sidebar">{libraryMenu}</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -80,7 +80,6 @@ export const LibraryMenu = ({
|
||||
onInsertLibraryItems,
|
||||
pendingElements,
|
||||
onAddToLibrary,
|
||||
theme,
|
||||
setAppState,
|
||||
files,
|
||||
libraryReturnUrl,
|
||||
@@ -93,7 +92,6 @@ export const LibraryMenu = ({
|
||||
onClose: () => void;
|
||||
onInsertLibraryItems: (libraryItems: LibraryItems) => void;
|
||||
onAddToLibrary: () => void;
|
||||
theme: AppState["theme"];
|
||||
files: BinaryFiles;
|
||||
setAppState: React.Component<any, AppState>["setState"];
|
||||
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
|
||||
@@ -105,12 +103,12 @@ export const LibraryMenu = ({
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const device = useDevice();
|
||||
|
||||
useOnClickOutside(
|
||||
ref,
|
||||
useCallback(
|
||||
(event) => {
|
||||
// If click on the library icon, do nothing.
|
||||
// If click on the library icon, do nothing so that LibraryButton
|
||||
// can toggle library menu
|
||||
if ((event.target as Element).closest(".ToolIcon__library")) {
|
||||
return;
|
||||
}
|
||||
@@ -290,7 +288,7 @@ export const LibraryMenu = ({
|
||||
appState={appState}
|
||||
libraryReturnUrl={libraryReturnUrl}
|
||||
library={library}
|
||||
theme={theme}
|
||||
theme={appState.theme}
|
||||
files={files}
|
||||
id={id}
|
||||
selectedItems={selectedItems}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { chunk } from "lodash";
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { saveLibraryAsJSON, serializeLibraryAsJSON } from "../data/json";
|
||||
import Library from "../data/library";
|
||||
@@ -11,7 +10,7 @@ import {
|
||||
LibraryItem,
|
||||
LibraryItems,
|
||||
} from "../types";
|
||||
import { arrayToMap, muteFSAbortError } from "../utils";
|
||||
import { arrayToMap, chunk, muteFSAbortError } from "../utils";
|
||||
import { useDevice } from "./App";
|
||||
import ConfirmDialog from "./ConfirmDialog";
|
||||
import { close, exportToFileIcon, load, publishIcon, trash } from "./icons";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import { AppState } from "../types";
|
||||
import { AppState, ExcalidrawProps } from "../types";
|
||||
import { ActionManager } from "../actions/manager";
|
||||
import { t } from "../i18n";
|
||||
import Stack from "./Stack";
|
||||
@@ -18,6 +18,8 @@ import { UserList } from "./UserList";
|
||||
import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
|
||||
import { LibraryButton } from "./LibraryButton";
|
||||
import { PenModeButton } from "./PenModeButton";
|
||||
import { Stats } from "./Stats";
|
||||
import { actionToggleStats } from "../actions";
|
||||
|
||||
type MobileMenuProps = {
|
||||
appState: AppState;
|
||||
@@ -42,7 +44,7 @@ type MobileMenuProps = {
|
||||
isMobile: boolean,
|
||||
appState: AppState,
|
||||
) => JSX.Element | null;
|
||||
renderStats: () => JSX.Element | null;
|
||||
renderCustomStats?: ExcalidrawProps["renderCustomStats"];
|
||||
};
|
||||
|
||||
export const MobileMenu = ({
|
||||
@@ -62,7 +64,7 @@ export const MobileMenu = ({
|
||||
showThemeBtn,
|
||||
onImageAction,
|
||||
renderTopRightUI,
|
||||
renderStats,
|
||||
renderCustomStats,
|
||||
}: MobileMenuProps) => {
|
||||
const renderToolbar = () => {
|
||||
return (
|
||||
@@ -119,7 +121,6 @@ export const MobileMenu = ({
|
||||
const renderAppToolbar = () => {
|
||||
// Render eraser conditionally in mobile
|
||||
const showEraser =
|
||||
!appState.viewModeEnabled &&
|
||||
!appState.editingElement &&
|
||||
getSelectedElements(elements, appState).length === 0;
|
||||
|
||||
@@ -138,11 +139,11 @@ export const MobileMenu = ({
|
||||
|
||||
{actionManager.renderAction("undo")}
|
||||
{actionManager.renderAction("redo")}
|
||||
{showEraser && actionManager.renderAction("eraser")}
|
||||
|
||||
{actionManager.renderAction(
|
||||
appState.multiElement ? "finalize" : "duplicateSelection",
|
||||
)}
|
||||
{showEraser
|
||||
? actionManager.renderAction("eraser")
|
||||
: actionManager.renderAction(
|
||||
appState.multiElement ? "finalize" : "duplicateSelection",
|
||||
)}
|
||||
{actionManager.renderAction("deleteSelectedElements")}
|
||||
</div>
|
||||
);
|
||||
@@ -184,7 +185,17 @@ export const MobileMenu = ({
|
||||
return (
|
||||
<>
|
||||
{!appState.viewModeEnabled && renderToolbar()}
|
||||
{renderStats()}
|
||||
{!appState.openMenu && appState.showStats && (
|
||||
<Stats
|
||||
appState={appState}
|
||||
setAppState={setAppState}
|
||||
elements={elements}
|
||||
onClose={() => {
|
||||
actionManager.executeAction(actionToggleStats);
|
||||
}}
|
||||
renderCustomStats={renderCustomStats}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className="App-bottom-bar"
|
||||
style={{
|
||||
@@ -221,7 +232,6 @@ export const MobileMenu = ({
|
||||
appState={appState}
|
||||
elements={elements}
|
||||
renderAction={actionManager.renderAction}
|
||||
activeTool={appState.activeTool.type}
|
||||
/>
|
||||
</Section>
|
||||
) : null}
|
||||
|
||||
@@ -2,7 +2,6 @@ import React from "react";
|
||||
import { getCommonBounds } from "../element/bounds";
|
||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||
import { t } from "../i18n";
|
||||
import { useDevice } from "../components/App";
|
||||
import { getTargetElements } from "../scene";
|
||||
import { AppState, ExcalidrawProps } from "../types";
|
||||
import { close } from "./icons";
|
||||
@@ -16,13 +15,10 @@ export const Stats = (props: {
|
||||
onClose: () => void;
|
||||
renderCustomStats: ExcalidrawProps["renderCustomStats"];
|
||||
}) => {
|
||||
const device = useDevice();
|
||||
const boundingBox = getCommonBounds(props.elements);
|
||||
const selectedElements = getTargetElements(props.elements, props.appState);
|
||||
const selectedBoundingBox = getCommonBounds(selectedElements);
|
||||
if (device.isMobile && props.appState.openMenu) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="Stats">
|
||||
<Island padding={2}>
|
||||
|
||||
@@ -187,3 +187,5 @@ ToolButton.defaultProps = {
|
||||
className: "",
|
||||
size: "medium",
|
||||
};
|
||||
|
||||
ToolButton.displayName = "ToolButton";
|
||||
|
||||
@@ -356,7 +356,7 @@ export const getFileHandle = async (
|
||||
};
|
||||
|
||||
/**
|
||||
* attemps to detect if a buffer is a valid image by checking its leading bytes
|
||||
* attempts to detect if a buffer is a valid image by checking its leading bytes
|
||||
*/
|
||||
const getActualMimeTypeFromImage = (buffer: ArrayBuffer) => {
|
||||
let mimeType: ValueOf<Pick<typeof MIME_TYPES, "png" | "jpg" | "gif">> | null =
|
||||
@@ -396,7 +396,7 @@ export const createFile = (
|
||||
});
|
||||
};
|
||||
|
||||
/** attemps to detect correct mimeType if none is set, or if an image
|
||||
/** attempts to detect correct mimeType if none is set, or if an image
|
||||
* has an incorrect extension.
|
||||
* Note: doesn't handle missing .excalidraw/.excalidrawlib extension */
|
||||
export const normalizeFile = async (file: File) => {
|
||||
|
||||
@@ -32,6 +32,7 @@ import { getElementAbsoluteCoords } from "./";
|
||||
|
||||
import "./Hyperlink.scss";
|
||||
import { trackEvent } from "../analytics";
|
||||
import { useExcalidrawAppState } from "../components/App";
|
||||
|
||||
const CONTAINER_WIDTH = 320;
|
||||
const SPACE_BOTTOM = 85;
|
||||
@@ -48,15 +49,15 @@ let IS_HYPERLINK_TOOLTIP_VISIBLE = false;
|
||||
|
||||
export const Hyperlink = ({
|
||||
element,
|
||||
appState,
|
||||
setAppState,
|
||||
onLinkOpen,
|
||||
}: {
|
||||
element: NonDeletedExcalidrawElement;
|
||||
appState: AppState;
|
||||
setAppState: React.Component<any, AppState>["setState"];
|
||||
onLinkOpen: ExcalidrawProps["onLinkOpen"];
|
||||
}) => {
|
||||
const appState = useExcalidrawAppState();
|
||||
|
||||
const linkVal = element.link || "";
|
||||
|
||||
const [inputVal, setInputVal] = useState(linkVal);
|
||||
|
||||
@@ -107,12 +107,19 @@ const solveQuadratic = (
|
||||
return false;
|
||||
}
|
||||
|
||||
const t1 = (-b + Math.sqrt(sqrtPart)) / (2 * a);
|
||||
const t2 = (-b - Math.sqrt(sqrtPart)) / (2 * a);
|
||||
|
||||
let s1 = null;
|
||||
let s2 = null;
|
||||
|
||||
let t1 = Infinity;
|
||||
let t2 = Infinity;
|
||||
|
||||
if (a === 0) {
|
||||
t1 = t2 = -c / b;
|
||||
} else {
|
||||
t1 = (-b + Math.sqrt(sqrtPart)) / (2 * a);
|
||||
t2 = (-b - Math.sqrt(sqrtPart)) / (2 * a);
|
||||
}
|
||||
|
||||
if (t1 >= 0 && t1 <= 1) {
|
||||
s1 = getBezierValueForT(t1, p0, p1, p2, p3);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,11 @@ import {
|
||||
getGridPoint,
|
||||
rotatePoint,
|
||||
centerPoint,
|
||||
getControlPointsForBezierCurve,
|
||||
getBezierXY,
|
||||
getBezierCurveLength,
|
||||
mapIntervalToBezierT,
|
||||
arePointsEqual,
|
||||
} from "../math";
|
||||
import { getElementAbsoluteCoords, getLockedLinearCursorAlignSize } from ".";
|
||||
import { getElementPointsCoords } from "./bounds";
|
||||
@@ -29,6 +34,17 @@ import { tupleToCoors } from "../utils";
|
||||
import { isBindingElement } from "./typeChecks";
|
||||
import { shouldRotateWithDiscreteAngle } from "../keys";
|
||||
|
||||
const editorMidPointsCache: {
|
||||
version: number | null;
|
||||
points: (Point | null)[];
|
||||
zoom: number | null;
|
||||
} = { version: null, points: [], zoom: null };
|
||||
|
||||
const visiblePointIndexesCache: {
|
||||
points: number[];
|
||||
zoom: number | null;
|
||||
isEditingLinearElement: boolean;
|
||||
} = { points: [], zoom: null, isEditingLinearElement: false };
|
||||
export class LinearElementEditor {
|
||||
public readonly elementId: ExcalidrawElement["id"] & {
|
||||
_brand: "excalidrawLinearElementId";
|
||||
@@ -52,9 +68,14 @@ export class LinearElementEditor {
|
||||
| "keep";
|
||||
public readonly endBindingElement: ExcalidrawBindableElement | null | "keep";
|
||||
public readonly hoverPointIndex: number;
|
||||
public readonly midPointHovered: boolean;
|
||||
public readonly segmentMidPointHoveredCoords: Point | null;
|
||||
|
||||
constructor(element: NonDeleted<ExcalidrawLinearElement>, scene: Scene) {
|
||||
constructor(
|
||||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
scene: Scene,
|
||||
appState: AppState,
|
||||
editingLinearElement = false,
|
||||
) {
|
||||
this.elementId = element.id as string & {
|
||||
_brand: "excalidrawLinearElementId";
|
||||
};
|
||||
@@ -72,7 +93,7 @@ export class LinearElementEditor {
|
||||
lastClickedPoint: -1,
|
||||
};
|
||||
this.hoverPointIndex = -1;
|
||||
this.midPointHovered = false;
|
||||
this.segmentMidPointHoveredCoords = null;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -80,7 +101,6 @@ export class LinearElementEditor {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static POINT_HANDLE_SIZE = 10;
|
||||
|
||||
/**
|
||||
* @param id the `elementId` from the instance of this class (so that we can
|
||||
* statically guarantee this method returns an ExcalidrawLinearElement)
|
||||
@@ -359,7 +379,60 @@ export class LinearElementEditor {
|
||||
};
|
||||
}
|
||||
|
||||
static isHittingMidPoint = (
|
||||
static getEditorMidPoints = (
|
||||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
appState: AppState,
|
||||
): typeof editorMidPointsCache["points"] => {
|
||||
// Since its not needed outside editor unless 2 pointer lines
|
||||
if (!appState.editingLinearElement && element.points.length > 2) {
|
||||
return [];
|
||||
}
|
||||
if (
|
||||
editorMidPointsCache.version === element.version &&
|
||||
editorMidPointsCache.zoom === appState.zoom.value
|
||||
) {
|
||||
return editorMidPointsCache.points;
|
||||
}
|
||||
LinearElementEditor.updateEditorMidPointsCache(element, appState);
|
||||
return editorMidPointsCache.points!;
|
||||
};
|
||||
|
||||
static updateEditorMidPointsCache = (
|
||||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
appState: AppState,
|
||||
) => {
|
||||
const points = LinearElementEditor.getPointsGlobalCoordinates(element);
|
||||
|
||||
let index = 0;
|
||||
const midpoints: (Point | null)[] = [];
|
||||
while (index < points.length - 1) {
|
||||
if (
|
||||
LinearElementEditor.isSegmentTooShort(
|
||||
element,
|
||||
element.points[index],
|
||||
element.points[index + 1],
|
||||
appState.zoom,
|
||||
)
|
||||
) {
|
||||
midpoints.push(null);
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
const segmentMidPoint = LinearElementEditor.getSegmentMidPoint(
|
||||
element,
|
||||
points[index],
|
||||
points[index + 1],
|
||||
index + 1,
|
||||
);
|
||||
midpoints.push(segmentMidPoint);
|
||||
index++;
|
||||
}
|
||||
editorMidPointsCache.points = midpoints;
|
||||
editorMidPointsCache.version = element.version;
|
||||
editorMidPointsCache.zoom = appState.zoom.value;
|
||||
};
|
||||
|
||||
static getSegmentMidpointHitCoords = (
|
||||
linearElementEditor: LinearElementEditor,
|
||||
scenePointer: { x: number; y: number },
|
||||
appState: AppState,
|
||||
@@ -367,46 +440,187 @@ export class LinearElementEditor {
|
||||
const { elementId } = linearElementEditor;
|
||||
const element = LinearElementEditor.getElement(elementId);
|
||||
if (!element) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
const clickedPointIndex = LinearElementEditor.getPointIndexUnderCursor(
|
||||
element,
|
||||
appState.selectedLinearElement,
|
||||
appState.zoom,
|
||||
scenePointer.x,
|
||||
scenePointer.y,
|
||||
);
|
||||
if (clickedPointIndex >= 0) {
|
||||
return false;
|
||||
}
|
||||
const points = LinearElementEditor.getPointsGlobalCoordinates(element);
|
||||
if (points.length >= 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const midPoint = LinearElementEditor.getMidPoint(linearElementEditor);
|
||||
if (midPoint) {
|
||||
const threshold =
|
||||
LinearElementEditor.POINT_HANDLE_SIZE / appState.zoom.value;
|
||||
const distance = distance2d(
|
||||
midPoint[0],
|
||||
midPoint[1],
|
||||
scenePointer.x,
|
||||
scenePointer.y,
|
||||
);
|
||||
return distance <= threshold;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
static getMidPoint(linearElementEditor: LinearElementEditor) {
|
||||
const { elementId } = linearElementEditor;
|
||||
const element = LinearElementEditor.getElement(elementId);
|
||||
if (!element) {
|
||||
return null;
|
||||
}
|
||||
const points = LinearElementEditor.getPointsGlobalCoordinates(element);
|
||||
if (points.length >= 3 && !appState.editingLinearElement) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return centerPoint(points[0], points.at(-1)!);
|
||||
const threshold =
|
||||
LinearElementEditor.POINT_HANDLE_SIZE / appState.zoom.value;
|
||||
|
||||
const existingSegmentMidpointHitCoords =
|
||||
linearElementEditor.segmentMidPointHoveredCoords;
|
||||
if (existingSegmentMidpointHitCoords) {
|
||||
const distance = distance2d(
|
||||
existingSegmentMidpointHitCoords[0],
|
||||
existingSegmentMidpointHitCoords[1],
|
||||
scenePointer.x,
|
||||
scenePointer.y,
|
||||
);
|
||||
if (distance <= threshold) {
|
||||
return existingSegmentMidpointHitCoords;
|
||||
}
|
||||
}
|
||||
let index = 0;
|
||||
const midPoints: typeof editorMidPointsCache["points"] =
|
||||
LinearElementEditor.getEditorMidPoints(element, appState);
|
||||
while (index < midPoints.length) {
|
||||
if (midPoints[index] !== null) {
|
||||
const distance = distance2d(
|
||||
midPoints[index]![0],
|
||||
midPoints[index]![1],
|
||||
scenePointer.x,
|
||||
scenePointer.y,
|
||||
);
|
||||
if (distance <= threshold) {
|
||||
return midPoints[index];
|
||||
}
|
||||
}
|
||||
|
||||
index++;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
static isSegmentTooShort(
|
||||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
startPoint: Point,
|
||||
endPoint: Point,
|
||||
zoom: AppState["zoom"],
|
||||
) {
|
||||
let distance = distance2d(
|
||||
startPoint[0],
|
||||
startPoint[1],
|
||||
endPoint[0],
|
||||
endPoint[1],
|
||||
);
|
||||
if (element.points.length > 2 && element.strokeSharpness === "round") {
|
||||
distance = getBezierCurveLength(element, endPoint);
|
||||
}
|
||||
|
||||
return distance * zoom.value < LinearElementEditor.POINT_HANDLE_SIZE * 4;
|
||||
}
|
||||
|
||||
static getSegmentMidPoint(
|
||||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
startPoint: Point,
|
||||
endPoint: Point,
|
||||
endPointIndex: number,
|
||||
) {
|
||||
let segmentMidPoint = centerPoint(startPoint, endPoint);
|
||||
if (element.points.length > 2 && element.strokeSharpness === "round") {
|
||||
const controlPoints = getControlPointsForBezierCurve(
|
||||
element,
|
||||
element.points[endPointIndex],
|
||||
);
|
||||
if (controlPoints) {
|
||||
const t = mapIntervalToBezierT(
|
||||
element,
|
||||
element.points[endPointIndex],
|
||||
0.5,
|
||||
);
|
||||
|
||||
const [tx, ty] = getBezierXY(
|
||||
controlPoints[0],
|
||||
controlPoints[1],
|
||||
controlPoints[2],
|
||||
controlPoints[3],
|
||||
t,
|
||||
);
|
||||
segmentMidPoint = LinearElementEditor.getPointGlobalCoordinates(
|
||||
element,
|
||||
[tx, ty],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return segmentMidPoint;
|
||||
}
|
||||
|
||||
static getSegmentMidPointIndex(
|
||||
linearElementEditor: LinearElementEditor,
|
||||
appState: AppState,
|
||||
midPoint: Point,
|
||||
) {
|
||||
const element = LinearElementEditor.getElement(
|
||||
linearElementEditor.elementId,
|
||||
);
|
||||
if (!element) {
|
||||
return -1;
|
||||
}
|
||||
const midPoints = LinearElementEditor.getEditorMidPoints(element, appState);
|
||||
let index = 0;
|
||||
while (index < midPoints.length - 1) {
|
||||
if (LinearElementEditor.arePointsEqual(midPoint, midPoints[index])) {
|
||||
return index + 1;
|
||||
}
|
||||
index++;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static getVisiblePointIndexes(
|
||||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
appState: AppState,
|
||||
): typeof visiblePointIndexesCache["points"] {
|
||||
const isEditingLinearElement = !!appState.editingLinearElement;
|
||||
if (appState.editingLinearElement) {
|
||||
// So that when we exit the editor the points are calculated again
|
||||
visiblePointIndexesCache.isEditingLinearElement = true;
|
||||
return element.points.map((_, index) => index);
|
||||
}
|
||||
|
||||
if (
|
||||
visiblePointIndexesCache.points &&
|
||||
visiblePointIndexesCache.zoom === appState.zoom.value &&
|
||||
isEditingLinearElement === visiblePointIndexesCache.isEditingLinearElement
|
||||
) {
|
||||
return visiblePointIndexesCache.points;
|
||||
}
|
||||
|
||||
LinearElementEditor.updateVisiblePointIndexesCache(element, appState);
|
||||
return visiblePointIndexesCache.points;
|
||||
}
|
||||
|
||||
static updateVisiblePointIndexesCache(
|
||||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
appState: AppState,
|
||||
) {
|
||||
const visiblePointIndexes: number[] = [];
|
||||
let previousPoint: Point | null = null;
|
||||
element.points.forEach((point, index) => {
|
||||
let distance = Infinity;
|
||||
if (previousPoint) {
|
||||
distance =
|
||||
distance2d(point[0], point[1], previousPoint[0], previousPoint[1]) *
|
||||
appState.zoom.value;
|
||||
}
|
||||
const isExtremePoint = index === 0 || index === element.points.length - 1;
|
||||
const threshold = 2 * LinearElementEditor.POINT_HANDLE_SIZE;
|
||||
if (isExtremePoint || distance >= threshold) {
|
||||
// hide n-1 point if distance is less than threshold
|
||||
if (isExtremePoint && distance < threshold) {
|
||||
visiblePointIndexes.pop();
|
||||
}
|
||||
visiblePointIndexes.push(index);
|
||||
previousPoint = point;
|
||||
}
|
||||
});
|
||||
visiblePointIndexesCache.points = visiblePointIndexes;
|
||||
visiblePointIndexesCache.zoom = appState.zoom.value;
|
||||
visiblePointIndexesCache.isEditingLinearElement =
|
||||
!!appState.editingLinearElement;
|
||||
}
|
||||
|
||||
static handlePointerDown(
|
||||
@@ -438,44 +652,34 @@ export class LinearElementEditor {
|
||||
if (!element) {
|
||||
return ret;
|
||||
}
|
||||
const hittingMidPoint = LinearElementEditor.isHittingMidPoint(
|
||||
const segmentMidPoint = LinearElementEditor.getSegmentMidpointHitCoords(
|
||||
linearElementEditor,
|
||||
scenePointer,
|
||||
appState,
|
||||
);
|
||||
if (
|
||||
LinearElementEditor.isHittingMidPoint(
|
||||
if (segmentMidPoint) {
|
||||
const index = LinearElementEditor.getSegmentMidPointIndex(
|
||||
linearElementEditor,
|
||||
scenePointer,
|
||||
appState,
|
||||
)
|
||||
) {
|
||||
const midPoint = LinearElementEditor.getMidPoint(linearElementEditor);
|
||||
if (midPoint) {
|
||||
mutateElement(element, {
|
||||
points: [
|
||||
element.points[0],
|
||||
LinearElementEditor.createPointAt(
|
||||
element,
|
||||
midPoint[0],
|
||||
midPoint[1],
|
||||
appState.gridSize,
|
||||
),
|
||||
...element.points.slice(1),
|
||||
],
|
||||
});
|
||||
}
|
||||
segmentMidPoint,
|
||||
);
|
||||
const newMidPoint = LinearElementEditor.createPointAt(
|
||||
element,
|
||||
segmentMidPoint[0],
|
||||
segmentMidPoint[1],
|
||||
appState.gridSize,
|
||||
);
|
||||
const points = [
|
||||
...element.points.slice(0, index),
|
||||
newMidPoint,
|
||||
...element.points.slice(index),
|
||||
];
|
||||
mutateElement(element, {
|
||||
points,
|
||||
});
|
||||
|
||||
ret.didAddPoint = true;
|
||||
ret.isMidPoint = true;
|
||||
ret.linearElementEditor = {
|
||||
...linearElementEditor,
|
||||
selectedPointsIndices: element.points[1],
|
||||
pointerDownState: {
|
||||
prevSelectedPointsIndices: linearElementEditor.selectedPointsIndices,
|
||||
lastClickedPoint: -1,
|
||||
},
|
||||
lastUncommittedPoint: null,
|
||||
};
|
||||
}
|
||||
if (event.altKey && appState.editingLinearElement) {
|
||||
if (linearElementEditor.lastUncommittedPoint == null) {
|
||||
@@ -512,7 +716,7 @@ export class LinearElementEditor {
|
||||
}
|
||||
|
||||
const clickedPointIndex = LinearElementEditor.getPointIndexUnderCursor(
|
||||
element,
|
||||
appState.selectedLinearElement,
|
||||
appState.zoom,
|
||||
scenePointer.x,
|
||||
scenePointer.y,
|
||||
@@ -520,7 +724,7 @@ export class LinearElementEditor {
|
||||
|
||||
// if we clicked on a point, set the element as hitElement otherwise
|
||||
// it would get deselected if the point is outside the hitbox area
|
||||
if (clickedPointIndex >= 0 || hittingMidPoint) {
|
||||
if (clickedPointIndex >= 0 || segmentMidPoint) {
|
||||
ret.hitElement = element;
|
||||
} else {
|
||||
// You might be wandering why we are storing the binding elements on
|
||||
@@ -575,21 +779,37 @@ export class LinearElementEditor {
|
||||
}
|
||||
: { x: 0, y: 0 },
|
||||
};
|
||||
|
||||
if (ret.didAddPoint) {
|
||||
ret.linearElementEditor = {
|
||||
...ret.linearElementEditor,
|
||||
};
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
static arePointsEqual(point1: Point | null, point2: Point | null) {
|
||||
if (!point1 && !point2) {
|
||||
return true;
|
||||
}
|
||||
if (!point1 || !point2) {
|
||||
return false;
|
||||
}
|
||||
return arePointsEqual(point1, point2);
|
||||
}
|
||||
|
||||
static handlePointerMove(
|
||||
event: React.PointerEvent<HTMLCanvasElement>,
|
||||
scenePointerX: number,
|
||||
scenePointerY: number,
|
||||
linearElementEditor: LinearElementEditor,
|
||||
gridSize: number | null,
|
||||
): LinearElementEditor {
|
||||
const { elementId, lastUncommittedPoint } = linearElementEditor;
|
||||
appState: AppState,
|
||||
): LinearElementEditor | null {
|
||||
if (!appState.editingLinearElement) {
|
||||
return null;
|
||||
}
|
||||
const { elementId, lastUncommittedPoint } = appState.editingLinearElement;
|
||||
const element = LinearElementEditor.getElement(elementId);
|
||||
if (!element) {
|
||||
return linearElementEditor;
|
||||
return appState.editingLinearElement;
|
||||
}
|
||||
|
||||
const { points } = element;
|
||||
@@ -599,7 +819,10 @@ export class LinearElementEditor {
|
||||
if (lastPoint === lastUncommittedPoint) {
|
||||
LinearElementEditor.deletePoints(element, [points.length - 1]);
|
||||
}
|
||||
return { ...linearElementEditor, lastUncommittedPoint: null };
|
||||
return {
|
||||
...appState.editingLinearElement,
|
||||
lastUncommittedPoint: null,
|
||||
};
|
||||
}
|
||||
|
||||
let newPoint: Point;
|
||||
@@ -611,7 +834,7 @@ export class LinearElementEditor {
|
||||
element,
|
||||
lastCommittedPoint,
|
||||
[scenePointerX, scenePointerY],
|
||||
gridSize,
|
||||
appState.gridSize,
|
||||
);
|
||||
|
||||
newPoint = [
|
||||
@@ -621,9 +844,9 @@ export class LinearElementEditor {
|
||||
} else {
|
||||
newPoint = LinearElementEditor.createPointAt(
|
||||
element,
|
||||
scenePointerX - linearElementEditor.pointerOffset.x,
|
||||
scenePointerY - linearElementEditor.pointerOffset.y,
|
||||
gridSize,
|
||||
scenePointerX - appState.editingLinearElement.pointerOffset.x,
|
||||
scenePointerY - appState.editingLinearElement.pointerOffset.y,
|
||||
appState.gridSize,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -635,11 +858,10 @@ export class LinearElementEditor {
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
LinearElementEditor.addPoints(element, [{ point: newPoint }]);
|
||||
LinearElementEditor.addPoints(element, appState, [{ point: newPoint }]);
|
||||
}
|
||||
|
||||
return {
|
||||
...linearElementEditor,
|
||||
...appState.editingLinearElement,
|
||||
lastUncommittedPoint: element.points[element.points.length - 1],
|
||||
};
|
||||
}
|
||||
@@ -686,7 +908,9 @@ export class LinearElementEditor {
|
||||
|
||||
const point = element.points[index];
|
||||
const { x, y } = element;
|
||||
return rotate(x + point[0], y + point[1], cx, cy, element.angle);
|
||||
return point
|
||||
? rotate(x + point[0], y + point[1], cx, cy, element.angle)
|
||||
: rotate(x, y, cx, cy, element.angle);
|
||||
}
|
||||
|
||||
static pointFromAbsoluteCoords(
|
||||
@@ -707,25 +931,37 @@ export class LinearElementEditor {
|
||||
}
|
||||
|
||||
static getPointIndexUnderCursor(
|
||||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
linearElementEditor: LinearElementEditor | null,
|
||||
zoom: AppState["zoom"],
|
||||
x: number,
|
||||
y: number,
|
||||
) {
|
||||
if (!linearElementEditor) {
|
||||
return -1;
|
||||
}
|
||||
const element = LinearElementEditor.getElement(
|
||||
linearElementEditor.elementId,
|
||||
);
|
||||
if (!element) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const pointHandles =
|
||||
LinearElementEditor.getPointsGlobalCoordinates(element);
|
||||
let idx = pointHandles.length;
|
||||
let counter = visiblePointIndexesCache.points.length;
|
||||
|
||||
// loop from right to left because points on the right are rendered over
|
||||
// points on the left, thus should take precedence when clicking, if they
|
||||
// overlap
|
||||
while (--idx > -1) {
|
||||
const point = pointHandles[idx];
|
||||
while (--counter >= 0) {
|
||||
const index = visiblePointIndexesCache.points[counter];
|
||||
const point = pointHandles[index];
|
||||
if (
|
||||
distance2d(x, y, point[0], point[1]) * zoom.value <
|
||||
// +1px to account for outline stroke
|
||||
LinearElementEditor.POINT_HANDLE_SIZE + 1
|
||||
) {
|
||||
return idx;
|
||||
return index;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
@@ -882,6 +1118,7 @@ export class LinearElementEditor {
|
||||
|
||||
static addPoints(
|
||||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
appState: AppState,
|
||||
targetPoints: { point: Point }[],
|
||||
) {
|
||||
const offsetX = 0;
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
import { DEFAULT_VERSION } from "../constants";
|
||||
import { t } from "../i18n";
|
||||
import { copyTextToSystemClipboard } from "../clipboard";
|
||||
import { AppState } from "../types";
|
||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||
type StorageSizes = { scene: number; total: number };
|
||||
|
||||
const STORAGE_SIZE_TIMEOUT = 500;
|
||||
@@ -20,6 +22,8 @@ const getStorageSizes = debounce((cb: (sizes: StorageSizes) => void) => {
|
||||
|
||||
type Props = {
|
||||
setToast: (message: string) => void;
|
||||
elements: readonly NonDeletedExcalidrawElement[];
|
||||
appState: AppState;
|
||||
};
|
||||
const CustomStats = (props: Props) => {
|
||||
const [storageSizes, setStorageSizes] = useState<StorageSizes>({
|
||||
@@ -31,7 +35,7 @@ const CustomStats = (props: Props) => {
|
||||
getStorageSizes((sizes) => {
|
||||
setStorageSizes(sizes);
|
||||
});
|
||||
});
|
||||
}, [props.elements, props.appState]);
|
||||
useEffect(() => () => getStorageSizes.cancel(), []);
|
||||
|
||||
const version = getVersion();
|
||||
|
||||
@@ -33,7 +33,6 @@ 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",
|
||||
VERSION_DATA_STATE: "version-dataState",
|
||||
VERSION_FILES: "version-files",
|
||||
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
INITIAL_SCENE_UPDATE_TIMEOUT,
|
||||
LOAD_IMAGES_TIMEOUT,
|
||||
WS_SCENE_EVENT_TYPES,
|
||||
STORAGE_KEYS,
|
||||
SYNC_FULL_SCENE_INTERVAL_MS,
|
||||
} from "../app_constants";
|
||||
import {
|
||||
@@ -169,8 +168,7 @@ class Collab extends PureComponent<Props, CollabState> {
|
||||
|
||||
if (
|
||||
process.env.NODE_ENV === ENV.TEST ||
|
||||
process.env.NODE_ENV === ENV.DEVELOPMENT ||
|
||||
process.env.REACT_APP_VERCEL_ENV === "preview"
|
||||
process.env.NODE_ENV === ENV.DEVELOPMENT
|
||||
) {
|
||||
window.collab = window.collab || ({} as Window["collab"]);
|
||||
Object.defineProperties(window, {
|
||||
@@ -226,18 +224,6 @@ class Collab extends PureComponent<Props, CollabState> {
|
||||
|
||||
preventUnload(event);
|
||||
}
|
||||
|
||||
if (this.isCollaborating || this.portal.roomId) {
|
||||
try {
|
||||
localStorage?.setItem(
|
||||
STORAGE_KEYS.LOCAL_STORAGE_KEY_COLLAB_FORCE_FLAG,
|
||||
JSON.stringify({
|
||||
timestamp: Date.now(),
|
||||
room: this.portal.roomId,
|
||||
}),
|
||||
);
|
||||
} catch {}
|
||||
}
|
||||
});
|
||||
|
||||
saveCollabRoomToFirebase = async (
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
} from "../app_constants";
|
||||
import { UserIdleState } from "../../types";
|
||||
import { trackEvent } from "../../analytics";
|
||||
import { throttle } from "lodash";
|
||||
import throttle from "lodash.throttle";
|
||||
import { newElementWith } from "../../element/mutateElement";
|
||||
import { BroadcastedExcalidrawElement } from "./reconciliation";
|
||||
import { encryptData } from "../../data/encryption";
|
||||
|
||||
@@ -194,7 +194,13 @@ const initializeScene = async (opts: {
|
||||
scene: {
|
||||
...scene,
|
||||
appState: {
|
||||
...restoreAppState(scene?.appState, excalidrawAPI.getAppState()),
|
||||
...restoreAppState(
|
||||
{
|
||||
...scene?.appState,
|
||||
theme: localDataState?.appState?.theme || scene?.appState?.theme,
|
||||
},
|
||||
excalidrawAPI.getAppState(),
|
||||
),
|
||||
// necessary if we're invoking from a hashchange handler which doesn't
|
||||
// go through App.initializeScene() that resets this flag
|
||||
isLoading: false,
|
||||
@@ -666,10 +672,15 @@ const ExcalidrawWrapper = () => {
|
||||
[langCode],
|
||||
);
|
||||
|
||||
const renderCustomStats = () => {
|
||||
const renderCustomStats = (
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
appState: AppState,
|
||||
) => {
|
||||
return (
|
||||
<CustomStats
|
||||
setToast={(message) => excalidrawAPI!.setToast({ message })}
|
||||
appState={appState}
|
||||
elements={elements}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
3
src/global.d.ts
vendored
3
src/global.d.ts
vendored
@@ -16,9 +16,6 @@ interface Window {
|
||||
EXCALIDRAW_EXPORT_SOURCE: string;
|
||||
EXCALIDRAW_THROTTLE_RENDER: boolean | undefined;
|
||||
gtag: Function;
|
||||
logTime: (name: string, time?: number) => void;
|
||||
logTimeAverage: (name: string, time?: number) => void;
|
||||
DEBUG_LOG_TIMES: boolean;
|
||||
}
|
||||
|
||||
// https://github.com/facebook/create-react-app/blob/ddcb7d5/packages/react-scripts/lib/react-app.d.ts
|
||||
|
||||
@@ -1,55 +1,55 @@
|
||||
{
|
||||
"labels": {
|
||||
"paste": "",
|
||||
"pasteCharts": "",
|
||||
"selectAll": "",
|
||||
"multiSelect": "",
|
||||
"moveCanvas": "",
|
||||
"cut": "",
|
||||
"copy": "",
|
||||
"copyAsPng": "",
|
||||
"copyAsSvg": "",
|
||||
"copyText": "",
|
||||
"bringForward": "",
|
||||
"sendToBack": "",
|
||||
"bringToFront": "",
|
||||
"sendBackward": "",
|
||||
"delete": "",
|
||||
"copyStyles": "",
|
||||
"pasteStyles": "",
|
||||
"stroke": "",
|
||||
"background": "",
|
||||
"fill": "",
|
||||
"strokeWidth": "",
|
||||
"strokeStyle": "",
|
||||
"strokeStyle_solid": "",
|
||||
"strokeStyle_dashed": "",
|
||||
"strokeStyle_dotted": "",
|
||||
"sloppiness": "",
|
||||
"opacity": "",
|
||||
"textAlign": "",
|
||||
"edges": "",
|
||||
"sharp": "",
|
||||
"round": "",
|
||||
"arrowheads": "",
|
||||
"arrowhead_none": "",
|
||||
"arrowhead_arrow": "",
|
||||
"arrowhead_bar": "",
|
||||
"arrowhead_dot": "",
|
||||
"arrowhead_triangle": "",
|
||||
"fontSize": "",
|
||||
"fontFamily": "",
|
||||
"onlySelected": "",
|
||||
"withBackground": "",
|
||||
"exportEmbedScene": "",
|
||||
"exportEmbedScene_details": "",
|
||||
"paste": "পেস্ট করুন",
|
||||
"pasteCharts": "চার্টগুলো পেস্ট করুন",
|
||||
"selectAll": "সব সিলেক্ট করুন",
|
||||
"multiSelect": "সিলেকশনে এলিমেন্ট এ্যাড করুন",
|
||||
"moveCanvas": "ক্যানভাস মুভ করুন",
|
||||
"cut": "কাট করুন",
|
||||
"copy": "কপি করুন",
|
||||
"copyAsPng": "PNG হিসেবে ক্লিপবোর্ডে কপি করুন",
|
||||
"copyAsSvg": "SVG হিসেবে ক্লিপবোর্ডে কপি করুন",
|
||||
"copyText": "টেক্সট হিসেবে ক্লিপবোর্ডে কপি করুন",
|
||||
"bringForward": "সামনে আনুন",
|
||||
"sendToBack": "একদম পেছনে পাঠান",
|
||||
"bringToFront": "একদম সামনে আনুন",
|
||||
"sendBackward": "পেছনে পাঠান",
|
||||
"delete": "ডিলিট করুন",
|
||||
"copyStyles": "স্টাইলগুলো কপি করুন",
|
||||
"pasteStyles": "স্টাইলগুলো পেস্ট করুন",
|
||||
"stroke": "স্ট্রোক",
|
||||
"background": "ব্যাকগ্রাউন্ড",
|
||||
"fill": "ফিল",
|
||||
"strokeWidth": "স্ট্রোকের পুরুত্ব",
|
||||
"strokeStyle": "স্ট্রোকের স্টাইল",
|
||||
"strokeStyle_solid": "সলিড",
|
||||
"strokeStyle_dashed": "কাটা-কাটা",
|
||||
"strokeStyle_dotted": "ফোটা-ফোটা",
|
||||
"sloppiness": "স্ট্রোকের ধরণ",
|
||||
"opacity": "অস্বচ্ছতা",
|
||||
"textAlign": "লেখার দিক",
|
||||
"edges": "কোণা",
|
||||
"sharp": "তীক্ষ্ণ",
|
||||
"round": "গোলাকার",
|
||||
"arrowheads": "তীরের মাথা",
|
||||
"arrowhead_none": "কিছু না",
|
||||
"arrowhead_arrow": "তীর",
|
||||
"arrowhead_bar": "বার",
|
||||
"arrowhead_dot": "ডট",
|
||||
"arrowhead_triangle": "ত্রিভুজ",
|
||||
"fontSize": "ফন্ট সাইজ",
|
||||
"fontFamily": "ফন্ট ফ্যামিলি",
|
||||
"onlySelected": "শুধুমাত্র সিলেক্টেডগুলো",
|
||||
"withBackground": "ব্যাকগ্রাউন্ড",
|
||||
"exportEmbedScene": "সিন এম্বেড করুন",
|
||||
"exportEmbedScene_details": "সিনের ডেটা এক্সপোর্টকৃত PNG/SVG ফাইলের মধ্যে সেভ করা হবে যাতে করে পরবর্তী সময়ে আপনি এডিট করতে পারেন । তবে এতে ফাইলের সাইজ বাড়বে ।.",
|
||||
"addWatermark": "",
|
||||
"handDrawn": "",
|
||||
"normal": "",
|
||||
"code": "",
|
||||
"small": "",
|
||||
"medium": "",
|
||||
"large": "",
|
||||
"handDrawn": "হাতে আঁকা",
|
||||
"normal": "স্বাভাবিক",
|
||||
"code": "কোড",
|
||||
"small": "ছোট",
|
||||
"medium": "মধ্যবর্তী",
|
||||
"large": "বড়",
|
||||
"veryLarge": "",
|
||||
"solid": "",
|
||||
"hachure": "",
|
||||
@@ -99,7 +99,7 @@
|
||||
"flipVertical": "",
|
||||
"viewMode": "",
|
||||
"toggleExportColorScheme": "",
|
||||
"share": "",
|
||||
"share": "শেয়ার করুন",
|
||||
"showStroke": "",
|
||||
"showBackground": "",
|
||||
"toggleTheme": "",
|
||||
|
||||
@@ -110,13 +110,13 @@
|
||||
"unbindText": "",
|
||||
"bindText": "",
|
||||
"link": {
|
||||
"edit": "",
|
||||
"create": "",
|
||||
"label": ""
|
||||
"edit": "Redeguoti nuorodą",
|
||||
"create": "Sukurti nuorodą",
|
||||
"label": "Nuoroda"
|
||||
},
|
||||
"elementLock": {
|
||||
"lock": "",
|
||||
"unlock": "",
|
||||
"lock": "Užrakinti",
|
||||
"unlock": "Atrakinti",
|
||||
"lockAll": "",
|
||||
"unlockAll": ""
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"ar-SA": 91,
|
||||
"bg-BG": 58,
|
||||
"bn-BD": 0,
|
||||
"bn-BD": 13,
|
||||
"ca-ES": 99,
|
||||
"cs-CZ": 27,
|
||||
"da-DK": 34,
|
||||
@@ -23,7 +23,7 @@
|
||||
"kab-KAB": 95,
|
||||
"kk-KZ": 22,
|
||||
"ko-KR": 99,
|
||||
"lt-LT": 22,
|
||||
"lt-LT": 24,
|
||||
"lv-LV": 100,
|
||||
"mr-IN": 100,
|
||||
"my-MM": 44,
|
||||
@@ -44,7 +44,7 @@
|
||||
"ta-IN": 98,
|
||||
"tr-TR": 99,
|
||||
"uk-UA": 100,
|
||||
"vi-VN": 13,
|
||||
"vi-VN": 16,
|
||||
"zh-CN": 100,
|
||||
"zh-HK": 27,
|
||||
"zh-TW": 100
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"labels": {
|
||||
"paste": "Dán",
|
||||
"pasteCharts": "",
|
||||
"pasteCharts": "Dán biểu đồ",
|
||||
"selectAll": "Chọn tất cả",
|
||||
"multiSelect": "",
|
||||
"moveCanvas": "",
|
||||
"multiSelect": "Thêm mới vào Select",
|
||||
"moveCanvas": "Di chuyển Canvas",
|
||||
"cut": "Cắt",
|
||||
"copy": "Sao chép",
|
||||
"copyAsPng": "Sao chép vào bộ nhớ tạm dưới dạng PNG",
|
||||
@@ -43,22 +43,22 @@
|
||||
"withBackground": "Nền",
|
||||
"exportEmbedScene": "",
|
||||
"exportEmbedScene_details": "",
|
||||
"addWatermark": "",
|
||||
"addWatermark": "Làm với Excalidraw\"",
|
||||
"handDrawn": "",
|
||||
"normal": "Bình thường",
|
||||
"code": "Mã",
|
||||
"small": "Nhỏ",
|
||||
"medium": "Vừa",
|
||||
"large": "Lớn",
|
||||
"veryLarge": "",
|
||||
"solid": "",
|
||||
"veryLarge": "Rất lớn",
|
||||
"solid": "Đặc",
|
||||
"hachure": "",
|
||||
"crossHatch": "",
|
||||
"thin": "",
|
||||
"bold": "",
|
||||
"left": "",
|
||||
"center": "",
|
||||
"right": "",
|
||||
"thin": "Mỏng",
|
||||
"bold": "In đậm",
|
||||
"left": "Trái",
|
||||
"center": "Giữa",
|
||||
"right": "Phải",
|
||||
"extraBold": "",
|
||||
"architect": "",
|
||||
"artist": "",
|
||||
|
||||
166
src/math.ts
166
src/math.ts
@@ -1,6 +1,8 @@
|
||||
import { NormalizedZoomValue, Point, Zoom } from "./types";
|
||||
import { LINE_CONFIRM_THRESHOLD } from "./constants";
|
||||
import { ExcalidrawLinearElement } from "./element/types";
|
||||
import { ExcalidrawLinearElement, NonDeleted } from "./element/types";
|
||||
import { getShapeForElement } from "./renderer/renderElement";
|
||||
import { getCurvePathOps } from "./element/bounds";
|
||||
|
||||
export const rotate = (
|
||||
x1: number,
|
||||
@@ -263,3 +265,165 @@ export const getGridPoint = (
|
||||
}
|
||||
return [x, y];
|
||||
};
|
||||
|
||||
export const getControlPointsForBezierCurve = (
|
||||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
endPoint: Point,
|
||||
) => {
|
||||
const shape = getShapeForElement(element as ExcalidrawLinearElement);
|
||||
if (!shape) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ops = getCurvePathOps(shape[0]);
|
||||
let currentP: Mutable<Point> = [0, 0];
|
||||
let index = 0;
|
||||
let minDistance = Infinity;
|
||||
let controlPoints: Mutable<Point>[] | null = null;
|
||||
|
||||
while (index < ops.length) {
|
||||
const { op, data } = ops[index];
|
||||
if (op === "move") {
|
||||
currentP = data as unknown as Mutable<Point>;
|
||||
}
|
||||
if (op === "bcurveTo") {
|
||||
const p0 = currentP;
|
||||
const p1 = [data[0], data[1]] as Mutable<Point>;
|
||||
const p2 = [data[2], data[3]] as Mutable<Point>;
|
||||
const p3 = [data[4], data[5]] as Mutable<Point>;
|
||||
const distance = distance2d(p3[0], p3[1], endPoint[0], endPoint[1]);
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
controlPoints = [p0, p1, p2, p3];
|
||||
}
|
||||
currentP = p3;
|
||||
}
|
||||
index++;
|
||||
}
|
||||
|
||||
return controlPoints;
|
||||
};
|
||||
|
||||
export const getBezierXY = (
|
||||
p0: Point,
|
||||
p1: Point,
|
||||
p2: Point,
|
||||
p3: Point,
|
||||
t: number,
|
||||
) => {
|
||||
const equation = (t: number, idx: number) =>
|
||||
Math.pow(1 - t, 3) * p3[idx] +
|
||||
3 * t * Math.pow(1 - t, 2) * p2[idx] +
|
||||
3 * Math.pow(t, 2) * (1 - t) * p1[idx] +
|
||||
p0[idx] * Math.pow(t, 3);
|
||||
const tx = equation(t, 0);
|
||||
const ty = equation(t, 1);
|
||||
return [tx, ty];
|
||||
};
|
||||
|
||||
export const getPointsInBezierCurve = (
|
||||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
endPoint: Point,
|
||||
) => {
|
||||
const controlPoints: Mutable<Point>[] = getControlPointsForBezierCurve(
|
||||
element,
|
||||
endPoint,
|
||||
)!;
|
||||
if (!controlPoints) {
|
||||
return [];
|
||||
}
|
||||
const pointsOnCurve: Mutable<Point>[] = [];
|
||||
let t = 1;
|
||||
// Take 20 points on curve for better accuracy
|
||||
while (t > 0) {
|
||||
const point = getBezierXY(
|
||||
controlPoints[0],
|
||||
controlPoints[1],
|
||||
controlPoints[2],
|
||||
controlPoints[3],
|
||||
t,
|
||||
);
|
||||
pointsOnCurve.push([point[0], point[1]]);
|
||||
t -= 0.05;
|
||||
}
|
||||
if (pointsOnCurve.length) {
|
||||
if (arePointsEqual(pointsOnCurve.at(-1)!, endPoint)) {
|
||||
pointsOnCurve.push([endPoint[0], endPoint[1]]);
|
||||
}
|
||||
}
|
||||
return pointsOnCurve;
|
||||
};
|
||||
|
||||
export const getBezierCurveArcLengths = (
|
||||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
endPoint: Point,
|
||||
) => {
|
||||
const arcLengths: number[] = [];
|
||||
arcLengths[0] = 0;
|
||||
const points = getPointsInBezierCurve(element, endPoint);
|
||||
let index = 0;
|
||||
let distance = 0;
|
||||
while (index < points.length - 1) {
|
||||
const segmentDistance = distance2d(
|
||||
points[index][0],
|
||||
points[index][1],
|
||||
points[index + 1][0],
|
||||
points[index + 1][1],
|
||||
);
|
||||
distance += segmentDistance;
|
||||
arcLengths.push(distance);
|
||||
index++;
|
||||
}
|
||||
|
||||
return arcLengths;
|
||||
};
|
||||
|
||||
export const getBezierCurveLength = (
|
||||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
endPoint: Point,
|
||||
) => {
|
||||
const arcLengths = getBezierCurveArcLengths(element, endPoint);
|
||||
return arcLengths.at(-1) as number;
|
||||
};
|
||||
|
||||
// This maps interval to actual interval t on the curve so that when t = 0.5, its actually the point at 50% of the length
|
||||
export const mapIntervalToBezierT = (
|
||||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
endPoint: Point,
|
||||
interval: number, // The interval between 0 to 1 for which you want to find the point on the curve,
|
||||
) => {
|
||||
const arcLengths = getBezierCurveArcLengths(element, endPoint);
|
||||
const pointsCount = arcLengths.length - 1;
|
||||
const curveLength = arcLengths.at(-1) as number;
|
||||
const targetLength = interval * curveLength;
|
||||
let low = 0;
|
||||
let high = pointsCount;
|
||||
let index = 0;
|
||||
// Doing a binary search to find the largest length that is less than the target length
|
||||
while (low < high) {
|
||||
index = Math.floor(low + (high - low) / 2);
|
||||
if (arcLengths[index] < targetLength) {
|
||||
low = index + 1;
|
||||
} else {
|
||||
high = index;
|
||||
}
|
||||
}
|
||||
if (arcLengths[index] > targetLength) {
|
||||
index--;
|
||||
}
|
||||
if (arcLengths[index] === targetLength) {
|
||||
return index / pointsCount;
|
||||
}
|
||||
|
||||
return (
|
||||
1 -
|
||||
(index +
|
||||
(targetLength - arcLengths[index]) /
|
||||
(arcLengths[index + 1] - arcLengths[index])) /
|
||||
pointsCount
|
||||
);
|
||||
};
|
||||
|
||||
export const arePointsEqual = (p1: Point, p2: Point) => {
|
||||
return p1[0] === p2[0] && p1[1] === p2[1];
|
||||
};
|
||||
|
||||
@@ -18,6 +18,7 @@ Please add the latest change on the top under the correct section.
|
||||
#### Features
|
||||
|
||||
- Added support for storing [`customData`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#storing-custom-data-to-excalidraw-elements) on Excalidraw elements [#5592].
|
||||
- Added `exportPadding?: number;` to [exportToCanvas](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#exporttocanvas) and [exportToBlob](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#exporttoblob). The default value of the padding is 10.
|
||||
|
||||
#### Breaking Changes
|
||||
|
||||
@@ -25,867 +26,21 @@ Please add the latest change on the top under the correct section.
|
||||
|
||||
## 0.12.0 (2022-07-07)
|
||||
|
||||
### Excalidraw API
|
||||
|
||||
#### Features
|
||||
|
||||
- Add [`UIOptions.dockedSidebarBreakpoint`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#dockedSidebarBreakpoint) to customize at which point to break from the docked sidebar [#5274](https://github.com/excalidraw/excalidraw/pull/5274).
|
||||
|
||||
- Added support for supplying user `id` in the Collaborator object (see `collaborators` in [`updateScene()`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#updateScene)), which will be used to deduplicate users when rendering collaborator avatar list. Cursors will still be rendered for every user. [#5309](https://github.com/excalidraw/excalidraw/pull/5309)
|
||||
|
||||
- Export API to [set](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#setCursor) and [reset](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#resetCursor) mouse cursor on the canvas [#5215](https://github.com/excalidraw/excalidraw/pull/5215).
|
||||
|
||||
- Export [`sceneCoordsToViewportCoords`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#onPointerDown) and [`viewportCoordsToSceneCoords`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#onPointerDown) utilities [#5187](https://github.com/excalidraw/excalidraw/pull/5187).
|
||||
|
||||
- Added [`useHandleLibrary`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#useHandleLibrary) hook to automatically handle importing of libraries when `#addLibrary` URL hash key is present, and potentially for initializing library as well [#5115](https://github.com/excalidraw/excalidraw/pull/5115).
|
||||
|
||||
Also added [`parseLibraryTokensFromUrl`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#parseLibraryTokensFromUrl) to help in manually importing library from URL if desired.
|
||||
|
||||
##### BREAKING CHANGE
|
||||
|
||||
- Libraries are no longer automatically initialized from URL when `#addLibrary` hash key is present. Host apps now need to handle this themselves with the help of either of the above APIs (`useHandleLibrary` is recommended).
|
||||
|
||||
- Added [`updateLibrary`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#updateLibrary) API to update (replace/merge) the library [#5115](https://github.com/excalidraw/excalidraw/pull/5115).
|
||||
|
||||
##### BREAKING CHANGE
|
||||
|
||||
- `updateScene` API no longer supports passing `libraryItems`. Instead, use the `updateLibrary` API.
|
||||
|
||||
- Add support for integrating custom elements [#5164](https://github.com/excalidraw/excalidraw/pull/5164).
|
||||
|
||||
- Add [`onPointerDown`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#onPointerDown) callback which gets triggered on pointer down events.
|
||||
- Add [`onScrollChange`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#onScrollChange) callback which gets triggered when scrolling the canvas.
|
||||
- Add API [`setActiveTool`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#setActiveTool) which host can call to set the active tool.
|
||||
|
||||
- Exported [`loadSceneOrLibraryFromBlob`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#loadSceneOrLibraryFromBlob) function [#5057](https://github.com/excalidraw/excalidraw/pull/5057).
|
||||
- Export [`MIME_TYPES`](https://github.com/excalidraw/excalidraw/blob/master/src/constants.ts#L92) supported by Excalidraw [#5135](https://github.com/excalidraw/excalidraw/pull/5135).
|
||||
- Support [`avatarUrl`](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L50) for collaborators. Now onwards host can pass `avatarUrl` to render the customized avatar for collaborators [#5114](https://github.com/excalidraw/excalidraw/pull/5114), renamed in [#5177](https://github.com/excalidraw/excalidraw/pull/5177).
|
||||
- Support `libraryItems` argument in `initialData.libraryItems` and `updateScene({ libraryItems })` to be a Promise resolving to `LibraryItems`, and support functional update of `libraryItems` in [`updateScene({ libraryItems })`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#updateScene). [#5101](https://github.com/excalidraw/excalidraw/pull/5101).
|
||||
- Expose util [`mergeLibraryItems`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#mergeLibraryItems) [#5101](https://github.com/excalidraw/excalidraw/pull/5101).
|
||||
- Expose util [`exportToClipboard`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#exportToClipboard) which allows to copy the scene contents to clipboard as `svg`, `png` or `json` [#5103](https://github.com/excalidraw/excalidraw/pull/5103).
|
||||
- Expose `window.EXCALIDRAW_EXPORT_SOURCE` which you can use to overwrite the `source` field in exported data [#5095](https://github.com/excalidraw/excalidraw/pull/5095).
|
||||
- The `exportToBlob` utility now supports the `exportEmbedScene` option when generating a png image [#5047](https://github.com/excalidraw/excalidraw/pull/5047).
|
||||
- Exported [`restoreLibraryItems`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#restoreLibraryItems) API [#4995](https://github.com/excalidraw/excalidraw/pull/4995).
|
||||
|
||||
#### Fixes
|
||||
|
||||
- Allow returning `null ` in [`renderFooter`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#renderFooter) prop [#5282](https://github.com/excalidraw/excalidraw/pull/5282).
|
||||
|
||||
- Transpile `browser-fs-access` dependency so that its `for await` syntax doesn't force `es2018` requirement onto dependent projects [#5041](https://github.com/excalidraw/excalidraw/pull/5041).
|
||||
|
||||
- Use `window.EXCALIDRAW_ASSET_PATH` for fonts when exporting to svg [#5065](https://github.com/excalidraw/excalidraw/pull/5065).
|
||||
- Library menu now properly rerenders if open when library is updated using `updateScene({ libraryItems })` [#4995](https://github.com/excalidraw/excalidraw/pull/4995).
|
||||
|
||||
#### Refactor
|
||||
|
||||
- Rename `appState.elementLocked` to `appState.activeTool.locked` [#4983](https://github.com/excalidraw/excalidraw/pull/4983).
|
||||
- Expose [`serializeLibraryAsJSON`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#serializeLibraryAsJSON) helper that we use when saving Excalidraw Library to a file.
|
||||
|
||||
##### BREAKING CHANGE
|
||||
|
||||
You will need to pass `activeTool.locked` instead of `elementType` from now onwards in `appState`.
|
||||
|
||||
- Rename `appState.elementType` to [`appState.activeTool`](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L80) which is now an object [#4698](https://github.com/excalidraw/excalidraw/pull/4968).
|
||||
|
||||
##### BREAKING CHANGE
|
||||
|
||||
You will need to pass `activeTool` instead of `elementType` from now onwards in `appState`
|
||||
|
||||
### Build
|
||||
|
||||
- Use only named exports [#5045](https://github.com/excalidraw/excalidraw/pull/5045).
|
||||
|
||||
#### BREAKING CHANGE
|
||||
|
||||
You will need to import the named export from now onwards to use the component
|
||||
|
||||
Using bundler :point_down:
|
||||
|
||||
```js
|
||||
import { Excalidraw } from "@excalidraw/excalidraw";
|
||||
```
|
||||
|
||||
In Browser :point_down:
|
||||
|
||||
```js
|
||||
React.createElement(ExcalidrawLib.Excalidraw, opts);
|
||||
```
|
||||
|
||||
## Excalidraw Library
|
||||
Check out the [release notes](https://github.com/excalidraw/excalidraw/releases/tag/v0.12.0) )
|
||||
|
||||
**_This section lists the updates made to the excalidraw library and will not affect the integration._**
|
||||
|
||||
### Features
|
||||
|
||||
- Throttle scene rendering to animation framerate [#5422](https://github.com/excalidraw/excalidraw/pull/5422)
|
||||
|
||||
- Make toast closable and allow custom duration [#5308](https://github.com/excalidraw/excalidraw/pull/5308)
|
||||
|
||||
- Collab component state handling rewrite & fixes [#5046](https://github.com/excalidraw/excalidraw/pull/5046)
|
||||
|
||||
- Support debugging PWA in dev [#4853](https://github.com/excalidraw/excalidraw/pull/4853)
|
||||
|
||||
- Redirect vscode.excalidraw.com to vscode marketplace [#5285](https://github.com/excalidraw/excalidraw/pull/5285)
|
||||
|
||||
- Go-to-excalidrawplus button [#5202](https://github.com/excalidraw/excalidraw/pull/5202)
|
||||
|
||||
- Autoredirect to Excalidraw+ if special cookie is present [#5183](https://github.com/excalidraw/excalidraw/pull/5183)
|
||||
|
||||
- Support resubmitting published library items [#5174](https://github.com/excalidraw/excalidraw/pull/5174)
|
||||
|
||||
- Support adding multiple library items on canvas [#5116](https://github.com/excalidraw/excalidraw/pull/5116)
|
||||
|
||||
- Support customType in activeTool [#5144](https://github.com/excalidraw/excalidraw/pull/5144)
|
||||
|
||||
- Stop event propagation when key handled [#5091](https://github.com/excalidraw/excalidraw/pull/5091)
|
||||
|
||||
- Rewrite library state management & related refactor [#5067](https://github.com/excalidraw/excalidraw/pull/5067)
|
||||
|
||||
- Delay initial loading message & tweak design [#5049](https://github.com/excalidraw/excalidraw/pull/5049)
|
||||
|
||||
- Reconcile when saving to firebase [#4991](https://github.com/excalidraw/excalidraw/pull/4991)
|
||||
|
||||
- Hide trash button during collaboration [#5037](https://github.com/excalidraw/excalidraw/pull/5037)
|
||||
|
||||
- Refactor local persistence & fix race condition on SW reload [#5032](https://github.com/excalidraw/excalidraw/pull/5032)
|
||||
|
||||
- Element locking [#4964](https://github.com/excalidraw/excalidraw/pull/4964)
|
||||
|
||||
- Copy to clipboard all text nodes as text [#5013](https://github.com/excalidraw/excalidraw/pull/5013)
|
||||
|
||||
- Create and expose serializeLibraryAsJSON [#5009](https://github.com/excalidraw/excalidraw/pull/5009)
|
||||
|
||||
- Hide penMode button on reload if not enabled [#4992](https://github.com/excalidraw/excalidraw/pull/4992)
|
||||
|
||||
- Eraser toggle to switch back to the previous tool [#4981](https://github.com/excalidraw/excalidraw/pull/4981)
|
||||
|
||||
- Save penDetected and penMode, and detect pen already on ToolButton click [#4955](https://github.com/excalidraw/excalidraw/pull/4955)
|
||||
|
||||
- Support binding text to container via context menu [#4935](https://github.com/excalidraw/excalidraw/pull/4935)
|
||||
|
||||
- Map shortcut O to ellipse and Add eraser shortcut E [#4930](https://github.com/excalidraw/excalidraw/pull/4930)
|
||||
|
||||
- Update eraser cursor [#4922](https://github.com/excalidraw/excalidraw/pull/4922)
|
||||
|
||||
- Add Eraser 🎉 [#4887](https://github.com/excalidraw/excalidraw/pull/4887)
|
||||
|
||||
- Added optional REACT_APP_WS_SERVER_URL for forks usecases [#4889](https://github.com/excalidraw/excalidraw/pull/4889)
|
||||
|
||||
- Rewrite collab server connecting [#4881](https://github.com/excalidraw/excalidraw/pull/4881)
|
||||
|
||||
- Support vertical text align for bound containers [#4852](https://github.com/excalidraw/excalidraw/pull/4852)
|
||||
|
||||
- Support custom colors 🎉 [#4843](https://github.com/excalidraw/excalidraw/pull/4843)
|
||||
|
||||
- Support Links in Exported SVG [#4791](https://github.com/excalidraw/excalidraw/pull/4791)
|
||||
|
||||
- Scale font size when bound text containers resized with shift pressed [#4828](https://github.com/excalidraw/excalidraw/pull/4828)
|
||||
|
||||
### Fixes
|
||||
|
||||
- Autorelease job name [#5412](https://github.com/excalidraw/excalidraw/pull/5412)
|
||||
|
||||
- Action name for autorelease [#5411](https://github.com/excalidraw/excalidraw/pull/5411)
|
||||
|
||||
- Typecast file to fix the build [#5410](https://github.com/excalidraw/excalidraw/pull/5410)
|
||||
|
||||
- File handle not persisted when importing excalidraw files [#5372](https://github.com/excalidraw/excalidraw/pull/5372)
|
||||
|
||||
- Library not scrollable when no published items installed [#5352](https://github.com/excalidraw/excalidraw/pull/5352)
|
||||
|
||||
- Focus traps inside popovers [#5317](https://github.com/excalidraw/excalidraw/pull/5317)
|
||||
|
||||
- Unable to use cmd/ctrl-delete/backspace in inputs [#5348](https://github.com/excalidraw/excalidraw/pull/5348)
|
||||
|
||||
- Delay loading until language imported [#5344](https://github.com/excalidraw/excalidraw/pull/5344)
|
||||
|
||||
- Command to trigger release [#5347](https://github.com/excalidraw/excalidraw/pull/5347)
|
||||
|
||||
- Remove unnecessary options passed to language detector [#5336](https://github.com/excalidraw/excalidraw/pull/5336)
|
||||
|
||||
- Stale `appState.pendingImageElement` [#5322](https://github.com/excalidraw/excalidraw/pull/5322)
|
||||
|
||||
- Non-letter shortcuts being swallowed by color picker [#5316](https://github.com/excalidraw/excalidraw/pull/5316)
|
||||
|
||||
- Bind text to correct container when nested [#5307](https://github.com/excalidraw/excalidraw/pull/5307)
|
||||
|
||||
- Copy bound text style when copying element having bound text [#5305](https://github.com/excalidraw/excalidraw/pull/5305)
|
||||
|
||||
- Copy arrow head when using copy styles [#5303](https://github.com/excalidraw/excalidraw/pull/5303)
|
||||
|
||||
- Unsafely accessing draggingElement [#5216](https://github.com/excalidraw/excalidraw/pull/5216)
|
||||
|
||||
- Library load button does not work [#5205](https://github.com/excalidraw/excalidraw/pull/5205)
|
||||
|
||||
- Do not deselect when not zooming using touchscreen pinch [#5181](https://github.com/excalidraw/excalidraw/pull/5181)
|
||||
|
||||
- Wheel zoom normalization [#5165](https://github.com/excalidraw/excalidraw/pull/5165)
|
||||
|
||||
- Hide sidebar when `custom` tool active [#5179](https://github.com/excalidraw/excalidraw/pull/5179)
|
||||
|
||||
- Don't save deleted ExcalidrawElements to Firebase [#5108](https://github.com/excalidraw/excalidraw/pull/5108)
|
||||
|
||||
- Eraser removed deleted elements [#5155](https://github.com/excalidraw/excalidraw/pull/5155)
|
||||
|
||||
- Handle `ColorPicker` parentSelector being undefined [#5152](https://github.com/excalidraw/excalidraw/pull/5152)
|
||||
|
||||
- Library multiselect not accounting for published state [#5132](https://github.com/excalidraw/excalidraw/pull/5132)
|
||||
|
||||
- Chart display fix [#5154](https://github.com/excalidraw/excalidraw/pull/5154)
|
||||
|
||||
- Update opacity of bound text when opacity of container updated [#5142](https://github.com/excalidraw/excalidraw/pull/5142)
|
||||
|
||||
- Jumping of text when typing single line in bound text [#5139](https://github.com/excalidraw/excalidraw/pull/5139)
|
||||
|
||||
- Remove opacity scroll wheel interaction [#5111](https://github.com/excalidraw/excalidraw/pull/5111)
|
||||
|
||||
- Propagate keydown events from excalidraw-wysiwyg inputs [#5099](https://github.com/excalidraw/excalidraw/pull/5099)
|
||||
|
||||
- Don't bind text to container if double clicked else instead of center [#5105](https://github.com/excalidraw/excalidraw/pull/5105)
|
||||
|
||||
- ToolIcon height not using rem [#5092](https://github.com/excalidraw/excalidraw/pull/5092)
|
||||
|
||||
- Excalidraw named export type [#5078](https://github.com/excalidraw/excalidraw/pull/5078)
|
||||
|
||||
- BoundElementIds when arrows bound to elements are deleted [#5077](https://github.com/excalidraw/excalidraw/pull/5077)
|
||||
|
||||
- Don't merge libraryItems on updateScene [#5076](https://github.com/excalidraw/excalidraw/pull/5076)
|
||||
|
||||
- SVG metadata extraction regex on multiline elements [#5074](https://github.com/excalidraw/excalidraw/pull/5074)
|
||||
|
||||
- Eraser cursor showing on theme change when not using eraser [#4990](https://github.com/excalidraw/excalidraw/pull/4990)
|
||||
|
||||
- Update `storage.rules` [#5020](https://github.com/excalidraw/excalidraw/pull/5020)
|
||||
|
||||
- Add image button not working on iPad [#5038](https://github.com/excalidraw/excalidraw/pull/5038)
|
||||
|
||||
- Ensure svg image dimensions are always set [#5044](https://github.com/excalidraw/excalidraw/pull/5044)
|
||||
|
||||
- Pinch zoom in view mode [#5001](https://github.com/excalidraw/excalidraw/pull/5001)
|
||||
|
||||
- Select whole group on righclick & few lock-related fixes [#5022](https://github.com/excalidraw/excalidraw/pull/5022)
|
||||
|
||||
- Export serializeLibraryAsJSON from the package [#5017](https://github.com/excalidraw/excalidraw/pull/5017)
|
||||
|
||||
- Support copying PNG to clipboard on Safari [#3746](https://github.com/excalidraw/excalidraw/pull/3746)
|
||||
|
||||
- More copyText fixes [#5016](https://github.com/excalidraw/excalidraw/pull/5016)
|
||||
|
||||
- Copy to clipboard all text nodes as text [#5014](https://github.com/excalidraw/excalidraw/pull/5014)
|
||||
|
||||
- Update cursorButton once freedraw is released [#4996](https://github.com/excalidraw/excalidraw/pull/4996)
|
||||
|
||||
- Decouple actionFinalize and actionErase [#4984](https://github.com/excalidraw/excalidraw/pull/4984)
|
||||
|
||||
- Using stale state when switching tools [#4989](https://github.com/excalidraw/excalidraw/pull/4989)
|
||||
|
||||
- UpdateWysiwygStyle updatedElement is undefined TypeError [#4980](https://github.com/excalidraw/excalidraw/pull/4980)
|
||||
|
||||
- Adding check for link length to prevent early return [#4982](https://github.com/excalidraw/excalidraw/pull/4982)
|
||||
|
||||
- Show link icon for bound text containers [#4960](https://github.com/excalidraw/excalidraw/pull/4960)
|
||||
|
||||
- Cancel erase elements on pointer up if eraser is not active on pointer up [#4956](https://github.com/excalidraw/excalidraw/pull/4956)
|
||||
|
||||
- Restore original opacities when alt pressed while erasing [#4954](https://github.com/excalidraw/excalidraw/pull/4954)
|
||||
|
||||
- Don't bind text to container if already present [#4946](https://github.com/excalidraw/excalidraw/pull/4946)
|
||||
|
||||
- Erase all elements which are hit with single point click [#4934](https://github.com/excalidraw/excalidraw/pull/4934)
|
||||
|
||||
- Add multiElement-edit finalize action to Desktop (currently only visible in Mobile view) [#4764](https://github.com/excalidraw/excalidraw/pull/4764)
|
||||
|
||||
- Hide eraser in view mode in desktop [#4929](https://github.com/excalidraw/excalidraw/pull/4929)
|
||||
|
||||
- Undo when erasing elements by clicking [#4921](https://github.com/excalidraw/excalidraw/pull/4921)
|
||||
|
||||
- Undo when erasing [#4900](https://github.com/excalidraw/excalidraw/pull/4900)
|
||||
|
||||
- Incorrectly erasing on mobile [#4899](https://github.com/excalidraw/excalidraw/pull/4899)
|
||||
|
||||
- Don't crash on drop highlighted text onto canvas [#4890](https://github.com/excalidraw/excalidraw/pull/4890)
|
||||
|
||||
- Paste styles shortcut [#4886](https://github.com/excalidraw/excalidraw/pull/4886)
|
||||
|
||||
- Freedraw element's background fill color missing from SVG when exporting with package API exportToSvg() [#4871](https://github.com/excalidraw/excalidraw/pull/4871)
|
||||
|
||||
- Improve pointer syncing performance [#4883](https://github.com/excalidraw/excalidraw/pull/4883)
|
||||
|
||||
- Collab room initialization [#4882](https://github.com/excalidraw/excalidraw/pull/4882)
|
||||
|
||||
- Ensure verticalAlign properties not shown when no element selected [#4860](https://github.com/excalidraw/excalidraw/pull/4860)
|
||||
|
||||
- Binding text to non-bindable containers and not always preferring selection [#4655](https://github.com/excalidraw/excalidraw/pull/4655)
|
||||
|
||||
- Don't show align icons for single bound container element [#4846](https://github.com/excalidraw/excalidraw/pull/4846)
|
||||
|
||||
- Redraw text bounding box when pasting styles [#4845](https://github.com/excalidraw/excalidraw/pull/4845)
|
||||
|
||||
- Restore cursor position after bound text container value updated [#4836](https://github.com/excalidraw/excalidraw/pull/4836)
|
||||
|
||||
- Support resizing multiple bound text containers [#4824](https://github.com/excalidraw/excalidraw/pull/4824)
|
||||
|
||||
- Also check overflowY: overlay in detectScroll [#4806](https://github.com/excalidraw/excalidraw/pull/4806)
|
||||
|
||||
- Stuck resizing when resizing bound text container very fast beyond threshold [#4804](https://github.com/excalidraw/excalidraw/pull/4804)
|
||||
|
||||
### Refactor
|
||||
|
||||
- Don't pass array to handleBindTextResize [#4826](https://github.com/excalidraw/excalidraw/pull/4826)
|
||||
|
||||
### Build
|
||||
|
||||
- Extract all i18n files into locales folder [#5419](https://github.com/excalidraw/excalidraw/pull/5419)
|
||||
|
||||
- Automate release step fully [#5414](https://github.com/excalidraw/excalidraw/pull/5414)
|
||||
|
||||
- Use next and preview tags instead of separate packages for next and preview release [#5346](https://github.com/excalidraw/excalidraw/pull/5346)
|
||||
|
||||
- Support runtime React Jsx in @excalidraw/utils [#4866](https://github.com/excalidraw/excalidraw/pull/4866)
|
||||
|
||||
- Release @excalidraw/utils 0.1.1 [#4862](https://github.com/excalidraw/excalidraw/pull/4862)
|
||||
|
||||
- Remove build packages workflow [#4835](https://github.com/excalidraw/excalidraw/pull/4835)
|
||||
|
||||
---
|
||||
|
||||
## 0.11.0 (2022-02-17)
|
||||
|
||||
## Excalidraw API
|
||||
|
||||
### Features
|
||||
|
||||
- Add `onLinkOpen` prop which will be triggered when clicked on element hyperlink if present [#4694](https://github.com/excalidraw/excalidraw/pull/4694).
|
||||
- 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 overridden. Check [readme](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#customizing-styles) on how to do so [#4387](https://github.com/excalidraw/excalidraw/pull/4387).
|
||||
|
||||
- [`exportToBlob`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#exportToBlob) now automatically sets `appState.exportBackground` to `true` if exporting to `image/jpeg` MIME type (to ensure that alpha channel is not compressed to black color) [#4342](https://github.com/excalidraw/excalidraw/pull/4342).
|
||||
|
||||
#### BREAKING CHANGE
|
||||
|
||||
Remove `getElementMap` util [#4306](https://github.com/excalidraw/excalidraw/pull/4306).
|
||||
|
||||
- Changes to [`exportToCanvas`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#exportToCanvas) util function [#4321](https://github.com/excalidraw/excalidraw/pull/4321):
|
||||
|
||||
- Add `maxWidthOrHeight?: number` attribute.
|
||||
- `scale` returned from `getDimensions()` is now optional (default to `1`).
|
||||
|
||||
- Image support added for host [PR](https://github.com/excalidraw/excalidraw/pull/4011)
|
||||
|
||||
General notes:
|
||||
|
||||
- File data are encoded as DataURLs (base64) for portability reasons.
|
||||
|
||||
[ExcalidrawAPI](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#onLibraryChange):
|
||||
|
||||
- added `getFiles()` to get current `BinaryFiles` (`Record<FileId, BinaryFileData>`). It may contain files that aren't referenced by any element, so if you're persisting the files to a storage, you should compare them against stored elements.
|
||||
|
||||
Excalidraw app props:
|
||||
|
||||
- added `generateIdForFile(file: File)` optional prop so you can generate your own ids for added files.
|
||||
- `onChange(elements, appState, files)` prop callback is now passed `BinaryFiles` as third argument.
|
||||
- `onPaste(data, event)` data prop should contain `data.files` (`BinaryFiles`) if the elements pasted are referencing new files.
|
||||
- `initialData` object now supports additional `files` (`BinaryFiles`) attribute.
|
||||
|
||||
Other notes:
|
||||
|
||||
- `.excalidraw` files may now contain top-level `files` key in format of `Record<FileId, BinaryFileData>` when exporting any (image) elements.
|
||||
- Changes were made to various export utilities exported from the package so that they take `files`, you can refer to the docs for the same.
|
||||
|
||||
- Export [`isLinearElement`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#isLinearElement) and [`getNonDeletedElements`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#getNonDeletedElements) [#4072](https://github.com/excalidraw/excalidraw/pull/4072).
|
||||
|
||||
- Support [`renderTopRightUI`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#renderTopRightUI) in mobile UI [#4065](https://github.com/excalidraw/excalidraw/pull/4065).
|
||||
|
||||
- Export `THEME` constant from the package so host can use this when passing the theme [#4055](https://github.com/excalidraw/excalidraw/pull/4055).
|
||||
|
||||
#### BREAKING CHANGE
|
||||
|
||||
The `Appearance` type is now removed and renamed to `Theme` so `Theme` type needs to be used.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Reset `unmounted` state on the component once component mounts to fix the mounting/unmounting repeatedly when used with `useEffect` [#4682](https://github.com/excalidraw/excalidraw/pull/4682).
|
||||
- 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 [#4445](https://github.com/excalidraw/excalidraw/pull/4445).
|
||||
|
||||
### Build
|
||||
|
||||
- Release preview package [@excalidraw/excalidraw-preview](https://www.npmjs.com/package/@excalidraw/excalidraw-preview) when triggered via comment
|
||||
|
||||
```
|
||||
@excalibot trigger release
|
||||
```
|
||||
|
||||
[#4750](https://github.com/excalidraw/excalidraw/pull/4750).
|
||||
|
||||
- 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` [#4488](https://github.com/excalidraw/excalidraw/pull/4488)
|
||||
|
||||
- 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).
|
||||
|
||||
---
|
||||
|
||||
## Excalidraw Library
|
||||
|
||||
**_This section lists the updates made to the excalidraw library and will not affect the integration._**
|
||||
|
||||
### Features
|
||||
|
||||
- Show group/group and link action in mobile [#4795](https://github.com/excalidraw/excalidraw/pull/4795)
|
||||
|
||||
- Support background fill for freedraw shapes [#4610](https://github.com/excalidraw/excalidraw/pull/4610)
|
||||
|
||||
- Keep selected tool on canvas reset [#4728](https://github.com/excalidraw/excalidraw/pull/4728)
|
||||
|
||||
- Make whole element clickable in view mode when it has hyperlink [#4735](https://github.com/excalidraw/excalidraw/pull/4735)
|
||||
|
||||
- Allow any precision when zooming [#4730](https://github.com/excalidraw/excalidraw/pull/4730)
|
||||
|
||||
- Throttle `pointermove` events per framerate [#4727](https://github.com/excalidraw/excalidraw/pull/4727)
|
||||
|
||||
- Support hyperlinks 🔥 [#4620](https://github.com/excalidraw/excalidraw/pull/4620)
|
||||
|
||||
- Added penMode for palm rejection [#4657](https://github.com/excalidraw/excalidraw/pull/4657)
|
||||
|
||||
- Support unbinding bound text [#4686](https://github.com/excalidraw/excalidraw/pull/4686)
|
||||
|
||||
- Sync local storage state across tabs when out of sync [#4545](https://github.com/excalidraw/excalidraw/pull/4545)
|
||||
|
||||
- Support contextMenuLabel to be of function type to support dynamic labels [#4654](https://github.com/excalidraw/excalidraw/pull/4654)
|
||||
|
||||
- Support decreasing/increasing `fontSize` via keyboard [#4553](https://github.com/excalidraw/excalidraw/pull/4553)
|
||||
|
||||
- Link to new LP for excalidraw plus [#4549](https://github.com/excalidraw/excalidraw/pull/4549)
|
||||
|
||||
- Update stroke color of bounded text along with container [#4541](https://github.com/excalidraw/excalidraw/pull/4541)
|
||||
|
||||
- Hints and shortcuts help around deep selection [#4502](https://github.com/excalidraw/excalidraw/pull/4502)
|
||||
|
||||
- Support updating text properties by clicking on container [#4499](https://github.com/excalidraw/excalidraw/pull/4499)
|
||||
|
||||
- Bind text to shapes when pressing enter and support sticky notes 🎉 [#4343](https://github.com/excalidraw/excalidraw/pull/4343)
|
||||
|
||||
- Change `boundElementIds` → `boundElements` [#4404](https://github.com/excalidraw/excalidraw/pull/4404)
|
||||
|
||||
- Support selecting multiple points when editing line [#4373](https://github.com/excalidraw/excalidraw/pull/4373)
|
||||
|
||||
- Horizontally center toolbar menu [commit link](https://github.com/excalidraw/excalidraw/commit/9b8ee3cacfec239617c357693cf2a3ca9972d2cb)
|
||||
|
||||
- Add support for rounded corners in diamond [#4369](https://github.com/excalidraw/excalidraw/pull/4369)
|
||||
|
||||
- Allow zooming up to 3000% [#4358](https://github.com/excalidraw/excalidraw/pull/4358)
|
||||
|
||||
- Stop discarding precision when rendering [#4357](https://github.com/excalidraw/excalidraw/pull/4357)
|
||||
|
||||
- Support Image binding [#4347](https://github.com/excalidraw/excalidraw/pull/4347)
|
||||
|
||||
- Add `element.updated` [#4070](https://github.com/excalidraw/excalidraw/pull/4070)
|
||||
|
||||
- Compress shareLink data when uploading to json server [#4225](https://github.com/excalidraw/excalidraw/pull/4225)
|
||||
|
||||
- Supply `version` param when installing libraries [#4305](https://github.com/excalidraw/excalidraw/pull/4305)
|
||||
|
||||
- Log FS abortError to console [#4279](https://github.com/excalidraw/excalidraw/pull/4279)
|
||||
|
||||
- Add validation for website and remove validation for library item name [#4269](https://github.com/excalidraw/excalidraw/pull/4269)
|
||||
|
||||
- Allow publishing libraries from UI [#4115](https://github.com/excalidraw/excalidraw/pull/4115)
|
||||
|
||||
- Create confirm dialog to use instead of window.confirm [#4256](https://github.com/excalidraw/excalidraw/pull/4256)
|
||||
|
||||
- Allow letters in IDs for storing files in backend [#4224](https://github.com/excalidraw/excalidraw/pull/4224)
|
||||
|
||||
- Remove support for V1 unencrypted backend [#4189](https://github.com/excalidraw/excalidraw/pull/4189)
|
||||
|
||||
- Use separate backend for local storage [#4187](https://github.com/excalidraw/excalidraw/pull/4187)
|
||||
|
||||
- Add hint around canvas panning [#4159](https://github.com/excalidraw/excalidraw/pull/4159)
|
||||
|
||||
- Stop using production services for development [#4113](https://github.com/excalidraw/excalidraw/pull/4113)
|
||||
|
||||
- Add triangle arrowhead [#4024](https://github.com/excalidraw/excalidraw/pull/4024)
|
||||
|
||||
- Add rewrite to webex landing page [#4102](https://github.com/excalidraw/excalidraw/pull/4102)
|
||||
|
||||
- Switch collab server [#4092](https://github.com/excalidraw/excalidraw/pull/4092)
|
||||
|
||||
- Use dialog component for clear canvas instead of window confirm [#4075](https://github.com/excalidraw/excalidraw/pull/4075)
|
||||
|
||||
### Fixes
|
||||
|
||||
- Rename --color-primary-chubb to --color-primary-contrast-offset and fallback to primary color if not present [#4803](https://github.com/excalidraw/excalidraw/pull/4803)
|
||||
|
||||
- Add commits directly pushed to master in changelog [#4798](https://github.com/excalidraw/excalidraw/pull/4798)
|
||||
|
||||
- Don't bump element version when adding files data [#4794](https://github.com/excalidraw/excalidraw/pull/4794)
|
||||
|
||||
- Mobile link click [#4742](https://github.com/excalidraw/excalidraw/pull/4742)
|
||||
|
||||
- ContextMenu timer & pointers not correctly reset on iOS [#4765](https://github.com/excalidraw/excalidraw/pull/4765)
|
||||
|
||||
- Use absolute coords when rendering link popover [#4753](https://github.com/excalidraw/excalidraw/pull/4753)
|
||||
|
||||
- Changing font size when text is not selected or edited [#4751](https://github.com/excalidraw/excalidraw/pull/4751)
|
||||
|
||||
- Disable contextmenu on non-secondary `pen` events or `touch` [#4675](https://github.com/excalidraw/excalidraw/pull/4675)
|
||||
|
||||
- Mobile context menu won't show on long press [#4741](https://github.com/excalidraw/excalidraw/pull/4741)
|
||||
|
||||
- Do not open links twice [#4738](https://github.com/excalidraw/excalidraw/pull/4738)
|
||||
|
||||
- Make link icon clickable in mobile [#4736](https://github.com/excalidraw/excalidraw/pull/4736)
|
||||
|
||||
- Apple Pen missing strokes [#4705](https://github.com/excalidraw/excalidraw/pull/4705)
|
||||
|
||||
- Freedraw slow movement jittery lines [#4726](https://github.com/excalidraw/excalidraw/pull/4726)
|
||||
|
||||
- Disable three finger pinch zoom in penMode [#4725](https://github.com/excalidraw/excalidraw/pull/4725)
|
||||
|
||||
- Remove click listener for opening popup [#4700](https://github.com/excalidraw/excalidraw/pull/4700)
|
||||
|
||||
- Link popup position not accounting for offsets [#4695](https://github.com/excalidraw/excalidraw/pull/4695)
|
||||
|
||||
- PenMode darkmode style [#4692](https://github.com/excalidraw/excalidraw/pull/4692)
|
||||
|
||||
- Typing `_+` in wysiwyg not working [#4681](https://github.com/excalidraw/excalidraw/pull/4681)
|
||||
|
||||
- Keyboard-zooming in wysiwyg should zoom canvas [#4676](https://github.com/excalidraw/excalidraw/pull/4676)
|
||||
|
||||
- SceneCoordsToViewportCoords, jumping text when there is an offset [#4413](https://github.com/excalidraw/excalidraw/pull/4413) (#4630)
|
||||
|
||||
- Right-click object menu displays partially off-screen [#4572](https://github.com/excalidraw/excalidraw/pull/4572) (#4631)
|
||||
|
||||
- Support collaboration in bound text [#4573](https://github.com/excalidraw/excalidraw/pull/4573)
|
||||
|
||||
- Cmd/ctrl native browser behavior blocked in inputs [#4589](https://github.com/excalidraw/excalidraw/pull/4589)
|
||||
|
||||
- Use cached width when calculating min width during resize [#4585](https://github.com/excalidraw/excalidraw/pull/4585)
|
||||
|
||||
- Support collaboration in bounded text [#4580](https://github.com/excalidraw/excalidraw/pull/4580)
|
||||
|
||||
- Port for collab server and update docs [#4569](https://github.com/excalidraw/excalidraw/pull/4569)
|
||||
|
||||
- Don't mutate the bounded text if not updated when submitted [#4543](https://github.com/excalidraw/excalidraw/pull/4543)
|
||||
|
||||
- Prevent canvas drag while editing text [#4552](https://github.com/excalidraw/excalidraw/pull/4552)
|
||||
|
||||
- Support shift+P for freedraw [#4550](https://github.com/excalidraw/excalidraw/pull/4550)
|
||||
|
||||
- Prefer spreadsheet data over image [#4533](https://github.com/excalidraw/excalidraw/pull/4533)
|
||||
|
||||
- Show text properties button states correctly for bounded text [#4542](https://github.com/excalidraw/excalidraw/pull/4542)
|
||||
|
||||
- Rotate bounded text when container is rotated before typing [#4535](https://github.com/excalidraw/excalidraw/pull/4535)
|
||||
|
||||
- Undo should work when selecting bounded textr [#4537](https://github.com/excalidraw/excalidraw/pull/4537)
|
||||
|
||||
- Reduce padding to 5px for bounded text [#4530](https://github.com/excalidraw/excalidraw/pull/4530)
|
||||
|
||||
- Bound text doesn't inherit container [#4521](https://github.com/excalidraw/excalidraw/pull/4521)
|
||||
|
||||
- Text wrapping with grid [#4505](https://github.com/excalidraw/excalidraw/pull/4505) (#4506)
|
||||
|
||||
- Check if process is defined before using so it works in browser [#4497](https://github.com/excalidraw/excalidraw/pull/4497)
|
||||
|
||||
- Pending review fixes for sticky notes [#4493](https://github.com/excalidraw/excalidraw/pull/4493)
|
||||
|
||||
- Pasted elements except binded text once paste action is complete [#4472](https://github.com/excalidraw/excalidraw/pull/4472)
|
||||
|
||||
- Don't select binded text when ungrouping [#4470](https://github.com/excalidraw/excalidraw/pull/4470)
|
||||
|
||||
- Set height correctly when text properties updated while editing in container until first submit [#4469](https://github.com/excalidraw/excalidraw/pull/4469)
|
||||
|
||||
- Align and distribute binded text in container and cleanup [#4468](https://github.com/excalidraw/excalidraw/pull/4468)
|
||||
|
||||
- Move binded text when moving container using keyboard [#4466](https://github.com/excalidraw/excalidraw/pull/4466)
|
||||
|
||||
- Support dragging binded text in container selected in a group [#4462](https://github.com/excalidraw/excalidraw/pull/4462)
|
||||
|
||||
- Vertically align single line when deleting text in bounded container [#4460](https://github.com/excalidraw/excalidraw/pull/4460)
|
||||
|
||||
- Update height correctly when updating text properties in binded text [#4459](https://github.com/excalidraw/excalidraw/pull/4459)
|
||||
|
||||
- Align library item previews to center [#4447](https://github.com/excalidraw/excalidraw/pull/4447)
|
||||
|
||||
- Vertically center align text when text deleted [#4457](https://github.com/excalidraw/excalidraw/pull/4457)
|
||||
|
||||
- Vertically center the first line as user starts typing in container [#4454](https://github.com/excalidraw/excalidraw/pull/4454)
|
||||
|
||||
- Switch cursor to center of container when adding text when dimensions are too small [#4452](https://github.com/excalidraw/excalidraw/pull/4452)
|
||||
|
||||
- Vertically center align the bounded text correctly when zoomed [#4444](https://github.com/excalidraw/excalidraw/pull/4444)
|
||||
|
||||
- Support updating stroke color for text by typing in color picker input [#4415](https://github.com/excalidraw/excalidraw/pull/4415)
|
||||
|
||||
- Bound text not atomic with container when changing z-index [#4414](https://github.com/excalidraw/excalidraw/pull/4414)
|
||||
|
||||
- Update viewport coords correctly when editing text [#4416](https://github.com/excalidraw/excalidraw/pull/4416)
|
||||
|
||||
- Use word-break break-word only and update text editor height only when binded to container [#4410](https://github.com/excalidraw/excalidraw/pull/4410)
|
||||
|
||||
- Husky not able to execute pre-commit on windows [#4370](https://github.com/excalidraw/excalidraw/pull/4370)
|
||||
|
||||
- Make firebase config parsing not fail on undefined env [#4381](https://github.com/excalidraw/excalidraw/pull/4381)
|
||||
|
||||
- Adding to library via contextmenu when no image is selected [#4356](https://github.com/excalidraw/excalidraw/pull/4356)
|
||||
|
||||
- Export scale quality regression [#4316](https://github.com/excalidraw/excalidraw/pull/4316)
|
||||
|
||||
- Remove `100%` height from tooltip container to fix layout issues [#3980](https://github.com/excalidraw/excalidraw/pull/3980)
|
||||
|
||||
- Inline ENV variables when building excalidraw package [#4311](https://github.com/excalidraw/excalidraw/pull/4311)
|
||||
|
||||
- SVG export in dark mode with embedded bitmap image [#4285](https://github.com/excalidraw/excalidraw/pull/4285)
|
||||
|
||||
- New FS API not working on Linux [#4280](https://github.com/excalidraw/excalidraw/pull/4280)
|
||||
|
||||
- Url -> URL for consistency [#4277](https://github.com/excalidraw/excalidraw/pull/4277)
|
||||
|
||||
- Prevent adding images to library via contextMenu [#4264](https://github.com/excalidraw/excalidraw/pull/4264)
|
||||
|
||||
- Account for libraries v2 when prompting [#4263](https://github.com/excalidraw/excalidraw/pull/4263)
|
||||
|
||||
- Skia rendering issues [#4200](https://github.com/excalidraw/excalidraw/pull/4200)
|
||||
|
||||
- Ellipse roughness when `0` [#4194](https://github.com/excalidraw/excalidraw/pull/4194)
|
||||
|
||||
- Proper string for invalid SVG [#4191](https://github.com/excalidraw/excalidraw/pull/4191)
|
||||
|
||||
- Images not initialized correctly [#4157](https://github.com/excalidraw/excalidraw/pull/4157)
|
||||
|
||||
- Image-related fixes [#4147](https://github.com/excalidraw/excalidraw/pull/4147)
|
||||
|
||||
- Rewrite collab element reconciliation to fix z-index issues [#4076](https://github.com/excalidraw/excalidraw/pull/4076)
|
||||
|
||||
- Redirect excalidraw.com/about to for-webex.excalidraw.com [#4104](https://github.com/excalidraw/excalidraw/pull/4104)
|
||||
|
||||
- Redirect to webex LP instead of rewrite to fix SW [#4103](https://github.com/excalidraw/excalidraw/pull/4103)
|
||||
|
||||
- Clear image/shape cache of affected elements when adding files [#4089](https://github.com/excalidraw/excalidraw/pull/4089)
|
||||
|
||||
- Clear `LibraryUnit` DOM on unmount [#4084](https://github.com/excalidraw/excalidraw/pull/4084)
|
||||
|
||||
- Pasting images on firefox [#4085](https://github.com/excalidraw/excalidraw/pull/4085)
|
||||
|
||||
### Refactor
|
||||
|
||||
- Simplify zoom by removing `zoom.translation` [#4477](https://github.com/excalidraw/excalidraw/pull/4477)
|
||||
|
||||
- Deduplicate encryption helpers [#4146](https://github.com/excalidraw/excalidraw/pull/4146)
|
||||
|
||||
### Performance
|
||||
|
||||
- Cache approx line height in textwysiwg [#4651](https://github.com/excalidraw/excalidraw/pull/4651)
|
||||
|
||||
### Build
|
||||
|
||||
- Rename release command to 'release package' [#4783](https://github.com/excalidraw/excalidraw/pull/4783)
|
||||
|
||||
- Deploy excalidraw package example [#4762](https://github.com/excalidraw/excalidraw/pull/4762)
|
||||
|
||||
- Allow package.json changes when autoreleasing next [#4068](https://github.com/excalidraw/excalidraw/pull/4068)
|
||||
|
||||
---
|
||||
Check out the [release notes](https://github.com/excalidraw/excalidraw/releases/tag/v0.11.0)
|
||||
|
||||
## 0.10.0 (2021-10-13)
|
||||
|
||||
## Excalidraw API
|
||||
|
||||
### Fixes
|
||||
|
||||
- Don't show save file to disk button in export dialog when `saveFileToDisk` passed as `false` in [`UIOptions.canvasActions.export`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#exportOpts) [#4073](https://github.com/excalidraw/excalidraw/pull/4073).
|
||||
|
||||
- [`onPaste`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#onPaste) prop should return false to prevent the native excalidraw paste action [#3974](https://github.com/excalidraw/excalidraw/pull/3974).
|
||||
|
||||
#### BREAKING CHANGE
|
||||
|
||||
- Earlier the paste action was prevented when the prop [`onPaste`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#onPaste) returned true, but now it should return false to prevent the paste action. This was done to make it semantically more correct and intuitive.
|
||||
|
||||
### Build
|
||||
|
||||
- Enable jsx transform in webpack [#4049](https://github.com/excalidraw/excalidraw/pull/4049)
|
||||
|
||||
### Docs
|
||||
|
||||
- Correct exportToBackend in README to onExportToBackend [#3952](https://github.com/excalidraw/excalidraw/pull/3952)
|
||||
|
||||
## Excalidraw Library
|
||||
|
||||
**_This section lists the updates made to the excalidraw library and will not affect the integration._**
|
||||
|
||||
### Features
|
||||
|
||||
- Improve freedraw shape [#3984](https://github.com/excalidraw/excalidraw/pull/3984)
|
||||
|
||||
- Make color ARIA labels better [#3871](https://github.com/excalidraw/excalidraw/pull/3871)
|
||||
|
||||
- Add origin trial tokens [#3853](https://github.com/excalidraw/excalidraw/pull/3853)
|
||||
|
||||
- Re-order zoom buttons [#3837](https://github.com/excalidraw/excalidraw/pull/3837)
|
||||
|
||||
- Add undo/redo buttons & tweak footer [#3832](https://github.com/excalidraw/excalidraw/pull/3832)
|
||||
|
||||
- Resave to png/svg with metadata if you loaded your scene from a png/svg file [#3645](https://github.com/excalidraw/excalidraw/pull/3645)
|
||||
|
||||
### Fixes
|
||||
|
||||
- Abstract and fix legacy fs [#4032](https://github.com/excalidraw/excalidraw/pull/4032)
|
||||
|
||||
- Context menu positioning [#4025](https://github.com/excalidraw/excalidraw/pull/4025)
|
||||
|
||||
- Added alert for bad encryption key [#3998](https://github.com/excalidraw/excalidraw/pull/3998)
|
||||
|
||||
- OnPaste should return false to prevent paste action [#3974](https://github.com/excalidraw/excalidraw/pull/3974)
|
||||
|
||||
- Help-icon now visible on Safari [#3939](https://github.com/excalidraw/excalidraw/pull/3939)
|
||||
|
||||
- Permanent zoom mode [#3931](https://github.com/excalidraw/excalidraw/pull/3931)
|
||||
|
||||
- Undo/redo buttons gap in Safari [#3836](https://github.com/excalidraw/excalidraw/pull/3836)
|
||||
|
||||
- Prevent gradual canvas misalignment [#3833](https://github.com/excalidraw/excalidraw/pull/3833)
|
||||
|
||||
- Color picker shortcuts not working when elements selected [#3817](https://github.com/excalidraw/excalidraw/pull/3817)
|
||||
|
||||
---
|
||||
Check out the [release notes](https://github.com/excalidraw/excalidraw/releases/tag/v0.10.0)
|
||||
|
||||
## 0.9.0 (2021-07-10)
|
||||
|
||||
## Excalidraw API
|
||||
|
||||
### Features
|
||||
|
||||
- [`restore(data, localAppState, localElements)`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#restore) and [`restoreElements(elements, localElements)`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#restoreElements) now take `localElements` argument which will be used to ensure existing elements' versions are used and incremented. This fixes an issue where importing the same file would resolve to elements with older versions, potentially causing issues when reconciling [#3797](https://github.com/excalidraw/excalidraw/pull/3797).
|
||||
|
||||
#### BREAKING CHANGE
|
||||
|
||||
- `localElements` argument is mandatory (can be `null`/`undefined`) if using TypeScript.
|
||||
|
||||
- Support `appState.exportEmbedScene` attribute in [`exportToSvg`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#exportToSvg) which allows to embed the scene data [#3777](https://github.com/excalidraw/excalidraw/pull/3777).
|
||||
|
||||
#### BREAKING CHANGE
|
||||
|
||||
- The attribute `metadata` is now removed as `metadata` was only used to embed scene data which is now supported with the `appState.exportEmbedScene` attribute.
|
||||
- [`exportToSvg`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#exportToSvg) now resolves to a promise which resolves to `svg` of the exported drawing.
|
||||
|
||||
- Expose [`loadLibraryFromBlob`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#loadLibraryFromBlobY), [`loadFromBlob`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#loadFromBlob), and [`getFreeDrawSvgPath`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#getFreeDrawSvgPath) [#3764](https://github.com/excalidraw/excalidraw/pull/3764).
|
||||
|
||||
- Expose [`FONT_FAMILY`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#FONT_FAMILY) so that consumer can use when passing `initialData.appState.currentItemFontFamily` [#3710](https://github.com/excalidraw/excalidraw/pull/3710).
|
||||
|
||||
- Added prop [`autoFocus`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#autoFocus) to focus the excalidraw component on page load when enabled, defaults to false [#3691](https://github.com/excalidraw/excalidraw/pull/3691).
|
||||
|
||||
Note: Earlier Excalidraw component was focused by default on page load, you need to enable `autoFocus` prop to retain the same behaviour.
|
||||
|
||||
- Added prop `UIOptions.canvasActions.export.renderCustomUI` to support Custom UI rendering inside export dialog [#3666](https://github.com/excalidraw/excalidraw/pull/3666).
|
||||
- Added prop `UIOptions.canvasActions.saveAsImage` to show/hide the **Save as image** button in the canvas actions. Defaults to `true` hence the **Save as Image** button is rendered [#3662](https://github.com/excalidraw/excalidraw/pull/3662).
|
||||
|
||||
- Export dialog can be customised with [`UiOptions.canvasActions.export`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#exportOpts) [#3658](https://github.com/excalidraw/excalidraw/pull/3658).
|
||||
|
||||
Also, [`UIOptions`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#UIOptions) is now memoized to avoid unnecessary rerenders.
|
||||
|
||||
#### BREAKING CHANGE
|
||||
|
||||
- `UIOptions.canvasActions.saveAsScene` is now renamed to `UiOptions.canvasActions.export.saveFileToDisk`. Defaults to `true` hence the **save file to disk** button is rendered inside the export dialog.
|
||||
- `exportToBackend` is now renamed to `UIOptions.canvasActions.export.exportToBackend`. If this prop is not passed, the **shareable-link** button will not be rendered, same as before.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Use excalidraw Id in elements so every element has unique id [#3696](https://github.com/excalidraw/excalidraw/pull/3696).
|
||||
|
||||
### Refactor
|
||||
|
||||
- #### BREAKING CHANGE
|
||||
- Rename `UIOptions.canvasActions.saveScene` to `UIOptions.canvasActions.saveToActiveFile`[#3657](https://github.com/excalidraw/excalidraw/pull/3657).
|
||||
- Removed `shouldAddWatermark: boolean` attribute from options for [export](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#export-utilities) APIs [#3639](https://github.com/excalidraw/excalidraw/pull/3639).
|
||||
- Removed `appState.shouldAddWatermark` so in case you were passing `shouldAddWatermark` in [initialData.AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L42) it will not work anymore.
|
||||
|
||||
## Excalidraw Library
|
||||
|
||||
**_This section lists the updates made to the excalidraw library and will not affect the integration._**
|
||||
|
||||
### Features
|
||||
|
||||
- Switch to selection tool on library item insert [#3773](https://github.com/excalidraw/excalidraw/pull/3773)
|
||||
|
||||
- Show active file name when saving to current file [#3733](https://github.com/excalidraw/excalidraw/pull/3733)
|
||||
|
||||
- Add hint around text editing [#3708](https://github.com/excalidraw/excalidraw/pull/3708)
|
||||
|
||||
- Change library icon to be more clear [#3583](https://github.com/excalidraw/excalidraw/pull/3583)
|
||||
|
||||
- Pass current `theme` when installing libraries [#3701](https://github.com/excalidraw/excalidraw/pull/3701)
|
||||
|
||||
- Update virgil font [#3692](https://github.com/excalidraw/excalidraw/pull/3692)
|
||||
|
||||
- Support exporting json to excalidraw plus [#3678](https://github.com/excalidraw/excalidraw/pull/3678)
|
||||
|
||||
- Save exportScale in AppState [#3580](https://github.com/excalidraw/excalidraw/pull/3580)
|
||||
|
||||
- Add shortcuts for stroke and background color picker [#3318](https://github.com/excalidraw/excalidraw/pull/3318)
|
||||
|
||||
- Exporting redesign [#3613](https://github.com/excalidraw/excalidraw/pull/3613)
|
||||
|
||||
- Auto-position tooltip and support overflowing container [#3631](https://github.com/excalidraw/excalidraw/pull/3631)
|
||||
|
||||
- Auto release @excalidraw/excalidraw-next on every change [#3614](https://github.com/excalidraw/excalidraw/pull/3614)
|
||||
|
||||
- Allow inner-drag-selecting with cmd/ctrl [#3603](https://github.com/excalidraw/excalidraw/pull/3603)
|
||||
|
||||
### Fixes
|
||||
|
||||
- view mode cursor adjustments [#3809](https://github.com/excalidraw/excalidraw/pull/3809).
|
||||
|
||||
- Pass next release to updatePackageVersion & replace ## unreleased with new version [#3806](https://github.com/excalidraw/excalidraw/pull/3806)
|
||||
|
||||
- Include deleted elements when passing to restore [#3802](https://github.com/excalidraw/excalidraw/pull/3802)
|
||||
|
||||
- Import React before using jsx [#3804](https://github.com/excalidraw/excalidraw/pull/3804)
|
||||
|
||||
- Ensure `s` and `g` shortcuts work on no selection [#3800](https://github.com/excalidraw/excalidraw/pull/3800)
|
||||
|
||||
- Keep binding for attached arrows after changing text [#3754](https://github.com/excalidraw/excalidraw/pull/3754)
|
||||
|
||||
- Deselect elements on viewMode toggle [#3741](https://github.com/excalidraw/excalidraw/pull/3741)
|
||||
|
||||
- Allow pointer events for disable zen mode button [#3743](https://github.com/excalidraw/excalidraw/pull/3743)
|
||||
|
||||
- Use rgba instead of shorthand alpha [#3688](https://github.com/excalidraw/excalidraw/pull/3688)
|
||||
|
||||
- Color pickers not opening on mobile [#3676](https://github.com/excalidraw/excalidraw/pull/3676)
|
||||
|
||||
- On contextMenu, use selected element regardless of z-index [#3668](https://github.com/excalidraw/excalidraw/pull/3668)
|
||||
|
||||
- SelectedGroupIds not being stored in history [#3630](https://github.com/excalidraw/excalidraw/pull/3630)
|
||||
|
||||
- Overscroll on touch devices [#3663](https://github.com/excalidraw/excalidraw/pull/3663)
|
||||
|
||||
- Small UI issues around image export dialog [#3642](https://github.com/excalidraw/excalidraw/pull/3642)
|
||||
|
||||
- Normalize linear element points on restore [#3633](https://github.com/excalidraw/excalidraw/pull/3633)
|
||||
|
||||
- Disable pointer-events on footer-center container [#3629](https://github.com/excalidraw/excalidraw/pull/3629)
|
||||
|
||||
### Refactor
|
||||
|
||||
- Delete React SyntheticEvent persist [#3700](https://github.com/excalidraw/excalidraw/pull/3700)
|
||||
|
||||
- Code clean up [#3681](https://github.com/excalidraw/excalidraw/pull/3681)
|
||||
|
||||
### Performance
|
||||
|
||||
- Improve arrow head sizing [#3480](https://github.com/excalidraw/excalidraw/pull/3480)
|
||||
|
||||
### Build
|
||||
|
||||
- Add release script to update relevant files and commit for next release [#3805](https://github.com/excalidraw/excalidraw/pull/3805)
|
||||
|
||||
- Add script to update changelog before a stable release [#3784](https://github.com/excalidraw/excalidraw/pull/3784)
|
||||
|
||||
- Add script to update readme before stable release [#3781](https://github.com/excalidraw/excalidraw/pull/3781)
|
||||
|
||||
---
|
||||
Check out the [release notes](https://github.com/excalidraw/excalidraw/releases/tag/v0.9.0)
|
||||
|
||||
## 0.8.0 (2021-05-15)
|
||||
|
||||
|
||||
@@ -482,8 +482,8 @@ You can add `customData` to elements when passing them as `initialData`, or usin
|
||||
|
||||
You can pass a `ref` when you want to access some excalidraw APIs. We expose the below APIs:
|
||||
|
||||
| API | signature | Usage |
|
||||
| --- | --- | --- | --- |
|
||||
| API | Signature | Usage |
|
||||
| --- | --- | --- |
|
||||
| ready | `boolean` | This is set to true once Excalidraw is rendered |
|
||||
| readyPromise | [resolvablePromise](https://github.com/excalidraw/excalidraw/blob/master/src/utils.ts#L317) | This promise will be resolved with the api once excalidraw has rendered. This will be helpful when you want do some action on the host app once this promise resolves. For this to work you will have to pass ref as shown [here](#readyPromise) |
|
||||
| [updateScene](#updateScene) | <code>(scene: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L207">sceneData</a>) => void </code> | updates the scene with the sceneData |
|
||||
@@ -496,11 +496,11 @@ You can pass a `ref` when you want to access some excalidraw APIs. We expose the
|
||||
| history | `{ clear: () => void }` | This is the history API. `history.clear()` will clear the history |
|
||||
| scrollToContent | <code> (target?: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106">ExcalidrawElement</a> | <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106">ExcalidrawElement</a>[]) => void </code> | Scroll the nearest element out of the elements supplied to the center. Defaults to the elements on the scene. |
|
||||
| refresh | `() => void` | Updates the offsets for the Excalidraw component so that the coordinates are computed correctly (for example the cursor position). You don't have to call this when the position is changed on page scroll or when the excalidraw container resizes (we handle that ourselves). For any other cases if the position of excalidraw is updated (example due to scroll on parent container and not page scroll) you should call this API. |
|
||||
| [importLibrary](#importlibrary) | `(url: string, token?: string) => void` | Imports library from given URL |
|
||||
| [setToast](#setToast) | `({message: string, closable?:boolean, duration?:number} | null) => void` | This API can be used to show the toast with custom message. |
|
||||
| [importLibrary](#importlibrary) | <code>(url: string, token?: string) => void</code> | Imports library from given URL |
|
||||
| [setToast](#setToast) | <code>({ message: string, closable?:boolean, duration?:number } | null) => void</code> | This API can be used to show the toast with custom message. |
|
||||
| [id](#id) | string | Unique ID for the excalidraw component. |
|
||||
| [getFiles](#getFiles) | <code>() => <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L64">files</a> </code> | This API can be used to get the files present in the scene. It may contain files that aren't referenced by any element, so if you're persisting the files to a storage, you should compare them against stored elements. |
|
||||
| [setActiveTool](#setActiveTool) | <code>(tool: { type: typeof <a href="https://github.com/excalidraw/excalidraw/blob/master/src/shapes.tsx#L4">SHAPES</a>[number]["value"] | "eraser" } | { type: "custom"; customType: string }) => void</code> | This API can be used to set the active tool |
|
||||
| [setActiveTool](#setActiveTool) | <code>(tool: { type: typeof <a href="https://github.com/excalidraw/excalidraw/blob/master/src/shapes.tsx#L4">SHAPES</a> [number]["value"]| "eraser" } | { type: "custom"; customType: string }) => void</code> | This API can be used to set the active tool |
|
||||
| [setCursor](#setCursor) | <code>(cursor: string) => void </code> | This API can be used to set customise the mouse cursor on the canvas |
|
||||
| [resetCursor](#resetCursor) | <code>() => void </code> | This API can be used to reset to default mouse cursor on the canvas |
|
||||
|
||||
@@ -929,7 +929,8 @@ This function normalizes library items elements, adding missing values when need
|
||||
elements,
|
||||
appState
|
||||
getDimensions,
|
||||
files
|
||||
files,
|
||||
exportPadding?: number;
|
||||
}: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/packages/utils.ts#L12">ExportOpts</a>
|
||||
</pre>
|
||||
|
||||
@@ -940,6 +941,7 @@ This function normalizes library items elements, adding missing values when need
|
||||
| getDimensions | `(width: number, height: number) => { width: number, height: number, scale?: number }` | undefined | A function which returns the `width`, `height`, and optionally `scale` (defaults `1`), with which canvas is to be exported. |
|
||||
| maxWidthOrHeight | `number` | undefined | The maximum width or height of the exported image. If provided, `getDimensions` is ignored. |
|
||||
| files | [BinaryFiles](The [`BinaryFiles`](<[BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L64)>) | undefined | The files added to the scene. |
|
||||
| exportPadding | number | 10 | The padding to be added on canvas |
|
||||
|
||||
**How to use**
|
||||
|
||||
@@ -957,7 +959,8 @@ This function returns the canvas with the exported elements, appState and dimens
|
||||
exportToBlob(
|
||||
opts: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/packages/utils.ts#L14">ExportOpts</a> & {
|
||||
mimeType?: string,
|
||||
quality?: number;
|
||||
quality?: number,
|
||||
exportPadding?: number;
|
||||
})
|
||||
</pre>
|
||||
|
||||
@@ -966,6 +969,7 @@ exportToBlob(
|
||||
| opts | | | This param is passed to `exportToCanvas`. You can refer to [`exportToCanvas`](#exportToCanvas) |
|
||||
| mimeType | string | "image/png" | Indicates the image format |
|
||||
| quality | number | 0.92 | A value between 0 and 1 indicating the [image quality](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob#parameters). Applies only to `image/jpeg`/`image/webp` MIME types. |
|
||||
| exportPadding | number | 10 | The padding to be added on canvas |
|
||||
|
||||
**How to use**
|
||||
|
||||
@@ -1101,10 +1105,10 @@ import { loadLibraryFromBlob } from "@excalidraw/excalidraw";
|
||||
**_Signature_**
|
||||
|
||||
<pre>
|
||||
loadLibraryFromBlob(blob: <a href="https://developer.mozilla.org/en-US/docs/Web/API/Blob">Blob</a>)
|
||||
loadLibraryFromBlob(blob: <a href="https://developer.mozilla.org/en-US/docs/Web/API/Blob">Blob</a>, defaultStatus: "published" | "unpublished")
|
||||
</pre>
|
||||
|
||||
This function loads the library from the blob.
|
||||
This function loads the library from the blob. Additonally takes `defaultStatus` param which sets the default status for library item if not present, defaults to `unpublished`.
|
||||
|
||||
#### `loadFromBlob`
|
||||
|
||||
|
||||
@@ -282,7 +282,7 @@ export default function App() {
|
||||
files: excalidrawAPI?.getFiles(),
|
||||
type,
|
||||
});
|
||||
window.alert(`Copied to clipboard as ${type} sucessfully`);
|
||||
window.alert(`Copied to clipboard as ${type} successfully`);
|
||||
};
|
||||
|
||||
const [pointerData, setPointerData] = useState<{
|
||||
|
||||
@@ -35,7 +35,10 @@ export const exportToCanvas = ({
|
||||
files,
|
||||
maxWidthOrHeight,
|
||||
getDimensions,
|
||||
}: ExportOpts) => {
|
||||
exportPadding,
|
||||
}: ExportOpts & {
|
||||
exportPadding?: number;
|
||||
}) => {
|
||||
const { elements: restoredElements, appState: restoredAppState } = restore(
|
||||
{ elements, appState },
|
||||
null,
|
||||
@@ -46,7 +49,7 @@ export const exportToCanvas = ({
|
||||
getNonDeletedElements(restoredElements),
|
||||
{ ...restoredAppState, offsetTop: 0, offsetLeft: 0, width: 0, height: 0 },
|
||||
files || {},
|
||||
{ exportBackground, viewBackgroundColor },
|
||||
{ exportBackground, exportPadding, viewBackgroundColor },
|
||||
(width: number, height: number) => {
|
||||
const canvas = document.createElement("canvas");
|
||||
|
||||
@@ -87,6 +90,7 @@ export const exportToBlob = async (
|
||||
opts: ExportOpts & {
|
||||
mimeType?: string;
|
||||
quality?: number;
|
||||
exportPadding?: number;
|
||||
},
|
||||
): Promise<Blob> => {
|
||||
let { mimeType = MIME_TYPES.png, quality } = opts;
|
||||
|
||||
@@ -159,13 +159,15 @@ const strokeGrid = (
|
||||
|
||||
const renderSingleLinearPoint = (
|
||||
context: CanvasRenderingContext2D,
|
||||
appState: AppState,
|
||||
renderConfig: RenderConfig,
|
||||
point: Point,
|
||||
radius: number,
|
||||
isSelected: boolean,
|
||||
isPhantomPoint = false,
|
||||
) => {
|
||||
if (!point) {
|
||||
return;
|
||||
}
|
||||
context.strokeStyle = "#5e5ad8";
|
||||
context.setLineDash([]);
|
||||
context.fillStyle = "rgba(255, 255, 255, 0.9)";
|
||||
@@ -197,35 +199,37 @@ const renderLinearPointHandles = (
|
||||
context.translate(renderConfig.scrollX, renderConfig.scrollY);
|
||||
context.lineWidth = 1 / renderConfig.zoom.value;
|
||||
const points = LinearElementEditor.getPointsGlobalCoordinates(element);
|
||||
const centerPoint = LinearElementEditor.getMidPoint(
|
||||
appState.selectedLinearElement,
|
||||
);
|
||||
if (!centerPoint) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { POINT_HANDLE_SIZE } = LinearElementEditor;
|
||||
const radius = appState.editingLinearElement
|
||||
? POINT_HANDLE_SIZE
|
||||
: POINT_HANDLE_SIZE / 2;
|
||||
points.forEach((point, idx) => {
|
||||
const isSelected =
|
||||
!!appState.editingLinearElement?.selectedPointsIndices?.includes(idx);
|
||||
|
||||
renderSingleLinearPoint(
|
||||
context,
|
||||
appState,
|
||||
renderConfig,
|
||||
point,
|
||||
radius,
|
||||
isSelected,
|
||||
);
|
||||
const visiblePointIndexes = LinearElementEditor.getVisiblePointIndexes(
|
||||
element,
|
||||
appState,
|
||||
);
|
||||
visiblePointIndexes.forEach((index) => {
|
||||
const isSelected =
|
||||
!!appState.editingLinearElement?.selectedPointsIndices?.includes(index);
|
||||
const point = points[index];
|
||||
renderSingleLinearPoint(context, renderConfig, point, radius, isSelected);
|
||||
});
|
||||
|
||||
if (points.length < 3) {
|
||||
if (appState.selectedLinearElement.midPointHovered) {
|
||||
const centerPoint = LinearElementEditor.getMidPoint(
|
||||
appState.selectedLinearElement,
|
||||
)!;
|
||||
//Rendering segment mid points
|
||||
const midPoints = LinearElementEditor.getEditorMidPoints(
|
||||
element,
|
||||
appState,
|
||||
).filter((midPoint) => midPoint !== null) as Point[];
|
||||
|
||||
midPoints.forEach((segmentMidPoint) => {
|
||||
if (
|
||||
appState?.selectedLinearElement?.segmentMidPointHoveredCoords &&
|
||||
LinearElementEditor.arePointsEqual(
|
||||
segmentMidPoint,
|
||||
appState.selectedLinearElement.segmentMidPointHoveredCoords,
|
||||
)
|
||||
) {
|
||||
// The order of renderingSingleLinearPoint and highLight points is different
|
||||
// inside vs outside editor as hover states are different,
|
||||
// in editor when hovered the original point is not visible as hover state fully covers it whereas outside the
|
||||
@@ -233,36 +237,34 @@ const renderLinearPointHandles = (
|
||||
if (appState.editingLinearElement) {
|
||||
renderSingleLinearPoint(
|
||||
context,
|
||||
appState,
|
||||
renderConfig,
|
||||
centerPoint,
|
||||
segmentMidPoint,
|
||||
radius,
|
||||
false,
|
||||
);
|
||||
highlightPoint(centerPoint, context, renderConfig);
|
||||
highlightPoint(segmentMidPoint, context, renderConfig);
|
||||
} else {
|
||||
highlightPoint(centerPoint, context, renderConfig);
|
||||
highlightPoint(segmentMidPoint, context, renderConfig);
|
||||
renderSingleLinearPoint(
|
||||
context,
|
||||
appState,
|
||||
|
||||
renderConfig,
|
||||
centerPoint,
|
||||
segmentMidPoint,
|
||||
radius,
|
||||
false,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
} else if (appState.editingLinearElement || points.length === 2) {
|
||||
renderSingleLinearPoint(
|
||||
context,
|
||||
appState,
|
||||
renderConfig,
|
||||
centerPoint,
|
||||
segmentMidPoint,
|
||||
POINT_HANDLE_SIZE / 2,
|
||||
false,
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
context.restore();
|
||||
};
|
||||
@@ -330,7 +332,6 @@ export const _renderScene = ({
|
||||
if (canvas === null) {
|
||||
return { atLeastOneVisibleElement: false };
|
||||
}
|
||||
window.logTimeAverage("renderScene");
|
||||
const {
|
||||
renderScrollbars = true,
|
||||
renderSelection = true,
|
||||
@@ -404,6 +405,20 @@ export const _renderScene = ({
|
||||
visibleElements.forEach((element) => {
|
||||
try {
|
||||
renderElement(element, rc, context, renderConfig);
|
||||
// Getting the element using LinearElementEditor during collab mismatches version - being one head of visible elements due to
|
||||
// ShapeCache returns empty hence making sure that we get the
|
||||
// correct element from visible elements
|
||||
if (appState.editingLinearElement?.elementId === element.id) {
|
||||
if (element) {
|
||||
renderLinearPointHandles(
|
||||
context,
|
||||
appState,
|
||||
renderConfig,
|
||||
element as NonDeleted<ExcalidrawLinearElement>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!isExporting) {
|
||||
renderLinkIcon(element, context, appState);
|
||||
}
|
||||
@@ -412,15 +427,6 @@ export const _renderScene = ({
|
||||
}
|
||||
});
|
||||
|
||||
if (appState.editingLinearElement) {
|
||||
const element = LinearElementEditor.getElement(
|
||||
appState.editingLinearElement.elementId,
|
||||
);
|
||||
if (element) {
|
||||
renderLinearPointHandles(context, appState, renderConfig, element);
|
||||
}
|
||||
}
|
||||
|
||||
// Paint selection element
|
||||
if (appState.selectionElement) {
|
||||
try {
|
||||
@@ -442,7 +448,22 @@ export const _renderScene = ({
|
||||
appState.selectedLinearElement &&
|
||||
appState.selectedLinearElement.hoverPointIndex >= 0
|
||||
) {
|
||||
renderLinearElementPointHighlight(context, appState, renderConfig);
|
||||
const element = LinearElementEditor.getElement(
|
||||
appState.selectedLinearElement.elementId,
|
||||
);
|
||||
if (element) {
|
||||
const visiblePointIndexes = LinearElementEditor.getVisiblePointIndexes(
|
||||
element,
|
||||
appState,
|
||||
);
|
||||
if (
|
||||
visiblePointIndexes.includes(
|
||||
appState.selectedLinearElement.hoverPointIndex,
|
||||
)
|
||||
) {
|
||||
renderLinearElementPointHighlight(context, appState, renderConfig);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Paint selected elements
|
||||
if (
|
||||
|
||||
@@ -10982,7 +10982,6 @@ Object {
|
||||
"hoverPointIndex": -1,
|
||||
"isDragging": false,
|
||||
"lastUncommittedPoint": null,
|
||||
"midPointHovered": false,
|
||||
"pointerDownState": Object {
|
||||
"lastClickedPoint": -1,
|
||||
"prevSelectedPointsIndices": null,
|
||||
@@ -10991,8 +10990,12 @@ Object {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
"segmentMidPointHoveredCoords": null,
|
||||
"selectedPointsIndices": null,
|
||||
"startBindingElement": "keep",
|
||||
"visiblePointIndexes": Array [
|
||||
1,
|
||||
],
|
||||
},
|
||||
"selectionElement": null,
|
||||
"shouldCacheIgnoreZoom": false,
|
||||
@@ -11208,7 +11211,6 @@ Object {
|
||||
"hoverPointIndex": -1,
|
||||
"isDragging": false,
|
||||
"lastUncommittedPoint": null,
|
||||
"midPointHovered": false,
|
||||
"pointerDownState": Object {
|
||||
"lastClickedPoint": -1,
|
||||
"prevSelectedPointsIndices": null,
|
||||
@@ -11217,8 +11219,12 @@ Object {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
"segmentMidPointHoveredCoords": null,
|
||||
"selectedPointsIndices": null,
|
||||
"startBindingElement": "keep",
|
||||
"visiblePointIndexes": Array [
|
||||
1,
|
||||
],
|
||||
},
|
||||
"selectionElement": null,
|
||||
"shouldCacheIgnoreZoom": false,
|
||||
@@ -11661,7 +11667,6 @@ Object {
|
||||
"hoverPointIndex": -1,
|
||||
"isDragging": false,
|
||||
"lastUncommittedPoint": null,
|
||||
"midPointHovered": false,
|
||||
"pointerDownState": Object {
|
||||
"lastClickedPoint": -1,
|
||||
"prevSelectedPointsIndices": null,
|
||||
@@ -11670,8 +11675,12 @@ Object {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
"segmentMidPointHoveredCoords": null,
|
||||
"selectedPointsIndices": null,
|
||||
"startBindingElement": "keep",
|
||||
"visiblePointIndexes": Array [
|
||||
1,
|
||||
],
|
||||
},
|
||||
"selectionElement": null,
|
||||
"shouldCacheIgnoreZoom": false,
|
||||
@@ -12066,7 +12075,6 @@ Object {
|
||||
"hoverPointIndex": -1,
|
||||
"isDragging": false,
|
||||
"lastUncommittedPoint": null,
|
||||
"midPointHovered": false,
|
||||
"pointerDownState": Object {
|
||||
"lastClickedPoint": -1,
|
||||
"prevSelectedPointsIndices": null,
|
||||
@@ -12075,8 +12083,12 @@ Object {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
"segmentMidPointHoveredCoords": null,
|
||||
"selectedPointsIndices": null,
|
||||
"startBindingElement": "keep",
|
||||
"visiblePointIndexes": Array [
|
||||
1,
|
||||
],
|
||||
},
|
||||
"selectionElement": null,
|
||||
"shouldCacheIgnoreZoom": false,
|
||||
|
||||
571
src/tests/linearElementEditor.test.tsx
Normal file
571
src/tests/linearElementEditor.test.tsx
Normal file
@@ -0,0 +1,571 @@
|
||||
import ReactDOM from "react-dom";
|
||||
import { ExcalidrawLinearElement } from "../element/types";
|
||||
import ExcalidrawApp from "../excalidraw-app";
|
||||
import { centerPoint } from "../math";
|
||||
import { reseed } from "../random";
|
||||
import * as Renderer from "../renderer/renderScene";
|
||||
import { Keyboard, Pointer } from "./helpers/ui";
|
||||
import { screen, render, fireEvent } from "./test-utils";
|
||||
import { API } from "../tests/helpers/api";
|
||||
import { Point } from "../types";
|
||||
import { KEYS } from "../keys";
|
||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||
|
||||
const renderScene = jest.spyOn(Renderer, "renderScene");
|
||||
|
||||
const { h } = window;
|
||||
|
||||
describe(" Test Linear Elements", () => {
|
||||
let container: HTMLElement;
|
||||
let canvas: HTMLCanvasElement;
|
||||
|
||||
beforeEach(async () => {
|
||||
// Unmount ReactDOM from root
|
||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||
localStorage.clear();
|
||||
renderScene.mockClear();
|
||||
reseed(7);
|
||||
const comp = await render(<ExcalidrawApp />);
|
||||
container = comp.container;
|
||||
canvas = container.querySelector("canvas")!;
|
||||
canvas.width = 1000;
|
||||
canvas.height = 1000;
|
||||
});
|
||||
|
||||
const p1: Point = [20, 20];
|
||||
const p2: Point = [60, 20];
|
||||
const midpoint = centerPoint(p1, p2);
|
||||
const delta = 50;
|
||||
const mouse = new Pointer("mouse");
|
||||
|
||||
const createTwoPointerLinearElement = (
|
||||
type: ExcalidrawLinearElement["type"],
|
||||
strokeSharpness: ExcalidrawLinearElement["strokeSharpness"] = "sharp",
|
||||
roughness: ExcalidrawLinearElement["roughness"] = 0,
|
||||
) => {
|
||||
h.elements = [
|
||||
API.createElement({
|
||||
x: p1[0],
|
||||
y: p1[1],
|
||||
width: p2[0] - p1[0],
|
||||
height: 0,
|
||||
type,
|
||||
roughness,
|
||||
points: [
|
||||
[0, 0],
|
||||
[p2[0] - p1[0], p2[1] - p1[1]],
|
||||
],
|
||||
strokeSharpness,
|
||||
}),
|
||||
];
|
||||
|
||||
mouse.clickAt(p1[0], p1[1]);
|
||||
};
|
||||
|
||||
const createThreePointerLinearElement = (
|
||||
type: ExcalidrawLinearElement["type"],
|
||||
strokeSharpness: ExcalidrawLinearElement["strokeSharpness"] = "sharp",
|
||||
roughness: ExcalidrawLinearElement["roughness"] = 0,
|
||||
) => {
|
||||
//dragging line from midpoint
|
||||
const p3 = [midpoint[0] + delta - p1[0], midpoint[1] + delta - p1[1]];
|
||||
h.elements = [
|
||||
API.createElement({
|
||||
x: p1[0],
|
||||
y: p1[1],
|
||||
width: p3[0] - p1[0],
|
||||
height: 0,
|
||||
type,
|
||||
roughness,
|
||||
points: [
|
||||
[0, 0],
|
||||
[p3[0], p3[1]],
|
||||
[p2[0] - p1[0], p2[1] - p1[1]],
|
||||
],
|
||||
strokeSharpness,
|
||||
}),
|
||||
];
|
||||
mouse.clickAt(p1[0], p1[1]);
|
||||
};
|
||||
|
||||
const enterLineEditingMode = (line: ExcalidrawLinearElement) => {
|
||||
mouse.clickAt(p1[0], p1[1]);
|
||||
Keyboard.keyPress(KEYS.ENTER);
|
||||
expect(h.state.editingLinearElement?.elementId).toEqual(line.id);
|
||||
};
|
||||
|
||||
const drag = (startPoint: Point, endPoint: Point) => {
|
||||
fireEvent.pointerDown(canvas, {
|
||||
clientX: startPoint[0],
|
||||
clientY: startPoint[1],
|
||||
});
|
||||
fireEvent.pointerMove(canvas, {
|
||||
clientX: endPoint[0],
|
||||
clientY: endPoint[1],
|
||||
});
|
||||
fireEvent.pointerUp(canvas, {
|
||||
clientX: endPoint[0],
|
||||
clientY: endPoint[1],
|
||||
});
|
||||
};
|
||||
|
||||
const deletePoint = (point: Point) => {
|
||||
fireEvent.pointerDown(canvas, {
|
||||
clientX: point[0],
|
||||
clientY: point[1],
|
||||
});
|
||||
fireEvent.pointerUp(canvas, {
|
||||
clientX: point[0],
|
||||
clientY: point[1],
|
||||
});
|
||||
Keyboard.keyPress(KEYS.DELETE);
|
||||
};
|
||||
|
||||
it("should allow dragging line from midpoint in 2 pointer lines outside editor", async () => {
|
||||
createTwoPointerLinearElement("line");
|
||||
const line = h.elements[0] as ExcalidrawLinearElement;
|
||||
|
||||
expect(renderScene).toHaveBeenCalledTimes(6);
|
||||
expect((h.elements[0] as ExcalidrawLinearElement).points.length).toEqual(2);
|
||||
|
||||
// drag line from midpoint
|
||||
drag(midpoint, [midpoint[0] + delta, midpoint[1] + delta]);
|
||||
expect(renderScene).toHaveBeenCalledTimes(9);
|
||||
expect(line.points.length).toEqual(3);
|
||||
expect(line.points).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
0,
|
||||
0,
|
||||
],
|
||||
Array [
|
||||
70,
|
||||
50,
|
||||
],
|
||||
Array [
|
||||
40,
|
||||
0,
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
describe("Inside editor", () => {
|
||||
it("should allow dragging line from midpoint in 2 pointer lines", async () => {
|
||||
createTwoPointerLinearElement("line");
|
||||
|
||||
const line = h.elements[0] as ExcalidrawLinearElement;
|
||||
enterLineEditingMode(line);
|
||||
|
||||
// drag line from midpoint
|
||||
drag(midpoint, [midpoint[0] + delta, midpoint[1] + delta]);
|
||||
expect(renderScene).toHaveBeenCalledTimes(13);
|
||||
|
||||
expect(line.points.length).toEqual(3);
|
||||
expect(line.points).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
0,
|
||||
0,
|
||||
],
|
||||
Array [
|
||||
70,
|
||||
50,
|
||||
],
|
||||
Array [
|
||||
40,
|
||||
0,
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it("should update the midpoints when element sharpness changed", async () => {
|
||||
createThreePointerLinearElement("line");
|
||||
|
||||
const line = h.elements[0] as ExcalidrawLinearElement;
|
||||
expect(line.points.length).toEqual(3);
|
||||
|
||||
enterLineEditingMode(line);
|
||||
|
||||
const midPointsWithSharpEdge = LinearElementEditor.getEditorMidPoints(
|
||||
line,
|
||||
h.state,
|
||||
);
|
||||
|
||||
// update sharpness
|
||||
fireEvent.click(screen.getByTitle("Round"));
|
||||
|
||||
expect(renderScene).toHaveBeenCalledTimes(11);
|
||||
const midPointsWithRoundEdge = LinearElementEditor.getEditorMidPoints(
|
||||
h.elements[0] as ExcalidrawLinearElement,
|
||||
h.state,
|
||||
);
|
||||
expect(midPointsWithRoundEdge[0]).not.toEqual(midPointsWithSharpEdge[0]);
|
||||
expect(midPointsWithRoundEdge[1]).not.toEqual(midPointsWithSharpEdge[1]);
|
||||
|
||||
expect(midPointsWithRoundEdge).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
55.9697848965255,
|
||||
47.442326230998205,
|
||||
],
|
||||
Array [
|
||||
76.08587175006699,
|
||||
43.294165939653226,
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it("should update all the midpoints when element position changed", async () => {
|
||||
createThreePointerLinearElement("line", "round");
|
||||
|
||||
const line = h.elements[0] as ExcalidrawLinearElement;
|
||||
expect(line.points.length).toEqual(3);
|
||||
enterLineEditingMode(line);
|
||||
|
||||
const points = LinearElementEditor.getPointsGlobalCoordinates(line);
|
||||
expect([line.x, line.y]).toEqual(points[0]);
|
||||
|
||||
const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state);
|
||||
|
||||
const startPoint = centerPoint(points[0], midPoints[0] as Point);
|
||||
const deltaX = 50;
|
||||
const deltaY = 20;
|
||||
const endPoint: Point = [startPoint[0] + deltaX, startPoint[1] + deltaY];
|
||||
|
||||
// Move the element
|
||||
drag(startPoint, endPoint);
|
||||
|
||||
expect(renderScene).toHaveBeenCalledTimes(14);
|
||||
expect([line.x, line.y]).toEqual([
|
||||
points[0][0] + deltaX,
|
||||
points[0][1] + deltaY,
|
||||
]);
|
||||
|
||||
const newMidPoints = LinearElementEditor.getEditorMidPoints(
|
||||
line,
|
||||
h.state,
|
||||
);
|
||||
expect(midPoints[0]).not.toEqual(newMidPoints[0]);
|
||||
expect(midPoints[1]).not.toEqual(newMidPoints[1]);
|
||||
expect(newMidPoints).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
105.96978489652551,
|
||||
67.4423262309982,
|
||||
],
|
||||
Array [
|
||||
126.08587175006699,
|
||||
63.294165939653226,
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
describe("When edges are sharp", () => {
|
||||
// This is the expected midpoint for line with sharp edge
|
||||
// hence hardcoding it so if later some bug is introduced
|
||||
// this will fail and we can fix it
|
||||
const firstSegmentMidpoint: Point = [55, 45];
|
||||
const lastSegmentMidpoint: Point = [75, 40];
|
||||
|
||||
let line: ExcalidrawLinearElement;
|
||||
|
||||
beforeEach(() => {
|
||||
createThreePointerLinearElement("line");
|
||||
line = h.elements[0] as ExcalidrawLinearElement;
|
||||
expect(line.points.length).toEqual(3);
|
||||
|
||||
enterLineEditingMode(line);
|
||||
});
|
||||
|
||||
it("should allow dragging lines from midpoints in between segments", async () => {
|
||||
// drag line via first segment midpoint
|
||||
drag(firstSegmentMidpoint, [
|
||||
firstSegmentMidpoint[0] + delta,
|
||||
firstSegmentMidpoint[1] + delta,
|
||||
]);
|
||||
expect(line.points.length).toEqual(4);
|
||||
|
||||
// drag line from last segment midpoint
|
||||
drag(lastSegmentMidpoint, [
|
||||
lastSegmentMidpoint[0] + delta,
|
||||
lastSegmentMidpoint[1] + delta,
|
||||
]);
|
||||
|
||||
expect(renderScene).toHaveBeenCalledTimes(18);
|
||||
expect(line.points.length).toEqual(5);
|
||||
|
||||
expect((h.elements[0] as ExcalidrawLinearElement).points)
|
||||
.toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
0,
|
||||
0,
|
||||
],
|
||||
Array [
|
||||
85,
|
||||
75,
|
||||
],
|
||||
Array [
|
||||
70,
|
||||
50,
|
||||
],
|
||||
Array [
|
||||
105,
|
||||
75,
|
||||
],
|
||||
Array [
|
||||
40,
|
||||
0,
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it("should update only the first segment midpoint when its point is dragged", async () => {
|
||||
const points = LinearElementEditor.getPointsGlobalCoordinates(line);
|
||||
const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state);
|
||||
|
||||
const hitCoords: Point = [points[0][0], points[0][1]];
|
||||
|
||||
// Drag from first point
|
||||
drag(hitCoords, [hitCoords[0] - delta, hitCoords[1] - delta]);
|
||||
|
||||
expect(renderScene).toHaveBeenCalledTimes(14);
|
||||
|
||||
const newPoints = LinearElementEditor.getPointsGlobalCoordinates(line);
|
||||
expect([newPoints[0][0], newPoints[0][1]]).toEqual([
|
||||
points[0][0] - delta,
|
||||
points[0][1] - delta,
|
||||
]);
|
||||
|
||||
const newMidPoints = LinearElementEditor.getEditorMidPoints(
|
||||
line,
|
||||
h.state,
|
||||
);
|
||||
|
||||
expect(midPoints[0]).not.toEqual(newMidPoints[0]);
|
||||
expect(midPoints[1]).toEqual(newMidPoints[1]);
|
||||
});
|
||||
|
||||
it("should hide midpoints in the segment when points moved close", async () => {
|
||||
const points = LinearElementEditor.getPointsGlobalCoordinates(line);
|
||||
const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state);
|
||||
|
||||
const hitCoords: Point = [points[0][0], points[0][1]];
|
||||
|
||||
// Drag from first point
|
||||
drag(hitCoords, [hitCoords[0] + delta, hitCoords[1] + delta]);
|
||||
|
||||
expect(renderScene).toHaveBeenCalledTimes(14);
|
||||
|
||||
const newPoints = LinearElementEditor.getPointsGlobalCoordinates(line);
|
||||
expect([newPoints[0][0], newPoints[0][1]]).toEqual([
|
||||
points[0][0] + delta,
|
||||
points[0][1] + delta,
|
||||
]);
|
||||
|
||||
const newMidPoints = LinearElementEditor.getEditorMidPoints(
|
||||
line,
|
||||
h.state,
|
||||
);
|
||||
// This midpoint is hidden since the points are too close
|
||||
expect(newMidPoints[0]).toBeNull();
|
||||
expect(midPoints[1]).toEqual(newMidPoints[1]);
|
||||
});
|
||||
|
||||
it("should remove the midpoint when one of the points in the segment is deleted", async () => {
|
||||
const line = h.elements[0] as ExcalidrawLinearElement;
|
||||
enterLineEditingMode(line);
|
||||
const points = LinearElementEditor.getPointsGlobalCoordinates(line);
|
||||
|
||||
// dragging line from last segment midpoint
|
||||
drag(lastSegmentMidpoint, [
|
||||
lastSegmentMidpoint[0] + 50,
|
||||
lastSegmentMidpoint[1] + 50,
|
||||
]);
|
||||
expect(line.points.length).toEqual(4);
|
||||
|
||||
const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state);
|
||||
|
||||
// delete 3rd point
|
||||
deletePoint(points[2]);
|
||||
expect(line.points.length).toEqual(3);
|
||||
expect(renderScene).toHaveBeenCalledTimes(19);
|
||||
|
||||
const newMidPoints = LinearElementEditor.getEditorMidPoints(
|
||||
line,
|
||||
h.state,
|
||||
);
|
||||
expect(newMidPoints.length).toEqual(2);
|
||||
expect(midPoints[0]).toEqual(newMidPoints[0]);
|
||||
expect(midPoints[1]).toEqual(newMidPoints[1]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("When edges are round", () => {
|
||||
// This is the expected midpoint for line with round edge
|
||||
// hence hardcoding it so if later some bug is introduced
|
||||
// this will fail and we can fix it
|
||||
const firstSegmentMidpoint: Point = [
|
||||
55.9697848965255, 47.442326230998205,
|
||||
];
|
||||
const lastSegmentMidpoint: Point = [
|
||||
76.08587175006699, 43.294165939653226,
|
||||
];
|
||||
let line: ExcalidrawLinearElement;
|
||||
|
||||
beforeEach(() => {
|
||||
createThreePointerLinearElement("line", "round");
|
||||
line = h.elements[0] as ExcalidrawLinearElement;
|
||||
expect(line.points.length).toEqual(3);
|
||||
|
||||
enterLineEditingMode(line);
|
||||
});
|
||||
|
||||
it("should allow dragging lines from midpoints in between segments", async () => {
|
||||
// drag line from first segment midpoint
|
||||
drag(firstSegmentMidpoint, [
|
||||
firstSegmentMidpoint[0] + delta,
|
||||
firstSegmentMidpoint[1] + delta,
|
||||
]);
|
||||
expect(line.points.length).toEqual(4);
|
||||
|
||||
// drag line from last segment midpoint
|
||||
drag(lastSegmentMidpoint, [
|
||||
lastSegmentMidpoint[0] + delta,
|
||||
lastSegmentMidpoint[1] + delta,
|
||||
]);
|
||||
expect(renderScene).toHaveBeenCalledTimes(18);
|
||||
|
||||
expect(line.points.length).toEqual(5);
|
||||
|
||||
expect((h.elements[0] as ExcalidrawLinearElement).points)
|
||||
.toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
0,
|
||||
0,
|
||||
],
|
||||
Array [
|
||||
85.96978489652551,
|
||||
77.4423262309982,
|
||||
],
|
||||
Array [
|
||||
70,
|
||||
50,
|
||||
],
|
||||
Array [
|
||||
104.58050066266131,
|
||||
74.24758482724201,
|
||||
],
|
||||
Array [
|
||||
40,
|
||||
0,
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it("should update all the midpoints when its point is dragged", async () => {
|
||||
const points = LinearElementEditor.getPointsGlobalCoordinates(line);
|
||||
const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state);
|
||||
|
||||
const hitCoords: Point = [points[0][0], points[0][1]];
|
||||
|
||||
// Drag from first point
|
||||
drag(hitCoords, [hitCoords[0] - delta, hitCoords[1] - delta]);
|
||||
|
||||
const newPoints = LinearElementEditor.getPointsGlobalCoordinates(line);
|
||||
expect([newPoints[0][0], newPoints[0][1]]).toEqual([
|
||||
points[0][0] - delta,
|
||||
points[0][1] - delta,
|
||||
]);
|
||||
|
||||
const newMidPoints = LinearElementEditor.getEditorMidPoints(
|
||||
line,
|
||||
h.state,
|
||||
);
|
||||
|
||||
expect(midPoints[0]).not.toEqual(newMidPoints[0]);
|
||||
expect(midPoints[1]).not.toEqual(newMidPoints[1]);
|
||||
expect(newMidPoints).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
31.884084517616053,
|
||||
23.13275505472383,
|
||||
],
|
||||
Array [
|
||||
77.74792546875662,
|
||||
44.57840982272327,
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it("should hide midpoints in the segment when points moved close", async () => {
|
||||
const points = LinearElementEditor.getPointsGlobalCoordinates(line);
|
||||
const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state);
|
||||
|
||||
const hitCoords: Point = [points[0][0], points[0][1]];
|
||||
|
||||
// Drag from first point
|
||||
drag(hitCoords, [hitCoords[0] + delta, hitCoords[1] + delta]);
|
||||
|
||||
expect(renderScene).toHaveBeenCalledTimes(14);
|
||||
|
||||
const newPoints = LinearElementEditor.getPointsGlobalCoordinates(line);
|
||||
expect([newPoints[0][0], newPoints[0][1]]).toEqual([
|
||||
points[0][0] + delta,
|
||||
points[0][1] + delta,
|
||||
]);
|
||||
|
||||
const newMidPoints = LinearElementEditor.getEditorMidPoints(
|
||||
line,
|
||||
h.state,
|
||||
);
|
||||
// This mid point is hidden due to point being too close
|
||||
expect(newMidPoints[0]).toBeNull();
|
||||
expect(newMidPoints[1]).not.toEqual(midPoints[1]);
|
||||
});
|
||||
|
||||
it("should update all the midpoints when a point is deleted", async () => {
|
||||
drag(lastSegmentMidpoint, [
|
||||
lastSegmentMidpoint[0] + delta,
|
||||
lastSegmentMidpoint[1] + delta,
|
||||
]);
|
||||
expect(line.points.length).toEqual(4);
|
||||
|
||||
const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state);
|
||||
const points = LinearElementEditor.getPointsGlobalCoordinates(line);
|
||||
|
||||
// delete 3rd point
|
||||
deletePoint(points[2]);
|
||||
expect(line.points.length).toEqual(3);
|
||||
|
||||
const newMidPoints = LinearElementEditor.getEditorMidPoints(
|
||||
line,
|
||||
h.state,
|
||||
);
|
||||
expect(newMidPoints.length).toEqual(2);
|
||||
expect(midPoints[0]).not.toEqual(newMidPoints[0]);
|
||||
expect(midPoints[1]).not.toEqual(newMidPoints[1]);
|
||||
expect(newMidPoints).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
55.9697848965255,
|
||||
47.442326230998205,
|
||||
],
|
||||
Array [
|
||||
76.08587175006699,
|
||||
43.294165939653226,
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
137
yarn.lock
137
yarn.lock
@@ -2998,7 +2998,7 @@ async-limiter@~1.0.0:
|
||||
resolved "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz"
|
||||
integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==
|
||||
|
||||
async@^2.6.2, async@^2.6.4:
|
||||
async@^2.6.2:
|
||||
version "2.6.4"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-2.6.4.tgz#706b7ff6084664cd7eae713f6f965433b5504221"
|
||||
integrity sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==
|
||||
@@ -3269,13 +3269,6 @@ base@^0.11.1:
|
||||
mixin-deep "^1.2.0"
|
||||
pascalcase "^0.1.1"
|
||||
|
||||
basic-auth@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.1.tgz#b998279bf47ce38344b4f3cf916d4679bbf51e3a"
|
||||
integrity sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==
|
||||
dependencies:
|
||||
safe-buffer "5.1.2"
|
||||
|
||||
batch@0.6.1:
|
||||
version "0.6.1"
|
||||
resolved "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz"
|
||||
@@ -3734,14 +3727,6 @@ chalk@^4.0.0, chalk@^4.1.0:
|
||||
ansi-styles "^4.1.0"
|
||||
supports-color "^7.1.0"
|
||||
|
||||
chalk@^4.1.2:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
|
||||
integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
|
||||
dependencies:
|
||||
ansi-styles "^4.1.0"
|
||||
supports-color "^7.1.0"
|
||||
|
||||
char-regex@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz"
|
||||
@@ -4164,11 +4149,6 @@ core-util-is@~1.0.0:
|
||||
resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz"
|
||||
integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
|
||||
|
||||
corser@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/corser/-/corser-2.0.1.tgz#8eda252ecaab5840dcd975ceb90d9370c819ff87"
|
||||
integrity sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==
|
||||
|
||||
cosmiconfig@^5.0.0:
|
||||
version "5.2.1"
|
||||
resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz"
|
||||
@@ -4237,14 +4217,7 @@ create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7:
|
||||
safe-buffer "^5.0.1"
|
||||
sha.js "^2.4.8"
|
||||
|
||||
cross-env@7.0.3:
|
||||
version "7.0.3"
|
||||
resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf"
|
||||
integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==
|
||||
dependencies:
|
||||
cross-spawn "^7.0.1"
|
||||
|
||||
cross-spawn@7.0.3, cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3:
|
||||
cross-spawn@7.0.3, cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3:
|
||||
version "7.0.3"
|
||||
resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz"
|
||||
integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
|
||||
@@ -4557,7 +4530,7 @@ debug@4:
|
||||
dependencies:
|
||||
ms "2.1.2"
|
||||
|
||||
debug@^3.1.1, debug@^3.2.6, debug@^3.2.7:
|
||||
debug@^3.1.1, debug@^3.2.6:
|
||||
version "3.2.7"
|
||||
resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz"
|
||||
integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==
|
||||
@@ -6337,13 +6310,6 @@ html-encoding-sniffer@^2.0.1:
|
||||
dependencies:
|
||||
whatwg-encoding "^1.0.5"
|
||||
|
||||
html-encoding-sniffer@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz#2cb1a8cf0db52414776e5b2a7a04d5dd98158de9"
|
||||
integrity sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==
|
||||
dependencies:
|
||||
whatwg-encoding "^2.0.0"
|
||||
|
||||
html-entities@^1.2.1, html-entities@^1.3.1:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.npmjs.org/html-entities/-/html-entities-1.4.0.tgz"
|
||||
@@ -6444,7 +6410,7 @@ http-proxy-middleware@0.19.1:
|
||||
lodash "^4.17.11"
|
||||
micromatch "^3.1.10"
|
||||
|
||||
http-proxy@^1.17.0, http-proxy@^1.18.1:
|
||||
http-proxy@^1.17.0:
|
||||
version "1.18.1"
|
||||
resolved "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz"
|
||||
integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==
|
||||
@@ -6453,25 +6419,6 @@ http-proxy@^1.17.0, http-proxy@^1.18.1:
|
||||
follow-redirects "^1.0.0"
|
||||
requires-port "^1.0.0"
|
||||
|
||||
http-server@14.1.1:
|
||||
version "14.1.1"
|
||||
resolved "https://registry.yarnpkg.com/http-server/-/http-server-14.1.1.tgz#d60fbb37d7c2fdff0f0fbff0d0ee6670bd285e2e"
|
||||
integrity sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A==
|
||||
dependencies:
|
||||
basic-auth "^2.0.1"
|
||||
chalk "^4.1.2"
|
||||
corser "^2.0.1"
|
||||
he "^1.2.0"
|
||||
html-encoding-sniffer "^3.0.0"
|
||||
http-proxy "^1.18.1"
|
||||
mime "^1.6.0"
|
||||
minimist "^1.2.6"
|
||||
opener "^1.5.1"
|
||||
portfinder "^1.0.28"
|
||||
secure-compare "3.0.1"
|
||||
union "~0.5.0"
|
||||
url-join "^4.0.1"
|
||||
|
||||
https-browserify@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz"
|
||||
@@ -6514,13 +6461,6 @@ iconv-lite@0.4.24:
|
||||
dependencies:
|
||||
safer-buffer ">= 2.1.2 < 3"
|
||||
|
||||
iconv-lite@0.6.3:
|
||||
version "0.6.3"
|
||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
|
||||
integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
|
||||
dependencies:
|
||||
safer-buffer ">= 2.1.2 < 3.0.0"
|
||||
|
||||
icss-utils@^4.0.0, icss-utils@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.npmjs.org/icss-utils/-/icss-utils-4.1.1.tgz"
|
||||
@@ -8235,7 +8175,7 @@ mime-types@^2.1.27, mime-types@~2.1.17, mime-types@~2.1.24:
|
||||
dependencies:
|
||||
mime-db "1.46.0"
|
||||
|
||||
mime@1.6.0, mime@^1.6.0:
|
||||
mime@1.6.0:
|
||||
version "1.6.0"
|
||||
resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz"
|
||||
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
|
||||
@@ -8282,7 +8222,7 @@ minimatch@3.0.4, minimatch@^3.0.4:
|
||||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6:
|
||||
minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5:
|
||||
version "1.2.6"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
|
||||
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
|
||||
@@ -8354,13 +8294,6 @@ mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.5, mkdirp@~0.5.1:
|
||||
dependencies:
|
||||
minimist "^1.2.5"
|
||||
|
||||
mkdirp@^0.5.6:
|
||||
version "0.5.6"
|
||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6"
|
||||
integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==
|
||||
dependencies:
|
||||
minimist "^1.2.6"
|
||||
|
||||
mkdirp@^1.0.3, mkdirp@^1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz"
|
||||
@@ -8771,11 +8704,6 @@ open@^7.0.2:
|
||||
is-docker "^2.0.0"
|
||||
is-wsl "^2.1.1"
|
||||
|
||||
opener@^1.5.1:
|
||||
version "1.5.2"
|
||||
resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598"
|
||||
integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==
|
||||
|
||||
opn@^5.5.0:
|
||||
version "5.5.0"
|
||||
resolved "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz"
|
||||
@@ -9104,6 +9032,17 @@ performance-now@^2.1.0:
|
||||
resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz"
|
||||
integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
|
||||
|
||||
pica@7.1.1:
|
||||
version "7.1.1"
|
||||
resolved "https://registry.yarnpkg.com/pica/-/pica-7.1.1.tgz#c68b42f5cfa6cc26eaec5cfa10cc0a5299ef3b7a"
|
||||
integrity sha512-WY73tMvNzXWEld2LicT9Y260L43isrZ85tPuqRyvtkljSDLmnNFQmZICt4xUJMVulmcc6L9O7jbBrtx3DOz/YQ==
|
||||
dependencies:
|
||||
glur "^1.1.2"
|
||||
inherits "^2.0.3"
|
||||
multimath "^2.0.0"
|
||||
object-assign "^4.1.1"
|
||||
webworkify "^1.5.0"
|
||||
|
||||
pica@^7.1.0:
|
||||
version "7.1.0"
|
||||
resolved "https://registry.npmjs.org/pica/-/pica-7.1.0.tgz"
|
||||
@@ -9241,15 +9180,6 @@ portfinder@^1.0.26:
|
||||
debug "^3.1.1"
|
||||
mkdirp "^0.5.5"
|
||||
|
||||
portfinder@^1.0.28:
|
||||
version "1.0.32"
|
||||
resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.32.tgz#2fe1b9e58389712429dc2bea5beb2146146c7f81"
|
||||
integrity sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==
|
||||
dependencies:
|
||||
async "^2.6.4"
|
||||
debug "^3.2.7"
|
||||
mkdirp "^0.5.6"
|
||||
|
||||
posix-character-classes@^0.1.0:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz"
|
||||
@@ -10144,13 +10074,6 @@ qs@6.7.0:
|
||||
resolved "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz"
|
||||
integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==
|
||||
|
||||
qs@^6.4.0:
|
||||
version "6.11.0"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a"
|
||||
integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==
|
||||
dependencies:
|
||||
side-channel "^1.0.4"
|
||||
|
||||
query-string@^4.1.0:
|
||||
version "4.3.4"
|
||||
resolved "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz"
|
||||
@@ -10838,7 +10761,7 @@ safe-regex@^1.1.0:
|
||||
dependencies:
|
||||
ret "~0.1.10"
|
||||
|
||||
"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.1.0:
|
||||
"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.1.0:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
||||
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
||||
@@ -10929,11 +10852,6 @@ schema-utils@^3.0.0:
|
||||
ajv "^6.12.5"
|
||||
ajv-keywords "^3.5.2"
|
||||
|
||||
secure-compare@3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/secure-compare/-/secure-compare-3.0.1.tgz#f1a0329b308b221fae37b9974f3d578d0ca999e3"
|
||||
integrity sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==
|
||||
|
||||
select-hose@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz"
|
||||
@@ -12125,13 +12043,6 @@ union-value@^1.0.0:
|
||||
is-extendable "^0.1.1"
|
||||
set-value "^2.0.1"
|
||||
|
||||
union@~0.5.0:
|
||||
version "0.5.0"
|
||||
resolved "https://registry.yarnpkg.com/union/-/union-0.5.0.tgz#b2c11be84f60538537b846edb9ba266ba0090075"
|
||||
integrity sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==
|
||||
dependencies:
|
||||
qs "^6.4.0"
|
||||
|
||||
uniq@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz"
|
||||
@@ -12208,11 +12119,6 @@ urix@^0.1.0:
|
||||
resolved "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz"
|
||||
integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=
|
||||
|
||||
url-join@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/url-join/-/url-join-4.0.1.tgz#b642e21a2646808ffa178c4c5fda39844e12cde7"
|
||||
integrity sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==
|
||||
|
||||
url-loader@4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.npmjs.org/url-loader/-/url-loader-4.1.1.tgz"
|
||||
@@ -12519,13 +12425,6 @@ whatwg-encoding@^1.0.5:
|
||||
dependencies:
|
||||
iconv-lite "0.4.24"
|
||||
|
||||
whatwg-encoding@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz#e7635f597fd87020858626805a2729fa7698ac53"
|
||||
integrity sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==
|
||||
dependencies:
|
||||
iconv-lite "0.6.3"
|
||||
|
||||
whatwg-fetch@2.0.4:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz"
|
||||
|
||||
Reference in New Issue
Block a user