kopia lustrzana https://github.com/Tldraw/Tldraw
feat: replace window confirm with a dialog (#898)
* feat: replace window confirm with react dialog * add dialog provider * export func * remove unused code * changes * Create file_open.tldr * Update TldrawApp.ts * clean, and add description * add a custom container for alert dialog * Fix fonts * Fix logic for open project * Style panel * Improve styling Co-authored-by: Steve Ruiz <steveruizok@gmail.com>pull/915/head
rodzic
6dccf26c43
commit
97b0b52a6e
|
@ -6,11 +6,14 @@ import { ContextMenu } from '~components/ContextMenu'
|
|||
import { ErrorFallback } from '~components/ErrorFallback'
|
||||
import { FocusButton } from '~components/FocusButton'
|
||||
import { Loading } from '~components/Loading'
|
||||
import { AlertDialog } from '~components/Primitives/AlertDialog'
|
||||
import { ToolsPanel } from '~components/ToolsPanel'
|
||||
import { TopPanel } from '~components/TopPanel'
|
||||
import { GRID_SIZE } from '~constants'
|
||||
import {
|
||||
AlertDialogContext,
|
||||
ContainerContext,
|
||||
DialogState,
|
||||
TldrawContext,
|
||||
useKeyboardShortcuts,
|
||||
useStylesheet,
|
||||
|
@ -163,6 +166,21 @@ export function Tldraw({
|
|||
return app
|
||||
})
|
||||
|
||||
const [onCancel, setOnCancel] = React.useState<(() => void) | null>(null)
|
||||
const [onYes, setOnYes] = React.useState<(() => void) | null>(null)
|
||||
const [onNo, setOnNo] = React.useState<(() => void) | null>(null)
|
||||
const [dialogState, setDialogState] = React.useState<DialogState | null>(null)
|
||||
|
||||
const openDialog = React.useCallback(
|
||||
(dialogState: DialogState, onYes: () => void, onNo: () => void, onCancel: () => void) => {
|
||||
setDialogState(() => dialogState)
|
||||
setOnCancel(() => onCancel)
|
||||
setOnYes(() => onYes)
|
||||
setOnNo(() => onNo)
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
// Create a new app if the `id` prop changes.
|
||||
React.useLayoutEffect(() => {
|
||||
if (id === sId) return
|
||||
|
@ -297,19 +315,23 @@ export function Tldraw({
|
|||
// Use the `key` to ensure that new selector hooks are made when the id changes
|
||||
return (
|
||||
<TldrawContext.Provider value={app}>
|
||||
<InnerTldraw
|
||||
key={sId || 'Tldraw'}
|
||||
id={sId}
|
||||
autofocus={autofocus}
|
||||
showPages={showPages}
|
||||
showMenu={showMenu}
|
||||
showMultiplayerMenu={showMultiplayerMenu}
|
||||
showStyles={showStyles}
|
||||
showZoom={showZoom}
|
||||
showTools={showTools}
|
||||
showUI={showUI}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
<AlertDialogContext.Provider
|
||||
value={{ onYes, onCancel, onNo, dialogState, setDialogState, openDialog }}
|
||||
>
|
||||
<InnerTldraw
|
||||
key={sId || 'Tldraw'}
|
||||
id={sId}
|
||||
autofocus={autofocus}
|
||||
showPages={showPages}
|
||||
showMenu={showMenu}
|
||||
showMultiplayerMenu={showMultiplayerMenu}
|
||||
showStyles={showStyles}
|
||||
showZoom={showZoom}
|
||||
showTools={showTools}
|
||||
showUI={showUI}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
</AlertDialogContext.Provider>
|
||||
</TldrawContext.Provider>
|
||||
)
|
||||
}
|
||||
|
@ -340,6 +362,7 @@ const InnerTldraw = React.memo(function InnerTldraw({
|
|||
showUI,
|
||||
}: InnerTldrawProps) {
|
||||
const app = useTldrawApp()
|
||||
const [dialogContainer, setDialogContainer] = React.useState<any>(null)
|
||||
|
||||
const rWrapper = React.useRef<HTMLDivElement>(null)
|
||||
|
||||
|
@ -439,6 +462,7 @@ const InnerTldraw = React.memo(function InnerTldraw({
|
|||
return (
|
||||
<ContainerContext.Provider value={rWrapper}>
|
||||
<IntlProvider locale={translation.locale} messages={translation.messages}>
|
||||
<AlertDialog container={dialogContainer} />
|
||||
<StyledLayout ref={rWrapper} tabIndex={-0}>
|
||||
<Loading />
|
||||
<OneOff focusableRef={rWrapper} autofocus={autofocus} />
|
||||
|
@ -523,7 +547,7 @@ const InnerTldraw = React.memo(function InnerTldraw({
|
|||
</ErrorBoundary>
|
||||
</ContextMenu>
|
||||
{showUI && (
|
||||
<StyledUI>
|
||||
<StyledUI ref={setDialogContainer}>
|
||||
{settings.isFocusMode ? (
|
||||
<FocusButton onSelect={app.toggleFocusMode} />
|
||||
) : (
|
||||
|
|
|
@ -0,0 +1,160 @@
|
|||
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'
|
||||
import * as React from 'react'
|
||||
import { DialogState, useDialog } from '~hooks'
|
||||
import { styled } from '~styles'
|
||||
|
||||
interface ContentProps {
|
||||
children: React.ReactNode
|
||||
onClose?: () => void
|
||||
container: any
|
||||
}
|
||||
|
||||
function Content({ children, onClose, container }: ContentProps) {
|
||||
const handleKeyDown = (event: React.KeyboardEvent) => {
|
||||
switch (event.key) {
|
||||
case 'Escape':
|
||||
onClose?.()
|
||||
break
|
||||
}
|
||||
}
|
||||
return (
|
||||
<AlertDialogPrimitive.Portal container={container}>
|
||||
<StyledOverlay />
|
||||
<StyledContent onKeyDown={handleKeyDown}>{children}</StyledContent>
|
||||
</AlertDialogPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
const StyledDescription = styled(AlertDialogPrimitive.Description, {
|
||||
marginBottom: 20,
|
||||
color: '$text',
|
||||
fontSize: '$2',
|
||||
lineHeight: 1.5,
|
||||
textAlign: 'center',
|
||||
maxWidth: '62%',
|
||||
minWidth: 0,
|
||||
alignSelf: 'center',
|
||||
})
|
||||
|
||||
export const AlertDialogRoot = AlertDialogPrimitive.Root
|
||||
export const AlertDialogContent = Content
|
||||
export const AlertDialogDescription = StyledDescription
|
||||
export const AlertDialogAction = AlertDialogPrimitive.Action
|
||||
export const AlertDialogCancel = AlertDialogPrimitive.Cancel
|
||||
|
||||
const descriptions: Record<DialogState, string> = {
|
||||
saveFirstTime: 'Do you want to save your current project?',
|
||||
saveAgain: 'Do you want to save changes to your current project?',
|
||||
}
|
||||
|
||||
export const AlertDialog = ({ container }: { container: any }) => {
|
||||
const { setDialogState, dialogState, onCancel, onNo, onYes } = useDialog()
|
||||
|
||||
return (
|
||||
<AlertDialogRoot open={dialogState !== null}>
|
||||
<AlertDialogContent onClose={() => setDialogState(null)} container={container}>
|
||||
{dialogState && (
|
||||
<AlertDialogDescription>{descriptions[dialogState]}</AlertDialogDescription>
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
gap: '$6',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
{onCancel && (
|
||||
<AlertDialogCancel asChild>
|
||||
<Button
|
||||
css={{ color: '$text' }}
|
||||
onClick={() => {
|
||||
onCancel()
|
||||
setDialogState(null)
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</AlertDialogCancel>
|
||||
)}
|
||||
<div style={{ flexShrink: 0 }}>
|
||||
{onNo && (
|
||||
<AlertDialogAction asChild>
|
||||
<Button
|
||||
onClick={() => {
|
||||
onNo()
|
||||
setDialogState(null)
|
||||
}}
|
||||
>
|
||||
No
|
||||
</Button>
|
||||
</AlertDialogAction>
|
||||
)}
|
||||
{onYes && (
|
||||
<AlertDialogAction asChild>
|
||||
<Button
|
||||
css={{ backgroundColor: '#2F80ED', color: 'White' }}
|
||||
onClick={() => {
|
||||
onYes()
|
||||
setDialogState(null)
|
||||
}}
|
||||
>
|
||||
Yes
|
||||
</Button>
|
||||
</AlertDialogAction>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</AlertDialogContent>
|
||||
</AlertDialogRoot>
|
||||
)
|
||||
}
|
||||
|
||||
const StyledOverlay = styled(AlertDialogPrimitive.Overlay, {
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
backgroundColor: 'rgba(0, 0, 0, .15)',
|
||||
pointerEvents: 'all',
|
||||
})
|
||||
|
||||
export const StyledDialogOverlay = styled(AlertDialogPrimitive.Overlay, {
|
||||
backgroundColor: 'rgba(0, 0, 0, .15)',
|
||||
position: 'absolute',
|
||||
pointerEvents: 'all',
|
||||
inset: 0,
|
||||
})
|
||||
|
||||
const StyledContent = styled(AlertDialogPrimitive.Content, {
|
||||
position: 'fixed',
|
||||
font: '$ui',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: 'max-content',
|
||||
padding: '$3',
|
||||
pointerEvents: 'all',
|
||||
backgroundColor: '$panel',
|
||||
borderRadius: '$3',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
fontFamily: '$ui',
|
||||
border: '1px solid $panelContrast',
|
||||
boxShadow: '$panel',
|
||||
})
|
||||
|
||||
const Button = styled('button', {
|
||||
all: 'unset',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: '$2',
|
||||
padding: '0 15px',
|
||||
fontSize: '$1',
|
||||
lineHeight: 1,
|
||||
fontWeight: 'normal',
|
||||
height: 36,
|
||||
color: '$text',
|
||||
cursor: 'pointer',
|
||||
minWidth: 48,
|
||||
})
|
|
@ -0,0 +1 @@
|
|||
export * from './AlertDialog'
|
|
@ -29,7 +29,7 @@ export function DMContent({
|
|||
const container = useContainer()
|
||||
|
||||
return (
|
||||
<DropdownMenu.Portal container={container.current} dir="ltr">
|
||||
<DropdownMenu.Portal container={overflow ? undefined : container.current} dir="ltr">
|
||||
<DropdownMenu.Content
|
||||
align={align}
|
||||
alignOffset={alignOffset}
|
||||
|
|
Plik diff jest za duży
Load Diff
|
@ -5,3 +5,4 @@ export * from './useStylesheet'
|
|||
export * from './useTheme'
|
||||
export * from './useTldrawApp'
|
||||
export * from './useTranslation'
|
||||
export * from './useDialog'
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
import * as React from 'react'
|
||||
|
||||
export type DialogState = 'saveFirstTime' | 'saveAgain'
|
||||
|
||||
interface AlertDialogProps {
|
||||
dialogState: DialogState | null
|
||||
setDialogState: (dialogState: DialogState | null) => void
|
||||
onYes: (() => void) | null
|
||||
onNo: (() => void) | null
|
||||
onCancel: (() => void) | null
|
||||
openDialog: (
|
||||
dialogState: DialogState,
|
||||
onYes: () => void,
|
||||
onNo: () => void,
|
||||
onCancel: () => void
|
||||
) => void
|
||||
}
|
||||
|
||||
export const AlertDialogContext = React.createContext<AlertDialogProps>({} as AlertDialogProps)
|
||||
|
||||
export const useDialog = () => {
|
||||
const context = React.useContext(AlertDialogContext)
|
||||
if (!context) throw new Error('useCtx must be inside a Provider with a value')
|
||||
return context
|
||||
}
|
|
@ -1,29 +1,72 @@
|
|||
import * as React from 'react'
|
||||
import type { TldrawApp } from '~state'
|
||||
import { DialogState } from './useDialog'
|
||||
|
||||
export function useFileSystem() {
|
||||
const promptSaveBeforeChange = React.useCallback(async (app: TldrawApp) => {
|
||||
if (app.isDirty) {
|
||||
if (app.fileSystemHandle) {
|
||||
if (window.confirm('Do you want to save changes to your current project?')) {
|
||||
await app.saveProject()
|
||||
}
|
||||
} else {
|
||||
if (window.confirm('Do you want to save your current project?')) {
|
||||
await app.saveProject()
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const onNewProject = React.useCallback(
|
||||
async (app: TldrawApp) => {
|
||||
if (window.confirm('Do you want to create a new project?')) {
|
||||
await promptSaveBeforeChange(app)
|
||||
app.newProject()
|
||||
}
|
||||
async (
|
||||
app: TldrawApp,
|
||||
openDialog: (
|
||||
dialogState: DialogState,
|
||||
onYes: () => Promise<void>,
|
||||
onNo: () => Promise<void>,
|
||||
onCancel: () => Promise<void>
|
||||
) => void
|
||||
) => {
|
||||
openDialog(
|
||||
app.fileSystemHandle ? 'saveFirstTime' : 'saveAgain',
|
||||
async () => {
|
||||
// user pressed yes
|
||||
try {
|
||||
await app.saveProject()
|
||||
app.newProject()
|
||||
} catch (e) {
|
||||
// noop
|
||||
}
|
||||
},
|
||||
async () => {
|
||||
// user pressed no
|
||||
app.newProject()
|
||||
},
|
||||
async () => {
|
||||
// user pressed cancel
|
||||
}
|
||||
)
|
||||
},
|
||||
[promptSaveBeforeChange]
|
||||
[]
|
||||
)
|
||||
|
||||
const onOpenProject = React.useCallback(
|
||||
async (
|
||||
app: TldrawApp,
|
||||
openDialog: (
|
||||
dialogState: DialogState,
|
||||
onYes: () => Promise<void>,
|
||||
onNo: () => Promise<void>,
|
||||
onCancel: () => Promise<void>
|
||||
) => void
|
||||
) => {
|
||||
openDialog(
|
||||
app.fileSystemHandle ? 'saveFirstTime' : 'saveAgain',
|
||||
async () => {
|
||||
// user pressed yes
|
||||
try {
|
||||
await app.saveProject()
|
||||
await app.openProject()
|
||||
} catch (e) {
|
||||
// noop
|
||||
}
|
||||
},
|
||||
async () => {
|
||||
// user pressed no
|
||||
app.openProject()
|
||||
},
|
||||
async () => {
|
||||
// user pressed cancel
|
||||
}
|
||||
)
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const onSaveProject = React.useCallback((app: TldrawApp) => {
|
||||
|
@ -34,14 +77,6 @@ export function useFileSystem() {
|
|||
app.saveProjectAs()
|
||||
}, [])
|
||||
|
||||
const onOpenProject = React.useCallback(
|
||||
async (app: TldrawApp) => {
|
||||
await promptSaveBeforeChange(app)
|
||||
app.openProject()
|
||||
},
|
||||
[promptSaveBeforeChange]
|
||||
)
|
||||
|
||||
const onOpenMedia = React.useCallback(async (app: TldrawApp) => {
|
||||
app.openAsset?.()
|
||||
}, [])
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
import * as React from 'react'
|
||||
import { useTldrawApp } from '~hooks'
|
||||
import { useDialog, useTldrawApp } from '~hooks'
|
||||
|
||||
export function useFileSystemHandlers() {
|
||||
const app = useTldrawApp()
|
||||
|
||||
const { openDialog } = useDialog()
|
||||
|
||||
const onNewProject = React.useCallback(
|
||||
async (e?: React.MouseEvent | React.KeyboardEvent | KeyboardEvent) => {
|
||||
if (e && app.callbacks.onOpenProject) e.preventDefault()
|
||||
app.callbacks.onNewProject?.(app)
|
||||
app.callbacks.onNewProject?.(app, openDialog)
|
||||
},
|
||||
[app]
|
||||
[app, openDialog]
|
||||
)
|
||||
|
||||
const onSaveProject = React.useCallback(
|
||||
|
@ -31,9 +33,9 @@ export function useFileSystemHandlers() {
|
|||
const onOpenProject = React.useCallback(
|
||||
async (e?: React.MouseEvent | React.KeyboardEvent | KeyboardEvent) => {
|
||||
if (e && app.callbacks.onOpenProject) e.preventDefault()
|
||||
app.callbacks.onOpenProject?.(app)
|
||||
app.callbacks.onOpenProject?.(app, openDialog)
|
||||
},
|
||||
[app]
|
||||
[app, openDialog]
|
||||
)
|
||||
|
||||
const onOpenMedia = React.useCallback(
|
||||
|
|
|
@ -199,8 +199,8 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
useHotkeys(
|
||||
'ctrl+n,⌘+n',
|
||||
(e) => {
|
||||
e.preventDefault()
|
||||
if (!canHandleEvent()) return
|
||||
|
||||
onNewProject(e)
|
||||
},
|
||||
undefined,
|
||||
|
|
|
@ -21,6 +21,7 @@ import {
|
|||
USER_COLORS,
|
||||
VIDEO_EXTENSIONS,
|
||||
} from '~constants'
|
||||
import { DialogState } from '~hooks'
|
||||
import { shapeUtils } from '~state/shapes'
|
||||
import { defaultStyle } from '~state/shapes/shared'
|
||||
import {
|
||||
|
@ -97,7 +98,16 @@ export interface TDCallbacks {
|
|||
/**
|
||||
* (optional) A callback to run when the user creates a new project through the menu or through a keyboard shortcut.
|
||||
*/
|
||||
onNewProject?: (app: TldrawApp, e?: KeyboardEvent) => void
|
||||
onNewProject?: (
|
||||
app: TldrawApp,
|
||||
openDialog: (
|
||||
dialogState: DialogState,
|
||||
onYes: () => void,
|
||||
onNo: () => void,
|
||||
onCancel: () => void
|
||||
) => void,
|
||||
e?: KeyboardEvent
|
||||
) => void
|
||||
/**
|
||||
* (optional) A callback to run when the user saves a project through the menu or through a keyboard shortcut.
|
||||
*/
|
||||
|
@ -109,7 +119,16 @@ export interface TDCallbacks {
|
|||
/**
|
||||
* (optional) A callback to run when the user opens new project through the menu or through a keyboard shortcut.
|
||||
*/
|
||||
onOpenProject?: (app: TldrawApp, e?: KeyboardEvent) => void
|
||||
onOpenProject?: (
|
||||
app: TldrawApp,
|
||||
openDialog: (
|
||||
dialogState: DialogState,
|
||||
onYes: () => void,
|
||||
onNo: () => void,
|
||||
onCancel: () => void
|
||||
) => void,
|
||||
e?: KeyboardEvent
|
||||
) => void
|
||||
/**
|
||||
* (optional) A callback to run when the opens a file to upload.
|
||||
*/
|
||||
|
@ -1384,18 +1403,13 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
*/
|
||||
saveProject = async () => {
|
||||
if (this.readOnly) return
|
||||
try {
|
||||
const fileHandle = await saveToFileSystem(
|
||||
migrate(this.state, TldrawApp.version).document,
|
||||
this.fileSystemHandle
|
||||
)
|
||||
this.fileSystemHandle = fileHandle
|
||||
this.persist({})
|
||||
this.isDirty = false
|
||||
} catch (e: any) {
|
||||
// Likely cancelled
|
||||
console.error(e.message)
|
||||
}
|
||||
const fileHandle = await saveToFileSystem(
|
||||
migrate(this.state, TldrawApp.version).document,
|
||||
this.fileSystemHandle
|
||||
)
|
||||
this.fileSystemHandle = fileHandle
|
||||
this.persist({})
|
||||
this.isDirty = false
|
||||
return this
|
||||
}
|
||||
|
||||
|
@ -2100,7 +2114,6 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
/**
|
||||
* Copy one or more shapes as SVG.
|
||||
* @param ids The ids of the shapes to copy.
|
||||
* @param pageId The page from which to copy the shapes.
|
||||
* @returns A string containing the JSON.
|
||||
*/
|
||||
copySvg = async (
|
||||
|
@ -2220,7 +2233,6 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
/**
|
||||
* Copy one or more shapes as JSON.
|
||||
* @param ids The ids of the shapes to copy.
|
||||
* @param pageId The page from which to copy the shapes.
|
||||
* @returns A string containing the JSON.
|
||||
*/
|
||||
copyJson = (ids = this.selectedIds) => {
|
||||
|
|
Ładowanie…
Reference in New Issue