import type { Directive } from 'vue' export interface ResizeEvent { clientY: number deltaY: number initialHeight: number } export interface ResizeOptions { onStart?: (event: MouseEvent | TouchEvent) => void onResize?: (event: ResizeEvent) => void onEnd?: (event: ResizeEvent) => void minHeight?: number maxHeight?: number handle?: string gridSize?: number handleClass?: string } const RESIZE_KEY = Symbol('resize') function initResize(el: HTMLElement, binding: { value?: ResizeOptions }) { const options = binding.value || {} let isResizing = false let startY = 0 let startHeight = 0 let handleElement: HTMLElement | null = null // Create or find the resize handle if (options.handle) { handleElement = el.querySelector(options.handle) as HTMLElement if (!handleElement) { // If handle not found, create it handleElement = document.createElement('div') handleElement.className = options.handle.replace('.', '') + (options.handleClass ? ` ${options.handleClass}` : '') handleElement.style.cssText = ` position: absolute; bottom: 0; left: 0; right: 0; height: 6px; background: rgba(0, 0, 0, 0.1); cursor: ns-resize; transition: background 0.2s; ` el.appendChild(handleElement) } } else { handleElement = el } // Add hover effect handleElement.addEventListener('mouseenter', () => { if (handleElement !== el) { // Only change background if it's a dedicated handle handleElement!.style.background = 'rgba(0, 0, 0, 0.2)' } }) handleElement.addEventListener('mouseleave', () => { if (handleElement !== el && !isResizing) { // Only change background if it's a dedicated handle and not resizing handleElement!.style.background = 'rgba(0, 0, 0, 0.1)' } }) handleElement.style.cursor = 'ns-resize' handleElement.style.userSelect = 'none' function createResizeEvent(clientY: number): ResizeEvent { return { clientY, deltaY: clientY - startY, initialHeight: startHeight } } function applyGridSnapping(height: number): number { if (!options.gridSize) { return height } return Math.round(height / options.gridSize) * options.gridSize } function handleMove(e: MouseEvent | TouchEvent) { if (!isResizing) { return } e.preventDefault() const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY const event = createResizeEvent(clientY) let newHeight = startHeight + event.deltaY // Apply min/max constraints if (options.minHeight !== undefined) { newHeight = Math.max(newHeight, options.minHeight) } if (options.maxHeight !== undefined) { newHeight = Math.min(newHeight, options.maxHeight) } // Apply grid snapping if enabled newHeight = applyGridSnapping(newHeight) el.style.height = `${newHeight}px` if (options.onResize) { options.onResize(event) } // Update handle appearance during resize if (handleElement !== el) { handleElement!.style.background = 'rgba(0, 0, 0, 0.3)' } } function handleEnd(e: MouseEvent | TouchEvent) { if (!isResizing) { return } const clientY = 'touches' in e ? e.changedTouches[0].clientY : e.clientY const event = createResizeEvent(clientY) isResizing = false document.body.style.cursor = '' document.body.style.userSelect = '' // Reset handle appearance if (handleElement !== el) { handleElement!.style.background = 'rgba(0, 0, 0, 0.1)' } if (options.onEnd) { options.onEnd(event) } document.removeEventListener('mousemove', handleMove) document.removeEventListener('mouseup', handleEnd) document.removeEventListener('touchmove', handleMove) document.removeEventListener('touchend', handleEnd) document.removeEventListener('touchcancel', handleEnd) } function handleStart(e: MouseEvent | TouchEvent) { // Only handle left mouse button for mouse events if (e instanceof MouseEvent && e.button !== 0) { return } e.preventDefault() isResizing = true startY = 'touches' in e ? e.touches[0].clientY : e.clientY startHeight = el.offsetHeight if (options.onStart) { options.onStart(e) } document.body.style.cursor = 'ns-resize' document.body.style.userSelect = 'none' document.addEventListener('mousemove', handleMove, { passive: false }) document.addEventListener('mouseup', handleEnd) document.addEventListener('touchmove', handleMove, { passive: false }) document.addEventListener('touchend', handleEnd) document.addEventListener('touchcancel', handleEnd) } // Add initial event listeners handleElement.addEventListener('mousedown', handleStart) handleElement.addEventListener('touchstart', handleStart, { passive: false }) // Store cleanup function ;(el as any)[RESIZE_KEY] = () => { if (handleElement !== el) { el.removeChild(handleElement!) } handleElement.removeEventListener('mousedown', handleStart) handleElement.removeEventListener('touchstart', handleStart) handleElement.removeEventListener('mouseenter', handleStart) handleElement.removeEventListener('mouseleave', handleStart) document.removeEventListener('mousemove', handleMove) document.removeEventListener('mouseup', handleEnd) document.removeEventListener('touchmove', handleMove) document.removeEventListener('touchend', handleEnd) document.removeEventListener('touchcancel', handleEnd) } } export const vResize: Directive = { mounted(el, binding) { initResize(el, binding) }, unmounted(el) { const cleanup = (el as any)[RESIZE_KEY] if (typeof cleanup === 'function') { cleanup() delete (el as any)[RESIZE_KEY] } }, updated(el, binding) { if (binding.value !== binding.oldValue) { const cleanup = (el as any)[RESIZE_KEY] if (typeof cleanup === 'function') { cleanup() delete (el as any)[RESIZE_KEY] } initResize(el, binding) } }, getSSRProps() { return {} }, }