working on DND kit
This commit is contained in:
1
package-lock.json
generated
1
package-lock.json
generated
@@ -10,6 +10,7 @@
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.4.0",
|
||||
"@dnd-kit/core": "^6.1.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@reduxjs/toolkit": "^2.2.7",
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.4.0",
|
||||
"@dnd-kit/core": "^6.1.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@reduxjs/toolkit": "^2.2.7",
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
|
||||
66
src/App.js
66
src/App.js
@@ -7,17 +7,28 @@ import WidgetsContainer from './sidebar/widgetsContainer'
|
||||
import UploadsContainer from './sidebar/uploadsContainer'
|
||||
import Canvas from './canvas/canvas'
|
||||
import Header from './components/header'
|
||||
import { DndContext, useSensors, useSensor, PointerSensor, closestCorners, DragOverlay } from '@dnd-kit/core'
|
||||
import { DraggableWidgetCard } from './components/cards'
|
||||
|
||||
|
||||
function App() {
|
||||
|
||||
const [uploadedAssets, setUploadedAssets] = useState([]) // a global storage for assets, since redux can't store files(serialize files)
|
||||
|
||||
const [sidebarWidgets, setSidebarWidgets] = useState([])
|
||||
const [canvasWidgets, setCanvasWidgets] = useState([]) // contains the reference to the widgets inside the canvas
|
||||
|
||||
const [activeSidebarWidget, setActiveSidebarWidget] = useState(null) // helps with the dnd overlay
|
||||
|
||||
const tabs = [
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor)
|
||||
)
|
||||
|
||||
const sidebarTabs = [
|
||||
{
|
||||
name: "Widgets",
|
||||
icon: <LayoutFilled />,
|
||||
content: <WidgetsContainer />
|
||||
content: <WidgetsContainer onWidgetsUpdate={(widgets) => setSidebarWidgets(widgets)}/>
|
||||
},
|
||||
{
|
||||
name: "Extensions",
|
||||
@@ -32,15 +43,56 @@ function App() {
|
||||
}
|
||||
]
|
||||
|
||||
const handleDragStart = (event) => {
|
||||
console.log("Dragging", event.active)
|
||||
const draggedItem = sidebarWidgets.find((item) => item.name === event.active.id)
|
||||
setActiveSidebarWidget(draggedItem)
|
||||
}
|
||||
|
||||
const handleDragMove = (event) => {
|
||||
}
|
||||
|
||||
const handleDragEnd = (event) => {
|
||||
// add items to canvas from sidebar
|
||||
const widgetItem = event.active.data.current?.title
|
||||
|
||||
if (event.over?.id !== "cart-droppable" || !widgetItem) return
|
||||
// const temp = [...widgets]
|
||||
// temp.push(widgetItem)
|
||||
// setCanvasWidgets(temp)
|
||||
setActiveSidebarWidget(null)
|
||||
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className="tw-w-full tw-h-[100vh] tw-flex tw-flex-col tw-bg-primaryBg">
|
||||
<Header className="tw-h-[6vh]"/>
|
||||
<div className="tw-w-full tw-h-[94vh] tw-flex">
|
||||
<Sidebar tabs={tabs}/>
|
||||
<Canvas />
|
||||
</div>
|
||||
|
||||
<DndContext sensors={sensors} collisionDetection={closestCorners}
|
||||
onDragStart={handleDragStart}
|
||||
onDragMove={handleDragMove}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<div className="tw-w-full tw-h-[94vh] tw-flex">
|
||||
<Sidebar tabs={sidebarTabs}/>
|
||||
<Canvas widgets={canvasWidgets}/>
|
||||
</div>
|
||||
|
||||
{/* dragOverlay (dnd-kit) helps move items from one container to another */}
|
||||
<DragOverlay>
|
||||
{activeSidebarWidget ? (
|
||||
<DraggableWidgetCard name={activeSidebarWidget.name}
|
||||
img={activeSidebarWidget.img}
|
||||
url={activeSidebarWidget.url}
|
||||
/>
|
||||
):
|
||||
null}
|
||||
</DragOverlay>
|
||||
|
||||
</DndContext>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import React from "react"
|
||||
import * as fabric from 'fabric'
|
||||
|
||||
import {DndContext} from '@dnd-kit/core'
|
||||
|
||||
import { FullscreenOutlined, ReloadOutlined } from "@ant-design/icons"
|
||||
import { Button, Tooltip } from "antd"
|
||||
|
||||
import Droppable from "../components/utils/droppable"
|
||||
import Widget from "./widgets/base"
|
||||
import Cursor from "./constants/cursor"
|
||||
import { UID } from "../utils/uid"
|
||||
@@ -60,10 +63,6 @@ class Canvas extends React.Component {
|
||||
this.getCanvasObjectsBoundingBox = this.getCanvasObjectsBoundingBox.bind(this)
|
||||
this.fitCanvasToBoundingBox = this.fitCanvasToBoundingBox.bind(this)
|
||||
|
||||
this.updateWidgetPosition = this.updateWidgetPosition.bind(this)
|
||||
|
||||
this.checkAndExpandCanvas = this.checkAndExpandCanvas.bind(this)
|
||||
this.expandCanvas = this.expandCanvas.bind(this)
|
||||
|
||||
this.clearSelections = this.clearSelections.bind(this)
|
||||
this.clearCanvas = this.clearCanvas.bind(this)
|
||||
@@ -195,7 +194,7 @@ class Canvas extends React.Component {
|
||||
const newPosY = y + (deltaY/this.state.zoom) // account for the zoom, since the widget is relative to canvas
|
||||
|
||||
widget.setPos(newPosX, newPosY)
|
||||
this.checkAndExpandCanvas(newPosX, newPosY, widget.getSize().width, widget.getSize().height)
|
||||
// this.checkAndExpandCanvas(newPosX, newPosY, widget)
|
||||
})
|
||||
// this.fitCanvasToBoundingBox(10)
|
||||
|
||||
@@ -219,123 +218,10 @@ class Canvas extends React.Component {
|
||||
wheelZoom(event){
|
||||
let delta = event.deltaY
|
||||
let zoom = this.state.zoom * 0.999 ** delta
|
||||
|
||||
this.setZoom(zoom, {x: event.offsetX, y: event.offsetY})
|
||||
}
|
||||
|
||||
checkAndExpandCanvas(widgetX, widgetY, widgetWidth, widgetHeight) {
|
||||
const canvasWidth = this.canvasRef.current.offsetWidth
|
||||
const canvasHeight = this.canvasRef.current.offsetHeight
|
||||
|
||||
const canvasRect = this.canvasRef.current.getBoundingClientRect()
|
||||
|
||||
// Get the zoom level
|
||||
const zoom = this.state.zoom
|
||||
|
||||
// Calculate effective canvas boundaries considering zoom
|
||||
const effectiveCanvasRight = canvasWidth
|
||||
const effectiveCanvasBottom = canvasHeight
|
||||
|
||||
// Calculate widget boundaries
|
||||
const widgetRight = widgetX + widgetWidth
|
||||
const widgetBottom = widgetY + widgetHeight
|
||||
|
||||
// Determine if expansion is needed
|
||||
const expandRight = widgetRight > effectiveCanvasRight
|
||||
const expandDown = widgetBottom > effectiveCanvasBottom
|
||||
const expandLeft = widgetX < canvasRect.left * this.state.zoom
|
||||
const expandUp = widgetY < canvasRect.top
|
||||
|
||||
if (expandRight || expandLeft || expandDown || expandUp) {
|
||||
this.expandCanvas(expandRight, expandLeft, expandDown, expandUp, widgetX, widgetY, widgetWidth, widgetHeight)
|
||||
}
|
||||
}
|
||||
|
||||
// Expand the canvas method
|
||||
/**
|
||||
*
|
||||
* @param {boolean} expandRight
|
||||
* @param {boolean} expandLeft
|
||||
* @param {boolean} expandDown
|
||||
* @param {boolean} expandUp
|
||||
* @param {number} widgetX
|
||||
* @param {number} widgetY
|
||||
* @param {number} widgetRight
|
||||
* @param {number} widgetBottom
|
||||
*/
|
||||
expandCanvas(expandRight, expandLeft, expandDown, expandUp, widgetX, widgetY, widgetWidth, widgetHeight) {
|
||||
const currentWidth = this.canvasRef.current.offsetWidth
|
||||
const currentHeight = this.canvasRef.current.offsetHeight
|
||||
|
||||
console.log("current: ", expandRight, expandDown, expandLeft, expandUp)
|
||||
|
||||
let newWidth = currentWidth
|
||||
let newHeight = currentHeight
|
||||
let newTranslateX = this.state.currentTranslate.x
|
||||
let newTranslateY = this.state.currentTranslate.y
|
||||
|
||||
if (expandRight) {
|
||||
// const requiredWidth = widgetRight - newTranslateX // Add padding
|
||||
// newWidth = Math.max(requiredWidth, currentWidth)
|
||||
newWidth = currentWidth + 50
|
||||
}
|
||||
|
||||
if (expandLeft) {
|
||||
// const leftOffset = widgetX + newTranslateX // Position of the widget relative to the left edge
|
||||
// const requiredLeftExpansion = -leftOffset + 50 // Add padding
|
||||
newWidth = currentWidth + widgetWidth
|
||||
newTranslateX -= widgetWidth // Adjust translation to move the canvas to the left
|
||||
}
|
||||
|
||||
if (expandDown) {
|
||||
newHeight = currentHeight + 50
|
||||
|
||||
// const requiredHeight = widgetBottom - newTranslateY // Add padding
|
||||
// newHeight = Math.max(requiredHeight, currentHeight)
|
||||
}
|
||||
|
||||
if (expandUp) {
|
||||
newHeight = currentHeight + widgetHeight
|
||||
newTranslateY -= widgetHeight
|
||||
// const topOffset = widgetY + newTranslateY // Position of the widget relative to the top edge
|
||||
// const requiredTopExpansion = -topOffset + 50 // Add padding
|
||||
// newHeight = currentHeight + requiredTopExpansion
|
||||
// newTranslateY -= requiredTopExpansion // Adjust translation to move the canvas upwards
|
||||
}
|
||||
|
||||
// Apply new dimensions and translation
|
||||
this.canvasRef.current.style.width = `${newWidth}px`
|
||||
this.canvasRef.current.style.height = `${newHeight}px`
|
||||
|
||||
console.log("translate: ", this.canvasRef.current.offsetWidth, )
|
||||
// Now, to keep the widget in the same relative position:
|
||||
const updatedWidgetX = widgetX - newTranslateX / this.state.zoom;
|
||||
const updatedWidgetY = widgetY - newTranslateY / this.state.zoom;
|
||||
|
||||
this.setState({
|
||||
currentTranslate: {
|
||||
x: newTranslateX,
|
||||
y: newTranslateY
|
||||
}
|
||||
}, () => {
|
||||
this.applyTransform()
|
||||
this.updateWidgetPosition(updatedWidgetX, updatedWidgetY, widgetWidth, widgetHeight)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// TODO: FIX this, to ensure that the widget position remains the same
|
||||
// Function to update the widget's position based on new updated canvas coordinates, use it after expandCanvas
|
||||
updateWidgetPosition(widgetX, widgetY, widgetWidth, widgetHeight) {
|
||||
const widgetElement = this.selectedWidgets[0].current; // Assuming the widget is referenced via `widgetRef`
|
||||
|
||||
console.log("widget element: ", this.selectedWidgets[0].current)
|
||||
widgetElement.style.left = `${widgetX}px`;
|
||||
widgetElement.style.top = `${widgetY}px`;
|
||||
widgetElement.style.width = `${widgetWidth}px`;
|
||||
widgetElement.style.height = `${widgetHeight}px`;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* fits the canvas size to fit the widgets bounding box
|
||||
*/
|
||||
@@ -382,6 +268,9 @@ class Canvas extends React.Component {
|
||||
}
|
||||
}, this.applyTransform)
|
||||
|
||||
// this.canvasRef.current.style.width = `${100/zoom}%`
|
||||
// this.canvasRef.current.style.height = `${100/zoom}%`
|
||||
|
||||
}
|
||||
|
||||
resetTransforms() {
|
||||
@@ -485,19 +374,24 @@ class Canvas extends React.Component {
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="tw-w-full tw-relative tw-h-full tw-overflow-hidden"
|
||||
ref={this.canvasContainerRef}
|
||||
style={{transition: " transform 0.3s ease-in-out"}}
|
||||
>
|
||||
<div data-canvas className="tw-w-full tw-absolute tw-top-0 tw-h-full tw-bg-green-300"
|
||||
ref={this.canvasRef}>
|
||||
<div className="tw-relative tw-w-full tw-h-full">
|
||||
{
|
||||
this.state.widgets.map(this.renderWidget)
|
||||
}
|
||||
<Droppable id="canvas-droppable" className="tw-w-full tw-h-full">
|
||||
{/* Canvas container */}
|
||||
<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"}}
|
||||
>
|
||||
{/* Canvas */}
|
||||
<div data-canvas className="tw-w-full tw-h-full tw-absolute tw-top-0 tw-select-none
|
||||
t tw-bg-green-300 tw-overflow-hidden"
|
||||
ref={this.canvasRef}>
|
||||
<div className="tw-relative tw-w-full tw-h-full">
|
||||
{
|
||||
this.state.widgets.map(this.renderWidget)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Droppable>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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}){
|
||||
|
||||
const urlIcon = useMemo(() => {
|
||||
if (url){
|
||||
@@ -28,8 +28,8 @@ export function DraggableWidgetCard({name, img, url}){
|
||||
|
||||
|
||||
return (
|
||||
<Draggable className="tw-cursor-pointer">
|
||||
<div className="tw-w-full tw-h-[240px] tw-flex tw-flex-col tw-rounded-md tw-overflow-hidden
|
||||
<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
|
||||
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,18 +1,23 @@
|
||||
import React from "react"
|
||||
import {useDraggable} from "@dnd-kit/core"
|
||||
|
||||
import { CSS } from "@dnd-kit/utilities"
|
||||
|
||||
function Draggable(props) {
|
||||
const {attributes, listeners, setNodeRef, transform} = useDraggable({
|
||||
id: 'draggable',
|
||||
id: props.id,
|
||||
data: {title: props.children}
|
||||
})
|
||||
const style = transform ? {
|
||||
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
|
||||
transform: CSS.Translate.toString(transform),
|
||||
} : undefined
|
||||
|
||||
|
||||
return (
|
||||
<button className={`tw-bg-transparent tw-outline-none tw-border-none ${props.className}`} ref={setNodeRef} style={style} {...listeners} {...attributes}>
|
||||
<button className={`tw-bg-transparent tw-outline-none tw-border-none ${props.className}`}
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
{...listeners}
|
||||
{...attributes}>
|
||||
{props.children}
|
||||
</button>
|
||||
)
|
||||
|
||||
20
src/components/utils/droppable.js
Normal file
20
src/components/utils/droppable.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import React from 'react'
|
||||
import {useDroppable} from '@dnd-kit/core'
|
||||
|
||||
function Droppable(props) {
|
||||
const {isOver, setNodeRef} = useDroppable({
|
||||
id: props.id,
|
||||
})
|
||||
const style = {
|
||||
color: isOver ? 'green' : undefined,
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div ref={setNodeRef} style={style} className={props.className || ''}>
|
||||
{props.children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Droppable
|
||||
415
src/components/utils/panzoom.js
Normal file
415
src/components/utils/panzoom.js
Normal file
@@ -0,0 +1,415 @@
|
||||
// This is only for testing purpose, not really meant to be used
|
||||
import './polyfills'
|
||||
|
||||
import {
|
||||
addPointer,
|
||||
getDistance,
|
||||
getMiddle,
|
||||
removePointer
|
||||
} from './pointers'
|
||||
import { destroyPointer, eventNames, onPointer } from './events'
|
||||
import { getDimensions, setStyle, setTransform, setTransition } from './css'
|
||||
|
||||
import isAttached from './isAttached'
|
||||
import isExcluded from './isExcluded'
|
||||
import isSVGElement from './isSVGElement'
|
||||
import shallowClone from './shallowClone'
|
||||
|
||||
const defaultOptions = {
|
||||
animate: false,
|
||||
canvas: false,
|
||||
cursor: 'move',
|
||||
disablePan: false,
|
||||
disableZoom: false,
|
||||
disableXAxis: false,
|
||||
disableYAxis: false,
|
||||
duration: 200,
|
||||
easing: 'ease-in-out',
|
||||
exclude: [],
|
||||
excludeClass: 'panzoom-exclude',
|
||||
handleStartEvent: (e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
},
|
||||
maxScale: 4,
|
||||
minScale: 0.125,
|
||||
overflow: 'hidden',
|
||||
panOnlyWhenZoomed: false,
|
||||
pinchAndPan: false,
|
||||
relative: false,
|
||||
setTransform,
|
||||
startX: 0,
|
||||
startY: 0,
|
||||
startScale: 1,
|
||||
step: 0.3,
|
||||
touchAction: 'none'
|
||||
}
|
||||
|
||||
function Panzoom(elem, options = {}) {
|
||||
if (!elem) {
|
||||
throw new Error('Panzoom requires an element as an argument')
|
||||
}
|
||||
if (elem.nodeType !== 1) {
|
||||
throw new Error('Panzoom requires an element with a nodeType of 1')
|
||||
}
|
||||
if (!isAttached(elem)) {
|
||||
throw new Error('Panzoom should be called on elements that have been attached to the DOM')
|
||||
}
|
||||
|
||||
options = {
|
||||
...defaultOptions,
|
||||
...options
|
||||
}
|
||||
|
||||
const isSVG = isSVGElement(elem)
|
||||
const parent = elem.parentNode
|
||||
|
||||
// Set parent styles
|
||||
parent.style.overflow = options.overflow // hidden
|
||||
parent.style.userSelect = 'none'
|
||||
parent.style.touchAction = options.touchAction
|
||||
(options.canvas ? parent : elem).style.cursor = options.cursor
|
||||
|
||||
// Set element styles
|
||||
elem.style.userSelect = 'none'
|
||||
elem.style.touchAction = options.touchAction
|
||||
setStyle(
|
||||
elem,
|
||||
'transformOrigin',
|
||||
typeof options.origin === 'string' ? options.origin : isSVG ? '0 0' : '50% 50%'
|
||||
)
|
||||
|
||||
function resetStyle() {
|
||||
parent.style.overflow = ''
|
||||
parent.style.userSelect = ''
|
||||
parent.style.touchAction = ''
|
||||
parent.style.cursor = ''
|
||||
elem.style.cursor = ''
|
||||
elem.style.userSelect = ''
|
||||
elem.style.touchAction = ''
|
||||
setStyle(elem, 'transformOrigin', '')
|
||||
}
|
||||
|
||||
function setOptions(opts = {}) {
|
||||
for (const key in opts) {
|
||||
if (opts.hasOwnProperty(key)) {
|
||||
options[key] = opts[key]
|
||||
}
|
||||
}
|
||||
// Handle option side-effects
|
||||
if (opts.hasOwnProperty('cursor') || opts.hasOwnProperty('canvas')) {
|
||||
parent.style.cursor = elem.style.cursor = ''
|
||||
(options.canvas ? parent : elem).style.cursor = options.cursor
|
||||
}
|
||||
if (opts.hasOwnProperty('overflow')) {
|
||||
parent.style.overflow = opts.overflow
|
||||
}
|
||||
if (opts.hasOwnProperty('touchAction')) {
|
||||
parent.style.touchAction = opts.touchAction
|
||||
elem.style.touchAction = opts.touchAction
|
||||
}
|
||||
}
|
||||
|
||||
let x = 0
|
||||
let y = 0
|
||||
let scale = 1
|
||||
let isPanning = false
|
||||
zoom(options.startScale, { animate: false, force: true })
|
||||
// Wait for scale to update
|
||||
// for accurate dimensions
|
||||
// to constrain initial values
|
||||
setTimeout(() => {
|
||||
pan(options.startX, options.startY, { animate: false, force: true })
|
||||
})
|
||||
|
||||
function trigger(eventName, detail, opts) {
|
||||
if (opts.silent) {
|
||||
return
|
||||
}
|
||||
const event = new CustomEvent(eventName, { detail })
|
||||
elem.dispatchEvent(event)
|
||||
}
|
||||
|
||||
function setTransformWithEvent(
|
||||
eventName,
|
||||
opts,
|
||||
originalEvent
|
||||
) {
|
||||
const value = { x, y, scale, isSVG, originalEvent }
|
||||
requestAnimationFrame(() => {
|
||||
if (typeof opts.animate === 'boolean') {
|
||||
if (opts.animate) {
|
||||
setTransition(elem, opts)
|
||||
} else {
|
||||
setStyle(elem, 'transition', 'none')
|
||||
}
|
||||
}
|
||||
opts.setTransform(elem, value, opts)
|
||||
trigger(eventName, value, opts)
|
||||
trigger('panzoomchange', value, opts)
|
||||
})
|
||||
return value
|
||||
}
|
||||
|
||||
function constrainXY(
|
||||
toX,
|
||||
toY,
|
||||
toScale,
|
||||
panOptions
|
||||
) {
|
||||
const opts = { ...options, ...panOptions }
|
||||
const result = { x, y, opts }
|
||||
if (!opts.force && (opts.disablePan || (opts.panOnlyWhenZoomed && scale === opts.startScale))) {
|
||||
return result
|
||||
}
|
||||
toX = parseFloat(toX)
|
||||
toY = parseFloat(toY)
|
||||
|
||||
if (!opts.disableXAxis) {
|
||||
result.x = (opts.relative ? x : 0) + toX
|
||||
}
|
||||
|
||||
if (!opts.disableYAxis) {
|
||||
result.y = (opts.relative ? y : 0) + toY
|
||||
}
|
||||
|
||||
if (opts.contain) {
|
||||
const dims = getDimensions(elem)
|
||||
const realWidth = dims.elem.width / scale
|
||||
const realHeight = dims.elem.height / scale
|
||||
const scaledWidth = realWidth * toScale
|
||||
const scaledHeight = realHeight * toScale
|
||||
const diffHorizontal = (scaledWidth - realWidth) / 2
|
||||
const diffVertical = (scaledHeight - realHeight) / 2
|
||||
|
||||
if (opts.contain === 'inside') {
|
||||
const minX = (-dims.elem.margin.left - dims.parent.padding.left + diffHorizontal) / toScale
|
||||
const maxX =
|
||||
(dims.parent.width -
|
||||
scaledWidth -
|
||||
dims.parent.padding.left -
|
||||
dims.elem.margin.left -
|
||||
dims.parent.border.left -
|
||||
dims.parent.border.right +
|
||||
diffHorizontal) /
|
||||
toScale
|
||||
result.x = Math.max(Math.min(result.x, maxX), minX)
|
||||
const minY = (-dims.elem.margin.top - dims.parent.padding.top + diffVertical) / toScale
|
||||
const maxY =
|
||||
(dims.parent.height -
|
||||
scaledHeight -
|
||||
dims.parent.padding.top -
|
||||
dims.elem.margin.top -
|
||||
dims.parent.border.top -
|
||||
dims.parent.border.bottom +
|
||||
diffVertical) /
|
||||
toScale
|
||||
result.y = Math.max(Math.min(result.y, maxY), minY)
|
||||
} else if (opts.contain === 'outside') {
|
||||
const minX =
|
||||
(-(scaledWidth - dims.parent.width) -
|
||||
dims.parent.padding.left -
|
||||
dims.parent.border.left -
|
||||
dims.parent.border.right +
|
||||
diffHorizontal) /
|
||||
toScale
|
||||
const maxX = (diffHorizontal - dims.parent.padding.left) / toScale
|
||||
result.x = Math.max(Math.min(result.x, maxX), minX)
|
||||
const minY =
|
||||
(-(scaledHeight - dims.parent.height) -
|
||||
dims.parent.padding.top -
|
||||
dims.parent.border.top -
|
||||
dims.parent.border.bottom +
|
||||
diffVertical) /
|
||||
toScale
|
||||
const maxY = (diffVertical - dims.parent.padding.top) / toScale
|
||||
result.y = Math.max(Math.min(result.y, maxY), minY)
|
||||
}
|
||||
}
|
||||
|
||||
if (opts.roundPixels) {
|
||||
result.x = Math.round(result.x)
|
||||
result.y = Math.round(result.y)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function constrainScale(toScale, zoomOptions) {
|
||||
const opts = { ...options, ...zoomOptions }
|
||||
const result = { scale, opts }
|
||||
if (!opts.force && opts.disableZoom) {
|
||||
return result
|
||||
}
|
||||
|
||||
let minScale = options.minScale
|
||||
let maxScale = options.maxScale
|
||||
|
||||
if (opts.contain) {
|
||||
const dims = getDimensions(elem)
|
||||
const elemWidth = dims.elem.width / scale
|
||||
const elemHeight = dims.elem.height / scale
|
||||
if (elemWidth > 1 && elemHeight > 1) {
|
||||
const parentWidth = dims.parent.width - dims.parent.border.left - dims.parent.border.right
|
||||
const parentHeight = dims.parent.height - dims.parent.border.top - dims.parent.border.bottom
|
||||
const elemScaledWidth = parentWidth / elemWidth
|
||||
const elemScaledHeight = parentHeight / elemHeight
|
||||
if (options.contain === 'inside') {
|
||||
maxScale = Math.min(maxScale, elemScaledWidth, elemScaledHeight)
|
||||
} else if (options.contain === 'outside') {
|
||||
minScale = Math.max(minScale, elemScaledWidth, elemScaledHeight)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.scale = Math.min(Math.max(toScale, minScale), maxScale)
|
||||
return result
|
||||
}
|
||||
|
||||
function pan(
|
||||
toX,
|
||||
toY,
|
||||
panOptions,
|
||||
originalEvent
|
||||
) {
|
||||
const result = constrainXY(toX, toY, scale, panOptions)
|
||||
|
||||
// Only try to set if the result is somehow different
|
||||
if (x !== result.x || y !== result.y) {
|
||||
x = result.x
|
||||
y = result.y
|
||||
return setTransformWithEvent('panzoompan', result.opts, originalEvent)
|
||||
}
|
||||
return { x, y, scale, isSVG, originalEvent }
|
||||
}
|
||||
|
||||
function zoom(
|
||||
toScale,
|
||||
zoomOptions,
|
||||
focal
|
||||
) {
|
||||
const result = constrainScale(toScale, zoomOptions)
|
||||
const zoomScale = result.scale
|
||||
const zoomOpts = result.opts
|
||||
if (scale !== zoomScale) {
|
||||
scale = zoomScale
|
||||
let toX = x
|
||||
let toY = y
|
||||
if (focal) {
|
||||
const focalX = focal.x || 0
|
||||
const focalY = focal.y || 0
|
||||
const diffScale = zoomScale / scale
|
||||
toX = focalX - (focalX - x) * diffScale
|
||||
toY = focalY - (focalY - y) * diffScale
|
||||
}
|
||||
return pan(toX, toY, zoomOpts, zoomOptions.originalEvent)
|
||||
}
|
||||
return { x, y, scale, isSVG: isSVGElement(elem), originalEvent: zoomOptions.originalEvent }
|
||||
}
|
||||
|
||||
function zoomIn(zoomOptions) {
|
||||
zoom(scale + options.step, zoomOptions)
|
||||
}
|
||||
|
||||
function zoomOut(zoomOptions) {
|
||||
zoom(scale - options.step, zoomOptions)
|
||||
}
|
||||
|
||||
function zoomToPoint(toScale, point, zoomOptions) {
|
||||
zoom(toScale, zoomOptions, point)
|
||||
}
|
||||
|
||||
function zoomWithWheel(event) {
|
||||
const delta = Math.sign(event.deltaY) * -0.1
|
||||
const zoomFactor = Math.pow(1.2, delta)
|
||||
zoom(scale * zoomFactor, {}, {
|
||||
x: event.clientX,
|
||||
y: event.clientY
|
||||
})
|
||||
}
|
||||
|
||||
function handleDown(event) {
|
||||
if (isExcluded(event.target, options.exclude, options.excludeClass)) {
|
||||
return
|
||||
}
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
isPanning = true
|
||||
const point = { x: event.clientX, y: event.clientY }
|
||||
addPointer(event)
|
||||
elem.style.cursor = options.cursor
|
||||
trigger('panzoomstart', { x, y, scale, originalEvent: event })
|
||||
|
||||
function handleMove(e) {
|
||||
if (!isPanning) {
|
||||
return
|
||||
}
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
if (options.pinchAndPan && e.pointerType === 'touch') {
|
||||
if (e.pointerId === e.pointers[0].pointerId) {
|
||||
e.target.style.cursor = options.cursor
|
||||
return
|
||||
}
|
||||
e.target.style.cursor = ''
|
||||
const dist = getDistance(e)
|
||||
const middle = getMiddle(e)
|
||||
zoom(scale * (dist / e.pointers[0].distance), {
|
||||
animate: false,
|
||||
originalEvent: e
|
||||
}, middle)
|
||||
} else {
|
||||
pan(
|
||||
x + (e.clientX - point.x) / scale,
|
||||
y + (e.clientY - point.y) / scale,
|
||||
{ animate: false, force: true },
|
||||
e
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function handleUp(e) {
|
||||
isPanning = false
|
||||
removePointer(e)
|
||||
if (e.pointerType !== 'touch' || e.pointerId !== e.pointers[0].pointerId) {
|
||||
return
|
||||
}
|
||||
elem.style.cursor = ''
|
||||
trigger('panzoomend', { x, y, scale, originalEvent: e })
|
||||
}
|
||||
|
||||
onPointer(handleMove, handleUp, event)
|
||||
}
|
||||
|
||||
function reset() {
|
||||
setOptions({
|
||||
...defaultOptions,
|
||||
...options
|
||||
})
|
||||
zoom(options.startScale, { animate: false, force: true })
|
||||
pan(options.startX, options.startY, { animate: false, force: true })
|
||||
}
|
||||
|
||||
reset()
|
||||
|
||||
return {
|
||||
zoom,
|
||||
zoomIn,
|
||||
zoomOut,
|
||||
zoomToPoint,
|
||||
zoomWithWheel,
|
||||
pan,
|
||||
reset,
|
||||
setOptions,
|
||||
destroy() {
|
||||
setStyle(elem, 'transition', '')
|
||||
resetStyle()
|
||||
destroyPointer()
|
||||
eventNames.forEach((eventName) => {
|
||||
elem.removeEventListener(eventName, handleDown)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Panzoom
|
||||
@@ -8,7 +8,13 @@ import ButtonWidget from "../assets/widgets/button.png"
|
||||
|
||||
import { filterObjectListStartingWith } from "../utils/filter"
|
||||
|
||||
function WidgetsContainer(){
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {function} onWidgetsUpdate - this is a callback that will be called once the sidebar is populated with widgets
|
||||
* @returns
|
||||
*/
|
||||
function WidgetsContainer({onWidgetsUpdate}){
|
||||
|
||||
const widgets = useMemo(() => {
|
||||
return [
|
||||
@@ -42,8 +48,14 @@ function WidgetsContainer(){
|
||||
|
||||
setWidgetData(widgets)
|
||||
|
||||
if (onWidgetsUpdate){
|
||||
onWidgetsUpdate(widgets)
|
||||
}
|
||||
|
||||
}, [widgets])
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
if (searchValue.length > 0){
|
||||
@@ -78,14 +90,15 @@ function WidgetsContainer(){
|
||||
</div>
|
||||
</div>
|
||||
<div className="tw-flex tw-flex-col tw-gap-2 tw-h-full tw-p-1">
|
||||
|
||||
{
|
||||
widgetData.map((widget, index) => {
|
||||
return (
|
||||
<DraggableWidgetCard key={widget.name}
|
||||
name={widget.name}
|
||||
img={widget.img}
|
||||
url={widget.link}
|
||||
/>
|
||||
name={widget.name}
|
||||
img={widget.img}
|
||||
url={widget.link}
|
||||
/>
|
||||
|
||||
)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user