Merge branch 'master' into kb/auto-save-support
This commit is contained in:
commit
5e1e16c150
@ -24,10 +24,10 @@
|
|||||||
"@testing-library/jest-dom": "5.11.9",
|
"@testing-library/jest-dom": "5.11.9",
|
||||||
"@testing-library/react": "11.2.5",
|
"@testing-library/react": "11.2.5",
|
||||||
"@types/jest": "26.0.20",
|
"@types/jest": "26.0.20",
|
||||||
"@types/react": "17.0.2",
|
"@types/react": "17.0.3",
|
||||||
"@types/react-dom": "17.0.1",
|
"@types/react-dom": "17.0.2",
|
||||||
"@types/socket.io-client": "1.4.36",
|
"@types/socket.io-client": "1.4.36",
|
||||||
"browser-fs-access": "0.14.2",
|
"browser-fs-access": "0.15.3",
|
||||||
"clsx": "1.1.1",
|
"clsx": "1.1.1",
|
||||||
"firebase": "8.2.10",
|
"firebase": "8.2.10",
|
||||||
"i18next-browser-languagedetector": "6.0.1",
|
"i18next-browser-languagedetector": "6.0.1",
|
||||||
|
@ -88,6 +88,8 @@
|
|||||||
<link rel="stylesheet" href="fonts.css" type="text/css" />
|
<link rel="stylesheet" href="fonts.css" type="text/css" />
|
||||||
<script>
|
<script>
|
||||||
window.EXCALIDRAW_ASSET_PATH = "/";
|
window.EXCALIDRAW_ASSET_PATH = "/";
|
||||||
|
// setting this so that libraries installation reuses this window tab.
|
||||||
|
window.name = "_excalidraw";
|
||||||
</script>
|
</script>
|
||||||
<% if (process.env.REACT_APP_GOOGLE_ANALYTICS_ID) { %>
|
<% if (process.env.REACT_APP_GOOGLE_ANALYTICS_ID) { %>
|
||||||
<script
|
<script
|
||||||
|
@ -11,6 +11,7 @@ import { t } from "../i18n";
|
|||||||
import useIsMobile from "../is-mobile";
|
import useIsMobile from "../is-mobile";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
import { supported } from "browser-fs-access";
|
||||||
|
|
||||||
export const actionChangeProjectName = register({
|
export const actionChangeProjectName = register({
|
||||||
name: "changeProjectName",
|
name: "changeProjectName",
|
||||||
@ -23,7 +24,9 @@ export const actionChangeProjectName = register({
|
|||||||
label={t("labels.fileTitle")}
|
label={t("labels.fileTitle")}
|
||||||
value={appState.name || "Unnamed"}
|
value={appState.name || "Unnamed"}
|
||||||
onChange={(name: string) => updateData(name)}
|
onChange={(name: string) => updateData(name)}
|
||||||
isNameEditable={typeof appProps.name === "undefined"}
|
isNameEditable={
|
||||||
|
typeof appProps.name === "undefined" && !appState.viewModeEnabled
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
@ -162,9 +165,7 @@ export const actionSaveAsScene = register({
|
|||||||
title={t("buttons.saveAs")}
|
title={t("buttons.saveAs")}
|
||||||
aria-label={t("buttons.saveAs")}
|
aria-label={t("buttons.saveAs")}
|
||||||
showAriaLabel={useIsMobile()}
|
showAriaLabel={useIsMobile()}
|
||||||
hidden={
|
hidden={!supported}
|
||||||
!("chooseFileSystemEntries" in window || "showOpenFilePicker" in window)
|
|
||||||
}
|
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { AppState } from "../../src/types";
|
import { AppState } from "../../src/types";
|
||||||
import { ButtonIconSelect } from "../components/ButtonIconSelect";
|
import { ButtonIconSelect } from "../components/ButtonIconSelect";
|
||||||
import { ButtonSelect } from "../components/ButtonSelect";
|
|
||||||
import { ColorPicker } from "../components/ColorPicker";
|
import { ColorPicker } from "../components/ColorPicker";
|
||||||
import { IconPicker } from "../components/IconPicker";
|
import { IconPicker } from "../components/IconPicker";
|
||||||
import {
|
import {
|
||||||
@ -21,6 +20,16 @@ import {
|
|||||||
StrokeStyleDottedIcon,
|
StrokeStyleDottedIcon,
|
||||||
StrokeStyleSolidIcon,
|
StrokeStyleSolidIcon,
|
||||||
StrokeWidthIcon,
|
StrokeWidthIcon,
|
||||||
|
FontSizeSmallIcon,
|
||||||
|
FontSizeMediumIcon,
|
||||||
|
FontSizeLargeIcon,
|
||||||
|
FontSizeExtraLargeIcon,
|
||||||
|
FontFamilyHandDrawnIcon,
|
||||||
|
FontFamilyNormalIcon,
|
||||||
|
FontFamilyCodeIcon,
|
||||||
|
TextAlignLeftIcon,
|
||||||
|
TextAlignCenterIcon,
|
||||||
|
TextAlignRightIcon,
|
||||||
} from "../components/icons";
|
} from "../components/icons";
|
||||||
import { DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE } from "../constants";
|
import { DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE } from "../constants";
|
||||||
import {
|
import {
|
||||||
@ -413,13 +422,29 @@ export const actionChangeFontSize = register({
|
|||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData }) => (
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{t("labels.fontSize")}</legend>
|
<legend>{t("labels.fontSize")}</legend>
|
||||||
<ButtonSelect
|
<ButtonIconSelect
|
||||||
group="font-size"
|
group="font-size"
|
||||||
options={[
|
options={[
|
||||||
{ value: 16, text: t("labels.small") },
|
{
|
||||||
{ value: 20, text: t("labels.medium") },
|
value: 16,
|
||||||
{ value: 28, text: t("labels.large") },
|
text: t("labels.small"),
|
||||||
{ value: 36, text: t("labels.veryLarge") },
|
icon: <FontSizeSmallIcon theme={appState.theme} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 20,
|
||||||
|
text: t("labels.medium"),
|
||||||
|
icon: <FontSizeMediumIcon theme={appState.theme} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 28,
|
||||||
|
text: t("labels.large"),
|
||||||
|
icon: <FontSizeLargeIcon theme={appState.theme} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 36,
|
||||||
|
text: t("labels.veryLarge"),
|
||||||
|
icon: <FontSizeExtraLargeIcon theme={appState.theme} />,
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
value={getFormValue(
|
value={getFormValue(
|
||||||
elements,
|
elements,
|
||||||
@ -456,16 +481,28 @@ export const actionChangeFontFamily = register({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, updateData }) => {
|
PanelComponent: ({ elements, appState, updateData }) => {
|
||||||
const options: { value: FontFamily; text: string }[] = [
|
const options: { value: FontFamily; text: string; icon: JSX.Element }[] = [
|
||||||
{ value: 1, text: t("labels.handDrawn") },
|
{
|
||||||
{ value: 2, text: t("labels.normal") },
|
value: 1,
|
||||||
{ value: 3, text: t("labels.code") },
|
text: t("labels.handDrawn"),
|
||||||
|
icon: <FontFamilyHandDrawnIcon theme={appState.theme} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 2,
|
||||||
|
text: t("labels.normal"),
|
||||||
|
icon: <FontFamilyNormalIcon theme={appState.theme} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 3,
|
||||||
|
text: t("labels.code"),
|
||||||
|
icon: <FontFamilyCodeIcon theme={appState.theme} />,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{t("labels.fontFamily")}</legend>
|
<legend>{t("labels.fontFamily")}</legend>
|
||||||
<ButtonSelect<FontFamily | false>
|
<ButtonIconSelect<FontFamily | false>
|
||||||
group="font-family"
|
group="font-family"
|
||||||
options={options}
|
options={options}
|
||||||
value={getFormValue(
|
value={getFormValue(
|
||||||
@ -506,12 +543,24 @@ export const actionChangeTextAlign = register({
|
|||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData }) => (
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{t("labels.textAlign")}</legend>
|
<legend>{t("labels.textAlign")}</legend>
|
||||||
<ButtonSelect<TextAlign | false>
|
<ButtonIconSelect<TextAlign | false>
|
||||||
group="text-align"
|
group="text-align"
|
||||||
options={[
|
options={[
|
||||||
{ value: "left", text: t("labels.left") },
|
{
|
||||||
{ value: "center", text: t("labels.center") },
|
value: "left",
|
||||||
{ value: "right", text: t("labels.right") },
|
text: t("labels.left"),
|
||||||
|
icon: <TextAlignLeftIcon theme={appState.theme} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "center",
|
||||||
|
text: t("labels.center"),
|
||||||
|
icon: <TextAlignCenterIcon theme={appState.theme} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "right",
|
||||||
|
text: t("labels.right"),
|
||||||
|
icon: <TextAlignRightIcon theme={appState.theme} />,
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
value={getFormValue(
|
value={getFormValue(
|
||||||
elements,
|
elements,
|
||||||
|
@ -7,12 +7,10 @@ import { AppState } from "./types";
|
|||||||
import { SVG_EXPORT_TAG } from "./scene/export";
|
import { SVG_EXPORT_TAG } from "./scene/export";
|
||||||
import { tryParseSpreadsheet, Spreadsheet, VALID_SPREADSHEET } from "./charts";
|
import { tryParseSpreadsheet, Spreadsheet, VALID_SPREADSHEET } from "./charts";
|
||||||
import { canvasToBlob } from "./data/blob";
|
import { canvasToBlob } from "./data/blob";
|
||||||
|
import { EXPORT_DATA_TYPES } from "./constants";
|
||||||
const TYPE_ELEMENTS = "excalidraw/elements";
|
|
||||||
|
|
||||||
type ElementsClipboard = {
|
type ElementsClipboard = {
|
||||||
type: typeof TYPE_ELEMENTS;
|
type: typeof EXPORT_DATA_TYPES.excalidrawClipboard;
|
||||||
created: number;
|
|
||||||
elements: ExcalidrawElement[];
|
elements: ExcalidrawElement[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -31,8 +29,16 @@ export const probablySupportsClipboardBlob =
|
|||||||
"ClipboardItem" in window &&
|
"ClipboardItem" in window &&
|
||||||
"toBlob" in HTMLCanvasElement.prototype;
|
"toBlob" in HTMLCanvasElement.prototype;
|
||||||
|
|
||||||
const isElementsClipboard = (contents: any): contents is ElementsClipboard => {
|
const clipboardContainsElements = (
|
||||||
if (contents?.type === TYPE_ELEMENTS) {
|
contents: any,
|
||||||
|
): contents is { elements: ExcalidrawElement[] } => {
|
||||||
|
if (
|
||||||
|
[
|
||||||
|
EXPORT_DATA_TYPES.excalidraw,
|
||||||
|
EXPORT_DATA_TYPES.excalidrawClipboard,
|
||||||
|
].includes(contents?.type) &&
|
||||||
|
Array.isArray(contents.elements)
|
||||||
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@ -43,8 +49,7 @@ export const copyToClipboard = async (
|
|||||||
appState: AppState,
|
appState: AppState,
|
||||||
) => {
|
) => {
|
||||||
const contents: ElementsClipboard = {
|
const contents: ElementsClipboard = {
|
||||||
type: TYPE_ELEMENTS,
|
type: EXPORT_DATA_TYPES.excalidrawClipboard,
|
||||||
created: Date.now(),
|
|
||||||
elements: getSelectedElements(elements, appState),
|
elements: getSelectedElements(elements, appState),
|
||||||
};
|
};
|
||||||
const json = JSON.stringify(contents);
|
const json = JSON.stringify(contents);
|
||||||
@ -131,15 +136,9 @@ export const parseClipboard = async (
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const systemClipboardData = JSON.parse(systemClipboard);
|
const systemClipboardData = JSON.parse(systemClipboard);
|
||||||
// system clipboard elements are newer than in-app clipboard
|
if (clipboardContainsElements(systemClipboardData)) {
|
||||||
if (
|
|
||||||
isElementsClipboard(systemClipboardData) &&
|
|
||||||
(!appClipboardData?.created ||
|
|
||||||
appClipboardData.created < systemClipboardData.created)
|
|
||||||
) {
|
|
||||||
return { elements: systemClipboardData.elements };
|
return { elements: systemClipboardData.elements };
|
||||||
}
|
}
|
||||||
// in-app clipboard is newer than system clipboard
|
|
||||||
return appClipboardData;
|
return appClipboardData;
|
||||||
} catch {
|
} catch {
|
||||||
// system clipboard doesn't contain excalidraw elements → return plaintext
|
// system clipboard doesn't contain excalidraw elements → return plaintext
|
||||||
|
@ -3,6 +3,7 @@ import React from "react";
|
|||||||
import { RoughCanvas } from "roughjs/bin/canvas";
|
import { RoughCanvas } from "roughjs/bin/canvas";
|
||||||
import rough from "roughjs/bin/rough";
|
import rough from "roughjs/bin/rough";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
import { supported } from "browser-fs-access";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
actionAddToLibrary,
|
actionAddToLibrary,
|
||||||
@ -526,8 +527,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
let zenModeEnabled = actionResult?.appState?.zenModeEnabled || false;
|
let zenModeEnabled = actionResult?.appState?.zenModeEnabled || false;
|
||||||
let gridSize = actionResult?.appState?.gridSize || null;
|
let gridSize = actionResult?.appState?.gridSize || null;
|
||||||
let theme = actionResult?.appState?.theme || "light";
|
let theme = actionResult?.appState?.theme || "light";
|
||||||
let name = actionResult?.appState?.name || this.state.name;
|
let name = actionResult?.appState?.name ?? this.state.name;
|
||||||
|
|
||||||
if (typeof this.props.viewModeEnabled !== "undefined") {
|
if (typeof this.props.viewModeEnabled !== "undefined") {
|
||||||
viewModeEnabled = this.props.viewModeEnabled;
|
viewModeEnabled = this.props.viewModeEnabled;
|
||||||
}
|
}
|
||||||
@ -3629,10 +3629,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
file?.name.endsWith(".excalidraw")
|
file?.name.endsWith(".excalidraw")
|
||||||
) {
|
) {
|
||||||
this.setState({ isLoading: true });
|
this.setState({ isLoading: true });
|
||||||
if (
|
if (supported) {
|
||||||
"chooseFileSystemEntries" in window ||
|
|
||||||
"showOpenFilePicker" in window
|
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
// This will only work as of Chrome 86,
|
// This will only work as of Chrome 86,
|
||||||
// but can be safely ignored on older releases.
|
// but can be safely ignored on older releases.
|
||||||
|
@ -31,12 +31,16 @@
|
|||||||
.ExportDialog__name {
|
.ExportDialog__name {
|
||||||
grid-column: project-name;
|
grid-column: project-name;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
.TextInput {
|
.TextInput {
|
||||||
height: calc(1rem - 3px);
|
height: calc(1rem - 3px);
|
||||||
width: 200px;
|
width: 200px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
margin-left: 8px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
&--readonly {
|
&--readonly {
|
||||||
background: none;
|
background: none;
|
||||||
@ -44,6 +48,9 @@
|
|||||||
&:hover {
|
&:hover {
|
||||||
background: none;
|
background: none;
|
||||||
}
|
}
|
||||||
|
width: auto;
|
||||||
|
max-width: 200px;
|
||||||
|
padding-left: 2px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -144,6 +144,8 @@ const LibraryMenuItems = ({
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{!!library.length && (
|
||||||
|
<>
|
||||||
<ToolButton
|
<ToolButton
|
||||||
key="export"
|
key="export"
|
||||||
type="button"
|
type="button"
|
||||||
@ -171,9 +173,12 @@ const LibraryMenuItems = ({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<a
|
<a
|
||||||
href={`https://libraries.excalidraw.com?referrer=${referrer}`}
|
href={`https://libraries.excalidraw.com?target=${
|
||||||
|
window.name || "_blank"
|
||||||
|
}&referrer=${referrer}`}
|
||||||
target="_excalidraw_libraries"
|
target="_excalidraw_libraries"
|
||||||
>
|
>
|
||||||
{t("labels.libraries")}
|
{t("labels.libraries")}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import "./TextInput.scss";
|
import "./TextInput.scss";
|
||||||
|
|
||||||
import React, { Component } from "react";
|
import React, { Component } from "react";
|
||||||
import { selectNode, removeSelection } from "../utils";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
value: string;
|
value: string;
|
||||||
@ -10,17 +9,18 @@ type Props = {
|
|||||||
isNameEditable: boolean;
|
isNameEditable: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class ProjectName extends Component<Props> {
|
type State = {
|
||||||
private handleFocus = (event: React.FocusEvent<HTMLElement>) => {
|
fileName: string;
|
||||||
selectNode(event.currentTarget);
|
|
||||||
};
|
};
|
||||||
|
export class ProjectName extends Component<Props, State> {
|
||||||
private handleBlur = (event: React.FocusEvent<HTMLElement>) => {
|
state = {
|
||||||
const value = event.currentTarget.innerText.trim();
|
fileName: this.props.value,
|
||||||
|
};
|
||||||
|
private handleBlur = (event: any) => {
|
||||||
|
const value = event.target.value;
|
||||||
if (value !== this.props.value) {
|
if (value !== this.props.value) {
|
||||||
this.props.onChange(value);
|
this.props.onChange(value);
|
||||||
}
|
}
|
||||||
removeSelection();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private handleKeyDown = (event: React.KeyboardEvent<HTMLElement>) => {
|
private handleKeyDown = (event: React.KeyboardEvent<HTMLElement>) => {
|
||||||
@ -32,39 +32,30 @@ export class ProjectName extends Component<Props> {
|
|||||||
event.currentTarget.blur();
|
event.currentTarget.blur();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
private makeEditable = (editable: HTMLSpanElement | null) => {
|
|
||||||
if (!editable) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
editable.contentEditable = "plaintext-only";
|
|
||||||
} catch {
|
|
||||||
editable.contentEditable = "true";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
return this.props.isNameEditable ? (
|
return (
|
||||||
<span
|
<>
|
||||||
suppressContentEditableWarning
|
<label htmlFor="file-name">
|
||||||
ref={this.makeEditable}
|
{`${this.props.label}${this.props.isNameEditable ? "" : ":"}`}
|
||||||
data-type="wysiwyg"
|
</label>
|
||||||
|
{this.props.isNameEditable ? (
|
||||||
|
<input
|
||||||
className="TextInput"
|
className="TextInput"
|
||||||
role="textbox"
|
|
||||||
aria-label={this.props.label}
|
|
||||||
onBlur={this.handleBlur}
|
onBlur={this.handleBlur}
|
||||||
onKeyDown={this.handleKeyDown}
|
onKeyDown={this.handleKeyDown}
|
||||||
onFocus={this.handleFocus}
|
id="file-name"
|
||||||
>
|
value={this.state.fileName}
|
||||||
{this.props.value}
|
onChange={(event) =>
|
||||||
</span>
|
this.setState({ fileName: event.target.value })
|
||||||
|
}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<span
|
<span className="TextInput TextInput--readonly" id="file-name">
|
||||||
className="TextInput TextInput--readonly"
|
|
||||||
aria-label={this.props.label}
|
|
||||||
>
|
|
||||||
{this.props.value}
|
{this.props.value}
|
||||||
</span>
|
</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -123,6 +123,22 @@ export const shareIOS = createIcon(
|
|||||||
{ width: 24, height: 24 },
|
{ width: 24, height: 24 },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const shareWindows = createIcon(
|
||||||
|
<>
|
||||||
|
<path
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M40 5.6v6.1l-4.1.7c-8.9 1.4-16.5 6.9-20.6 15C13 32 10.9 43 12.4 43c.4 0 2.4-1.3 4.4-3 5-3.9 12.1-7 18.2-7.7l5-.6v12.8l11.2-11.3L62.5 22 51.2 10.8 40-.5v6.1zm10.2 22.6L44 34.5v-6.8l-6.9.6c-3.9.3-9.8 1.7-13.2 3.1-3.5 1.4-6.5 2.4-6.7 2.2-.9-1 3-7.5 6.4-10.8C28 18.6 34.4 16 40.1 16c3.7 0 3.9-.1 3.9-3.2V9.5l6.2 6.3 6.3 6.2-6.3 6.2z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M0 36v20h48v-6.2c0-6 0-6.1-2-4.3-1.1 1-2 2.9-2 4.2V52H4V34c0-17.3-.1-18-2-18s-2 .7-2 20z"
|
||||||
|
/>
|
||||||
|
</>,
|
||||||
|
{ width: 64, height: 64 },
|
||||||
|
);
|
||||||
|
|
||||||
// Icon imported form Storybook
|
// Icon imported form Storybook
|
||||||
// Storybook is licensed under MIT https://github.com/storybookjs/storybook/blob/next/LICENSE
|
// Storybook is licensed under MIT https://github.com/storybookjs/storybook/blob/next/LICENSE
|
||||||
export const resetZoom = createIcon(
|
export const resetZoom = createIcon(
|
||||||
@ -794,3 +810,121 @@ export const ArrowheadBarIcon = React.memo(
|
|||||||
{ width: 40, height: 20 },
|
{ width: 40, height: 20 },
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const FontSizeSmallIcon = React.memo(
|
||||||
|
({ theme }: { theme: "light" | "dark" }) =>
|
||||||
|
createIcon(
|
||||||
|
<path
|
||||||
|
fill={iconFillColor(theme)}
|
||||||
|
d="M 0 69.092 L 0 55.03 A 124.24 124.24 0 0 0 4.706 57.02 Q 6.826 57.863 8.708 58.5 A 53.466 53.466 0 0 0 12.231 59.571 Q 17.236 60.889 21.387 60.889 A 20.909 20.909 0 0 0 24.265 60.704 Q 25.719 60.502 26.903 60.077 A 8.649 8.649 0 0 0 29.028 58.985 Q 31.689 57.08 31.689 53.321 Q 31.689 51.221 30.518 49.585 A 10.126 10.126 0 0 0 29.282 48.177 Q 28.352 47.287 27.075 46.436 A 23.719 23.719 0 0 0 25.752 45.627 Q 23.774 44.492 20.176 42.735 A 254.44 254.44 0 0 0 17.822 41.602 Q 11.503 38.631 8.236 35.888 A 19.742 19.742 0 0 1 8.008 35.694 A 22.18 22.18 0 0 1 2.783 29.102 Q 0.83 25.342 0.83 20.313 A 22.471 22.471 0 0 1 1.733 13.778 A 17.283 17.283 0 0 1 7.251 5.42 A 21.486 21.486 0 0 1 15.177 1.272 Q 18.361 0.338 22.166 0.09 A 43.573 43.573 0 0 1 25 0 A 42.399 42.399 0 0 1 34.349 1.01 A 39.075 39.075 0 0 1 35.62 1.319 A 67.407 67.407 0 0 1 42.108 3.382 A 83.357 83.357 0 0 1 46.191 5.03 L 41.309 16.797 Q 35.596 14.453 31.86 13.526 A 30.762 30.762 0 0 0 25.417 12.612 A 28.337 28.337 0 0 0 24.512 12.598 A 14.846 14.846 0 0 0 22.022 12.793 Q 19.498 13.224 17.92 14.6 Q 15.625 16.602 15.625 19.824 Q 15.625 21.826 16.553 23.316 Q 17.48 24.805 19.507 26.197 A 18.343 18.343 0 0 0 20.659 26.912 Q 22.596 28.035 26.516 29.953 A 299.99 299.99 0 0 0 29.102 31.201 Q 37.91 35.412 41.841 39.642 A 16.553 16.553 0 0 1 42.822 40.796 A 17.675 17.675 0 0 1 46.301 49.233 A 23.517 23.517 0 0 1 46.533 52.588 A 21.581 21.581 0 0 1 45.471 59.515 A 17.733 17.733 0 0 1 39.575 67.823 Q 33.745 72.486 24.094 73.243 A 49.683 49.683 0 0 1 20.215 73.389 A 51.712 51.712 0 0 1 9.448 72.315 A 40.672 40.672 0 0 1 0 69.092 Z"
|
||||||
|
/>,
|
||||||
|
{ width: 47, height: 77 },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const FontSizeMediumIcon = React.memo(
|
||||||
|
({ theme }: { theme: "light" | "dark" }) =>
|
||||||
|
createIcon(
|
||||||
|
<path
|
||||||
|
fill={iconFillColor(theme)}
|
||||||
|
d="M 44.092 71.387 L 30.225 71.387 L 13.037 15.381 L 12.598 15.381 A 1505.093 1505.093 0 0 1 12.959 22.313 Q 13.426 31.715 13.508 36.4 A 102.991 102.991 0 0 1 13.525 38.184 L 13.525 71.387 L 0 71.387 L 0 0 L 20.605 0 L 37.5 54.59 L 37.793 54.59 L 55.713 0 L 76.318 0 L 76.318 71.387 L 62.207 71.387 L 62.207 37.598 Q 62.207 35.205 62.28 32.08 A 160.703 160.703 0 0 1 62.326 30.544 Q 62.452 26.754 62.866 17.168 A 5390.536 5390.536 0 0 1 62.939 15.479 L 62.5 15.479 L 44.092 71.387 Z"
|
||||||
|
/>,
|
||||||
|
{ width: 77, height: 75 },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const FontSizeLargeIcon = React.memo(
|
||||||
|
({ theme }: { theme: "light" | "dark" }) =>
|
||||||
|
createIcon(
|
||||||
|
<path
|
||||||
|
fill={iconFillColor(theme)}
|
||||||
|
d="M 44.092 71.387 L 0 71.387 L 0 0 L 15.137 0 L 15.137 58.887 L 44.092 58.887 L 44.092 71.387 Z"
|
||||||
|
/>,
|
||||||
|
{ width: 45, height: 75 },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const FontSizeExtraLargeIcon = React.memo(
|
||||||
|
({ theme }: { theme: "light" | "dark" }) =>
|
||||||
|
createIcon(
|
||||||
|
<path
|
||||||
|
fill={iconFillColor(theme)}
|
||||||
|
d="M 42.578 35.4 L 66.699 71.387 L 49.414 71.387 L 32.813 44.385 L 16.211 71.387 L 0 71.387 L 23.682 34.57 L 1.514 0 L 18.213 0 L 33.594 25.684 L 48.682 0 L 64.99 0 L 42.578 35.4 Z M 119.775 71.387 L 75.684 71.387 L 75.684 0 L 90.82 0 L 90.82 58.887 L 119.775 58.887 L 119.775 71.387 Z"
|
||||||
|
/>,
|
||||||
|
{ width: 120, height: 75 },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const FontFamilyHandDrawnIcon = React.memo(
|
||||||
|
({ theme }: { theme: "light" | "dark" }) =>
|
||||||
|
createIcon(
|
||||||
|
<path
|
||||||
|
fill={iconFillColor(theme)}
|
||||||
|
d="M290.74 93.24l128.02 128.02-277.99 277.99-114.14 12.6C11.35 513.54-1.56 500.62.14 485.34l12.7-114.22 277.9-277.88zm207.2-19.06l-60.11-60.11c-18.75-18.75-49.16-18.75-67.91 0l-56.55 56.55 128.02 128.02 56.55-56.55c18.75-18.76 18.75-49.16 0-67.91z"
|
||||||
|
/>,
|
||||||
|
{ width: 448, height: 512 },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const FontFamilyNormalIcon = React.memo(
|
||||||
|
({ theme }: { theme: "light" | "dark" }) =>
|
||||||
|
createIcon(
|
||||||
|
<>
|
||||||
|
<path
|
||||||
|
fill={iconFillColor(theme)}
|
||||||
|
d="M 63.818 71.68 L 54.492 71.68 L 45.898 49.561 L 17.578 49.561 L 9.082 71.68 L 0 71.68 L 27.881 0 L 35.986 0 L 63.818 71.68 Z M 20.605 41.602 L 43.213 41.602 L 35.205 19.971 L 31.787 9.277 Q 30.322 15.137 28.711 19.971 L 20.605 41.602 Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill={iconFillColor(theme)}
|
||||||
|
d="M 68.994 71.68 L 52.686 71.68 L 47.51 54.688 L 21.484 54.688 L 16.309 71.68 L 0 71.68 L 25.195 0 L 43.701 0 L 68.994 71.68 Z M 25.293 41.992 L 43.896 41.992 A 27590.463 27590.463 0 0 1 42.2 36.532 Q 36.965 19.676 35.937 16.273 A 120.932 120.932 0 0 1 35.815 15.869 A 131.65 131.65 0 0 1 35.396 14.435 Q 34.951 12.879 34.675 11.741 A 34.866 34.866 0 0 1 34.521 11.084 A 141.762 141.762 0 0 1 33.706 14.075 Q 31.482 21.957 25.293 41.992 Z"
|
||||||
|
/>
|
||||||
|
</>,
|
||||||
|
{ width: 70, height: 78 },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const FontFamilyCodeIcon = React.memo(
|
||||||
|
({ theme }: { theme: "light" | "dark" }) =>
|
||||||
|
createIcon(
|
||||||
|
<>
|
||||||
|
<path
|
||||||
|
fill={iconFillColor(theme)}
|
||||||
|
d="M278.9 511.5l-61-17.7c-6.4-1.8-10-8.5-8.2-14.9L346.2 8.7c1.8-6.4 8.5-10 14.9-8.2l61 17.7c6.4 1.8 10 8.5 8.2 14.9L293.8 503.3c-1.9 6.4-8.5 10.1-14.9 8.2zm-114-112.2l43.5-46.4c4.6-4.9 4.3-12.7-.8-17.2L117 256l90.6-79.7c5.1-4.5 5.5-12.3.8-17.2l-43.5-46.4c-4.5-4.8-12.1-5.1-17-.5L3.8 247.2c-5.1 4.7-5.1 12.8 0 17.5l144.1 135.1c4.9 4.6 12.5 4.4 17-.5zm327.2.6l144.1-135.1c5.1-4.7 5.1-12.8 0-17.5L492.1 112.1c-4.8-4.5-12.4-4.3-17 .5L431.6 159c-4.6 4.9-4.3 12.7.8 17.2L523 256l-90.6 79.7c-5.1 4.5-5.5 12.3-.8 17.2l43.5 46.4c4.5 4.9 12.1 5.1 17 .6z"
|
||||||
|
/>
|
||||||
|
</>,
|
||||||
|
{ width: 640, height: 512 },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const TextAlignLeftIcon = React.memo(
|
||||||
|
({ theme }: { theme: "light" | "dark" }) =>
|
||||||
|
createIcon(
|
||||||
|
<path
|
||||||
|
d="M12.83 352h262.34A12.82 12.82 0 00288 339.17v-38.34A12.82 12.82 0 00275.17 288H12.83A12.82 12.82 0 000 300.83v38.34A12.82 12.82 0 0012.83 352zm0-256h262.34A12.82 12.82 0 00288 83.17V44.83A12.82 12.82 0 00275.17 32H12.83A12.82 12.82 0 000 44.83v38.34A12.82 12.82 0 0012.83 96zM432 160H16a16 16 0 00-16 16v32a16 16 0 0016 16h416a16 16 0 0016-16v-32a16 16 0 00-16-16zm0 256H16a16 16 0 00-16 16v32a16 16 0 0016 16h416a16 16 0 0016-16v-32a16 16 0 00-16-16z"
|
||||||
|
fill={iconFillColor(theme)}
|
||||||
|
/>,
|
||||||
|
{ width: 448, height: 512 },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const TextAlignCenterIcon = React.memo(
|
||||||
|
({ theme }: { theme: "light" | "dark" }) =>
|
||||||
|
createIcon(
|
||||||
|
<path
|
||||||
|
d="M432 160H16a16 16 0 00-16 16v32a16 16 0 0016 16h416a16 16 0 0016-16v-32a16 16 0 00-16-16zm0 256H16a16 16 0 00-16 16v32a16 16 0 0016 16h416a16 16 0 0016-16v-32a16 16 0 00-16-16zM108.1 96h231.81A12.09 12.09 0 00352 83.9V44.09A12.09 12.09 0 00339.91 32H108.1A12.09 12.09 0 0096 44.09V83.9A12.1 12.1 0 00108.1 96zm231.81 256A12.09 12.09 0 00352 339.9v-39.81A12.09 12.09 0 00339.91 288H108.1A12.09 12.09 0 0096 300.09v39.81a12.1 12.1 0 0012.1 12.1z"
|
||||||
|
fill={iconFillColor(theme)}
|
||||||
|
/>,
|
||||||
|
{ width: 448, height: 512 },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const TextAlignRightIcon = React.memo(
|
||||||
|
({ theme }: { theme: "light" | "dark" }) =>
|
||||||
|
createIcon(
|
||||||
|
<path
|
||||||
|
d="M16 224h416a16 16 0 0016-16v-32a16 16 0 00-16-16H16a16 16 0 00-16 16v32a16 16 0 0016 16zm416 192H16a16 16 0 00-16 16v32a16 16 0 0016 16h416a16 16 0 0016-16v-32a16 16 0 00-16-16zm3.17-384H172.83A12.82 12.82 0 00160 44.83v38.34A12.82 12.82 0 00172.83 96h262.34A12.82 12.82 0 00448 83.17V44.83A12.82 12.82 0 00435.17 32zm0 256H172.83A12.82 12.82 0 00160 300.83v38.34A12.82 12.82 0 00172.83 352h262.34A12.82 12.82 0 00448 339.17v-38.34A12.82 12.82 0 00435.17 288z"
|
||||||
|
fill={iconFillColor(theme)}
|
||||||
|
/>,
|
||||||
|
{ width: 448, height: 512 },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
@ -84,9 +84,15 @@ export const MIME_TYPES = {
|
|||||||
excalidrawlib: "application/vnd.excalidrawlib+json",
|
excalidrawlib: "application/vnd.excalidrawlib+json",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const EXPORT_DATA_TYPES = {
|
||||||
|
excalidraw: "excalidraw",
|
||||||
|
excalidrawClipboard: "excalidraw/clipboard",
|
||||||
|
excalidrawLibrary: "excalidrawlib",
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const STORAGE_KEYS = {
|
export const STORAGE_KEYS = {
|
||||||
LOCAL_STORAGE_LIBRARY: "excalidraw-library",
|
LOCAL_STORAGE_LIBRARY: "excalidraw-library",
|
||||||
};
|
} as const;
|
||||||
|
|
||||||
// time in milliseconds
|
// time in milliseconds
|
||||||
export const TAP_TWICE_TIMEOUT = 300;
|
export const TAP_TWICE_TIMEOUT = 300;
|
||||||
|
@ -222,7 +222,8 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
svg {
|
svg {
|
||||||
width: 36px;
|
width: 36px;
|
||||||
height: 18px;
|
height: 14px;
|
||||||
|
padding: 2px;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
&.active svg {
|
&.active svg {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { cleanAppStateForExport } from "../appState";
|
import { cleanAppStateForExport } from "../appState";
|
||||||
import { MIME_TYPES } from "../constants";
|
import { EXPORT_DATA_TYPES, MIME_TYPES } from "../constants";
|
||||||
import { clearElementsForExport } from "../element";
|
import { clearElementsForExport } from "../element";
|
||||||
import { CanvasError } from "../errors";
|
import { CanvasError } from "../errors";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
@ -121,7 +121,7 @@ export const loadFromBlob = async (
|
|||||||
export const loadLibraryFromBlob = async (blob: Blob) => {
|
export const loadLibraryFromBlob = async (blob: Blob) => {
|
||||||
const contents = await parseFileContents(blob);
|
const contents = await parseFileContents(blob);
|
||||||
const data: LibraryData = JSON.parse(contents);
|
const data: LibraryData = JSON.parse(contents);
|
||||||
if (data.type !== "excalidrawlib") {
|
if (data.type !== EXPORT_DATA_TYPES.excalidrawLibrary) {
|
||||||
throw new Error(t("alerts.couldNotLoadInvalidFile"));
|
throw new Error(t("alerts.couldNotLoadInvalidFile"));
|
||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
|
@ -2,7 +2,7 @@ import decodePng from "png-chunks-extract";
|
|||||||
import tEXt from "png-chunk-text";
|
import tEXt from "png-chunk-text";
|
||||||
import encodePng from "png-chunks-encode";
|
import encodePng from "png-chunks-encode";
|
||||||
import { stringToBase64, encode, decode, base64ToString } from "./encode";
|
import { stringToBase64, encode, decode, base64ToString } from "./encode";
|
||||||
import { MIME_TYPES } from "../constants";
|
import { EXPORT_DATA_TYPES, MIME_TYPES } from "../constants";
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// PNG
|
// PNG
|
||||||
@ -67,7 +67,10 @@ export const decodePngMetadata = async (blob: Blob) => {
|
|||||||
const encodedData = JSON.parse(metadata.text);
|
const encodedData = JSON.parse(metadata.text);
|
||||||
if (!("encoded" in encodedData)) {
|
if (!("encoded" in encodedData)) {
|
||||||
// legacy, un-encoded scene JSON
|
// legacy, un-encoded scene JSON
|
||||||
if ("type" in encodedData && encodedData.type === "excalidraw") {
|
if (
|
||||||
|
"type" in encodedData &&
|
||||||
|
encodedData.type === EXPORT_DATA_TYPES.excalidraw
|
||||||
|
) {
|
||||||
return metadata.text;
|
return metadata.text;
|
||||||
}
|
}
|
||||||
throw new Error("FAILED");
|
throw new Error("FAILED");
|
||||||
@ -115,7 +118,10 @@ export const decodeSvgMetadata = async ({ svg }: { svg: string }) => {
|
|||||||
const encodedData = JSON.parse(json);
|
const encodedData = JSON.parse(json);
|
||||||
if (!("encoded" in encodedData)) {
|
if (!("encoded" in encodedData)) {
|
||||||
// legacy, un-encoded scene JSON
|
// legacy, un-encoded scene JSON
|
||||||
if ("type" in encodedData && encodedData.type === "excalidraw") {
|
if (
|
||||||
|
"type" in encodedData &&
|
||||||
|
encodedData.type === EXPORT_DATA_TYPES.excalidraw
|
||||||
|
) {
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
throw new Error("FAILED");
|
throw new Error("FAILED");
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { fileOpen, fileSave } from "browser-fs-access";
|
import { fileOpen, fileSave } from "browser-fs-access";
|
||||||
import { cleanAppStateForExport } from "../appState";
|
import { cleanAppStateForExport } from "../appState";
|
||||||
import { MIME_TYPES } from "../constants";
|
import { EXPORT_DATA_TYPES, MIME_TYPES } from "../constants";
|
||||||
import { clearElementsForExport } from "../element";
|
import { clearElementsForExport } from "../element";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
@ -14,7 +14,7 @@ export const serializeAsJSON = (
|
|||||||
): string =>
|
): string =>
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
{
|
{
|
||||||
type: "excalidraw",
|
type: EXPORT_DATA_TYPES.excalidraw,
|
||||||
version: 2,
|
version: 2,
|
||||||
source: window.location.origin,
|
source: window.location.origin,
|
||||||
elements: clearElementsForExport(elements),
|
elements: clearElementsForExport(elements),
|
||||||
@ -69,7 +69,7 @@ export const isValidExcalidrawData = (data?: {
|
|||||||
appState?: any;
|
appState?: any;
|
||||||
}): data is ImportedDataState => {
|
}): data is ImportedDataState => {
|
||||||
return (
|
return (
|
||||||
data?.type === "excalidraw" &&
|
data?.type === EXPORT_DATA_TYPES.excalidraw &&
|
||||||
(!data.elements ||
|
(!data.elements ||
|
||||||
(Array.isArray(data.elements) &&
|
(Array.isArray(data.elements) &&
|
||||||
(!data.appState || typeof data.appState === "object")))
|
(!data.appState || typeof data.appState === "object")))
|
||||||
@ -80,7 +80,7 @@ export const isValidLibrary = (json: any) => {
|
|||||||
return (
|
return (
|
||||||
typeof json === "object" &&
|
typeof json === "object" &&
|
||||||
json &&
|
json &&
|
||||||
json.type === "excalidrawlib" &&
|
json.type === EXPORT_DATA_TYPES.excalidrawLibrary &&
|
||||||
json.version === 1
|
json.version === 1
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -89,7 +89,7 @@ export const saveLibraryAsJSON = async () => {
|
|||||||
const library = await Library.loadLibrary();
|
const library = await Library.loadLibrary();
|
||||||
const serialized = JSON.stringify(
|
const serialized = JSON.stringify(
|
||||||
{
|
{
|
||||||
type: "excalidrawlib",
|
type: EXPORT_DATA_TYPES.excalidrawLibrary,
|
||||||
version: 1,
|
version: 1,
|
||||||
library,
|
library,
|
||||||
},
|
},
|
||||||
|
@ -207,7 +207,8 @@ export const textWysiwyg = ({
|
|||||||
// prevent blur when changing properties from the menu
|
// prevent blur when changing properties from the menu
|
||||||
const onPointerDown = (event: MouseEvent) => {
|
const onPointerDown = (event: MouseEvent) => {
|
||||||
if (
|
if (
|
||||||
event.target instanceof HTMLElement &&
|
(event.target instanceof HTMLElement ||
|
||||||
|
event.target instanceof SVGElement) &&
|
||||||
event.target.closest(`.${CLASSES.SHAPE_ACTIONS_MENU}`) &&
|
event.target.closest(`.${CLASSES.SHAPE_ACTIONS_MENU}`) &&
|
||||||
!isWritableElement(event.target)
|
!isWritableElement(event.target)
|
||||||
) {
|
) {
|
||||||
|
@ -448,15 +448,8 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
|||||||
|
|
||||||
private handleRemoteSceneUpdate = (
|
private handleRemoteSceneUpdate = (
|
||||||
elements: ReconciledElements,
|
elements: ReconciledElements,
|
||||||
{
|
{ init = false }: { init?: boolean } = {},
|
||||||
init = false,
|
|
||||||
initFromSnapshot = false,
|
|
||||||
}: { init?: boolean; initFromSnapshot?: boolean } = {},
|
|
||||||
) => {
|
) => {
|
||||||
if (init || initFromSnapshot) {
|
|
||||||
this.excalidrawAPI.setScrollToContent(elements);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.excalidrawAPI.updateScene({
|
this.excalidrawAPI.updateScene({
|
||||||
elements,
|
elements,
|
||||||
commitToHistory: !!init,
|
commitToHistory: !!init,
|
||||||
|
@ -7,12 +7,27 @@ import {
|
|||||||
stop,
|
stop,
|
||||||
share,
|
share,
|
||||||
shareIOS,
|
shareIOS,
|
||||||
|
shareWindows,
|
||||||
} from "../../components/icons";
|
} from "../../components/icons";
|
||||||
import { ToolButton } from "../../components/ToolButton";
|
import { ToolButton } from "../../components/ToolButton";
|
||||||
import { t } from "../../i18n";
|
import { t } from "../../i18n";
|
||||||
import "./RoomDialog.scss";
|
import "./RoomDialog.scss";
|
||||||
import Stack from "../../components/Stack";
|
import Stack from "../../components/Stack";
|
||||||
|
|
||||||
|
const getShareIcon = () => {
|
||||||
|
const navigator = window.navigator as any;
|
||||||
|
const isAppleBrowser = /Apple/.test(navigator.vendor);
|
||||||
|
const isWindowsBrowser = navigator.appVersion.indexOf("Win") !== -1;
|
||||||
|
|
||||||
|
if (isAppleBrowser) {
|
||||||
|
return shareIOS;
|
||||||
|
} else if (isWindowsBrowser) {
|
||||||
|
return shareWindows;
|
||||||
|
}
|
||||||
|
|
||||||
|
return share;
|
||||||
|
};
|
||||||
|
|
||||||
const RoomDialog = ({
|
const RoomDialog = ({
|
||||||
handleClose,
|
handleClose,
|
||||||
activeRoomLink,
|
activeRoomLink,
|
||||||
@ -31,8 +46,6 @@ const RoomDialog = ({
|
|||||||
setErrorMessage: (message: string) => void;
|
setErrorMessage: (message: string) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const roomLinkInput = useRef<HTMLInputElement>(null);
|
const roomLinkInput = useRef<HTMLInputElement>(null);
|
||||||
const navigator = window.navigator as any;
|
|
||||||
const isAppleBrowser = /Apple/.test(navigator.vendor);
|
|
||||||
|
|
||||||
const copyRoomLink = async () => {
|
const copyRoomLink = async () => {
|
||||||
try {
|
try {
|
||||||
@ -93,7 +106,7 @@ const RoomDialog = ({
|
|||||||
{"share" in navigator ? (
|
{"share" in navigator ? (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
type="button"
|
type="button"
|
||||||
icon={isAppleBrowser ? shareIOS : share}
|
icon={getShareIcon()}
|
||||||
title={t("labels.share")}
|
title={t("labels.share")}
|
||||||
aria-label={t("labels.share")}
|
aria-label={t("labels.share")}
|
||||||
onClick={shareRoomLink}
|
onClick={shareRoomLink}
|
||||||
|
@ -80,8 +80,10 @@ export type SocketUpdateData = SocketUpdateDataSource[keyof SocketUpdateDataSour
|
|||||||
_brand: "socketUpdateData";
|
_brand: "socketUpdateData";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const IV_LENGTH_BYTES = 12; // 96 bits
|
||||||
|
|
||||||
export const createIV = () => {
|
export const createIV = () => {
|
||||||
const arr = new Uint8Array(12);
|
const arr = new Uint8Array(IV_LENGTH_BYTES);
|
||||||
return window.crypto.getRandomValues(arr);
|
return window.crypto.getRandomValues(arr);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -175,6 +177,22 @@ export const getImportedKey = (key: string, usage: KeyUsage) =>
|
|||||||
[usage],
|
[usage],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const decryptImported = async (
|
||||||
|
iv: ArrayBuffer,
|
||||||
|
encrypted: ArrayBuffer,
|
||||||
|
privateKey: string,
|
||||||
|
): Promise<ArrayBuffer> => {
|
||||||
|
const key = await getImportedKey(privateKey, "decrypt");
|
||||||
|
return window.crypto.subtle.decrypt(
|
||||||
|
{
|
||||||
|
name: "AES-GCM",
|
||||||
|
iv,
|
||||||
|
},
|
||||||
|
key,
|
||||||
|
encrypted,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const importFromBackend = async (
|
const importFromBackend = async (
|
||||||
id: string | null,
|
id: string | null,
|
||||||
privateKey?: string | null,
|
privateKey?: string | null,
|
||||||
@ -183,6 +201,7 @@ const importFromBackend = async (
|
|||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
privateKey ? `${BACKEND_V2_GET}${id}` : `${BACKEND_GET}${id}.json`,
|
privateKey ? `${BACKEND_V2_GET}${id}` : `${BACKEND_GET}${id}.json`,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
window.alert(t("alerts.importBackendFailed"));
|
window.alert(t("alerts.importBackendFailed"));
|
||||||
return {};
|
return {};
|
||||||
@ -190,16 +209,19 @@ const importFromBackend = async (
|
|||||||
let data: ImportedDataState;
|
let data: ImportedDataState;
|
||||||
if (privateKey) {
|
if (privateKey) {
|
||||||
const buffer = await response.arrayBuffer();
|
const buffer = await response.arrayBuffer();
|
||||||
const key = await getImportedKey(privateKey, "decrypt");
|
|
||||||
const iv = new Uint8Array(12);
|
let decrypted: ArrayBuffer;
|
||||||
const decrypted = await window.crypto.subtle.decrypt(
|
try {
|
||||||
{
|
// Buffer should contain both the IV (fixed length) and encrypted data
|
||||||
name: "AES-GCM",
|
const iv = buffer.slice(0, IV_LENGTH_BYTES);
|
||||||
iv,
|
const encrypted = buffer.slice(IV_LENGTH_BYTES, buffer.byteLength);
|
||||||
},
|
decrypted = await decryptImported(iv, encrypted, privateKey);
|
||||||
key,
|
} catch (error) {
|
||||||
buffer,
|
// Fixed IV (old format, backward compatibility)
|
||||||
);
|
const fixedIv = new Uint8Array(IV_LENGTH_BYTES);
|
||||||
|
decrypted = await decryptImported(fixedIv, buffer, privateKey);
|
||||||
|
}
|
||||||
|
|
||||||
// We need to convert the decrypted array buffer to a string
|
// We need to convert the decrypted array buffer to a string
|
||||||
const string = new window.TextDecoder("utf-8").decode(
|
const string = new window.TextDecoder("utf-8").decode(
|
||||||
new Uint8Array(decrypted) as any,
|
new Uint8Array(decrypted) as any,
|
||||||
@ -263,9 +285,8 @@ export const exportToBackend = async (
|
|||||||
true, // extractable
|
true, // extractable
|
||||||
["encrypt", "decrypt"],
|
["encrypt", "decrypt"],
|
||||||
);
|
);
|
||||||
// The iv is set to 0. We are never going to reuse the same key so we don't
|
|
||||||
// need to have an iv. (I hope that's correct...)
|
const iv = createIV();
|
||||||
const iv = new Uint8Array(12);
|
|
||||||
// We use symmetric encryption. AES-GCM is the recommended algorithm and
|
// We use symmetric encryption. AES-GCM is the recommended algorithm and
|
||||||
// includes checks that the ciphertext has not been modified by an attacker.
|
// includes checks that the ciphertext has not been modified by an attacker.
|
||||||
const encrypted = await window.crypto.subtle.encrypt(
|
const encrypted = await window.crypto.subtle.encrypt(
|
||||||
@ -276,6 +297,11 @@ export const exportToBackend = async (
|
|||||||
key,
|
key,
|
||||||
encoded,
|
encoded,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Concatenate IV with encrypted data (IV does not have to be secret).
|
||||||
|
const payloadBlob = new Blob([iv.buffer, encrypted]);
|
||||||
|
const payload = await new Response(payloadBlob).arrayBuffer();
|
||||||
|
|
||||||
// We use jwk encoding to be able to extract just the base64 encoded key.
|
// We use jwk encoding to be able to extract just the base64 encoded key.
|
||||||
// We will hardcode the rest of the attributes when importing back the key.
|
// We will hardcode the rest of the attributes when importing back the key.
|
||||||
const exportedKey = await window.crypto.subtle.exportKey("jwk", key);
|
const exportedKey = await window.crypto.subtle.exportKey("jwk", key);
|
||||||
@ -283,7 +309,7 @@ export const exportToBackend = async (
|
|||||||
try {
|
try {
|
||||||
const response = await fetch(BACKEND_V2_POST, {
|
const response = await fetch(BACKEND_V2_POST, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: encrypted,
|
body: payload,
|
||||||
});
|
});
|
||||||
const json = await response.json();
|
const json = await response.json();
|
||||||
if (json.id) {
|
if (json.id) {
|
||||||
|
@ -61,7 +61,7 @@
|
|||||||
"architect": "Architect",
|
"architect": "Architect",
|
||||||
"artist": "Artist",
|
"artist": "Artist",
|
||||||
"cartoonist": "Cartoonist",
|
"cartoonist": "Cartoonist",
|
||||||
"fileTitle": "File title",
|
"fileTitle": "File name",
|
||||||
"colorPicker": "Color picker",
|
"colorPicker": "Color picker",
|
||||||
"canvasBackground": "Canvas background",
|
"canvasBackground": "Canvas background",
|
||||||
"drawingCanvas": "Drawing canvas",
|
"drawingCanvas": "Drawing canvas",
|
||||||
|
@ -12,12 +12,13 @@ The change should be grouped under one of the below section and must contain PR
|
|||||||
Please add the latest change on the top under the correct section.
|
Please add the latest change on the top under the correct section.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
## Unreleased
|
## 0.5.0 (2021-03-21)
|
||||||
|
|
||||||
## Excalidraw API
|
## Excalidraw API
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
|
- Set the target to `window.name` if present during excalidraw libraries installation so it opens in same tab for the host. If `window.name` is not set it will open in a new tab [#3299](https://github.com/excalidraw/excalidraw/pull/3299).
|
||||||
- Add `name` prop to indicate the name of the drawing which will be used when exporting the drawing. When supplied, the value takes precedence over `intialData.appState.name`, the `name` will be fully controlled by host app and the users won't be able to edit from within Excalidraw [#3273](https://github.com/excalidraw/excalidraw/pull/3273).
|
- Add `name` prop to indicate the name of the drawing which will be used when exporting the drawing. When supplied, the value takes precedence over `intialData.appState.name`, the `name` will be fully controlled by host app and the users won't be able to edit from within Excalidraw [#3273](https://github.com/excalidraw/excalidraw/pull/3273).
|
||||||
- Export API `setCanvasOffsets` via `ref` to set the offsets for Excalidraw[#3265](https://github.com/excalidraw/excalidraw/pull/3265).
|
- Export API `setCanvasOffsets` via `ref` to set the offsets for Excalidraw[#3265](https://github.com/excalidraw/excalidraw/pull/3265).
|
||||||
#### BREAKING CHANGE
|
#### BREAKING CHANGE
|
||||||
@ -36,6 +37,24 @@ Please add the latest change on the top under the correct section.
|
|||||||
- The class `Appearance_dark` is renamed to `theme--dark`.
|
- The class `Appearance_dark` is renamed to `theme--dark`.
|
||||||
- The class `Appearance_dark-background-none` is renamed to `theme--dark-background-none`.
|
- The class `Appearance_dark-background-none` is renamed to `theme--dark-background-none`.
|
||||||
|
|
||||||
|
## Excalidraw Library
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Support pasting file contents & always prefer system clip [#3257](https://github.com/excalidraw/excalidraw/pull/3257)
|
||||||
|
- Add label for name field and use input when editable in export dialog [#3286](https://github.com/excalidraw/excalidraw/pull/3286)
|
||||||
|
- Implement the Web Share Target API [#3230](https://github.com/excalidraw/excalidraw/pull/3230).
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- Don't show export and delete when library is empty [#3288](https://github.com/excalidraw/excalidraw/pull/3288)
|
||||||
|
- Overflow in textinput in export dialog [#3284](https://github.com/excalidraw/excalidraw/pull/3284).
|
||||||
|
- Bail on noop updates for newElementWith [#3279](https://github.com/excalidraw/excalidraw/pull/3279).
|
||||||
|
- Prevent State continuously updated when holding ctrl/cmd #3283
|
||||||
|
- Debounce flush not invoked if lastArgs not defined [#3281](https://github.com/excalidraw/excalidraw/pull/3281).
|
||||||
|
- Stop preventing canvas pointerdown/tapend events [#3207](https://github.com/excalidraw/excalidraw/pull/3207).
|
||||||
|
- Double scrollbar on modals [#3226](https://github.com/excalidraw/excalidraw/pull/3226).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 0.4.3 (2021-03-12)
|
## 0.4.3 (2021-03-12)
|
||||||
|
@ -28,7 +28,8 @@ If you want to load assets from a different path you can set a variable `window.
|
|||||||
|
|
||||||
[Try here](https://codesandbox.io/s/excalidraw-ehlz3).
|
[Try here](https://codesandbox.io/s/excalidraw-ehlz3).
|
||||||
|
|
||||||
### Usage
|
<details id="usage">
|
||||||
|
<summary><strong>Usage</strong></summary>
|
||||||
|
|
||||||
1. If you are using a Web bundler (for instance, Webpack), you can import it as an ES6 module as shown below
|
1. If you are using a Web bundler (for instance, Webpack), you can import it as an ES6 module as shown below
|
||||||
|
|
||||||
@ -163,6 +164,8 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To view the full example visit :point_down:
|
||||||
|
|
||||||
[](https://codesandbox.io/s/excalidraw-ehlz3?fontsize=14&hidenavigation=1&theme=dark)
|
[](https://codesandbox.io/s/excalidraw-ehlz3?fontsize=14&hidenavigation=1&theme=dark)
|
||||||
|
|
||||||
2. To use it in a browser directly:
|
2. To use it in a browser directly:
|
||||||
@ -341,6 +344,8 @@ const excalidrawWrapper = document.getElementById("app");
|
|||||||
ReactDOM.render(React.createElement(App), excalidrawWrapper);
|
ReactDOM.render(React.createElement(App), excalidrawWrapper);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To view the full example visit :point_down:
|
||||||
|
|
||||||
[](https://codesandbox.io/s/excalidraw-in-browser-tlqom?fontsize=14&hidenavigation=1&theme=dark)
|
[](https://codesandbox.io/s/excalidraw-in-browser-tlqom?fontsize=14&hidenavigation=1&theme=dark)
|
||||||
|
|
||||||
Since Excalidraw doesn't support server side rendering yet so you will have to make sure the component is rendered once host is mounted.
|
Since Excalidraw doesn't support server side rendering yet so you will have to make sure the component is rendered once host is mounted.
|
||||||
@ -356,7 +361,10 @@ export default function IndexPage() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Props
|
</details>
|
||||||
|
|
||||||
|
<details id="props">
|
||||||
|
<summary><strong>Props</strong></summary>
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
@ -527,19 +535,22 @@ This prop indicates whether the app is in `zen mode`. When supplied, the value t
|
|||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
### `libraryReturnUrl`
|
#### `libraryReturnUrl`
|
||||||
|
|
||||||
If supplied, this URL will be used when user tries to install a library from [libraries.excalidraw.com](https://libraries.excalidraw.com). Default to `window.location.origin`.
|
If supplied, this URL will be used when user tries to install a library from [libraries.excalidraw.com](https://libraries.excalidraw.com). Default to `window.location.origin`. To install the libraries in the same tab from which it was opened, you need to set `window.name` (to any alphanumeric string) — if it's not set it will open in a new tab.
|
||||||
|
|
||||||
### `theme`
|
#### `theme`
|
||||||
|
|
||||||
This prop controls Excalidraw's theme. When supplied, the value takes precedence over `intialData.appState.theme`, the theme will be fully controlled by the host app, and users won't be able to toggle it from within the app.
|
This prop controls Excalidraw's theme. When supplied, the value takes precedence over `intialData.appState.theme`, the theme will be fully controlled by the host app, and users won't be able to toggle it from within the app.
|
||||||
|
|
||||||
### `name`
|
#### `name`
|
||||||
|
|
||||||
This prop sets the name of the drawing which will be used when exporting the drawing. When supplied, the value takes precedence over `intialData.appState.name`, the `name` will be fully controlled by host app and the users won't be able to edit from within Excalidraw.
|
This prop sets the name of the drawing which will be used when exporting the drawing. When supplied, the value takes precedence over `intialData.appState.name`, the `name` will be fully controlled by host app and the users won't be able to edit from within Excalidraw.
|
||||||
|
|
||||||
### Extra API's
|
</details>
|
||||||
|
|
||||||
|
<details id="extra-apis">
|
||||||
|
<summary><strong>Extra API's</strong></summary>
|
||||||
|
|
||||||
#### `getSceneVersion`
|
#### `getSceneVersion`
|
||||||
|
|
||||||
@ -584,6 +595,9 @@ import { getElementsMap } from "@excalidraw/excalidraw";
|
|||||||
|
|
||||||
This function returns an object where each element is mapped to its id.
|
This function returns an object where each element is mapped to its id.
|
||||||
|
|
||||||
|
<details id="restore-utils">
|
||||||
|
<summary><strong>Restore utilities</strong></summary>
|
||||||
|
|
||||||
#### `restoreAppState`
|
#### `restoreAppState`
|
||||||
|
|
||||||
**_Signature_**
|
**_Signature_**
|
||||||
@ -632,7 +646,7 @@ import { restore } from "@excalidraw/excalidraw";
|
|||||||
|
|
||||||
This function makes sure elements and state is set to appropriate values and set to default value if not present. It is combination of [restoreElements](#restoreElements) and [restoreAppState](#restoreAppState)
|
This function makes sure elements and state is set to appropriate values and set to default value if not present. It is combination of [restoreElements](#restoreElements) and [restoreAppState](#restoreAppState)
|
||||||
|
|
||||||
**_The below APIs will be available in [next version](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/CHANGELOG.md#unreleased)_**
|
</details>
|
||||||
|
|
||||||
<details id="export-utils">
|
<details id="export-utils">
|
||||||
<summary><strong>Export utilities</strong></summary>
|
<summary><strong>Export utilities</strong></summary>
|
||||||
@ -721,3 +735,4 @@ This function returns a svg with the exported elements.
|
|||||||
| exportWithDarkMode | boolean | false | Indicates whether to export with dark mode |
|
| exportWithDarkMode | boolean | false | Indicates whether to export with dark mode |
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
</details>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@excalidraw/excalidraw",
|
"name": "@excalidraw/excalidraw",
|
||||||
"version": "0.4.3",
|
"version": "0.5.0",
|
||||||
"main": "dist/excalidraw.min.js",
|
"main": "dist/excalidraw.min.js",
|
||||||
"files": [
|
"files": [
|
||||||
"dist/*"
|
"dist/*"
|
||||||
@ -52,13 +52,13 @@
|
|||||||
"babel-loader": "8.2.2",
|
"babel-loader": "8.2.2",
|
||||||
"babel-plugin-transform-class-properties": "6.24.1",
|
"babel-plugin-transform-class-properties": "6.24.1",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"css-loader": "5.1.2",
|
"css-loader": "5.1.3",
|
||||||
"file-loader": "6.2.0",
|
"file-loader": "6.2.0",
|
||||||
"mini-css-extract-plugin": "1.3.9",
|
"mini-css-extract-plugin": "1.3.9",
|
||||||
"sass-loader": "11.0.1",
|
"sass-loader": "11.0.1",
|
||||||
"terser-webpack-plugin": "5.1.1",
|
"terser-webpack-plugin": "5.1.1",
|
||||||
"ts-loader": "8.0.18",
|
"ts-loader": "8.0.18",
|
||||||
"webpack": "5.24.3",
|
"webpack": "5.27.1",
|
||||||
"webpack-bundle-analyzer": "4.4.0",
|
"webpack-bundle-analyzer": "4.4.0",
|
||||||
"webpack-cli": "4.5.0"
|
"webpack-cli": "4.5.0"
|
||||||
},
|
},
|
||||||
|
@ -1497,10 +1497,10 @@ cross-spawn@^7.0.1, cross-spawn@^7.0.3:
|
|||||||
shebang-command "^2.0.0"
|
shebang-command "^2.0.0"
|
||||||
which "^2.0.1"
|
which "^2.0.1"
|
||||||
|
|
||||||
css-loader@5.1.2:
|
css-loader@5.1.3:
|
||||||
version "5.1.2"
|
version "5.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-5.1.2.tgz#b93dba498ec948b543b49d4fab5017205d4f5c3e"
|
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-5.1.3.tgz#87f6fc96816b20debe3cf682f85c7e56a963d0d1"
|
||||||
integrity sha512-T7vTXHSx0KrVEg/xjcl7G01RcVXpcw4OELwDPvkr7izQNny85A84dK3dqrczuEfBcu7Yg7mdTjJLSTibRUoRZg==
|
integrity sha512-CoPZvyh8sLiGARK3gqczpfdedbM74klGWurF2CsNZ2lhNaXdLIUks+3Mfax3WBeRuHoglU+m7KG/+7gY6G4aag==
|
||||||
dependencies:
|
dependencies:
|
||||||
camelcase "^6.2.0"
|
camelcase "^6.2.0"
|
||||||
cssesc "^3.0.0"
|
cssesc "^3.0.0"
|
||||||
@ -2663,10 +2663,10 @@ webpack-sources@^2.1.1:
|
|||||||
source-list-map "^2.0.1"
|
source-list-map "^2.0.1"
|
||||||
source-map "^0.6.1"
|
source-map "^0.6.1"
|
||||||
|
|
||||||
webpack@5.24.3:
|
webpack@5.27.1:
|
||||||
version "5.24.3"
|
version "5.27.1"
|
||||||
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.24.3.tgz#6ec0f5059f8d7c7961075fa553cfce7b7928acb3"
|
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.27.1.tgz#6808fb6e45e35290cdb8ae43c7a10884839a3079"
|
||||||
integrity sha512-x7lrWZ7wlWAdyKdML6YPvfVZkhD1ICuIZGODE5SzKJjqI9A4SpqGTjGJTc6CwaHqn19gGaoOR3ONJ46nYsn9rw==
|
integrity sha512-rxIDsPZ3Apl3JcqiemiLmWH+hAq04YeOXqvCxNZOnTp8ZgM9NEPtbu4CaMfMEf9KShnx/Ym8uLGmM6P4XnwCoA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/eslint-scope" "^3.7.0"
|
"@types/eslint-scope" "^3.7.0"
|
||||||
"@types/estree" "^0.0.46"
|
"@types/estree" "^0.0.46"
|
||||||
|
@ -44,11 +44,11 @@
|
|||||||
"babel-loader": "8.2.2",
|
"babel-loader": "8.2.2",
|
||||||
"babel-plugin-transform-class-properties": "6.24.1",
|
"babel-plugin-transform-class-properties": "6.24.1",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"css-loader": "5.1.2",
|
"css-loader": "5.1.3",
|
||||||
"file-loader": "6.2.0",
|
"file-loader": "6.2.0",
|
||||||
"sass-loader": "11.0.1",
|
"sass-loader": "11.0.1",
|
||||||
"ts-loader": "8.0.18",
|
"ts-loader": "8.0.18",
|
||||||
"webpack": "5.24.3",
|
"webpack": "5.27.1",
|
||||||
"webpack-bundle-analyzer": "4.4.0",
|
"webpack-bundle-analyzer": "4.4.0",
|
||||||
"webpack-cli": "4.5.0"
|
"webpack-cli": "4.5.0"
|
||||||
},
|
},
|
||||||
|
@ -1446,10 +1446,10 @@ cross-spawn@^7.0.1, cross-spawn@^7.0.3:
|
|||||||
shebang-command "^2.0.0"
|
shebang-command "^2.0.0"
|
||||||
which "^2.0.1"
|
which "^2.0.1"
|
||||||
|
|
||||||
css-loader@5.1.2:
|
css-loader@5.1.3:
|
||||||
version "5.1.2"
|
version "5.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-5.1.2.tgz#b93dba498ec948b543b49d4fab5017205d4f5c3e"
|
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-5.1.3.tgz#87f6fc96816b20debe3cf682f85c7e56a963d0d1"
|
||||||
integrity sha512-T7vTXHSx0KrVEg/xjcl7G01RcVXpcw4OELwDPvkr7izQNny85A84dK3dqrczuEfBcu7Yg7mdTjJLSTibRUoRZg==
|
integrity sha512-CoPZvyh8sLiGARK3gqczpfdedbM74klGWurF2CsNZ2lhNaXdLIUks+3Mfax3WBeRuHoglU+m7KG/+7gY6G4aag==
|
||||||
dependencies:
|
dependencies:
|
||||||
camelcase "^6.2.0"
|
camelcase "^6.2.0"
|
||||||
cssesc "^3.0.0"
|
cssesc "^3.0.0"
|
||||||
@ -2595,10 +2595,10 @@ webpack-sources@^2.1.1:
|
|||||||
source-list-map "^2.0.1"
|
source-list-map "^2.0.1"
|
||||||
source-map "^0.6.1"
|
source-map "^0.6.1"
|
||||||
|
|
||||||
webpack@5.24.3:
|
webpack@5.27.1:
|
||||||
version "5.24.3"
|
version "5.27.1"
|
||||||
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.24.3.tgz#6ec0f5059f8d7c7961075fa553cfce7b7928acb3"
|
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.27.1.tgz#6808fb6e45e35290cdb8ae43c7a10884839a3079"
|
||||||
integrity sha512-x7lrWZ7wlWAdyKdML6YPvfVZkhD1ICuIZGODE5SzKJjqI9A4SpqGTjGJTc6CwaHqn19gGaoOR3ONJ46nYsn9rw==
|
integrity sha512-rxIDsPZ3Apl3JcqiemiLmWH+hAq04YeOXqvCxNZOnTp8ZgM9NEPtbu4CaMfMEf9KShnx/Ym8uLGmM6P4XnwCoA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/eslint-scope" "^3.7.0"
|
"@types/eslint-scope" "^3.7.0"
|
||||||
"@types/estree" "^0.0.46"
|
"@types/estree" "^0.0.46"
|
||||||
|
@ -3,6 +3,7 @@ import { render, waitFor } from "./test-utils";
|
|||||||
import ExcalidrawApp from "../excalidraw-app";
|
import ExcalidrawApp from "../excalidraw-app";
|
||||||
import { API } from "./helpers/api";
|
import { API } from "./helpers/api";
|
||||||
import { getDefaultAppState } from "../appState";
|
import { getDefaultAppState } from "../appState";
|
||||||
|
import { EXPORT_DATA_TYPES } from "../constants";
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
|
|
||||||
@ -29,7 +30,7 @@ describe("appState", () => {
|
|||||||
new Blob(
|
new Blob(
|
||||||
[
|
[
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
type: "excalidraw",
|
type: EXPORT_DATA_TYPES.excalidraw,
|
||||||
appState: {
|
appState: {
|
||||||
viewBackgroundColor: "#000",
|
viewBackgroundColor: "#000",
|
||||||
},
|
},
|
||||||
|
@ -111,11 +111,11 @@ describe("<Excalidraw/>", () => {
|
|||||||
const { container } = await render(<Excalidraw />);
|
const { container } = await render(<Excalidraw />);
|
||||||
|
|
||||||
fireEvent.click(queryByTestId(container, "export-button")!);
|
fireEvent.click(queryByTestId(container, "export-button")!);
|
||||||
const textInput = document.querySelector(
|
const textInput: HTMLInputElement | null = document.querySelector(
|
||||||
".ExportDialog__name .TextInput",
|
".ExportDialog__name .TextInput",
|
||||||
);
|
);
|
||||||
expect(textInput?.textContent).toContain(`${t("labels.untitled")}`);
|
expect(textInput?.value).toContain(`${t("labels.untitled")}`);
|
||||||
expect(textInput?.hasAttribute("data-type")).toBe(true);
|
expect(textInput?.nodeName).toBe("INPUT");
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set the name and not allow editing when the name prop is present"', async () => {
|
it('should set the name and not allow editing when the name prop is present"', async () => {
|
||||||
@ -127,8 +127,7 @@ describe("<Excalidraw/>", () => {
|
|||||||
".ExportDialog__name .TextInput--readonly",
|
".ExportDialog__name .TextInput--readonly",
|
||||||
);
|
);
|
||||||
expect(textInput?.textContent).toEqual(name);
|
expect(textInput?.textContent).toEqual(name);
|
||||||
|
expect(textInput?.nodeName).toBe("SPAN");
|
||||||
expect(textInput?.hasAttribute("data-type")).toBe(false);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -6,6 +6,7 @@ import { API } from "./helpers/api";
|
|||||||
import { getDefaultAppState } from "../appState";
|
import { getDefaultAppState } from "../appState";
|
||||||
import { waitFor } from "@testing-library/react";
|
import { waitFor } from "@testing-library/react";
|
||||||
import { createUndoAction, createRedoAction } from "../actions/actionHistory";
|
import { createUndoAction, createRedoAction } from "../actions/actionHistory";
|
||||||
|
import { EXPORT_DATA_TYPES } from "../constants";
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
|
|
||||||
@ -76,7 +77,7 @@ describe("history", () => {
|
|||||||
new Blob(
|
new Blob(
|
||||||
[
|
[
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
type: "excalidraw",
|
type: EXPORT_DATA_TYPES.excalidraw,
|
||||||
appState: {
|
appState: {
|
||||||
...getDefaultAppState(),
|
...getDefaultAppState(),
|
||||||
viewBackgroundColor: "#000",
|
viewBackgroundColor: "#000",
|
||||||
|
@ -607,7 +607,7 @@ describe("regression tests", () => {
|
|||||||
it("updates fontSize & fontFamily appState", () => {
|
it("updates fontSize & fontFamily appState", () => {
|
||||||
UI.clickTool("text");
|
UI.clickTool("text");
|
||||||
expect(h.state.currentItemFontFamily).toEqual(1); // Virgil
|
expect(h.state.currentItemFontFamily).toEqual(1); // Virgil
|
||||||
fireEvent.click(screen.getByText(/code/i));
|
fireEvent.click(screen.getByTitle(/code/i));
|
||||||
expect(h.state.currentItemFontFamily).toEqual(3); // Cascadia
|
expect(h.state.currentItemFontFamily).toEqual(3); // Cascadia
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user