import type { App, Directive, DirectiveBinding } from 'vue';
|
|
interface DraggableElement extends HTMLElement {
|
__modal_draggable__?: {
|
handleEl: HTMLElement;
|
onMousedown: (e: MouseEvent) => void;
|
};
|
}
|
|
const DRAGGABLE_KEY = '__modal_draggable__' as const;
|
|
function setupDrag(modalWrap: DraggableElement, handleSelector: string) {
|
const dragDom = modalWrap.querySelector('.ant-modal') as HTMLElement;
|
const handleEl = modalWrap.querySelector(handleSelector) as HTMLElement;
|
|
if (!dragDom || !handleEl) return;
|
if (modalWrap[DRAGGABLE_KEY]) return;
|
|
handleEl.style.cursor = 'move';
|
handleEl.style.userSelect = 'none';
|
|
const onMousedown = (e: MouseEvent) => {
|
if (!e || modalWrap.classList.contains('fullscreen-modal')) return;
|
const disX = e.clientX;
|
const disY = e.clientY;
|
const screenWidth = document.body.clientWidth;
|
const screenHeight = document.documentElement.clientHeight;
|
|
const dragDomWidth = dragDom.offsetWidth;
|
const dragDomHeight = dragDom.offsetHeight;
|
const minDragDomLeft = dragDom.offsetLeft;
|
const maxDragDomLeft = screenWidth - dragDom.offsetLeft - dragDomWidth;
|
const minDragDomTop = dragDom.offsetTop;
|
let maxDragDomTop = screenHeight - dragDom.offsetTop - dragDomHeight;
|
if (maxDragDomTop < 0) maxDragDomTop = screenHeight - dragDom.offsetTop;
|
|
const domLeft = getComputedStyle(dragDom).left;
|
const domTop = getComputedStyle(dragDom).top;
|
let styL = +domLeft;
|
let styT = +domTop;
|
|
if (domLeft.includes('%')) {
|
styL = +document.body.clientWidth * (+domLeft.replaceAll('%', '') / 100);
|
styT = +document.body.clientHeight * (+domTop.replaceAll('%', '') / 100);
|
} else {
|
styL = +domLeft.replaceAll('px', '');
|
styT = +domTop.replaceAll('px', '');
|
}
|
|
document.body.style.cursor = 'move';
|
document.body.style.userSelect = 'none';
|
|
const onMousemove = (e: MouseEvent) => {
|
let left = e.clientX - disX;
|
let top = e.clientY - disY;
|
|
if (-left > minDragDomLeft) {
|
left = -minDragDomLeft;
|
} else if (left > maxDragDomLeft) {
|
left = maxDragDomLeft;
|
}
|
|
if (-top > minDragDomTop) {
|
top = -minDragDomTop;
|
} else if (top > maxDragDomTop) {
|
top = maxDragDomTop;
|
}
|
|
dragDom.style.cssText += `;left:${left + styL}px;top:${top + styT}px;`;
|
};
|
|
const onMouseup = () => {
|
document.body.style.cursor = '';
|
document.body.style.userSelect = '';
|
document.removeEventListener('mousemove', onMousemove);
|
document.removeEventListener('mouseup', onMouseup);
|
};
|
|
document.addEventListener('mousemove', onMousemove);
|
document.addEventListener('mouseup', onMouseup);
|
};
|
|
handleEl.addEventListener('mousedown', onMousedown);
|
(modalWrap as DraggableElement)[DRAGGABLE_KEY] = { handleEl, onMousedown };
|
}
|
|
function cleanupDrag(modalWrap: DraggableElement) {
|
const dragData = modalWrap[DRAGGABLE_KEY];
|
if (dragData) {
|
dragData.handleEl.removeEventListener('mousedown', dragData.onMousedown);
|
delete modalWrap[DRAGGABLE_KEY];
|
}
|
}
|
|
/** 自动为 .common-container-modal 添加拖拽支持 */
|
export function setupGlobalDragObserver() {
|
const observer = new MutationObserver((mutations) => {
|
for (const mutation of mutations) {
|
for (const node of mutation.addedNodes) {
|
if (node instanceof HTMLElement) {
|
if (node.classList.contains('ant-modal-wrap')) {
|
const modal = node.querySelector('.common-container-modal');
|
if (modal) {
|
setTimeout(() => setupDrag(node as DraggableElement, '.ant-modal-header'), 50);
|
}
|
}
|
const wraps = node.querySelectorAll?.('.ant-modal-wrap');
|
wraps?.forEach((wrap: HTMLElement) => {
|
const modal = wrap.querySelector('.common-container-modal');
|
if (modal) {
|
setTimeout(() => setupDrag(wrap as DraggableElement, '.ant-modal-header'), 50);
|
}
|
});
|
}
|
}
|
for (const node of mutation.removedNodes) {
|
if (node instanceof HTMLElement) {
|
if (node.classList.contains('ant-modal-wrap')) {
|
cleanupDrag(node as DraggableElement);
|
}
|
const wraps = node.querySelectorAll?.('.ant-modal-wrap');
|
wraps?.forEach((wrap: HTMLElement) => cleanupDrag(wrap as DraggableElement));
|
}
|
}
|
}
|
});
|
|
observer.observe(document.body, { childList: true, subtree: true });
|
return observer;
|
}
|
|
/** v-draggable 指令,用于显式标记需要拖拽的 modal */
|
export const vDraggable: Directive = {
|
mounted(el: HTMLElement, binding: DirectiveBinding<string | undefined>) {
|
const handleSelector = binding.value || '.ant-modal-header';
|
const modalWrap = (el.classList.contains('ant-modal-wrap') ? el : el.closest('.ant-modal-wrap')) as DraggableElement;
|
if (!modalWrap) return;
|
|
const observer = new MutationObserver(() => {
|
const dragDom = modalWrap.querySelector('.ant-modal');
|
if (dragDom) {
|
observer.disconnect();
|
setupDrag(modalWrap, handleSelector);
|
}
|
});
|
|
observer.observe(modalWrap, { childList: true, subtree: true });
|
|
if (modalWrap.querySelector('.ant-modal')) {
|
observer.disconnect();
|
setupDrag(modalWrap, handleSelector);
|
}
|
},
|
|
unmounted(el: HTMLElement) {
|
const modalWrap = (el.classList.contains('ant-modal-wrap') ? el : el.closest('.ant-modal-wrap')) as DraggableElement;
|
if (!modalWrap) return;
|
cleanupDrag(modalWrap);
|
},
|
};
|
|
/** 全局注册指令和自动拖拽 */
|
export function registerDraggable(app: App) {
|
app.directive('draggable', vDraggable);
|
setupGlobalDragObserver();
|
}
|