diff --git a/editor.html b/editor.html index 65431262..35c151b6 100644 --- a/editor.html +++ b/editor.html @@ -37,10 +37,5 @@ <body> <div id="root"></div> <script type="module" src="/src/main.tsx"></script> - <script> - window.onbeforeunload = function () { - return 'You are about to leave this page. All progress in QuickMock will be lost. Are you sure you want to proceed?'; - }; - </script> </body> </html> diff --git a/index.html b/index.html index 093d0723..d8affb1c 100644 --- a/index.html +++ b/index.html @@ -357,7 +357,8 @@ <h1> </p> </div> </div> - <a href="editor.html" class="link">Launch QuickMock</a> + <a href="editor.html" class="link hide-mobile">START DESIGN</a> + <p class="mobile-text mobile-only">Now, only available on desktop.</p> </div> </main> </body> diff --git a/src/core/providers/canvas/canvas.model.ts b/src/core/providers/canvas/canvas.model.ts index 4e45ddde..996c58f8 100644 --- a/src/core/providers/canvas/canvas.model.ts +++ b/src/core/providers/canvas/canvas.model.ts @@ -124,6 +124,7 @@ export interface CanvasContextModel { updateColorSlot: (color: string, index: number) => void; dropRef: React.MutableRefObject<HTMLDivElement | null>; setDropRef: (dropRef: React.MutableRefObject<HTMLDivElement | null>) => void; + setIsDirty: (dirty: boolean) => void; loadSampleDocument: boolean; setLoadSampleDocument: React.Dispatch<React.SetStateAction<boolean>>; } diff --git a/src/core/providers/canvas/canvas.provider.tsx b/src/core/providers/canvas/canvas.provider.tsx index 5e1ae786..d94b9768 100644 --- a/src/core/providers/canvas/canvas.provider.tsx +++ b/src/core/providers/canvas/canvas.provider.tsx @@ -51,10 +51,34 @@ export const CanvasProvider: React.FC<Props> = props => { addSnapshot ); - const selectionInfo = useSelection(document, setDocument); + const [isDirty, setIsDirty] = React.useState(false); + + const setDocumentAndMarkDirtyState = ( + updater: DocumentModel | ((prev: DocumentModel) => DocumentModel), + isDirty = true + ) => { + setDocument(updater); + setIsDirty(isDirty); + }; + + const selectionInfo = useSelection( + document, + setDocument, + setDocumentAndMarkDirtyState + ); + + React.useEffect(() => { + const handleBeforeUnload = (e: BeforeUnloadEvent) => { + if (isDirty) { + e.preventDefault(); + } + }; + window.addEventListener('beforeunload', handleBeforeUnload); + return () => window.removeEventListener('beforeunload', handleBeforeUnload); + }, [isDirty]); const addNewPage = () => { - setDocument(lastDocument => + setDocumentAndMarkDirtyState(lastDocument => produce(lastDocument, draft => { const newActiveIndex = draft.pages.length; draft.pages.push({ @@ -76,7 +100,7 @@ export const CanvasProvider: React.FC<Props> = props => { } ); - setDocument(lastDocument => + setDocumentAndMarkDirtyState(lastDocument => produce(lastDocument, draft => { const newPage = { id: uuidv4(), @@ -96,7 +120,7 @@ export const CanvasProvider: React.FC<Props> = props => { ? document.pages[pageIndex + 1].id // If it's not the last page, select the next one : document.pages[pageIndex - 1].id; // Otherwise, select the previous one - setDocument(lastDocument => + setDocumentAndMarkDirtyState(lastDocument => produce(lastDocument, draft => { draft.pages = draft.pages.filter( currentPage => document.pages[pageIndex].id !== currentPage.id @@ -119,7 +143,7 @@ export const CanvasProvider: React.FC<Props> = props => { selectionInfo.clearSelection(); selectionInfo.shapeRefs.current = {}; - setDocument(lastDocument => + setDocumentAndMarkDirtyState(lastDocument => produce(lastDocument, draft => { const pageIndex = draft.pages.findIndex(page => page.id === pageId); if (pageIndex !== -1) { @@ -130,7 +154,7 @@ export const CanvasProvider: React.FC<Props> = props => { }; const editPageTitle = (pageIndex: number, newName: string) => { - setDocument(lastDocument => + setDocumentAndMarkDirtyState(lastDocument => produce(lastDocument, draft => { draft.pages[pageIndex].name = newName; }) @@ -138,7 +162,7 @@ export const CanvasProvider: React.FC<Props> = props => { }; const swapPages = (id1: string, id2: string) => { - setDocument(lastDocument => + setDocumentAndMarkDirtyState(lastDocument => produce(lastDocument, draft => { const index1 = draft.pages.findIndex(page => page.id === id1); const index2 = draft.pages.findIndex(page => page.id === id2); @@ -159,7 +183,7 @@ export const CanvasProvider: React.FC<Props> = props => { }); if (isPageIndexValid(document)) { - setDocument(lastDocument => + setDocumentAndMarkDirtyState(lastDocument => produce(lastDocument, draft => { draft.pages[lastDocument.activePageIndex].shapes.push(...newShapes); }) @@ -198,13 +222,13 @@ export const CanvasProvider: React.FC<Props> = props => { ); const createNewFullDocument = () => { - setDocument(createDefaultDocumentModel()); + setDocumentAndMarkDirtyState(createDefaultDocumentModel(), false); setFileName(''); }; const deleteSelectedShapes = () => { if (isPageIndexValid(document)) { - setDocument(lastDocument => + setDocumentAndMarkDirtyState(lastDocument => produce(lastDocument, draft => { draft.pages[lastDocument.activePageIndex].shapes = removeShapesFromList( @@ -230,7 +254,7 @@ export const CanvasProvider: React.FC<Props> = props => { const newShape = createShape({ x, y }, type, otherProps); - setDocument(lastDocument => + setDocumentAndMarkDirtyState(lastDocument => produce(lastDocument, draft => { draft.pages[lastDocument.activePageIndex].shapes.push(newShape); }) @@ -260,7 +284,7 @@ export const CanvasProvider: React.FC<Props> = props => { }); }); } else { - setDocument(fullDocument => { + setDocumentAndMarkDirtyState(fullDocument => { return produce(fullDocument, draft => { draft.pages[document.activePageIndex].shapes = draft.pages[ document.activePageIndex @@ -274,7 +298,7 @@ export const CanvasProvider: React.FC<Props> = props => { const updateShapePosition = (id: string, { x, y }: Coord) => { if (isPageIndexValid(document)) { - setDocument(fullDocument => { + setDocumentAndMarkDirtyState(fullDocument => { return produce(fullDocument, draft => { draft.pages[document.activePageIndex].shapes = draft.pages[ document.activePageIndex @@ -307,6 +331,7 @@ export const CanvasProvider: React.FC<Props> = props => { }; const loadDocument = (document: DocumentModel) => { + setDocumentAndMarkDirtyState(document, false); loadSampleDocument && setLoadSampleDocument(false); setDocument(document); setHowManyLoadedDocuments(numberOfDocuments => numberOfDocuments + 1); @@ -373,6 +398,7 @@ export const CanvasProvider: React.FC<Props> = props => { updateColorSlot, dropRef, setDropRef, + setIsDirty, loadSampleDocument, setLoadSampleDocument, }} diff --git a/src/core/providers/canvas/use-selection.hook.ts b/src/core/providers/canvas/use-selection.hook.ts index 1ae9c249..995a5a72 100644 --- a/src/core/providers/canvas/use-selection.hook.ts +++ b/src/core/providers/canvas/use-selection.hook.ts @@ -8,7 +8,11 @@ import { produce } from 'immer'; export const useSelection = ( document: DocumentModel, - setDocument: React.Dispatch<React.SetStateAction<DocumentModel>> + setDocument: React.Dispatch<React.SetStateAction<DocumentModel>>, + setDocumentAndMarkDirtyState: ( + updater: DocumentModel | ((prev: DocumentModel) => DocumentModel), + isDirty?: boolean + ) => void ): SelectionInfo => { const transformerRef = useRef<Konva.Transformer>(null); const shapeRefs = useRef<ShapeRefs>({}); @@ -165,7 +169,7 @@ export const useSelection = ( } const selectedShapeId = selectedShapesIds[0]; - setDocument(prevDocument => + setDocumentAndMarkDirtyState(prevDocument => produce(prevDocument, draft => { draft.pages[prevDocument.activePageIndex].shapes = draft.pages[ prevDocument.activePageIndex @@ -181,7 +185,7 @@ export const useSelection = ( key: K, value: OtherProps[K] ) => { - setDocument(prevDocument => + setDocumentAndMarkDirtyState(prevDocument => produce(prevDocument, draft => { draft.pages[prevDocument.activePageIndex].shapes = draft.pages[ prevDocument.activePageIndex @@ -198,7 +202,7 @@ export const useSelection = ( key: K, value: OtherProps[K] ) => { - setDocument(prevDocument => + setDocumentAndMarkDirtyState(prevDocument => produce(prevDocument, draft => { draft.pages[prevDocument.activePageIndex].shapes = draft.pages[ prevDocument.activePageIndex @@ -225,7 +229,6 @@ export const useSelection = ( if (selectedShapesIds.length === 1) { const selectedShapeId = selectedShapesIds[0]; updateOtherPropsOnSelectedSingleShape(selectedShapeId, key, value); - return; } diff --git a/src/pods/toolbar/components/save-button/save-button.tsx b/src/pods/toolbar/components/save-button/save-button.tsx index 61294a91..b1b5d9c5 100644 --- a/src/pods/toolbar/components/save-button/save-button.tsx +++ b/src/pods/toolbar/components/save-button/save-button.tsx @@ -2,13 +2,20 @@ import { SaveIcon } from '@/common/components/icons/save-icon.component'; import classes from '@/pods/toolbar/toolbar.pod.module.css'; import { ToolbarButton } from '../toolbar-button'; import { useLocalDisk } from '@/core/local-disk'; +import { useCanvasContext } from '@/core/providers'; export const SaveButton: React.FC = () => { const { handleSave } = useLocalDisk(); + const { setIsDirty } = useCanvasContext(); + + const handleSaveLocal = () => { + handleSave(); + setIsDirty(false); + }; return ( <ToolbarButton - onClick={handleSave} + onClick={handleSaveLocal} className={classes.button} icon={<SaveIcon />} label="Save"