excalidraw/src/components/ExportDialog.tsx
Jed Fox 7a7a73b78d
Initial support for mobile devices (#787)
* Initial support for mobile devices

No editing yet, but UI looks nice and you can open the canvas menu

* Add support for editing shape color, etc

* Allow the mobile menus to cover the shape selector

* Hopefully fix test error

* Fix touch on canvas

* Fix safe area handling & remove unused Island
2020-02-20 15:44:38 -08:00

263 lines
7.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import "./ExportDialog.css";
import React, { useState, useEffect, useRef } from "react";
import { Modal } from "./Modal";
import { ToolButton } from "./ToolButton";
import { clipboard, exportFile, link } from "./icons";
import { Island } from "./Island";
import { ExcalidrawElement } from "../element/types";
import { AppState } from "../types";
import { exportToCanvas } from "../scene/export";
import { ActionsManagerInterface } from "../actions/types";
import Stack from "./Stack";
import { t } from "../i18n";
import { KEYS } from "../keys";
import { probablySupportsClipboardBlob } from "../clipboard";
import { getSelectedElements, isSomeElementSelected } from "../scene";
import useIsMobile from "../is-mobile";
const scales = [1, 2, 3];
const defaultScale = scales.includes(devicePixelRatio) ? devicePixelRatio : 1;
type ExportCB = (
elements: readonly ExcalidrawElement[],
scale?: number,
) => void;
function ExportModal({
elements,
appState,
exportPadding = 10,
actionManager,
onExportToPng,
onExportToSvg,
onExportToClipboard,
onExportToBackend,
onCloseRequest,
}: {
appState: AppState;
elements: readonly ExcalidrawElement[];
exportPadding?: number;
actionManager: ActionsManagerInterface;
onExportToPng: ExportCB;
onExportToSvg: ExportCB;
onExportToClipboard: ExportCB;
onExportToBackend: ExportCB;
onCloseRequest: () => void;
}) {
const someElementIsSelected = isSomeElementSelected(elements);
const [scale, setScale] = useState(defaultScale);
const [exportSelected, setExportSelected] = useState(someElementIsSelected);
const previewRef = useRef<HTMLDivElement>(null);
const { exportBackground, viewBackgroundColor } = appState;
const pngButton = useRef<HTMLButtonElement>(null);
const closeButton = useRef<HTMLButtonElement>(null);
const onlySelectedInput = useRef<HTMLInputElement>(null);
const exportedElements = exportSelected
? getSelectedElements(elements)
: elements;
useEffect(() => {
setExportSelected(someElementIsSelected);
}, [someElementIsSelected]);
useEffect(() => {
const previewNode = previewRef.current;
const canvas = exportToCanvas(exportedElements, {
exportBackground,
viewBackgroundColor,
exportPadding,
scale,
});
previewNode?.appendChild(canvas);
return () => {
previewNode?.removeChild(canvas);
};
}, [
exportedElements,
exportBackground,
exportPadding,
viewBackgroundColor,
scale,
]);
useEffect(() => {
pngButton.current?.focus();
}, []);
function handleKeyDown(e: React.KeyboardEvent) {
if (e.key === KEYS.TAB) {
const { activeElement } = document;
if (e.shiftKey) {
if (activeElement === pngButton.current) {
closeButton.current?.focus();
e.preventDefault();
}
} else {
if (activeElement === closeButton.current) {
pngButton.current?.focus();
e.preventDefault();
}
if (activeElement === onlySelectedInput.current) {
closeButton.current?.focus();
e.preventDefault();
}
}
}
}
return (
<div className="ExportDialog__dialog" onKeyDown={handleKeyDown}>
<Island padding={4}>
<button
className="ExportDialog__close"
onClick={onCloseRequest}
aria-label={t("buttons.close")}
ref={closeButton}
>
</button>
<h2 id="export-title">{t("buttons.export")}</h2>
<div className="ExportDialog__preview" ref={previewRef}></div>
<div className="ExportDialog__actions">
<Stack.Col gap={1}>
<Stack.Row gap={2}>
<ToolButton
type="button"
label="PNG"
title={t("buttons.exportToPng")}
aria-label={t("buttons.exportToPng")}
onClick={() => onExportToPng(exportedElements, scale)}
ref={pngButton}
/>
<ToolButton
type="button"
label="SVG"
title={t("buttons.exportToSvg")}
aria-label={t("buttons.exportToSvg")}
onClick={() => onExportToSvg(exportedElements, scale)}
/>
{probablySupportsClipboardBlob && (
<ToolButton
type="button"
icon={clipboard}
title={t("buttons.copyToClipboard")}
aria-label={t("buttons.copyToClipboard")}
onClick={() => onExportToClipboard(exportedElements, scale)}
/>
)}
<ToolButton
type="button"
icon={link}
title={t("buttons.getShareableLink")}
aria-label={t("buttons.getShareableLink")}
onClick={() => onExportToBackend(exportedElements)}
/>
</Stack.Row>
</Stack.Col>
{actionManager.renderAction("changeProjectName")}
<Stack.Col gap={1}>
<div className="ExportDialog__scales">
<Stack.Row gap={2} align="baseline">
{scales.map(s => (
<ToolButton
key={s}
size="s"
type="radio"
icon={`x${s}`}
name="export-canvas-scale"
aria-label={`Scale ${s} x`}
id="export-canvas-scale"
checked={scale === s}
onChange={() => setScale(s)}
/>
))}
</Stack.Row>
</div>
{actionManager.renderAction("changeExportBackground")}
{someElementIsSelected && (
<div>
<label>
<input
type="checkbox"
checked={exportSelected}
onChange={e => setExportSelected(e.currentTarget.checked)}
ref={onlySelectedInput}
/>{" "}
{t("labels.onlySelected")}
</label>
</div>
)}
</Stack.Col>
</div>
</Island>
</div>
);
}
export function ExportDialog({
elements,
appState,
exportPadding = 10,
actionManager,
onExportToPng,
onExportToSvg,
onExportToClipboard,
onExportToBackend,
}: {
appState: AppState;
elements: readonly ExcalidrawElement[];
exportPadding?: number;
actionManager: ActionsManagerInterface;
onExportToPng: ExportCB;
onExportToSvg: ExportCB;
onExportToClipboard: ExportCB;
onExportToBackend: ExportCB;
}) {
const [modalIsShown, setModalIsShown] = useState(false);
const triggerButton = useRef<HTMLButtonElement>(null);
const handleClose = React.useCallback(() => {
setModalIsShown(false);
triggerButton.current?.focus();
}, []);
return (
<>
<ToolButton
onClick={() => setModalIsShown(true)}
icon={exportFile}
type="button"
aria-label={t("buttons.export")}
showAriaLabel={useIsMobile()}
title={t("buttons.export")}
ref={triggerButton}
/>
{modalIsShown && (
<Modal
maxWidth={800}
onCloseRequest={handleClose}
labelledBy="export-title"
>
<ExportModal
elements={elements}
appState={appState}
exportPadding={exportPadding}
actionManager={actionManager}
onExportToPng={onExportToPng}
onExportToSvg={onExportToSvg}
onExportToClipboard={onExportToClipboard}
onExportToBackend={onExportToBackend}
onCloseRequest={handleClose}
/>
</Modal>
)}
</>
);
}