import type { App, Directive } from 'vue';
|
|
interface ResizableElement extends HTMLElement {
|
__modal_resizable__?: {
|
handleEl: HTMLElement;
|
initialHeight?: number;
|
initialWidth?: number;
|
onMousedown: (e: MouseEvent) => void;
|
styleObserver: MutationObserver;
|
};
|
}
|
|
const RESIZABLE_KEY = '__modal_resizable__' as const;
|
|
function resetModalSize(modalWrap: ResizableElement) {
|
const dragDom = modalWrap.querySelector('.ant-modal') as HTMLElement;
|
const modalContent = modalWrap.querySelector('.ant-modal-content') as HTMLElement;
|
if (dragDom) {
|
dragDom.style.width = '';
|
dragDom.style.left = '';
|
dragDom.style.top = '';
|
}
|
if (modalContent) {
|
modalContent.style.height = '';
|
modalContent.classList.remove('is-modal-resized');
|
}
|
const resizeData = modalWrap[RESIZABLE_KEY];
|
if (resizeData) {
|
resizeData.initialWidth = undefined;
|
resizeData.initialHeight = undefined;
|
}
|
}
|
|
function setupResize(modalWrap: ResizableElement) {
|
const dragDom = modalWrap.querySelector('.ant-modal') as HTMLElement;
|
const modalContent = modalWrap.querySelector('.ant-modal-content') as HTMLElement;
|
|
if (!dragDom || !modalContent) return;
|
if (modalWrap[RESIZABLE_KEY]) return;
|
if (modalWrap.classList.contains('fullscreen-modal')) return;
|
if (dragDom.classList.contains('ant-modal-confirm')) return;
|
|
const handleEl = document.createElement('div');
|
handleEl.className = 'modal-resize-handle';
|
modalContent.append(handleEl);
|
|
const styleObserver = new MutationObserver(() => {
|
if (modalWrap.style.display === 'none') {
|
resetModalSize(modalWrap);
|
}
|
});
|
styleObserver.observe(modalWrap, { attributes: true, attributeFilter: ['style'] });
|
|
const onMousedown = (e: MouseEvent) => {
|
if (!e) return;
|
e.preventDefault();
|
e.stopPropagation();
|
|
const resizeData = modalWrap[RESIZABLE_KEY];
|
if (!resizeData) return;
|
if (!resizeData.initialWidth || !resizeData.initialHeight) {
|
resizeData.initialWidth = dragDom.offsetWidth;
|
resizeData.initialHeight = dragDom.offsetHeight;
|
}
|
|
const startX = e.clientX;
|
const startY = e.clientY;
|
const startWidth = dragDom.offsetWidth;
|
const startHeight = dragDom.offsetHeight;
|
const minWidth = resizeData.initialWidth;
|
const minHeight = resizeData.initialHeight;
|
const startLeft = dragDom.getBoundingClientRect().left;
|
const startTop = dragDom.getBoundingClientRect().top;
|
|
const screenWidth = document.body.clientWidth;
|
const screenHeight = document.documentElement.clientHeight;
|
|
document.body.style.cursor = 'nwse-resize';
|
document.body.style.userSelect = 'none';
|
|
const onMousemove = (e: MouseEvent) => {
|
const deltaX = e.clientX - startX;
|
const deltaY = e.clientY - startY;
|
|
let newWidth = Math.max(minWidth, startWidth + deltaX);
|
let newHeight = Math.max(minHeight, startHeight + deltaY);
|
|
newWidth = Math.min(newWidth, screenWidth - startLeft);
|
newHeight = Math.min(newHeight, screenHeight - startTop);
|
|
modalContent.classList.add('is-modal-resized');
|
dragDom.style.width = `${newWidth}px`;
|
modalContent.style.height = `${newHeight}px`;
|
|
const currentLeft = dragDom.getBoundingClientRect().left;
|
if (currentLeft !== startLeft) {
|
const offset = currentLeft - startLeft;
|
const currentStyleLeft = Number.parseFloat(getComputedStyle(dragDom).left) || 0;
|
dragDom.style.left = `${currentStyleLeft - offset}px`;
|
}
|
|
const currentTop = dragDom.getBoundingClientRect().top;
|
if (currentTop !== startTop) {
|
const offset = currentTop - startTop;
|
const currentStyleTop = Number.parseFloat(getComputedStyle(dragDom).top) || 0;
|
dragDom.style.top = `${currentStyleTop - offset}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 ResizableElement)[RESIZABLE_KEY] = { handleEl, onMousedown, styleObserver };
|
}
|
|
function cleanupResize(modalWrap: ResizableElement) {
|
const resizeData = modalWrap[RESIZABLE_KEY];
|
if (resizeData) {
|
resizeData.handleEl.removeEventListener('mousedown', resizeData.onMousedown);
|
resizeData.handleEl.remove();
|
resizeData.styleObserver.disconnect();
|
resetModalSize(modalWrap);
|
delete modalWrap[RESIZABLE_KEY];
|
}
|
}
|
|
/** 自动为所有 .ant-modal-wrap 添加 resize 支持 */
|
export function setupGlobalResizeObserver() {
|
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')) {
|
setTimeout(() => setupResize(node as ResizableElement), 50);
|
}
|
const wraps = node.querySelectorAll?.('.ant-modal-wrap');
|
wraps?.forEach((wrap) => {
|
setTimeout(() => setupResize(wrap as ResizableElement), 50);
|
});
|
}
|
}
|
for (const node of mutation.removedNodes) {
|
if (node instanceof HTMLElement) {
|
if (node.classList.contains('ant-modal-wrap')) {
|
cleanupResize(node as ResizableElement);
|
}
|
const wraps = node.querySelectorAll?.('.ant-modal-wrap');
|
wraps?.forEach((wrap) => cleanupResize(wrap as ResizableElement));
|
}
|
}
|
}
|
});
|
|
observer.observe(document.body, { childList: true, subtree: true });
|
return observer;
|
}
|
|
/** v-resizable 指令,用于显式标记需要 resize 的 modal */
|
export const vResizable: Directive = {
|
mounted(el: HTMLElement) {
|
const modalWrap = (el.classList.contains('ant-modal-wrap') ? el : el.closest('.ant-modal-wrap')) as ResizableElement;
|
if (!modalWrap) return;
|
|
const observer = new MutationObserver(() => {
|
const dragDom = modalWrap.querySelector('.ant-modal');
|
if (dragDom) {
|
observer.disconnect();
|
setupResize(modalWrap);
|
}
|
});
|
|
observer.observe(modalWrap, { childList: true, subtree: true });
|
|
if (modalWrap.querySelector('.ant-modal')) {
|
observer.disconnect();
|
setupResize(modalWrap);
|
}
|
},
|
|
unmounted(el: HTMLElement) {
|
const modalWrap = (el.classList.contains('ant-modal-wrap') ? el : el.closest('.ant-modal-wrap')) as ResizableElement;
|
if (!modalWrap) return;
|
cleanupResize(modalWrap);
|
},
|
};
|
|
/** 全局注册指令和自动 resize */
|
export function registerResizable(app: App) {
|
app.directive('resizable', vResizable);
|
setupGlobalResizeObserver();
|
}
|