excalidraw/src/components/ImageExportDialog.tsx
Barnabás Molnár 6334bd832f
feat: editor redesign 🔥 (#5780)
* Placed eraser into shape switcher (top toolbar).
Redesigned top toolbar.

* Redesigned zoom and undo-redo buttons.

* Started redesigning left toolbar.

* Redesigned help dialog.

* Colour picker now somewhat in line with new design

* [WIP] Changed a bunch of icons.
TODO: organise new icons.

* [WIP] Organised a bunch of icons. Still some to do

* [WIP] Started working on hamburger menu.

* Fixed some bugs with hamburger menu.

* Menu and left toolbar positioning.

* Added some more items to hamburger menu.

* Changed some icons.

* Modal/dialog styling & bunch of fixes.

* Some more dialog improvements & fixes.

* Mobile menu changes.

* Menu can now be closed with outside click.

* Collab avatars and button changes.

* Icon sizing. Left toolbar positioning.

* Implemented welcome screen rendering logic.

* [WIP] Welcome screen content + design.

* Some more welcome screen content and design.

* Merge fixes.

* Tweaked icon set.

* Welcome screen darkmode fix.

* Content updates.

* Various small fixes & adjustments.
Moved language selection into menu.
Fixed some problematic icons.
Slightly moved encryption icon.

* Sidebar header redesign.

* Libraries content rendering logic + some styling.

* Somem more library sidebar styling.

* Publish library dialog styling.

* scroll-back-to-content btn styling

* ColorPicker positioning.

* Library button styling.

* ColorPicker positioning "fix".

* Misc adjustments.

* PenMode button changes.

* Trying to make mobile somewhat usable.

* Added a couple of icons.

* Added some shortcuts.

* Prevent welcome screen flickering.
Fix issue with welcome screen interactivity.
Don't show sidebar button when docked.

* Icon sizing on smaller screens.

* Sidebar styling changes.

* Alignment button... well... alignments.

* Fix inconsistent padding in left toolbar.

* HintViewer changes.

* Hamburger menu changes.

* Move encryption badge back to its original pos.

* Arrowhead changes.
Active state, colours + stronger shadow.

* Added new custom font.

* Fixed bug with library button not rendering.

* Fixed issue with lang selection colours.

* Add tooltips for undo, redo.

* Address some dark mode contrast issues.

* (Re)introduce counter for selectedItems in sidebar

* [WIP] Tweaked bounding box colour & padding.

* Dashed bounding box for remote clients.

* Some more bounding box tweaks.

* Removed docking animation for now...

* Address some RTL issues.

* Welcome screen responsiveness.

* use lighter selection color in dark mode & align naming

* use rounded corners for transform handles

* use lighter gray for welcomeScreen text in dark mode

* disable selection on dialog buttons

* change selection button icon

* fix library item width being flexible

* library: visually align spinner with first section heading

* lint

* fix scrollbar color in dark mode & make thinner

* adapt properties panel max-height

* add shrotcut label to save-to-current-file

* fix unrelated `useOutsideClick` firing for active modal

* add promo color to e+ menu item

* fix type

* lowered button size

* fix transform handles raidus not accounting for zoom

* attempt fix for excal logo on safari

* final fix for excal logo on safari

* fixing fhd resolution button sized

* remove TODO shortcut

* Collab related styling changes.
Expanding avatar list no longer offsets top toolbar.
Added active state & collaborator count badge for collab button.

* Tweaked collab button active colours.

* Added active style to collab btn in hamburger menu

* Remove unnecessary comment.

* Added back promo link for non (signed in) E+ users

* Go to E+ button added for signed in E+ users.

* Close menu & dropdown on modal close.

* tweak icons & fix rendering on smaller sizes [part one]

* align welcomeScreen icons with other UI

* switch icon resize mq to `device-width`

* disable welcomeScreen items `:hover` when selecting on canvas

* change selection box color and style

* reduce selection padding and fix group selection styling

* improve collab cursor styling

- make name borders round
- hide status when "active"
- remove black/gray colors

* add Twitter to hamburger menu

* align collab button

* add shortcut for image export dialog

* revert yarn.lock

* fix more tabler icons

* slightly better-looking penMode button

* change penMode button & tooltip

* revert hamburger menu icon

* align padding on lang picker & canvas bg

* updated robot txt to allow twitter bot and fb bot

* added new OG and tweaked the OG state

* add tooltip to collab button

* align style for scroll-to-content button

* fix pointer-events around toolbar

* fix decor arrow positioning and RTL

* fix welcomeScreen-item active state in dark mode

* change `load` button copy

* prevent shadow anim when opening a docked sidebar

* update E+ links ga params

* show redirect-to-eplus welcomeScreen subheading for signed-in users

* make more generic

* add ga for eplus redirect button

* change copy and icons for hamburger export buttons

* update snaps

* trim the username to account for trailing spaces

* tweaks around decor breakpoints

* fix linear element editor test

* remove .env change

* remove `it.only`

Co-authored-by: dwelle <luzar.david@gmail.com>
Co-authored-by: Maielo <maielo.mv@gmail.com>
Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
2022-11-01 17:29:58 +01:00

264 lines
7.5 KiB
TypeScript

import React, { useEffect, useRef, useState } from "react";
import { render, unmountComponentAtNode } from "react-dom";
import { probablySupportsClipboardBlob } from "../clipboard";
import { canvasToBlob } from "../data/blob";
import { NonDeletedExcalidrawElement } from "../element/types";
import { CanvasError } from "../errors";
import { t } from "../i18n";
import { getSelectedElements, isSomeElementSelected } from "../scene";
import { exportToCanvas } from "../scene/export";
import { AppState, BinaryFiles } from "../types";
import { Dialog } from "./Dialog";
import { clipboard } from "./icons";
import Stack from "./Stack";
import "./ExportDialog.scss";
import OpenColor from "open-color";
import { CheckboxItem } from "./CheckboxItem";
import { DEFAULT_EXPORT_PADDING } from "../constants";
import { nativeFileSystemSupported } from "../data/filesystem";
import { ActionManager } from "../actions/manager";
const supportsContextFilters =
"filter" in document.createElement("canvas").getContext("2d")!;
export const ErrorCanvasPreview = () => {
return (
<div>
<h3>{t("canvasError.cannotShowPreview")}</h3>
<p>
<span>{t("canvasError.canvasTooBig")}</span>
</p>
<em>({t("canvasError.canvasTooBigTip")})</em>
</div>
);
};
const renderPreview = (
content: HTMLCanvasElement | Error,
previewNode: HTMLDivElement,
) => {
unmountComponentAtNode(previewNode);
previewNode.innerHTML = "";
if (content instanceof HTMLCanvasElement) {
previewNode.appendChild(content);
} else {
render(<ErrorCanvasPreview />, previewNode);
}
};
export type ExportCB = (
elements: readonly NonDeletedExcalidrawElement[],
scale?: number,
) => void;
const ExportButton: React.FC<{
color: keyof OpenColor;
onClick: () => void;
title: string;
shade?: number;
children?: React.ReactNode;
}> = ({ children, title, onClick, color, shade = 6 }) => {
return (
<button
className="ExportDialog-imageExportButton"
style={{
["--button-color" as any]: OpenColor[color][shade],
["--button-color-darker" as any]: OpenColor[color][shade + 1],
["--button-color-darkest" as any]: OpenColor[color][shade + 2],
}}
title={title}
aria-label={title}
onClick={onClick}
>
{children}
</button>
);
};
const ImageExportModal = ({
elements,
appState,
files,
exportPadding = DEFAULT_EXPORT_PADDING,
actionManager,
onExportToPng,
onExportToSvg,
onExportToClipboard,
}: {
appState: AppState;
elements: readonly NonDeletedExcalidrawElement[];
files: BinaryFiles;
exportPadding?: number;
actionManager: ActionManager;
onExportToPng: ExportCB;
onExportToSvg: ExportCB;
onExportToClipboard: ExportCB;
onCloseRequest: () => void;
}) => {
const someElementIsSelected = isSomeElementSelected(elements, appState);
const [exportSelected, setExportSelected] = useState(someElementIsSelected);
const previewRef = useRef<HTMLDivElement>(null);
const { exportBackground, viewBackgroundColor } = appState;
const exportedElements = exportSelected
? getSelectedElements(elements, appState, true)
: elements;
useEffect(() => {
setExportSelected(someElementIsSelected);
}, [someElementIsSelected]);
useEffect(() => {
const previewNode = previewRef.current;
if (!previewNode) {
return;
}
exportToCanvas(exportedElements, appState, files, {
exportBackground,
viewBackgroundColor,
exportPadding,
})
.then((canvas) => {
// if converting to blob fails, there's some problem that will
// likely prevent preview and export (e.g. canvas too big)
return canvasToBlob(canvas).then(() => {
renderPreview(canvas, previewNode);
});
})
.catch((error) => {
console.error(error);
renderPreview(new CanvasError(), previewNode);
});
}, [
appState,
files,
exportedElements,
exportBackground,
exportPadding,
viewBackgroundColor,
]);
return (
<div className="ExportDialog">
<div className="ExportDialog__preview" ref={previewRef} />
{supportsContextFilters &&
actionManager.renderAction("exportWithDarkMode")}
<div style={{ display: "grid", gridTemplateColumns: "1fr" }}>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(190px, 1fr))",
// dunno why this is needed, but when the items wrap it creates
// an overflow
overflow: "hidden",
}}
>
{actionManager.renderAction("changeExportBackground")}
{someElementIsSelected && (
<CheckboxItem
checked={exportSelected}
onChange={(checked) => setExportSelected(checked)}
>
{t("labels.onlySelected")}
</CheckboxItem>
)}
{actionManager.renderAction("changeExportEmbedScene")}
</div>
</div>
<div style={{ display: "flex", alignItems: "center", marginTop: ".6em" }}>
<Stack.Row gap={2}>
{actionManager.renderAction("changeExportScale")}
</Stack.Row>
<p style={{ marginLeft: "1em", userSelect: "none" }}>
{t("buttons.scale")}
</p>
</div>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
margin: ".6em 0",
}}
>
{!nativeFileSystemSupported &&
actionManager.renderAction("changeProjectName")}
</div>
<Stack.Row gap={2} justifyContent="center" style={{ margin: "2em 0" }}>
<ExportButton
color="indigo"
title={t("buttons.exportToPng")}
aria-label={t("buttons.exportToPng")}
onClick={() => onExportToPng(exportedElements)}
>
PNG
</ExportButton>
<ExportButton
color="red"
title={t("buttons.exportToSvg")}
aria-label={t("buttons.exportToSvg")}
onClick={() => onExportToSvg(exportedElements)}
>
SVG
</ExportButton>
{probablySupportsClipboardBlob && (
<ExportButton
title={t("buttons.copyPngToClipboard")}
onClick={() => onExportToClipboard(exportedElements)}
color="gray"
shade={7}
>
{clipboard}
</ExportButton>
)}
</Stack.Row>
</div>
);
};
export const ImageExportDialog = ({
elements,
appState,
setAppState,
files,
exportPadding = DEFAULT_EXPORT_PADDING,
actionManager,
onExportToPng,
onExportToSvg,
onExportToClipboard,
}: {
appState: AppState;
setAppState: React.Component<any, AppState>["setState"];
elements: readonly NonDeletedExcalidrawElement[];
files: BinaryFiles;
exportPadding?: number;
actionManager: ActionManager;
onExportToPng: ExportCB;
onExportToSvg: ExportCB;
onExportToClipboard: ExportCB;
}) => {
const handleClose = React.useCallback(() => {
setAppState({ openDialog: null });
}, [setAppState]);
return (
<>
{appState.openDialog === "imageExport" && (
<Dialog onCloseRequest={handleClose} title={t("buttons.exportImage")}>
<ImageExportModal
elements={elements}
appState={appState}
files={files}
exportPadding={exportPadding}
actionManager={actionManager}
onExportToPng={onExportToPng}
onExportToSvg={onExportToSvg}
onExportToClipboard={onExportToClipboard}
onCloseRequest={handleClose}
/>
</Dialog>
)}
</>
);
};