fix: fixed the selection and delete widget
This commit is contained in:
61
src/App.js
61
src/App.js
@@ -19,6 +19,9 @@ function App() {
|
||||
* @type {Canvas | null>}
|
||||
*/
|
||||
const canvasRef = useRef()
|
||||
const widgetOverlayRef = useRef()
|
||||
const [initialPosition, setInitialPosition] = useState({ x: 0, y: 0 })
|
||||
|
||||
|
||||
const [uploadedAssets, setUploadedAssets] = useState([]) // a global storage for assets, since redux can't store files(serialize files)
|
||||
|
||||
@@ -56,9 +59,23 @@ function App() {
|
||||
console.log("Drag start: ", event)
|
||||
const draggedItem = sidebarWidgets.find((item) => item.name === event.active.id)
|
||||
setActiveSidebarWidget(draggedItem)
|
||||
|
||||
const activeItemElement = widgetOverlayRef.current
|
||||
|
||||
if (activeItemElement) {
|
||||
const rect = activeItemElement.getBoundingClientRect()
|
||||
|
||||
// Store the initial position of the dragged element
|
||||
setInitialPosition({
|
||||
x: rect.left,
|
||||
y: rect.top,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleDragMove = (event) => {
|
||||
|
||||
// console.log("drag move: ", event)
|
||||
}
|
||||
|
||||
const handleDragEnd = (event) => {
|
||||
@@ -67,10 +84,11 @@ function App() {
|
||||
const {active, over, delta, activatorEvent} = event
|
||||
|
||||
const widgetItem = active.data.current?.title
|
||||
const activeItemElement = document.getElementById(`${active.id}`)
|
||||
const activeItemElement = widgetOverlayRef.current
|
||||
|
||||
// console.log("ended: ", activatorEvent, "delta", delta, "drag ended: ", event, "active: ", active, "over: ", over)
|
||||
console.log("over: ", active, over, activeItemElement)
|
||||
|
||||
console.log("ended: ", activatorEvent.clientX, activatorEvent.clientY)
|
||||
// console.log("over: ", active, over, activeItemElement)
|
||||
if (over?.id !== "canvas-droppable" || !widgetItem) {
|
||||
setDropAnimation({ duration: 250, easing: "ease" })
|
||||
return
|
||||
@@ -79,34 +97,40 @@ function App() {
|
||||
|
||||
// FIXME: drop offset is not correct
|
||||
// Calculate the dragged item's bounding rectangle
|
||||
// const itemRect = activeItemElement.getBoundingClientRect();
|
||||
// const itemCenterX = itemRect.left + itemRect.width / 2
|
||||
// const itemCenterY = itemRect.top + itemRect.height / 2
|
||||
const itemRect = activeItemElement.getBoundingClientRect();
|
||||
const itemCenterX = itemRect.left + itemRect.width / 2
|
||||
const itemCenterY = itemRect.top + itemRect.height / 2
|
||||
|
||||
console.log("widget overlay: ", delta, itemRect)
|
||||
|
||||
|
||||
// // Calculate cursor position relative to the canvas
|
||||
// const cursorX = activatorEvent.clientX
|
||||
// const cursorY = activatorEvent.clientY
|
||||
|
||||
// // Calculate the offset from the center of the item to the cursor
|
||||
// const offsetX = cursorX - itemCenterX
|
||||
// const offsetY = cursorY - itemCenterY
|
||||
// Get widget dimensions (assuming you have a way to get these)
|
||||
const widgetWidth = activeItemElement.offsetWidth; // Adjust this based on how you get widget size
|
||||
const widgetHeight = activeItemElement.offsetHeight; // Adjust this based on how you get widget size
|
||||
|
||||
|
||||
const canvasContainerRect = canvasRef.current.getCanvasContainerBoundingRect()
|
||||
const canvasTranslate = canvasRef.current.getCanvasTranslation()
|
||||
const zoom = canvasRef.current.getZoom()
|
||||
|
||||
// let finalPosition = {
|
||||
// x: (delta.x - canvasContainerRect.x - canvasTranslate.x) / zoom,
|
||||
// y: (delta.y - canvasContainerRect.y - canvasTranslate.y) / zoom,
|
||||
// }
|
||||
|
||||
let finalPosition = {
|
||||
x: (delta.x - canvasContainerRect.x - canvasTranslate.x) / zoom,
|
||||
y: (delta.y - canvasContainerRect.y - canvasTranslate.y) / zoom,
|
||||
x: (initialPosition.x + delta.x - canvasContainerRect.x - canvasTranslate.x) / zoom - (widgetWidth / 2),
|
||||
y: (initialPosition.y + delta.y - canvasContainerRect.y - canvasTranslate.y) / zoom - (widgetHeight / 2),
|
||||
}
|
||||
|
||||
|
||||
// find the center of the active widget then set the final position
|
||||
|
||||
// finalPosition = {
|
||||
// finalPosition
|
||||
// }
|
||||
|
||||
// console.log("drop position: ", "delta: ", delta, "activator", canvasContainerRect, canvasTranslate,)
|
||||
console.log("drop position: ", "delta: ", delta, "activator", finalPosition, canvasTranslate,)
|
||||
|
||||
canvasRef.current.addWidget(Widget, ({id, widgetRef}) => {
|
||||
widgetRef.current.setPos(finalPosition.x, finalPosition.y)
|
||||
@@ -126,7 +150,7 @@ function App() {
|
||||
<div className="tw-w-full tw-h-[100vh] tw-flex tw-flex-col tw-bg-primaryBg">
|
||||
<Header className="tw-h-[6vh]"/>
|
||||
|
||||
<DndContext sensors={sensors}
|
||||
<DndContext
|
||||
modifiers={[snapCenterToCursor]}
|
||||
collisionDetection={rectIntersection}
|
||||
onDragStart={handleDragStart}
|
||||
@@ -140,11 +164,12 @@ function App() {
|
||||
</div>
|
||||
|
||||
{/* dragOverlay (dnd-kit) helps move items from one container to another */}
|
||||
<DragOverlay dropAnimation={dropAnimation} >
|
||||
<DragOverlay dropAnimation={dropAnimation}>
|
||||
{activeSidebarWidget ? (
|
||||
<DraggableWidgetCard name={activeSidebarWidget.name}
|
||||
img={activeSidebarWidget.img}
|
||||
url={activeSidebarWidget.link}
|
||||
innerRef={widgetOverlayRef}
|
||||
/>
|
||||
):
|
||||
null}
|
||||
|
||||
@@ -2,13 +2,14 @@ import React from "react"
|
||||
|
||||
import {DndContext} from '@dnd-kit/core'
|
||||
|
||||
import { CloseOutlined, FullscreenOutlined, ReloadOutlined } from "@ant-design/icons"
|
||||
import { CloseOutlined, DeleteOutlined, EditOutlined, FullscreenOutlined, ReloadOutlined } from "@ant-design/icons"
|
||||
import { Button, Tooltip, Dropdown } from "antd"
|
||||
|
||||
import Droppable from "../components/utils/droppable"
|
||||
import Widget from "./widgets/base"
|
||||
import Cursor from "./constants/cursor"
|
||||
import { UID } from "../utils/uid"
|
||||
import { removeDuplicateObjects } from "../utils/common"
|
||||
|
||||
|
||||
const CanvasModes = {
|
||||
@@ -71,6 +72,8 @@ class Canvas extends React.Component {
|
||||
this.fitCanvasToBoundingBox = this.fitCanvasToBoundingBox.bind(this)
|
||||
this.getCanvasBoundingRect = this.getCanvasContainerBoundingRect.bind(this)
|
||||
|
||||
this.deleteSelectedWidgets = this.deleteSelectedWidgets.bind(this)
|
||||
this.removeWidget = this.removeWidget.bind(this)
|
||||
this.clearSelections = this.clearSelections.bind(this)
|
||||
this.clearCanvas = this.clearCanvas.bind(this)
|
||||
|
||||
@@ -159,19 +162,23 @@ class Canvas extends React.Component {
|
||||
if (selectedWidget){
|
||||
// if the widget is selected don't pan, instead move the widget
|
||||
if (!selectedWidget._disableSelection){
|
||||
selectedWidget.select()
|
||||
|
||||
if (this.selectedWidgets.length >= 1){ // allow only one selection for now
|
||||
this.clearSelections()
|
||||
const selectedLength = this.selectedWidgets.length
|
||||
|
||||
if (selectedLength === 0 || (selectedLength === 1 && selectedWidget.__id !== this.selectedWidgets[0].__id)){
|
||||
this.selectedWidgets[0]?.deSelect() // deselect the previous widget before adding the new one
|
||||
this.selectedWidgets[0]?.setZIndex(0)
|
||||
|
||||
selectedWidget.setZIndex(1000)
|
||||
selectedWidget.select()
|
||||
this.selectedWidgets[0] = selectedWidget
|
||||
}
|
||||
|
||||
this.selectedWidgets.push(selectedWidget)
|
||||
this.currentMode = CanvasModes.MOVE_WIDGET
|
||||
}
|
||||
|
||||
this.currentMode = CanvasModes.PAN
|
||||
|
||||
}else if (this.state?.widgets?.length > 0){
|
||||
}else if (!selectedWidget){
|
||||
// get the canvas ready to pan, if there are widgets on the canvas
|
||||
this.clearSelections()
|
||||
this.currentMode = CanvasModes.PAN
|
||||
@@ -187,24 +194,36 @@ class Canvas extends React.Component {
|
||||
// })
|
||||
}else if (event.button === 2){
|
||||
//right click
|
||||
if (selectedWidget)
|
||||
this.setState({
|
||||
contextMenuItems: [{
|
||||
key: "delete",
|
||||
label: "Delete"
|
||||
}]
|
||||
})
|
||||
|
||||
if (this.selectedWidgets.length > 0 && this.selectedWidgets[0].__id !== selectedWidget.__id){
|
||||
this.clearSelections()
|
||||
}
|
||||
|
||||
// this.setState({
|
||||
// showContextMenu: true
|
||||
// })
|
||||
console.log("button: ", selectedWidget)
|
||||
if (selectedWidget)
|
||||
this.selectedWidgets[0] = selectedWidget
|
||||
console.log("renaming: ", selectedWidget)
|
||||
this.setState({
|
||||
contextMenuItems: [
|
||||
{
|
||||
key: "rename",
|
||||
label: (<div onClick={() => selectedWidget.openRenaming()}><EditOutlined /> Rename</div>),
|
||||
icons: <EditOutlined />,
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: (<div onClick={() => this.deleteSelectedWidgets([selectedWidget])}><DeleteOutlined /> Delete</div>),
|
||||
icons: <DeleteOutlined />,
|
||||
danger: true
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
mouseMoveEvent(event){
|
||||
|
||||
// console.log("mode: ", this.currentMode, this.getActiveObjects())
|
||||
if (this.mousePressed && [CanvasModes.PAN, CanvasModes.MOVE_WIDGET].includes(this.currentMode)) {
|
||||
const deltaX = event.clientX - this.mousePos.x
|
||||
@@ -401,16 +420,38 @@ class Canvas extends React.Component {
|
||||
return {id, widgetRef}
|
||||
}
|
||||
|
||||
deleteSelectedWidgets(widgets){
|
||||
|
||||
|
||||
let activeWidgets = removeDuplicateObjects([...widgets, ...this.selectedWidgets], "__id")
|
||||
console.log("active widget: ", widgets, activeWidgets)
|
||||
const widgetIds = activeWidgets.map(widget => widget.__id)
|
||||
|
||||
for (let widgetId of widgetIds){
|
||||
console.log("removed: ", widgetId)
|
||||
|
||||
// this.widgetRefs[widgetId]?.current.remove()
|
||||
delete this.widgetRefs[widgetId]
|
||||
|
||||
this.setState((prevState) => ({
|
||||
widgets: prevState.widgets.filter(widget => widget.id !== widgetId)
|
||||
}))
|
||||
// value.current?.remove()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* removes all the widgets from the canvas
|
||||
*/
|
||||
clearCanvas(){
|
||||
|
||||
for (let [key, value] of Object.entries(this.widgetRefs)){
|
||||
console.log("removed: ", value, value.current?.getElement())
|
||||
// NOTE: Don't remove from it using remove() function since, it already removed from the DOM tree when its removed from widgets
|
||||
// for (let [key, value] of Object.entries(this.widgetRefs)){
|
||||
// console.log("removed: ", value, value.current?.getElement())
|
||||
|
||||
value.current?.remove()
|
||||
}
|
||||
// value.current?.remove()
|
||||
// }
|
||||
|
||||
this.widgetRefs = {}
|
||||
this.setState(() => ({
|
||||
@@ -422,7 +463,7 @@ class Canvas extends React.Component {
|
||||
|
||||
removeWidget(widgetId){
|
||||
|
||||
this.widgetRefs[widgetId]?.current.remove()
|
||||
// this.widgetRefs[widgetId]?.current.remove()
|
||||
delete this.widgetRefs[widgetId]
|
||||
|
||||
this.setState((prevState) => ({
|
||||
@@ -454,7 +495,7 @@ class Canvas extends React.Component {
|
||||
</div>
|
||||
|
||||
<Droppable id="canvas-droppable" className="tw-w-full tw-h-full">
|
||||
<Dropdown trigger={['contextMenu']} menu={{items: this.state.contextMenuItems, }}>
|
||||
<Dropdown trigger={['contextMenu']} mouseLeaveDelay={0} menu={{items: this.state.contextMenuItems, }}>
|
||||
<div className="tw-w-full tw-h-full tw-flex tw-relative tw-bg-black tw-overflow-hidden"
|
||||
ref={this.canvasContainerRef}
|
||||
style={{transition: " transform 0.3s ease-in-out"}}
|
||||
|
||||
@@ -23,8 +23,6 @@ class Widget extends React.Component{
|
||||
// console.log("Id: ", id)
|
||||
// this id has to be unique inside the canvas, it will be set automatically and should never be changed
|
||||
this.__id = id
|
||||
this._zIndex = 0
|
||||
|
||||
this.canvas = canvasRef?.current || null
|
||||
|
||||
// this._selected = false
|
||||
@@ -85,6 +83,7 @@ class Widget extends React.Component{
|
||||
size: { width: 100, height: 100 },
|
||||
selected: false,
|
||||
widgetName: widgetName || 'unnamed widget', // this will later be converted to variable name
|
||||
enableRename: false, // will open the widgets editable div for renaming
|
||||
resizing: false,
|
||||
resizeCorner: ""
|
||||
}
|
||||
@@ -93,6 +92,8 @@ class Widget extends React.Component{
|
||||
this.getElement = this.getElement.bind(this)
|
||||
this.getBoundingRect = this.getBoundingRect.bind(this)
|
||||
|
||||
// this.openRenaming = this.openRenaming.bind(this)
|
||||
|
||||
this.isSelected = this.isSelected.bind(this)
|
||||
|
||||
this.getPos = this.getPos.bind(this)
|
||||
@@ -155,7 +156,7 @@ class Widget extends React.Component{
|
||||
this.setState({
|
||||
selected: true
|
||||
})
|
||||
console.log("selected")
|
||||
|
||||
}
|
||||
|
||||
deSelect(){
|
||||
@@ -209,9 +210,9 @@ class Widget extends React.Component{
|
||||
}
|
||||
|
||||
getSize(){
|
||||
const boundingRect = this.getBoundingRect()
|
||||
// const boundingRect = this.getBoundingRect()
|
||||
|
||||
return {width: boundingRect.width, height: boundingRect.height}
|
||||
return {width: this.state.size.width, height: this.state.size.height}
|
||||
}
|
||||
|
||||
getScaleAwareDimensions() {
|
||||
@@ -263,6 +264,12 @@ class Widget extends React.Component{
|
||||
this.setState({ resizing: true, resizeCorner: corner })
|
||||
}
|
||||
|
||||
setZIndex(zIndex){
|
||||
this.setState({
|
||||
zIndex: zIndex
|
||||
})
|
||||
}
|
||||
|
||||
handleResize(event) {
|
||||
if (!this.state.resizing) return
|
||||
|
||||
@@ -311,6 +318,19 @@ class Widget extends React.Component{
|
||||
}
|
||||
}
|
||||
|
||||
openRenaming(){
|
||||
this.setState({
|
||||
selected: true,
|
||||
enableRename: true
|
||||
})
|
||||
}
|
||||
|
||||
closeRenaming(){
|
||||
this.setState({
|
||||
enableRename: false
|
||||
})
|
||||
}
|
||||
|
||||
renderContent(){
|
||||
// throw new NotImplementedError("render method has to be implemented")
|
||||
return (
|
||||
@@ -328,11 +348,12 @@ class Widget extends React.Component{
|
||||
|
||||
let style = {
|
||||
cursor: this.cursor,
|
||||
zIndex: this.state.zIndex,
|
||||
position: "absolute", // don't change this if it has to be movable on the canvas
|
||||
top: `${this.state.pos.y}px`,
|
||||
left: `${this.state.pos.x}px`,
|
||||
width: `${this.state.size.width}px`,
|
||||
height: `${this.state.size.height}px`,
|
||||
position: "absolute" // don't change this
|
||||
}
|
||||
|
||||
let selectionStyle = {
|
||||
@@ -349,7 +370,7 @@ class Widget extends React.Component{
|
||||
widgetName: value.length > 0 ? value : prev.widgetName
|
||||
}))
|
||||
}
|
||||
|
||||
// console.log("selected: ", this.state.selected)
|
||||
return (
|
||||
|
||||
<div data-id={this.__id} ref={this.elementRef} className="tw-absolute tw-w-fit tw-h-fit"
|
||||
@@ -364,6 +385,7 @@ class Widget extends React.Component{
|
||||
<div className="tw-relative tw-w-full tw-h-full">
|
||||
<EditableDiv value={this.state.widgetName} onChange={onWidgetNameChange}
|
||||
maxLength={40}
|
||||
openEdit={this.state.enableRename}
|
||||
className="tw-text-sm tw-w-fit tw-max-w-[160px] tw-text-clip tw-min-w-[150px]
|
||||
tw-overflow-hidden tw-absolute tw--top-6 tw-h-6"
|
||||
/>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { FileImageOutlined, GithubOutlined, GitlabOutlined, LinkOutlined,
|
||||
FileTextOutlined} from "@ant-design/icons"
|
||||
|
||||
|
||||
export function DraggableWidgetCard({ name, img, url}){
|
||||
export function DraggableWidgetCard({ name, img, url, innerRef}){
|
||||
|
||||
const urlIcon = useMemo(() => {
|
||||
if (url){
|
||||
@@ -29,7 +29,7 @@ export function DraggableWidgetCard({ name, img, url}){
|
||||
|
||||
return (
|
||||
<Draggable className="tw-cursor-pointer" id={name}>
|
||||
<div className="tw-w-full tw-select-none tw-h-[240px] tw-flex tw-flex-col tw-rounded-md tw-overflow-hidden
|
||||
<div ref={innerRef} className="tw-select-none tw-h-[240px] tw-w-[280px] tw-flex tw-flex-col tw-rounded-md tw-overflow-hidden
|
||||
tw-gap-2 tw-text-gray-600 tw-bg-[#ffffff44] tw-border-solid tw-border-[1px] tw-border-[#888] ">
|
||||
<div className="tw-h-[200px] tw-w-full tw-overflow-hidden">
|
||||
<img src={img} alt={name} className="tw-object-contain tw-h-full tw-w-full tw-select-none" />
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { useState, useRef, useEffect } from 'react'
|
||||
|
||||
|
||||
function EditableDiv({value, onChange, maxLength=Infinity, className='', inputClassName}) {
|
||||
const [isEditable, setIsEditable] = useState(false)
|
||||
function EditableDiv({value, onChange, openEdit=false, maxLength=Infinity, className='', inputClassName}) {
|
||||
const [isEditable, setIsEditable] = useState(openEdit)
|
||||
const [content, setContent] = useState(value)
|
||||
const inputRef = useRef(null)
|
||||
|
||||
@@ -12,6 +12,17 @@ function EditableDiv({value, onChange, maxLength=Infinity, className='', inputCl
|
||||
|
||||
}, [value])
|
||||
|
||||
useEffect(() => {
|
||||
setIsEditable(openEdit)
|
||||
|
||||
if (openEdit){
|
||||
setTimeout(() => {
|
||||
inputRef.current.focus()
|
||||
}, 15)
|
||||
}
|
||||
|
||||
}, [openEdit])
|
||||
|
||||
const handleInput = (event) => {
|
||||
|
||||
console.log("Event key: ", event.key)
|
||||
|
||||
@@ -14,10 +14,10 @@ function Draggable(props) {
|
||||
|
||||
return (
|
||||
<button className={`tw-bg-transparent tw-outline-none tw-border-none ${props.className}`}
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
{...listeners}
|
||||
{...attributes}>
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
{...listeners}
|
||||
{...attributes}>
|
||||
{props.children}
|
||||
</button>
|
||||
)
|
||||
|
||||
14
src/utils/common.js
Normal file
14
src/utils/common.js
Normal file
@@ -0,0 +1,14 @@
|
||||
// contains commonly used functions to manipulate objects, array etc.
|
||||
|
||||
|
||||
export function removeDuplicateObjects(array, key) {
|
||||
const seen = new Set()
|
||||
|
||||
return array.filter(item => {
|
||||
if (!seen.has(item[key])) {
|
||||
seen.add(item[key])
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user