vue3-draggable-resizable
Version:
[Vue3 Component] 拖拽缩放并具有自动吸附对齐、参考线等功能
585 lines (574 loc) • 16.7 kB
text/typescript
import { onMounted, onUnmounted, ref, watch, Ref, computed } from 'vue'
import {
getElSize,
filterHandles,
getId,
getReferenceLineMap,
addEvent,
removeEvent
} from './utils'
import {
ContainerProvider,
MatchedLine,
ReferenceLineMap,
ResizingHandle
} from './types'
type HandleEvent = MouseEvent | TouchEvent
export function useState<T>(initialState: T): [Ref<T>, (value: T) => T] {
const state = ref(initialState) as Ref<T>
const setState = (value: T): T => {
state.value = value
return value
}
return [state, setState]
}
export function initState(props: any, emit: any) {
const [width, setWidth] = useState<number>(props.initW)
const [height, setHeight] = useState<number>(props.initH)
const [left, setLeft] = useState<number>(props.x)
const [top, setTop] = useState<number>(props.y)
const [enable, setEnable] = useState<boolean>(props.active)
const [dragging, setDragging] = useState<boolean>(false)
const [resizing, setResizing] = useState<boolean>(false)
const [resizingHandle, setResizingHandle] = useState<ResizingHandle>('')
const [resizingMaxWidth, setResizingMaxWidth] = useState<number>(Infinity)
const [resizingMaxHeight, setResizingMaxHeight] = useState<number>(Infinity)
const [resizingMinWidth, setResizingMinWidth] = useState<number>(props.minW)
const [resizingMinHeight, setResizingMinHeight] = useState<number>(props.minH)
const aspectRatio = computed(() => height.value / width.value)
watch(
width,
(newVal) => {
emit('update:w', newVal)
},
{ immediate: true }
)
watch(
height,
(newVal) => {
emit('update:h', newVal)
},
{ immediate: true }
)
watch(top, (newVal) => {
emit('update:y', newVal)
})
watch(left, (newVal) => {
emit('update:x', newVal)
})
watch(enable, (newVal, oldVal) => {
emit('update:active', newVal)
if (!oldVal && newVal) {
emit('activated')
} else if (oldVal && !newVal) {
emit('deactivated')
}
})
watch(
() => props.active,
(newVal: boolean) => {
setEnable(newVal)
}
)
return {
id: getId(),
width,
height,
top,
left,
enable,
dragging,
resizing,
resizingHandle,
resizingMaxHeight,
resizingMaxWidth,
resizingMinWidth,
resizingMinHeight,
aspectRatio,
setEnable,
setDragging,
setResizing,
setResizingHandle,
setResizingMaxHeight,
setResizingMaxWidth,
setResizingMinWidth,
setResizingMinHeight,
setWidth: (val: number) => setWidth(Math.floor(val)),
setHeight: (val: number) => setHeight(Math.floor(val)),
setTop: (val: number) => setTop(Math.floor(val)),
setLeft: (val: number) => setLeft(Math.floor(val))
}
}
export function initParent(containerRef: Ref<HTMLElement | undefined>) {
const parentWidth = ref(0)
const parentHeight = ref(0)
onMounted(() => {
if (containerRef.value && containerRef.value.parentElement) {
const { width, height } = getElSize(containerRef.value.parentElement)
parentWidth.value = width
parentHeight.value = height
}
})
return {
parentWidth,
parentHeight
}
}
export function initLimitSizeAndMethods(
props: any,
parentSize: ReturnType<typeof initParent>,
containerProps: ReturnType<typeof initState>
) {
const {
width,
height,
left,
top,
resizingMaxWidth,
resizingMaxHeight,
resizingMinWidth,
resizingMinHeight
} = containerProps
const { setWidth, setHeight, setTop, setLeft } = containerProps
const { parentWidth, parentHeight } = parentSize
const limitProps = {
minWidth: computed(() => {
return resizingMinWidth.value
}),
minHeight: computed(() => {
return resizingMinHeight.value
}),
maxWidth: computed(() => {
let max = Infinity
if (props.parent) {
max = Math.min(parentWidth.value, resizingMaxWidth.value)
}
return max
}),
maxHeight: computed(() => {
let max = Infinity
if (props.parent) {
max = Math.min(parentHeight.value, resizingMaxHeight.value)
}
return max
}),
minLeft: computed(() => {
return props.parent ? 0 : -Infinity
}),
minTop: computed(() => {
return props.parent ? 0 : -Infinity
}),
maxLeft: computed(() => {
return props.parent ? parentWidth.value - width.value : Infinity
}),
maxTop: computed(() => {
return props.parent ? parentHeight.value - height.value : Infinity
})
}
const limitMethods = {
setWidth(val: number) {
if (props.disabledW) {
return width.value
}
return setWidth(
Math.min(
limitProps.maxWidth.value,
Math.max(limitProps.minWidth.value, val)
)
)
},
setHeight(val: number) {
if (props.disabledH) {
return height.value
}
return setHeight(
Math.min(
limitProps.maxHeight.value,
Math.max(limitProps.minHeight.value, val)
)
)
},
setTop(val: number) {
if (props.disabledY) {
return top.value
}
return setTop(
Math.min(
limitProps.maxTop.value,
Math.max(limitProps.minTop.value, val)
)
)
},
setLeft(val: number) {
if (props.disabledX) {
return left.value
}
return setLeft(
Math.min(
limitProps.maxLeft.value,
Math.max(limitProps.minLeft.value, val)
)
)
}
}
return {
...limitProps,
...limitMethods
}
}
const DOWN_HANDLES: (keyof HTMLElementEventMap)[] = ['mousedown', 'touchstart']
const UP_HANDLES: (keyof HTMLElementEventMap)[] = ['mouseup', 'touchend']
const MOVE_HANDLES: (keyof HTMLElementEventMap)[] = ['mousemove', 'touchmove']
function getPosition(e: HandleEvent) {
if ('touches' in e) {
return [e.touches[0].pageX, e.touches[0].pageY]
} else {
return [e.pageX, e.pageY]
}
}
export function initDraggableContainer(
containerRef: Ref<HTMLElement | undefined>,
containerProps: ReturnType<typeof initState>,
limitProps: ReturnType<typeof initLimitSizeAndMethods>,
draggable: Ref<boolean>,
emit: any,
containerProvider: ContainerProvider | null,
parentSize: ReturnType<typeof initParent>
) {
const { left: x, top: y, width: w, height: h, dragging, id } = containerProps
const {
setDragging,
setEnable,
setResizing,
setResizingHandle
} = containerProps
const { setTop, setLeft } = limitProps
let lstX = 0
let lstY = 0
let lstPageX = 0
let lstPageY = 0
let referenceLineMap: ReferenceLineMap | null = null
const documentElement = document.documentElement
const _unselect = (e: HandleEvent) => {
const target = e.target
if (!containerRef.value?.contains(<Node>target)) {
setEnable(false)
setDragging(false)
setResizing(false)
setResizingHandle('')
}
}
const handleUp = () => {
setDragging(false)
// document.documentElement.removeEventListener('mouseup', handleUp)
// document.documentElement.removeEventListener('mousemove', handleDrag)
removeEvent(documentElement, UP_HANDLES, handleUp)
removeEvent(documentElement, MOVE_HANDLES, handleDrag)
referenceLineMap = null
if (containerProvider) {
containerProvider.updatePosition(id, {
x: x.value,
y: y.value,
w: w.value,
h: h.value
})
containerProvider.setMatchedLine(null)
}
}
const handleDrag = (e: MouseEvent) => {
e.preventDefault()
if (!(dragging.value && containerRef.value)) return
const [pageX, pageY] = getPosition(e)
const deltaX = pageX - lstPageX
const deltaY = pageY - lstPageY
let newLeft = lstX + deltaX
let newTop = lstY + deltaY
if (referenceLineMap !== null) {
const widgetSelfLine = {
col: [newLeft, newLeft + w.value / 2, newLeft + w.value],
row: [newTop, newTop + h.value / 2, newTop + h.value]
}
const matchedLine: unknown = {
row: widgetSelfLine.row
.map((i, index) => {
let match = null
Object.values(referenceLineMap!.row).forEach((referItem) => {
if (i >= referItem.min && i <= referItem.max) {
match = referItem.value
}
})
if (match !== null) {
if (index === 0) {
newTop = match
} else if (index === 1) {
newTop = Math.floor(match - h.value / 2)
} else if (index === 2) {
newTop = Math.floor(match - h.value)
}
}
return match
})
.filter((i) => i !== null),
col: widgetSelfLine.col
.map((i, index) => {
let match = null
Object.values(referenceLineMap!.col).forEach((referItem) => {
if (i >= referItem.min && i <= referItem.max) {
match = referItem.value
}
})
if (match !== null) {
if (index === 0) {
newLeft = match
} else if (index === 1) {
newLeft = Math.floor(match - w.value / 2)
} else if (index === 2) {
newLeft = Math.floor(match - w.value)
}
}
return match
})
.filter((i) => i !== null)
}
containerProvider!.setMatchedLine(matchedLine as MatchedLine)
}
emit('dragging', { x: setLeft(newLeft), y: setTop(newTop) })
}
const handleDown = (e: HandleEvent) => {
if (!draggable.value) return
setDragging(true)
lstX = x.value
lstY = y.value
lstPageX = getPosition(e)[0]
lstPageY = getPosition(e)[1]
// document.documentElement.addEventListener('mousemove', handleDrag)
// document.documentElement.addEventListener('mouseup', handleUp)
addEvent(documentElement, MOVE_HANDLES, handleDrag)
addEvent(documentElement, UP_HANDLES, handleUp)
if (containerProvider && !containerProvider.disabled.value) {
referenceLineMap = getReferenceLineMap(containerProvider, parentSize, id)
}
}
watch(dragging, (cur, pre) => {
if (!pre && cur) {
emit('drag-start', { x: x.value, y: y.value })
setEnable(true)
setDragging(true)
} else {
emit('drag-end', { x: x.value, y: y.value })
setDragging(false)
}
})
onMounted(() => {
const el = containerRef.value
if (!el) return
el.style.left = x + 'px'
el.style.top = y + 'px'
// document.documentElement.addEventListener('mousedown', _unselect)
// el.addEventListener('mousedown', handleDown)
addEvent(documentElement, DOWN_HANDLES, _unselect)
addEvent(el, DOWN_HANDLES, handleDown)
})
onUnmounted(() => {
if (!containerRef.value) return
// document.documentElement.removeEventListener('mousedown', _unselect)
// document.documentElement.removeEventListener('mouseup', handleUp)
// document.documentElement.removeEventListener('mousemove', handleDrag)
removeEvent(documentElement, DOWN_HANDLES, _unselect)
removeEvent(documentElement, UP_HANDLES, handleUp)
removeEvent(documentElement, MOVE_HANDLES, handleDrag)
})
return { containerRef }
}
export function initResizeHandle(
containerProps: ReturnType<typeof initState>,
limitProps: ReturnType<typeof initLimitSizeAndMethods>,
parentSize: ReturnType<typeof initParent>,
props: any,
emit: any
) {
const { setWidth, setHeight, setLeft, setTop } = limitProps
const { width, height, left, top, aspectRatio } = containerProps
const {
setResizing,
setResizingHandle,
setResizingMaxWidth,
setResizingMaxHeight,
setResizingMinWidth,
setResizingMinHeight
} = containerProps
const { parentWidth, parentHeight } = parentSize
let lstW = 0
let lstH = 0
let lstX = 0
let lstY = 0
let lstPageX = 0
let lstPageY = 0
let tmpAspectRatio = 1
let idx0 = ''
let idx1 = ''
const documentElement = document.documentElement
const resizeHandleDrag = (e: HandleEvent) => {
e.preventDefault()
let [_pageX, _pageY] = getPosition(e)
let deltaX = _pageX - lstPageX
let deltaY = _pageY - lstPageY
let _deltaX = deltaX
let _deltaY = deltaY
if (props.lockAspectRatio) {
deltaX = Math.abs(deltaX)
deltaY = deltaX * tmpAspectRatio
if (idx0 === 't') {
if (_deltaX < 0 || (idx1 === 'm' && _deltaY < 0)) {
deltaX = -deltaX
deltaY = -deltaY
}
} else {
if (_deltaX < 0 || (idx1 === 'm' && _deltaY < 0)) {
deltaX = -deltaX
deltaY = -deltaY
}
}
}
if (idx0 === 't') {
setHeight(lstH - deltaY)
setTop(lstY - (height.value - lstH))
} else if (idx0 === 'b') {
setHeight(lstH + deltaY)
}
if (idx1 === 'l') {
setWidth(lstW - deltaX)
setLeft(lstX - (width.value - lstW))
} else if (idx1 === 'r') {
setWidth(lstW + deltaX)
}
emit('resizing', {
x: left.value,
y: top.value,
w: width.value,
h: height.value
})
}
const resizeHandleUp = () => {
emit('resize-end', {
x: left.value,
y: top.value,
w: width.value,
h: height.value
})
setResizingHandle('')
setResizing(false)
setResizingMaxWidth(Infinity)
setResizingMaxHeight(Infinity)
setResizingMinWidth(props.minW)
setResizingMinHeight(props.minH)
// document.documentElement.removeEventListener('mousemove', resizeHandleDrag)
// document.documentElement.removeEventListener('mouseup', resizeHandleUp)
removeEvent(documentElement, MOVE_HANDLES, resizeHandleDrag)
removeEvent(documentElement, UP_HANDLES, resizeHandleUp)
}
const resizeHandleDown = (e: HandleEvent, handleType: ResizingHandle) => {
if (!props.resizable) return
e.stopPropagation()
setResizingHandle(handleType)
setResizing(true)
idx0 = handleType[0]
idx1 = handleType[1]
if (props.lockAspectRatio) {
if (['tl', 'tm', 'ml', 'bl'].includes(handleType)) {
idx0 = 't'
idx1 = 'l'
} else {
idx0 = 'b'
idx1 = 'r'
}
}
let minHeight = props.minH as number
let minWidth = props.minW as number
if (props.lockAspectRatio) {
if (minHeight / minWidth > aspectRatio.value) {
minWidth = minHeight / aspectRatio.value
} else {
minHeight = minWidth * aspectRatio.value
}
}
setResizingMinWidth(minWidth)
setResizingMinHeight(minHeight)
if (props.parent) {
let maxHeight =
idx0 === 't' ? top.value + height.value : parentHeight.value - top.value
let maxWidth =
idx1 === 'l' ? left.value + width.value : parentWidth.value - left.value
if (props.lockAspectRatio) {
if (maxHeight / maxWidth < aspectRatio.value) {
maxWidth = maxHeight / aspectRatio.value
} else {
maxHeight = maxWidth * aspectRatio.value
}
}
setResizingMaxHeight(maxHeight)
setResizingMaxWidth(maxWidth)
}
lstW = width.value
lstH = height.value
lstX = left.value
lstY = top.value
const lstPagePosition = getPosition(e)
lstPageX = lstPagePosition[0]
lstPageY = lstPagePosition[1]
tmpAspectRatio = aspectRatio.value
emit('resize-start', {
x: left.value,
y: top.value,
w: width.value,
h: height.value
})
// document.documentElement.addEventListener('mousemove', resizeHandleDrag)
// document.documentElement.addEventListener('mouseup', resizeHandleUp)
addEvent(documentElement, MOVE_HANDLES, resizeHandleDrag)
addEvent(documentElement, UP_HANDLES, resizeHandleUp)
}
onUnmounted(() => {
// document.documentElement.removeEventListener('mouseup', resizeHandleDrag)
// document.documentElement.removeEventListener('mousemove', resizeHandleUp)
removeEvent(documentElement, UP_HANDLES, resizeHandleUp)
removeEvent(documentElement, MOVE_HANDLES, resizeHandleDrag)
})
const handlesFiltered = computed(() =>
props.resizable ? filterHandles(props.handles) : []
)
return {
handlesFiltered,
resizeHandleDown
}
}
export function watchProps(
props: any,
limits: ReturnType<typeof initLimitSizeAndMethods>
) {
const { setWidth, setHeight, setLeft, setTop } = limits
watch(
() => props.w,
(newVal: number) => {
setWidth(newVal)
}
)
watch(
() => props.h,
(newVal: number) => {
setHeight(newVal)
}
)
watch(
() => props.x,
(newVal: number) => {
setLeft(newVal)
}
)
watch(
() => props.y,
(newVal: number) => {
setTop(newVal)
}
)
}