Compare commits

..

42 Commits

Author SHA1 Message Date
Aakansha Doshi 98c16659c9 Add safe check so collab stats dont show up for host apps 2021-02-08 21:42:05 +05:30
Aakansha Doshi 5644063fc7 remove ts ignore 2021-02-07 21:27:18 +05:30
Aakansha Doshi 85b8050cc5 move getAverage to util 2021-02-07 20:59:09 +05:30
Aakansha Doshi 60d3bf1718 fix 2021-02-07 20:44:42 +05:30
Aakansha Doshi b8e1b1f3ad rename 2021-02-07 20:43:46 +05:30
Lipis 57432b9779 Update src/utils.ts 2021-02-07 17:11:30 +02:00
Lipis 156073a407 Update src/networkStats.ts 2021-02-07 17:06:57 +02:00
Aakansha Doshi f676a20332 rename 2021-02-07 20:34:54 +05:30
Aakansha Doshi 63af29d345 cleanup 2021-02-07 20:03:37 +05:30
Aakansha Doshi 69a1b74e05 simulate ping every 5 sec 2021-02-07 19:34:04 +05:30
Panayiotis Lipiridis 163dbd47d4 Merge branch 'aakansha-net-stats' of github.com:excalidraw/excalidraw into aakansha-net-stats
* 'aakansha-net-stats' of github.com:excalidraw/excalidraw:
  Update src/utils.ts
  Update Stats.scss
2021-02-07 15:52:01 +02:00
Panayiotis Lipiridis 08889adfa5 4 2021-02-07 15:51:56 +02:00
Panayiotis Lipiridis aaf4943fa3 Average speed 2021-02-07 15:51:12 +02:00
Lipis 4fd18d1b3e Update src/utils.ts
Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
2021-02-07 14:55:50 +02:00
Lipis f8cf19cae9 Update Stats.scss 2021-02-07 14:47:39 +02:00
Panayiotis Lipiridis 6540c5460e Bits 2021-02-07 14:38:15 +02:00
Panayiotis Lipiridis dc95cf3447 bytes 2021-02-07 14:34:33 +02:00
Panayiotis Lipiridis 1ca985aa4a Bytes 2021-02-07 14:33:26 +02:00
Aakansha Doshi bc9c8f7ee2 clear timer before starting new one 2021-02-07 17:36:27 +05:30
Panayiotis Lipiridis 3eacd6f07b Speed 2021-02-07 13:54:57 +02:00
Panayiotis Lipiridis afa929b2a2 Bigger image 2021-02-07 13:17:17 +02:00
Panayiotis Lipiridis 370c9a643f Source 2021-02-07 13:15:10 +02:00
Panayiotis Lipiridis 22c57c8f4a const 2021-02-07 12:40:27 +02:00
Panayiotis Lipiridis eef9662195 Fix tests 2021-02-07 12:25:37 +02:00
Panayiotis Lipiridis 69a7b1f2b5 2021-02-07 12:16:17 +02:00
Panayiotis Lipiridis 3b11b1d9d3 minor fixes 2021-02-07 12:11:59 +02:00
Aakansha Doshi b778035854 update interval to be 5sec 2021-02-07 15:10:27 +05:30
Aakansha Doshi 92c2a42edf use image from our server, this fixes the time mismatch issue i was facing earlier due to diff url/image 2021-02-07 15:04:24 +05:30
Aakansha Doshi 7cadc3de52 update image size to be ~3kb and check speed every 15seconds 2021-02-07 02:56:52 +05:30
Aakansha Doshi 336b7222de Merge remote-tracking branch 'origin/master' into aakansha-net-stats 2021-02-06 22:20:58 +05:30
Aakansha Doshi 4d0ebf5ac5 move collab stats before version 2021-02-06 22:19:04 +05:30
Lipis 86222662f2 docs: Add open collective in readme (#2948)
Co-authored-by: David Luzar <luzar.david@gmail.com>
2021-02-06 18:12:35 +02:00
Aakansha Doshi 066560311b feat: add props zenModeEnabled and gridModeEnabled so host can control completely (#2901)
* feat: add props zenModeEnabled and gridModeEnabled so host can control completely

* dnt show exit zenmode button when prop present

* fix

* update when props change

* Add tests

* Add tests

* update changelog and readme

* update

* Update src/tests/excalidrawPackage.test.tsx

* Update src/packages/excalidraw/README.md

Co-authored-by: Lipis <lipiridis@gmail.com>

* Update src/packages/excalidraw/README.md

Co-authored-by: David Luzar <luzar.david@gmail.com>

* Apply suggestions from code review

Co-authored-by: David Luzar <luzar.david@gmail.com>

* fix specs

Co-authored-by: Lipis <lipiridis@gmail.com>
Co-authored-by: David Luzar <luzar.david@gmail.com>
2021-02-06 21:22:28 +05:30
Aakansha Doshi 949e9ae03a fix specs 2021-02-06 20:20:48 +05:30
Aakansha Doshi b5151bda5a fix 2021-02-06 20:08:30 +05:30
Aakansha Doshi 8157c84d11 feat: show network stats for collaboration 2021-02-06 19:50:35 +05:30
Arun d6ca981f7a fix: Rename 'Grid mode' to 'Show grid' (#2944) 2021-02-05 23:50:53 +02:00
Excalidraw Bot f7f98d9dda chore: Update translations from Crowdin (#2930) 2021-02-05 20:07:33 +02:00
Thomas Steiner 1a67642fd1 chore: [Idle detection] Deal with users on systems that don't handle emoji (#2941)
* Deal with users on systems that don't handle emoji

* Leave no trailing space

* Move function to utils, and actually call it 🤣
Chapeau, TypeScript!

* Use grinning face instead of koala

* Tweak globalAlpha
2021-02-05 18:34:35 +01:00
David Luzar 6aa22bada8 fix: mobile toolbar tooltip regression (#2939) 2021-02-05 16:09:40 +01:00
David Luzar 00209ef9c3 fix: hide collaborator list on mobile if empty (#2938)
Co-authored-by: Lipis <lipiridis@gmail.com>
2021-02-05 15:45:44 +01:00
David Luzar b79ef0d428 fix: don't prompt on empty scenes (#2937) 2021-02-05 12:04:33 +01:00
31 changed files with 643 additions and 249 deletions
+9
View File
@@ -11,6 +11,7 @@
<img src="https://badges.crowdin.net/excalidraw/localized.svg">
</a>
</p>
<p>Ask questions or hang out on our <a target="_blank" href="https://discord.gg/UexuTaE">discord.gg/UexuTaE</a>.</p>
</div>
## Try it now
@@ -19,6 +20,14 @@ Go to [excalidraw.com](https://excalidraw.com) to start sketching.
Read the latest news and updates on our [blog](https://blog.excalidraw.com). A good start is to see all the updates of [One Year of Excalidraw](https://blog.excalidraw.com/one-year-of-excalidraw/).
## We accept donations
If you like the project, you can become a sponsor at [Open Collective](https://opencollective.com/excalidraw).
<a href="https://opencollective.com/excalidraw#category-CONTRIBUTE" target="_blank"><img src="https://opencollective.com/excalidraw/tiers/sponsors.svg?avatarHeight=64"/></a>
<a href="https://opencollective.com/excalidraw#category-CONTRIBUTE" target="_blank"><img src="https://opencollective.com/excalidraw/tiers/backers.svg?avatarHeight=32"/></a>
## Documentation
### Shortcuts
+1 -1
View File
@@ -17,6 +17,6 @@ export const actionToggleGridMode = register({
};
},
checked: (appState: AppState) => appState.gridSize !== null,
contextItemLabel: "labels.gridMode",
contextItemLabel: "labels.showGrid",
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.code === CODES.QUOTE,
});
+4
View File
@@ -73,6 +73,8 @@ export const getDefaultAppState = (): Omit<
zenModeEnabled: false,
zoom: { value: 1 as NormalizedZoomValue, translation: { x: 0, y: 0 } },
viewModeEnabled: false,
networkSpeed: 0,
networkPing: 0,
};
};
@@ -153,6 +155,8 @@ const APP_STATE_STORAGE_CONF = (<
zenModeEnabled: { browser: true, export: false },
zoom: { browser: true, export: false },
viewModeEnabled: { browser: false, export: false },
networkSpeed: { browser: false, export: false },
networkPing: { browser: false, export: false },
});
const _clearAppStateForStorage = <ExportType extends "export" | "browser">(
+107 -36
View File
@@ -48,8 +48,10 @@ import {
ELEMENT_TRANSLATE_AMOUNT,
ENV,
EVENT,
GRID_SIZE,
LINE_CONFIRM_THRESHOLD,
MIME_TYPES,
NETWORK_TIMEOUT_MS,
POINTER_BUTTON,
TAP_TWICE_TIMEOUT,
TEXT_TO_CENTER_SNAP_THRESHOLD,
@@ -182,6 +184,7 @@ import LayerUI from "./LayerUI";
import { Stats } from "./Stats";
import { Toast } from "./Toast";
import { actionToggleViewMode } from "../actions/actionToggleViewMode";
import { getNetworkSpeed, getNetworkPing } from "../networkStats";
const { history } = createHistory();
@@ -204,6 +207,9 @@ const gesture: Gesture = {
initialScale: null,
};
const shouldEnableNetworkStats = !!(
typeof process !== "undefined" && process.env?.REACT_APP_SOCKET_SERVER_URL
);
export type PointerDownState = Readonly<{
// The first position at which pointerDown happened
origin: Readonly<{ x: number; y: number }>;
@@ -288,6 +294,8 @@ class App extends React.Component<ExcalidrawProps, AppState> {
height: window.innerHeight,
};
private scene: Scene;
private networkSpeedIntervalId?: any;
private networkPingIntervalId?: any;
constructor(props: ExcalidrawProps) {
super(props);
const defaultAppState = getDefaultAppState();
@@ -299,6 +307,8 @@ class App extends React.Component<ExcalidrawProps, AppState> {
offsetTop,
excalidrawRef,
viewModeEnabled = false,
zenModeEnabled = false,
gridModeEnabled = false,
} = props;
this.state = {
...defaultAppState,
@@ -307,6 +317,8 @@ class App extends React.Component<ExcalidrawProps, AppState> {
height,
...this.getCanvasOffsets({ offsetLeft, offsetTop }),
viewModeEnabled,
zenModeEnabled,
gridSize: gridModeEnabled ? GRID_SIZE : null,
};
if (excalidrawRef) {
const readyPromise =
@@ -453,6 +465,9 @@ class App extends React.Component<ExcalidrawProps, AppState> {
onExportToBackend={onExportToBackend}
renderCustomFooter={renderFooter}
viewModeEnabled={viewModeEnabled}
showExitZenModeBtn={
typeof this.props?.zenModeEnabled === "undefined" && zenModeEnabled
}
/>
<div className="excalidraw-textEditorContainer" />
{this.state.showStats && (
@@ -461,6 +476,8 @@ class App extends React.Component<ExcalidrawProps, AppState> {
setAppState={this.setAppState}
elements={this.scene.getElements()}
onClose={this.toggleStats}
isCollaborating={this.props.isCollaborating}
shouldEnableNetworkStats={shouldEnableNetworkStats}
/>
)}
{this.state.toastMessage !== null && (
@@ -511,11 +528,21 @@ class App extends React.Component<ExcalidrawProps, AppState> {
}
let viewModeEnabled = actionResult?.appState?.viewModeEnabled || false;
let zenModeEnabled = actionResult?.appState?.zenModeEnabled || false;
let gridSize = actionResult?.appState?.gridSize || null;
if (typeof this.props.viewModeEnabled !== "undefined") {
viewModeEnabled = this.props.viewModeEnabled;
}
if (typeof this.props.zenModeEnabled !== "undefined") {
zenModeEnabled = this.props.zenModeEnabled;
}
if (typeof this.props.gridModeEnabled !== "undefined") {
gridSize = this.props.gridModeEnabled ? GRID_SIZE : null;
}
this.setState(
(state) => ({
...actionResult.appState,
@@ -526,6 +553,8 @@ class App extends React.Component<ExcalidrawProps, AppState> {
offsetTop: state.offsetTop,
offsetLeft: state.offsetLeft,
viewModeEnabled,
zenModeEnabled,
gridSize,
}),
() => {
if (actionResult.syncHistory) {
@@ -728,6 +757,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
this.removeEventListeners();
this.scene.destroy();
clearTimeout(touchTimeout);
clearTimeout(this.networkSpeedIntervalId);
touchTimeout = 0;
}
@@ -845,6 +875,43 @@ class App extends React.Component<ExcalidrawProps, AppState> {
this.addEventListeners();
}
if (prevProps.zenModeEnabled !== this.props.zenModeEnabled) {
this.setState({ zenModeEnabled: !!this.props.zenModeEnabled });
}
if (prevProps.gridModeEnabled !== this.props.gridModeEnabled) {
this.setState({
gridSize: this.props.gridModeEnabled ? GRID_SIZE : null,
});
}
if (
shouldEnableNetworkStats &&
(prevState.showStats !== this.state.showStats ||
prevProps.isCollaborating !== this.props.isCollaborating)
) {
const navigator: Navigator & {
connection?: {
addEventListener: Function;
removeEventListener: Function;
};
} = window.navigator;
if (this.state.showStats && this.props.isCollaborating) {
this.calculateNetStats();
navigator?.connection?.addEventListener(
"change",
this.calculateNetStats,
);
} else {
navigator?.connection?.removeEventListener(
"change",
this.calculateNetStats,
);
clearTimeout(this.networkSpeedIntervalId);
}
}
document
.querySelector(".excalidraw")
?.classList.toggle("Appearance_dark", this.state.appearance === "dark");
@@ -970,6 +1037,42 @@ class App extends React.Component<ExcalidrawProps, AppState> {
}
}
private calculateNetStats = () => {
this.checkNetworkPing();
this.checkNetworkSpeed();
};
private checkNetworkPing = async () => {
if (!this.state.showStats || !this.props.isCollaborating) {
clearTimeout(this.networkPingIntervalId);
return;
}
const networkPing = await getNetworkPing();
this.setState({ networkPing });
if (this.networkPingIntervalId) {
clearTimeout(this.networkPingIntervalId);
}
this.networkPingIntervalId = setTimeout(
this.checkNetworkPing,
NETWORK_TIMEOUT_MS,
);
};
private checkNetworkSpeed = async () => {
if (!this.state.showStats || !this.props.isCollaborating) {
clearTimeout(this.networkSpeedIntervalId);
return;
}
const networkSpeed = await getNetworkSpeed();
this.setState({ networkSpeed });
if (this.networkSpeedIntervalId) {
clearTimeout(this.networkSpeedIntervalId);
}
this.networkSpeedIntervalId = setTimeout(
this.checkNetworkSpeed,
NETWORK_TIMEOUT_MS,
);
};
// Copy/paste
private onCut = withBatchedUpdates((event: ClipboardEvent) => {
@@ -3453,40 +3556,11 @@ class App extends React.Component<ExcalidrawProps, AppState> {
}
};
private handleCanvasImageDrop = async (
event: React.DragEvent<HTMLCanvasElement>,
file: File,
) => {
try {
const shapes = await (
await import(
/* webpackChunkName: "pixelated-image" */ "../data/pixelated-image"
)
).pixelateImage(file, 20, 1200, event.clientX, event.clientY);
const nextElements = [
...this.scene.getElementsIncludingDeleted(),
...shapes,
];
this.scene.replaceAllElements(nextElements);
} catch (error) {
return this.setState({
isLoading: false,
errorMessage: error.message,
});
}
};
private handleCanvasOnDrop = async (
event: React.DragEvent<HTMLCanvasElement>,
) => {
let imageFile: File | null = null;
try {
const file = event.dataTransfer.files[0];
if (file?.type.indexOf("image/") === 0) {
imageFile = file;
}
if (file?.type === "image/png" || file?.type === "image/svg+xml") {
const { elements, appState } = await loadFromBlob(file, this.state);
this.syncActionResult({
@@ -3498,13 +3572,8 @@ class App extends React.Component<ExcalidrawProps, AppState> {
commitToHistory: true,
});
return;
} else if (imageFile) {
return await this.handleCanvasImageDrop(event, imageFile);
}
} catch (error) {
if (imageFile) {
return await this.handleCanvasImageDrop(event, imageFile);
}
return this.setState({
isLoading: false,
errorMessage: error.message,
@@ -3751,8 +3820,10 @@ class App extends React.Component<ExcalidrawProps, AppState> {
separator,
actionSelectAll,
separator,
actionToggleGridMode,
actionToggleZenMode,
typeof this.props.gridModeEnabled === "undefined" &&
actionToggleGridMode,
typeof this.props.zenModeEnabled === "undefined" &&
actionToggleZenMode,
typeof this.props.viewModeEnabled === "undefined" &&
actionToggleViewMode,
actionToggleStats,
+1 -1
View File
@@ -224,7 +224,7 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
shortcuts={[getShortcutKey("Alt+Z")]}
/>
<Shortcut
label={t("labels.gridMode")}
label={t("labels.showGrid")}
shortcuts={[getShortcutKey("CtrlOrCmd+'")]}
/>
<Shortcut
+15 -12
View File
@@ -52,6 +52,7 @@ interface LayerUIProps {
onLockToggle: () => void;
onInsertElements: (elements: readonly NonDeletedExcalidrawElement[]) => void;
zenModeEnabled: boolean;
showExitZenModeBtn: boolean;
toggleZenMode: () => void;
langCode: Language["code"];
isCollaborating: boolean;
@@ -296,6 +297,7 @@ const LayerUI = ({
onLockToggle,
onInsertElements,
zenModeEnabled,
showExitZenModeBtn,
toggleZenMode,
isCollaborating,
onExportToBackend,
@@ -513,17 +515,18 @@ const LayerUI = ({
"transition-right": zenModeEnabled,
})}
>
{Array.from(appState.collaborators)
// Collaborator is either not initialized or is actually the current user.
.filter(([_, client]) => Object.keys(client).length !== 0)
.map(([clientId, client]) => (
<Tooltip
label={client.username || "Unknown user"}
key={clientId}
>
{actionManager.renderAction("goToCollaborator", clientId)}
</Tooltip>
))}
{appState.collaborators.size > 0 &&
Array.from(appState.collaborators)
// Collaborator is either not initialized or is actually the current user.
.filter(([_, client]) => Object.keys(client).length !== 0)
.map(([clientId, client]) => (
<Tooltip
label={client.username || "Unknown user"}
key={clientId}
>
{actionManager.renderAction("goToCollaborator", clientId)}
</Tooltip>
))}
</UserList>
</div>
</FixedSideContainer>
@@ -578,7 +581,7 @@ const LayerUI = ({
</div>
<button
className={clsx("disable-zen-mode", {
"disable-zen-mode--visible": zenModeEnabled,
"disable-zen-mode--visible": showExitZenModeBtn,
})}
onClick={toggleZenMode}
>
+20 -18
View File
@@ -152,24 +152,26 @@ export const MobileMenu = ({
<Stack.Col gap={4}>
{renderCanvasActions()}
{renderCustomFooter?.(true)}
<fieldset>
<legend>{t("labels.collaborators")}</legend>
<UserList mobile>
{Array.from(appState.collaborators)
// Collaborator is either not initialized or is actually the current user.
.filter(
([_, client]) => Object.keys(client).length !== 0,
)
.map(([clientId, client]) => (
<React.Fragment key={clientId}>
{actionManager.renderAction(
"goToCollaborator",
clientId,
)}
</React.Fragment>
))}
</UserList>
</fieldset>
{appState.collaborators.size > 0 && (
<fieldset>
<legend>{t("labels.collaborators")}</legend>
<UserList mobile>
{Array.from(appState.collaborators)
// Collaborator is either not initialized or is actually the current user.
.filter(
([_, client]) => Object.keys(client).length !== 0,
)
.map(([clientId, client]) => (
<React.Fragment key={clientId}>
{actionManager.renderAction(
"goToCollaborator",
clientId,
)}
</React.Fragment>
))}
</UserList>
</fieldset>
)}
</Stack.Col>
</div>
</Section>
+40 -1
View File
@@ -11,7 +11,13 @@ import { t } from "../i18n";
import useIsMobile from "../is-mobile";
import { getTargetElements } from "../scene";
import { AppState } from "../types";
import { debounce, getVersion, nFormatter } from "../utils";
import {
debounce,
formatSpeedBits,
formatTime,
getVersion,
nFormatter,
} from "../utils";
import { close } from "./icons";
import { Island } from "./Island";
import "./Stats.scss";
@@ -30,6 +36,8 @@ export const Stats = (props: {
setAppState: React.Component<any, AppState>["setState"];
elements: readonly NonDeletedExcalidrawElement[];
onClose: () => void;
isCollaborating?: boolean;
shouldEnableNetworkStats: boolean;
}) => {
const isMobile = useIsMobile();
const [storageSizes, setStorageSizes] = useState<StorageSizes>({
@@ -170,6 +178,37 @@ export const Stats = (props: {
</td>
</tr>
)}
{props.shouldEnableNetworkStats && props.isCollaborating ? (
<>
<tr>
<th colSpan={2}>{t("stats.collaboration")}</th>
</tr>
<tr>
<td>{t("stats.collaborators")}</td>
<td>{props.appState.collaborators.size}</td>
</tr>
<tr>
<td>{t("stats.ping")}</td>
<td>
{props.appState.networkPing === 0
? "…"
: props.appState.networkPing > 0
? formatTime(props.appState.networkPing)
: t("stats.error")}
</td>
</tr>
<tr>
<td>{t("stats.speed")}</td>
<td>
{props.appState.networkSpeed === 0
? "…"
: props.appState.networkSpeed > 0
? formatSpeedBits(props.appState.networkSpeed)
: t("stats.error")}
</td>
</tr>
</>
) : null}
<tr>
<th colSpan={2}>{t("stats.version")}</th>
</tr>
+2
View File
@@ -8,6 +8,8 @@ export const ELEMENT_SHIFT_TRANSLATE_AMOUNT = 5;
export const ELEMENT_TRANSLATE_AMOUNT = 1;
export const TEXT_TO_CENTER_SNAP_THRESHOLD = 30;
export const SHIFT_LOCKING_ANGLE = Math.PI / 12;
export const NETWORK_TIMEOUT_MS = 4000;
export const CURSOR_TYPE = {
TEXT: "text",
CROSSHAIR: "crosshair",
+2 -1
View File
@@ -227,7 +227,8 @@
.App-top-bar {
z-index: var(--zIndex-layerUI);
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
}
.App-bottom-bar {
-106
View File
@@ -1,106 +0,0 @@
import { ExcalidrawGenericElement, NonDeleted } from "../element/types";
import { newElement } from "../element";
import { DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE } from "../constants";
import { randomId } from "../random";
const loadImage = async (url: string): Promise<HTMLImageElement> => {
const image = new Image();
return new Promise<HTMLImageElement>((resolve, reject) => {
image.onload = () => resolve(image);
image.onerror = (err) =>
reject(
new Error(
`Failed to load image: ${err ? err.toString : "unknown error"}`,
),
);
image.onabort = () =>
reject(new Error(`Failed to load image: image load aborted`));
image.src = url;
});
};
const commonProps = {
fillStyle: "solid",
fontFamily: DEFAULT_FONT_FAMILY,
fontSize: DEFAULT_FONT_SIZE,
opacity: 100,
roughness: 1,
strokeColor: "transparent",
strokeSharpness: "sharp",
strokeStyle: "solid",
strokeWidth: 1,
verticalAlign: "middle",
} as const;
export const pixelateImage = async (
blob: Blob,
cellSize: number,
suggestedMaxShapeCount: number,
x: number,
y: number,
) => {
const url = URL.createObjectURL(blob);
try {
const image = await loadImage(url);
// initialize canvas for pixelation
const { width, height } = image;
let canvasWidth = Math.floor(width / cellSize);
let canvasHeight = Math.floor(height / cellSize);
const shapeCount = canvasHeight * canvasWidth;
if (shapeCount > suggestedMaxShapeCount) {
canvasWidth = Math.floor(
canvasWidth * (suggestedMaxShapeCount / shapeCount),
);
canvasHeight = Math.floor(
canvasHeight * (suggestedMaxShapeCount / shapeCount),
);
}
const xOffset = x - (canvasWidth * cellSize) / 2;
const yOffset = y - (canvasHeight * cellSize) / 2;
const canvas =
"OffscreenCanvas" in window
? new OffscreenCanvas(canvasWidth, canvasHeight)
: document.createElement("canvas");
canvas.width = canvasWidth;
canvas.height = canvasHeight;
// Draw image on canvas
const ctx = canvas.getContext("2d")!;
ctx.drawImage(image, 0, 0, width, height, 0, 0, canvasWidth, canvasHeight);
const imageData = ctx.getImageData(0, 0, canvasWidth, canvasHeight);
const buffer = imageData.data;
const groupId = randomId();
const shapes: NonDeleted<ExcalidrawGenericElement>[] = [];
for (let row = 0; row < canvasHeight; row++) {
for (let col = 0; col < canvasWidth; col++) {
const offset = row * canvasWidth * 4 + col * 4;
const r = buffer[offset];
const g = buffer[offset + 1];
const b = buffer[offset + 2];
const alpha = buffer[offset + 3];
if (alpha) {
const color = `rgba(${r}, ${g}, ${b}, ${alpha})`;
const rectangle = newElement({
backgroundColor: color,
groupIds: [groupId],
...commonProps,
type: "rectangle",
x: xOffset + col * cellSize,
y: yOffset + row * cellSize,
width: cellSize,
height: cellSize,
});
shapes.push(rectangle);
}
}
}
return shapes;
} finally {
URL.revokeObjectURL(url);
}
};
+8 -1
View File
@@ -80,7 +80,14 @@ const initializeScene = async (opts: {
let roomLinkData = getCollaborationLinkData(window.location.href);
const isExternalScene = !!(id || jsonMatch || roomLinkData);
if (isExternalScene) {
if (roomLinkData || window.confirm(t("alerts.loadSceneOverridePrompt"))) {
if (
// don't prompt if scene is empty
!scene.elements.length ||
// don't prompt for collab scenes because we don't override local storage
roomLinkData ||
// otherwise, prompt whether user wants to override current scene
window.confirm(t("alerts.loadSceneOverridePrompt"))
) {
// Backwards compatibility with legacy url format
if (id) {
scene = await loadScene(id, null, initialData);
+4 -4
View File
@@ -235,14 +235,14 @@
"storage": "Speicher",
"title": "Statistiken für Nerds",
"total": "Gesamt",
"version": "",
"versionCopy": "",
"versionNotAvailable": "",
"version": "Version",
"versionCopy": "Zum Kopieren klicken",
"versionNotAvailable": "Version nicht verfügbar",
"width": "Breite"
},
"toast": {
"copyStyles": "Formatierung kopiert.",
"copyToClipboard": "",
"copyToClipboard": "In die Zwischenablage kopiert.",
"copyToClipboardAsPng": "In die Zwischenablage als PNG kopiert."
}
}
+6 -1
View File
@@ -77,7 +77,7 @@
"group": "Group selection",
"ungroup": "Ungroup selection",
"collaborators": "Collaborators",
"gridMode": "Grid mode",
"showGrid": "Show grid",
"addToLibrary": "Add to library",
"removeFromLibrary": "Remove from library",
"libraryLoadingMessage": "Loading library…",
@@ -227,11 +227,16 @@
},
"stats": {
"angle": "Angle",
"collaboration": "Collaboration",
"collaborators": "Collaborators",
"element": "Element",
"elements": "Elements",
"error": "Error",
"height": "Height",
"ping": "Ping",
"scene": "Scene",
"selected": "Selected",
"speed": "Speed",
"storage": "Storage",
"title": "Stats for nerds",
"total": "Total",
+5 -5
View File
@@ -80,7 +80,7 @@
"gridMode": "Modo cuadrícula",
"addToLibrary": "Añadir a la biblioteca",
"removeFromLibrary": "Eliminar de la biblioteca",
"libraryLoadingMessage": "Cargando biblioteca…",
"libraryLoadingMessage": "Cargando librería…",
"libraries": "Explorar bibliotecas",
"loadingScene": "Cargando escena…",
"align": "Alinear",
@@ -235,14 +235,14 @@
"storage": "Almacenamiento",
"title": "Estadísticas para nerds",
"total": "Total",
"version": "",
"versionCopy": "",
"versionNotAvailable": "",
"version": "Versión",
"versionCopy": "Clic para copiar",
"versionNotAvailable": "Versión no disponible",
"width": "Ancho"
},
"toast": {
"copyStyles": "Estilos copiados.",
"copyToClipboard": "",
"copyToClipboard": "Copiado en el portapapeles.",
"copyToClipboardAsPng": "Copiado al portapapeles como PNG."
}
}
+4 -4
View File
@@ -235,14 +235,14 @@
"storage": "Tallennustila",
"title": "Nörttien tilastot",
"total": "Yhteensä",
"version": "",
"versionCopy": "",
"versionNotAvailable": "",
"version": "Versio",
"versionCopy": "Klikkaa kopioidaksesi",
"versionNotAvailable": "Versio ei saatavilla",
"width": "Leveys"
},
"toast": {
"copyStyles": "Tyylit kopioitu.",
"copyToClipboard": "",
"copyToClipboard": "Kopioitu leikepöydälle.",
"copyToClipboardAsPng": "Kopioitu leikepöydälle PNG-tiedostona."
}
}
+3 -3
View File
@@ -235,9 +235,9 @@
"storage": "Aḥraz",
"title": "",
"total": "Aɣrud",
"version": "",
"versionCopy": "",
"versionNotAvailable": "",
"version": "Alqem",
"versionCopy": "Sit ad tneɣleḍ",
"versionNotAvailable": "Ur inuḥ ulqem",
"width": "Tehri"
},
"toast": {
+27 -27
View File
@@ -92,7 +92,7 @@
"centerHorizontally": "수평으로 중앙 정렬",
"distributeHorizontally": "수평으로 분배",
"distributeVertically": "수직으로 분배",
"viewMode": ""
"viewMode": "보기 모드"
},
"buttons": {
"clearReset": "캔버스 초기화",
@@ -136,7 +136,7 @@
"decryptFailed": "데이터를 복호화하지 못했습니다.",
"uploadedSecurly": "업로드는 종단 간 암호화로 보호되므로 Excalidraw 서버 및 타사가 콘텐츠를 읽을 수 없습니다.",
"loadSceneOverridePrompt": "외부 파일을 불러 오면 기존 콘텐츠가 대체됩니다. 계속 진행할까요?",
"collabStopOverridePrompt": "",
"collabStopOverridePrompt": "협업 세션을 종료하면 로컬 저장소에 있는 그림이 협업 세션의 그림으로 대체됩니다. 진행하겠습니까?\n\n(로컬 저장소에 있는 그림을 유지하려면 현재 브라우저 탭을 닫아주세요.)",
"errorLoadingLibrary": "외부 라이브러리를 불러오는 중에 문제가 발생했습니다.",
"confirmAddLibrary": "{{numShapes}}개의 모양이 라이브러리에 추가됩니다. 계속하시겠어요?",
"imageDoesNotContainScene": "이미지에서 불러오기는 현재 지원되지 않습니다.\n\n화면을 불러오려고 하셨나요? 이미지에 화면 정보가 없는 것 같습니다. 내보낼 때 화면을 포함했나요?",
@@ -202,25 +202,25 @@
"title": "오류"
},
"helpDialog": {
"blog": "",
"click": "",
"curvedArrow": "",
"curvedLine": "",
"documentation": "",
"drag": "",
"editor": "",
"github": "",
"howto": "",
"or": "",
"preventBinding": "",
"shapes": "",
"shortcuts": "",
"textFinish": "",
"textNewLine": "",
"title": "",
"view": "",
"zoomToFit": "",
"zoomToSelection": ""
"blog": "블로그 읽어보기",
"click": "클릭",
"curvedArrow": "곡선 화살표",
"curvedLine": "곡선",
"documentation": "설명서",
"drag": "드래그",
"editor": "에디터",
"github": "문제 제보하기",
"howto": "가이드 참고하기",
"or": "또는",
"preventBinding": "화살표가 붙지 않게 하기",
"shapes": "도형",
"shortcuts": "키보드 단축키",
"textFinish": "편집 완료 (텍스트)",
"textNewLine": "줄바꿈 (텍스트)",
"title": "도움말",
"view": "보기",
"zoomToFit": "모든 요소가 보이도록 확대/축소",
"zoomToSelection": "선택 영역으로 확대/축소"
},
"encrypted": {
"tooltip": "그림은 종단 간 암호화되므로 Excalidraw의 서버는 절대로 내용을 알 수 없습니다."
@@ -235,14 +235,14 @@
"storage": "저장공간",
"title": "덕후들을 위한 통계",
"total": "합계",
"version": "",
"versionCopy": "",
"versionNotAvailable": "",
"version": "버전",
"versionCopy": "복사하려면 클릭",
"versionNotAvailable": "해당 버전 사용 불가능",
"width": "너비"
},
"toast": {
"copyStyles": "",
"copyToClipboard": "",
"copyToClipboardAsPng": ""
"copyStyles": "스타일 복사.",
"copyToClipboard": "클립보드로 복사.",
"copyToClipboardAsPng": "클립보드로 PNG 이미지 복사."
}
}
+4 -4
View File
@@ -235,14 +235,14 @@
"storage": "Opslag",
"title": "Statistieken voor nerds",
"total": "Totaal",
"version": "",
"versionCopy": "",
"versionNotAvailable": "",
"version": "Versie",
"versionCopy": "Klik om te kopiëren",
"versionNotAvailable": "Versie niet beschikbaar",
"width": "Breedte"
},
"toast": {
"copyStyles": "Stijlen gekopieerd.",
"copyToClipboard": "",
"copyToClipboard": "Gekopieerd naar het klembord.",
"copyToClipboardAsPng": "Gekopieerd naar klembord als PNG."
}
}
+8 -8
View File
@@ -2,12 +2,12 @@
"ar-SA": 87,
"bg-BG": 91,
"ca-ES": 87,
"de-DE": 98,
"de-DE": 100,
"el-GR": 99,
"en": 100,
"es-ES": 98,
"es-ES": 100,
"fa-IR": 95,
"fi-FI": 98,
"fi-FI": 100,
"fr-FR": 100,
"he-IL": 87,
"hi-IN": 98,
@@ -15,20 +15,20 @@
"id-ID": 97,
"it-IT": 100,
"ja-JP": 79,
"kab-KAB": 94,
"ko-KR": 87,
"kab-KAB": 96,
"ko-KR": 100,
"my-MM": 81,
"nb-NO": 100,
"nl-NL": 97,
"nl-NL": 99,
"nn-NO": 90,
"pa-IN": 100,
"pl-PL": 88,
"pt-BR": 100,
"pt-PT": 97,
"ro-RO": 100,
"ru-RU": 97,
"ru-RU": 98,
"sk-SK": 100,
"sv-SE": 98,
"sv-SE": 100,
"tr-TR": 87,
"uk-UA": 97,
"zh-CN": 98,
+1 -1
View File
@@ -236,7 +236,7 @@
"title": "Статистика для ботаников",
"total": "Всего",
"version": "",
"versionCopy": "",
"versionCopy": "Копировать",
"versionNotAvailable": "",
"width": "Ширина"
},
+4 -4
View File
@@ -235,14 +235,14 @@
"storage": "Lagring",
"title": "Statistik för nördar",
"total": "Totalt",
"version": "",
"versionCopy": "",
"versionNotAvailable": "",
"version": "Version",
"versionCopy": "Klicka för att kopiera",
"versionNotAvailable": "Versionen är inte tillgänglig",
"width": "Bredd"
},
"toast": {
"copyStyles": "Kopierade stilar.",
"copyToClipboard": "",
"copyToClipboard": "Kopierad till urklipp.",
"copyToClipboardAsPng": "Kopierat till urklipp som PNG."
}
}
+64
View File
@@ -0,0 +1,64 @@
import { getAverage } from "./utils";
const IMAGE_URL = `${process.env.REACT_APP_SOCKET_SERVER_URL}/test256.png`;
const IMAGE_SIZE_BITS = 141978 * 8;
const AVERAGE_MAX = 4;
const speedHistory: number[] = [];
const pushSpeed = (speed: number): void => {
speedHistory.push(speed);
if (speedHistory.length > AVERAGE_MAX) {
speedHistory.shift();
}
};
const getSpeedBits = (
imageSize: number,
startTime: number,
endTime: number,
): number => {
const duration = (endTime - startTime) / 1000;
if (duration > 0) {
return imageSize / duration;
}
return 0;
};
const processImage = (): Promise<number> => {
return new Promise((resolve) => {
const image = new Image();
let endTime: number;
image.onload = () => {
endTime = new Date().getTime();
const speed = getSpeedBits(IMAGE_SIZE_BITS, startTime, endTime);
pushSpeed(speed);
resolve(getAverage(speedHistory));
};
image.onerror = () => {
resolve(-1);
};
const startTime = new Date().getTime();
image.src = `${IMAGE_URL}?t=${startTime}`;
});
};
export const getNetworkSpeed = async (): Promise<number> => {
return await processImage();
};
export const getNetworkPing = async () => {
const startTime = new Date().getTime();
try {
await fetch(process.env.REACT_APP_SOCKET_SERVER_URL, {
mode: "no-cors",
method: "HEAD",
});
const endTime = new Date().getTime();
return endTime - startTime;
} catch (error) {
return -1;
}
};
+1
View File
@@ -18,6 +18,7 @@ Please add the latest change on the top under the correct section.
### Features
- Add `zenModeEnabled` and `gridModeEnabled` prop which enables zen mode and grid mode respectively [#2901](https://github.com/excalidraw/excalidraw/pull/2901). When this prop is used, the zen mode / grid mode will be fully controlled by the host app.
- Add `viewModeEnabled` prop which enabled the view mode [#2840](https://github.com/excalidraw/excalidraw/pull/2840). When this prop is used, the view mode will not show up in context menu is so it is fully controlled by host.
- Expose `getAppState` on `excalidrawRef` [#2834](https://github.com/excalidraw/excalidraw/pull/2834).
+12 -2
View File
@@ -138,7 +138,9 @@ export default function App() {
| [`onExportToBackend`](#onExportToBackend) | Function | | Callback triggered when link button is clicked on export dialog |
| [`langCode`](#langCode) | string | `en` | Language code string |
| [`renderFooter `](#renderFooter) | Function | | Function that renders custom UI footer |
| [`viewModeEnabled`](#viewModeEnabled) | boolean | false | This implies if the app is in view mode. |
| [`viewModeEnabled`](#viewModeEnabled) | boolean | | This implies if the app is in view mode. |
| [`zenModeEnabled`](#zenModeEnabled) | boolean | | This implies if the zen mode is enabled |
| [`gridModeEnabled`](#gridModeEnabled) | boolean | | This implies if the grid mode is enabled |
### `Extra API's`
@@ -334,4 +336,12 @@ A function that renders (returns JSX) custom UI footer. For example, you can use
#### `viewModeEnabled`
This prop indicates if the app is in `view mode`. When this prop is used, the `view mode` will not show up in context menu is so it is fully controlled by host. Also the value of this prop if passed will be used over the value of `intialData.appState.viewModeEnabled`
This prop indicates whether the app is in `view mode`. When supplied, the value takes precedence over `intialData.appState.viewModeEnabled`, the `view mode` will be fully controlled by the host app, and users won't be able to toggle it from within the app.
#### `zenModeEnabled`
This prop indicates whether the app is in `zen mode`. When supplied, the value takes precedence over `intialData.appState.zenModeEnabled`, the `zen mode` will be fully controlled by the host app, and users won't be able to toggle it from within the app.
#### `gridModeEnabled`
This prop indicates whether the shows the grid. When supplied, the value takes precedence over `intialData.appState.gridModeEnabled`, the grid will be fully controlled by the host app, and users won't be able to toggle it from within the app.
+4
View File
@@ -27,6 +27,8 @@ const Excalidraw = (props: ExcalidrawProps) => {
renderFooter,
langCode = defaultLang.code,
viewModeEnabled,
zenModeEnabled,
gridModeEnabled,
} = props;
useEffect(() => {
@@ -66,6 +68,8 @@ const Excalidraw = (props: ExcalidrawProps) => {
renderFooter={renderFooter}
langCode={langCode}
viewModeEnabled={viewModeEnabled}
zenModeEnabled={zenModeEnabled}
gridModeEnabled={gridModeEnabled}
/>
</IsMobileProvider>
</InitializeApp>
+22 -9
View File
@@ -47,9 +47,11 @@ import {
TransformHandles,
TransformHandleType,
} from "../element/transformHandles";
import { viewportCoordsToSceneCoords } from "../utils";
import { viewportCoordsToSceneCoords, supportsEmoji } from "../utils";
import { UserIdleState } from "../excalidraw-app/collab/types";
const hasEmojiSupport = supportsEmoji();
const strokeRectWithRotation = (
context: CanvasRenderingContext2D,
x: number,
@@ -449,7 +451,7 @@ export const renderScene = (
const userState = sceneState.remotePointerUserStates[clientId];
if (isOutOfBounds || userState === UserIdleState.AWAY) {
context.globalAlpha = 0.2;
context.globalAlpha = 0.48;
}
if (
@@ -481,13 +483,24 @@ export const renderScene = (
context.stroke();
const username = sceneState.remotePointerUsernames[clientId];
const usernameAndIdleState = `${username ? `${username} ` : ""}${
userState === UserIdleState.AWAY
? "⚫️"
: userState === UserIdleState.IDLE
? "💤"
: "🟢"
}`;
let usernameAndIdleState;
if (hasEmojiSupport) {
usernameAndIdleState = `${username ? `${username} ` : ""}${
userState === UserIdleState.AWAY
? "⚫️"
: userState === UserIdleState.IDLE
? "💤"
: "🟢"
}`;
} else {
usernameAndIdleState = `${username ? `${username}` : ""}${
userState === UserIdleState.AWAY
? ` (${UserIdleState.AWAY})`
: userState === UserIdleState.IDLE
? ` (${UserIdleState.IDLE})`
: ""
}`;
}
if (!isOutOfBounds && usernameAndIdleState) {
const offsetX = x + width;
@@ -40,6 +40,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -500,6 +502,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -966,6 +970,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -1741,6 +1747,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -1944,6 +1952,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -2401,6 +2411,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -2653,6 +2665,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -2816,6 +2830,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -3292,6 +3308,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -3599,6 +3617,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -3802,6 +3822,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -4045,6 +4067,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -4296,6 +4320,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -4678,6 +4704,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -4972,6 +5000,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -5278,6 +5308,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -5485,6 +5517,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -5648,6 +5682,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -6100,6 +6136,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -6417,6 +6455,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -8450,6 +8490,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -8811,6 +8853,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -9065,6 +9109,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -9317,6 +9363,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -9631,6 +9679,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -9794,6 +9844,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -9957,6 +10009,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -10120,6 +10174,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -10313,6 +10369,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -10506,6 +10564,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -10699,6 +10759,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -10892,6 +10954,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -11055,6 +11119,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -11218,6 +11284,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -11411,6 +11479,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -11574,6 +11644,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -11767,6 +11839,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -12482,6 +12556,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -12734,6 +12810,8 @@ Object {
"lastPointerDownWith": "touch",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -12835,6 +12913,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -12934,6 +13014,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -13097,6 +13179,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -13404,6 +13488,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -13711,6 +13797,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -13874,6 +13962,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -14069,6 +14159,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -14317,6 +14409,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -14640,6 +14734,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -15478,6 +15574,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -15785,6 +15883,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -16092,6 +16192,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -16470,6 +16572,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -16636,6 +16740,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -16956,6 +17062,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -17194,6 +17302,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -17448,6 +17558,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -17774,6 +17886,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -17873,6 +17987,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -18036,6 +18152,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -18856,6 +18974,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -18955,6 +19075,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -19708,6 +19830,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -20112,6 +20236,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -20384,6 +20510,8 @@ Object {
"lastPointerDownWith": "touch",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -20485,6 +20613,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -20982,6 +21112,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
@@ -21081,6 +21213,8 @@ Object {
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0,
"offsetTop": 0,
"openMenu": null,
+89
View File
@@ -0,0 +1,89 @@
import React from "react";
import { fireEvent, GlobalTestState, render } from "./test-utils";
import Excalidraw from "../packages/excalidraw/index";
import { queryByText } from "@testing-library/react";
import { GRID_SIZE } from "../constants";
const { h } = window;
describe("<Excalidraw/>", () => {
describe("Test zenModeEnabled prop", () => {
it('should show exit zen mode button when zen mode is set and zen mode option in context menu when zenModeEnabled is "undefined"', async () => {
const { container } = await render(<Excalidraw />);
expect(
container.getElementsByClassName("disable-zen-mode--visible").length,
).toBe(0);
expect(h.state.zenModeEnabled).toBe(false);
fireEvent.contextMenu(GlobalTestState.canvas, {
button: 2,
clientX: 1,
clientY: 1,
});
const contextMenu = document.querySelector(".context-menu");
fireEvent.click(queryByText(contextMenu as HTMLElement, "Zen mode")!);
expect(h.state.zenModeEnabled).toBe(true);
expect(
container.getElementsByClassName("disable-zen-mode--visible").length,
).toBe(1);
});
it("should not show exit zen mode button and zen mode option in context menu when zenModeEnabled is set", async () => {
const { container } = await render(<Excalidraw zenModeEnabled={true} />);
expect(
container.getElementsByClassName("disable-zen-mode--visible").length,
).toBe(0);
expect(h.state.zenModeEnabled).toBe(true);
fireEvent.contextMenu(GlobalTestState.canvas, {
button: 2,
clientX: 1,
clientY: 1,
});
const contextMenu = document.querySelector(".context-menu");
expect(queryByText(contextMenu as HTMLElement, "Zen mode")).toBe(null);
expect(h.state.zenModeEnabled).toBe(true);
expect(
container.getElementsByClassName("disable-zen-mode--visible").length,
).toBe(0);
});
});
describe("Test gridModeEnabled prop", () => {
it('should show grid mode in context menu when gridModeEnabled is "undefined"', async () => {
const { container } = await render(<Excalidraw />);
expect(h.state.gridSize).toBe(null);
expect(
container.getElementsByClassName("disable-zen-mode--visible").length,
).toBe(0);
fireEvent.contextMenu(GlobalTestState.canvas, {
button: 2,
clientX: 1,
clientY: 1,
});
const contextMenu = document.querySelector(".context-menu");
fireEvent.click(queryByText(contextMenu as HTMLElement, "Show grid")!);
expect(h.state.gridSize).toBe(GRID_SIZE);
});
it('should not show grid mode in context menu when gridModeEnabled is not "undefined"', async () => {
const { container } = await render(
<Excalidraw gridModeEnabled={false} />,
);
expect(h.state.gridSize).toBe(null);
expect(
container.getElementsByClassName("disable-zen-mode--visible").length,
).toBe(0);
fireEvent.contextMenu(GlobalTestState.canvas, {
button: 2,
clientX: 1,
clientY: 1,
});
const contextMenu = document.querySelector(".context-menu");
expect(queryByText(contextMenu as HTMLElement, "Show grid")).toBe(null);
expect(h.state.gridSize).toBe(null);
});
});
});
+4
View File
@@ -88,6 +88,8 @@ export type AppState = {
appearance: "light" | "dark";
gridSize: number | null;
viewModeEnabled: boolean;
networkSpeed: number;
networkPing: number;
/** top-most selected groups (i.e. does not include nested groups) */
selectedGroupIds: { [groupId: string]: boolean };
@@ -185,6 +187,8 @@ export interface ExcalidrawProps {
renderFooter?: (isMobile: boolean) => JSX.Element;
langCode?: Language["code"];
viewModeEnabled?: boolean;
zenModeEnabled?: boolean;
gridModeEnabled?: boolean;
}
export type SceneData = {
+38
View File
@@ -363,9 +363,47 @@ export const nFormatter = (num: number, digits: number): string => {
);
};
export const formatSpeedBits = (speed: number): string => {
// source: https://en.wikipedia.org/wiki/Data-rate_units#Conversion_table
const suffix = ["bps", "kbps", "Mbps", "Gbps"];
let index = 0;
while (speed > 1000) {
index++;
speed = speed / 1000;
}
return `${speed.toFixed(index > 1 ? 1 : 0)} ${suffix[index]}`;
};
export const getVersion = () => {
return (
document.querySelector<HTMLMetaElement>('meta[name="version"]')?.content ||
DEFAULT_VERSION
);
};
export const formatTime = (mseconds: number): string => {
return mseconds < 1000
? `${mseconds} ms`
: `${(mseconds / 1000).toFixed(1)} s`;
};
export const getAverage = (arr: Array<number>): number => {
return arr.reduce((sum, currentVal) => sum + currentVal) / arr.length;
};
// Adapted from https://github.com/Modernizr/Modernizr/blob/master/feature-detects/emoji.js
export const supportsEmoji = () => {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
if (!ctx) {
return false;
}
const offset = 12;
ctx.fillStyle = "#f00";
ctx.textBaseline = "top";
ctx.font = "32px Arial";
// Modernizr used 🐨, but it is sort of supported on Windows 7.
// Luckily 😀 isn't supported.
ctx.fillText("😀", 0, 0);
return ctx.getImageData(offset, offset, 1, 1).data[0] !== 0;
};