123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282 |
- import "./ExportDialog.css";
- import React, { useState, useEffect, useRef } from "react";
- import { Modal } from "./Modal";
- import { ToolButton } from "./ToolButton";
- import { clipboard, exportFile, downloadFile, svgFile, link } from "./icons";
- import { Island } from "./Island";
- import { ExcalidrawElement } from "../element/types";
- import { AppState } from "../types";
- import { exportToCanvas } from "../scene/export";
- import { ActionsManagerInterface, UpdaterFn } from "../actions/types";
- import Stack from "./Stack";
- import { useTranslation } from "react-i18next";
- import { KEYS } from "../keys";
- const probablySupportsClipboard =
- "toBlob" in HTMLCanvasElement.prototype &&
- "clipboard" in navigator &&
- "write" in navigator.clipboard &&
- "ClipboardItem" in window;
- 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,
- syncActionResult,
- onExportToPng,
- onExportToSvg,
- onExportToClipboard,
- onExportToBackend,
- onCloseRequest,
- }: {
- appState: AppState;
- elements: readonly ExcalidrawElement[];
- exportPadding?: number;
- actionManager: ActionsManagerInterface;
- syncActionResult: UpdaterFn;
- onExportToPng: ExportCB;
- onExportToSvg: ExportCB;
- onExportToClipboard: ExportCB;
- onExportToBackend: ExportCB;
- onCloseRequest: () => void;
- }) {
- const { t } = useTranslation();
- const someElementIsSelected = elements.some(element => element.isSelected);
- 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
- ? elements.filter(element => element.isSelected)
- : 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"
- icon={downloadFile}
- title={t("buttons.exportToPng")}
- aria-label={t("buttons.exportToPng")}
- onClick={() => onExportToPng(exportedElements, scale)}
- ref={pngButton}
- />
- <ToolButton
- type="button"
- icon={svgFile}
- title={t("buttons.exportToSvg")}
- aria-label={t("buttons.exportToSvg")}
- onClick={() => onExportToSvg(exportedElements, scale)}
- />
- {probablySupportsClipboard && (
- <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",
- elements,
- appState,
- syncActionResult,
- t,
- )}
- <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",
- elements,
- appState,
- syncActionResult,
- t,
- )}
- {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,
- syncActionResult,
- onExportToPng,
- onExportToSvg,
- onExportToClipboard,
- onExportToBackend,
- }: {
- appState: AppState;
- elements: readonly ExcalidrawElement[];
- exportPadding?: number;
- actionManager: ActionsManagerInterface;
- syncActionResult: UpdaterFn;
- onExportToPng: ExportCB;
- onExportToSvg: ExportCB;
- onExportToClipboard: ExportCB;
- onExportToBackend: ExportCB;
- }) {
- const { t } = useTranslation();
- 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")}
- title={t("buttons.export")}
- ref={triggerButton}
- />
- {modalIsShown && (
- <Modal
- maxWidth={640}
- onCloseRequest={handleClose}
- labelledBy="export-title"
- >
- <ExportModal
- elements={elements}
- appState={appState}
- exportPadding={exportPadding}
- actionManager={actionManager}
- syncActionResult={syncActionResult}
- onExportToPng={onExportToPng}
- onExportToSvg={onExportToSvg}
- onExportToClipboard={onExportToClipboard}
- onExportToBackend={onExportToBackend}
- onCloseRequest={handleClose}
- />
- </Modal>
- )}
- </>
- );
- }
|