group object selection preview added, object locked/unloack feature added
This commit is contained in:
parent
24f99a77fb
commit
fa766e8ed3
13 changed files with 669 additions and 68 deletions
221
package-lock.json
generated
221
package-lock.json
generated
|
|
@ -21,6 +21,7 @@
|
||||||
"@radix-ui/react-slot": "^1.1.0",
|
"@radix-ui/react-slot": "^1.1.0",
|
||||||
"@radix-ui/react-switch": "^1.1.2",
|
"@radix-ui/react-switch": "^1.1.2",
|
||||||
"@radix-ui/react-tabs": "^1.1.2",
|
"@radix-ui/react-tabs": "^1.1.2",
|
||||||
|
"@radix-ui/react-toast": "^1.2.4",
|
||||||
"@radix-ui/react-tooltip": "^1.1.4",
|
"@radix-ui/react-tooltip": "^1.1.4",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
|
@ -2324,6 +2325,226 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-toast": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-Sch9idFJHJTMH9YNpxxESqABcAFweJG4tKv+0zo0m5XBvUSL8FM5xKcJLFLXononpePs8IclyX1KieL5SDUNgA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.1",
|
||||||
|
"@radix-ui/react-collection": "1.1.1",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.1",
|
||||||
|
"@radix-ui/react-context": "1.1.1",
|
||||||
|
"@radix-ui/react-dismissable-layer": "1.1.3",
|
||||||
|
"@radix-ui/react-portal": "1.1.3",
|
||||||
|
"@radix-ui/react-presence": "1.1.2",
|
||||||
|
"@radix-ui/react-primitive": "2.0.1",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.0",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.1.0",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.0",
|
||||||
|
"@radix-ui/react-visually-hidden": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-toast/node_modules/@radix-ui/primitive": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-collection": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-LwT3pSho9Dljg+wY2KN2mrrh6y3qELfftINERIzBUO9e0N+t0oMTyn3k9iv+ZqgrwGkRnLpNJrsMv9BZlt2yuA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.1",
|
||||||
|
"@radix-ui/react-context": "1.1.1",
|
||||||
|
"@radix-ui/react-primitive": "2.0.1",
|
||||||
|
"@radix-ui/react-slot": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-compose-refs": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-dismissable-layer": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-onrWn/72lQoEucDmJnr8uczSNTujT0vJnA/X5+3AkChVPowr8n1yvIKIabhWyMQeMvvmdpsvcyDqx3X1LEXCPg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.1",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.1",
|
||||||
|
"@radix-ui/react-primitive": "2.0.1",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.0",
|
||||||
|
"@radix-ui/react-use-escape-keydown": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-portal": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-primitive": "2.0.1",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-presence": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.1",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-primitive": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-slot": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-slot": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-visually-hidden": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-vVfA2IZ9q/J+gEamvj761Oq1FpWgCDaNOOIfbPVp2MVPLEomUr5+Vf7kJGwQ24YxZSlQVar7Bes8kyTo5Dshpg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-primitive": "2.0.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-tooltip": {
|
"node_modules/@radix-ui/react-tooltip": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.4.tgz",
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
"@radix-ui/react-slot": "^1.1.0",
|
"@radix-ui/react-slot": "^1.1.0",
|
||||||
"@radix-ui/react-switch": "^1.1.2",
|
"@radix-ui/react-switch": "^1.1.2",
|
||||||
"@radix-ui/react-tabs": "^1.1.2",
|
"@radix-ui/react-tabs": "^1.1.2",
|
||||||
|
"@radix-ui/react-toast": "^1.2.4",
|
||||||
"@radix-ui/react-tooltip": "^1.1.4",
|
"@radix-ui/react-tooltip": "^1.1.4",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
|
@ -57,4 +58,4 @@
|
||||||
"tailwindcss": "^3.4.14",
|
"tailwindcss": "^3.4.14",
|
||||||
"vite": "^5.4.10"
|
"vite": "^5.4.10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import Header from './components/Layouts/Header';
|
||||||
import SheetRightPanel from './components/Layouts/SheetRightPanel';
|
import SheetRightPanel from './components/Layouts/SheetRightPanel';
|
||||||
import SheetLeftPanel from './components/Layouts/SheetLeftPanel';
|
import SheetLeftPanel from './components/Layouts/SheetLeftPanel';
|
||||||
import CanvasCapture from "./components/CanvasCapture";
|
import CanvasCapture from "./components/CanvasCapture";
|
||||||
|
import { Toaster } from './components/ui/toaster';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -68,6 +69,7 @@ function App() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex flex-col h-screen overflow-hidden">
|
<div className="relative flex flex-col h-screen overflow-hidden">
|
||||||
|
<Toaster />
|
||||||
<SheetLeftPanel />
|
<SheetLeftPanel />
|
||||||
<Header />
|
<Header />
|
||||||
<SheetRightPanel />
|
<SheetRightPanel />
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@ const Canvas = () => {
|
||||||
};
|
};
|
||||||
}, [removeSelected]);
|
}, [removeSelected]);
|
||||||
|
|
||||||
// console.log(activeObject);
|
console.log(activeObject);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col items-center relative w-full h-full overflow-hidden'>
|
<div className='flex flex-col items-center relative w-full h-full overflow-hidden'>
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ const AddImageIntoShape = () => {
|
||||||
const [rotation, setRotation] = useState("0");
|
const [rotation, setRotation] = useState("0");
|
||||||
const [format, setFormat] = useState('JPEG');
|
const [format, setFormat] = useState('JPEG');
|
||||||
const fileInputRef = useRef(null);
|
const fileInputRef = useRef(null);
|
||||||
const [isOpen, setIsOpen] = useState(true);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
const handleResize = (file, callback) => {
|
const handleResize = (file, callback) => {
|
||||||
Resizer.imageFileResizer(
|
Resizer.imageFileResizer(
|
||||||
|
|
@ -136,14 +136,35 @@ const AddImageIntoShape = () => {
|
||||||
absolutePositioned: true,
|
absolutePositioned: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Calculate scale factors based on clip object size
|
||||||
|
let scaleX = activeObject.width / img.width;
|
||||||
|
let scaleY = activeObject.height / img.height;
|
||||||
|
if (activeObject?.width < 100) {
|
||||||
|
scaleX = 0.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeObject.height < 100) {
|
||||||
|
scaleY = 0.2;
|
||||||
|
}
|
||||||
|
|
||||||
// Create a fabric image object with scaling and clipPath
|
// Create a fabric image object with scaling and clipPath
|
||||||
const fabricImage = new fabric.Image(img, {
|
const fabricImage = new fabric.Image(img, {
|
||||||
scaleX: 0.2,
|
scaleX: scaleX,
|
||||||
scaleY: 0.2,
|
scaleY: scaleY,
|
||||||
left: activeObject.left,
|
left: activeObject.left,
|
||||||
top: activeObject.top,
|
top: activeObject.top,
|
||||||
clipPath: activeObject, // Apply clipPath to the image
|
clipPath: activeObject, // Apply clipPath to the image
|
||||||
|
originX: activeObject.originX, // Match origin point
|
||||||
|
originY: activeObject.originY // Match origin point
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Adjust position based on the clipPath's transformations
|
||||||
|
fabricImage.set({
|
||||||
|
left: activeObject.left,
|
||||||
|
top: activeObject.top,
|
||||||
|
angle: activeObject.angle // Match rotation if any
|
||||||
|
});
|
||||||
|
|
||||||
canvas.add(fabricImage);
|
canvas.add(fabricImage);
|
||||||
canvas.setActiveObject(fabricImage);
|
canvas.setActiveObject(fabricImage);
|
||||||
setActiveObject(fabricImage);
|
setActiveObject(fabricImage);
|
||||||
|
|
|
||||||
68
src/components/EachComponent/Customization/LockObject.jsx
Normal file
68
src/components/EachComponent/Customization/LockObject.jsx
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
import ActiveObjectContext from '@/components/Context/activeObject/ObjectContext';
|
||||||
|
import CanvasContext from '@/components/Context/canvasContext/CanvasContext';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { useToast } from '@/hooks/use-toast';
|
||||||
|
import { useContext, useEffect, useState } from 'react';
|
||||||
|
import { Lock, Unlock } from "lucide-react";
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
|
||||||
|
const LockObject = () => {
|
||||||
|
const { canvas } = useContext(CanvasContext);
|
||||||
|
const { activeObject } = useContext(ActiveObjectContext);
|
||||||
|
const [isLocked, setIsLocked] = useState(false);
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeObject?.lockMovementX === false) {
|
||||||
|
setIsLocked(false);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
setIsLocked(true);
|
||||||
|
}
|
||||||
|
}, [activeObject])
|
||||||
|
|
||||||
|
const toggleLock = () => {
|
||||||
|
if (!canvas || !activeObject) {
|
||||||
|
toast({
|
||||||
|
title: "No object selected",
|
||||||
|
description: "Please select an object to lock or unlock.",
|
||||||
|
variant: "destructive",
|
||||||
|
})
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newLockState = !activeObject.lockMovementX;
|
||||||
|
activeObject.set({
|
||||||
|
lockMovementX: newLockState,
|
||||||
|
lockMovementY: newLockState,
|
||||||
|
lockRotation: newLockState,
|
||||||
|
lockScalingX: newLockState,
|
||||||
|
lockScalingY: newLockState,
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsLocked(newLockState);
|
||||||
|
canvas.requestRenderAll();
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: newLockState ? "Object locked" : "Object unlocked",
|
||||||
|
description: newLockState ? "The object is now locked in place." : "The object can now be moved and resized.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="p-2">
|
||||||
|
<h2 className='font-bold'>{!isLocked ? "Lock" : "Unlock"} Object</h2>
|
||||||
|
<Button
|
||||||
|
onClick={toggleLock}
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
disabled={!activeObject}
|
||||||
|
title={isLocked ? "Unlock object" : "Lock object"}
|
||||||
|
>
|
||||||
|
{isLocked ? <Unlock className="h-4 w-4" /> : <Lock className="h-4 w-4" />}
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LockObject
|
||||||
|
|
@ -1,55 +1,80 @@
|
||||||
import ActiveObjectContext from '@/components/Context/activeObject/ObjectContext';
|
import ActiveObjectContext from '@/components/Context/activeObject/ObjectContext';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
import { useContext, useEffect, useState } from 'react'
|
import { useContext, useEffect, useRef, useState } from 'react'
|
||||||
import { Card, } from '@/components/ui/card';
|
import { Card, } from '@/components/ui/card';
|
||||||
import CanvasContext from '@/components/Context/canvasContext/CanvasContext';
|
import CanvasContext from '@/components/Context/canvasContext/CanvasContext';
|
||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
|
import { fabric } from 'fabric';
|
||||||
|
|
||||||
const SelectObjectFromGroup = () => {
|
const SelectObjectFromGroup = () => {
|
||||||
const [groupObjects, setGroupObjects] = useState([]);
|
const [groupObjects, setGroupObjects] = useState([])
|
||||||
const [selectedObject, setSelectedObject] = useState(null);
|
const [selectedObject, setSelectedObject] = useState(null)
|
||||||
const { setActiveObject } = useContext(ActiveObjectContext);
|
const { setActiveObject } = useContext(ActiveObjectContext)
|
||||||
const { canvas } = useContext(CanvasContext);
|
const { canvas } = useContext(CanvasContext)
|
||||||
|
const previewCanvasRef = useRef(null)
|
||||||
|
|
||||||
const activeObject = canvas?.getActiveObject();
|
const activeObject = canvas?.getActiveObject()
|
||||||
|
|
||||||
// Update group objects when the active object changes
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeObject?.type === 'group') {
|
if (activeObject?.type === 'group') {
|
||||||
setGroupObjects(activeObject._objects || []);
|
setGroupObjects(activeObject._objects || [])
|
||||||
setSelectedObject(null); // Reset selection when group changes
|
setSelectedObject(null)
|
||||||
} else {
|
} else {
|
||||||
setGroupObjects([]);
|
setGroupObjects([])
|
||||||
setSelectedObject(null);
|
setSelectedObject(null)
|
||||||
}
|
}
|
||||||
}, [activeObject]);
|
}, [activeObject])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedObject && previewCanvasRef.current) {
|
||||||
|
const previewCanvas = new fabric.StaticCanvas(previewCanvasRef.current);
|
||||||
|
|
||||||
|
previewCanvas.setDimensions({
|
||||||
|
width: 100,
|
||||||
|
height: 100
|
||||||
|
})
|
||||||
|
|
||||||
|
// Clone the active object to create a true deep copy
|
||||||
|
selectedObject.clone((clonedObject) => {
|
||||||
|
// Add the cloned object to the canvas
|
||||||
|
clonedObject.set({
|
||||||
|
left: 50,
|
||||||
|
top: 50,
|
||||||
|
originX: 'center',
|
||||||
|
originY: 'center',
|
||||||
|
selectable: false,
|
||||||
|
evented: false,
|
||||||
|
})
|
||||||
|
previewCanvas.add(clonedObject);
|
||||||
|
previewCanvas.renderAll();
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
previewCanvas.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [selectedObject])
|
||||||
|
|
||||||
// Handle object selection from dropdown
|
|
||||||
const handleSelectObject = (value) => {
|
const handleSelectObject = (value) => {
|
||||||
const selected = groupObjects[parseInt(value)];
|
const selected = groupObjects[parseInt(value)]
|
||||||
setSelectedObject(selected);
|
setSelectedObject(selected)
|
||||||
setActiveObject(selected);
|
setActiveObject(selected)
|
||||||
};
|
|
||||||
|
|
||||||
// Don't render if there's no active group
|
|
||||||
if (!activeObject || activeObject.type !== 'group') {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(selectedObject);
|
if (!activeObject || activeObject.type !== 'group') {
|
||||||
console.log(selectedObject?.group);
|
return null
|
||||||
console.log(selectedObject?.fill);
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Card className="p-2">
|
<Card className="p-4">
|
||||||
<h2 className='font-bold mb-2'>Group Objects</h2>
|
<h2 className='font-bold mb-4'>Group Objects</h2>
|
||||||
<div className='flex flex-col items-center justify-center'>
|
<div className='flex flex-col items-center justify-center space-y-4'>
|
||||||
<Select
|
<Select
|
||||||
onValueChange={handleSelectObject}
|
onValueChange={handleSelectObject}
|
||||||
value={selectedObject ? groupObjects.indexOf(selectedObject).toString() : undefined}
|
value={selectedObject ? groupObjects.indexOf(selectedObject).toString() : undefined}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-[200px]">
|
<SelectTrigger className="w-full max-w-xs">
|
||||||
<SelectValue placeholder="Select object" />
|
<SelectValue placeholder="Select object" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|
@ -60,37 +85,16 @@ const SelectObjectFromGroup = () => {
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
|
||||||
|
|
||||||
{selectedObject && (
|
{selectedObject && (
|
||||||
<div className='flex items-center justify-center bg-muted rounded-md my-2 mx-auto'>
|
<div className='w-[100px] h-[100px] bg-muted rounded-md overflow-hidden border border-gray-300'>
|
||||||
<svg viewBox='0 0 24 24'>
|
<canvas ref={previewCanvasRef} width={100} height={100} />
|
||||||
{selectedObject?.fill?.coords && selectedObject?.fill?.colorStops && (
|
</div>
|
||||||
<defs>
|
)}
|
||||||
<linearGradient id="gradient1"
|
</div>
|
||||||
x1={selectedObject.fill.coords.x1 || 0}
|
|
||||||
y1={selectedObject.fill.coords.y1 || 0}
|
|
||||||
x2={selectedObject.fill.coords.x2 || 1}
|
|
||||||
y2={selectedObject.fill.coords.y2 || 1}>
|
|
||||||
{selectedObject.fill.colorStops.map((stop, index) => (
|
|
||||||
<stop key={index}
|
|
||||||
offset={`${(stop.offset || 0) * 100}%`}
|
|
||||||
stopColor={stop.color || 'black'} />
|
|
||||||
))}
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
)}
|
|
||||||
<path
|
|
||||||
d={selectedObject?.d}
|
|
||||||
fill={selectedObject?.fill?.coords ? 'url(#gradient1)' : selectedObject?.fill}
|
|
||||||
stroke={selectedObject?.stroke}
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Card>
|
</Card>
|
||||||
<Separator className="my-2" />
|
<Separator className="my-4" />
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
export default SelectObjectFromGroup;
|
export default SelectObjectFromGroup;
|
||||||
|
|
@ -22,7 +22,7 @@ const ShadowCustomization = () => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeObject) {
|
if (activeObject) {
|
||||||
setShadowColor(activeObject?.shadow?.color || "#ffffff");
|
setShadowColor(activeObject?.shadow?.color || "#db1d62");
|
||||||
setOffsetX(activeObject?.shadow?.offsetX || 5);
|
setOffsetX(activeObject?.shadow?.offsetX || 5);
|
||||||
setOffsetY(activeObject?.shadow?.offsetY || 5);
|
setOffsetY(activeObject?.shadow?.offsetY || 5);
|
||||||
setBlur(activeObject?.shadow?.blur || 0.5);
|
setBlur(activeObject?.shadow?.blur || 0.5);
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import PositionCustomization from './Customization/PositionCustomization';
|
||||||
import CollapsibleComponent from './Customization/CollapsibleComponent';
|
import CollapsibleComponent from './Customization/CollapsibleComponent';
|
||||||
import ImageCustomization from './Customization/ImageCustomization';
|
import ImageCustomization from './Customization/ImageCustomization';
|
||||||
import SelectObjectFromGroup from './Customization/SelectObjectFromGroup';
|
import SelectObjectFromGroup from './Customization/SelectObjectFromGroup';
|
||||||
|
import LockObject from './Customization/LockObject';
|
||||||
|
|
||||||
const CustomizeShape = () => {
|
const CustomizeShape = () => {
|
||||||
const { canvas } = useContext(CanvasContext);
|
const { canvas } = useContext(CanvasContext);
|
||||||
|
|
@ -29,11 +30,14 @@ const CustomizeShape = () => {
|
||||||
return <p className='text-sm font-semibold'>No active object found</p>;
|
return <p className='text-sm font-semibold'>No active object found</p>;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(activeObject?.type);
|
// console.log(activeObject?.type);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='p-1'>
|
<div className='p-1'>
|
||||||
|
|
||||||
|
<LockObject />
|
||||||
|
<Separator className="my-2" />
|
||||||
|
|
||||||
<SelectObjectFromGroup />
|
<SelectObjectFromGroup />
|
||||||
|
|
||||||
{/* Apply fill and background color */}
|
{/* Apply fill and background color */}
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,15 @@ import * as lucideIcons from "lucide-react";
|
||||||
import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
|
import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
|
||||||
import { fabric } from 'fabric';
|
import { fabric } from 'fabric';
|
||||||
import ActiveObjectContext from "@/components/Context/activeObject/ObjectContext";
|
import ActiveObjectContext from "@/components/Context/activeObject/ObjectContext";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
|
||||||
const AllIconsPage = () => {
|
const AllIconsPage = () => {
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const { canvas } = useContext(CanvasContext);
|
const { canvas } = useContext(CanvasContext);
|
||||||
const { setActiveObject } = useContext(ActiveObjectContext);
|
const { setActiveObject } = useContext(ActiveObjectContext);
|
||||||
|
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
// Assume icons is already defined as shown previously, and filtered is created based on the search query
|
// Assume icons is already defined as shown previously, and filtered is created based on the search query
|
||||||
const icons = Object.entries(lucideIcons)?.filter(([name, Icon]) =>
|
const icons = Object.entries(lucideIcons)?.filter(([name, Icon]) =>
|
||||||
!name.includes("Icon") && Icon?.$$typeof
|
!name.includes("Icon") && Icon?.$$typeof
|
||||||
|
|
@ -32,7 +35,11 @@ const AllIconsPage = () => {
|
||||||
const svgString = new XMLSerializer().serializeToString(e.target);
|
const svgString = new XMLSerializer().serializeToString(e.target);
|
||||||
handleAddIcon(svgString);
|
handleAddIcon(svgString);
|
||||||
} else {
|
} else {
|
||||||
window.alert('The target is a path element! Select the full icon.');
|
toast({
|
||||||
|
title: "Invalid Choice",
|
||||||
|
description: "The target is a path element! Select the full icon.",
|
||||||
|
variant: "destructive",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -66,8 +73,8 @@ const AllIconsPage = () => {
|
||||||
|
|
||||||
const iconGroup = fabric.util.groupSVGElements(objects, options);
|
const iconGroup = fabric.util.groupSVGElements(objects, options);
|
||||||
iconGroup.set({
|
iconGroup.set({
|
||||||
left: 100,
|
left: canvas.width / 2,
|
||||||
top: 100,
|
top: canvas.height / 2,
|
||||||
originX: 'center',
|
originX: 'center',
|
||||||
originY: 'center',
|
originY: 'center',
|
||||||
scaleX: 6,
|
scaleX: 6,
|
||||||
|
|
|
||||||
85
src/components/ui/toast.jsx
Normal file
85
src/components/ui/toast.jsx
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import * as ToastPrimitives from "@radix-ui/react-toast"
|
||||||
|
import { cva } from "class-variance-authority";
|
||||||
|
import { X } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const ToastProvider = ToastPrimitives.Provider
|
||||||
|
|
||||||
|
const ToastViewport = React.forwardRef(({ className, ...props }, ref) => (
|
||||||
|
<ToastPrimitives.Viewport
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"fixed top-0 z-[1000] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props} />
|
||||||
|
))
|
||||||
|
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
|
||||||
|
|
||||||
|
const toastVariants = cva(
|
||||||
|
"group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "border bg-background text-foreground",
|
||||||
|
destructive:
|
||||||
|
"destructive group border-destructive bg-destructive text-destructive-foreground",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const Toast = React.forwardRef(({ className, variant, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
(<ToastPrimitives.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(toastVariants({ variant }), className)}
|
||||||
|
{...props} />)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
Toast.displayName = ToastPrimitives.Root.displayName
|
||||||
|
|
||||||
|
const ToastAction = React.forwardRef(({ className, ...props }, ref) => (
|
||||||
|
<ToastPrimitives.Action
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props} />
|
||||||
|
))
|
||||||
|
ToastAction.displayName = ToastPrimitives.Action.displayName
|
||||||
|
|
||||||
|
const ToastClose = React.forwardRef(({ className, ...props }, ref) => (
|
||||||
|
<ToastPrimitives.Close
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
toast-close=""
|
||||||
|
{...props}>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</ToastPrimitives.Close>
|
||||||
|
))
|
||||||
|
ToastClose.displayName = ToastPrimitives.Close.displayName
|
||||||
|
|
||||||
|
const ToastTitle = React.forwardRef(({ className, ...props }, ref) => (
|
||||||
|
<ToastPrimitives.Title
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm font-semibold [&+div]:text-xs", className)}
|
||||||
|
{...props} />
|
||||||
|
))
|
||||||
|
ToastTitle.displayName = ToastPrimitives.Title.displayName
|
||||||
|
|
||||||
|
const ToastDescription = React.forwardRef(({ className, ...props }, ref) => (
|
||||||
|
<ToastPrimitives.Description ref={ref} className={cn("text-sm opacity-90", className)} {...props} />
|
||||||
|
))
|
||||||
|
ToastDescription.displayName = ToastPrimitives.Description.displayName
|
||||||
|
|
||||||
|
export { ToastProvider, ToastViewport, Toast, ToastTitle, ToastDescription, ToastClose, ToastAction };
|
||||||
33
src/components/ui/toaster.jsx
Normal file
33
src/components/ui/toaster.jsx
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { useToast } from "@/hooks/use-toast"
|
||||||
|
import {
|
||||||
|
Toast,
|
||||||
|
ToastClose,
|
||||||
|
ToastDescription,
|
||||||
|
ToastProvider,
|
||||||
|
ToastTitle,
|
||||||
|
ToastViewport,
|
||||||
|
} from "@/components/ui/toast"
|
||||||
|
|
||||||
|
export function Toaster() {
|
||||||
|
const { toasts } = useToast()
|
||||||
|
|
||||||
|
return (
|
||||||
|
(<ToastProvider>
|
||||||
|
{toasts.map(function ({ id, title, description, action, ...props }) {
|
||||||
|
return (
|
||||||
|
(<Toast key={id} {...props}>
|
||||||
|
<div className="grid gap-1">
|
||||||
|
{title && <ToastTitle>{title}</ToastTitle>}
|
||||||
|
{description && (
|
||||||
|
<ToastDescription>{description}</ToastDescription>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{action}
|
||||||
|
<ToastClose />
|
||||||
|
</Toast>)
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<ToastViewport />
|
||||||
|
</ToastProvider>)
|
||||||
|
);
|
||||||
|
}
|
||||||
155
src/hooks/use-toast.js
Normal file
155
src/hooks/use-toast.js
Normal file
|
|
@ -0,0 +1,155 @@
|
||||||
|
"use client";
|
||||||
|
// Inspired by react-hot-toast library
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
const TOAST_LIMIT = 1
|
||||||
|
const TOAST_REMOVE_DELAY = 1000000
|
||||||
|
|
||||||
|
const actionTypes = {
|
||||||
|
ADD_TOAST: "ADD_TOAST",
|
||||||
|
UPDATE_TOAST: "UPDATE_TOAST",
|
||||||
|
DISMISS_TOAST: "DISMISS_TOAST",
|
||||||
|
REMOVE_TOAST: "REMOVE_TOAST"
|
||||||
|
}
|
||||||
|
|
||||||
|
let count = 0
|
||||||
|
|
||||||
|
function genId() {
|
||||||
|
count = (count + 1) % Number.MAX_SAFE_INTEGER
|
||||||
|
return count.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
const toastTimeouts = new Map()
|
||||||
|
|
||||||
|
const addToRemoveQueue = (toastId) => {
|
||||||
|
if (toastTimeouts.has(toastId)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
toastTimeouts.delete(toastId)
|
||||||
|
dispatch({
|
||||||
|
type: "REMOVE_TOAST",
|
||||||
|
toastId: toastId,
|
||||||
|
})
|
||||||
|
}, TOAST_REMOVE_DELAY)
|
||||||
|
|
||||||
|
toastTimeouts.set(toastId, timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const reducer = (state, action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case "ADD_TOAST":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||||
|
};
|
||||||
|
|
||||||
|
case "UPDATE_TOAST":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: state.toasts.map((t) =>
|
||||||
|
t.id === action.toast.id ? { ...t, ...action.toast } : t),
|
||||||
|
};
|
||||||
|
|
||||||
|
case "DISMISS_TOAST": {
|
||||||
|
const { toastId } = action
|
||||||
|
|
||||||
|
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
||||||
|
// but I'll keep it here for simplicity
|
||||||
|
if (toastId) {
|
||||||
|
addToRemoveQueue(toastId)
|
||||||
|
} else {
|
||||||
|
state.toasts.forEach((toast) => {
|
||||||
|
addToRemoveQueue(toast.id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: state.toasts.map((t) =>
|
||||||
|
t.id === toastId || toastId === undefined
|
||||||
|
? {
|
||||||
|
...t,
|
||||||
|
open: false,
|
||||||
|
}
|
||||||
|
: t),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case "REMOVE_TOAST":
|
||||||
|
if (action.toastId === undefined) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const listeners = []
|
||||||
|
|
||||||
|
let memoryState = { toasts: [] }
|
||||||
|
|
||||||
|
function dispatch(action) {
|
||||||
|
memoryState = reducer(memoryState, action)
|
||||||
|
listeners.forEach((listener) => {
|
||||||
|
listener(memoryState)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function toast({
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
const id = genId()
|
||||||
|
|
||||||
|
const update = (props) =>
|
||||||
|
dispatch({
|
||||||
|
type: "UPDATE_TOAST",
|
||||||
|
toast: { ...props, id },
|
||||||
|
})
|
||||||
|
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: "ADD_TOAST",
|
||||||
|
toast: {
|
||||||
|
...props,
|
||||||
|
id,
|
||||||
|
open: true,
|
||||||
|
onOpenChange: (open) => {
|
||||||
|
if (!open) dismiss()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: id,
|
||||||
|
dismiss,
|
||||||
|
update,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function useToast() {
|
||||||
|
const [state, setState] = React.useState(memoryState)
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
listeners.push(setState)
|
||||||
|
return () => {
|
||||||
|
const index = listeners.indexOf(setState)
|
||||||
|
if (index > -1) {
|
||||||
|
listeners.splice(index, 1)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [state])
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toast,
|
||||||
|
dismiss: (toastId) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useToast, toast }
|
||||||
Loading…
Add table
Reference in a new issue