Compare commits

..

1 Commits

Author SHA1 Message Date
Aakansha Doshi 313097233d refactor: rename docs to dev-docs 2022-07-26 16:44:21 +05:30
119 changed files with 2591 additions and 3810 deletions
-43
View File
@@ -1,43 +0,0 @@
{
// 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"] }
}
}
}
+1 -1
View File
@@ -88,7 +88,7 @@ Try out [`@excalidraw/excalidraw`](https://www.npmjs.com/package/@excalidraw/exc
### Code Sandbox
- Go to https://codesandbox.io/p/github/excalidraw/excalidraw
- Go to https://codesandbox.io/s/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!
-1
View File
@@ -42,7 +42,6 @@
"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",
+2 -3
View File
@@ -1,12 +1,11 @@
const fs = require("fs");
const path = require("path");
// for development purposes we want to have the service-worker.js file
// accessible from the public folder. On build though, we need to compile it
// and CRA expects that file to be in src/ folder.
const moveServiceWorkerScript = () => {
const oldPath = path.resolve(__dirname, "../public/service-worker.js");
const newPath = path.resolve(__dirname, "../src/service-worker.js");
const oldPath = "./public/service-worker.js";
const newPath = "./src/service-worker.js";
fs.rename(oldPath, newPath, (error) => {
if (error) {
+1 -1
View File
@@ -244,7 +244,7 @@ export const actionLoadScene = register({
}
},
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.O,
PanelComponent: ({ updateData }) => (
PanelComponent: ({ updateData, appState }) => (
<ToolButton
type="button"
icon={load}
+1 -10
View File
@@ -13,7 +13,7 @@ import {
maybeBindLinearElement,
bindOrUnbindLinearElement,
} from "../element/binding";
import { isBindingElement, isLinearElement } from "../element/typeChecks";
import { isBindingElement } from "../element/typeChecks";
import { AppState } from "../types";
export const actionFinalize = register({
@@ -33,9 +33,6 @@ export const actionFinalize = register({
endBindingElement,
);
}
const selectedLinearElement = appState.selectedLinearElement
? new LinearElementEditor(element, scene, appState)
: null;
return {
elements:
element.points.length < 2 || isInvisiblySmallElement(element)
@@ -45,7 +42,6 @@ export const actionFinalize = register({
...appState,
cursorButton: "up",
editingLinearElement: null,
selectedLinearElement,
},
commitToHistory: true,
};
@@ -185,11 +181,6 @@ export const actionFinalize = register({
[multiPointElement.id]: true,
}
: appState.selectedElementIds,
// To select the linear element when user has finished mutipoint editing
selectedLinearElement:
multiPointElement && isLinearElement(multiPointElement)
? new LinearElementEditor(multiPointElement, scene, appState)
: appState.selectedLinearElement,
pendingImageElementId: null,
},
commitToHistory: appState.activeTool.type === "freedraw",
+14 -24
View File
@@ -3,42 +3,32 @@ import { register } from "./register";
import { selectGroupsForSelectedElements } from "../groups";
import { getNonDeletedElements, isTextElement } from "../element";
import { ExcalidrawElement } from "../element/types";
import { isLinearElement } from "../element/typeChecks";
import { LinearElementEditor } from "../element/linearElementEditor";
export const actionSelectAll = register({
name: "selectAll",
trackEvent: { category: "canvas" },
perform: (elements, appState, value, app) => {
perform: (elements, appState) => {
if (appState.editingLinearElement) {
return false;
}
const selectedElementIds = elements.reduce(
(map: Record<ExcalidrawElement["id"], true>, element) => {
if (
!element.isDeleted &&
!(isTextElement(element) && element.containerId) &&
!element.locked
) {
map[element.id] = true;
}
return map;
},
{},
);
return {
appState: selectGroupsForSelectedElements(
{
...appState,
selectedLinearElement:
// single linear element selected
Object.keys(selectedElementIds).length === 1 &&
isLinearElement(elements[0])
? new LinearElementEditor(elements[0], app.scene, appState)
: null,
editingGroupId: null,
selectedElementIds,
selectedElementIds: elements.reduce(
(map: Record<ExcalidrawElement["id"], true>, element) => {
if (
!element.isDeleted &&
!(isTextElement(element) && element.containerId) &&
!element.locked
) {
map[element.id] = true;
}
return map;
},
{},
),
},
getNonDeletedElements(elements),
),
+3 -6
View File
@@ -17,19 +17,16 @@ export const actionToggleLock = register({
const operation = getOperation(selectedElements);
const selectedElementsMap = arrayToMap(selectedElements);
const lock = operation === "lock";
return {
elements: elements.map((element) => {
if (!selectedElementsMap.has(element.id)) {
return element;
}
return newElementWith(element, { locked: lock });
return newElementWith(element, { locked: operation === "lock" });
}),
appState: {
...appState,
selectedLinearElement: lock ? null : appState.selectedLinearElement,
},
appState,
commitToHistory: true,
};
},
-1
View File
@@ -147,7 +147,6 @@ 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) => {
-2
View File
@@ -90,7 +90,6 @@ export const getDefaultAppState = (): Omit<
viewModeEnabled: false,
pendingImageElementId: null,
showHyperlinkPopup: false,
selectedLinearElement: null,
};
};
@@ -182,7 +181,6 @@ const APP_STATE_STORAGE_CONF = (<
viewModeEnabled: { browser: false, export: false, server: false },
pendingImageElementId: { browser: false, export: false, server: false },
showHyperlinkPopup: { browser: false, export: false, server: false },
selectedLinearElement: { browser: true, export: false, server: false },
});
const _clearAppStateForStorage = <
+12 -54
View File
@@ -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(appState.activeTool.type) ||
hasBackground(activeTool) ||
targetElements.some(
(element) =>
hasBackground(element.type) && !isTransparent(element.backgroundColor),
);
const showChangeBackgroundIcons =
hasBackground(appState.activeTool.type) ||
hasBackground(activeTool) ||
targetElements.some((element) => hasBackground(element.type));
const showLinkIcon =
@@ -79,23 +79,23 @@ export const SelectedShapeActions = ({
return (
<div className="panelColumn">
{((hasStrokeColor(appState.activeTool.type) &&
appState.activeTool.type !== "image" &&
{((hasStrokeColor(activeTool) &&
activeTool !== "image" &&
commonSelectedType !== "image") ||
targetElements.some((element) => hasStrokeColor(element.type))) &&
renderAction("changeStrokeColor")}
{showChangeBackgroundIcons && renderAction("changeBackgroundColor")}
{showFillIcons && renderAction("changeFillStyle")}
{(hasStrokeWidth(appState.activeTool.type) ||
{(hasStrokeWidth(activeTool) ||
targetElements.some((element) => hasStrokeWidth(element.type))) &&
renderAction("changeStrokeWidth")}
{(appState.activeTool.type === "freedraw" ||
{(activeTool === "freedraw" ||
targetElements.some((element) => element.type === "freedraw")) &&
renderAction("changeStrokeShape")}
{(hasStrokeStyle(appState.activeTool.type) ||
{(hasStrokeStyle(activeTool) ||
targetElements.some((element) => hasStrokeStyle(element.type))) && (
<>
{renderAction("changeStrokeStyle")}
@@ -103,12 +103,12 @@ export const SelectedShapeActions = ({
</>
)}
{(canChangeSharpness(appState.activeTool.type) ||
{(canChangeSharpness(activeTool) ||
targetElements.some((element) => canChangeSharpness(element.type))) && (
<>{renderAction("changeSharpness")}</>
)}
{(hasText(appState.activeTool.type) ||
{(hasText(activeTool) ||
targetElements.some((element) => hasText(element.type))) && (
<>
{renderAction("changeFontSize")}
@@ -123,7 +123,7 @@ export const SelectedShapeActions = ({
(element) =>
hasBoundTextElement(element) || isBoundToContainer(element),
) && renderAction("changeVerticalAlign")}
{(canHaveArrowheads(appState.activeTool.type) ||
{(canHaveArrowheads(activeTool) ||
targetElements.some((element) => canHaveArrowheads(element.type))) && (
<>{renderAction("changeArrowhead")}</>
)}
@@ -271,45 +271,3 @@ 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>
);
+205 -471
View File
@@ -1,6 +1,4 @@
import React, { useContext } from "react";
import { flushSync } from "react-dom";
import { RoughCanvas } from "roughjs/bin/canvas";
import rough from "roughjs/bin/rough";
import clsx from "clsx";
@@ -88,9 +86,9 @@ import {
getDragOffsetXY,
getElementWithTransformHandleType,
getNormalizedDimensions,
getPerfectElementSize,
getResizeArrowDirection,
getResizeOffsetXY,
getLockedLinearCursorAlignSize,
getTransformHandleTypeFromCoords,
hitTest,
isHittingElementBoundingBoxWithoutHittingElement,
@@ -106,7 +104,6 @@ import {
updateTextElement,
} from "../element";
import {
bindOrUnbindLinearElement,
bindOrUnbindSelectedElements,
fixBindingsAfterDeletion,
fixBindingsAfterDuplication,
@@ -262,7 +259,6 @@ import {
isPointHittingLinkIcon,
isLocalLink,
} from "../element/Hyperlink";
import { shouldShowBoundingBox } from "../element/transformHandles";
const deviceContextInitialValue = {
isSmScreen: false,
@@ -272,7 +268,6 @@ const deviceContextInitialValue = {
};
const DeviceContext = React.createContext<Device>(deviceContextInitialValue);
export const useDevice = () => useContext<Device>(DeviceContext);
const ExcalidrawContainerContext = React.createContext<{
container: HTMLDivElement | null;
id: string | null;
@@ -280,22 +275,6 @@ 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;
@@ -522,69 +501,63 @@ class App extends React.Component<AppProps, AppState> {
value={this.excalidrawContainerValue}
>
<DeviceContext.Provider value={this.device}>
<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>
<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>
</DeviceContext.Provider>
</ExcalidrawContainerContext.Provider>
</div>
@@ -935,6 +908,7 @@ class App extends React.Component<AppProps, AppState> {
} else {
this.updateDOMRect(this.initializeScene);
}
this.checkIfBrowserZoomed();
}
public componentWillUnmount() {
@@ -947,8 +921,25 @@ class App extends React.Component<AppProps, AppState> {
clearTimeout(touchTimeout);
touchTimeout = 0;
}
private checkIfBrowserZoomed = () => {
if (!this.device.isMobile) {
const scrollBarWidth = 10;
const widthRatio =
(window.outerWidth - scrollBarWidth) / window.innerWidth;
const isBrowserZoomed = widthRatio < 0.75 || widthRatio > 1.1;
if (isBrowserZoomed) {
this.setToast({
message: t("alerts.browserZoom"),
closable: true,
duration: Infinity,
});
} else {
this.setToast(null);
}
}
};
private onResize = withBatchedUpdates(() => {
this.checkIfBrowserZoomed();
this.scene
.getElementsIncludingDeleted()
.forEach((element) => invalidateShapeForElement(element));
@@ -1159,16 +1150,6 @@ class App extends React.Component<AppProps, AppState> {
this.actionManager.executeAction(actionFinalize);
});
}
if (
this.state.selectedLinearElement &&
!this.state.selectedElementIds[this.state.selectedLinearElement.elementId]
) {
// To make sure `selectedLinearElement` is in sync with `selectedElementIds`, however this shouldn't be needed once
// we have a single API to update `selectedElementIds`
this.setState({ selectedLinearElement: null });
}
const { multiElement } = prevState;
if (
prevState.activeTool !== this.state.activeTool &&
@@ -1188,23 +1169,7 @@ class App extends React.Component<AppProps, AppState> {
),
);
}
this.renderScene();
this.history.record(this.state, this.scene.getElementsIncludingDeleted());
// Do not notify consumers if we're still loading the scene. Among other
// potential issues, this fixes a case where the tab isn't focused during
// init, which would trigger onChange with empty elements, which would then
// override whatever is in localStorage currently.
if (!this.state.isLoading) {
this.props.onChange?.(
this.scene.getElementsIncludingDeleted(),
this.state,
this.files,
);
}
}
private renderScene = () => {
const cursorButton: {
[id: string]: string | undefined;
} = {};
@@ -1241,7 +1206,6 @@ class App extends React.Component<AppProps, AppState> {
);
cursorButton[socketId] = user.button;
});
const renderingElements = this.scene
.getNonDeletedElements()
.filter((element) => {
@@ -1263,43 +1227,42 @@ class App extends React.Component<AppProps, AppState> {
});
renderScene(
renderingElements,
this.state,
this.state.selectionElement,
window.devicePixelRatio,
this.rc!,
this.canvas!,
{
elements: renderingElements,
appState: this.state,
scale: window.devicePixelRatio,
rc: this.rc!,
canvas: this.canvas!,
renderConfig: {
scrollX: this.state.scrollX,
scrollY: this.state.scrollY,
viewBackgroundColor: this.state.viewBackgroundColor,
zoom: this.state.zoom,
remotePointerViewportCoords: pointerViewportCoords,
remotePointerButton: cursorButton,
remoteSelectedElementIds,
remotePointerUsernames: pointerUsernames,
remotePointerUserStates: pointerUserStates,
shouldCacheIgnoreZoom: this.state.shouldCacheIgnoreZoom,
theme: this.state.theme,
imageCache: this.imageCache,
isExporting: false,
renderScrollbars: !this.device.isMobile,
},
callback: ({ atLeastOneVisibleElement, scrollBars }) => {
if (scrollBars) {
currentScrollBars = scrollBars;
}
const scrolledOutside =
// hide when editing text
isTextElement(this.state.editingElement)
? false
: !atLeastOneVisibleElement && renderingElements.length > 0;
if (this.state.scrolledOutside !== scrolledOutside) {
this.setState({ scrolledOutside });
}
scrollX: this.state.scrollX,
scrollY: this.state.scrollY,
viewBackgroundColor: this.state.viewBackgroundColor,
zoom: this.state.zoom,
remotePointerViewportCoords: pointerViewportCoords,
remotePointerButton: cursorButton,
remoteSelectedElementIds,
remotePointerUsernames: pointerUsernames,
remotePointerUserStates: pointerUserStates,
shouldCacheIgnoreZoom: this.state.shouldCacheIgnoreZoom,
theme: this.state.theme,
imageCache: this.imageCache,
isExporting: false,
renderScrollbars: !this.device.isMobile,
},
({ atLeastOneVisibleElement, scrollBars }) => {
if (scrollBars) {
currentScrollBars = scrollBars;
}
const scrolledOutside =
// hide when editing text
isTextElement(this.state.editingElement)
? false
: !atLeastOneVisibleElement && renderingElements.length > 0;
if (this.state.scrolledOutside !== scrolledOutside) {
this.setState({ scrolledOutside });
}
this.scheduleImageRefresh();
},
this.scheduleImageRefresh();
},
THROTTLE_NEXT_RENDER && window.EXCALIDRAW_THROTTLE_RENDER === true,
);
@@ -1307,7 +1270,21 @@ class App extends React.Component<AppProps, AppState> {
if (!THROTTLE_NEXT_RENDER) {
THROTTLE_NEXT_RENDER = true;
}
};
this.history.record(this.state, this.scene.getElementsIncludingDeleted());
// Do not notify consumers if we're still loading the scene. Among other
// potential issues, this fixes a case where the tab isn't focused during
// init, which would trigger onChange with empty elements, which would then
// override whatever is in localStorage currently.
if (!this.state.isLoading) {
this.props.onChange?.(
this.scene.getElementsIncludingDeleted(),
this.state,
this.files,
);
}
}
private onScroll = debounce(() => {
const { offsetTop, offsetLeft } = this.getCanvasOffsets();
@@ -1907,8 +1884,6 @@ class App extends React.Component<AppProps, AppState> {
editingLinearElement: new LinearElementEditor(
selectedElements[0],
this.scene,
this.state,
true,
),
});
}
@@ -2487,8 +2462,6 @@ class App extends React.Component<AppProps, AppState> {
editingLinearElement: new LinearElementEditor(
selectedElements[0],
this.scene,
this.state,
true,
),
});
}
@@ -2722,23 +2695,13 @@ class App extends React.Component<AppProps, AppState> {
event,
scenePointerX,
scenePointerY,
this.state,
this.state.editingLinearElement,
this.state.gridSize,
);
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,
});
});
if (editingLinearElement !== this.state.editingLinearElement) {
this.setState({ editingLinearElement });
}
if (editingLinearElement?.lastUncommittedPoint != null) {
if (editingLinearElement.lastUncommittedPoint != null) {
this.maybeSuggestBindingAtCursor(scenePointer);
} else {
this.setState({ suggestedBindings: [] });
@@ -2803,30 +2766,6 @@ class App extends React.Component<AppProps, AppState> {
points: points.slice(0, -1),
});
} else {
const [gridX, gridY] = getGridPoint(
scenePointerX,
scenePointerY,
this.state.gridSize,
);
const [lastCommittedX, lastCommittedY] =
multiElement?.lastCommittedPoint ?? [0, 0];
let dxFromLastCommitted = gridX - rx - lastCommittedX;
let dyFromLastCommitted = gridY - ry - lastCommittedY;
if (shouldRotateWithDiscreteAngle(event)) {
({ width: dxFromLastCommitted, height: dyFromLastCommitted } =
getLockedLinearCursorAlignSize(
// actual coordinate of the last committed point
lastCommittedX + rx,
lastCommittedY + ry,
// cursor-grid coordinate
gridX,
gridY,
));
}
if (isPathALoop(points, this.state.zoom.value)) {
setCursor(this.canvas, CURSOR_TYPE.POINTER);
}
@@ -2834,10 +2773,7 @@ class App extends React.Component<AppProps, AppState> {
mutateElement(multiElement, {
points: [
...points.slice(0, -1),
[
lastCommittedX + dxFromLastCommitted,
lastCommittedY + dyFromLastCommitted,
],
[scenePointerX - rx, scenePointerY - ry],
],
});
}
@@ -2936,12 +2872,22 @@ class App extends React.Component<AppProps, AppState> {
setCursor(this.canvas, CURSOR_TYPE.GRAB);
} else if (isOverScrollBar) {
setCursor(this.canvas, CURSOR_TYPE.AUTO);
} else if (this.state.selectedLinearElement) {
this.handleHoverSelectedLinearElement(
this.state.selectedLinearElement,
scenePointerX,
scenePointerY,
} else if (this.state.editingLinearElement) {
const element = LinearElementEditor.getElement(
this.state.editingLinearElement.elementId,
);
if (
element &&
isHittingElementNotConsideringBoundingBox(element, this.state, [
scenePointer.x,
scenePointer.y,
])
) {
setCursor(this.canvas, CURSOR_TYPE.MOVE);
} else {
setCursor(this.canvas, CURSOR_TYPE.AUTO);
}
} else if (
// if using cmd/ctrl, we're not dragging
!event[KEYS.CTRL_OR_CMD] &&
@@ -3053,85 +2999,6 @@ class App extends React.Component<AppProps, AppState> {
invalidateContextMenu = true;
};
handleHoverSelectedLinearElement(
linearElementEditor: LinearElementEditor,
scenePointerX: number,
scenePointerY: number,
) {
const element = LinearElementEditor.getElement(
linearElementEditor.elementId,
);
if (!element) {
return;
}
if (this.state.selectedLinearElement) {
let hoverPointIndex = -1;
let segmentMidPointHoveredCoords = null;
if (
isHittingElementNotConsideringBoundingBox(element, this.state, [
scenePointerX,
scenePointerY,
])
) {
hoverPointIndex = LinearElementEditor.getPointIndexUnderCursor(
this.state.selectedLinearElement,
this.state.zoom,
scenePointerX,
scenePointerY,
);
segmentMidPointHoveredCoords =
LinearElementEditor.getSegmentMidpointHitCoords(
linearElementEditor,
{ x: scenePointerX, y: scenePointerY },
this.state,
);
if (hoverPointIndex >= 0 || segmentMidPointHoveredCoords) {
setCursor(this.canvas, CURSOR_TYPE.POINTER);
} else {
setCursor(this.canvas, CURSOR_TYPE.MOVE);
}
} else if (
shouldShowBoundingBox([element], this.state) &&
isHittingElementBoundingBoxWithoutHittingElement(
element,
this.state,
scenePointerX,
scenePointerY,
)
) {
setCursor(this.canvas, CURSOR_TYPE.MOVE);
}
if (
this.state.selectedLinearElement.hoverPointIndex !== hoverPointIndex
) {
this.setState({
selectedLinearElement: {
...this.state.selectedLinearElement,
hoverPointIndex,
},
});
}
if (
!LinearElementEditor.arePointsEqual(
this.state.selectedLinearElement.segmentMidPointHoveredCoords,
segmentMidPointHoveredCoords,
)
) {
this.setState({
selectedLinearElement: {
...this.state.selectedLinearElement,
segmentMidPointHoveredCoords,
},
});
}
} else {
setCursor(this.canvas, CURSOR_TYPE.AUTO);
}
}
private handleCanvasPointerDown = (
event: React.PointerEvent<HTMLCanvasElement>,
) => {
@@ -3528,6 +3395,7 @@ class App extends React.Component<AppProps, AppState> {
origin,
selectedElements,
),
hasHitElementInside: false,
},
drag: {
hasOccurred: false,
@@ -3661,27 +3529,18 @@ class App extends React.Component<AppProps, AppState> {
);
}
} else {
if (this.state.selectedLinearElement) {
const linearElementEditor =
this.state.editingLinearElement || this.state.selectedLinearElement;
if (this.state.editingLinearElement) {
const ret = LinearElementEditor.handlePointerDown(
event,
this.state,
(appState) => this.setState(appState),
this.history,
pointerDownState.origin,
linearElementEditor,
);
if (ret.hitElement) {
pointerDownState.hit.element = ret.hitElement;
}
if (ret.linearElementEditor) {
this.setState({ selectedLinearElement: ret.linearElementEditor });
if (this.state.editingLinearElement) {
this.setState({ editingLinearElement: ret.linearElementEditor });
}
}
if (ret.didAddPoint && !ret.isMidPoint) {
if (ret.didAddPoint) {
return true;
}
}
@@ -3695,16 +3554,22 @@ class App extends React.Component<AppProps, AppState> {
if (pointerDownState.hit.element) {
// Early return if pointer is hitting link icon
const hitLinkElement = this.getElementLinkAtPosition(
{
x: pointerDownState.origin.x,
y: pointerDownState.origin.y,
},
pointerDownState.hit.element,
);
if (hitLinkElement) {
if (
isPointHittingLinkIcon(
pointerDownState.hit.element,
this.state,
[pointerDownState.origin.x, pointerDownState.origin.y],
this.device.isMobile,
)
) {
return false;
}
pointerDownState.hit.hasHitElementInside =
isHittingElementNotConsideringBoundingBox(
pointerDownState.hit.element,
this.state,
[pointerDownState.origin.x, pointerDownState.origin.y],
);
}
// For overlapped elements one position may hit
@@ -4164,7 +4029,6 @@ class App extends React.Component<AppProps, AppState> {
// to ensure we don't create a 2-point arrow by mistake when
// user clicks mouse in a way that it moves a tiny bit (thus
// triggering pointermove)
if (
!pointerDownState.drag.hasOccurred &&
(this.state.activeTool.type === "arrow" ||
@@ -4181,6 +4045,7 @@ class App extends React.Component<AppProps, AppState> {
return;
}
}
if (pointerDownState.resize.isResizing) {
pointerDownState.lastCoords.x = pointerCoords.x;
pointerDownState.lastCoords.y = pointerCoords.y;
@@ -4189,12 +4054,10 @@ class App extends React.Component<AppProps, AppState> {
}
}
if (this.state.selectedLinearElement) {
const linearElementEditor =
this.state.editingLinearElement || this.state.selectedLinearElement;
if (this.state.editingLinearElement) {
const didDrag = LinearElementEditor.handlePointDragging(
event,
this.state,
(appState) => this.setState(appState),
pointerCoords.x,
pointerCoords.y,
(element, pointsSceneCoords) => {
@@ -4203,31 +4066,11 @@ class App extends React.Component<AppProps, AppState> {
pointsSceneCoords,
);
},
linearElementEditor,
);
if (didDrag) {
pointerDownState.lastCoords.x = pointerCoords.x;
pointerDownState.lastCoords.y = pointerCoords.y;
pointerDownState.drag.hasOccurred = true;
if (
this.state.editingLinearElement &&
!this.state.editingLinearElement.isDragging
) {
this.setState({
editingLinearElement: {
...this.state.editingLinearElement,
isDragging: true,
},
});
}
if (!this.state.selectedLinearElement.isDragging) {
this.setState({
selectedLinearElement: {
...this.state.selectedLinearElement,
isDragging: true,
},
});
}
return;
}
}
@@ -4236,15 +4079,17 @@ class App extends React.Component<AppProps, AppState> {
(element) => this.isASelectedElement(element),
);
const isSelectingPointsInLineEditor =
this.state.editingLinearElement &&
event.shiftKey &&
this.state.editingLinearElement.elementId ===
pointerDownState.hit.element?.id;
if (
(hasHitASelectedElement ||
pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements) &&
!isSelectingPointsInLineEditor
// this allows for box-selecting points when clicking inside the
// line's bounding box
(!this.state.editingLinearElement || !event.shiftKey) &&
// box-selecting without shift when editing line, not clicking on a line
(!this.state.editingLinearElement ||
this.state.editingLinearElement?.elementId !==
pointerDownState.hit.element?.id ||
pointerDownState.hit.hasHitElementInside)
) {
const selectedElements = getSelectedElements(
this.scene.getNonDeletedElements(),
@@ -4272,6 +4117,7 @@ class App extends React.Component<AppProps, AppState> {
// We only drag in one direction if shift is pressed
const lockDirection = event.shiftKey;
dragSelectedElements(
pointerDownState,
selectedElements,
@@ -4383,19 +4229,16 @@ class App extends React.Component<AppProps, AppState> {
let dy = gridY - draggingElement.y;
if (shouldRotateWithDiscreteAngle(event) && points.length === 2) {
({ width: dx, height: dy } = getLockedLinearCursorAlignSize(
draggingElement.x,
draggingElement.y,
pointerCoords.x,
pointerCoords.y,
({ width: dx, height: dy } = getPerfectElementSize(
this.state.activeTool.type,
dx,
dy,
));
}
if (points.length === 1) {
mutateElement(draggingElement, {
points: [...points, [dx, dy]],
});
} else if (points.length === 2) {
mutateElement(draggingElement, { points: [...points, [dx, dy]] });
} else if (points.length > 1) {
mutateElement(draggingElement, {
points: [...points.slice(0, -1), [dx, dy]],
});
@@ -4485,16 +4328,6 @@ class App extends React.Component<AppProps, AppState> {
elementsWithinSelection[0].link
? "info"
: false,
// select linear element only when we haven't box-selected anything else
selectedLinearElement:
elementsWithinSelection.length === 1 &&
isLinearElement(elementsWithinSelection[0])
? new LinearElementEditor(
elementsWithinSelection[0],
this.scene,
this.state,
)
: null,
},
this.scene.getNonDeletedElements(),
),
@@ -4564,8 +4397,9 @@ class App extends React.Component<AppProps, AppState> {
if (this.state.editingLinearElement) {
if (
!pointerDownState.boxSelection.hasOccurred &&
pointerDownState.hit?.element?.id !==
this.state.editingLinearElement.elementId
(pointerDownState.hit?.element?.id !==
this.state.editingLinearElement.elementId ||
!pointerDownState.hit.hasHitElementInside)
) {
this.actionManager.executeAction(actionFinalize);
} else {
@@ -4581,47 +4415,6 @@ class App extends React.Component<AppProps, AppState> {
});
}
}
} else if (this.state.selectedLinearElement) {
if (
pointerDownState.hit?.element?.id !==
this.state.selectedLinearElement.elementId
) {
const selectedELements = getSelectedElements(
this.scene.getNonDeletedElements(),
this.state,
);
// set selectedLinearElement to null if there is more than one element selected since we don't want to show linear element handles
if (selectedELements.length > 1) {
this.setState({ selectedLinearElement: null });
}
} else {
const linearElementEditor = LinearElementEditor.handlePointerUp(
childEvent,
this.state.selectedLinearElement,
this.state,
);
const { startBindingElement, endBindingElement } =
linearElementEditor;
const element = this.scene.getElement(linearElementEditor.elementId);
if (isBindingElement(element)) {
bindOrUnbindLinearElement(
element,
startBindingElement,
endBindingElement,
);
}
if (linearElementEditor !== this.state.selectedLinearElement) {
this.setState({
selectedLinearElement: {
...linearElementEditor,
selectedPointsIndices: null,
},
suggestedBindings: [],
});
}
}
}
lastPointerUp = null;
@@ -4754,11 +4547,6 @@ class App extends React.Component<AppProps, AppState> {
...prevState.selectedElementIds,
[draggingElement.id]: true,
},
selectedLinearElement: new LinearElementEditor(
draggingElement,
this.scene,
this.state,
),
}));
} else {
this.setState((prevState) => ({
@@ -4810,26 +4598,6 @@ class App extends React.Component<AppProps, AppState> {
// Code below handles selection when element(s) weren't
// drag or added to selection on pointer down phase.
const hitElement = pointerDownState.hit.element;
if (
this.state.selectedLinearElement?.elementId !== hitElement?.id &&
isLinearElement(hitElement)
) {
const selectedELements = getSelectedElements(
this.scene.getNonDeletedElements(),
this.state,
);
// set selectedLinearElement when no other element selected except
// the one we've hit
if (selectedELements.length === 1) {
this.setState({
selectedLinearElement: new LinearElementEditor(
hitElement,
this.scene,
this.state,
),
});
}
}
if (isEraserActive(this.state)) {
const draggedDistance = distance2d(
this.lastPointerDown!.clientX,
@@ -4902,43 +4670,26 @@ class App extends React.Component<AppProps, AppState> {
...idsOfSelectedElementsThatAreInGroups,
},
}));
// if not gragging a linear element point (outside editor)
} else if (!this.state.selectedLinearElement?.isDragging) {
} else {
// remove element from selection while
// keeping prev elements selected
this.setState((prevState) => {
const newSelectedElementIds = {
...prevState.selectedElementIds,
[hitElement!.id]: false,
};
const newSelectedElements = getSelectedElements(
this.scene.getNonDeletedElements(),
{ ...prevState, selectedElementIds: newSelectedElementIds },
);
return selectGroupsForSelectedElements(
this.setState((prevState) =>
selectGroupsForSelectedElements(
{
...prevState,
selectedElementIds: newSelectedElementIds,
// set selectedLinearElement only if thats the only element selected
selectedLinearElement:
newSelectedElements.length === 1 &&
isLinearElement(newSelectedElements[0])
? new LinearElementEditor(
newSelectedElements[0],
this.scene,
this.state,
)
: prevState.selectedLinearElement,
selectedElementIds: {
...prevState.selectedElementIds,
[hitElement!.id]: false,
},
},
this.scene.getNonDeletedElements(),
);
});
),
);
}
} else {
// add element to selection while
// keeping prev elements selected
this.setState((_prevState) => ({
selectedElementIds: {
..._prevState.selectedElementIds,
@@ -4952,17 +4703,6 @@ class App extends React.Component<AppProps, AppState> {
{
...prevState,
selectedElementIds: { [hitElement.id]: true },
selectedLinearElement:
isLinearElement(hitElement) &&
// 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,
this.state,
)
: prevState.selectedLinearElement,
},
this.scene.getNonDeletedElements(),
),
@@ -4971,6 +4711,7 @@ class App extends React.Component<AppProps, AppState> {
}
if (
!this.state.editingLinearElement &&
!pointerDownState.drag.hasOccurred &&
!this.state.isResizing &&
((hitElement &&
@@ -4983,16 +4724,13 @@ class App extends React.Component<AppProps, AppState> {
(!hitElement &&
pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements))
) {
if (this.state.editingLinearElement) {
this.setState({ editingLinearElement: null });
} else {
// Deselect selected elements
this.setState({
selectedElementIds: {},
selectedGroupIds: {},
editingGroupId: null,
});
}
// Deselect selected elements
this.setState({
selectedElementIds: {},
selectedGroupIds: {},
editingGroupId: null,
});
return;
}
@@ -5720,9 +5458,6 @@ class App extends React.Component<AppProps, AppState> {
{
...this.state,
selectedElementIds: { [element.id]: true },
selectedLinearElement: isLinearElement(element)
? new LinearElementEditor(element, this.scene, this.state)
: null,
},
this.scene.getNonDeletedElements(),
),
@@ -6263,5 +5998,4 @@ if (
},
});
}
export default App;
-2
View File
@@ -343,8 +343,6 @@ const ColorInput = React.forwardRef(
},
);
ColorInput.displayName = "ColorInput";
export const ColorPicker = ({
type,
color,
-1
View File
@@ -85,7 +85,6 @@ export const Dialog = (props: DialogProps) => {
<button
className="Modal__close"
onClick={onClose}
title={t("buttons.close")}
aria-label={t("buttons.close")}
>
{useDevice().isMobile ? back : close}
-106
View File
@@ -1,106 +0,0 @@
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;
+178 -78
View File
@@ -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 } from "./Actions";
import { SelectedShapeActions, ShapesSwitcher, ZoomActions } 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 Footer from "./Footer";
import { actionToggleZenMode } from "../actions";
interface LayerUIProps {
actionManager: ActionManager;
@@ -71,8 +71,8 @@ const LayerUI = ({
appState,
files,
setAppState,
elements,
canvas,
elements,
onCollabButtonClick,
onLockToggle,
onPenModeToggle,
@@ -210,8 +210,8 @@ const LayerUI = ({
)}
</Stack.Row>
<BackgroundPickerAndDarkModeToggle
appState={appState}
actionManager={actionManager}
appState={appState}
setAppState={setAppState}
showThemeBtn={showThemeBtn}
/>
@@ -244,6 +244,7 @@ const LayerUI = ({
appState={appState}
elements={elements}
renderAction={actionManager.renderAction}
activeTool={appState.activeTool.type}
/>
</Island>
</Section>
@@ -278,6 +279,7 @@ const LayerUI = ({
libraryReturnUrl={libraryReturnUrl}
focusContainer={focusContainer}
library={library}
theme={appState.theme}
files={files}
id={id}
appState={appState}
@@ -381,7 +383,100 @@ const LayerUI = ({
);
};
return (
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 = (
<>
{appState.isLoading && <LoadingMessage delay={250} />}
{appState.errorMessage && (
@@ -409,81 +504,86 @@ 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}
/>
)}
</>
);
{!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)` }
: {}
}
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),
});
}}
>
{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>
)}
</>
{t("buttons.scrollBackToContent")}
</button>
)}
</div>
{appState.isLibraryOpen && (
<div className="layer-ui__sidebar">{libraryMenu}</div>
)}
</>
);
+5 -3
View File
@@ -80,6 +80,7 @@ export const LibraryMenu = ({
onInsertLibraryItems,
pendingElements,
onAddToLibrary,
theme,
setAppState,
files,
libraryReturnUrl,
@@ -92,6 +93,7 @@ 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"];
@@ -103,12 +105,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 so that LibraryButton
// can toggle library menu
// If click on the library icon, do nothing.
if ((event.target as Element).closest(".ToolIcon__library")) {
return;
}
@@ -288,7 +290,7 @@ export const LibraryMenu = ({
appState={appState}
libraryReturnUrl={libraryReturnUrl}
library={library}
theme={appState.theme}
theme={theme}
files={files}
id={id}
selectedItems={selectedItems}
+2 -1
View File
@@ -1,3 +1,4 @@
import { chunk } from "lodash";
import React, { useCallback, useState } from "react";
import { saveLibraryAsJSON, serializeLibraryAsJSON } from "../data/json";
import Library from "../data/library";
@@ -10,7 +11,7 @@ import {
LibraryItem,
LibraryItems,
} from "../types";
import { arrayToMap, chunk, muteFSAbortError } from "../utils";
import { arrayToMap, muteFSAbortError } from "../utils";
import { useDevice } from "./App";
import ConfirmDialog from "./ConfirmDialog";
import { close, exportToFileIcon, load, publishIcon, trash } from "./icons";
+11 -21
View File
@@ -1,5 +1,5 @@
import React from "react";
import { AppState, ExcalidrawProps } from "../types";
import { AppState } from "../types";
import { ActionManager } from "../actions/manager";
import { t } from "../i18n";
import Stack from "./Stack";
@@ -18,8 +18,6 @@ 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;
@@ -44,7 +42,7 @@ type MobileMenuProps = {
isMobile: boolean,
appState: AppState,
) => JSX.Element | null;
renderCustomStats?: ExcalidrawProps["renderCustomStats"];
renderStats: () => JSX.Element | null;
};
export const MobileMenu = ({
@@ -64,7 +62,7 @@ export const MobileMenu = ({
showThemeBtn,
onImageAction,
renderTopRightUI,
renderCustomStats,
renderStats,
}: MobileMenuProps) => {
const renderToolbar = () => {
return (
@@ -121,6 +119,7 @@ export const MobileMenu = ({
const renderAppToolbar = () => {
// Render eraser conditionally in mobile
const showEraser =
!appState.viewModeEnabled &&
!appState.editingElement &&
getSelectedElements(elements, appState).length === 0;
@@ -139,11 +138,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>
);
@@ -185,17 +184,7 @@ export const MobileMenu = ({
return (
<>
{!appState.viewModeEnabled && renderToolbar()}
{!appState.openMenu && appState.showStats && (
<Stats
appState={appState}
setAppState={setAppState}
elements={elements}
onClose={() => {
actionManager.executeAction(actionToggleStats);
}}
renderCustomStats={renderCustomStats}
/>
)}
{renderStats()}
<div
className="App-bottom-bar"
style={{
@@ -232,6 +221,7 @@ export const MobileMenu = ({
appState={appState}
elements={elements}
renderAction={actionManager.renderAction}
activeTool={appState.activeTool.type}
/>
</Section>
) : null}
+2 -2
View File
@@ -46,7 +46,7 @@ const ChartPreviewBtn = (props: {
},
null, // files
);
previewNode.replaceChildren();
previewNode.appendChild(svg);
if (props.selected) {
@@ -55,7 +55,7 @@ const ChartPreviewBtn = (props: {
})();
return () => {
previewNode.replaceChildren();
previewNode.removeChild(svg);
};
}, [props.spreadsheet, props.chartType, props.selected]);
+1
View File
@@ -69,6 +69,7 @@ export const Popover = ({
if (fitInViewport && popoverRef.current) {
const element = popoverRef.current;
const { x, y, width, height } = element.getBoundingClientRect();
const { innerWidth: viewportWidth, innerHeight: viewportHeight } = window;
//Position correctly when clicked on rightmost part or the bottom part of viewport
if (x + width - offsetLeft > viewportWidth) {
+5 -1
View File
@@ -2,6 +2,7 @@ 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";
@@ -15,10 +16,13 @@ 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}>
-2
View File
@@ -187,5 +187,3 @@ ToolButton.defaultProps = {
className: "",
size: "medium",
};
ToolButton.displayName = "ToolButton";
+2 -2
View File
@@ -356,7 +356,7 @@ export const getFileHandle = async (
};
/**
* attempts to detect if a buffer is a valid image by checking its leading bytes
* attemps 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 = (
});
};
/** attempts to detect correct mimeType if none is set, or if an image
/** attemps 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) => {
+4 -9
View File
@@ -67,14 +67,13 @@ const getFontFamilyByName = (fontFamilyName: string): FontFamilyValues => {
};
const restoreElementWithProperties = <
T extends Required<Omit<ExcalidrawElement, "customData">> & {
customData?: ExcalidrawElement["customData"];
T extends ExcalidrawElement,
K extends Pick<T, keyof Omit<Required<T>, keyof ExcalidrawElement>>,
>(
element: Required<T> & {
/** @deprecated */
boundElementIds?: readonly ExcalidrawElement["id"][];
},
K extends Pick<T, keyof Omit<Required<T>, keyof ExcalidrawElement>>,
>(
element: T,
extra: Pick<
T,
// This extra Pick<T, keyof K> ensure no excess properties are passed.
@@ -116,10 +115,6 @@ const restoreElementWithProperties = <
locked: element.locked ?? false,
};
if ("customData" in element) {
base.customData = element.customData;
}
return {
...base,
...getNormalizedDimensions(base),
+2 -3
View File
@@ -32,7 +32,6 @@ import { getElementAbsoluteCoords } from "./";
import "./Hyperlink.scss";
import { trackEvent } from "../analytics";
import { useExcalidrawAppState } from "../components/App";
const CONTAINER_WIDTH = 320;
const SPACE_BOTTOM = 85;
@@ -49,15 +48,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);
+29 -113
View File
@@ -18,7 +18,6 @@ import { rescalePoints } from "../points";
// x and y position of top left corner, x and y position of bottom right corner
export type Bounds = readonly [number, number, number, number];
type MaybeQuadraticSolution = [number | null, number | null] | false;
// If the element is created from right to left, the width is going to be negative
// This set of functions retrieves the absolute position of the 4 points.
@@ -69,102 +68,11 @@ export const getCurvePathOps = (shape: Drawable): Op[] => {
return shape.sets[0].ops;
};
// reference: https://eliot-jones.com/2019/12/cubic-bezier-curve-bounding-boxes
const getBezierValueForT = (
t: number,
p0: number,
p1: number,
p2: number,
p3: number,
) => {
const oneMinusT = 1 - t;
return (
Math.pow(oneMinusT, 3) * p0 +
3 * Math.pow(oneMinusT, 2) * t * p1 +
3 * oneMinusT * Math.pow(t, 2) * p2 +
Math.pow(t, 3) * p3
);
};
const solveQuadratic = (
p0: number,
p1: number,
p2: number,
p3: number,
): MaybeQuadraticSolution => {
const i = p1 - p0;
const j = p2 - p1;
const k = p3 - p2;
const a = 3 * i - 6 * j + 3 * k;
const b = 6 * j - 6 * i;
const c = 3 * i;
const sqrtPart = b * b - 4 * a * c;
const hasSolution = sqrtPart >= 0;
if (!hasSolution) {
return false;
}
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);
}
if (t2 >= 0 && t2 <= 1) {
s2 = getBezierValueForT(t2, p0, p1, p2, p3);
}
return [s1, s2];
};
const getCubicBezierCurveBound = (
p0: Point,
p1: Point,
p2: Point,
p3: Point,
): Bounds => {
const solX = solveQuadratic(p0[0], p1[0], p2[0], p3[0]);
const solY = solveQuadratic(p0[1], p1[1], p2[1], p3[1]);
let minX = Math.min(p0[0], p3[0]);
let maxX = Math.max(p0[0], p3[0]);
if (solX) {
const xs = solX.filter((x) => x !== null) as number[];
minX = Math.min(minX, ...xs);
maxX = Math.max(maxX, ...xs);
}
let minY = Math.min(p0[1], p3[1]);
let maxY = Math.max(p0[1], p3[1]);
if (solY) {
const ys = solY.filter((y) => y !== null) as number[];
minY = Math.min(minY, ...ys);
maxY = Math.max(maxY, ...ys);
}
return [minX, minY, maxX, maxY];
};
const getMinMaxXYFromCurvePathOps = (
ops: Op[],
transformXY?: (x: number, y: number) => [number, number],
): [number, number, number, number] => {
let currentP: Point = [0, 0];
const { minX, minY, maxX, maxY } = ops.reduce(
(limits, { op, data }) => {
// There are only four operation types:
@@ -175,29 +83,38 @@ const getMinMaxXYFromCurvePathOps = (
// move operation does not draw anything; so, it always
// returns false
} else if (op === "bcurveTo") {
const _p1 = [data[0], data[1]] as Point;
const _p2 = [data[2], data[3]] as Point;
const _p3 = [data[4], data[5]] as Point;
// create points from bezier curve
// bezier curve stores data as a flattened array of three positions
// [x1, y1, x2, y2, x3, y3]
const p1 = [data[0], data[1]] as Point;
const p2 = [data[2], data[3]] as Point;
const p3 = [data[4], data[5]] as Point;
const p1 = transformXY ? transformXY(..._p1) : _p1;
const p2 = transformXY ? transformXY(..._p2) : _p2;
const p3 = transformXY ? transformXY(..._p3) : _p3;
const p0 = currentP;
currentP = p3;
const p0 = transformXY ? transformXY(...currentP) : currentP;
currentP = _p3;
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 [minX, minY, maxX, maxY] = getCubicBezierCurveBound(
p0,
p1,
p2,
p3,
);
let t = 0;
while (t <= 1.0) {
let x = equation(t, 0);
let y = equation(t, 1);
if (transformXY) {
[x, y] = transformXY(x, y);
}
limits.minX = Math.min(limits.minX, minX);
limits.minY = Math.min(limits.minY, minY);
limits.minY = Math.min(limits.minY, y);
limits.minX = Math.min(limits.minX, x);
limits.maxX = Math.max(limits.maxX, maxX);
limits.maxY = Math.max(limits.maxY, maxY);
limits.maxX = Math.max(limits.maxX, x);
limits.maxY = Math.max(limits.maxY, y);
t += 0.1;
}
} else if (op === "lineTo") {
// TODO: Implement this
} else if (op === "qcurveTo") {
@@ -207,6 +124,7 @@ const getMinMaxXYFromCurvePathOps = (
},
{ minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity },
);
return [minX, minY, maxX, maxY];
};
@@ -502,7 +420,6 @@ export const getResizedElementAbsoluteCoords = (
element: ExcalidrawElement,
nextWidth: number,
nextHeight: number,
normalizePoints: boolean,
): [number, number, number, number] => {
if (!(isLinearElement(element) || isFreeDrawElement(element))) {
return [
@@ -516,8 +433,7 @@ export const getResizedElementAbsoluteCoords = (
const points = rescalePoints(
0,
nextWidth,
rescalePoints(1, nextHeight, element.points, normalizePoints),
normalizePoints,
rescalePoints(1, nextHeight, element.points),
);
let bounds: [number, number, number, number];
+1 -5
View File
@@ -35,7 +35,6 @@ import { getShapeForElement } from "../renderer/renderElement";
import { hasBoundTextElement, isImageElement } from "./typeChecks";
import { isTextElement } from ".";
import { isTransparent } from "../utils";
import { shouldShowBoundingBox } from "./transformHandles";
const isElementDraggableFromInside = (
element: NonDeletedExcalidrawElement,
@@ -65,10 +64,7 @@ export const hitTest = (
const threshold = 10 / appState.zoom.value;
const point: Point = [x, y];
if (
isElementSelected(appState, element) &&
shouldShowBoundingBox([element], appState)
) {
if (isElementSelected(appState, element)) {
return isPointHittingElementBoundingBox(element, point, threshold);
}
+6 -17
View File
@@ -105,26 +105,15 @@ export const dragNewElement = (
true */
widthAspectRatio?: number | null,
) => {
if (shouldMaintainAspectRatio && draggingElement.type !== "selection") {
if (shouldMaintainAspectRatio) {
if (widthAspectRatio) {
height = width / widthAspectRatio;
} else {
// Depending on where the cursor is at (x, y) relative to where the starting point is
// (originX, originY), we use ONLY width or height to control size increase.
// This allows the cursor to always "stick" to one of the sides of the bounding box.
if (Math.abs(y - originY) > Math.abs(x - originX)) {
({ width, height } = getPerfectElementSize(
elementType,
height,
x < originX ? -width : width,
));
} else {
({ width, height } = getPerfectElementSize(
elementType,
width,
y < originY ? -height : height,
));
}
({ width, height } = getPerfectElementSize(
elementType,
width,
y < originY ? -height : height,
));
if (height < 0) {
height = -height;
-1
View File
@@ -53,7 +53,6 @@ export { textWysiwyg } from "./textWysiwyg";
export { redrawTextBoundingBox } from "./textElement";
export {
getPerfectElementSize,
getLockedLinearCursorAlignSize,
isInvisiblySmallElement,
resizePerfectLineForNWHandler,
getNormalizedDimensions,
+140 -540
View File
@@ -5,20 +5,8 @@ import {
PointBinding,
ExcalidrawBindableElement,
} from "./types";
import {
distance2d,
rotate,
isPathALoop,
getGridPoint,
rotatePoint,
centerPoint,
getControlPointsForBezierCurve,
getBezierXY,
getBezierCurveLength,
mapIntervalToBezierT,
arePointsEqual,
} from "../math";
import { getElementAbsoluteCoords, getLockedLinearCursorAlignSize } from ".";
import { distance2d, rotate, isPathALoop, getGridPoint } from "../math";
import { getElementAbsoluteCoords } from ".";
import { getElementPointsCoords } from "./bounds";
import { Point, AppState } from "../types";
import { mutateElement } from "./mutateElement";
@@ -32,50 +20,28 @@ import {
} from "./binding";
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"] & {
public elementId: ExcalidrawElement["id"] & {
_brand: "excalidrawLinearElementId";
};
/** indices */
public readonly selectedPointsIndices: readonly number[] | null;
public selectedPointsIndices: readonly number[] | null;
public readonly pointerDownState: Readonly<{
public pointerDownState: Readonly<{
prevSelectedPointsIndices: readonly number[] | null;
/** index */
lastClickedPoint: number;
}>;
/** whether you're dragging a point */
public readonly isDragging: boolean;
public readonly lastUncommittedPoint: Point | null;
public readonly pointerOffset: Readonly<{ x: number; y: number }>;
public readonly startBindingElement:
| ExcalidrawBindableElement
| null
| "keep";
public readonly endBindingElement: ExcalidrawBindableElement | null | "keep";
public readonly hoverPointIndex: number;
public readonly segmentMidPointHoveredCoords: Point | null;
public isDragging: boolean;
public lastUncommittedPoint: Point | null;
public pointerOffset: Readonly<{ x: number; y: number }>;
public startBindingElement: ExcalidrawBindableElement | null | "keep";
public endBindingElement: ExcalidrawBindableElement | null | "keep";
constructor(
element: NonDeleted<ExcalidrawLinearElement>,
scene: Scene,
appState: AppState,
editingLinearElement = false,
) {
constructor(element: NonDeleted<ExcalidrawLinearElement>, scene: Scene) {
this.elementId = element.id as string & {
_brand: "excalidrawLinearElementId";
};
@@ -92,15 +58,14 @@ export class LinearElementEditor {
prevSelectedPointsIndices: null,
lastClickedPoint: -1,
};
this.hoverPointIndex = -1;
this.segmentMidPointHoveredCoords = null;
}
// ---------------------------------------------------------------------------
// static methods
// ---------------------------------------------------------------------------
static POINT_HANDLE_SIZE = 10;
static POINT_HANDLE_SIZE = 20;
/**
* @param id the `elementId` from the instance of this class (so that we can
* statically guarantee this method returns an ExcalidrawLinearElement)
@@ -167,20 +132,22 @@ export class LinearElementEditor {
/** @returns whether point was dragged */
static handlePointDragging(
event: PointerEvent,
appState: AppState,
setState: React.Component<any, AppState>["setState"],
scenePointerX: number,
scenePointerY: number,
maybeSuggestBinding: (
element: NonDeleted<ExcalidrawLinearElement>,
pointSceneCoords: { x: number; y: number }[],
) => void,
linearElementEditor: LinearElementEditor,
): boolean {
if (!linearElementEditor) {
if (!appState.editingLinearElement) {
return false;
}
const { selectedPointsIndices, elementId } = linearElementEditor;
const { editingLinearElement } = appState;
const { selectedPointsIndices, elementId, isDragging } =
editingLinearElement;
const element = LinearElementEditor.getElement(elementId);
if (!element) {
return false;
@@ -188,72 +155,55 @@ export class LinearElementEditor {
// point that's being dragged (out of all selected points)
const draggingPoint = element.points[
linearElementEditor.pointerDownState.lastClickedPoint
editingLinearElement.pointerDownState.lastClickedPoint
] as [number, number] | undefined;
if (selectedPointsIndices && draggingPoint) {
if (
shouldRotateWithDiscreteAngle(event) &&
selectedPointsIndices.length === 1 &&
element.points.length > 1
) {
const selectedIndex = selectedPointsIndices[0];
const referencePoint =
element.points[selectedIndex === 0 ? 1 : selectedIndex - 1];
const [width, height] = LinearElementEditor._getShiftLockedDelta(
element,
referencePoint,
[scenePointerX, scenePointerY],
appState.gridSize,
);
LinearElementEditor.movePoints(element, [
{
index: selectedIndex,
point: [width + referencePoint[0], height + referencePoint[1]],
isDragging:
selectedIndex ===
linearElementEditor.pointerDownState.lastClickedPoint,
if (isDragging === false) {
setState({
editingLinearElement: {
...editingLinearElement,
isDragging: true,
},
]);
} else {
const newDraggingPointPosition = LinearElementEditor.createPointAt(
element,
scenePointerX - linearElementEditor.pointerOffset.x,
scenePointerY - linearElementEditor.pointerOffset.y,
appState.gridSize,
);
const deltaX = newDraggingPointPosition[0] - draggingPoint[0];
const deltaY = newDraggingPointPosition[1] - draggingPoint[1];
LinearElementEditor.movePoints(
element,
selectedPointsIndices.map((pointIndex) => {
const newPointPosition =
pointIndex ===
linearElementEditor.pointerDownState.lastClickedPoint
? LinearElementEditor.createPointAt(
element,
scenePointerX - linearElementEditor.pointerOffset.x,
scenePointerY - linearElementEditor.pointerOffset.y,
appState.gridSize,
)
: ([
element.points[pointIndex][0] + deltaX,
element.points[pointIndex][1] + deltaY,
] as const);
return {
index: pointIndex,
point: newPointPosition,
isDragging:
pointIndex ===
linearElementEditor.pointerDownState.lastClickedPoint,
};
}),
);
});
}
const newDraggingPointPosition = LinearElementEditor.createPointAt(
element,
scenePointerX - editingLinearElement.pointerOffset.x,
scenePointerY - editingLinearElement.pointerOffset.y,
appState.gridSize,
);
const deltaX = newDraggingPointPosition[0] - draggingPoint[0];
const deltaY = newDraggingPointPosition[1] - draggingPoint[1];
LinearElementEditor.movePoints(
element,
selectedPointsIndices.map((pointIndex) => {
const newPointPosition =
pointIndex ===
editingLinearElement.pointerDownState.lastClickedPoint
? LinearElementEditor.createPointAt(
element,
scenePointerX - editingLinearElement.pointerOffset.x,
scenePointerY - editingLinearElement.pointerOffset.y,
appState.gridSize,
)
: ([
element.points[pointIndex][0] + deltaX,
element.points[pointIndex][1] + deltaY,
] as const);
return {
index: pointIndex,
point: newPointPosition,
isDragging:
pointIndex ===
editingLinearElement.pointerDownState.lastClickedPoint,
};
}),
);
// suggest bindings for first and last point if selected
if (isBindingElement(element, false)) {
const coords: { x: number; y: number }[] = [];
@@ -306,12 +256,10 @@ export class LinearElementEditor {
return editingLinearElement;
}
const bindings: Mutable<
Partial<
Pick<
InstanceType<typeof LinearElementEditor>,
"startBindingElement" | "endBindingElement"
>
const bindings: Partial<
Pick<
InstanceType<typeof LinearElementEditor>,
"startBindingElement" | "endBindingElement"
>
> = {};
@@ -379,310 +327,34 @@ export class LinearElementEditor {
};
}
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,
) => {
const { elementId } = linearElementEditor;
const element = LinearElementEditor.getElement(elementId);
if (!element) {
return null;
}
const clickedPointIndex = LinearElementEditor.getPointIndexUnderCursor(
appState.selectedLinearElement,
appState.zoom,
scenePointer.x,
scenePointer.y,
);
if (clickedPointIndex >= 0) {
return null;
}
const points = LinearElementEditor.getPointsGlobalCoordinates(element);
if (points.length >= 3 && !appState.editingLinearElement) {
return null;
}
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(
event: React.PointerEvent<HTMLCanvasElement>,
appState: AppState,
setState: React.Component<any, AppState>["setState"],
history: History,
scenePointer: { x: number; y: number },
linearElementEditor: LinearElementEditor,
): {
didAddPoint: boolean;
hitElement: NonDeleted<ExcalidrawElement> | null;
linearElementEditor: LinearElementEditor | null;
isMidPoint: boolean;
} {
const ret: ReturnType<typeof LinearElementEditor["handlePointerDown"]> = {
didAddPoint: false,
hitElement: null,
linearElementEditor: null,
isMidPoint: false,
};
if (!linearElementEditor) {
if (!appState.editingLinearElement) {
return ret;
}
const { elementId } = linearElementEditor;
const { elementId } = appState.editingLinearElement;
const element = LinearElementEditor.getElement(elementId);
if (!element) {
return ret;
}
const segmentMidPoint = LinearElementEditor.getSegmentMidpointHitCoords(
linearElementEditor,
scenePointer,
appState,
);
if (segmentMidPoint) {
const index = LinearElementEditor.getSegmentMidPointIndex(
linearElementEditor,
appState,
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;
}
if (event.altKey && appState.editingLinearElement) {
if (linearElementEditor.lastUncommittedPoint == null) {
if (event.altKey) {
if (appState.editingLinearElement.lastUncommittedPoint == null) {
mutateElement(element, {
points: [
...element.points,
@@ -694,29 +366,30 @@ export class LinearElementEditor {
),
],
});
ret.didAddPoint = true;
}
history.resumeRecording();
ret.linearElementEditor = {
...linearElementEditor,
pointerDownState: {
prevSelectedPointsIndices: linearElementEditor.selectedPointsIndices,
lastClickedPoint: -1,
setState({
editingLinearElement: {
...appState.editingLinearElement,
pointerDownState: {
prevSelectedPointsIndices:
appState.editingLinearElement.selectedPointsIndices,
lastClickedPoint: -1,
},
selectedPointsIndices: [element.points.length - 1],
lastUncommittedPoint: null,
endBindingElement: getHoveredElementForBinding(
scenePointer,
Scene.getScene(element)!,
),
},
selectedPointsIndices: [element.points.length - 1],
lastUncommittedPoint: null,
endBindingElement: getHoveredElementForBinding(
scenePointer,
Scene.getScene(element)!,
),
};
});
ret.didAddPoint = true;
return ret;
}
const clickedPointIndex = LinearElementEditor.getPointIndexUnderCursor(
appState.selectedLinearElement,
element,
appState.zoom,
scenePointer.x,
scenePointer.y,
@@ -724,7 +397,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 || segmentMidPoint) {
if (clickedPointIndex > -1) {
ret.hitElement = element;
} else {
// You might be wandering why we are storing the binding elements on
@@ -732,7 +405,8 @@ export class LinearElementEditor {
// from the end points of the `linearElement` - this is to allow disabling
// binding (which needs to happen at the point the user finishes moving
// the point).
const { startBindingElement, endBindingElement } = linearElementEditor;
const { startBindingElement, endBindingElement } =
appState.editingLinearElement;
if (isBindingEnabled(appState) && isBindingElement(element)) {
bindOrUnbindLinearElement(
element,
@@ -758,58 +432,47 @@ export class LinearElementEditor {
const nextSelectedPointsIndices =
clickedPointIndex > -1 || event.shiftKey
? event.shiftKey ||
linearElementEditor.selectedPointsIndices?.includes(clickedPointIndex)
appState.editingLinearElement.selectedPointsIndices?.includes(
clickedPointIndex,
)
? normalizeSelectedPoints([
...(linearElementEditor.selectedPointsIndices || []),
...(appState.editingLinearElement.selectedPointsIndices || []),
clickedPointIndex,
])
: [clickedPointIndex]
: null;
ret.linearElementEditor = {
...linearElementEditor,
pointerDownState: {
prevSelectedPointsIndices: linearElementEditor.selectedPointsIndices,
lastClickedPoint: clickedPointIndex,
},
selectedPointsIndices: nextSelectedPointsIndices,
pointerOffset: targetPoint
? {
x: scenePointer.x - targetPoint[0],
y: scenePointer.y - targetPoint[1],
}
: { 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);
setState({
editingLinearElement: {
...appState.editingLinearElement,
pointerDownState: {
prevSelectedPointsIndices:
appState.editingLinearElement.selectedPointsIndices,
lastClickedPoint: clickedPointIndex,
},
selectedPointsIndices: nextSelectedPointsIndices,
pointerOffset: targetPoint
? {
x: scenePointer.x - targetPoint[0],
y: scenePointer.y - targetPoint[1],
}
: { x: 0, y: 0 },
},
});
return ret;
}
static handlePointerMove(
event: React.PointerEvent<HTMLCanvasElement>,
scenePointerX: number,
scenePointerY: number,
appState: AppState,
): LinearElementEditor | null {
if (!appState.editingLinearElement) {
return null;
}
const { elementId, lastUncommittedPoint } = appState.editingLinearElement;
editingLinearElement: LinearElementEditor,
gridSize: number | null,
): LinearElementEditor {
const { elementId, lastUncommittedPoint } = editingLinearElement;
const element = LinearElementEditor.getElement(elementId);
if (!element) {
return appState.editingLinearElement;
return editingLinearElement;
}
const { points } = element;
@@ -819,36 +482,15 @@ export class LinearElementEditor {
if (lastPoint === lastUncommittedPoint) {
LinearElementEditor.deletePoints(element, [points.length - 1]);
}
return {
...appState.editingLinearElement,
lastUncommittedPoint: null,
};
return { ...editingLinearElement, lastUncommittedPoint: null };
}
let newPoint: Point;
if (shouldRotateWithDiscreteAngle(event) && points.length >= 2) {
const lastCommittedPoint = points[points.length - 2];
const [width, height] = LinearElementEditor._getShiftLockedDelta(
element,
lastCommittedPoint,
[scenePointerX, scenePointerY],
appState.gridSize,
);
newPoint = [
width + lastCommittedPoint[0],
height + lastCommittedPoint[1],
];
} else {
newPoint = LinearElementEditor.createPointAt(
element,
scenePointerX - appState.editingLinearElement.pointerOffset.x,
scenePointerY - appState.editingLinearElement.pointerOffset.y,
appState.gridSize,
);
}
const newPoint = LinearElementEditor.createPointAt(
element,
scenePointerX - editingLinearElement.pointerOffset.x,
scenePointerY - editingLinearElement.pointerOffset.y,
gridSize,
);
if (lastPoint === lastUncommittedPoint) {
LinearElementEditor.movePoints(element, [
@@ -858,10 +500,11 @@ export class LinearElementEditor {
},
]);
} else {
LinearElementEditor.addPoints(element, appState, [{ point: newPoint }]);
LinearElementEditor.addPoints(element, [{ point: newPoint }]);
}
return {
...appState.editingLinearElement,
...editingLinearElement,
lastUncommittedPoint: element.points[element.points.length - 1],
};
}
@@ -883,14 +526,14 @@ export class LinearElementEditor {
/** scene coords */
static getPointsGlobalCoordinates(
element: NonDeleted<ExcalidrawLinearElement>,
): Point[] {
) {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
const cx = (x1 + x2) / 2;
const cy = (y1 + y2) / 2;
return element.points.map((point) => {
let { x, y } = element;
[x, y] = rotate(x + point[0], y + point[1], cx, cy, element.angle);
return [x, y] as const;
return [x, y];
});
}
@@ -908,9 +551,7 @@ export class LinearElementEditor {
const point = element.points[index];
const { x, y } = element;
return point
? rotate(x + point[0], y + point[1], cx, cy, element.angle)
: rotate(x, y, cx, cy, element.angle);
return rotate(x + point[0], y + point[1], cx, cy, element.angle);
}
static pointFromAbsoluteCoords(
@@ -931,37 +572,24 @@ export class LinearElementEditor {
}
static getPointIndexUnderCursor(
linearElementEditor: LinearElementEditor | null,
element: NonDeleted<ExcalidrawLinearElement>,
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 counter = visiblePointIndexesCache.points.length;
const pointHandles = this.getPointsGlobalCoordinates(element);
let idx = pointHandles.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 (--counter >= 0) {
const index = visiblePointIndexesCache.points[counter];
const point = pointHandles[index];
while (--idx > -1) {
const point = pointHandles[idx];
if (
distance2d(x, y, point[0], point[1]) * zoom.value <
// +1px to account for outline stroke
LinearElementEditor.POINT_HANDLE_SIZE + 1
this.POINT_HANDLE_SIZE / 2 + 1
) {
return index;
return idx;
}
}
return -1;
@@ -1118,7 +746,6 @@ export class LinearElementEditor {
static addPoints(
element: NonDeleted<ExcalidrawLinearElement>,
appState: AppState,
targetPoints: { point: Point }[],
) {
const offsetX = 0;
@@ -1148,9 +775,9 @@ export class LinearElementEditor {
if (selectedOriginPoint) {
offsetX =
selectedOriginPoint.point[0] + points[selectedOriginPoint.index][0];
selectedOriginPoint.point[0] - points[selectedOriginPoint.index][0];
offsetY =
selectedOriginPoint.point[1] + points[selectedOriginPoint.index][1];
selectedOriginPoint.point[1] - points[selectedOriginPoint.index][1];
}
const nextPoints = points.map((point, idx) => {
@@ -1213,33 +840,6 @@ export class LinearElementEditor {
y: element.y + rotated[1],
});
}
private static _getShiftLockedDelta(
element: NonDeleted<ExcalidrawLinearElement>,
referencePoint: Point,
scenePointer: Point,
gridSize: number | null,
) {
const referencePointCoords = LinearElementEditor.getPointGlobalCoordinates(
element,
referencePoint,
);
const [gridX, gridY] = getGridPoint(
scenePointer[0],
scenePointer[1],
gridSize,
);
const { width, height } = getLockedLinearCursorAlignSize(
referencePointCoords[0],
referencePointCoords[1],
gridX,
gridY,
);
return rotatePoint([width, height], [0, 0], -element.angle);
}
}
const normalizeSelectedPoints = (
-1
View File
@@ -198,7 +198,6 @@ const getAdjustedDimensions = (
element,
nextWidth,
nextHeight,
false,
);
const deltaX1 = (x1 - nextX1) / 2;
const deltaY1 = (y1 - nextY1) / 2;
+128 -139
View File
@@ -18,7 +18,6 @@ import {
getElementAbsoluteCoords,
getCommonBounds,
getResizedElementAbsoluteCoords,
getCommonBoundingBox,
} from "./bounds";
import {
isFreeDrawElement,
@@ -138,10 +137,8 @@ export const transformElements = (
transformHandleType === "se"
) {
resizeMultipleElements(
pointerDownState,
selectedElements,
transformHandleType,
shouldResizeFromCenter,
pointerX,
pointerY,
);
@@ -264,15 +261,13 @@ const rescalePointsInElement = (
element: NonDeletedExcalidrawElement,
width: number,
height: number,
normalizePoints: boolean,
) =>
isLinearElement(element) || isFreeDrawElement(element)
? {
points: rescalePoints(
0,
width,
rescalePoints(1, height, element.points, normalizePoints),
normalizePoints,
rescalePoints(1, height, element.points),
),
}
: {};
@@ -376,7 +371,6 @@ const resizeSingleTextElement = (
element,
nextWidth,
nextHeight,
false,
);
const deltaX1 = (x1 - nextX1) / 2;
const deltaY1 = (y1 - nextY1) / 2;
@@ -418,7 +412,6 @@ export const resizeSingleElement = (
stateAtResizeStart,
stateAtResizeStart.width,
stateAtResizeStart.height,
true,
);
const startTopLeft: Point = [x1, y1];
const startBottomRight: Point = [x2, y2];
@@ -436,7 +429,6 @@ export const resizeSingleElement = (
element,
element.width,
element.height,
true,
);
const boundsCurrentWidth = esx2 - esx1;
@@ -530,7 +522,6 @@ export const resizeSingleElement = (
stateAtResizeStart,
eleNewWidth,
eleNewHeight,
true,
);
const newBoundsWidth = newBoundsX2 - newBoundsX1;
const newBoundsHeight = newBoundsY2 - newBoundsY1;
@@ -601,7 +592,6 @@ export const resizeSingleElement = (
stateAtResizeStart,
eleNewWidth,
eleNewHeight,
true,
);
// For linear elements (x,y) are the coordinates of the first drawn point not the top-left corner
// So we need to readjust (x,y) to be where the first point should be
@@ -647,147 +637,146 @@ export const resizeSingleElement = (
};
const resizeMultipleElements = (
pointerDownState: PointerDownState,
selectedElements: readonly NonDeletedExcalidrawElement[],
elements: readonly NonDeletedExcalidrawElement[],
transformHandleType: "nw" | "ne" | "sw" | "se",
shouldResizeFromCenter: boolean,
pointerX: number,
pointerY: number,
) => {
// map selected elements to the original elements. While it never should
// happen that pointerDownState.originalElements won't contain the selected
// elements during resize, this coupling isn't guaranteed, so to ensure
// type safety we need to transform only those elements we filter.
const targetElements = selectedElements.reduce(
(
acc: {
/** element at resize start */
orig: NonDeletedExcalidrawElement;
/** latest element */
latest: NonDeletedExcalidrawElement;
}[],
element,
) => {
const origElement = pointerDownState.originalElements.get(element.id);
if (origElement) {
acc.push({ orig: origElement, latest: element });
}
return acc;
},
[],
);
const { minX, minY, maxX, maxY, midX, midY } = getCommonBoundingBox(
targetElements.map(({ orig }) => orig),
);
const direction = transformHandleType;
const mapDirectionsToAnchors: Record<typeof direction, Point> = {
ne: [minX, maxY],
se: [minX, minY],
sw: [maxX, minY],
nw: [maxX, maxY],
};
// anchor point must be on the opposite side of the dragged selection handle
// or be the center of the selection if alt is pressed
const [anchorX, anchorY]: Point = shouldResizeFromCenter
? [midX, midY]
: mapDirectionsToAnchors[direction];
const mapDirectionsToPointerSides: Record<
typeof direction,
[x: boolean, y: boolean]
> = {
ne: [pointerX >= anchorX, pointerY <= anchorY],
se: [pointerX >= anchorX, pointerY >= anchorY],
sw: [pointerX <= anchorX, pointerY >= anchorY],
nw: [pointerX <= anchorX, pointerY <= anchorY],
};
// pointer side relative to anchor
const [pointerSideX, pointerSideY] = mapDirectionsToPointerSides[
direction
].map((condition) => (condition ? 1 : -1));
// stop resizing if a pointer is on the other side of selection
if (pointerSideX < 0 && pointerSideY < 0) {
return;
}
const scale =
Math.max(
(pointerSideX * Math.abs(pointerX - anchorX)) / (maxX - minX),
(pointerSideY * Math.abs(pointerY - anchorY)) / (maxY - minY),
) * (shouldResizeFromCenter ? 2 : 1);
if (scale === 1) {
return;
}
targetElements.forEach((element) => {
const width = element.orig.width * scale;
const height = element.orig.height * scale;
const x = anchorX + (element.orig.x - anchorX) * scale;
const y = anchorY + (element.orig.y - anchorY) * scale;
// readjust points for linear & free draw elements
const rescaledPoints = rescalePointsInElement(
element.orig,
width,
height,
false,
);
const update: {
width: number;
height: number;
x: number;
y: number;
points?: Point[];
fontSize?: number;
baseline?: number;
} = {
width,
height,
x,
y,
...rescaledPoints,
};
let boundTextUpdates: { fontSize: number; baseline: number } | null = null;
const boundTextElement = getBoundTextElement(element.latest);
if (boundTextElement || isTextElement(element.orig)) {
const optionalPadding = boundTextElement ? BOUND_TEXT_PADDING * 2 : 0;
const textMeasurements = measureFontSizeFromWH(
boundTextElement ?? (element.orig as ExcalidrawTextElement),
width - optionalPadding,
height - optionalPadding,
const [x1, y1, x2, y2] = getCommonBounds(elements);
let scale: number;
let getNextXY: (
element: NonDeletedExcalidrawElement,
origCoords: readonly [number, number, number, number],
finalCoords: readonly [number, number, number, number],
) => { x: number; y: number };
switch (transformHandleType) {
case "se":
scale = Math.max(
(pointerX - x1) / (x2 - x1),
(pointerY - y1) / (y2 - y1),
);
if (textMeasurements) {
if (isTextElement(element.orig)) {
update.fontSize = textMeasurements.size;
update.baseline = textMeasurements.baseline;
getNextXY = (element, [origX1, origY1], [finalX1, finalY1]) => {
const x = element.x + (origX1 - x1) * (scale - 1) + origX1 - finalX1;
const y = element.y + (origY1 - y1) * (scale - 1) + origY1 - finalY1;
return { x, y };
};
break;
case "nw":
scale = Math.max(
(x2 - pointerX) / (x2 - x1),
(y2 - pointerY) / (y2 - y1),
);
getNextXY = (element, [, , origX2, origY2], [, , finalX2, finalY2]) => {
const x = element.x - (x2 - origX2) * (scale - 1) + origX2 - finalX2;
const y = element.y - (y2 - origY2) * (scale - 1) + origY2 - finalY2;
return { x, y };
};
break;
case "ne":
scale = Math.max(
(pointerX - x1) / (x2 - x1),
(y2 - pointerY) / (y2 - y1),
);
getNextXY = (element, [origX1, , , origY2], [finalX1, , , finalY2]) => {
const x = element.x + (origX1 - x1) * (scale - 1) + origX1 - finalX1;
const y = element.y - (y2 - origY2) * (scale - 1) + origY2 - finalY2;
return { x, y };
};
break;
case "sw":
scale = Math.max(
(x2 - pointerX) / (x2 - x1),
(pointerY - y1) / (y2 - y1),
);
getNextXY = (element, [, origY1, origX2], [, finalY1, finalX2]) => {
const x = element.x - (x2 - origX2) * (scale - 1) + origX2 - finalX2;
const y = element.y + (origY1 - y1) * (scale - 1) + origY1 - finalY1;
return { x, y };
};
break;
}
if (scale > 0) {
const updates = elements.reduce(
(prev, element) => {
if (!prev) {
return prev;
}
const width = element.width * scale;
const height = element.height * scale;
const boundTextElement = getBoundTextElement(element);
let font: { fontSize?: number; baseline?: number } = {};
if (boundTextElement) {
boundTextUpdates = {
fontSize: textMeasurements.size,
baseline: textMeasurements.baseline,
const nextFont = measureFontSizeFromWH(
boundTextElement,
width - BOUND_TEXT_PADDING * 2,
height - BOUND_TEXT_PADDING * 2,
);
if (nextFont === null) {
return null;
}
font = {
fontSize: nextFont.size,
baseline: nextFont.baseline,
};
}
}
}
mutateElement(element.latest, update);
if (isTextElement(element)) {
const nextFont = measureFontSizeFromWH(element, width, height);
if (nextFont === null) {
return null;
}
font = { fontSize: nextFont.size, baseline: nextFont.baseline };
}
const origCoords = getElementAbsoluteCoords(element);
if (boundTextElement && boundTextUpdates) {
mutateElement(boundTextElement, boundTextUpdates);
handleBindTextResize(element.latest, transformHandleType);
const rescaledPoints = rescalePointsInElement(element, width, height);
updateBoundElements(element, {
newSize: { width, height },
simultaneouslyUpdated: elements,
});
const finalCoords = getResizedElementAbsoluteCoords(
{
...element,
...rescaledPoints,
},
width,
height,
);
const { x, y } = getNextXY(element, origCoords, finalCoords);
return [...prev, { width, height, x, y, ...rescaledPoints, ...font }];
},
[] as
| {
width: number;
height: number;
x: number;
y: number;
points?: (readonly [number, number])[];
fontSize?: number;
baseline?: number;
}[]
| null,
);
if (updates) {
elements.forEach((element, index) => {
mutateElement(element, updates[index]);
const boundTextElement = getBoundTextElement(element);
if (boundTextElement) {
mutateElement(boundTextElement, {
fontSize: updates[index].fontSize,
baseline: updates[index].baseline,
});
handleBindTextResize(element, transformHandleType);
}
});
}
});
}
};
const rotateMultipleElements = (
+16 -18
View File
@@ -1,51 +1,49 @@
import { getPerfectElementSize } from "./sizeHelpers";
import * as constants from "../constants";
const EPSILON_DIGITS = 3;
describe("getPerfectElementSize", () => {
it("should return height:0 if `elementType` is line and locked angle is 0", () => {
const { height, width } = getPerfectElementSize("line", 149, 10);
expect(width).toBeCloseTo(149, EPSILON_DIGITS);
expect(height).toBeCloseTo(0, EPSILON_DIGITS);
expect(width).toEqual(149);
expect(height).toEqual(0);
});
it("should return width:0 if `elementType` is line and locked angle is 90 deg (Math.PI/2)", () => {
const { height, width } = getPerfectElementSize("line", 10, 140);
expect(width).toBeCloseTo(0, EPSILON_DIGITS);
expect(height).toBeCloseTo(140, EPSILON_DIGITS);
expect(width).toEqual(0);
expect(height).toEqual(140);
});
it("should return height:0 if `elementType` is arrow and locked angle is 0", () => {
const { height, width } = getPerfectElementSize("arrow", 200, 20);
expect(width).toBeCloseTo(200, EPSILON_DIGITS);
expect(height).toBeCloseTo(0, EPSILON_DIGITS);
expect(width).toEqual(200);
expect(height).toEqual(0);
});
it("should return width:0 if `elementType` is arrow and locked angle is 90 deg (Math.PI/2)", () => {
const { height, width } = getPerfectElementSize("arrow", 10, 100);
expect(width).toBeCloseTo(0, EPSILON_DIGITS);
expect(height).toBeCloseTo(100, EPSILON_DIGITS);
expect(width).toEqual(0);
expect(height).toEqual(100);
});
it("should return adjust height to be width * tan(locked angle)", () => {
const { height, width } = getPerfectElementSize("arrow", 120, 185);
expect(width).toBeCloseTo(120, EPSILON_DIGITS);
expect(height).toBeCloseTo(207.846, EPSILON_DIGITS);
expect(width).toEqual(120);
expect(height).toEqual(208);
});
it("should return height equals to width if locked angle is 45 deg", () => {
const { height, width } = getPerfectElementSize("arrow", 135, 145);
expect(width).toBeCloseTo(135, EPSILON_DIGITS);
expect(height).toBeCloseTo(135, EPSILON_DIGITS);
expect(width).toEqual(135);
expect(height).toEqual(135);
});
it("should return height:0 and width:0 when width and height are 0", () => {
const { height, width } = getPerfectElementSize("arrow", 0, 0);
expect(width).toBeCloseTo(0, EPSILON_DIGITS);
expect(height).toBeCloseTo(0, EPSILON_DIGITS);
expect(width).toEqual(0);
expect(height).toEqual(0);
});
describe("should respond to SHIFT_LOCKING_ANGLE constant", () => {
it("should have only 2 locking angles per section if SHIFT_LOCKING_ANGLE = 45 deg (Math.PI/4)", () => {
(constants as any).SHIFT_LOCKING_ANGLE = Math.PI / 4;
const { height, width } = getPerfectElementSize("arrow", 120, 185);
expect(width).toBeCloseTo(120, EPSILON_DIGITS);
expect(height).toBeCloseTo(120, EPSILON_DIGITS);
expect(width).toEqual(120);
expect(height).toEqual(120);
});
});
});
+3 -41
View File
@@ -37,7 +37,9 @@ export const getPerfectElementSize = (
} else if (lockedAngle === Math.PI / 2) {
width = 0;
} else {
height = absWidth * Math.tan(lockedAngle) * Math.sign(height) || height;
height =
Math.round(absWidth * Math.tan(lockedAngle)) * Math.sign(height) ||
height;
}
} else if (elementType !== "selection") {
height = absWidth * Math.sign(height);
@@ -45,46 +47,6 @@ export const getPerfectElementSize = (
return { width, height };
};
export const getLockedLinearCursorAlignSize = (
originX: number,
originY: number,
x: number,
y: number,
) => {
let width = x - originX;
let height = y - originY;
const lockedAngle =
Math.round(Math.atan(height / width) / SHIFT_LOCKING_ANGLE) *
SHIFT_LOCKING_ANGLE;
if (lockedAngle === 0) {
height = 0;
} else if (lockedAngle === Math.PI / 2) {
width = 0;
} else {
// locked angle line, y = mx + b => mx - y + b = 0
const a1 = Math.tan(lockedAngle);
const b1 = -1;
const c1 = originY - a1 * originX;
// line through cursor, perpendicular to locked angle line
const a2 = -1 / a1;
const b2 = -1;
const c2 = y - a2 * x;
// intersection of the two lines above
const intersectX = (b1 * c2 - b2 * c1) / (a1 * b2 - a2 * b1);
const intersectY = (c1 * a2 - c2 * a1) / (a1 * b2 - a2 * b1);
// delta
width = intersectX - originX;
height = intersectY - originY;
}
return { width, height };
};
export const resizePerfectLineForNWHandler = (
element: ExcalidrawElement,
x: number,
+13 -33
View File
@@ -1,15 +1,9 @@
import {
ExcalidrawElement,
NonDeletedExcalidrawElement,
PointerType,
} from "./types";
import { ExcalidrawElement, PointerType } from "./types";
import { getElementAbsoluteCoords, Bounds } from "./bounds";
import { rotate } from "../math";
import { AppState, Zoom } from "../types";
import { Zoom } from "../types";
import { isTextElement } from ".";
import { isLinearElement } from "./typeChecks";
import { DEFAULT_SPACING } from "../renderer/renderScene";
export type TransformHandleDirection =
| "n"
@@ -65,6 +59,8 @@ const OMIT_SIDES_FOR_LINE_BACKSLASH = {
s: true,
n: true,
w: true,
ne: true,
sw: true,
};
const generateTransformHandle = (
@@ -86,7 +82,6 @@ export const getTransformHandlesFromCoords = (
zoom: Zoom,
pointerType: PointerType,
omitSides: { [T in TransformHandleType]?: boolean } = {},
margin = 4,
): TransformHandles => {
const size = transformHandleSizes[pointerType];
const handleWidth = size / zoom.value;
@@ -99,7 +94,9 @@ export const getTransformHandlesFromCoords = (
const height = y2 - y1;
const cx = (x1 + x2) / 2;
const cy = (y1 + y2) / 2;
const dashedLineMargin = margin / zoom.value;
const dashedLineMargin = 4 / zoom.value;
const centeringOffset = (size - 8) / (2 * zoom.value);
const transformHandles: TransformHandles = {
@@ -233,7 +230,11 @@ export const getTransformHandles = (
}
let omitSides: { [T in TransformHandleType]?: boolean } = {};
if (element.type === "freedraw" || isLinearElement(element)) {
if (
element.type === "arrow" ||
element.type === "line" ||
element.type === "freedraw"
) {
if (element.points.length === 2) {
// only check the last point because starting point is always (0,0)
const [, p1] = element.points;
@@ -252,33 +253,12 @@ export const getTransformHandles = (
} else if (isTextElement(element)) {
omitSides = OMIT_SIDES_FOR_TEXT_ELEMENT;
}
const dashedLineMargin = isLinearElement(element)
? DEFAULT_SPACING * 3
: DEFAULT_SPACING;
return getTransformHandlesFromCoords(
getElementAbsoluteCoords(element),
element.angle,
zoom,
pointerType,
omitSides,
dashedLineMargin,
);
};
export const shouldShowBoundingBox = (
elements: NonDeletedExcalidrawElement[],
appState: AppState,
) => {
if (appState.editingLinearElement) {
return false;
}
if (elements.length > 1) {
return true;
}
const element = elements[0];
if (!isLinearElement(element)) {
return true;
}
return element.points.length > 2;
};
-1
View File
@@ -56,7 +56,6 @@ type _ExcalidrawElementBase = Readonly<{
updated: number;
link: string | null;
locked: boolean;
customData?: Record<string, any>;
}>;
export type ExcalidrawSelectionElement = _ExcalidrawElementBase & {
+1 -5
View File
@@ -7,8 +7,6 @@ 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;
@@ -22,8 +20,6 @@ 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>({
@@ -35,7 +31,7 @@ const CustomStats = (props: Props) => {
getStorageSizes((sizes) => {
setStorageSizes(sizes);
});
}, [props.elements, props.appState]);
});
useEffect(() => () => getStorageSizes.cancel(), []);
const version = getVersion();
+1
View File
@@ -33,6 +33,7 @@ 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",
+13
View File
@@ -25,6 +25,7 @@ import {
INITIAL_SCENE_UPDATE_TIMEOUT,
LOAD_IMAGES_TIMEOUT,
WS_SCENE_EVENT_TYPES,
STORAGE_KEYS,
SYNC_FULL_SCENE_INTERVAL_MS,
} from "../app_constants";
import {
@@ -224,6 +225,18 @@ 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 (
+1 -1
View File
@@ -14,7 +14,7 @@ import {
} from "../app_constants";
import { UserIdleState } from "../../types";
import { trackEvent } from "../../analytics";
import throttle from "lodash.throttle";
import { throttle } from "lodash";
import { newElementWith } from "../../element/mutateElement";
import { BroadcastedExcalidrawElement } from "./reconciliation";
import { encryptData } from "../../data/encryption";
+19 -39
View File
@@ -1,4 +1,3 @@
import polyfill from "../polyfill";
import LanguageDetector from "i18next-browser-languagedetector";
import { useCallback, useEffect, useRef, useState } from "react";
import { trackEvent } from "../analytics";
@@ -84,7 +83,6 @@ import { jotaiStore, useAtomWithInitialValue } from "../jotai";
import { reconcileElements } from "./collab/reconciliation";
import { parseLibraryTokensFromUrl, useHandleLibrary } from "../data/library";
polyfill();
window.EXCALIDRAW_THROTTLE_RENDER = true;
const isExcalidrawPlusSignedUser = document.cookie.includes(
@@ -98,7 +96,6 @@ languageDetector.init({
const initializeScene = async (opts: {
collabAPI: CollabAPI;
excalidrawAPI: ExcalidrawImperativeAPI;
}): Promise<
{ scene: ExcalidrawInitialDataState | null } & (
| { isExternalScene: true; id: string; key: string }
@@ -183,34 +180,8 @@ const initializeScene = async (opts: {
}
if (roomLinkData) {
const { excalidrawAPI } = opts;
const scene = await opts.collabAPI.startCollaboration(roomLinkData);
return {
// when collaborating, the state may have already been updated at this
// point (we may have received updates from other clients), so reconcile
// elements and appState with existing state
scene: {
...scene,
appState: {
...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,
},
elements: reconcileElements(
scene?.elements || [],
excalidrawAPI.getSceneElementsIncludingDeleted(),
excalidrawAPI.getAppState(),
),
},
scene: await opts.collabAPI.startCollaboration(roomLinkData),
isExternalScene: true,
id: roomLinkData.roomId,
key: roomLinkData.roomKey,
@@ -364,9 +335,23 @@ const ExcalidrawWrapper = () => {
}
};
initializeScene({ collabAPI, excalidrawAPI }).then(async (data) => {
initializeScene({ collabAPI }).then(async (data) => {
loadImages(data, /* isInitialLoad */ true);
initialStatePromiseRef.current.promise.resolve(data.scene);
initialStatePromiseRef.current.promise.resolve({
...data.scene,
// at this point the state may have already been updated (e.g. when
// collaborating, we may have received updates from other clients)
appState: restoreAppState(
data.scene?.appState,
excalidrawAPI.getAppState(),
),
elements: reconcileElements(
data.scene?.elements || [],
excalidrawAPI.getSceneElementsIncludingDeleted(),
excalidrawAPI.getAppState(),
),
});
});
const onHashChange = async (event: HashChangeEvent) => {
@@ -381,7 +366,7 @@ const ExcalidrawWrapper = () => {
}
excalidrawAPI.updateScene({ appState: { isLoading: true } });
initializeScene({ collabAPI, excalidrawAPI }).then((data) => {
initializeScene({ collabAPI }).then((data) => {
loadImages(data);
if (data.scene) {
excalidrawAPI.updateScene({
@@ -672,15 +657,10 @@ const ExcalidrawWrapper = () => {
[langCode],
);
const renderCustomStats = (
elements: readonly NonDeletedExcalidrawElement[],
appState: AppState,
) => {
const renderCustomStats = () => {
return (
<CustomStats
setToast={(message) => excalidrawAPI!.setToast({ message })}
appState={appState}
elements={elements}
/>
);
};
+1 -1
View File
@@ -80,5 +80,5 @@ export const shouldMaintainAspectRatio = (event: MouseEvent | KeyboardEvent) =>
event.shiftKey;
export const shouldRotateWithDiscreteAngle = (
event: MouseEvent | KeyboardEvent | React.PointerEvent<HTMLCanvasElement>,
event: MouseEvent | KeyboardEvent,
) => event.shiftKey;
+2 -1
View File
@@ -187,7 +187,8 @@
"invalidSceneUrl": "تعذر استيراد المشهد من عنوان URL المتوفر. إما أنها مشوهة، أو لا تحتوي على بيانات Excalidraw JSON صالحة.",
"resetLibrary": "هذا سوف يمسح مكتبتك. هل أنت متأكد؟",
"removeItemsFromsLibrary": "حذف {{count}} عنصر (عناصر) من المكتبة؟",
"invalidEncryptionKey": "مفتاح التشفير يجب أن يكون من 22 حرفاً. التعاون المباشر معطل."
"invalidEncryptionKey": "مفتاح التشفير يجب أن يكون من 22 حرفاً. التعاون المباشر معطل.",
"browserZoom": ""
},
"errors": {
"unsupportedFileType": "نوع الملف غير مدعوم.",
+2 -1
View File
@@ -187,7 +187,8 @@
"invalidSceneUrl": "",
"resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": ""
"invalidEncryptionKey": "",
"browserZoom": ""
},
"errors": {
"unsupportedFileType": "Този файлов формат не се поддържа.",
+52 -51
View File
@@ -1,55 +1,55 @@
{
"labels": {
"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 ফাইলের মধ্যে সেভ করা হবে যাতে করে পরবর্তী সময়ে আপনি এডিট করতে পারেন । তবে এতে ফাইলের সাইজ বাড়বে ।.",
"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": "",
"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": "",
@@ -187,7 +187,8 @@
"invalidSceneUrl": "",
"resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": ""
"invalidEncryptionKey": "",
"browserZoom": ""
},
"errors": {
"unsupportedFileType": "",
+10 -9
View File
@@ -108,7 +108,7 @@
"decreaseFontSize": "Redueix la mida de la lletra",
"increaseFontSize": "Augmenta la mida de la lletra",
"unbindText": "Desvincular el text",
"bindText": "Ajusta el text al contenidor",
"bindText": "",
"link": {
"edit": "Edita l'enllaç",
"create": "Crea un enllaç",
@@ -121,12 +121,12 @@
"unlockAll": "Desbloca-ho tot"
},
"statusPublished": "Publicat",
"sidebarLock": "Manté la barra lateral oberta"
"sidebarLock": ""
},
"library": {
"noItems": "Encara no s'hi han afegit elements...",
"hint_emptyLibrary": "Trieu un element o un llenç per a afegir-lo aquí, o instal·leu una biblioteca del repositori públic, més avall.",
"hint_emptyPrivateLibrary": "Trieu un element o un llenç per a afegir-lo aquí."
"noItems": "",
"hint_emptyLibrary": "",
"hint_emptyPrivateLibrary": ""
},
"buttons": {
"clearReset": "Neteja el llenç",
@@ -187,7 +187,8 @@
"invalidSceneUrl": "No s'ha pogut importar l'escena des de l'adreça URL proporcionada. Està malformada o no conté dades Excalidraw JSON vàlides.",
"resetLibrary": "Això buidarà la biblioteca. N'esteu segur?",
"removeItemsFromsLibrary": "Suprimir {{count}} element(s) de la biblioteca?",
"invalidEncryptionKey": "La clau d'encriptació ha de tenir 22 caràcters. La col·laboració en directe està desactivada."
"invalidEncryptionKey": "La clau d'encriptació ha de tenir 22 caràcters. La col·laboració en directe està desactivada.",
"browserZoom": ""
},
"errors": {
"unsupportedFileType": "Tipus de fitxer no suportat.",
@@ -195,7 +196,7 @@
"fileTooBig": "El fitxer és massa gros. La mida màxima permesa és {{maxSize}}.",
"svgImageInsertError": "No ha estat possible inserir la imatge SVG. Les marques SVG semblen invàlides.",
"invalidSVGString": "SVG no vàlid.",
"cannotResolveCollabServer": "No ha estat possible connectar amb el servidor collab. Si us plau recarregueu la pàgina i torneu a provar.",
"cannotResolveCollabServer": "",
"importLibraryError": "No s'ha pogut carregar la biblioteca"
},
"toolBar": {
@@ -306,7 +307,7 @@
"view": "Visualització",
"zoomToFit": "Zoom per veure tots els elements",
"zoomToSelection": "Zoom per veure la selecció",
"toggleElementLock": "Blocar/desblocar la selecció"
"toggleElementLock": ""
},
"clearCanvasDialog": {
"title": "Neteja el llenç"
@@ -324,7 +325,7 @@
"authorName": "Nom o usuari",
"libraryName": "Nom de la vostra biblioteca",
"libraryDesc": "Descripció de la biblioteca per a ajudar a la gent a entendre'n el funcionament",
"githubHandle": "Identificador de GitHub (opcional), per tal que pugueu editar la biblioteca una vegada enviada per a ser revisada",
"githubHandle": "",
"twitterHandle": "Usuari de twitter (opcional), per tal que puguem donar-vos crèdit quan fem la promoció a Twitter",
"website": "Enllaç al vostre lloc web personal o a qualsevol altre (opcional)"
},
+11 -10
View File
@@ -51,10 +51,10 @@
"medium": "Střední",
"large": "Velké",
"veryLarge": "Velmi velké",
"solid": "Plný",
"solid": "",
"hachure": "",
"crossHatch": "",
"thin": "Tenký",
"thin": "",
"bold": "",
"left": "",
"center": "",
@@ -68,7 +68,7 @@
"canvasColors": "",
"canvasBackground": "Pozadí plátna",
"drawingCanvas": "",
"layers": "Vrstvy",
"layers": "",
"actions": "",
"language": "",
"liveCollaboration": "",
@@ -87,16 +87,16 @@
"libraries": "",
"loadingScene": "",
"align": "",
"alignTop": "Zarovnat nahoru",
"alignBottom": "Zarovnat dolů",
"alignLeft": "Zarovnat vlevo",
"alignRight": "Zarovnejte vpravo",
"alignTop": "",
"alignBottom": "",
"alignLeft": "",
"alignRight": "",
"centerVertically": "",
"centerHorizontally": "",
"distributeHorizontally": "",
"distributeVertically": "",
"flipHorizontal": "Převrátit vodorovně",
"flipVertical": "Převrátit svisle",
"flipHorizontal": "",
"flipVertical": "",
"viewMode": "Náhled",
"toggleExportColorScheme": "",
"share": "Sdílet",
@@ -187,7 +187,8 @@
"invalidSceneUrl": "",
"resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": ""
"invalidEncryptionKey": "",
"browserZoom": ""
},
"errors": {
"unsupportedFileType": "",
+2 -1
View File
@@ -187,7 +187,8 @@
"invalidSceneUrl": "",
"resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": ""
"invalidEncryptionKey": "",
"browserZoom": ""
},
"errors": {
"unsupportedFileType": "",
+2 -1
View File
@@ -187,7 +187,8 @@
"invalidSceneUrl": "Die Szene konnte nicht von der angegebenen URL importiert werden. Sie ist entweder fehlerhaft oder enthält keine gültigen Excalidraw JSON-Daten.",
"resetLibrary": "Dieses löscht deine Bibliothek. Bist du sicher?",
"removeItemsFromsLibrary": "{{count}} Element(e) aus der Bibliothek löschen?",
"invalidEncryptionKey": "Verschlüsselungsschlüssel muss 22 Zeichen lang sein. Die Live-Zusammenarbeit ist deaktiviert."
"invalidEncryptionKey": "Verschlüsselungsschlüssel muss 22 Zeichen lang sein. Die Live-Zusammenarbeit ist deaktiviert.",
"browserZoom": "Die Zoomstufe Deines Browsers ist nicht auf 100% gesetzt, was dazu führen kann, dass der Zeichenbereich falsch angezeigt wird"
},
"errors": {
"unsupportedFileType": "Nicht unterstützter Dateityp.",
+68 -67
View File
@@ -9,7 +9,7 @@
"copy": "Αντιγραφή",
"copyAsPng": "Αντιγραφή στο πρόχειρο ως PNG",
"copyAsSvg": "Αντιγραφή στο πρόχειρο ως SVG",
"copyText": "Αντιγραφή στο πρόχειρο ως κείμενο",
"copyText": "",
"bringForward": "Στο προσκήνιο",
"sendToBack": "Ένα επίπεδο πίσω",
"bringToFront": "Ένα επίπεδο μπροστά",
@@ -41,7 +41,7 @@
"fontFamily": "Γραμματοσειρά",
"onlySelected": "Μόνο τα Επιλεγμένα",
"withBackground": "Φόντο",
"exportEmbedScene": "Ενσωμάτωση σκηνής",
"exportEmbedScene": "",
"exportEmbedScene_details": "Τα δεδομένα σκηνής θα αποθηκευτούν στο αρχείο PNG/SVG προς εξαγωγή ώστε η σκηνή να είναι δυνατό να αποκατασταθεί από αυτό.\nΘα αυξήσει το μέγεθος του αρχείου προς εξαγωγή.",
"addWatermark": "Προσθήκη \"Φτιαγμένο με Excalidraw\"",
"handDrawn": "Σχεδιασμένο στο χέρι",
@@ -65,7 +65,7 @@
"cartoonist": "Σκιτσογράφος",
"fileTitle": "Όνομα αρχείου",
"colorPicker": "Επιλογή Χρώματος",
"canvasColors": "Χρησιμοποείται στον καμβά",
"canvasColors": "",
"canvasBackground": "Φόντο καμβά",
"drawingCanvas": "Σχεδίαση καμβά",
"layers": "Στρώματα",
@@ -105,28 +105,28 @@
"toggleTheme": "Εναλλαγή θέματος",
"personalLib": "Προσωπική Βιβλιοθήκη",
"excalidrawLib": "Βιβλιοθήκη Excalidraw",
"decreaseFontSize": "Μείωση μεγέθους γραμματοσειράς",
"increaseFontSize": "Αύξηση μεγέθους γραμματοσειράς",
"unbindText": "Αποσύνδεση κειμένου",
"bindText": "Δέσμευση κειμένου στο δοχείο",
"decreaseFontSize": "",
"increaseFontSize": "",
"unbindText": "",
"bindText": "",
"link": {
"edit": "Επεξεργασία συνδέσμου",
"create": "Δημιουργία συνδέσμου",
"label": "Σύνδεσμος"
"edit": "",
"create": "",
"label": ""
},
"elementLock": {
"lock": "Κλείδωμα",
"unlock": "Ξεκλείδωμα",
"lockAll": "Κλείδωμα όλων",
"unlockAll": "Ξεκλείδωμα όλων"
"lock": "",
"unlock": "",
"lockAll": "",
"unlockAll": ""
},
"statusPublished": "Δημοσιευμένο",
"sidebarLock": "Κρατήστε την πλαϊνή μπάρα ανοιχτή"
"statusPublished": "",
"sidebarLock": ""
},
"library": {
"noItems": "Δεν έχουν προστεθεί αντικείμενα ακόμη...",
"hint_emptyLibrary": "Επιλέξτε ένα στοιχείο στον καμβά για να το προσθέσετε εδώ, ή εγκαταστήστε μια βιβλιοθήκη από το δημόσιο αποθετήριο, παρακάτω.",
"hint_emptyPrivateLibrary": "Επιλέξτε ένα στοιχείο στον καμβά για να το προσθέσετε εδώ."
"noItems": "",
"hint_emptyLibrary": "",
"hint_emptyPrivateLibrary": ""
},
"buttons": {
"clearReset": "Επαναφορά του καμβά",
@@ -174,7 +174,7 @@
"couldNotLoadInvalidFile": "Δεν μπόρεσε να ανοίξει εσφαλμένο αρχείο",
"importBackendFailed": "Η εισαγωγή από το backend απέτυχε.",
"cannotExportEmptyCanvas": "Δεν είναι δυνατή η εξαγωγή κενού καμβά.",
"couldNotCopyToClipboard": "Αδυναμία αντιγραφής στο πρόχειρο.",
"couldNotCopyToClipboard": "",
"decryptFailed": "Δεν ήταν δυνατή η αποκρυπτογράφηση δεδομένων.",
"uploadedSecurly": "Η μεταφόρτωση έχει εξασφαλιστεί με κρυπτογράφηση από άκρο σε άκρο, πράγμα που σημαίνει ότι ο διακομιστής Excalidraw και τρίτα μέρη δεν μπορούν να διαβάσουν το περιεχόμενο.",
"loadSceneOverridePrompt": "Η φόρτωση εξωτερικού σχεδίου θα αντικαταστήσει το υπάρχον περιεχόμενο. Επιθυμείτε να συνεχίσετε;",
@@ -182,21 +182,22 @@
"errorAddingToLibrary": "Αδυναμία προσθήκης αντικειμένου στη βιβλιοθήκη",
"errorRemovingFromLibrary": "Αδυναμία αφαίρεσης αντικειμένου από τη βιβλιοθήκη",
"confirmAddLibrary": "Αυτό θα προσθέσει {{numShapes}} σχήμα(τα) στη βιβλιοθήκη σας. Είστε σίγουροι;",
"imageDoesNotContainScene": "Αυτή η εικόνα δεν φαίνεται να περιέχει δεδομένα σκηνής. Έχετε ενεργοποιήσει την ενσωμάτωση σκηνής κατά την εξαγωγή;",
"imageDoesNotContainScene": "",
"cannotRestoreFromImage": "Η σκηνή δεν ήταν δυνατό να αποκατασταθεί από αυτό το αρχείο εικόνας",
"invalidSceneUrl": "Δεν ήταν δυνατή η εισαγωγή σκηνής από το URL που δώσατε. Είτε έχει λάθος μορφή, είτε δεν περιέχει έγκυρα δεδομένα JSON Excalidraw.",
"invalidSceneUrl": "",
"resetLibrary": "Αυτό θα καθαρίσει τη βιβλιοθήκη σας. Είστε σίγουροι;",
"removeItemsFromsLibrary": "Διαγραφή {{count}} αντικειμένου(ων) από τη βιβλιοθήκη;",
"invalidEncryptionKey": "Το κλειδί κρυπτογράφησης πρέπει να είναι 22 χαρακτήρες. Η ζωντανή συνεργασία είναι απενεργοποιημένη."
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "Το κλειδί κρυπτογράφησης πρέπει να είναι 22 χαρακτήρες. Η ζωντανή συνεργασία είναι απενεργοποιημένη.",
"browserZoom": ""
},
"errors": {
"unsupportedFileType": "Μη υποστηριζόμενος τύπος αρχείου.",
"imageInsertError": "Αδυναμία εισαγωγής εικόνας. Προσπαθήστε ξανά αργότερα...",
"fileTooBig": "Το αρχείο είναι πολύ μεγάλο. Το μέγιστο επιτρεπόμενο μέγεθος είναι {{maxSize}}.",
"svgImageInsertError": "Αδυναμία εισαγωγής εικόνας SVG. Η σήμανση της SVG δεν φαίνεται έγκυρη.",
"svgImageInsertError": "",
"invalidSVGString": "Μη έγκυρο SVG.",
"cannotResolveCollabServer": "Αδυναμία σύνδεσης με τον διακομιστή συνεργασίας. Παρακαλώ ανανεώστε τη σελίδα και προσπαθήστε ξανά.",
"importLibraryError": "Αδυναμία φόρτωσης βιβλιοθήκης"
"cannotResolveCollabServer": "",
"importLibraryError": ""
},
"toolBar": {
"selection": "Επιλογή",
@@ -211,8 +212,8 @@
"library": "Βιβλιοθήκη",
"lock": "Κράτησε επιλεγμένο το εργαλείο μετά το σχέδιο",
"penMode": "",
"link": "Προσθήκη/ Ενημέρωση συνδέσμου για ένα επιλεγμένο σχήμα",
"eraser": "Γόμα"
"link": "",
"eraser": ""
},
"headings": {
"canvasActions": "Ενέργειες καμβά",
@@ -229,16 +230,16 @@
"linearElementMulti": "Κάνε κλικ στο τελευταίο σημείο ή πάτησε Escape ή Enter για να τελειώσεις",
"lockAngle": "Μπορείτε να περιορίσετε τη γωνία κρατώντας πατημένο το SHIFT",
"resize": "Μπορείς να περιορίσεις τις αναλογίες κρατώντας το SHIFT ενώ αλλάζεις μέγεθος,\nκράτησε πατημένο το ALT για αλλαγή μεγέθους από το κέντρο",
"resizeImage": "Μπορείτε να αλλάξετε το μέγεθος ελεύθερα κρατώντας πατημένο το SHIFT,\nκρατήστε πατημένο το ALT για να αλλάξετε το μέγεθος από το κέντρο",
"resizeImage": "",
"rotate": "Μπορείς να περιορίσεις τις γωνίες κρατώντας πατημένο το πλήκτρο SHIFT κατά την περιστροφή",
"lineEditor_info": "Διπλό-κλικ ή πιέστε Enter για να επεξεργαστείτε τα σημεία",
"lineEditor_pointSelected": "Πατήστε Διαγραφή για αφαίρεση σημείου(ων),\nCtrlOrCmd+D για αντιγραφή, ή σύρετε για μετακίνηση",
"lineEditor_nothingSelected": "Επιλέξτε ένα σημείο για να επεξεργαστείτε (κρατήστε πατημένο το SHIFT για να επιλέξετε πολλαπλά),\nή κρατήστε πατημένο το Alt και κάντε κλικ για να προσθέσετε νέα σημεία",
"placeImage": "Κάντε κλικ για να τοποθετήσετε την εικόνα ή κάντε κλικ και σύρετε για να ορίσετε το μέγεθός της χειροκίνητα",
"lineEditor_pointSelected": "",
"lineEditor_nothingSelected": "",
"placeImage": "",
"publishLibrary": "Δημοσιεύστε τη δική σας βιβλιοθήκη",
"bindTextToElement": "Πατήστε Enter για προσθήκη κειμένου",
"deepBoxSelect": "Κρατήστε πατημένο το CtrlOrCmd για να επιλέξετε βαθιά, και να αποτρέψετε τη μεταφορά",
"eraserRevert": "Κρατήστε πατημένο το Alt για να επαναφέρετε τα στοιχεία που σημειώθηκαν για διαγραφή"
"bindTextToElement": "",
"deepBoxSelect": "",
"eraserRevert": ""
},
"canvasError": {
"cannotShowPreview": "Αδυναμία εμφάνισης προεπισκόπησης",
@@ -266,39 +267,39 @@
"desc_inProgressIntro": "Η ζωντανή συνεργασία με άλλους είναι σε ενεργή.",
"desc_shareLink": "Μοιραστείτε τον σύνδεσμο με όποιον θέλετε να δουλέψετε μαζί:",
"desc_exitSession": "Η διακοπή θα σας αποσυνδέσει από το δωμάτιο, αλλά θα μπορείτε να συνεχίσετε να δουλεύετε στον πίνακα, τοπικά. Σημειώσατε ότι αυτό δεν θα επηρεάσει τον πίνακα άλλων, και θα μπορούν ακόμα να συνεισφέρουν στην δική τους έκδοση.",
"shareTitle": "Συμμετάσχετε σε μια ζωντανή συνεδρία συνεργασίας για το Excalidraw"
"shareTitle": ""
},
"errorDialog": {
"title": "Σφάλμα"
},
"exportDialog": {
"disk_title": "Αποθήκευση στο δίσκο",
"disk_details": "Εξαγωγή δεδομένων σκηνής σε ένα αρχείο από το οποίο μπορείτε να εισάγετε αργότερα.",
"disk_details": "",
"disk_button": "Αποθήκευση σε αρχείο",
"link_title": "Κοινόχρηστος σύνδεσμος",
"link_details": "Εξαγωγή ως σύνδεσμο μόνο για ανάγνωση.",
"link_button": "Εξαγωγή σε Σύνδεση",
"excalidrawplus_description": "Αποθηκεύστε τη σκηνή στο χώρο εργασίας σας Excalidraw+.",
"excalidrawplus_description": "",
"excalidrawplus_button": "Εξαγωγή",
"excalidrawplus_exportError": "Δεν ήταν δυνατή η εξαγωγή στο Excalidraw+ αυτή τη στιγμή..."
"excalidrawplus_exportError": ""
},
"helpDialog": {
"blog": "Διαβάστε το Blog μας",
"click": "κλικ",
"deepSelect": "Βαθιά επιλογή",
"deepBoxSelect": "Βαθιά επιλογή μέσα στο πλαίσιο και αποτροπή συρσίματος",
"deepSelect": "",
"deepBoxSelect": "",
"curvedArrow": "Κυρτό βέλος",
"curvedLine": "Κυρτή γραμμή",
"documentation": "Εγχειρίδιο",
"doubleClick": "διπλό κλικ",
"drag": "σύρε",
"editor": "Επεξεργαστής",
"editSelectedShape": "Επεξεργασία επιλεγμένου σχήματος (κείμενο/βέλος/γραμμή)",
"editSelectedShape": "",
"github": "Βρήκατε πρόβλημα; Υποβάλετε το",
"howto": "Ακολουθήστε τους οδηγούς μας",
"or": "ή",
"preventBinding": "Αποτροπή δέσμευσης βέλων",
"tools": "Εργαλεία",
"tools": "",
"shortcuts": "Συντομεύσεις πληκτρολογίου",
"textFinish": "Ολοκλήρωση επεξεργασίας (επεξεργαστής κειμένου)",
"textNewLine": "Προσθήκη νέας γραμμής (επεξεργαστής κειμένου)",
@@ -306,54 +307,54 @@
"view": "Προβολή",
"zoomToFit": "Zoom ώστε να χωρέσουν όλα τα στοιχεία",
"zoomToSelection": "Ζουμ στην επιλογή",
"toggleElementLock": "Κλείδωμα/Ξεκλείδωμα επιλογής"
"toggleElementLock": ""
},
"clearCanvasDialog": {
"title": "Καθαρισμός καμβά"
},
"publishDialog": {
"title": "Δημοσίευση βιβλιοθήκης",
"itemName": "Όνομα αντικειμένου",
"title": "",
"itemName": "",
"authorName": "Όνομα δημιουργού",
"githubUsername": "GitHub username",
"twitterUsername": "Twitter username",
"libraryName": "Όνομα βιβλιοθήκης",
"libraryDesc": "Περιγραφή βιβλιοθήκης",
"libraryDesc": "",
"website": "Ιστοσελίδα",
"placeholder": {
"authorName": "Όνομα ή όνομα χρήστη",
"libraryName": "Όνομα της βιβλιοθήκης σας",
"libraryDesc": "Περιγραφή της βιβλιοθήκης σας ώστε να βοηθήσει το κοινό να κατανοήσει τη χρήση της",
"githubHandle": "Όνομα χρήστη στο GitHub (προαιρετικό), ώστε να μπορείτε να επεξεργαστείτε τη βιβλιοθήκη αφού υποβληθεί για αξιολόγηση",
"twitterHandle": "Όνομα χρήστη Twitter (προαιρετικό), ώστε να γνωρίζουμε σε ποιον/η να δώσουμε εύσημα κατά την προώθηση μέσω Twitter",
"website": "Σύνδεσμος για την προσωπική σας ιστοσελίδα ή αλλού (προαιρετικό)"
"authorName": "",
"libraryName": "",
"libraryDesc": "",
"githubHandle": "",
"twitterHandle": "",
"website": ""
},
"errors": {
"required": "Απαιτείται",
"website": "Εισάγετε μια έγκυρη διεύθυνση URL"
},
"noteDescription": {
"pre": "Υποβάλετε τη βιβλιοθήκη σας για να συμπεριληφθεί στο ",
"link": "δημόσιο αποθετήριο βιβλιοθήκης",
"post": "ώστε να χρησιμοποιηθεί από άλλα άτομα στα σχέδιά τους."
"pre": "",
"link": "",
"post": ""
},
"noteGuidelines": {
"pre": "Η βιβλιοθήκη πρέπει πρώτα να εγκριθεί χειροκίνητα. Παρακαλώ διαβάστε τους ",
"pre": "",
"link": "οδηγίες",
"post": " πριν την υποβολή. Θα χρειαστείτε έναν λογαριασμό GitHub για την επικοινωνία και για να προβείτε σε αλλαγές εφ' όσον χρειαστεί, αλλά δεν είναι αυστηρή απαίτηση."
"post": ""
},
"noteLicense": {
"pre": "Με την υποβολή, συμφωνείτε ότι η βιβλιοθήκη θα δημοσιευθεί υπό την ",
"link": "Άδεια MIT, ",
"post": "που εν συντομία σημαίνει ότι ο καθένας μπορεί να τα χρησιμοποιήσει χωρίς περιορισμούς."
"pre": "",
"link": "",
"post": ""
},
"noteItems": "Κάθε αντικείμενο της βιβλιοθήκης πρέπει να έχει το δικό του όνομα ώστε να μπορεί να φιλτραριστεί. Θα συμπεριληφθούν τα ακόλουθα αντικείμενα βιβλιοθήκης:",
"atleastOneLibItem": "Παρακαλώ επιλέξτε τουλάχιστον ένα αντικείμενο βιβλιοθήκης για να ξεκινήσετε",
"republishWarning": "Σημείωση: μερικά από τα επιλεγμένα αντικέιμενα έχουν ήδη επισημανθεί ως δημοσιευμένα/υποβεβλημένα. Θα πρέπει να υποβάλετε αντικείμενα εκ νέου μόνο για να ενημερώσετε μία ήδη υπάρχουσα βιβλιοθήκη ή υποβολή."
"noteItems": "",
"atleastOneLibItem": "",
"republishWarning": ""
},
"publishSuccessDialog": {
"title": "Η βιβλιοθήκη υποβλήθηκε",
"content": "Ευχαριστούμε {{authorName}}. Η βιβλιοθήκη σας έχει υποβληθεί για αξιολόγηση. Μπορείτε να παρακολουθείτε τη διαδικασία",
"title": "",
"content": "",
"link": "εδώ"
},
"confirmDialog": {
+2 -1
View File
@@ -187,7 +187,8 @@
"invalidSceneUrl": "Couldn't import scene from the supplied URL. It's either malformed, or doesn't contain valid Excalidraw JSON data.",
"resetLibrary": "This will clear your library. Are you sure?",
"removeItemsFromsLibrary": "Delete {{count}} item(s) from library?",
"invalidEncryptionKey": "Encryption key must be of 22 characters. Live collaboration is disabled."
"invalidEncryptionKey": "Encryption key must be of 22 characters. Live collaboration is disabled.",
"browserZoom": "Your browser's zoom level is not set to 100% which may cause the board to display incorrectly"
},
"errors": {
"unsupportedFileType": "Unsupported file type.",
+3 -2
View File
@@ -124,7 +124,7 @@
"sidebarLock": "Mantener barra lateral abierta"
},
"library": {
"noItems": "No hay elementos añadidos todavía...",
"noItems": "",
"hint_emptyLibrary": "Seleccione un elemento en el lienzo para añadirlo aquí, o instale una biblioteca del repositorio público, a continuación.",
"hint_emptyPrivateLibrary": "Seleccione un elemento del lienzo para añadirlo aquí."
},
@@ -187,7 +187,8 @@
"invalidSceneUrl": "No se ha podido importar la escena desde la URL proporcionada. Está mal formada, o no contiene datos de Excalidraw JSON válidos.",
"resetLibrary": "Esto borrará tu biblioteca. ¿Estás seguro?",
"removeItemsFromsLibrary": "¿Eliminar {{count}} elemento(s) de la biblioteca?",
"invalidEncryptionKey": "La clave de cifrado debe tener 22 caracteres. La colaboración en vivo está deshabilitada."
"invalidEncryptionKey": "La clave de cifrado debe tener 22 caracteres. La colaboración en vivo está deshabilitada.",
"browserZoom": "El nivel de zoom de tu navegador no está configurado al 100%, lo que puede causar que el tablero se muestre de manera incorrecta"
},
"errors": {
"unsupportedFileType": "Tipo de archivo no admitido.",
+6 -5
View File
@@ -121,12 +121,12 @@
"unlockAll": "Desblokeatu guztiak"
},
"statusPublished": "Argitaratua",
"sidebarLock": "Mantendu alboko barra irekita"
"sidebarLock": ""
},
"library": {
"noItems": "Oraindik ez da elementurik gehitu...",
"hint_emptyLibrary": "Hautatu oihaleko elementu bat hemen gehitzeko, edo instalatu liburutegi bat beheko biltegi publikotik.",
"hint_emptyPrivateLibrary": "Hautatu oihaleko elementu bat hemen gehitzeko."
"noItems": "",
"hint_emptyLibrary": "",
"hint_emptyPrivateLibrary": ""
},
"buttons": {
"clearReset": "Garbitu oihala",
@@ -187,7 +187,8 @@
"invalidSceneUrl": "Ezin izan da eszena inportatu emandako URLtik. Gaizki eratuta dago edo ez du baliozko Excalidraw JSON daturik.",
"resetLibrary": "Honek zure liburutegia garbituko du. Ziur zaude?",
"removeItemsFromsLibrary": "Liburutegitik {{count}} elementu ezabatu?",
"invalidEncryptionKey": "Enkriptazio-gakoak 22 karaktere izan behar ditu. Zuzeneko lankidetza desgaituta dago."
"invalidEncryptionKey": "Enkriptazio-gakoak 22 karaktere izan behar ditu. Zuzeneko lankidetza desgaituta dago.",
"browserZoom": ""
},
"errors": {
"unsupportedFileType": "Onartu gabeko fitxategi mota.",
+2 -1
View File
@@ -187,7 +187,8 @@
"invalidSceneUrl": "بوم نقاشی از آدرس ارائه شده وارد نشد. این یا نادرست است، یا حاوی داده Excalidraw JSON معتبر نیست.",
"resetLibrary": "ین کار کل صفحه را پاک میکند. آیا مطمئنید?",
"removeItemsFromsLibrary": "حذف {{count}} آیتم(ها) از کتابخانه?",
"invalidEncryptionKey": "کلید رمزگذاری باید 22 کاراکتر باشد. همکاری زنده غیرفعال است."
"invalidEncryptionKey": "کلید رمزگذاری باید 22 کاراکتر باشد. همکاری زنده غیرفعال است.",
"browserZoom": ""
},
"errors": {
"unsupportedFileType": "نوع فایل پشتیبانی نشده.",
+2 -1
View File
@@ -187,7 +187,8 @@
"invalidSceneUrl": "Teosta ei voitu tuoda annetusta URL-osoitteesta. Tallenne on vioittunut, tai osoitteessa ei ole Excalidraw JSON-dataa.",
"resetLibrary": "Tämä tyhjentää kirjastosi. Jatketaanko?",
"removeItemsFromsLibrary": "Poista {{count}} kohdetta kirjastosta?",
"invalidEncryptionKey": "Salausavaimen on oltava 22 merkkiä pitkä. Live-yhteistyö ei ole käytössä."
"invalidEncryptionKey": "Salausavaimen on oltava 22 merkkiä pitkä. Live-yhteistyö ei ole käytössä.",
"browserZoom": ""
},
"errors": {
"unsupportedFileType": "Tiedostotyyppiä ei tueta.",
+35 -34
View File
@@ -10,29 +10,29 @@
"copyAsPng": "Copier dans le presse-papier en PNG",
"copyAsSvg": "Copier dans le presse-papier en SVG",
"copyText": "Copier dans le presse-papier en tant que texte",
"bringForward": "Avancer d'un plan",
"sendToBack": "Déplacer à l'arrière-plan",
"bringToFront": "Placer au premier plan",
"sendBackward": "Reculer d'un plan",
"bringForward": "Envoyer vers l'avant",
"sendToBack": "Mettre en arrière-plan",
"bringToFront": "Mettre au premier plan",
"sendBackward": "Envoyer vers l'arrière",
"delete": "Supprimer",
"copyStyles": "Copier les styles",
"pasteStyles": "Coller les styles",
"stroke": "Trait",
"background": "Fond",
"fill": "Motif du fond",
"strokeWidth": "Épaisseur du trait",
"background": "Arrière-plan",
"fill": "Remplissage",
"strokeWidth": "Largeur du trait",
"strokeStyle": "Style du trait",
"strokeStyle_solid": "Continu",
"strokeStyle_solid": "Plein",
"strokeStyle_dashed": "Tirets",
"strokeStyle_dotted": "Pointillés",
"strokeStyle_dotted": "Pointillé",
"sloppiness": "Style de tracé",
"opacity": "Opacité",
"textAlign": "Alignement du texte",
"edges": "Angles",
"sharp": "Pointus",
"round": "Arrondis",
"arrowheads": "Extrémités",
"arrowhead_none": "Sans",
"arrowheads": "Extrémités de flèche",
"arrowhead_none": "Aucune",
"arrowhead_arrow": "Flèche",
"arrowhead_bar": "Barre",
"arrowhead_dot": "Point",
@@ -43,23 +43,23 @@
"withBackground": "Arrière-plan",
"exportEmbedScene": "Intégrer la scène",
"exportEmbedScene_details": "Les données de scène seront enregistrées dans le fichier PNG/SVG exporté, afin que la scène puisse être restaurée à partir de celui-ci.\nCela augmentera la taille du fichier exporté.",
"addWatermark": "Ajouter \"Réalisé avec Excalidraw\"",
"addWatermark": "Ajouter \"Fait avec Excalidraw\"",
"handDrawn": "À la main",
"normal": "Normale",
"code": "Code",
"small": "Petite",
"medium": "Moyenne",
"large": "Grande",
"veryLarge": "Très grande",
"small": "Petit",
"medium": "Moyen",
"large": "Grand",
"veryLarge": "Très grand",
"solid": "Solide",
"hachure": "Hachures",
"crossHatch": "Hachures croisées",
"thin": "Fine",
"bold": "Épaisse",
"left": "À gauche",
"center": "Au centre",
"right": "À droite",
"extraBold": "Très épaisse",
"hachure": "Hachure",
"crossHatch": "Hachure croisée",
"thin": "Fin",
"bold": "Épais",
"left": "Gauche",
"center": "Centre",
"right": "Droite",
"extraBold": "Très épais",
"architect": "Architecte",
"artist": "Artiste",
"cartoonist": "Caricaturiste",
@@ -68,7 +68,7 @@
"canvasColors": "Utilisé sur la zone de dessin",
"canvasBackground": "Arrière-plan du canevas",
"drawingCanvas": "Zone de dessin",
"layers": "Disposition",
"layers": "Calques",
"actions": "Actions",
"language": "Langue",
"liveCollaboration": "Collaboration en direct",
@@ -86,22 +86,22 @@
"libraryLoadingMessage": "Chargement de la bibliothèque…",
"libraries": "Parcourir les bibliothèques",
"loadingScene": "Chargement de la scène…",
"align": "Alignement",
"align": "Aligner",
"alignTop": "Aligner en haut",
"alignBottom": "Aligner en bas",
"alignLeft": "Aligner à gauche",
"alignRight": "Aligner à droite",
"centerVertically": "Centrer verticalement",
"centerHorizontally": "Centrer horizontalement",
"distributeHorizontally": "Répartir horizontalement",
"distributeVertically": "Répartir verticalement",
"distributeHorizontally": "Distribuer horizontalement",
"distributeVertically": "Distribuer verticalement",
"flipHorizontal": "Retourner horizontalement",
"flipVertical": "Retourner verticalement",
"viewMode": "Mode présentation",
"toggleExportColorScheme": "Activer/Désactiver l'export du thème de couleur",
"share": "Partager",
"showStroke": "Afficher le sélecteur de couleur de trait",
"showBackground": "Afficher le sélecteur de couleur de fond",
"showBackground": "Afficher le sélecteur de couleur d'arrière-plan",
"toggleTheme": "Changer le thème",
"personalLib": "Bibliothèque personnelle",
"excalidrawLib": "Bibliothèque Excalidraw",
@@ -170,9 +170,9 @@
"alerts": {
"clearReset": "L'intégralité du canevas va être effacée. Êtes-vous sûr ?",
"couldNotCreateShareableLink": "Impossible de créer un lien de partage.",
"couldNotCreateShareableLinkTooBig": "Impossible de créer un lien de partage : la scène est trop volumineuse",
"couldNotCreateShareableLinkTooBig": "Impossible de créer un lien partageable : la scène est trop volumineuse",
"couldNotLoadInvalidFile": "Impossible de charger un fichier invalide",
"importBackendFailed": "L'importation depuis le serveur a échoué.",
"importBackendFailed": "L'importation depuis le backend a échoué.",
"cannotExportEmptyCanvas": "Impossible d'exporter un canevas vide.",
"couldNotCopyToClipboard": "Impossible de copier dans le presse-papiers.",
"decryptFailed": "Les données n'ont pas pu être déchiffrées.",
@@ -187,7 +187,8 @@
"invalidSceneUrl": "Impossible d'importer la scène depuis l'URL fournie. Elle est soit incorrecte, soit ne contient pas de données JSON Excalidraw valides.",
"resetLibrary": "Cela va effacer votre bibliothèque. Êtes-vous sûr·e ?",
"removeItemsFromsLibrary": "Supprimer {{count}} élément(s) de la bibliothèque ?",
"invalidEncryptionKey": "La clé de chiffrement doit comporter 22 caractères. La collaboration en direct est désactivée."
"invalidEncryptionKey": "La clé de chiffrement doit comporter 22 caractères. La collaboration en direct est désactivée.",
"browserZoom": "Le niveau de zoom de votre navigateur n'est pas défini sur 100 %, ce qui peut entraîner un affichage incorrect du tableau"
},
"errors": {
"unsupportedFileType": "Type de fichier non supporté.",
@@ -228,7 +229,7 @@
"text_editing": "Appuyez sur ÉCHAP ou Ctrl/Cmd+ENTRÉE pour terminer l'édition",
"linearElementMulti": "Cliquez sur le dernier point ou appuyez sur Échap ou Entrée pour terminer",
"lockAngle": "Vous pouvez restreindre l'angle en maintenant MAJ",
"resize": "Vous pouvez conserver les proportions en maintenant la touche MAJ pendant le redimensionnement, maintenez la touche ALT pour redimensionner par rapport au centre",
"resize": "Vous pouvez conserver les proportions en maintenant la touche MAJ pendant le redimensionnement,\nmaintenez la touche ALT pour redimensionner par rapport au centre",
"resizeImage": "Vous pouvez redimensionner librement en maintenant SHIFT,\nmaintenez ALT pour redimensionner depuis le centre",
"rotate": "Vous pouvez restreindre les angles en maintenant MAJ pendant la rotation",
"lineEditor_info": "Double-cliquez ou appuyez sur Entrée pour éditer les points",
@@ -237,7 +238,7 @@
"placeImage": "Cliquez pour placer l'image, ou cliquez et faites glisser pour définir sa taille manuellement",
"publishLibrary": "Publier votre propre bibliothèque",
"bindTextToElement": "Appuyer sur Entrée pour ajouter du texte",
"deepBoxSelect": "Maintenir Ctrl ou Cmd pour sélectionner dans les groupes et empêcher le déplacement",
"deepBoxSelect": "Maintenir CtrlOuCmd pour sélectionner dans les groupes, et empêcher le déplacement",
"eraserRevert": "Maintenez Alt enfoncé pour annuler les éléments marqués pour suppression"
},
"canvasError": {
+2 -1
View File
@@ -187,7 +187,8 @@
"invalidSceneUrl": "",
"resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": ""
"invalidEncryptionKey": "",
"browserZoom": ""
},
"errors": {
"unsupportedFileType": "",
+2 -1
View File
@@ -187,7 +187,8 @@
"invalidSceneUrl": "ייבוא המידע מן סצינה מכתובת האינטרנט נכשלה. המידע בנוי באופן משובש או שהוא אינו קובץ JSON תקין של Excalidraw.",
"resetLibrary": "פעולה זו תנקה את כל הלוח. אתה בטוח?",
"removeItemsFromsLibrary": "מחיקת {{count}} פריטים(ים) מתוך הספריה?",
"invalidEncryptionKey": "מפתח ההצפנה חייב להיות בן 22 תוים. השיתוף החי מבוטל."
"invalidEncryptionKey": "מפתח ההצפנה חייב להיות בן 22 תוים. השיתוף החי מבוטל.",
"browserZoom": ""
},
"errors": {
"unsupportedFileType": "סוג הקובץ אינו נתמך.",
+2 -1
View File
@@ -187,7 +187,8 @@
"invalidSceneUrl": "",
"resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": ""
"invalidEncryptionKey": "",
"browserZoom": "आपके ब्राउज़र का ज़ूम लेवल 100% नहीं हैं इस कारण दृष्य पटल ग़लत दिख सकता हैं"
},
"errors": {
"unsupportedFileType": "",
+2 -1
View File
@@ -187,7 +187,8 @@
"invalidSceneUrl": "Nem sikerült importálni a jelenetet a megadott URL-ről. Rossz formátumú, vagy nem tartalmaz érvényes Excalidraw JSON-adatokat.",
"resetLibrary": "Ezzel törlöd a könyvtárát. biztos vagy ebben?",
"removeItemsFromsLibrary": "{{count}} elemet törölsz a könyvtárból?",
"invalidEncryptionKey": "A titkosítási kulcsnak 22 karakterből kell állnia. Az élő együttműködés le van tiltva."
"invalidEncryptionKey": "A titkosítási kulcsnak 22 karakterből kell állnia. Az élő együttműködés le van tiltva.",
"browserZoom": ""
},
"errors": {
"unsupportedFileType": "Nem támogatott fájltípus.",
+2 -1
View File
@@ -187,7 +187,8 @@
"invalidSceneUrl": "Tidak dapat impor pemandangan dari URL. Kemungkinan URL itu rusak atau tidak berisi data JSON Excalidraw yang valid.",
"resetLibrary": "Ini akan menghapus pustaka Anda. Anda yakin?",
"removeItemsFromsLibrary": "Hapus {{count}} item dari pustaka?",
"invalidEncryptionKey": "Sandi enkripsi harus 22 karakter. Kolaborasi langsung dinonaktifkan."
"invalidEncryptionKey": "Sandi enkripsi harus 22 karakter. Kolaborasi langsung dinonaktifkan.",
"browserZoom": "Pembesaran peramban Anda tidak 100% yang mana dapat menyebabkan layar tidak menampilkan dengan benar"
},
"errors": {
"unsupportedFileType": "Tipe file tidak didukung.",
+2 -1
View File
@@ -187,7 +187,8 @@
"invalidSceneUrl": "Impossibile importare la scena dall'URL fornito. Potrebbe essere malformato o non contenere dati JSON Excalidraw validi.",
"resetLibrary": "Questa azione cancellerà l'intera libreria. Sei sicuro?",
"removeItemsFromsLibrary": "Eliminare {{count}} elementi dalla libreria?",
"invalidEncryptionKey": "La chiave di cifratura deve essere composta da 22 caratteri. La collaborazione live è disabilitata."
"invalidEncryptionKey": "La chiave di cifratura deve essere composta da 22 caratteri. La collaborazione live è disabilitata.",
"browserZoom": "Il livello di zoom del tuo browser non è impostato al 100%, il che potrebbe causare una visualizzazione scorretta della scheda"
},
"errors": {
"unsupportedFileType": "Tipo di file non supportato.",
+6 -5
View File
@@ -121,12 +121,12 @@
"unlockAll": "すべてのロックを解除"
},
"statusPublished": "公開済み",
"sidebarLock": "サイドバーを開いたままにする"
"sidebarLock": ""
},
"library": {
"noItems": "まだアイテムが追加されていません…",
"hint_emptyLibrary": "キャンバス上のアイテムを選択してここに追加するか、以下の公開リポジトリからライブラリをインストールしてください。",
"hint_emptyPrivateLibrary": "キャンバス上のアイテムを選択すると、ここに追加されます。"
"noItems": "",
"hint_emptyLibrary": "",
"hint_emptyPrivateLibrary": ""
},
"buttons": {
"clearReset": "キャンバスのリセット",
@@ -187,7 +187,8 @@
"invalidSceneUrl": "指定された URL からシーンをインポートできませんでした。不正な形式であるか、有効な Excalidraw JSON データが含まれていません。",
"resetLibrary": "ライブラリを消去します。本当によろしいですか?",
"removeItemsFromsLibrary": "{{count}} 個のアイテムをライブラリから削除しますか?",
"invalidEncryptionKey": "暗号化キーは22文字でなければなりません。ライブコラボレーションは無効化されています。"
"invalidEncryptionKey": "暗号化キーは22文字でなければなりません。ライブコラボレーションは無効化されています。",
"browserZoom": ""
},
"errors": {
"unsupportedFileType": "サポートされていないファイル形式です。",
+2 -1
View File
@@ -187,7 +187,8 @@
"invalidSceneUrl": "Ulamek taktert n usayes seg URL i d-ittunefken. Ahat mačči d tameɣtut neɣ ur tegbir ara isefka JSON n Excalidraw.",
"resetLibrary": "Ayagi ad isfeḍ tamkarḍit-inek•m. Tetḥeqqeḍ?",
"removeItemsFromsLibrary": "Ad tekkseḍ {{count}} n uferdis (en) si temkarḍit?",
"invalidEncryptionKey": "Tasarut n uwgelhen isefk ad tesɛu 22 n yiekkilen. Amɛiwen srid yensa."
"invalidEncryptionKey": "Tasarut n uwgelhen isefk ad tesɛu 22 n yiekkilen. Amɛiwen srid yensa.",
"browserZoom": ""
},
"errors": {
"unsupportedFileType": "Anaw n ufaylu ur yettwasefrak ara.",
+2 -1
View File
@@ -187,7 +187,8 @@
"invalidSceneUrl": "",
"resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": ""
"invalidEncryptionKey": "",
"browserZoom": ""
},
"errors": {
"unsupportedFileType": "",
+4 -3
View File
@@ -121,10 +121,10 @@
"unlockAll": "모두 잠금 해제"
},
"statusPublished": "게시됨",
"sidebarLock": "사이드바 유지"
"sidebarLock": ""
},
"library": {
"noItems": "추가된 아이템 없음",
"noItems": "",
"hint_emptyLibrary": "",
"hint_emptyPrivateLibrary": ""
},
@@ -187,7 +187,8 @@
"invalidSceneUrl": "제공된 URL에서 화면을 가져오는데 실패했습니다. 주소가 잘못되거나, 유효한 Excalidraw JSON 데이터를 포함하고 있지 않은 것일 수 있습니다.",
"resetLibrary": "당신의 라이브러리를 초기화 합니다. 계속하시겠습니까?",
"removeItemsFromsLibrary": "{{count}}개의 아이템을 라이브러리에서 삭제하시겠습니까?",
"invalidEncryptionKey": "암호화 키는 반드시 22글자여야 합니다. 실시간 협업이 비활성화됩니다."
"invalidEncryptionKey": "암호화 키는 반드시 22글자여야 합니다. 실시간 협업이 비활성화됩니다.",
"browserZoom": ""
},
"errors": {
"unsupportedFileType": "지원하지 않는 파일 형식 입니다.",
+7 -6
View File
@@ -110,13 +110,13 @@
"unbindText": "",
"bindText": "",
"link": {
"edit": "Redeguoti nuorodą",
"create": "Sukurti nuorodą",
"label": "Nuoroda"
"edit": "",
"create": "",
"label": ""
},
"elementLock": {
"lock": "Užrakinti",
"unlock": "Atrakinti",
"lock": "",
"unlock": "",
"lockAll": "",
"unlockAll": ""
},
@@ -187,7 +187,8 @@
"invalidSceneUrl": "",
"resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": ""
"invalidEncryptionKey": "",
"browserZoom": ""
},
"errors": {
"unsupportedFileType": "",
+2 -1
View File
@@ -187,7 +187,8 @@
"invalidSceneUrl": "Nevarēja importēt ainu no norādītā URL. Vai nu tas ir nederīgs, vai nesatur derīgus Excalidraw JSON datus.",
"resetLibrary": "Šī funkcija iztukšos bibliotēku. Vai turpināt?",
"removeItemsFromsLibrary": "Vai izņemt {{count}} vienumu(s) no bibliotēkas?",
"invalidEncryptionKey": "Šifrēšanas atslēgai jābūt 22 simbolus garai. Tiešsaistes sadarbība ir izslēgta."
"invalidEncryptionKey": "Šifrēšanas atslēgai jābūt 22 simbolus garai. Tiešsaistes sadarbība ir izslēgta.",
"browserZoom": "Pārlūka pietuvināšanas līmenis nav 100%; šis var sakropļot tāfeles izskatu"
},
"errors": {
"unsupportedFileType": "Neatbalstīts datnes veids.",
+2 -1
View File
@@ -187,7 +187,8 @@
"invalidSceneUrl": "दिलेल्या यू-आर-एल पासून दृश्य आणू शकलो नाही. तो एकतर बरोबार नाही आहे किंवा त्यात वैध एक्सकेलीड्रॉ जेसन डेटा नाही.",
"resetLibrary": "पटल स्वच्छ होणार, तुम्हाला खात्री आहे का?",
"removeItemsFromsLibrary": "संग्रहातून {{count}} तत्व (एक किव्हा अनेक) काढू?",
"invalidEncryptionKey": "कूटबद्धन कुंजी 22 अक्षरांची असणे आवश्यक आहे. थेट सहयोग अक्षम केले आहे."
"invalidEncryptionKey": "कूटबद्धन कुंजी 22 अक्षरांची असणे आवश्यक आहे. थेट सहयोग अक्षम केले आहे.",
"browserZoom": "वेब ब्राउज़र चे ज़ूम लेवल 100% नाही आहे त्या कारणानी पटल चूक दिसू सकतो"
},
"errors": {
"unsupportedFileType": "असमर्थित फाइल प्रकार.",
+2 -1
View File
@@ -187,7 +187,8 @@
"invalidSceneUrl": "",
"resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": ""
"invalidEncryptionKey": "",
"browserZoom": ""
},
"errors": {
"unsupportedFileType": "",
+2 -1
View File
@@ -187,7 +187,8 @@
"invalidSceneUrl": "Kunne ikke importere scene fra den oppgitte URL-en. Den er enten ødelagt, eller inneholder ikke gyldig Excalidraw JSON-data.",
"resetLibrary": "Dette vil tømme biblioteket ditt. Er du sikker?",
"removeItemsFromsLibrary": "Slett {{count}} element(er) fra biblioteket?",
"invalidEncryptionKey": "Krypteringsnøkkel må ha 22 tegn. Live-samarbeid er deaktivert."
"invalidEncryptionKey": "Krypteringsnøkkel må ha 22 tegn. Live-samarbeid er deaktivert.",
"browserZoom": "Nettleserens zoomnivå er ikke satt til 100%, som kan føre til at lerretet vises feil"
},
"errors": {
"unsupportedFileType": "Filtypen støttes ikke.",
+2 -1
View File
@@ -187,7 +187,8 @@
"invalidSceneUrl": "Kan scène niet importeren vanuit de opgegeven URL. Het is onjuist of bevat geen geldige Excalidraw JSON-gegevens.",
"resetLibrary": "Dit zal je bibliotheek wissen. Weet je het zeker?",
"removeItemsFromsLibrary": "Verwijder {{count}} item(s) uit bibliotheek?",
"invalidEncryptionKey": "Encryptiesleutel moet 22 tekens zijn. Live samenwerking is uitgeschakeld."
"invalidEncryptionKey": "Encryptiesleutel moet 22 tekens zijn. Live samenwerking is uitgeschakeld.",
"browserZoom": ""
},
"errors": {
"unsupportedFileType": "Niet-ondersteund bestandstype.",
+2 -1
View File
@@ -187,7 +187,8 @@
"invalidSceneUrl": "Kunne ikkje hente noko scene frå den URL-en. Ho er anten øydelagd eller inneheld ikkje gyldig Excalidraw JSON-data.",
"resetLibrary": "Dette vil fjerne alt innhald frå biblioteket. Er du sikker?",
"removeItemsFromsLibrary": "Slette {{count}} element frå biblioteket?",
"invalidEncryptionKey": "Krypteringsnøkkelen må ha 22 teikn. Sanntidssamarbeid er deaktivert."
"invalidEncryptionKey": "Krypteringsnøkkelen må ha 22 teikn. Sanntidssamarbeid er deaktivert.",
"browserZoom": ""
},
"errors": {
"unsupportedFileType": "Filtypen er ikkje støtta.",
+2 -1
View File
@@ -187,7 +187,8 @@
"invalidSceneUrl": "Importacion impossibla de la scèna a partir de lURL provesida. Es siá mal formatada o siá conten pas cap de donada JSON Excalidraw valida.",
"resetLibrary": "Aquò suprimirà vòstra bibliotèca. O volètz vertadièrament?",
"removeItemsFromsLibrary": "Suprimir {{count}} element(s) de la bibliotèca?",
"invalidEncryptionKey": "La clau de chiframent deu conténer 22 caractèrs. La collaboracion en dirèct es desactivada."
"invalidEncryptionKey": "La clau de chiframent deu conténer 22 caractèrs. La collaboracion en dirèct es desactivada.",
"browserZoom": ""
},
"errors": {
"unsupportedFileType": "Tipe de fichièr pas pres en carga.",
+2 -1
View File
@@ -187,7 +187,8 @@
"invalidSceneUrl": "ਦਿੱਤੀ ਗਈ URL 'ਚੋਂ ਦ੍ਰਿਸ਼ ਨੂੰ ਆਯਾਤ ਨਹੀਂ ਕਰ ਸਕੇ। ਇਹ ਜਾਂ ਤਾਂ ਖਰਾਬ ਹੈ, ਜਾਂ ਇਸ ਵਿੱਚ ਜਾਇਜ਼ Excalidraw JSON ਡਾਟਾ ਸ਼ਾਮਲ ਨਹੀਂ ਹੈ।",
"resetLibrary": "ਇਹ ਤੁਹਾਡੀ ਲਾਇਬ੍ਰੇਰੀ ਨੂੰ ਸਾਫ ਕਰ ਦੇਵੇਗਾ। ਕੀ ਤੁਸੀਂ ਪੱਕਾ ਇੰਝ ਕਰਨਾ ਚਾਹੁੰਦੇ ਹੋ?",
"removeItemsFromsLibrary": "ਲਾਇਬ੍ਰੇਰੀ ਵਿੱਚੋਂ {{count}} ਚੀਜ਼(-ਜ਼ਾਂ) ਮਿਟਾਉਣੀਆਂ ਹਨ?",
"invalidEncryptionKey": ""
"invalidEncryptionKey": "",
"browserZoom": ""
},
"errors": {
"unsupportedFileType": "",
+14 -14
View File
@@ -1,15 +1,15 @@
{
"ar-SA": 91,
"bg-BG": 58,
"bn-BD": 13,
"ca-ES": 99,
"cs-CZ": 27,
"bn-BD": 0,
"ca-ES": 97,
"cs-CZ": 24,
"da-DK": 34,
"de-DE": 100,
"el-GR": 99,
"el-GR": 82,
"en": 100,
"es-ES": 100,
"eu-ES": 100,
"es-ES": 99,
"eu-ES": 98,
"fa-IR": 98,
"fi-FI": 98,
"fr-FR": 100,
@@ -19,11 +19,11 @@
"hu-HU": 94,
"id-ID": 100,
"it-IT": 100,
"ja-JP": 100,
"ja-JP": 98,
"kab-KAB": 95,
"kk-KZ": 22,
"ko-KR": 99,
"lt-LT": 24,
"ko-KR": 98,
"lt-LT": 22,
"lv-LV": 100,
"mr-IN": 100,
"my-MM": 44,
@@ -31,10 +31,10 @@
"nl-NL": 86,
"nn-NO": 95,
"oc-FR": 98,
"pa-IN": 88,
"pa-IN": 87,
"pl-PL": 88,
"pt-BR": 100,
"pt-PT": 100,
"pt-BR": 95,
"pt-PT": 80,
"ro-RO": 100,
"ru-RU": 100,
"si-LK": 8,
@@ -43,8 +43,8 @@
"sv-SE": 100,
"ta-IN": 98,
"tr-TR": 99,
"uk-UA": 100,
"vi-VN": 16,
"uk-UA": 99,
"vi-VN": 13,
"zh-CN": 100,
"zh-HK": 27,
"zh-TW": 100
+2 -1
View File
@@ -187,7 +187,8 @@
"invalidSceneUrl": "Nie udało się zaimportować sceny z podanego adresu URL. Jest ona wadliwa lub nie zawiera poprawnych danych Excalidraw w formacie JSON.",
"resetLibrary": "To wyczyści twoją bibliotekę. Jesteś pewien?",
"removeItemsFromsLibrary": "Usunąć {{count}} element(ów) z biblioteki?",
"invalidEncryptionKey": "Klucz szyfrowania musi składać się z 22 znaków. Współpraca na żywo jest wyłączona."
"invalidEncryptionKey": "Klucz szyfrowania musi składać się z 22 znaków. Współpraca na żywo jest wyłączona.",
"browserZoom": ""
},
"errors": {
"unsupportedFileType": "Nieobsługiwany typ pliku.",
+18 -17
View File
@@ -9,7 +9,7 @@
"copy": "Copiar",
"copyAsPng": "Copiar para a área de transferência como PNG",
"copyAsSvg": "Copiar para a área de transferência como SVG",
"copyText": "Copiar para área de transferência como texto",
"copyText": "",
"bringForward": "Trazer para a frente",
"sendToBack": "Enviar para o fundo",
"bringToFront": "Trazer para o primeiro plano",
@@ -108,25 +108,25 @@
"decreaseFontSize": "Diminuir o tamanho da fonte",
"increaseFontSize": "Aumentar o tamanho da fonte",
"unbindText": "Desvincular texto",
"bindText": "Vincular texto ao contêiner",
"bindText": "",
"link": {
"edit": "Editar link",
"create": "Criar link",
"label": "Link"
},
"elementLock": {
"lock": "Bloquear",
"unlock": "Desbloquear",
"lockAll": "Bloquear tudo",
"unlockAll": "Desbloquear tudo"
"lock": "",
"unlock": "",
"lockAll": "",
"unlockAll": ""
},
"statusPublished": "Publicado",
"sidebarLock": "Manter barra lateral aberta"
"statusPublished": "",
"sidebarLock": ""
},
"library": {
"noItems": "Nenhum item adicionado ainda...",
"hint_emptyLibrary": "Selecione um item na tela para adicioná-lo aqui, ou instale uma biblioteca do repositório público, abaixo.",
"hint_emptyPrivateLibrary": "Selecione um item na tela para adicioná-lo aqui."
"noItems": "",
"hint_emptyLibrary": "",
"hint_emptyPrivateLibrary": ""
},
"buttons": {
"clearReset": "Limpar o canvas e redefinir a cor de fundo",
@@ -174,7 +174,7 @@
"couldNotLoadInvalidFile": "Não foi possível carregar o arquivo inválido",
"importBackendFailed": "A importação do servidor falhou.",
"cannotExportEmptyCanvas": "Não é possível exportar um canvas vazio.",
"couldNotCopyToClipboard": "Não foi possível copiar para a área de transferência.",
"couldNotCopyToClipboard": "",
"decryptFailed": "Não foi possível descriptografar os dados.",
"uploadedSecurly": "O upload foi protegido com criptografia de ponta a ponta, o que significa que o servidor do Excalidraw e terceiros não podem ler o conteúdo.",
"loadSceneOverridePrompt": "Carregar um desenho externo substituirá o seu conteúdo existente. Deseja continuar?",
@@ -187,7 +187,8 @@
"invalidSceneUrl": "Não foi possível importar a cena da URL fornecida. Ela está incompleta ou não contém dados JSON válidos do Excalidraw.",
"resetLibrary": "Isto limpará a sua biblioteca. Você tem certeza?",
"removeItemsFromsLibrary": "Excluir {{count}} item(ns) da biblioteca?",
"invalidEncryptionKey": "A chave de encriptação deve ter 22 caracteres. A colaboração ao vivo está desabilitada."
"invalidEncryptionKey": "A chave de encriptação deve ter 22 caracteres. A colaboração ao vivo está desabilitada.",
"browserZoom": ""
},
"errors": {
"unsupportedFileType": "Tipo de arquivo não suportado.",
@@ -196,7 +197,7 @@
"svgImageInsertError": "Não foi possível inserir a imagem SVG. A marcação SVG parece inválida.",
"invalidSVGString": "SVG Inválido.",
"cannotResolveCollabServer": "Não foi possível conectar-se ao servidor colaborativo. Por favor, recarregue a página e tente novamente.",
"importLibraryError": "Não foi possível carregar a biblioteca"
"importLibraryError": ""
},
"toolBar": {
"selection": "Seleção",
@@ -298,7 +299,7 @@
"howto": "Siga nossos guias",
"or": "ou",
"preventBinding": "Evitar fixação de seta",
"tools": "Ferramentas",
"tools": "",
"shortcuts": "Atalhos de teclado",
"textFinish": "Encerrar edição (editor de texto)",
"textNewLine": "Adicionar nova linha (editor de texto)",
@@ -306,7 +307,7 @@
"view": "Visualizar",
"zoomToFit": "Ampliar para encaixar todos os elementos",
"zoomToSelection": "Ampliar a seleção",
"toggleElementLock": "Bloquear/desbloquear seleção"
"toggleElementLock": ""
},
"clearCanvasDialog": {
"title": "Limpar a tela"
@@ -349,7 +350,7 @@
},
"noteItems": "Cada item da biblioteca deve ter seu próprio nome para que seja filtrável. Os seguintes itens da biblioteca serão incluídos:",
"atleastOneLibItem": "Por favor, selecione pelo menos um item da biblioteca para começar",
"republishWarning": "Nota: alguns dos itens selecionados estão marcados como já publicado/enviado. Você só deve reenviar itens ao atualizar uma biblioteca existente ou submissão."
"republishWarning": ""
},
"publishSuccessDialog": {
"title": "Biblioteca enviada",
+76 -75
View File
@@ -9,7 +9,7 @@
"copy": "Copiar",
"copyAsPng": "Copiar para a área de transferência como PNG",
"copyAsSvg": "Copiar para a área de transferência como SVG",
"copyText": "Copiar para Área de Transferência como texto",
"copyText": "",
"bringForward": "Trazer para o primeiro plano",
"sendToBack": "Enviar para o plano de fundo",
"bringToFront": "Trazer para o primeiro plano",
@@ -36,7 +36,7 @@
"arrowhead_arrow": "Seta",
"arrowhead_bar": "Barra",
"arrowhead_dot": "Ponto",
"arrowhead_triangle": "Triângulo",
"arrowhead_triangle": "",
"fontSize": "Tamanho da fonte",
"fontFamily": "Família da fontes",
"onlySelected": "Somente a seleção",
@@ -65,7 +65,7 @@
"cartoonist": "Caricaturista",
"fileTitle": "Nome do ficheiro",
"colorPicker": "Seletor de cores",
"canvasColors": "Usado na tela",
"canvasColors": "",
"canvasBackground": "Fundo da área de desenho",
"drawingCanvas": "Área de desenho",
"layers": "Camadas",
@@ -103,30 +103,30 @@
"showStroke": "Mostrar seletor de cores do traço",
"showBackground": "Mostrar seletor de cores do fundo",
"toggleTheme": "Alternar tema",
"personalLib": "Biblioteca pessoal",
"excalidrawLib": "Biblioteca do Excalidraw",
"decreaseFontSize": "Reduzir o tamanho do tipo de letra",
"increaseFontSize": "Aumentar o tamanho do tipo de letra",
"unbindText": "Desvincular texto",
"bindText": "Ligar texto ao recipiente",
"personalLib": "",
"excalidrawLib": "",
"decreaseFontSize": "",
"increaseFontSize": "",
"unbindText": "",
"bindText": "",
"link": {
"edit": "Editar ligação",
"create": "Criar ligação",
"label": "Ligação"
"edit": "",
"create": "",
"label": ""
},
"elementLock": {
"lock": "Bloquear",
"unlock": "Desbloquear",
"lockAll": "Bloquear todos",
"unlockAll": "Desbloquear todos"
"lock": "",
"unlock": "",
"lockAll": "",
"unlockAll": ""
},
"statusPublished": "Publicado",
"sidebarLock": "Manter a barra lateral aberta"
"statusPublished": "",
"sidebarLock": ""
},
"library": {
"noItems": "Ainda não foram adicionados nenhuns itens...",
"hint_emptyLibrary": "Seleccione um item na tela para adicioná-lo aqui, ou então instale uma biblioteca do repositório público abaixo.",
"hint_emptyPrivateLibrary": "Seleccione um item na tela para adicioná-lo aqui."
"noItems": "",
"hint_emptyLibrary": "",
"hint_emptyPrivateLibrary": ""
},
"buttons": {
"clearReset": "Limpar a área de desenho e redefinir a cor de fundo",
@@ -162,10 +162,10 @@
"exitZenMode": "Sair do modo zen",
"cancel": "Cancelar",
"clear": "Limpar",
"remove": "Remover",
"publishLibrary": "Publicar",
"submit": "Enviar",
"confirm": "Confirmar"
"remove": "",
"publishLibrary": "",
"submit": "",
"confirm": ""
},
"alerts": {
"clearReset": "Isto irá limpar toda a área de desenho. Tem a certeza?",
@@ -174,7 +174,7 @@
"couldNotLoadInvalidFile": "Não foi possível carregar o ficheiro inválido",
"importBackendFailed": "A importação do servidor falhou.",
"cannotExportEmptyCanvas": "Não é possível exportar uma área de desenho vazia.",
"couldNotCopyToClipboard": "Não foi possível copiar para a área de transferência.",
"couldNotCopyToClipboard": "",
"decryptFailed": "Não foi possível desencriptar os dados.",
"uploadedSecurly": "O upload foi protegido com criptografia de ponta a ponta, o que significa que o servidor do Excalidraw e terceiros não podem ler o conteúdo.",
"loadSceneOverridePrompt": "Se carregar um desenho externo substituirá o conteúdo existente. Quer continuar?",
@@ -186,17 +186,18 @@
"cannotRestoreFromImage": "Não foi possível restaurar a cena deste ficheiro de imagem",
"invalidSceneUrl": "Não foi possível importar a cena a partir do URL fornecido. Ou está mal formado ou não contém dados JSON do Excalidraw válidos.",
"resetLibrary": "Isto irá limpar a sua biblioteca. Tem a certeza?",
"removeItemsFromsLibrary": "Apagar {{count}} item(ns) da biblioteca?",
"invalidEncryptionKey": "Chave de encriptação deve ter 22 caracteres. A colaboração ao vivo está desativada."
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "Chave de encriptação deve ter 22 caracteres. A colaboração ao vivo está desativada.",
"browserZoom": ""
},
"errors": {
"unsupportedFileType": "Tipo de ficheiro não suportado.",
"imageInsertError": "Não foi possível inserir a imagem, tente novamente mais tarde...",
"fileTooBig": "O ficheiro é muito grande. O tamanho máximo permitido é {{maxSize}}.",
"svgImageInsertError": "Não foi possível inserir a imagem SVG. A marcação SVG parece inválida.",
"invalidSVGString": "SVG inválido.",
"cannotResolveCollabServer": "Não foi possível fazer a ligação ao servidor colaborativo. Por favor, volte a carregar a página e tente novamente.",
"importLibraryError": "Não foi possível carregar a biblioteca"
"invalidSVGString": "",
"cannotResolveCollabServer": "",
"importLibraryError": ""
},
"toolBar": {
"selection": "Seleção",
@@ -210,9 +211,9 @@
"text": "Texto",
"library": "Biblioteca",
"lock": "Manter a ferramenta selecionada ativa após desenhar",
"penMode": "Impedir o <em>pinch-zoom</em> e aceitar desenho livre apenas da caneta",
"link": "Acrescentar/ Adicionar ligação para uma forma seleccionada",
"eraser": "Borracha"
"penMode": "",
"link": "",
"eraser": ""
},
"headings": {
"canvasActions": "Ações da área de desenho",
@@ -220,7 +221,7 @@
"shapes": "Formas"
},
"hints": {
"canvasPanning": "Para mover a tela, carregue na roda do rato ou na barra de espaço enquanto arrasta",
"canvasPanning": "",
"linearElement": "Clique para iniciar vários pontos, arraste para uma única linha",
"freeDraw": "Clique e arraste, large quando terminar",
"text": "Dica: também pode adicionar texto clicando duas vezes em qualquer lugar com a ferramenta de seleção",
@@ -232,13 +233,13 @@
"resizeImage": "Pode redimensionar livremente mantendo pressionada a tecla SHIFT,\nmantenha pressionada a tecla ALT para redimensionar do centro",
"rotate": "Pode restringir os ângulos mantendo a tecla SHIFT premida enquanto roda",
"lineEditor_info": "Clique duas vezes ou pressione a tecla Enter para editar os pontos",
"lineEditor_pointSelected": "Carregue na tecla Delete para remover o(s) ponto(s), CtrlOuCmd+D para duplicar, ou arraste para mover",
"lineEditor_nothingSelected": "Seleccione um ponto para editar (carregue em SHIFT para seleccionar vários),\nou carregue em Alt e clique para acrescentar novos pontos",
"lineEditor_pointSelected": "",
"lineEditor_nothingSelected": "",
"placeImage": "Clique para colocar a imagem ou clique e arraste para definir o seu tamanho manualmente",
"publishLibrary": "Publique a sua própria biblioteca",
"bindTextToElement": "Carregue Enter para acrescentar texto",
"deepBoxSelect": "Mantenha a tecla CtrlOrCmd carregada para selecção profunda, impedindo o arrastamento",
"eraserRevert": "Carregue também em Alt para reverter os elementos marcados para serem apagados"
"publishLibrary": "",
"bindTextToElement": "",
"deepBoxSelect": "",
"eraserRevert": ""
},
"canvasError": {
"cannotShowPreview": "Não é possível mostrar uma pré-visualização",
@@ -285,8 +286,8 @@
"helpDialog": {
"blog": "Leia o nosso blogue",
"click": "clicar",
"deepSelect": "Selecção profunda",
"deepBoxSelect": "Selecção profunda dentro da caixa, impedindo que seja arrastada",
"deepSelect": "",
"deepBoxSelect": "",
"curvedArrow": "Seta curva",
"curvedLine": "Linha curva",
"documentation": "Documentação",
@@ -298,7 +299,7 @@
"howto": "Siga os nossos guias",
"or": "ou",
"preventBinding": "Prevenir fixação de seta",
"tools": "Ferramentas",
"tools": "",
"shortcuts": "Atalhos de teclado",
"textFinish": "Finalizar edição (editor texto)",
"textNewLine": "Adicionar nova linha (editor de texto)",
@@ -306,59 +307,59 @@
"view": "Visualizar",
"zoomToFit": "Ajustar para todos os elementos caberem",
"zoomToSelection": "Ampliar a seleção",
"toggleElementLock": "Trancar/destrancar selecção"
"toggleElementLock": ""
},
"clearCanvasDialog": {
"title": "Apagar tela"
"title": ""
},
"publishDialog": {
"title": "Publicar biblioteca",
"itemName": "Nome do item",
"authorName": "Nome do autor",
"githubUsername": "Nome de utilizador do GitHub",
"title": "",
"itemName": "",
"authorName": "",
"githubUsername": "",
"twitterUsername": "Nome de utilizador no Twitter",
"libraryName": "Nome da biblioteca",
"libraryDesc": "Descrição da biblioteca",
"website": "Página web",
"website": "",
"placeholder": {
"authorName": "Introduza o seu nome ou nome de utilizador",
"libraryName": "Nome da sua biblioteca",
"libraryDesc": "Descrição da sua biblioteca para ajudar as pessoas a entender a utilização dela",
"githubHandle": "Identificador do GitHub (opcional), para que possa editar a biblioteca depois desta ser enviada para revisão",
"twitterHandle": "Nome do Twitter (opcional), para que saibamos quem merece os créditos na promoção via Twitter",
"website": "Ligação para a sua página pessoal ou qualquer outra (opcional)"
"libraryName": "",
"libraryDesc": "",
"githubHandle": "",
"twitterHandle": "",
"website": ""
},
"errors": {
"required": "Obrigatório",
"website": "Introduza um URL válido"
"required": "",
"website": ""
},
"noteDescription": {
"pre": "Envie a sua biblioteca para ser incluída no ",
"link": "repositório de bibliotecas públicas",
"post": "para outras pessoas a poderem usar nos seus próprios desenhos."
"pre": "",
"link": "",
"post": ""
},
"noteGuidelines": {
"pre": "A biblioteca precisa ser aprovada manualmente primeiro. Por favor, leia ",
"pre": "",
"link": "orientações",
"post": " antes de enviar. Vai precisar de uma conta no GitHub para comunicar e fazer alterações se solicitado, mas não é estritamente necessária."
"post": ""
},
"noteLicense": {
"pre": "Ao enviar, concorda que a biblioteca será publicada sob a ",
"link": "Licença MIT, ",
"post": "o que significa, de forma resumida, que qualquer pessoa pode utilizá-la sem restrições."
"pre": "",
"link": "",
"post": ""
},
"noteItems": "Cada item da biblioteca deve ter o seu próprio nome para que este seja pesquisável com filtros. Os seguintes itens da biblioteca serão incluídos:",
"atleastOneLibItem": "Por favor, seleccione pelo menos um item da biblioteca para começar",
"republishWarning": "Nota: alguns dos itens seleccionados estão marcados como já publicados/enviados. Só deve reenviar itens ao actualizar uma biblioteca existente ou submissão."
"noteItems": "",
"atleastOneLibItem": "",
"republishWarning": ""
},
"publishSuccessDialog": {
"title": "Biblioteca enviada",
"content": "Obrigado {{authorName}}. A sua biblioteca foi enviada para análise. Pode acompanhar o status",
"link": "aqui"
"title": "",
"content": "",
"link": ""
},
"confirmDialog": {
"resetLibrary": "Repor a biblioteca",
"removeItemsFromLib": "Remover os itens seleccionados da biblioteca"
"resetLibrary": "",
"removeItemsFromLib": ""
},
"encrypted": {
"tooltip": "Os seus desenhos são encriptados de ponta-a-ponta, por isso os servidores do Excalidraw nunca os verão.",
@@ -380,7 +381,7 @@
"width": "Largura"
},
"toast": {
"addedToLibrary": "Acrescentado à biblioteca",
"addedToLibrary": "",
"copyStyles": "Estilos copiados.",
"copyToClipboard": "Copiado para a área de transferência.",
"copyToClipboardAsPng": "{{exportSelection}} copiado para a área de transferência como PNG\n({{exportColorScheme}})",
+2 -1
View File
@@ -187,7 +187,8 @@
"invalidSceneUrl": "Scena nu a putut fi importată din URL-ul furnizat. Este fie incorect formată, fie nu conține date JSON Excalidraw valide.",
"resetLibrary": "Această opțiune va elimina conținutul din bibliotecă. Confirmi?",
"removeItemsFromsLibrary": "Ștergi {{count}} element(e) din bibliotecă?",
"invalidEncryptionKey": "Cheia de criptare trebuie să aibă 22 de caractere. Colaborarea în direct este dezactivată."
"invalidEncryptionKey": "Cheia de criptare trebuie să aibă 22 de caractere. Colaborarea în direct este dezactivată.",
"browserZoom": "Nivelul de transfocare al navigatorului tău nu este setat la 100% ceea ce poate face ca panoul să fie afișat incorect"
},
"errors": {
"unsupportedFileType": "Tip de fișier neacceptat.",
+2 -1
View File
@@ -187,7 +187,8 @@
"invalidSceneUrl": "Невозможно импортировать сцену с предоставленного URL. Неверный формат, или не содержит верных Excalidraw JSON данных.",
"resetLibrary": "Это очистит вашу библиотеку. Вы уверены?",
"removeItemsFromsLibrary": "Удалить {{count}} объект(ов) из библиотеки?",
"invalidEncryptionKey": "Ключ шифрования должен состоять из 22 символов. Одновременное редактирование отключено."
"invalidEncryptionKey": "Ключ шифрования должен состоять из 22 символов. Одновременное редактирование отключено.",
"browserZoom": "Масштаб вашего браузера не установлен на 100%, из-за этого доска может отображаться неправильно"
},
"errors": {
"unsupportedFileType": "Неподдерживаемый тип файла.",
+2 -1
View File
@@ -187,7 +187,8 @@
"invalidSceneUrl": "",
"resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": ""
"invalidEncryptionKey": "",
"browserZoom": ""
},
"errors": {
"unsupportedFileType": "",
+2 -1
View File
@@ -187,7 +187,8 @@
"invalidSceneUrl": "Nepodarilo sa načítať scénu z poskytnutej URL. Je nevalidná alebo neobsahuje žiadne validné Excalidraw JSON dáta.",
"resetLibrary": "Týmto vyprázdnite vašu knižnicu. Ste si istý?",
"removeItemsFromsLibrary": "Odstrániť {{count}} položiek z knižnice?",
"invalidEncryptionKey": "Šifrovací kľúč musí mať 22 znakov. Živá spolupráca je vypnutá."
"invalidEncryptionKey": "Šifrovací kľúč musí mať 22 znakov. Živá spolupráca je vypnutá.",
"browserZoom": "Priblíženie vášho prehliadača nie je nastavené na 100%, čo môže spôsobiť nesprávne zobrazenie plátna"
},
"errors": {
"unsupportedFileType": "Nepodporovaný typ súboru.",
+2 -1
View File
@@ -187,7 +187,8 @@
"invalidSceneUrl": "S priloženega URL-ja ni bilo mogoče uvoziti scene. Je napačno oblikovana ali pa ne vsebuje veljavnih podatkov Excalidraw JSON.",
"resetLibrary": "To bo počistilo vašo knjižnico. Ali ste prepričani?",
"removeItemsFromsLibrary": "Izbriši elemente ({{count}}) iz knjižnice?",
"invalidEncryptionKey": "Ključ za šifriranje mora vsebovati 22 znakov. Sodelovanje v živo je onemogočeno."
"invalidEncryptionKey": "Ključ za šifriranje mora vsebovati 22 znakov. Sodelovanje v živo je onemogočeno.",
"browserZoom": "Stopnja povečave vašega brskalnika ni nastavljena na 100 %, kar lahko povzroči nepravilen prikaz table"
},
"errors": {
"unsupportedFileType": "Nepodprt tip datoteke.",
+2 -1
View File
@@ -187,7 +187,8 @@
"invalidSceneUrl": "Det gick inte att importera skiss från den angivna webbadressen. Antingen har den fel format, eller så innehåller den ingen giltig Excalidraw JSON data.",
"resetLibrary": "Detta kommer att rensa ditt bibliotek. Är du säker?",
"removeItemsFromsLibrary": "Ta bort {{count}} objekt från biblioteket?",
"invalidEncryptionKey": "Krypteringsnyckeln måste vara 22 tecken. Livesamarbetet är inaktiverat."
"invalidEncryptionKey": "Krypteringsnyckeln måste vara 22 tecken. Livesamarbetet är inaktiverat.",
"browserZoom": "Din webbläsares zoomnivå är inte satt till 100% vilket kan orsaka att tavlan visas felaktigt"
},
"errors": {
"unsupportedFileType": "Filtypen stöds inte.",
+2 -1
View File
@@ -187,7 +187,8 @@
"invalidSceneUrl": "வழங்கப்பட்ட உரலியிலிருந்து காட்சியை இறக்கவியலா. இது தவறான வடிவத்தில் உள்ளது, அ செல்லத்தக்க எக்ஸ்கேலிட்ரா JSON தரவைக் கொண்டில்லை.",
"resetLibrary": "இது உங்கள் நுலகத்தைத் துடைக்கும். நீங்கள் உறுதியா?",
"removeItemsFromsLibrary": "{{count}} உருப்படி(கள்)-ஐ உம் நூலகத்திலிருந்து அழிக்கவா?",
"invalidEncryptionKey": "மறையாக்க விசை 22 வரியுருக்கள் கொண்டிருக்கவேண்டும். நேரடி கூட்டுப்பணி முடக்கப்பட்டது."
"invalidEncryptionKey": "மறையாக்க விசை 22 வரியுருக்கள் கொண்டிருக்கவேண்டும். நேரடி கூட்டுப்பணி முடக்கப்பட்டது.",
"browserZoom": ""
},
"errors": {
"unsupportedFileType": "ஆதரிக்கப்படா கோப்பு வகை.",
+2 -1
View File
@@ -187,7 +187,8 @@
"invalidSceneUrl": "Verilen bağlantıdan çalışma alanı yüklenemedi. Dosya bozuk olabilir veya geçerli bir Excalidraw JSON verisi bulundurmuyor olabilir.",
"resetLibrary": "Bu işlem kütüphanenizi sıfırlayacak. Emin misiniz?",
"removeItemsFromsLibrary": "{{count}} öğe(ler) kitaplıktan kaldırılsın mı?",
"invalidEncryptionKey": "Şifreleme anahtarı 22 karakter olmalı. Canlı işbirliği devre dışı bırakıldı."
"invalidEncryptionKey": "Şifreleme anahtarı 22 karakter olmalı. Canlı işbirliği devre dışı bırakıldı.",
"browserZoom": "Tarayıcınızın yaklaştırma seviyesi %100 değil. Bu durum, tablonun yanlış görünmesine sebep olabilir"
},
"errors": {
"unsupportedFileType": "Desteklenmeyen dosya türü.",
+2 -1
View File
@@ -187,7 +187,8 @@
"invalidSceneUrl": "Не вдалося імпортувати сцену з наданого URL. Він або недоформований, або не містить дійсних даних Excalidraw JSON.",
"resetLibrary": "Це призведе до очищення бібліотеки. Ви впевнені?",
"removeItemsFromsLibrary": "Видалити {{count}} елемент(ів) з бібліотеки?",
"invalidEncryptionKey": "Ключ шифрування повинен бути довжиною до 22 символів. Спільну роботу вимкнено."
"invalidEncryptionKey": "Ключ шифрування повинен бути довжиною до 22 символів. Спільну роботу вимкнено.",
"browserZoom": ""
},
"errors": {
"unsupportedFileType": "Непідтримуваний тип файлу.",
+13 -12
View File
@@ -1,10 +1,10 @@
{
"labels": {
"paste": "Dán",
"pasteCharts": "Dán biểu đồ",
"pasteCharts": "",
"selectAll": "Chọn tất cả",
"multiSelect": "Thêm mới vào Select",
"moveCanvas": "Di chuyển Canvas",
"multiSelect": "",
"moveCanvas": "",
"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": "Làm với Excalidraw\"",
"addWatermark": "",
"handDrawn": "",
"normal": "Bình thường",
"code": "Mã",
"small": "Nhỏ",
"medium": "Vừa",
"large": "Lớn",
"veryLarge": "Rất lớn",
"solid": "Đặc",
"veryLarge": "",
"solid": "",
"hachure": "",
"crossHatch": "",
"thin": "Mỏng",
"bold": "In đậm",
"left": "Trái",
"center": "Giữa",
"right": "Phải",
"thin": "",
"bold": "",
"left": "",
"center": "",
"right": "",
"extraBold": "",
"architect": "",
"artist": "",
@@ -187,7 +187,8 @@
"invalidSceneUrl": "",
"resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": ""
"invalidEncryptionKey": "",
"browserZoom": ""
},
"errors": {
"unsupportedFileType": "",
+2 -1
View File
@@ -187,7 +187,8 @@
"invalidSceneUrl": "无法从提供的 URL 导入场景。它或者格式不正确,或者不包含有效的 Excalidraw JSON 数据。",
"resetLibrary": "这将会清除你的素材库。你确定要这么做吗?",
"removeItemsFromsLibrary": "确定要从素材库中删除 {{count}} 个项目吗?",
"invalidEncryptionKey": "密钥必须包含22个字符。实时协作已被禁用。"
"invalidEncryptionKey": "密钥必须包含22个字符。实时协作已被禁用。",
"browserZoom": "您的浏览器缩放程度没有设置为 100%,这可能会导致画板显示不正确"
},
"errors": {
"unsupportedFileType": "不支持的文件格式。",
+2 -1
View File
@@ -187,7 +187,8 @@
"invalidSceneUrl": "",
"resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": ""
"invalidEncryptionKey": "",
"browserZoom": ""
},
"errors": {
"unsupportedFileType": "",
+2 -1
View File
@@ -187,7 +187,8 @@
"invalidSceneUrl": "無法由提供的 URL 匯入場景。可能是發生異常,或未包含有效的 Excalidraw JSON 資料。",
"resetLibrary": "這會清除您的資料庫,是否確定?",
"removeItemsFromsLibrary": "從資料庫刪除 {{count}} 項?",
"invalidEncryptionKey": "加密鍵必須為22字元。即時協作已停用。"
"invalidEncryptionKey": "加密鍵必須為22字元。即時協作已停用。",
"browserZoom": "因瀏覽器之縮放值目前並非 100%,可能造成顯示錯誤"
},
"errors": {
"unsupportedFileType": "不支援的檔案類型。",
+1 -165
View File
@@ -1,8 +1,6 @@
import { NormalizedZoomValue, Point, Zoom } from "./types";
import { LINE_CONFIRM_THRESHOLD } from "./constants";
import { ExcalidrawLinearElement, NonDeleted } from "./element/types";
import { getShapeForElement } from "./renderer/renderElement";
import { getCurvePathOps } from "./element/bounds";
import { ExcalidrawLinearElement } from "./element/types";
export const rotate = (
x1: number,
@@ -265,165 +263,3 @@ 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];
};
+850 -9
View File
@@ -15,32 +15,873 @@ Please add the latest change on the top under the correct section.
### Excalidraw API
#### 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
- `setToastMessage` API is now renamed to `setToast` API and the function signature is also updated [#5427](https://github.com/excalidraw/excalidraw/pull/5427). You can also pass `duration` and `closable` attributes along with `message`.
## 0.12.0 (2022-07-07)
Check out the [release notes](https://github.com/excalidraw/excalidraw/releases/tag/v0.12.0) )
### 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
**_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)
Check out the [release notes](https://github.com/excalidraw/excalidraw/releases/tag/v0.11.0)
## 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)
---
## 0.10.0 (2021-10-13)
Check out the [release notes](https://github.com/excalidraw/excalidraw/releases/tag/v0.10.0)
## 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)
---
## 0.9.0 (2021-07-10)
Check out the [release notes](https://github.com/excalidraw/excalidraw/releases/tag/v0.9.0)
## 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)
---
## 0.8.0 (2021-05-15)
+10 -22
View File
@@ -394,7 +394,7 @@ For a complete list of variables, check [theme.scss](https://github.com/excalidr
| [`zenModeEnabled`](#zenModeEnabled) | boolean | | This implies if the zen mode is enabled |
| [`gridModeEnabled`](#gridModeEnabled) | boolean | | This implies if the grid mode is enabled |
| [`libraryReturnUrl`](#libraryReturnUrl) | string | | What URL should [libraries.excalidraw.com](https://libraries.excalidraw.com) be installed to |
| [`theme`](#theme) | [THEME.LIGHT](#THEME-1) &#124; [THEME.DARK](#THEME-1) | [THEME.LIGHT](#THEME-1) | The theme of the Excalidraw component |
| [`theme`](#theme) | [THEME.LIGHT](#THEME-1) &#124; [THEME.LIGHT](#THEME-1) | [THEME.LIGHT](#THEME-1) | The theme of the Excalidraw component |
| [`name`](#name) | string | | Name of the drawing |
| [`UIOptions`](#UIOptions) | <pre>{ canvasActions: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L208"> CanvasActions<a/> }</pre> | [DEFAULT UI OPTIONS](https://github.com/excalidraw/excalidraw/blob/master/src/constants.ts#L129) | To customise UI options. Currently we support customising [`canvas actions`](#canvasActions) |
| [`onPaste`](#onPaste) | <pre>(data: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/clipboard.ts#L21">ClipboardData</a>, event: ClipboardEvent &#124; null) => boolean</pre> | | Callback to be triggered if passed when the something is pasted in to the scene |
@@ -470,20 +470,12 @@ This helps to load Excalidraw with `initialData`. It must be an object or a [pro
You might want to use this when you want to load excalidraw with some initial elements and app state.
#### Storing custom data on Excalidraw elements
Beyond attributes that Excalidraw elements already support, you can store custom data on each element in a `customData` object. The type of the attribute is [`Record<string, any>`](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L59) and is optional.
You can use this to add any extra information you need to keep track of.
You can add `customData` to elements when passing them as `initialData`, or using [`updateScene`](#updateScene)/[`updateLibrary`](#updateLibrary) afterwards.
#### `ref`
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 +488,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> &#124; <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) | <code>(url: string, token?: string) => void</code> | Imports library from given URL |
| [setToast](#setToast) | <code>({ message: string, closable?:boolean, duration?:number } &#124; null) => void</code> | This API can be used to show the toast with custom message. |
| [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. |
| [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"]&#124; "eraser" } &#124; { 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"] &#124; "eraser" } &#124; { 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,8 +921,7 @@ This function normalizes library items elements, adding missing values when need
elements,
appState
getDimensions,
files,
exportPadding?: number;
files
}: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/packages/utils.ts#L12">ExportOpts</a>
</pre>
@@ -941,7 +932,6 @@ 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**
@@ -959,8 +949,7 @@ 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,
exportPadding?: number;
quality?: number;
})
</pre>
@@ -969,7 +958,6 @@ 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**
@@ -1105,10 +1093,10 @@ import { loadLibraryFromBlob } from "@excalidraw/excalidraw";
**_Signature_**
<pre>
loadLibraryFromBlob(blob: <a href="https://developer.mozilla.org/en-US/docs/Web/API/Blob">Blob</a>, defaultStatus: "published" | "unpublished")
loadLibraryFromBlob(blob: <a href="https://developer.mozilla.org/en-US/docs/Web/API/Blob">Blob</a>)
</pre>
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`.
This function loads the library from the blob.
#### `loadFromBlob`
-2
View File
@@ -1,7 +1,5 @@
import "./publicPath";
import polyfill from "../../polyfill";
import "../../../public/fonts.css";
polyfill();
export * from "./index";
+1 -1
View File
@@ -282,7 +282,7 @@ export default function App() {
files: excalidrawAPI?.getFiles(),
type,
});
window.alert(`Copied to clipboard as ${type} successfully`);
window.alert(`Copied to clipboard as ${type} sucessfully`);
};
const [pointerData, setPointerData] = useState<{
+7 -8
View File
@@ -1,13 +1,12 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
const rootElement = document.getElementById("root")!;
const root = createRoot(rootElement);
root.render(
<StrictMode>
const rootElement = document.getElementById("root");
ReactDOM.render(
<React.StrictMode>
<App />
</StrictMode>,
</React.StrictMode>,
rootElement,
);
@@ -17,8 +17,8 @@
<body>
<noscript> You need to enable JavaScript to run this app. </noscript>
<div id="root"></div>
<script src="https://unpkg.com/react@18.2.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18.2.0/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/react@17.0.2/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@17.0.2/umd/react-dom.development.js"></script>
<!-- This is so that we use the bundled excalidraw.development.js file instead
of the actual source code -->

Some files were not shown because too many files have changed in this diff Show More