@ -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 ,
) ;