|
@@ -104,6 +104,7 @@ import { LanguageList } from "./components/LanguageList";
|
|
|
import { Point } from "roughjs/bin/geometry";
|
|
|
import { t, languages, setLanguage, getLanguage } from "./i18n";
|
|
|
import { HintViewer } from "./components/HintViewer";
|
|
|
+import useIsMobile, { IsMobileProvider } from "./is-mobile";
|
|
|
|
|
|
import { copyToAppClipboard, getClipboardContent } from "./clipboard";
|
|
|
import { normalizeScroll } from "./scene/data";
|
|
@@ -135,6 +136,18 @@ const MOUSE_BUTTON = {
|
|
|
SECONDARY: 2,
|
|
|
};
|
|
|
|
|
|
+// Block pinch-zooming on iOS outside of the content area
|
|
|
+document.addEventListener(
|
|
|
+ "touchmove",
|
|
|
+ function(event) {
|
|
|
+ // @ts-ignore
|
|
|
+ if (event.scale !== 1) {
|
|
|
+ event.preventDefault();
|
|
|
+ }
|
|
|
+ },
|
|
|
+ { passive: false },
|
|
|
+);
|
|
|
+
|
|
|
let lastMouseUp: ((e: any) => void) | null = null;
|
|
|
|
|
|
export function viewportCoordsToSceneCoords(
|
|
@@ -211,64 +224,58 @@ const LayerUI = React.memo(
|
|
|
language,
|
|
|
setElements,
|
|
|
}: LayerUIProps) => {
|
|
|
- function renderCanvasActions() {
|
|
|
+ const isMobile = useIsMobile();
|
|
|
+
|
|
|
+ function renderExportDialog() {
|
|
|
return (
|
|
|
- <Stack.Col gap={4}>
|
|
|
- <Stack.Row justifyContent={"space-between"}>
|
|
|
- {actionManager.renderAction("loadScene")}
|
|
|
- {actionManager.renderAction("saveScene")}
|
|
|
- <ExportDialog
|
|
|
- elements={elements}
|
|
|
- appState={appState}
|
|
|
- actionManager={actionManager}
|
|
|
- onExportToPng={(exportedElements, scale) => {
|
|
|
- if (canvas) {
|
|
|
- exportCanvas("png", exportedElements, canvas, {
|
|
|
- exportBackground: appState.exportBackground,
|
|
|
- name: appState.name,
|
|
|
- viewBackgroundColor: appState.viewBackgroundColor,
|
|
|
- scale,
|
|
|
- });
|
|
|
- }
|
|
|
- }}
|
|
|
- onExportToSvg={(exportedElements, scale) => {
|
|
|
- if (canvas) {
|
|
|
- exportCanvas("svg", exportedElements, canvas, {
|
|
|
- exportBackground: appState.exportBackground,
|
|
|
- name: appState.name,
|
|
|
- viewBackgroundColor: appState.viewBackgroundColor,
|
|
|
- scale,
|
|
|
- });
|
|
|
- }
|
|
|
- }}
|
|
|
- onExportToClipboard={(exportedElements, scale) => {
|
|
|
- if (canvas) {
|
|
|
- exportCanvas("clipboard", exportedElements, canvas, {
|
|
|
- exportBackground: appState.exportBackground,
|
|
|
- name: appState.name,
|
|
|
- viewBackgroundColor: appState.viewBackgroundColor,
|
|
|
- scale,
|
|
|
- });
|
|
|
- }
|
|
|
- }}
|
|
|
- onExportToBackend={exportedElements => {
|
|
|
- if (canvas) {
|
|
|
- exportCanvas(
|
|
|
- "backend",
|
|
|
- exportedElements.map(element => ({
|
|
|
- ...element,
|
|
|
- isSelected: false,
|
|
|
- })),
|
|
|
- canvas,
|
|
|
- appState,
|
|
|
- );
|
|
|
- }
|
|
|
- }}
|
|
|
- />
|
|
|
- {actionManager.renderAction("clearCanvas")}
|
|
|
- </Stack.Row>
|
|
|
- {actionManager.renderAction("changeViewBackgroundColor")}
|
|
|
- </Stack.Col>
|
|
|
+ <ExportDialog
|
|
|
+ elements={elements}
|
|
|
+ appState={appState}
|
|
|
+ actionManager={actionManager}
|
|
|
+ onExportToPng={(exportedElements, scale) => {
|
|
|
+ if (canvas) {
|
|
|
+ exportCanvas("png", exportedElements, canvas, {
|
|
|
+ exportBackground: appState.exportBackground,
|
|
|
+ name: appState.name,
|
|
|
+ viewBackgroundColor: appState.viewBackgroundColor,
|
|
|
+ scale,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }}
|
|
|
+ onExportToSvg={(exportedElements, scale) => {
|
|
|
+ if (canvas) {
|
|
|
+ exportCanvas("svg", exportedElements, canvas, {
|
|
|
+ exportBackground: appState.exportBackground,
|
|
|
+ name: appState.name,
|
|
|
+ viewBackgroundColor: appState.viewBackgroundColor,
|
|
|
+ scale,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }}
|
|
|
+ onExportToClipboard={(exportedElements, scale) => {
|
|
|
+ if (canvas) {
|
|
|
+ exportCanvas("clipboard", exportedElements, canvas, {
|
|
|
+ exportBackground: appState.exportBackground,
|
|
|
+ name: appState.name,
|
|
|
+ viewBackgroundColor: appState.viewBackgroundColor,
|
|
|
+ scale,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }}
|
|
|
+ onExportToBackend={exportedElements => {
|
|
|
+ if (canvas) {
|
|
|
+ exportCanvas(
|
|
|
+ "backend",
|
|
|
+ exportedElements.map(element => ({
|
|
|
+ ...element,
|
|
|
+ isSelected: false,
|
|
|
+ })),
|
|
|
+ canvas,
|
|
|
+ appState,
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }}
|
|
|
+ />
|
|
|
);
|
|
|
}
|
|
|
|
|
@@ -284,51 +291,49 @@ const LayerUI = React.memo(
|
|
|
}
|
|
|
|
|
|
return (
|
|
|
- <Island padding={4}>
|
|
|
- <div className="panelColumn">
|
|
|
- {actionManager.renderAction("changeStrokeColor")}
|
|
|
- {(hasBackground(elementType) ||
|
|
|
- targetElements.some(element => hasBackground(element.type))) && (
|
|
|
- <>
|
|
|
- {actionManager.renderAction("changeBackgroundColor")}
|
|
|
-
|
|
|
- {actionManager.renderAction("changeFillStyle")}
|
|
|
- </>
|
|
|
- )}
|
|
|
+ <div className="panelColumn">
|
|
|
+ {actionManager.renderAction("changeStrokeColor")}
|
|
|
+ {(hasBackground(elementType) ||
|
|
|
+ targetElements.some(element => hasBackground(element.type))) && (
|
|
|
+ <>
|
|
|
+ {actionManager.renderAction("changeBackgroundColor")}
|
|
|
+
|
|
|
+ {actionManager.renderAction("changeFillStyle")}
|
|
|
+ </>
|
|
|
+ )}
|
|
|
|
|
|
- {(hasStroke(elementType) ||
|
|
|
- targetElements.some(element => hasStroke(element.type))) && (
|
|
|
- <>
|
|
|
- {actionManager.renderAction("changeStrokeWidth")}
|
|
|
+ {(hasStroke(elementType) ||
|
|
|
+ targetElements.some(element => hasStroke(element.type))) && (
|
|
|
+ <>
|
|
|
+ {actionManager.renderAction("changeStrokeWidth")}
|
|
|
|
|
|
- {actionManager.renderAction("changeSloppiness")}
|
|
|
- </>
|
|
|
- )}
|
|
|
+ {actionManager.renderAction("changeSloppiness")}
|
|
|
+ </>
|
|
|
+ )}
|
|
|
|
|
|
- {(hasText(elementType) ||
|
|
|
- targetElements.some(element => hasText(element.type))) && (
|
|
|
- <>
|
|
|
- {actionManager.renderAction("changeFontSize")}
|
|
|
+ {(hasText(elementType) ||
|
|
|
+ targetElements.some(element => hasText(element.type))) && (
|
|
|
+ <>
|
|
|
+ {actionManager.renderAction("changeFontSize")}
|
|
|
|
|
|
- {actionManager.renderAction("changeFontFamily")}
|
|
|
- </>
|
|
|
- )}
|
|
|
+ {actionManager.renderAction("changeFontFamily")}
|
|
|
+ </>
|
|
|
+ )}
|
|
|
|
|
|
- {actionManager.renderAction("changeOpacity")}
|
|
|
+ {actionManager.renderAction("changeOpacity")}
|
|
|
|
|
|
- <fieldset>
|
|
|
- <legend>{t("labels.layers")}</legend>
|
|
|
- <div className="buttonList">
|
|
|
- {actionManager.renderAction("sendToBack")}
|
|
|
- {actionManager.renderAction("sendBackward")}
|
|
|
- {actionManager.renderAction("bringToFront")}
|
|
|
- {actionManager.renderAction("bringForward")}
|
|
|
- </div>
|
|
|
- </fieldset>
|
|
|
+ <fieldset>
|
|
|
+ <legend>{t("labels.layers")}</legend>
|
|
|
+ <div className="buttonList">
|
|
|
+ {actionManager.renderAction("sendToBack")}
|
|
|
+ {actionManager.renderAction("sendBackward")}
|
|
|
+ {actionManager.renderAction("bringToFront")}
|
|
|
+ {actionManager.renderAction("bringForward")}
|
|
|
+ </div>
|
|
|
+ </fieldset>
|
|
|
|
|
|
- {actionManager.renderAction("deleteSelectedElements")}
|
|
|
- </div>
|
|
|
- </Island>
|
|
|
+ {actionManager.renderAction("deleteSelectedElements")}
|
|
|
+ </div>
|
|
|
);
|
|
|
}
|
|
|
|
|
@@ -378,7 +383,125 @@ const LayerUI = React.memo(
|
|
|
);
|
|
|
}
|
|
|
|
|
|
- return (
|
|
|
+ const lockButton = (
|
|
|
+ <LockIcon
|
|
|
+ checked={appState.elementLocked}
|
|
|
+ onChange={() => {
|
|
|
+ setAppState({
|
|
|
+ elementLocked: !appState.elementLocked,
|
|
|
+ elementType: appState.elementLocked
|
|
|
+ ? "selection"
|
|
|
+ : appState.elementType,
|
|
|
+ });
|
|
|
+ }}
|
|
|
+ title={t("toolBar.lock")}
|
|
|
+ />
|
|
|
+ );
|
|
|
+
|
|
|
+ return isMobile ? (
|
|
|
+ <>
|
|
|
+ {appState.openedMenu === "canvas" ? (
|
|
|
+ <section
|
|
|
+ className="App-mobile-menu"
|
|
|
+ aria-labelledby="canvas-actions-title"
|
|
|
+ >
|
|
|
+ <h2 className="visually-hidden" id="canvas-actions-title">
|
|
|
+ {t("headings.canvasActions")}
|
|
|
+ </h2>
|
|
|
+ <div className="App-mobile-menu-scroller">
|
|
|
+ <Stack.Col gap={4}>
|
|
|
+ {actionManager.renderAction("loadScene")}
|
|
|
+ {actionManager.renderAction("saveScene")}
|
|
|
+ {renderExportDialog()}
|
|
|
+ {actionManager.renderAction("clearCanvas")}
|
|
|
+ {actionManager.renderAction("changeViewBackgroundColor")}
|
|
|
+ </Stack.Col>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+ ) : appState.openedMenu === "shape" ? (
|
|
|
+ <section
|
|
|
+ className="App-mobile-menu"
|
|
|
+ aria-labelledby="selected-shape-title"
|
|
|
+ >
|
|
|
+ <h2 className="visually-hidden" id="selected-shape-title">
|
|
|
+ {t("headings.selectedShapeActions")}
|
|
|
+ </h2>
|
|
|
+ <div className="App-mobile-menu-scroller">
|
|
|
+ {renderSelectedShapeActions(elements)}
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+ ) : null}
|
|
|
+ <FixedSideContainer side="top">
|
|
|
+ <section aria-labelledby="shapes-title">
|
|
|
+ <Stack.Col gap={4} align="center">
|
|
|
+ <Stack.Row gap={1}>
|
|
|
+ <Island padding={1}>
|
|
|
+ <h2 className="visually-hidden" id="shapes-title">
|
|
|
+ {t("headings.shapes")}
|
|
|
+ </h2>
|
|
|
+ <Stack.Row gap={1}>{renderShapesSwitcher()}</Stack.Row>
|
|
|
+ </Island>
|
|
|
+ </Stack.Row>
|
|
|
+ </Stack.Col>
|
|
|
+ </section>
|
|
|
+ </FixedSideContainer>
|
|
|
+ <footer className="App-toolbar">
|
|
|
+ <div className="App-toolbar-content">
|
|
|
+ <ToolButton
|
|
|
+ type="button"
|
|
|
+ icon={
|
|
|
+ <span style={{ fontSize: "2em", marginTop: "-0.15em" }}>☰</span>
|
|
|
+ }
|
|
|
+ aria-label={t("buttons.menu")}
|
|
|
+ onClick={() =>
|
|
|
+ setAppState(({ openedMenu }: any) => ({
|
|
|
+ openedMenu: openedMenu === "canvas" ? null : "canvas",
|
|
|
+ }))
|
|
|
+ }
|
|
|
+ />
|
|
|
+ {lockButton}
|
|
|
+ <div
|
|
|
+ style={{
|
|
|
+ visibility: isSomeElementSelected(elements)
|
|
|
+ ? "visible"
|
|
|
+ : "hidden",
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <ToolButton
|
|
|
+ type="button"
|
|
|
+ icon={
|
|
|
+ <span style={{ fontSize: "2em", marginTop: "-0.15em" }}>
|
|
|
+ ✎
|
|
|
+ </span>
|
|
|
+ }
|
|
|
+ aria-label={t("buttons.menu")}
|
|
|
+ onClick={() =>
|
|
|
+ setAppState(({ openedMenu }: any) => ({
|
|
|
+ openedMenu: openedMenu === "shape" ? null : "shape",
|
|
|
+ }))
|
|
|
+ }
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <HintViewer
|
|
|
+ elementType={appState.elementType}
|
|
|
+ multiMode={appState.multiElement !== null}
|
|
|
+ isResizing={appState.isResizing}
|
|
|
+ elements={elements}
|
|
|
+ />
|
|
|
+ {appState.scrolledOutside && (
|
|
|
+ <button
|
|
|
+ className="scroll-back-to-content"
|
|
|
+ onClick={() => {
|
|
|
+ setAppState({ ...calculateScrollCenter(elements) });
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ {t("buttons.scrollBackToContent")}
|
|
|
+ </button>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ </footer>
|
|
|
+ </>
|
|
|
+ ) : (
|
|
|
<>
|
|
|
<FixedSideContainer side="top">
|
|
|
<div className="App-menu App-menu_top">
|
|
@@ -390,7 +513,17 @@ const LayerUI = React.memo(
|
|
|
<h2 className="visually-hidden" id="canvas-actions-title">
|
|
|
{t("headings.canvasActions")}
|
|
|
</h2>
|
|
|
- <Island padding={4}>{renderCanvasActions()}</Island>
|
|
|
+ <Island padding={4}>
|
|
|
+ <Stack.Col gap={4}>
|
|
|
+ <Stack.Row justifyContent={"space-between"}>
|
|
|
+ {actionManager.renderAction("loadScene")}
|
|
|
+ {actionManager.renderAction("saveScene")}
|
|
|
+ {renderExportDialog()}
|
|
|
+ {actionManager.renderAction("clearCanvas")}
|
|
|
+ </Stack.Row>
|
|
|
+ {actionManager.renderAction("changeViewBackgroundColor")}
|
|
|
+ </Stack.Col>
|
|
|
+ </Island>
|
|
|
</section>
|
|
|
<section
|
|
|
className="App-right-menu"
|
|
@@ -399,7 +532,9 @@ const LayerUI = React.memo(
|
|
|
<h2 className="visually-hidden" id="selected-shape-title">
|
|
|
{t("headings.selectedShapeActions")}
|
|
|
</h2>
|
|
|
- {renderSelectedShapeActions(elements)}
|
|
|
+ <Island padding={4}>
|
|
|
+ {renderSelectedShapeActions(elements)}
|
|
|
+ </Island>
|
|
|
</section>
|
|
|
</Stack.Col>
|
|
|
<section aria-labelledby="shapes-title">
|
|
@@ -411,18 +546,7 @@ const LayerUI = React.memo(
|
|
|
</h2>
|
|
|
<Stack.Row gap={1}>{renderShapesSwitcher()}</Stack.Row>
|
|
|
</Island>
|
|
|
- <LockIcon
|
|
|
- checked={appState.elementLocked}
|
|
|
- onChange={() => {
|
|
|
- setAppState({
|
|
|
- elementLocked: !appState.elementLocked,
|
|
|
- elementType: appState.elementLocked
|
|
|
- ? "selection"
|
|
|
- : appState.elementType,
|
|
|
- });
|
|
|
- }}
|
|
|
- title={t("toolBar.lock")}
|
|
|
- />
|
|
|
+ {lockButton}
|
|
|
</Stack.Row>
|
|
|
</Stack.Col>
|
|
|
</section>
|
|
@@ -2204,7 +2328,9 @@ class TopErrorBoundary extends React.Component {
|
|
|
|
|
|
ReactDOM.render(
|
|
|
<TopErrorBoundary>
|
|
|
- <App />
|
|
|
+ <IsMobileProvider>
|
|
|
+ <App />
|
|
|
+ </IsMobileProvider>
|
|
|
</TopErrorBoundary>,
|
|
|
rootElement,
|
|
|
);
|