excalidraw/src/components/ExportDialog.tsx

265 lines
7.7 KiB
TypeScript
Raw Normal View History

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";
Fix issues related to history (#701) * Separate UI from Canvas * Explicitly define history recording * ActionManager: Set syncActionState during construction instead of in every call * Add commit to history flag to necessary actions * Disable undoing during multiElement * Write custom equality function for UI component to render it only when specific props and elements change * Remove stale comments about history skipping * Stop undo/redoing when in resizing element mode * wip * correctly reset resizingElement & add undo check * Separate selection element from the rest of the array and stop redrawing the UI when dragging the selection * Remove selectionElement from local storage * Remove unnecessary readonly type casting in actionFinalize * Fix undo / redo for multi points * Fix an issue that did not update history when elements were locked * Disable committing to history for noops - deleteSelected without deleting anything - Basic selection * Use generateEntry only inside history and pass elements and appstate to history * Update component after every history resume * Remove last item from the history only if in multi mode * Resume recording when element type is not selection * ensure we prevent hotkeys only on writable elements * Remove selection clearing from history * Remove one point arrows as they are invisibly small * Remove shape of elements from local storage * Fix removing invisible element from the array * add missing history resuming cases & simplify slice * fix lint * don't regenerate elements if no elements deselected * regenerate elements array on selection * reset state.selectionElement unconditionally * Use getter instead of passing appState and scene data through functions to actions * fix import Co-authored-by: David Luzar <luzar.david@gmail.com>
2020-02-05 22:47:10 +04:00
import { ActionsManagerInterface } from "../actions/types";
import Stack from "./Stack";
import { t } from "../i18n";
import { KEYS } from "../keys";
2020-02-04 11:50:18 +01:00
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[],
2020-01-24 12:04:54 +02:00
scale?: number,
) => void;
function ExportModal({
elements,
appState,
exportPadding = 10,
actionManager,
onExportToPng,
onExportToSvg,
onExportToClipboard,
2020-01-24 12:04:54 +02:00
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);
2020-01-24 20:02:55 +00:00
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(() => {
2020-01-24 20:02:55 +00:00
const previewNode = previewRef.current;
const canvas = exportToCanvas(exportedElements, {
exportBackground,
viewBackgroundColor,
exportPadding,
2020-01-24 12:04:54 +02:00
scale,
});
previewNode?.appendChild(canvas);
return () => {
previewNode?.removeChild(canvas);
};
}, [
exportedElements,
exportBackground,
exportPadding,
viewBackgroundColor,
2020-01-24 12:04:54 +02:00
scale,
]);
useEffect(() => {
pngButton.current?.focus();
}, []);
function handleKeyDown(event: React.KeyboardEvent) {
if (event.key === KEYS.TAB) {
const { activeElement } = document;
if (event.shiftKey) {
if (activeElement === pngButton.current) {
closeButton.current?.focus();
event.preventDefault();
}
} else {
if (activeElement === closeButton.current) {
pngButton.current?.focus();
event.preventDefault();
}
if (activeElement === onlySelectedInput.current) {
closeButton.current?.focus();
event.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">
2020-01-29 02:25:47 +02:00
<Stack.Col gap={1}>
<Stack.Row gap={2}>
<ToolButton
type="button"
label="PNG"
2020-01-29 02:25:47 +02:00
title={t("buttons.exportToPng")}
aria-label={t("buttons.exportToPng")}
onClick={() => onExportToPng(exportedElements, scale)}
ref={pngButton}
/>
2020-01-29 02:25:47 +02:00
<ToolButton
type="button"
label="SVG"
2020-01-29 02:25:47 +02:00
title={t("buttons.exportToSvg")}
aria-label={t("buttons.exportToSvg")}
onClick={() => onExportToSvg(exportedElements, scale)}
/>
2020-02-04 11:50:18 +01:00
{probablySupportsClipboardBlob && (
2020-01-29 02:25:47 +02:00
<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>
Fix issues related to history (#701) * Separate UI from Canvas * Explicitly define history recording * ActionManager: Set syncActionState during construction instead of in every call * Add commit to history flag to necessary actions * Disable undoing during multiElement * Write custom equality function for UI component to render it only when specific props and elements change * Remove stale comments about history skipping * Stop undo/redoing when in resizing element mode * wip * correctly reset resizingElement & add undo check * Separate selection element from the rest of the array and stop redrawing the UI when dragging the selection * Remove selectionElement from local storage * Remove unnecessary readonly type casting in actionFinalize * Fix undo / redo for multi points * Fix an issue that did not update history when elements were locked * Disable committing to history for noops - deleteSelected without deleting anything - Basic selection * Use generateEntry only inside history and pass elements and appstate to history * Update component after every history resume * Remove last item from the history only if in multi mode * Resume recording when element type is not selection * ensure we prevent hotkeys only on writable elements * Remove selection clearing from history * Remove one point arrows as they are invisibly small * Remove shape of elements from local storage * Fix removing invisible element from the array * add missing history resuming cases & simplify slice * fix lint * don't regenerate elements if no elements deselected * regenerate elements array on selection * reset state.selectionElement unconditionally * Use getter instead of passing appState and scene data through functions to actions * fix import Co-authored-by: David Luzar <luzar.david@gmail.com>
2020-02-05 22:47:10 +04:00
{actionManager.renderAction("changeProjectName")}
<Stack.Col gap={1}>
<div className="ExportDialog__scales">
2020-01-29 02:25:47 +02:00
<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>
Fix issues related to history (#701) * Separate UI from Canvas * Explicitly define history recording * ActionManager: Set syncActionState during construction instead of in every call * Add commit to history flag to necessary actions * Disable undoing during multiElement * Write custom equality function for UI component to render it only when specific props and elements change * Remove stale comments about history skipping * Stop undo/redoing when in resizing element mode * wip * correctly reset resizingElement & add undo check * Separate selection element from the rest of the array and stop redrawing the UI when dragging the selection * Remove selectionElement from local storage * Remove unnecessary readonly type casting in actionFinalize * Fix undo / redo for multi points * Fix an issue that did not update history when elements were locked * Disable committing to history for noops - deleteSelected without deleting anything - Basic selection * Use generateEntry only inside history and pass elements and appstate to history * Update component after every history resume * Remove last item from the history only if in multi mode * Resume recording when element type is not selection * ensure we prevent hotkeys only on writable elements * Remove selection clearing from history * Remove one point arrows as they are invisibly small * Remove shape of elements from local storage * Fix removing invisible element from the array * add missing history resuming cases & simplify slice * fix lint * don't regenerate elements if no elements deselected * regenerate elements array on selection * reset state.selectionElement unconditionally * Use getter instead of passing appState and scene data through functions to actions * fix import Co-authored-by: David Luzar <luzar.david@gmail.com>
2020-02-05 22:47:10 +04:00
{actionManager.renderAction("changeExportBackground")}
{someElementIsSelected && (
<div>
<label>
<input
type="checkbox"
checked={exportSelected}
onChange={event =>
setExportSelected(event.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
2020-01-29 02:26:02 +02:00
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>
)}
</>
);
}