diff --git a/assets/icons/panfilledicon.svg b/assets/icons/panfilledicon.svg
new file mode 100644
index 00000000..38617ecd
--- /dev/null
+++ b/assets/icons/panfilledicon.svg
@@ -0,0 +1,7 @@
+
+
+
diff --git a/assets/icons/panicon.svg b/assets/icons/panicon.svg
new file mode 100644
index 00000000..ba86dc9e
--- /dev/null
+++ b/assets/icons/panicon.svg
@@ -0,0 +1,7 @@
+
+
+
diff --git a/src/components/Dashboard/common/TemplatePreview.jsx b/src/components/Dashboard/common/TemplatePreview.jsx
index 73bcced0..9a9ea2ed 100644
--- a/src/components/Dashboard/common/TemplatePreview.jsx
+++ b/src/components/Dashboard/common/TemplatePreview.jsx
@@ -11,6 +11,8 @@ import { Flex, Button, Input, Select } from 'antd'
import PlusIcon from '../../Icons/PlusIcon.jsx'
import MinusIcon from '../../Icons/MinusIcon.jsx'
import InfoCircleIcon from '../../Icons/InfoCircleIcon.jsx'
+import PanIcon from '../../Icons/PanIcon.jsx'
+import PanFilledIcon from '../../Icons/PanFilledIcon.jsx'
import ObjectProperty from '../common/ObjectProperty.jsx'
import ScrollBox from './ScrollBox.jsx'
import { ApiServerContext } from '../context/ApiServerContext.jsx'
@@ -39,6 +41,9 @@ const TemplatePreview = ({
const [reloadLoading, setReloadLoading] = useState(false)
const [previewScale, setPreviewScale] = useState(1)
const [previewType, setPreviewType] = useState('HTML')
+ const [panMode, setPanMode] = useState(false)
+ const [isPanning, setIsPanning] = useState(false)
+ const panStartRef = useRef({ x: 0, y: 0, scrollLeft: 0, scrollTop: 0 })
const [iframeSize, setIframeSize] = useState({
width: '100%',
height: 'auto'
@@ -236,6 +241,74 @@ const TemplatePreview = ({
}
}, [])
+ useEffect(() => {
+ if (!panMode) {
+ setIsPanning(false)
+ }
+ }, [panMode])
+
+ useEffect(() => {
+ const scrollEl = scrollElementRef.current
+ if (!panMode || !scrollEl || previewType != 'HTML') {
+ return
+ }
+
+ const handlePointerDown = (event) => {
+ if (event.pointerType === 'mouse' && event.button !== 0) {
+ return
+ }
+ event.preventDefault()
+ panStartRef.current = {
+ x: event.clientX,
+ y: event.clientY,
+ scrollLeft: scrollEl.scrollLeft,
+ scrollTop: scrollEl.scrollTop
+ }
+ setIsPanning(true)
+ }
+
+ scrollEl.addEventListener('pointerdown', handlePointerDown)
+ return () => {
+ scrollEl.removeEventListener('pointerdown', handlePointerDown)
+ }
+ }, [panMode, previewType])
+
+ useEffect(() => {
+ if (!isPanning) {
+ return
+ }
+
+ const previousCursor = document.body.style.cursor
+ const previousUserSelect = document.body.style.userSelect
+ document.body.style.cursor = 'grabbing'
+ document.body.style.userSelect = 'none'
+
+ const handlePointerMove = (event) => {
+ const scrollEl = scrollElementRef.current
+ if (!scrollEl) {
+ return
+ }
+ const { x, y, scrollLeft, scrollTop } = panStartRef.current
+ scrollEl.scrollLeft = scrollLeft - (event.clientX - x)
+ scrollEl.scrollTop = scrollTop - (event.clientY - y)
+ }
+
+ const handlePointerUp = () => {
+ setIsPanning(false)
+ }
+
+ window.addEventListener('pointermove', handlePointerMove)
+ window.addEventListener('pointerup', handlePointerUp)
+ window.addEventListener('pointercancel', handlePointerUp)
+ return () => {
+ document.body.style.cursor = previousCursor
+ document.body.style.userSelect = previousUserSelect
+ window.removeEventListener('pointermove', handlePointerMove)
+ window.removeEventListener('pointerup', handlePointerUp)
+ window.removeEventListener('pointercancel', handlePointerUp)
+ }
+ }, [isPanning])
+
return (
@@ -266,6 +339,19 @@ const TemplatePreview = ({
>
) : null}
+
+ ) : (
+
+ )
+ }
+ disabled={loading || reloadLoading || previewType == 'PDF'}
+ onClick={() => {
+ setPanMode((prev) => !prev)
+ }}
+ />
}
onClick={() => {
@@ -314,11 +400,22 @@ const TemplatePreview = ({
}}
>
{previewType == 'HTML' ? (
-
+
diff --git a/src/components/Icons/PanFilledIcon.jsx b/src/components/Icons/PanFilledIcon.jsx
new file mode 100644
index 00000000..0aca1ba4
--- /dev/null
+++ b/src/components/Icons/PanFilledIcon.jsx
@@ -0,0 +1,8 @@
+import Icon from '@ant-design/icons'
+import CustomIconSvg from '../../../assets/icons/panfilledicon.svg?react'
+
+const PanFilledIcon = (props) => (
+
+)
+
+export default PanFilledIcon
diff --git a/src/components/Icons/PanIcon.jsx b/src/components/Icons/PanIcon.jsx
new file mode 100644
index 00000000..b8034d0b
--- /dev/null
+++ b/src/components/Icons/PanIcon.jsx
@@ -0,0 +1,6 @@
+import Icon from '@ant-design/icons'
+import CustomIconSvg from '../../../assets/icons/panicon.svg?react'
+
+const PanIcon = (props) =>
+
+export default PanIcon