Compare commits

...

14 Commits

13 changed files with 75 additions and 3 deletions

View File

@ -18,11 +18,12 @@ export const actionChangeProjectName = register({
trackEvent("change", "title"); trackEvent("change", "title");
return { appState: { ...appState, name: value }, commitToHistory: false }; return { appState: { ...appState, name: value }, commitToHistory: false };
}, },
PanelComponent: ({ appState, updateData }) => ( PanelComponent: ({ appState, updateData, appProps }) => (
<ProjectName <ProjectName
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"}
/> />
), ),
}); });

View File

@ -122,6 +122,7 @@ export class ActionManager implements ActionsManagerInterface {
appState={this.getAppState()} appState={this.getAppState()}
updateData={updateData} updateData={updateData}
id={id} id={id}
appProps={this.app.props}
/> />
); );
} }

View File

@ -1,6 +1,6 @@
import React from "react"; import React from "react";
import { ExcalidrawElement } from "../element/types"; import { ExcalidrawElement } from "../element/types";
import { AppState } from "../types"; import { AppState, ExcalidrawProps } from "../types";
/** if false, the action should be prevented */ /** if false, the action should be prevented */
export type ActionResult = export type ActionResult =
@ -94,6 +94,7 @@ export interface Action {
elements: readonly ExcalidrawElement[]; elements: readonly ExcalidrawElement[];
appState: AppState; appState: AppState;
updateData: (formData?: any) => void; updateData: (formData?: any) => void;
appProps: ExcalidrawProps;
id?: string; id?: string;
}>; }>;
perform: ActionFn; perform: ActionFn;

View File

@ -303,6 +303,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
zenModeEnabled = false, zenModeEnabled = false,
gridModeEnabled = false, gridModeEnabled = false,
theme = defaultAppState.theme, theme = defaultAppState.theme,
name = defaultAppState.name,
} = props; } = props;
this.state = { this.state = {
...defaultAppState, ...defaultAppState,
@ -314,6 +315,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
viewModeEnabled, viewModeEnabled,
zenModeEnabled, zenModeEnabled,
gridSize: gridModeEnabled ? GRID_SIZE : null, gridSize: gridModeEnabled ? GRID_SIZE : null,
name,
}; };
if (excalidrawRef) { if (excalidrawRef) {
const readyPromise = const readyPromise =
@ -523,6 +525,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;
if (typeof this.props.viewModeEnabled !== "undefined") { if (typeof this.props.viewModeEnabled !== "undefined") {
viewModeEnabled = this.props.viewModeEnabled; viewModeEnabled = this.props.viewModeEnabled;
@ -540,6 +543,10 @@ class App extends React.Component<ExcalidrawProps, AppState> {
theme = this.props.theme; theme = this.props.theme;
} }
if (typeof this.props.name !== "undefined") {
name = this.props.name;
}
this.setState( this.setState(
(state) => { (state) => {
// using Object.assign instead of spread to fool TS 4.2.2+ into // using Object.assign instead of spread to fool TS 4.2.2+ into
@ -556,6 +563,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
zenModeEnabled, zenModeEnabled,
gridSize, gridSize,
theme, theme,
name,
}); });
}, },
() => { () => {
@ -890,6 +898,13 @@ class App extends React.Component<ExcalidrawProps, AppState> {
gridSize: this.props.gridModeEnabled ? GRID_SIZE : null, gridSize: this.props.gridModeEnabled ? GRID_SIZE : null,
}); });
} }
if (this.props.name && prevProps.name !== this.props.name) {
this.setState({
name: this.props.name,
});
}
document document
.querySelector(".excalidraw") .querySelector(".excalidraw")
?.classList.toggle("theme--dark", this.state.theme === "dark"); ?.classList.toggle("theme--dark", this.state.theme === "dark");

View File

@ -34,6 +34,14 @@
.TextInput { .TextInput {
height: calc(1rem - 3px); height: calc(1rem - 3px);
&--readonly {
background: none;
border: none;
&:hover {
background: none;
}
}
} }
} }

View File

@ -257,6 +257,7 @@ export const ExportDialog = ({
onClick={() => { onClick={() => {
setModalIsShown(true); setModalIsShown(true);
}} }}
data-testid="export-button"
icon={exportFile} icon={exportFile}
type="button" type="button"
aria-label={t("buttons.export")} aria-label={t("buttons.export")}

View File

@ -7,6 +7,7 @@ type Props = {
value: string; value: string;
onChange: (value: string) => void; onChange: (value: string) => void;
label: string; label: string;
isNameEditable: boolean;
}; };
export class ProjectName extends Component<Props> { export class ProjectName extends Component<Props> {
@ -43,7 +44,7 @@ export class ProjectName extends Component<Props> {
}; };
public render() { public render() {
return ( return this.props.isNameEditable ? (
<span <span
suppressContentEditableWarning suppressContentEditableWarning
ref={this.makeEditable} ref={this.makeEditable}
@ -57,6 +58,13 @@ export class ProjectName extends Component<Props> {
> >
{this.props.value} {this.props.value}
</span> </span>
) : (
<span
className="TextInput TextInput--readonly"
aria-label={this.props.label}
>
{this.props.value}
</span>
); );
} }
} }

View File

@ -58,6 +58,7 @@ export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
"ToolIcon--selected": props.selected, "ToolIcon--selected": props.selected,
}, },
)} )}
data-testid={props["data-testid"]}
hidden={props.hidden} hidden={props.hidden}
title={props.title} title={props.title}
aria-label={props["aria-label"]} aria-label={props["aria-label"]}

View File

@ -18,6 +18,7 @@ Please add the latest change on the top under the correct section.
### Features ### Features
- Add `name` prop which indicates 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
- `offsetLeft` and `offsetTop` props have been removed now so you have to use the `setCanvasOffsets` via `ref` to achieve the same. - `offsetLeft` and `offsetTop` props have been removed now so you have to use the `setCanvasOffsets` via `ref` to achieve the same.

View File

@ -376,6 +376,7 @@ export default function IndexPage() {
| [`gridModeEnabled`](#gridModeEnabled) | boolean | | This implies if the grid mode is enabled | | [`gridModeEnabled`](#gridModeEnabled) | boolean | | This implies if the grid mode is enabled |
| [`libraryReturnUrl`](#libraryReturnUrl) | string | | What URL should [libraries.excalidraw.com](https://libraries.excalidraw.com) be installed to | | [`libraryReturnUrl`](#libraryReturnUrl) | string | | What URL should [libraries.excalidraw.com](https://libraries.excalidraw.com) be installed to |
| [`theme`](#theme) | `light` or `dark` | | The theme of the Excalidraw component | | [`theme`](#theme) | `light` or `dark` | | The theme of the Excalidraw component |
| [`name`](#name) | string | | Name of the drawing |
#### `width` #### `width`
@ -534,6 +535,10 @@ If supplied, this URL will be used when user tries to install a library from [li
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`
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 ### Extra API's
#### `getSceneVersion` #### `getSceneVersion`

View File

@ -29,6 +29,7 @@ const Excalidraw = (props: ExcalidrawProps) => {
gridModeEnabled, gridModeEnabled,
libraryReturnUrl, libraryReturnUrl,
theme, theme,
name,
} = props; } = props;
useEffect(() => { useEffect(() => {
@ -69,6 +70,7 @@ const Excalidraw = (props: ExcalidrawProps) => {
gridModeEnabled={gridModeEnabled} gridModeEnabled={gridModeEnabled}
libraryReturnUrl={libraryReturnUrl} libraryReturnUrl={libraryReturnUrl}
theme={theme} theme={theme}
name={name}
/> />
</IsMobileProvider> </IsMobileProvider>
</InitializeApp> </InitializeApp>

View File

@ -3,6 +3,7 @@ import { fireEvent, GlobalTestState, render } from "./test-utils";
import Excalidraw from "../packages/excalidraw/index"; import Excalidraw from "../packages/excalidraw/index";
import { queryByText, queryByTestId } from "@testing-library/react"; import { queryByText, queryByTestId } from "@testing-library/react";
import { GRID_SIZE } from "../constants"; import { GRID_SIZE } from "../constants";
import { t } from "../i18n";
const { h } = window; const { h } = window;
@ -104,4 +105,30 @@ describe("<Excalidraw/>", () => {
expect(queryByTestId(container, "toggle-dark-mode")).toBe(null); expect(queryByTestId(container, "toggle-dark-mode")).toBe(null);
}); });
}); });
describe("Test name prop", () => {
it('should allow editing name when the name prop is "undefined"', async () => {
const { container } = await render(<Excalidraw />);
fireEvent.click(queryByTestId(container, "export-button")!);
const textInput = document.querySelector(
".ExportDialog__name .TextInput",
);
expect(textInput?.textContent).toContain(`${t("labels.untitled")}`);
expect(textInput?.hasAttribute("data-type")).toBe(true);
});
it('should set the name and not allow editing when the name prop is present"', async () => {
const name = "test";
const { container } = await render(<Excalidraw name={name} />);
await fireEvent.click(queryByTestId(container, "export-button")!);
const textInput = document.querySelector(
".ExportDialog__name .TextInput",
);
expect(textInput?.textContent).toEqual(name);
expect(textInput?.hasAttribute("data-type")).toBe(false);
});
});
}); });

View File

@ -187,6 +187,7 @@ export interface ExcalidrawProps {
gridModeEnabled?: boolean; gridModeEnabled?: boolean;
libraryReturnUrl?: string; libraryReturnUrl?: string;
theme?: "dark" | "light"; theme?: "dark" | "light";
name?: string;
} }
export type SceneData = { export type SceneData = {