514 lines
16 KiB
Vue
514 lines
16 KiB
Vue
import { defineComponent, reactive, onMounted, onUpdated, onUnmounted, nextTick, watch } from 'vue';
|
|
import classnames from '../../_util/classNames';
|
|
import getScrollBarSize from '../../_util/getScrollBarSize';
|
|
import KeyCode from '../../_util/KeyCode';
|
|
import omit from '../../_util/omit';
|
|
import supportsPassive from '../../_util/supportsPassive';
|
|
import { DrawerChildProps } from './IDrawerPropTypes';
|
|
import type { IDrawerChildProps } from './IDrawerPropTypes';
|
|
|
|
import {
|
|
addEventListener,
|
|
dataToArray,
|
|
getTouchParentScroll,
|
|
isNumeric,
|
|
removeEventListener,
|
|
transformArguments,
|
|
transitionEndFun,
|
|
transitionStr,
|
|
windowIsUndefined,
|
|
} from './utils';
|
|
|
|
const currentDrawer: Record<string, boolean> = {};
|
|
|
|
const DrawerChild = defineComponent({
|
|
inheritAttrs: false,
|
|
props: DrawerChildProps,
|
|
emits: ['close', 'handleClick', 'change'],
|
|
setup(props, { emit, slots, expose }) {
|
|
const state = reactive({
|
|
levelDom: [],
|
|
dom: null,
|
|
contentWrapper: null,
|
|
contentDom: null,
|
|
maskDom: null,
|
|
handlerDom: null,
|
|
drawerId: null,
|
|
timeout: null,
|
|
passive: null,
|
|
startPos: {
|
|
x: null,
|
|
y: null,
|
|
},
|
|
});
|
|
|
|
onMounted(() => {
|
|
nextTick(() => {
|
|
if (!windowIsUndefined) {
|
|
state.passive = supportsPassive ? { passive: false } : false;
|
|
}
|
|
const { open, getContainer, showMask, autoFocus } = props;
|
|
const container = getContainer?.();
|
|
state.drawerId = `drawer_id_${Number(
|
|
(Date.now() + Math.random())
|
|
.toString()
|
|
.replace('.', Math.round(Math.random() * 9).toString()),
|
|
).toString(16)}`;
|
|
getLevelDom(props);
|
|
if (open) {
|
|
if (container && container.parentNode === document.body) {
|
|
currentDrawer[state.drawerId] = open;
|
|
}
|
|
// 默认打开状态时推出 level;
|
|
openLevelTransition();
|
|
nextTick(() => {
|
|
if (autoFocus) {
|
|
domFocus();
|
|
}
|
|
});
|
|
if (showMask) {
|
|
props.scrollLocker?.lock();
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
onUpdated(() => {
|
|
const { open, getContainer, scrollLocker, showMask, autoFocus } = props;
|
|
const container = getContainer?.();
|
|
if (container && container.parentNode === document.body) {
|
|
currentDrawer[state.drawerId] = !!open;
|
|
}
|
|
openLevelTransition();
|
|
if (open) {
|
|
if (autoFocus) {
|
|
domFocus();
|
|
}
|
|
if (showMask) {
|
|
scrollLocker?.lock();
|
|
}
|
|
} else {
|
|
scrollLocker?.unLock();
|
|
}
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
const { open, scrollLocker } = props;
|
|
delete currentDrawer[state.drawerId];
|
|
if (open) {
|
|
setLevelTransform(false);
|
|
document.body.style.touchAction = '';
|
|
}
|
|
scrollLocker?.unLock();
|
|
});
|
|
|
|
watch(
|
|
() => props.placement,
|
|
val => {
|
|
if (val) {
|
|
// test 的 bug, 有动画过场,删除 dom
|
|
state.contentDom = null;
|
|
if (state.contentWrapper) {
|
|
state.contentWrapper.style.transition = `none`;
|
|
setTimeout(() => {
|
|
state.contentWrapper.style.transition = ``;
|
|
});
|
|
}
|
|
}
|
|
},
|
|
);
|
|
|
|
const domFocus = () => {
|
|
state.dom?.focus?.();
|
|
};
|
|
|
|
const removeStartHandler = (e: TouchEvent) => {
|
|
if (e.touches.length > 1) {
|
|
return;
|
|
}
|
|
state.startPos = {
|
|
x: e.touches[0].clientX,
|
|
y: e.touches[0].clientY,
|
|
};
|
|
};
|
|
|
|
const removeMoveHandler = (e: TouchEvent) => {
|
|
if (e.changedTouches.length > 1) {
|
|
return;
|
|
}
|
|
const currentTarget = e.currentTarget as HTMLElement;
|
|
const differX = e.changedTouches[0].clientX - state.startPos.x;
|
|
const differY = e.changedTouches[0].clientY - state.startPos.y;
|
|
if (
|
|
(currentTarget === state.maskDom ||
|
|
currentTarget === state.handlerDom ||
|
|
(currentTarget === state.contentDom &&
|
|
getTouchParentScroll(currentTarget, e.target as HTMLElement, differX, differY))) &&
|
|
e.cancelable
|
|
) {
|
|
e.preventDefault();
|
|
}
|
|
};
|
|
|
|
const transitionEnd = (e: TransitionEvent) => {
|
|
const dom: HTMLElement = e.target as HTMLElement;
|
|
removeEventListener(dom, transitionEndFun, transitionEnd);
|
|
dom.style.transition = '';
|
|
};
|
|
|
|
const onKeyDown = (e: KeyboardEvent) => {
|
|
if (e.keyCode === KeyCode.ESC) {
|
|
e.stopPropagation();
|
|
onClose(e);
|
|
}
|
|
};
|
|
|
|
const onClose = (e: Event) => {
|
|
emit('close', e);
|
|
};
|
|
|
|
const onWrapperTransitionEnd = (e: TransitionEvent) => {
|
|
const { open, afterVisibleChange } = props;
|
|
if (e.target === state.contentWrapper && e.propertyName.match(/transform$/)) {
|
|
state.dom.style.transition = '';
|
|
if (!open && getCurrentDrawerSome()) {
|
|
document.body.style.overflowX = '';
|
|
if (state.maskDom) {
|
|
state.maskDom.style.left = '';
|
|
state.maskDom.style.width = '';
|
|
}
|
|
}
|
|
if (afterVisibleChange) {
|
|
afterVisibleChange(!!open);
|
|
}
|
|
}
|
|
};
|
|
|
|
const openLevelTransition = () => {
|
|
const { open, width, height } = props;
|
|
const { isHorizontal, placementName } = getHorizontalBoolAndPlacementName();
|
|
const contentValue = state.contentDom
|
|
? state.contentDom.getBoundingClientRect()[isHorizontal ? 'width' : 'height']
|
|
: 0;
|
|
const value = (isHorizontal ? width : height) || contentValue;
|
|
setLevelAndScrolling(open, placementName, value);
|
|
};
|
|
|
|
const setLevelTransform = (
|
|
open?: boolean,
|
|
placementName?: string,
|
|
value?: string | number,
|
|
right?: number,
|
|
) => {
|
|
const { placement, levelMove, duration, ease, showMask } = props;
|
|
// router 切换时可能会导至页面失去滚动条,所以需要时时获取。
|
|
state.levelDom.forEach(dom => {
|
|
dom.style.transition = `transform ${duration} ${ease}`;
|
|
addEventListener(dom, transitionEndFun, transitionEnd);
|
|
let levelValue = open ? value : 0;
|
|
if (levelMove) {
|
|
const $levelMove = transformArguments(levelMove, { target: dom, open });
|
|
levelValue = open ? $levelMove[0] : $levelMove[1] || 0;
|
|
}
|
|
const $value = typeof levelValue === 'number' ? `${levelValue}px` : levelValue;
|
|
let placementPos = placement === 'left' || placement === 'top' ? $value : `-${$value}`;
|
|
placementPos =
|
|
showMask && placement === 'right' && right
|
|
? `calc(${placementPos} + ${right}px)`
|
|
: placementPos;
|
|
dom.style.transform = levelValue ? `${placementName}(${placementPos})` : '';
|
|
});
|
|
};
|
|
|
|
const setLevelAndScrolling = (
|
|
open?: boolean,
|
|
placementName?: string,
|
|
value?: string | number,
|
|
) => {
|
|
if (!windowIsUndefined) {
|
|
const right =
|
|
document.body.scrollHeight >
|
|
(window.innerHeight || document.documentElement.clientHeight) &&
|
|
window.innerWidth > document.body.offsetWidth
|
|
? getScrollBarSize(true)
|
|
: 0;
|
|
setLevelTransform(open, placementName, value, right);
|
|
toggleScrollingToDrawerAndBody(right);
|
|
}
|
|
emit('change', open);
|
|
};
|
|
|
|
const toggleScrollingToDrawerAndBody = (right: number) => {
|
|
const { getContainer, showMask, open } = props;
|
|
const container = getContainer?.();
|
|
// 处理 body 滚动
|
|
if (container && container.parentNode === document.body && showMask) {
|
|
const eventArray = ['touchstart'];
|
|
const domArray = [document.body, state.maskDom, state.handlerDom, state.contentDom];
|
|
if (open && document.body.style.overflow !== 'hidden') {
|
|
if (right) {
|
|
addScrollingEffect(right);
|
|
}
|
|
document.body.style.touchAction = 'none';
|
|
// 手机禁滚
|
|
domArray.forEach((item, i) => {
|
|
if (!item) {
|
|
return;
|
|
}
|
|
addEventListener(
|
|
item,
|
|
eventArray[i] || 'touchmove',
|
|
i ? removeMoveHandler : removeStartHandler,
|
|
state.passive,
|
|
);
|
|
});
|
|
} else if (getCurrentDrawerSome()) {
|
|
document.body.style.touchAction = '';
|
|
if (right) {
|
|
remScrollingEffect(right);
|
|
}
|
|
// 恢复事件
|
|
domArray.forEach((item, i) => {
|
|
if (!item) {
|
|
return;
|
|
}
|
|
removeEventListener(
|
|
item,
|
|
eventArray[i] || 'touchmove',
|
|
i ? removeMoveHandler : removeStartHandler,
|
|
state.passive,
|
|
);
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
const addScrollingEffect = (right: number) => {
|
|
const { placement, duration, ease } = props;
|
|
const widthTransition = `width ${duration} ${ease}`;
|
|
const transformTransition = `transform ${duration} ${ease}`;
|
|
state.dom.style.transition = 'none';
|
|
switch (placement) {
|
|
case 'right':
|
|
state.dom.style.transform = `translateX(-${right}px)`;
|
|
break;
|
|
case 'top':
|
|
case 'bottom':
|
|
state.dom.style.width = `calc(100% - ${right}px)`;
|
|
state.dom.style.transform = 'translateZ(0)';
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
clearTimeout(state.timeout);
|
|
state.timeout = setTimeout(() => {
|
|
if (state.dom) {
|
|
state.dom.style.transition = `${transformTransition},${widthTransition}`;
|
|
state.dom.style.width = '';
|
|
state.dom.style.transform = '';
|
|
}
|
|
});
|
|
};
|
|
|
|
const remScrollingEffect = (right: number) => {
|
|
const { placement, duration, ease } = props;
|
|
|
|
if (transitionStr) {
|
|
document.body.style.overflowX = 'hidden';
|
|
}
|
|
state.dom.style.transition = 'none';
|
|
let heightTransition: string;
|
|
let widthTransition = `width ${duration} ${ease}`;
|
|
const transformTransition = `transform ${duration} ${ease}`;
|
|
switch (placement) {
|
|
case 'left': {
|
|
state.dom.style.width = '100%';
|
|
widthTransition = `width 0s ${ease} ${duration}`;
|
|
break;
|
|
}
|
|
case 'right': {
|
|
state.dom.style.transform = `translateX(${right}px)`;
|
|
state.dom.style.width = '100%';
|
|
widthTransition = `width 0s ${ease} ${duration}`;
|
|
if (state.maskDom) {
|
|
state.maskDom.style.left = `-${right}px`;
|
|
state.maskDom.style.width = `calc(100% + ${right}px)`;
|
|
}
|
|
break;
|
|
}
|
|
case 'top':
|
|
case 'bottom': {
|
|
state.dom.style.width = `calc(100% + ${right}px)`;
|
|
state.dom.style.height = '100%';
|
|
state.dom.style.transform = 'translateZ(0)';
|
|
heightTransition = `height 0s ${ease} ${duration}`;
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
clearTimeout(state.timeout);
|
|
state.timeout = setTimeout(() => {
|
|
if (state.dom) {
|
|
state.dom.style.transition = `${transformTransition},${
|
|
heightTransition ? `${heightTransition},` : ''
|
|
}${widthTransition}`;
|
|
state.dom.style.transform = '';
|
|
state.dom.style.width = '';
|
|
state.dom.style.height = '';
|
|
}
|
|
});
|
|
};
|
|
|
|
const getCurrentDrawerSome = () => !Object.keys(currentDrawer).some(key => currentDrawer[key]);
|
|
|
|
const getLevelDom = ({ level, getContainer }: IDrawerChildProps) => {
|
|
if (windowIsUndefined) {
|
|
return;
|
|
}
|
|
const container = getContainer?.();
|
|
const parent = container ? (container.parentNode as HTMLElement) : null;
|
|
state.levelDom = [];
|
|
if (level === 'all') {
|
|
const children: HTMLElement[] = parent ? Array.prototype.slice.call(parent.children) : [];
|
|
children.forEach((child: HTMLElement) => {
|
|
if (
|
|
child.nodeName !== 'SCRIPT' &&
|
|
child.nodeName !== 'STYLE' &&
|
|
child.nodeName !== 'LINK' &&
|
|
child !== container
|
|
) {
|
|
state.levelDom.push(child);
|
|
}
|
|
});
|
|
} else if (level) {
|
|
dataToArray(level).forEach(key => {
|
|
document.querySelectorAll(key).forEach(item => {
|
|
state.levelDom.push(item);
|
|
});
|
|
});
|
|
}
|
|
};
|
|
|
|
const getHorizontalBoolAndPlacementName = () => {
|
|
const { placement } = props;
|
|
const isHorizontal = placement === 'left' || placement === 'right';
|
|
const placementName = `translate${isHorizontal ? 'X' : 'Y'}`;
|
|
return {
|
|
isHorizontal,
|
|
placementName,
|
|
};
|
|
};
|
|
|
|
const getDerivedStateFromProps = (
|
|
props: IDrawerChildProps,
|
|
{ prevProps }: { prevProps: IDrawerChildProps },
|
|
) => {
|
|
const nextState = {
|
|
prevProps: props,
|
|
};
|
|
if (prevProps !== undefined) {
|
|
const { placement, level } = props;
|
|
if (placement !== prevProps.placement) {
|
|
// test 的 bug, 有动画过场,删除 dom
|
|
state.contentDom = null;
|
|
}
|
|
if (level !== prevProps.level) {
|
|
getLevelDom(props);
|
|
}
|
|
}
|
|
return nextState;
|
|
};
|
|
|
|
expose({ getDerivedStateFromProps });
|
|
|
|
return () => {
|
|
const {
|
|
width,
|
|
height,
|
|
open: $open,
|
|
prefixCls,
|
|
placement,
|
|
level,
|
|
levelMove,
|
|
ease,
|
|
duration,
|
|
getContainer,
|
|
onChange,
|
|
afterVisibleChange,
|
|
showMask,
|
|
maskClosable,
|
|
maskStyle,
|
|
keyboard,
|
|
getOpenCount,
|
|
scrollLocker,
|
|
contentWrapperStyle,
|
|
style,
|
|
...otherProps
|
|
} = props;
|
|
// 首次渲染都将是关闭状态。
|
|
const open = state.dom ? $open : false;
|
|
const wrapperClassName = classnames(prefixCls, {
|
|
[`${prefixCls}-${placement}`]: true,
|
|
[`${prefixCls}-open`]: open,
|
|
'no-mask': !showMask,
|
|
});
|
|
|
|
const { placementName } = getHorizontalBoolAndPlacementName();
|
|
// 百分比与像素动画不同步,第一次打用后全用像素动画。
|
|
// const defaultValue = !this.contentDom || !level ? '100%' : `${value}px`;
|
|
const placementPos = placement === 'left' || placement === 'top' ? '-100%' : '100%';
|
|
const transform = open ? '' : `${placementName}(${placementPos})`;
|
|
|
|
return (
|
|
<div
|
|
{...omit(otherProps, ['switchScrollingEffect', 'autoFocus'])}
|
|
tabindex={-1}
|
|
class={wrapperClassName}
|
|
style={style}
|
|
ref={(c: HTMLElement | null) => {
|
|
state.dom = c as HTMLElement;
|
|
}}
|
|
onKeydown={open && keyboard ? onKeyDown : undefined}
|
|
onTransitionend={onWrapperTransitionEnd}
|
|
>
|
|
{showMask && (
|
|
<div
|
|
class={`${prefixCls}-mask`}
|
|
onClick={maskClosable ? onClose : undefined}
|
|
style={maskStyle}
|
|
ref={c => {
|
|
state.maskDom = c as HTMLElement;
|
|
}}
|
|
/>
|
|
)}
|
|
<div
|
|
class={`${prefixCls}-content-wrapper`}
|
|
style={{
|
|
transform,
|
|
msTransform: transform,
|
|
width: isNumeric(width) ? `${width}px` : width,
|
|
height: isNumeric(height) ? `${height}px` : height,
|
|
...contentWrapperStyle,
|
|
}}
|
|
ref={c => {
|
|
state.contentWrapper = c as HTMLElement;
|
|
}}
|
|
>
|
|
<div
|
|
class={`${prefixCls}-content`}
|
|
ref={c => {
|
|
state.contentDom = c as HTMLElement;
|
|
}}
|
|
>
|
|
{slots.children?.()}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
},
|
|
});
|
|
|
|
export default DrawerChild;
|