This commit is contained in:
2026-06-14 19:11:37 +03:30
parent 3254a6dadb
commit b3113bc398
9 changed files with 185 additions and 40 deletions
+90 -15
View File
@@ -1,24 +1,54 @@
import { computed, onMounted, onUnmounted, ref } from 'vue'
const DRAG_THRESHOLD = 6
const POSITION_VERSION = 1
type FabPosition = { x: number; y: number }
type SavedFabOffset = {
version: number
dx: number
dy: number
}
type DraggableFloatingOptions = {
width?: number
height?: number
defaultBottomOffset?: number
/** فاصله از پایینِ نوار منو + montzar (px) — مثل تصویر */
defaultBottomGap?: number
defaultRightOffset?: number
}
function readBottomChromeHeight(): number {
const styles = getComputedStyle(document.documentElement)
const direct = parseFloat(styles.getPropertyValue('--app-bottom-chrome-height'))
if (!Number.isNaN(direct) && direct > 0) {
return direct
}
const nav = parseFloat(styles.getPropertyValue('--app-nav-height')) || 80
const creditOffset = parseFloat(styles.getPropertyValue('--app-credit-footer-offset'))
if (!Number.isNaN(creditOffset) && creditOffset > 0) {
return nav + creditOffset
}
const probe = document.createElement('div')
probe.style.cssText = 'position:fixed;visibility:hidden;pointer-events:none;height:var(--app-bottom-chrome-height)'
document.body.appendChild(probe)
const measured = probe.getBoundingClientRect().height
document.body.removeChild(probe)
return measured > 0 ? measured : nav + 28
}
export function useDraggableFloatingButton(
storageKey: string,
options: DraggableFloatingOptions = {}
) {
const width = options.width ?? 96
const height = options.height ?? 44
const defaultBottomOffset = options.defaultBottomOffset ?? 120
const defaultRightOffset = options.defaultRightOffset ?? 20
const defaultBottomGap = options.defaultBottomGap ?? 12
const defaultRightOffset = options.defaultRightOffset ?? 16
const pos = ref<FabPosition>({ x: 0, y: 0 })
const dragging = ref(false)
@@ -44,26 +74,40 @@ export function useDraggableFloatingButton(
function getHorizontalOffset() {
const vw = window.innerWidth
const contentWidth = getViewportWidth()
return Math.max(0, (vw - contentWidth) / 2)
}
function resetToDefault() {
function getDefaultPosition(): FabPosition {
const vh = window.innerHeight
const offsetX = getHorizontalOffset()
const contentWidth = getViewportWidth()
const bottomChrome = readBottomChromeHeight()
pos.value = {
return {
x: offsetX + contentWidth - width - defaultRightOffset,
y: vh - defaultBottomOffset - height
y: vh - bottomChrome - defaultBottomGap - height
}
}
function applySavedOffset(dx: number, dy: number) {
const base = getDefaultPosition()
pos.value = {
x: base.x + dx,
y: base.y + dy
}
clampToViewport()
}
function clampToViewport() {
const vw = window.innerWidth
const vh = window.innerHeight
const offsetX = getHorizontalOffset()
const contentWidth = getViewportWidth()
const minX = offsetX
const maxX = offsetX + contentWidth - width
pos.value = {
x: Math.max(0, Math.min(vw - width, pos.value.x)),
x: Math.max(minX, Math.min(maxX, pos.value.x)),
y: Math.max(0, Math.min(vh - height, pos.value.y))
}
}
@@ -72,25 +116,56 @@ export function useDraggableFloatingButton(
try {
const raw = localStorage.getItem(storageKey)
if (!raw) return false
const parsed = JSON.parse(raw) as FabPosition
if (typeof parsed?.x === 'number' && typeof parsed?.y === 'number') {
pos.value = parsed
clampToViewport()
const parsed = JSON.parse(raw) as SavedFabOffset
if (
parsed?.version === POSITION_VERSION
&& typeof parsed.dx === 'number'
&& typeof parsed.dy === 'number'
) {
applySavedOffset(parsed.dx, parsed.dy)
return true
}
} catch {
// ignore
}
return false
}
function savePosition() {
localStorage.setItem(storageKey, JSON.stringify(pos.value))
const base = getDefaultPosition()
const payload: SavedFabOffset = {
version: POSITION_VERSION,
dx: pos.value.x - base.x,
dy: pos.value.y - base.y
}
localStorage.setItem(storageKey, JSON.stringify(payload))
}
function resetToDefault() {
pos.value = getDefaultPosition()
}
function handleResize() {
clampToViewport()
savePosition()
try {
const raw = localStorage.getItem(storageKey)
if (raw) {
const parsed = JSON.parse(raw) as SavedFabOffset
if (
parsed?.version === POSITION_VERSION
&& typeof parsed.dx === 'number'
&& typeof parsed.dy === 'number'
) {
applySavedOffset(parsed.dx, parsed.dy)
return
}
}
} catch {
// ignore
}
resetToDefault()
}
function handlePointerDown(event: PointerEvent) {