refactor: trigger
parent
6479864a6f
commit
aa4e8deabf
|
@ -1,20 +1,27 @@
|
|||
import type { BaseTransitionProps, CSSProperties, Ref } from 'vue';
|
||||
import type {
|
||||
BaseTransitionProps,
|
||||
CSSProperties,
|
||||
Ref,
|
||||
TransitionGroupProps,
|
||||
TransitionProps,
|
||||
} from 'vue';
|
||||
import { onBeforeUpdate } from 'vue';
|
||||
import { getCurrentInstance } from 'vue';
|
||||
import { defineComponent, nextTick, Transition as T, TransitionGroup as TG } from 'vue';
|
||||
|
||||
export const getTransitionProps = (transitionName: string, opt: object = {}) => {
|
||||
export const getTransitionProps = (transitionName: string, opt: TransitionProps = {}) => {
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
return opt;
|
||||
}
|
||||
const transitionProps = transitionName
|
||||
const transitionProps: TransitionProps = transitionName
|
||||
? {
|
||||
appear: true,
|
||||
type: 'animation',
|
||||
// appearFromClass: `${transitionName}-appear ${transitionName}-appear-prepare`,
|
||||
// appearActiveClass: `antdv-base-transtion`,
|
||||
appearToClass: `${transitionName}-appear ${transitionName}-appear-active`,
|
||||
// appearToClass: `${transitionName}-appear ${transitionName}-appear-active`,
|
||||
enterFromClass: `${transitionName}-enter ${transitionName}-enter-prepare`,
|
||||
// enterActiveClass: `antdv-base-transtion`,
|
||||
// enterActiveClass: `${transitionName}-enter ${transitionName}-enter-active`,
|
||||
enterToClass: `${transitionName}-enter ${transitionName}-enter-active`,
|
||||
leaveFromClass: ` ${transitionName}-leave`,
|
||||
leaveActiveClass: `${transitionName}-leave ${transitionName}-leave-active`,
|
||||
|
@ -25,8 +32,8 @@ export const getTransitionProps = (transitionName: string, opt: object = {}) =>
|
|||
return transitionProps;
|
||||
};
|
||||
|
||||
export const getTransitionGroupProps = (transitionName: string, opt: object = {}) => {
|
||||
const transitionProps = transitionName
|
||||
export const getTransitionGroupProps = (transitionName: string, opt: TransitionProps = {}) => {
|
||||
const transitionProps: TransitionGroupProps = transitionName
|
||||
? {
|
||||
appear: true,
|
||||
// appearFromClass: `${transitionName}-appear ${transitionName}-appear-prepare`,
|
||||
|
|
|
@ -8,6 +8,7 @@ import isVisible from '../vc-util/Dom/isVisible';
|
|||
import { isSamePoint, restoreFocus, monitorResize } from './util';
|
||||
import type { AlignType, AlignResult, TargetType, TargetPoint } from './interface';
|
||||
import useBuffer from './hooks/useBuffer';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
|
||||
type OnAlign = (source: HTMLElement, result: AlignResult) => void;
|
||||
|
||||
|
@ -53,11 +54,12 @@ export default defineComponent({
|
|||
props: alignProps,
|
||||
emits: ['align'],
|
||||
setup(props, { expose, slots }) {
|
||||
const cacheRef = ref<{ element?: HTMLElement; point?: TargetPoint }>({});
|
||||
const cacheRef = ref<{ element?: HTMLElement; point?: TargetPoint; align?: AlignType }>({});
|
||||
const nodeRef = ref();
|
||||
const forceAlignPropsRef = computed(() => ({
|
||||
disabled: props.disabled,
|
||||
target: props.target,
|
||||
align: props.align,
|
||||
onAlign: props.onAlign,
|
||||
}));
|
||||
|
||||
|
@ -66,10 +68,11 @@ export default defineComponent({
|
|||
const {
|
||||
disabled: latestDisabled,
|
||||
target: latestTarget,
|
||||
align: latestAlign,
|
||||
onAlign: latestOnAlign,
|
||||
} = forceAlignPropsRef.value;
|
||||
if (!latestDisabled && latestTarget && nodeRef.value && nodeRef.value.$el) {
|
||||
const source = nodeRef.value.$el;
|
||||
if (!latestDisabled && latestTarget && nodeRef.value) {
|
||||
const source = nodeRef.value;
|
||||
|
||||
let result: AlignResult;
|
||||
const element = getElement(latestTarget);
|
||||
|
@ -77,19 +80,32 @@ export default defineComponent({
|
|||
|
||||
cacheRef.value.element = element;
|
||||
cacheRef.value.point = point;
|
||||
|
||||
cacheRef.value.align = latestAlign;
|
||||
// IE lose focus after element realign
|
||||
// We should record activeElement and restore later
|
||||
const { activeElement } = document;
|
||||
|
||||
// const { activeElement } = document;
|
||||
// console.log(
|
||||
// '🌹',
|
||||
// source.style.display,
|
||||
// source.style.left,
|
||||
// source.style.top,
|
||||
// source.className,
|
||||
// );
|
||||
// We only align when element is visible
|
||||
if (element && isVisible(element)) {
|
||||
result = alignElement(source, element, props.align);
|
||||
result = alignElement(source, element, latestAlign);
|
||||
} else if (point) {
|
||||
result = alignPoint(source, point, props.align);
|
||||
result = alignPoint(source, point, latestAlign);
|
||||
}
|
||||
|
||||
restoreFocus(activeElement, source);
|
||||
// console.log(
|
||||
// '😸',
|
||||
// source.style.display,
|
||||
// source.style.left,
|
||||
// source.style.top,
|
||||
// source.className,
|
||||
// );
|
||||
// restoreFocus(activeElement, source);
|
||||
|
||||
if (latestOnAlign && result) {
|
||||
latestOnAlign(source, result);
|
||||
|
@ -118,13 +134,17 @@ export default defineComponent({
|
|||
const element = getElement(target);
|
||||
const point = getPoint(target);
|
||||
|
||||
if (nodeRef.value && nodeRef.value.$el !== sourceResizeMonitor.value.element) {
|
||||
if (nodeRef.value !== sourceResizeMonitor.value.element) {
|
||||
sourceResizeMonitor.value.cancel();
|
||||
sourceResizeMonitor.value.element = nodeRef.value.$el;
|
||||
sourceResizeMonitor.value.cancel = monitorResize(nodeRef.value.$el, forceAlign);
|
||||
sourceResizeMonitor.value.element = nodeRef.value;
|
||||
sourceResizeMonitor.value.cancel = monitorResize(nodeRef.value, forceAlign);
|
||||
}
|
||||
|
||||
if (cacheRef.value.element !== element || !isSamePoint(cacheRef.value.point, point)) {
|
||||
if (
|
||||
cacheRef.value.element !== element ||
|
||||
!isSamePoint(cacheRef.value.point, point) ||
|
||||
!isEqual(cacheRef.value.align, props.align)
|
||||
) {
|
||||
forceAlign();
|
||||
|
||||
// Add resize observer
|
||||
|
|
|
@ -0,0 +1,196 @@
|
|||
import type { PropType } from 'vue';
|
||||
import { defineComponent, ref, computed, onMounted, onUpdated, watch, onUnmounted } from 'vue';
|
||||
import { alignElement, alignPoint } from 'dom-align';
|
||||
import addEventListener from '../vc-util/Dom/addEventListener';
|
||||
import { cloneElement } from '../_util/vnode';
|
||||
import isVisible from '../vc-util/Dom/isVisible';
|
||||
|
||||
import { isSamePoint, restoreFocus, monitorResize } from './util';
|
||||
import type { AlignType, AlignResult, TargetType, TargetPoint } from './interface';
|
||||
import useBuffer from './hooks/useBuffer';
|
||||
|
||||
type OnAlign = (source: HTMLElement, result: AlignResult) => void;
|
||||
|
||||
export interface AlignProps {
|
||||
align: AlignType;
|
||||
target: TargetType;
|
||||
onAlign?: OnAlign;
|
||||
monitorBufferTime?: number;
|
||||
monitorWindowResize?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const alignProps = {
|
||||
align: Object as PropType<AlignType>,
|
||||
target: [Object, Function] as PropType<TargetType>,
|
||||
onAlign: Function as PropType<OnAlign>,
|
||||
monitorBufferTime: Number,
|
||||
monitorWindowResize: Boolean,
|
||||
disabled: Boolean,
|
||||
};
|
||||
|
||||
interface MonitorRef {
|
||||
element?: HTMLElement;
|
||||
cancel: () => void;
|
||||
}
|
||||
|
||||
export interface RefAlign {
|
||||
forceAlign: () => void;
|
||||
}
|
||||
|
||||
function getElement(func: TargetType) {
|
||||
if (typeof func !== 'function') return null;
|
||||
return func();
|
||||
}
|
||||
|
||||
function getPoint(point: TargetType) {
|
||||
if (typeof point !== 'object' || !point) return null;
|
||||
return point;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Align',
|
||||
props: alignProps,
|
||||
emits: ['align'],
|
||||
setup(props, { expose, slots }) {
|
||||
const cacheRef = ref<{ element?: HTMLElement; point?: TargetPoint }>({});
|
||||
const nodeRef = ref();
|
||||
const forceAlignPropsRef = computed(() => ({
|
||||
disabled: props.disabled,
|
||||
target: props.target,
|
||||
onAlign: props.onAlign,
|
||||
}));
|
||||
|
||||
const [forceAlign, cancelForceAlign] = useBuffer(
|
||||
() => {
|
||||
const {
|
||||
disabled: latestDisabled,
|
||||
target: latestTarget,
|
||||
onAlign: latestOnAlign,
|
||||
} = forceAlignPropsRef.value;
|
||||
if (!latestDisabled && latestTarget && nodeRef.value && nodeRef.value.$el) {
|
||||
const source = nodeRef.value.$el;
|
||||
|
||||
let result: AlignResult;
|
||||
const element = getElement(latestTarget);
|
||||
const point = getPoint(latestTarget);
|
||||
|
||||
cacheRef.value.element = element;
|
||||
cacheRef.value.point = point;
|
||||
|
||||
// IE lose focus after element realign
|
||||
// We should record activeElement and restore later
|
||||
const { activeElement } = document;
|
||||
|
||||
// We only align when element is visible
|
||||
if (element && isVisible(element)) {
|
||||
result = alignElement(source, element, props.align);
|
||||
} else if (point) {
|
||||
result = alignPoint(source, point, props.align);
|
||||
}
|
||||
|
||||
restoreFocus(activeElement, source);
|
||||
|
||||
if (latestOnAlign && result) {
|
||||
latestOnAlign(source, result);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
computed(() => props.monitorBufferTime),
|
||||
);
|
||||
|
||||
// ===================== Effect =====================
|
||||
// Listen for target updated
|
||||
const resizeMonitor = ref<MonitorRef>({
|
||||
cancel: () => {},
|
||||
});
|
||||
// Listen for source updated
|
||||
const sourceResizeMonitor = ref<MonitorRef>({
|
||||
cancel: () => {},
|
||||
});
|
||||
|
||||
const goAlign = () => {
|
||||
const target = props.target;
|
||||
const element = getElement(target);
|
||||
const point = getPoint(target);
|
||||
|
||||
if (nodeRef.value && nodeRef.value.$el !== sourceResizeMonitor.value.element) {
|
||||
sourceResizeMonitor.value.cancel();
|
||||
sourceResizeMonitor.value.element = nodeRef.value.$el;
|
||||
sourceResizeMonitor.value.cancel = monitorResize(nodeRef.value.$el, forceAlign);
|
||||
}
|
||||
|
||||
if (cacheRef.value.element !== element || !isSamePoint(cacheRef.value.point, point)) {
|
||||
forceAlign();
|
||||
|
||||
// Add resize observer
|
||||
if (resizeMonitor.value.element !== element) {
|
||||
resizeMonitor.value.cancel();
|
||||
resizeMonitor.value.element = element;
|
||||
resizeMonitor.value.cancel = monitorResize(element, forceAlign);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
goAlign();
|
||||
});
|
||||
|
||||
onUpdated(() => {
|
||||
goAlign();
|
||||
});
|
||||
|
||||
// Listen for disabled change
|
||||
watch(
|
||||
() => props.disabled,
|
||||
disabled => {
|
||||
if (!disabled) {
|
||||
forceAlign();
|
||||
} else {
|
||||
cancelForceAlign();
|
||||
}
|
||||
},
|
||||
{ flush: 'post' },
|
||||
);
|
||||
|
||||
// Listen for window resize
|
||||
const winResizeRef = ref<{ remove: Function }>(null);
|
||||
|
||||
watch(
|
||||
() => props.monitorWindowResize,
|
||||
monitorWindowResize => {
|
||||
if (monitorWindowResize) {
|
||||
if (!winResizeRef.value) {
|
||||
winResizeRef.value = addEventListener(window, 'resize', forceAlign);
|
||||
}
|
||||
} else if (winResizeRef.value) {
|
||||
winResizeRef.value.remove();
|
||||
winResizeRef.value = null;
|
||||
}
|
||||
},
|
||||
{ flush: 'post' },
|
||||
);
|
||||
onUnmounted(() => {
|
||||
resizeMonitor.value.cancel();
|
||||
sourceResizeMonitor.value.cancel();
|
||||
if (winResizeRef.value) winResizeRef.value.remove();
|
||||
cancelForceAlign();
|
||||
});
|
||||
|
||||
expose({
|
||||
forceAlign: () => forceAlign(true),
|
||||
});
|
||||
|
||||
return () => {
|
||||
const child = slots?.default();
|
||||
if (child) {
|
||||
return cloneElement(child[0], { ref: nodeRef }, true, true);
|
||||
}
|
||||
return child && child[0];
|
||||
};
|
||||
},
|
||||
});
|
|
@ -0,0 +1,39 @@
|
|||
import { Transition } from 'vue';
|
||||
import type { TransitionNameType, AnimationType } from '../interface';
|
||||
import { getMotion } from '../utils/motionUtil';
|
||||
|
||||
export interface MaskProps {
|
||||
prefixCls: string;
|
||||
visible?: boolean;
|
||||
zIndex?: number;
|
||||
mask?: boolean;
|
||||
maskAnimation?: AnimationType;
|
||||
maskTransitionName?: TransitionNameType;
|
||||
}
|
||||
|
||||
export default function Mask(props: MaskProps) {
|
||||
const { prefixCls, visible, zIndex, mask, maskAnimation, maskTransitionName } = props;
|
||||
|
||||
if (!mask) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let motion = {};
|
||||
|
||||
if (maskTransitionName || maskAnimation) {
|
||||
motion = {
|
||||
...getMotion({
|
||||
prefixCls,
|
||||
transitionName: maskTransitionName,
|
||||
animation: maskAnimation,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<Transition appear {...motion}>
|
||||
<div v-if={visible} style={{ zIndex }} class={`${prefixCls}-mask`} />
|
||||
</Transition>
|
||||
);
|
||||
}
|
||||
Mask.displayName = 'Mask';
|
|
@ -0,0 +1,56 @@
|
|||
import { CSSProperties, defineComponent, ref, Transition } from 'vue';
|
||||
import { flattenChildren } from 'ant-design-vue/es/_util/props-util';
|
||||
import classNames from 'ant-design-vue/es/_util/classNames';
|
||||
import { MobilePopupProps, mobileProps } from './interface';
|
||||
|
||||
export default defineComponent({
|
||||
props: mobileProps,
|
||||
emits: ['mouseenter', 'mouseleave', 'mousedown', 'touchstart', 'align'],
|
||||
inheritAttrs: false,
|
||||
name: 'MobilePopupInner',
|
||||
setup(props, { expose, slots }) {
|
||||
const elementRef = ref<HTMLDivElement>();
|
||||
|
||||
expose({
|
||||
forceAlign: () => {},
|
||||
getElement: () => elementRef.value,
|
||||
});
|
||||
|
||||
return () => {
|
||||
const {
|
||||
zIndex,
|
||||
visible,
|
||||
prefixCls,
|
||||
mobile: { popupClassName, popupStyle, popupMotion = {}, popupRender } = {},
|
||||
} = props as MobilePopupProps;
|
||||
// ======================== Render ========================
|
||||
const mergedStyle: CSSProperties = {
|
||||
zIndex,
|
||||
...popupStyle,
|
||||
};
|
||||
|
||||
let childNode: any = flattenChildren(slots.default?.());
|
||||
|
||||
// Wrapper when multiple children
|
||||
if (childNode.length > 1) {
|
||||
childNode = <div class={`${prefixCls}-content`}>{childNode}</div>;
|
||||
}
|
||||
|
||||
// Mobile support additional render
|
||||
if (popupRender) {
|
||||
childNode = popupRender(childNode);
|
||||
}
|
||||
|
||||
const mergedClassName = classNames(prefixCls, popupClassName);
|
||||
return (
|
||||
<Transition ref={elementRef} {...popupMotion}>
|
||||
{visible ? (
|
||||
<div class={mergedClassName} style={mergedStyle}>
|
||||
{childNode}
|
||||
</div>
|
||||
) : null}
|
||||
</Transition>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
|
@ -0,0 +1,187 @@
|
|||
import type { AlignType } from '../interface';
|
||||
import useVisibleStatus from './useVisibleStatus';
|
||||
import useStretchStyle from './useStretchStyle';
|
||||
import {
|
||||
computed,
|
||||
CSSProperties,
|
||||
defineComponent,
|
||||
nextTick,
|
||||
ref,
|
||||
toRef,
|
||||
Transition,
|
||||
watch,
|
||||
withModifiers,
|
||||
} from 'vue';
|
||||
import Align, { RefAlign } from 'ant-design-vue/es/vc-align/Align';
|
||||
import { getMotion } from '../utils/motionUtil';
|
||||
import { flattenChildren } from 'ant-design-vue/es/_util/props-util';
|
||||
import classNames from 'ant-design-vue/es/_util/classNames';
|
||||
import { innerProps, PopupInnerProps } from './interface';
|
||||
import { getTransitionProps } from 'ant-design-vue/es/_util/transition';
|
||||
import supportsPassive from 'ant-design-vue/es/_util/supportsPassive';
|
||||
|
||||
export default defineComponent({
|
||||
props: innerProps,
|
||||
name: 'PopupInner',
|
||||
emits: ['mouseenter', 'mouseleave', 'mousedown', 'touchstart', 'align'],
|
||||
inheritAttrs: false,
|
||||
setup(props, { expose, attrs, slots }) {
|
||||
const alignRef = ref<RefAlign>();
|
||||
const elementRef = ref<HTMLDivElement>();
|
||||
const alignedClassName = ref<string>();
|
||||
// ======================= Measure ========================
|
||||
const [stretchStyle, measureStretchStyle] = useStretchStyle(toRef(props, 'stretch'));
|
||||
|
||||
const doMeasure = () => {
|
||||
if (props.stretch) {
|
||||
measureStretchStyle(props.getRootDomNode());
|
||||
}
|
||||
};
|
||||
|
||||
// ======================== Status ========================
|
||||
const [status, goNextStatus] = useVisibleStatus(toRef(props, 'visible'), doMeasure);
|
||||
|
||||
// ======================== Aligns ========================
|
||||
const prepareResolveRef = ref<(value?: unknown) => void>();
|
||||
|
||||
// `target` on `rc-align` can accept as a function to get the bind element or a point.
|
||||
// ref: https://www.npmjs.com/package/rc-align
|
||||
const getAlignTarget = () => {
|
||||
if (props.point) {
|
||||
return props.point;
|
||||
}
|
||||
return props.getRootDomNode;
|
||||
};
|
||||
|
||||
const forceAlign = () => {
|
||||
alignRef.value?.forceAlign();
|
||||
};
|
||||
|
||||
const onInternalAlign = (popupDomNode: HTMLElement, matchAlign: AlignType) => {
|
||||
const nextAlignedClassName = props.getClassNameFromAlign(matchAlign);
|
||||
if (alignedClassName.value !== nextAlignedClassName) {
|
||||
nextTick(() => {
|
||||
alignedClassName.value = nextAlignedClassName;
|
||||
});
|
||||
}
|
||||
if (status.value === 'align') {
|
||||
// Repeat until not more align needed
|
||||
if (alignedClassName.value !== nextAlignedClassName) {
|
||||
Promise.resolve().then(() => {
|
||||
forceAlign();
|
||||
});
|
||||
} else {
|
||||
goNextStatus(() => {
|
||||
prepareResolveRef.value?.();
|
||||
});
|
||||
}
|
||||
|
||||
props.onAlign?.(popupDomNode, matchAlign);
|
||||
}
|
||||
};
|
||||
|
||||
// ======================== Motion ========================
|
||||
const motion = computed(() => {
|
||||
const m = { ...getMotion(props) };
|
||||
['onAfterEnter', 'onAfterLeave'].forEach(eventName => {
|
||||
m[eventName] = () => {
|
||||
goNextStatus();
|
||||
};
|
||||
});
|
||||
return m;
|
||||
});
|
||||
|
||||
const onShowPrepare = () => {
|
||||
return new Promise(resolve => {
|
||||
prepareResolveRef.value = resolve;
|
||||
});
|
||||
};
|
||||
|
||||
watch(
|
||||
[toRef(motion.value, 'name'), status],
|
||||
() => {
|
||||
if (!motion.value.name && status.value === 'motion') {
|
||||
goNextStatus();
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
expose({
|
||||
forceAlign,
|
||||
getElement: () => elementRef.value,
|
||||
});
|
||||
return () => {
|
||||
const {
|
||||
zIndex,
|
||||
visible,
|
||||
align,
|
||||
prefixCls,
|
||||
destroyPopupOnHide,
|
||||
onMouseenter,
|
||||
onMouseleave,
|
||||
onTouchstart,
|
||||
onMousedown,
|
||||
} = props as PopupInnerProps;
|
||||
const statusValue = status.value;
|
||||
// ======================== Render ========================
|
||||
const mergedStyle: CSSProperties = {
|
||||
...stretchStyle.value,
|
||||
zIndex,
|
||||
opacity: statusValue === 'motion' || statusValue === 'stable' || !visible ? undefined : 0,
|
||||
pointerEvents: statusValue === 'stable' ? undefined : 'none',
|
||||
...(attrs.style as object),
|
||||
};
|
||||
|
||||
// Align statusValue
|
||||
let alignDisabled = true;
|
||||
if (align?.points && (statusValue === 'align' || statusValue === 'stable')) {
|
||||
alignDisabled = false;
|
||||
}
|
||||
|
||||
let childNode: any = flattenChildren(slots.default?.());
|
||||
|
||||
// Wrapper when multiple children
|
||||
if (childNode.length > 1) {
|
||||
childNode = <div class={`${prefixCls}-content`}>{childNode}</div>;
|
||||
}
|
||||
const mergedClassName = classNames(prefixCls, attrs.class, alignedClassName.value);
|
||||
const transitionProps = getTransitionProps(motion.value.name, motion.value);
|
||||
return (
|
||||
<Transition
|
||||
ref={elementRef}
|
||||
{...transitionProps}
|
||||
onBeforeAppear={onShowPrepare}
|
||||
onBeforeEnter={onShowPrepare}
|
||||
>
|
||||
{!destroyPopupOnHide ? (
|
||||
<Align
|
||||
v-show={visible}
|
||||
target={getAlignTarget()}
|
||||
key="popup"
|
||||
ref={alignRef}
|
||||
monitorWindowResize
|
||||
disabled={alignDisabled}
|
||||
align={align}
|
||||
onAlign={onInternalAlign}
|
||||
>
|
||||
<div
|
||||
class={mergedClassName}
|
||||
onMouseenter={onMouseenter}
|
||||
onMouseleave={onMouseleave}
|
||||
onMousedown={withModifiers(onMousedown, ['capture'])}
|
||||
onTouchstart={withModifiers(
|
||||
onTouchstart,
|
||||
supportsPassive ? ['capture', 'passive'] : ['capture'],
|
||||
)}
|
||||
style={mergedStyle}
|
||||
>
|
||||
{childNode}
|
||||
</div>
|
||||
</Align>
|
||||
) : null}
|
||||
</Transition>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
|
@ -0,0 +1,46 @@
|
|||
import { defineComponent, ref, watch } from 'vue';
|
||||
import { popupProps } from './interface';
|
||||
import Mask from './Mask';
|
||||
import MobilePopupInner from './MobilePopupInner';
|
||||
import PopupInner from './PopupInner';
|
||||
|
||||
export default defineComponent({
|
||||
props: popupProps,
|
||||
inheritAttrs: false,
|
||||
name: 'Popup',
|
||||
setup(props, { attrs, slots }) {
|
||||
const innerVisible = ref(false);
|
||||
const inMobile = ref(false);
|
||||
const popupRef = ref();
|
||||
watch(
|
||||
[() => props.visible, () => props.mobile],
|
||||
() => {
|
||||
innerVisible.value = props.visible;
|
||||
if (props.visible && props.mobile) {
|
||||
inMobile.value = true;
|
||||
}
|
||||
},
|
||||
{ immediate: true, flush: 'post' },
|
||||
);
|
||||
return () => {
|
||||
const cloneProps = { ...props, ...attrs, visible: innerVisible.value };
|
||||
const popupNode = inMobile.value ? (
|
||||
<MobilePopupInner
|
||||
{...cloneProps}
|
||||
mobile={props.mobile}
|
||||
ref={popupRef}
|
||||
v-slots={{ default: slots.default }}
|
||||
></MobilePopupInner>
|
||||
) : (
|
||||
<PopupInner {...cloneProps} ref={popupRef} v-slots={{ default: slots.default }} />
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Mask {...cloneProps} />
|
||||
{popupNode}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
|
@ -0,0 +1,55 @@
|
|||
import type { Point, AlignType, StretchType, MobileConfig } from '../interface';
|
||||
import type { ExtractPropTypes, PropType } from 'vue';
|
||||
|
||||
export const innerProps = {
|
||||
visible: Boolean,
|
||||
|
||||
prefixCls: String,
|
||||
zIndex: Number,
|
||||
|
||||
destroyPopupOnHide: Boolean,
|
||||
forceRender: Boolean,
|
||||
|
||||
// Legacy Motion
|
||||
animation: String,
|
||||
transitionName: String,
|
||||
|
||||
// Measure
|
||||
stretch: { type: String as PropType<StretchType> },
|
||||
|
||||
// Align
|
||||
align: { type: Object as PropType<AlignType> },
|
||||
point: { type: Object as PropType<Point> },
|
||||
getRootDomNode: { type: Function as PropType<() => HTMLElement> },
|
||||
getClassNameFromAlign: { type: Function as PropType<(align: AlignType) => string> },
|
||||
onMouseenter: { type: Function as PropType<(align: MouseEvent) => void> },
|
||||
onMouseleave: { type: Function as PropType<(align: MouseEvent) => void> },
|
||||
onMousedown: { type: Function as PropType<(align: MouseEvent) => void> },
|
||||
onTouchstart: { type: Function as PropType<(align: MouseEvent) => void> },
|
||||
};
|
||||
export type PopupInnerProps = Partial<ExtractPropTypes<typeof innerProps>> & {
|
||||
align?: AlignType;
|
||||
};
|
||||
|
||||
export const mobileProps = {
|
||||
...innerProps,
|
||||
mobile: { type: Object as PropType<MobileConfig> },
|
||||
};
|
||||
|
||||
export type MobilePopupProps = Partial<ExtractPropTypes<typeof mobileProps>> & {
|
||||
align?: AlignType;
|
||||
mobile: MobileConfig;
|
||||
};
|
||||
|
||||
export const popupProps = {
|
||||
...innerProps,
|
||||
mask: Boolean,
|
||||
mobile: { type: Object as PropType<MobileConfig> },
|
||||
maskAnimation: String,
|
||||
maskTransitionName: String,
|
||||
};
|
||||
|
||||
export type PopupProps = Partial<ExtractPropTypes<typeof popupProps>> & {
|
||||
align?: AlignType;
|
||||
mobile: MobileConfig;
|
||||
};
|
|
@ -0,0 +1,41 @@
|
|||
import type { ComputedRef, CSSProperties, Ref } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import type { StretchType } from '../interface';
|
||||
|
||||
export default (
|
||||
stretch?: Ref<StretchType>,
|
||||
): [ComputedRef<CSSProperties>, (element: HTMLElement) => void] => {
|
||||
const targetSize = ref({ width: 0, height: 0 });
|
||||
|
||||
function measureStretch(element: HTMLElement) {
|
||||
targetSize.value = {
|
||||
width: element.offsetWidth,
|
||||
height: element.offsetHeight,
|
||||
};
|
||||
}
|
||||
|
||||
// Merge stretch style
|
||||
const style = computed(() => {
|
||||
const sizeStyle: CSSProperties = {};
|
||||
|
||||
if (stretch.value) {
|
||||
const { width, height } = targetSize.value;
|
||||
|
||||
// Stretch with target
|
||||
if (stretch.value.indexOf('height') !== -1 && height) {
|
||||
sizeStyle.height = `${height}px`;
|
||||
} else if (stretch.value.indexOf('minHeight') !== -1 && height) {
|
||||
sizeStyle.minHeight = `${height}px`;
|
||||
}
|
||||
if (stretch.value.indexOf('width') !== -1 && width) {
|
||||
sizeStyle.width = `${width}px`;
|
||||
} else if (stretch.value.indexOf('minWidth') !== -1 && width) {
|
||||
sizeStyle.minWidth = `${width}px`;
|
||||
}
|
||||
}
|
||||
|
||||
return sizeStyle;
|
||||
});
|
||||
|
||||
return [style, measureStretch];
|
||||
};
|
|
@ -0,0 +1,96 @@
|
|||
import type { Ref } from 'vue';
|
||||
import { onBeforeUnmount } from 'vue';
|
||||
import { ref, watch } from 'vue';
|
||||
import raf from '../../_util/raf';
|
||||
|
||||
/**
|
||||
* Popup should follow the steps for each component work correctly:
|
||||
* measure - check for the value stretch size
|
||||
* align - let component align the position
|
||||
* aligned - re-align again in case additional className changed the size
|
||||
* afterAlign - choice next step is trigger motion or finished
|
||||
* beforeMotion - should reset motion to invisible so that CSSMotion can do normal motion
|
||||
* motion - play the motion
|
||||
* stable - everything is done
|
||||
*/
|
||||
type PopupStatus = null | 'measure' | 'align' | 'aligned' | 'motion' | 'stable';
|
||||
|
||||
type Func = () => void;
|
||||
|
||||
const StatusQueue: PopupStatus[] = ['measure', 'align', null, 'motion'];
|
||||
|
||||
export default (
|
||||
visible: Ref<boolean>,
|
||||
doMeasure: Func,
|
||||
): [Ref<PopupStatus>, (callback?: () => void) => void] => {
|
||||
const status = ref<PopupStatus>(null);
|
||||
const rafRef = ref<number>();
|
||||
const destroyRef = ref(false);
|
||||
|
||||
function setStatus(nextStatus: PopupStatus) {
|
||||
if (!destroyRef.value) {
|
||||
status.value = nextStatus;
|
||||
}
|
||||
}
|
||||
|
||||
function cancelRaf() {
|
||||
raf.cancel(rafRef.value);
|
||||
}
|
||||
|
||||
function goNextStatus(callback?: () => void) {
|
||||
cancelRaf();
|
||||
rafRef.value = raf(() => {
|
||||
// Only align should be manually trigger
|
||||
let newStatus = status.value;
|
||||
switch (status.value) {
|
||||
case 'align':
|
||||
newStatus = 'motion';
|
||||
break;
|
||||
case 'motion':
|
||||
newStatus = 'stable';
|
||||
break;
|
||||
default:
|
||||
}
|
||||
setStatus(newStatus);
|
||||
|
||||
callback?.();
|
||||
});
|
||||
}
|
||||
|
||||
watch(
|
||||
visible,
|
||||
() => {
|
||||
status.value = 'measure';
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
// Go next status
|
||||
watch(
|
||||
status,
|
||||
() => {
|
||||
switch (status.value) {
|
||||
case 'measure':
|
||||
doMeasure();
|
||||
break;
|
||||
default:
|
||||
}
|
||||
|
||||
if (status.value) {
|
||||
rafRef.value = raf(async () => {
|
||||
const index = StatusQueue.indexOf(status.value);
|
||||
const nextStatus = StatusQueue[index + 1];
|
||||
if (nextStatus && index !== -1) {
|
||||
setStatus(nextStatus);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
onBeforeUnmount(() => {
|
||||
destroyRef.value = true;
|
||||
cancelRaf();
|
||||
});
|
||||
|
||||
return [status, goNextStatus];
|
||||
};
|
|
@ -1,6 +1,7 @@
|
|||
import { defineComponent, inject, provide } from 'vue';
|
||||
import { computed, defineComponent, inject, provide, ref } from 'vue';
|
||||
import PropTypes from '../_util/vue-types';
|
||||
import contains from '../vc-util/Dom/contains';
|
||||
import raf from '../_util/raf';
|
||||
import {
|
||||
hasProp,
|
||||
getComponent,
|
||||
|
@ -24,7 +25,10 @@ function returnEmptyString() {
|
|||
return '';
|
||||
}
|
||||
|
||||
function returnDocument() {
|
||||
function returnDocument(element) {
|
||||
if (element) {
|
||||
return element.ownerDocument;
|
||||
}
|
||||
return window.document;
|
||||
}
|
||||
const ALL_HANDLERS = [
|
||||
|
@ -76,18 +80,29 @@ export default defineComponent({
|
|||
maskAnimation: PropTypes.string,
|
||||
stretch: PropTypes.string,
|
||||
alignPoint: PropTypes.looseBool, // Maybe we can support user pass position in the future
|
||||
autoDestroy: PropTypes.looseBool.def(false),
|
||||
mobile: Object,
|
||||
},
|
||||
setup() {
|
||||
setup(props) {
|
||||
const align = computed(() => {
|
||||
const { popupPlacement, popupAlign, builtinPlacements } = props;
|
||||
if (popupPlacement && builtinPlacements) {
|
||||
return getAlignFromPlacement(builtinPlacements, popupPlacement, popupAlign);
|
||||
}
|
||||
return popupAlign;
|
||||
});
|
||||
return {
|
||||
vcTriggerContext: inject('vcTriggerContext', {}),
|
||||
savePopupRef: inject('savePopupRef', noop),
|
||||
dialogContext: inject('dialogContext', null),
|
||||
popupRef: ref(null),
|
||||
triggerRef: ref(null),
|
||||
align,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
const props = this.$props;
|
||||
let popupVisible;
|
||||
if (hasProp(this, 'popupVisible')) {
|
||||
if (this.popupVisible !== undefined) {
|
||||
popupVisible = !!props.popupVisible;
|
||||
} else {
|
||||
popupVisible = !!props.defaultPopupVisible;
|
||||
|
@ -97,12 +112,12 @@ export default defineComponent({
|
|||
this.fireEvents(h, e);
|
||||
};
|
||||
});
|
||||
this._component = null;
|
||||
this.focusTime = null;
|
||||
this.clickOutsideHandler = null;
|
||||
this.contextmenuOutsideHandler1 = null;
|
||||
this.contextmenuOutsideHandler2 = null;
|
||||
this.touchOutsideHandler = null;
|
||||
this.attachId = null;
|
||||
return {
|
||||
prevPopupVisible: popupVisible,
|
||||
sPopupVisible: popupVisible,
|
||||
|
@ -139,6 +154,7 @@ export default defineComponent({
|
|||
this.clearDelayTimer();
|
||||
this.clearOutsideHandler();
|
||||
clearTimeout(this.mouseDownTimeout);
|
||||
raf.cancel(this.attachId);
|
||||
},
|
||||
methods: {
|
||||
updatedCal() {
|
||||
|
@ -152,7 +168,7 @@ export default defineComponent({
|
|||
if (state.sPopupVisible) {
|
||||
let currentDocument;
|
||||
if (!this.clickOutsideHandler && (this.isClickToHide() || this.isContextmenuToShow())) {
|
||||
currentDocument = props.getDocument();
|
||||
currentDocument = props.getDocument(this.getRootDomNode());
|
||||
this.clickOutsideHandler = addEventListener(
|
||||
currentDocument,
|
||||
'mousedown',
|
||||
|
@ -161,7 +177,7 @@ export default defineComponent({
|
|||
}
|
||||
// always hide on mobile
|
||||
if (!this.touchOutsideHandler) {
|
||||
currentDocument = currentDocument || props.getDocument();
|
||||
currentDocument = currentDocument || props.getDocument(this.getRootDomNode());
|
||||
this.touchOutsideHandler = addEventListener(
|
||||
currentDocument,
|
||||
'touchstart',
|
||||
|
@ -171,7 +187,7 @@ export default defineComponent({
|
|||
}
|
||||
// close popup when trigger type contains 'onContextmenu' and document is scrolling.
|
||||
if (!this.contextmenuOutsideHandler1 && this.isContextmenuToShow()) {
|
||||
currentDocument = currentDocument || props.getDocument();
|
||||
currentDocument = currentDocument || props.getDocument(this.getRootDomNode());
|
||||
this.contextmenuOutsideHandler1 = addEventListener(
|
||||
currentDocument,
|
||||
'scroll',
|
||||
|
@ -215,9 +231,7 @@ export default defineComponent({
|
|||
e &&
|
||||
e.relatedTarget &&
|
||||
!e.relatedTarget.setTimeout &&
|
||||
this._component &&
|
||||
this._component.getPopupDomNode &&
|
||||
contains(this._component.getPopupDomNode(), e.relatedTarget)
|
||||
contains(this.popupRef?.getPopupDomNode(), e.relatedTarget)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
@ -323,19 +337,37 @@ export default defineComponent({
|
|||
return;
|
||||
}
|
||||
const target = event.target;
|
||||
const root = findDOMNode(this);
|
||||
if (!contains(root, target) && !this.hasPopupMouseDown) {
|
||||
const root = this.getRootDomNode();
|
||||
const popupNode = this.getPopupDomNode();
|
||||
if (
|
||||
// mousedown on the target should also close popup when action is contextMenu.
|
||||
// https://github.com/ant-design/ant-design/issues/29853
|
||||
(!contains(root, target) || this.isContextMenuOnly()) &&
|
||||
!contains(popupNode, target) &&
|
||||
!this.hasPopupMouseDown
|
||||
) {
|
||||
this.close();
|
||||
}
|
||||
},
|
||||
getPopupDomNode() {
|
||||
if (this._component && this._component.getPopupDomNode) {
|
||||
return this._component.getPopupDomNode();
|
||||
}
|
||||
return null;
|
||||
// for test
|
||||
return this.popupRef?.getElement() || null;
|
||||
},
|
||||
|
||||
getRootDomNode() {
|
||||
const { getTriggerDOMNode } = this.$props;
|
||||
if (getTriggerDOMNode) {
|
||||
return getTriggerDOMNode(this.triggerRef);
|
||||
}
|
||||
|
||||
try {
|
||||
const domNode = findDOMNode(this.triggerRef);
|
||||
if (domNode) {
|
||||
return domNode;
|
||||
}
|
||||
} catch (err) {
|
||||
// Do nothing
|
||||
}
|
||||
return findDOMNode(this);
|
||||
},
|
||||
|
||||
|
@ -366,10 +398,6 @@ export default defineComponent({
|
|||
}
|
||||
return popupAlign;
|
||||
},
|
||||
savePopup(node) {
|
||||
this._component = node;
|
||||
this.savePopupRef(node);
|
||||
},
|
||||
getComponent() {
|
||||
const self = this;
|
||||
const mouseProps = {};
|
||||
|
@ -386,7 +414,6 @@ export default defineComponent({
|
|||
prefixCls,
|
||||
destroyPopupOnHide,
|
||||
popupClassName,
|
||||
action,
|
||||
popupAnimation,
|
||||
popupTransitionName,
|
||||
popupStyle,
|
||||
|
@ -396,16 +423,16 @@ export default defineComponent({
|
|||
zIndex,
|
||||
stretch,
|
||||
alignPoint,
|
||||
mobile,
|
||||
forceRender,
|
||||
} = self.$props;
|
||||
const { sPopupVisible, point } = this.$data;
|
||||
const align = this.getPopupAlign();
|
||||
const popupProps = {
|
||||
prefixCls,
|
||||
destroyPopupOnHide,
|
||||
visible: sPopupVisible,
|
||||
point: alignPoint ? point : null,
|
||||
action,
|
||||
align,
|
||||
align: this.align,
|
||||
animation: popupAnimation,
|
||||
getClassNameFromAlign: handleGetPopupClassFromAlign,
|
||||
stretch,
|
||||
|
@ -417,28 +444,53 @@ export default defineComponent({
|
|||
maskTransitionName,
|
||||
getContainer,
|
||||
popupClassName,
|
||||
popupStyle,
|
||||
style: popupStyle,
|
||||
onAlign: $attrs.onPopupAlign || noop,
|
||||
...mouseProps,
|
||||
ref: this.savePopup,
|
||||
ref: 'popupRef',
|
||||
mobile,
|
||||
forceRender,
|
||||
};
|
||||
return <Popup {...popupProps}>{getComponent(self, 'popup')}</Popup>;
|
||||
},
|
||||
|
||||
attachParent(popupContainer) {
|
||||
raf.cancel(this.attachId);
|
||||
|
||||
const { getPopupContainer, getDocument } = this.$props;
|
||||
const domNode = this.getRootDomNode();
|
||||
|
||||
let mountNode;
|
||||
if (!getPopupContainer) {
|
||||
mountNode = getDocument(this.getRootDomNode()).body;
|
||||
} else if (domNode || getPopupContainer.length === 0) {
|
||||
// Compatible for legacy getPopupContainer with domNode argument.
|
||||
// If no need `domNode` argument, will call directly.
|
||||
// https://codesandbox.io/s/eloquent-mclean-ss93m?file=/src/App.js
|
||||
mountNode = getPopupContainer(domNode);
|
||||
}
|
||||
|
||||
if (mountNode) {
|
||||
mountNode.appendChild(popupContainer);
|
||||
} else {
|
||||
// Retry after frame render in case parent not ready
|
||||
this.attachId = raf(() => {
|
||||
this.attachParent(popupContainer);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
getContainer() {
|
||||
const { $props: props, dialogContext } = this;
|
||||
const popupContainer = document.createElement('div');
|
||||
const { $props: props } = this;
|
||||
const { getDocument } = props;
|
||||
const popupContainer = getDocument(this.getRootDomNode()).createElement('div');
|
||||
// Make sure default popup container will never cause scrollbar appearing
|
||||
// https://github.com/react-component/trigger/issues/41
|
||||
popupContainer.style.position = 'absolute';
|
||||
popupContainer.style.top = '0';
|
||||
popupContainer.style.left = '0';
|
||||
popupContainer.style.width = '100%';
|
||||
const mountNode = props.getPopupContainer
|
||||
? props.getPopupContainer(findDOMNode(this), dialogContext)
|
||||
: props.getDocument().body;
|
||||
mountNode.appendChild(popupContainer);
|
||||
this.popupContainer = popupContainer;
|
||||
this.attachParent(popupContainer);
|
||||
return popupContainer;
|
||||
},
|
||||
|
||||
|
@ -455,7 +507,7 @@ export default defineComponent({
|
|||
onPopupVisibleChange && onPopupVisibleChange(sPopupVisible);
|
||||
}
|
||||
// Always record the point position since mouseEnterDelay will delay the show
|
||||
if (alignPoint && event) {
|
||||
if (alignPoint && event && sPopupVisible) {
|
||||
this.setPoint(event);
|
||||
}
|
||||
},
|
||||
|
@ -534,6 +586,11 @@ export default defineComponent({
|
|||
return action.indexOf('click') !== -1 || showAction.indexOf('click') !== -1;
|
||||
},
|
||||
|
||||
isContextMenuOnly() {
|
||||
const { action } = this.$props;
|
||||
return action === 'contextmenu' || (action.length === 1 && action[0] === 'contextmenu');
|
||||
},
|
||||
|
||||
isContextmenuToShow() {
|
||||
const { action, showAction } = this.$props;
|
||||
return action.indexOf('contextmenu') !== -1 || showAction.indexOf('contextmenu') !== -1;
|
||||
|
@ -564,8 +621,8 @@ export default defineComponent({
|
|||
return action.indexOf('focus') !== -1 || hideAction.indexOf('blur') !== -1;
|
||||
},
|
||||
forcePopupAlign() {
|
||||
if (this.$data.sPopupVisible && this._component && this._component.alignInstance) {
|
||||
this._component.alignInstance.forceAlign();
|
||||
if (this.$data.sPopupVisible) {
|
||||
this.popupRef?.forceAlign();
|
||||
}
|
||||
},
|
||||
fireEvents(type, e) {
|
||||
|
@ -585,7 +642,7 @@ export default defineComponent({
|
|||
render() {
|
||||
const { sPopupVisible, $attrs } = this;
|
||||
const children = filterEmpty(getSlot(this));
|
||||
const { forceRender, alignPoint } = this.$props;
|
||||
const { forceRender, alignPoint, autoDestroy } = this.$props;
|
||||
|
||||
if (children.length > 1) {
|
||||
warning(false, 'Trigger children just support only one default', true);
|
||||
|
@ -641,10 +698,10 @@ export default defineComponent({
|
|||
if (childrenClassName) {
|
||||
newChildProps.class = childrenClassName;
|
||||
}
|
||||
const trigger = cloneElement(child, newChildProps);
|
||||
const trigger = cloneElement(child, { ...newChildProps, ref: 'triggerRef' }, true, true);
|
||||
let portal;
|
||||
// prevent unmounting after it's rendered
|
||||
if (sPopupVisible || this._component || forceRender) {
|
||||
if (sPopupVisible || this.popupRef || forceRender) {
|
||||
portal = (
|
||||
<Portal
|
||||
key="portal"
|
||||
|
@ -654,6 +711,14 @@ export default defineComponent({
|
|||
></Portal>
|
||||
);
|
||||
}
|
||||
return [portal, trigger];
|
||||
if (!sPopupVisible && autoDestroy) {
|
||||
portal = null;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{portal}
|
||||
{trigger}
|
||||
</>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -0,0 +1,659 @@
|
|||
import { defineComponent, inject, provide } from 'vue';
|
||||
import PropTypes from '../_util/vue-types';
|
||||
import contains from '../vc-util/Dom/contains';
|
||||
import {
|
||||
hasProp,
|
||||
getComponent,
|
||||
getEvents,
|
||||
filterEmpty,
|
||||
getSlot,
|
||||
findDOMNode,
|
||||
} from '../_util/props-util';
|
||||
import { requestAnimationTimeout, cancelAnimationTimeout } from '../_util/requestAnimationTimeout';
|
||||
import addEventListener from '../vc-util/Dom/addEventListener';
|
||||
import warning from '../_util/warning';
|
||||
import Popup from './Popup';
|
||||
import { getAlignFromPlacement, getAlignPopupClassName, noop } from './utils';
|
||||
import BaseMixin from '../_util/BaseMixin';
|
||||
import Portal from '../_util/Portal';
|
||||
import classNames from '../_util/classNames';
|
||||
import { cloneElement } from '../_util/vnode';
|
||||
import supportsPassive from '../_util/supportsPassive';
|
||||
|
||||
function returnEmptyString() {
|
||||
return '';
|
||||
}
|
||||
|
||||
function returnDocument() {
|
||||
return window.document;
|
||||
}
|
||||
const ALL_HANDLERS = [
|
||||
'onClick',
|
||||
'onMousedown',
|
||||
'onTouchstart',
|
||||
'onMouseenter',
|
||||
'onMouseleave',
|
||||
'onFocus',
|
||||
'onBlur',
|
||||
'onContextmenu',
|
||||
];
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Trigger',
|
||||
mixins: [BaseMixin],
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
action: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]).def([]),
|
||||
showAction: PropTypes.any.def([]),
|
||||
hideAction: PropTypes.any.def([]),
|
||||
getPopupClassNameFromAlign: PropTypes.any.def(returnEmptyString),
|
||||
onPopupVisibleChange: PropTypes.func.def(noop),
|
||||
afterPopupVisibleChange: PropTypes.func.def(noop),
|
||||
popup: PropTypes.any,
|
||||
popupStyle: PropTypes.object.def(() => ({})),
|
||||
prefixCls: PropTypes.string.def('rc-trigger-popup'),
|
||||
popupClassName: PropTypes.string.def(''),
|
||||
popupPlacement: PropTypes.string,
|
||||
builtinPlacements: PropTypes.object,
|
||||
popupTransitionName: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
||||
popupAnimation: PropTypes.any,
|
||||
mouseEnterDelay: PropTypes.number.def(0),
|
||||
mouseLeaveDelay: PropTypes.number.def(0.1),
|
||||
zIndex: PropTypes.number,
|
||||
focusDelay: PropTypes.number.def(0),
|
||||
blurDelay: PropTypes.number.def(0.15),
|
||||
getPopupContainer: PropTypes.func,
|
||||
getDocument: PropTypes.func.def(returnDocument),
|
||||
forceRender: PropTypes.looseBool,
|
||||
destroyPopupOnHide: PropTypes.looseBool.def(false),
|
||||
mask: PropTypes.looseBool.def(false),
|
||||
maskClosable: PropTypes.looseBool.def(true),
|
||||
// onPopupAlign: PropTypes.func.def(noop),
|
||||
popupAlign: PropTypes.object.def(() => ({})),
|
||||
popupVisible: PropTypes.looseBool,
|
||||
defaultPopupVisible: PropTypes.looseBool.def(false),
|
||||
maskTransitionName: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
||||
maskAnimation: PropTypes.string,
|
||||
stretch: PropTypes.string,
|
||||
alignPoint: PropTypes.looseBool, // Maybe we can support user pass position in the future
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
vcTriggerContext: inject('vcTriggerContext', {}),
|
||||
savePopupRef: inject('savePopupRef', noop),
|
||||
dialogContext: inject('dialogContext', null),
|
||||
};
|
||||
},
|
||||
data() {
|
||||
const props = this.$props;
|
||||
let popupVisible;
|
||||
if (hasProp(this, 'popupVisible')) {
|
||||
popupVisible = !!props.popupVisible;
|
||||
} else {
|
||||
popupVisible = !!props.defaultPopupVisible;
|
||||
}
|
||||
ALL_HANDLERS.forEach(h => {
|
||||
this[`fire${h}`] = e => {
|
||||
this.fireEvents(h, e);
|
||||
};
|
||||
});
|
||||
this._component = null;
|
||||
this.focusTime = null;
|
||||
this.clickOutsideHandler = null;
|
||||
this.contextmenuOutsideHandler1 = null;
|
||||
this.contextmenuOutsideHandler2 = null;
|
||||
this.touchOutsideHandler = null;
|
||||
return {
|
||||
prevPopupVisible: popupVisible,
|
||||
sPopupVisible: popupVisible,
|
||||
point: null,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
popupVisible(val) {
|
||||
if (val !== undefined) {
|
||||
this.prevPopupVisible = this.sPopupVisible;
|
||||
this.sPopupVisible = val;
|
||||
}
|
||||
},
|
||||
},
|
||||
created() {
|
||||
provide('vcTriggerContext', this);
|
||||
},
|
||||
deactivated() {
|
||||
this.setPopupVisible(false);
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this.updatedCal();
|
||||
});
|
||||
},
|
||||
|
||||
updated() {
|
||||
this.$nextTick(() => {
|
||||
this.updatedCal();
|
||||
});
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
this.clearDelayTimer();
|
||||
this.clearOutsideHandler();
|
||||
clearTimeout(this.mouseDownTimeout);
|
||||
},
|
||||
methods: {
|
||||
updatedCal() {
|
||||
const props = this.$props;
|
||||
const state = this.$data;
|
||||
|
||||
// We must listen to `mousedown` or `touchstart`, edge case:
|
||||
// https://github.com/ant-design/ant-design/issues/5804
|
||||
// https://github.com/react-component/calendar/issues/250
|
||||
// https://github.com/react-component/trigger/issues/50
|
||||
if (state.sPopupVisible) {
|
||||
let currentDocument;
|
||||
if (!this.clickOutsideHandler && (this.isClickToHide() || this.isContextmenuToShow())) {
|
||||
currentDocument = props.getDocument();
|
||||
this.clickOutsideHandler = addEventListener(
|
||||
currentDocument,
|
||||
'mousedown',
|
||||
this.onDocumentClick,
|
||||
);
|
||||
}
|
||||
// always hide on mobile
|
||||
if (!this.touchOutsideHandler) {
|
||||
currentDocument = currentDocument || props.getDocument();
|
||||
this.touchOutsideHandler = addEventListener(
|
||||
currentDocument,
|
||||
'touchstart',
|
||||
this.onDocumentClick,
|
||||
supportsPassive ? { passive: false } : false,
|
||||
);
|
||||
}
|
||||
// close popup when trigger type contains 'onContextmenu' and document is scrolling.
|
||||
if (!this.contextmenuOutsideHandler1 && this.isContextmenuToShow()) {
|
||||
currentDocument = currentDocument || props.getDocument();
|
||||
this.contextmenuOutsideHandler1 = addEventListener(
|
||||
currentDocument,
|
||||
'scroll',
|
||||
this.onContextmenuClose,
|
||||
);
|
||||
}
|
||||
// close popup when trigger type contains 'onContextmenu' and window is blur.
|
||||
if (!this.contextmenuOutsideHandler2 && this.isContextmenuToShow()) {
|
||||
this.contextmenuOutsideHandler2 = addEventListener(
|
||||
window,
|
||||
'blur',
|
||||
this.onContextmenuClose,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.clearOutsideHandler();
|
||||
}
|
||||
},
|
||||
onMouseenter(e) {
|
||||
const { mouseEnterDelay } = this.$props;
|
||||
this.fireEvents('onMouseenter', e);
|
||||
this.delaySetPopupVisible(true, mouseEnterDelay, mouseEnterDelay ? null : e);
|
||||
},
|
||||
|
||||
onMouseMove(e) {
|
||||
this.fireEvents('onMousemove', e);
|
||||
this.setPoint(e);
|
||||
},
|
||||
|
||||
onMouseleave(e) {
|
||||
this.fireEvents('onMouseleave', e);
|
||||
this.delaySetPopupVisible(false, this.$props.mouseLeaveDelay);
|
||||
},
|
||||
|
||||
onPopupMouseenter() {
|
||||
this.clearDelayTimer();
|
||||
},
|
||||
|
||||
onPopupMouseleave(e) {
|
||||
if (
|
||||
e &&
|
||||
e.relatedTarget &&
|
||||
!e.relatedTarget.setTimeout &&
|
||||
this._component &&
|
||||
this._component.getPopupDomNode &&
|
||||
contains(this._component.getPopupDomNode(), e.relatedTarget)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.delaySetPopupVisible(false, this.$props.mouseLeaveDelay);
|
||||
},
|
||||
|
||||
onFocus(e) {
|
||||
this.fireEvents('onFocus', e);
|
||||
// incase focusin and focusout
|
||||
this.clearDelayTimer();
|
||||
if (this.isFocusToShow()) {
|
||||
this.focusTime = Date.now();
|
||||
this.delaySetPopupVisible(true, this.$props.focusDelay);
|
||||
}
|
||||
},
|
||||
|
||||
onMousedown(e) {
|
||||
this.fireEvents('onMousedown', e);
|
||||
this.preClickTime = Date.now();
|
||||
},
|
||||
|
||||
onTouchstart(e) {
|
||||
this.fireEvents('onTouchstart', e);
|
||||
this.preTouchTime = Date.now();
|
||||
},
|
||||
|
||||
onBlur(e) {
|
||||
if (!contains(e.target, e.relatedTarget || document.activeElement)) {
|
||||
this.fireEvents('onBlur', e);
|
||||
this.clearDelayTimer();
|
||||
if (this.isBlurToHide()) {
|
||||
this.delaySetPopupVisible(false, this.$props.blurDelay);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onContextmenu(e) {
|
||||
e.preventDefault();
|
||||
this.fireEvents('onContextmenu', e);
|
||||
this.setPopupVisible(true, e);
|
||||
},
|
||||
|
||||
onContextmenuClose() {
|
||||
if (this.isContextmenuToShow()) {
|
||||
this.close();
|
||||
}
|
||||
},
|
||||
|
||||
onClick(event) {
|
||||
this.fireEvents('onClick', event);
|
||||
// focus will trigger click
|
||||
if (this.focusTime) {
|
||||
let preTime;
|
||||
if (this.preClickTime && this.preTouchTime) {
|
||||
preTime = Math.min(this.preClickTime, this.preTouchTime);
|
||||
} else if (this.preClickTime) {
|
||||
preTime = this.preClickTime;
|
||||
} else if (this.preTouchTime) {
|
||||
preTime = this.preTouchTime;
|
||||
}
|
||||
if (Math.abs(preTime - this.focusTime) < 20) {
|
||||
return;
|
||||
}
|
||||
this.focusTime = 0;
|
||||
}
|
||||
this.preClickTime = 0;
|
||||
this.preTouchTime = 0;
|
||||
// Only prevent default when all the action is click.
|
||||
// https://github.com/ant-design/ant-design/issues/17043
|
||||
// https://github.com/ant-design/ant-design/issues/17291
|
||||
if (
|
||||
this.isClickToShow() &&
|
||||
(this.isClickToHide() || this.isBlurToHide()) &&
|
||||
event &&
|
||||
event.preventDefault
|
||||
) {
|
||||
event.preventDefault();
|
||||
}
|
||||
if (event && event.domEvent) {
|
||||
event.domEvent.preventDefault();
|
||||
}
|
||||
const nextVisible = !this.$data.sPopupVisible;
|
||||
if ((this.isClickToHide() && !nextVisible) || (nextVisible && this.isClickToShow())) {
|
||||
this.setPopupVisible(!this.$data.sPopupVisible, event);
|
||||
}
|
||||
},
|
||||
onPopupMouseDown(...args) {
|
||||
const { vcTriggerContext = {} } = this;
|
||||
this.hasPopupMouseDown = true;
|
||||
|
||||
clearTimeout(this.mouseDownTimeout);
|
||||
this.mouseDownTimeout = setTimeout(() => {
|
||||
this.hasPopupMouseDown = false;
|
||||
}, 0);
|
||||
|
||||
if (vcTriggerContext.onPopupMouseDown) {
|
||||
vcTriggerContext.onPopupMouseDown(...args);
|
||||
}
|
||||
},
|
||||
|
||||
onDocumentClick(event) {
|
||||
if (this.$props.mask && !this.$props.maskClosable) {
|
||||
return;
|
||||
}
|
||||
const target = event.target;
|
||||
const root = findDOMNode(this);
|
||||
if (!contains(root, target) && !this.hasPopupMouseDown) {
|
||||
this.close();
|
||||
}
|
||||
},
|
||||
getPopupDomNode() {
|
||||
if (this._component && this._component.getPopupDomNode) {
|
||||
return this._component.getPopupDomNode();
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
getRootDomNode() {
|
||||
return findDOMNode(this);
|
||||
},
|
||||
|
||||
handleGetPopupClassFromAlign(align) {
|
||||
const className = [];
|
||||
const props = this.$props;
|
||||
const {
|
||||
popupPlacement,
|
||||
builtinPlacements,
|
||||
prefixCls,
|
||||
alignPoint,
|
||||
getPopupClassNameFromAlign,
|
||||
} = props;
|
||||
if (popupPlacement && builtinPlacements) {
|
||||
className.push(getAlignPopupClassName(builtinPlacements, prefixCls, align, alignPoint));
|
||||
}
|
||||
if (getPopupClassNameFromAlign) {
|
||||
className.push(getPopupClassNameFromAlign(align));
|
||||
}
|
||||
return className.join(' ');
|
||||
},
|
||||
|
||||
getPopupAlign() {
|
||||
const props = this.$props;
|
||||
const { popupPlacement, popupAlign, builtinPlacements } = props;
|
||||
if (popupPlacement && builtinPlacements) {
|
||||
return getAlignFromPlacement(builtinPlacements, popupPlacement, popupAlign);
|
||||
}
|
||||
return popupAlign;
|
||||
},
|
||||
savePopup(node) {
|
||||
this._component = node;
|
||||
this.savePopupRef(node);
|
||||
},
|
||||
getComponent() {
|
||||
const self = this;
|
||||
const mouseProps = {};
|
||||
if (this.isMouseEnterToShow()) {
|
||||
mouseProps.onMouseenter = self.onPopupMouseenter;
|
||||
}
|
||||
if (this.isMouseLeaveToHide()) {
|
||||
mouseProps.onMouseleave = self.onPopupMouseleave;
|
||||
}
|
||||
mouseProps.onMousedown = this.onPopupMouseDown;
|
||||
mouseProps[supportsPassive ? 'onTouchstartPassive' : 'onTouchstart'] = this.onPopupMouseDown;
|
||||
const { handleGetPopupClassFromAlign, getRootDomNode, getContainer, $attrs } = self;
|
||||
const {
|
||||
prefixCls,
|
||||
destroyPopupOnHide,
|
||||
popupClassName,
|
||||
action,
|
||||
popupAnimation,
|
||||
popupTransitionName,
|
||||
popupStyle,
|
||||
mask,
|
||||
maskAnimation,
|
||||
maskTransitionName,
|
||||
zIndex,
|
||||
stretch,
|
||||
alignPoint,
|
||||
} = self.$props;
|
||||
const { sPopupVisible, point } = this.$data;
|
||||
const align = this.getPopupAlign();
|
||||
const popupProps = {
|
||||
prefixCls,
|
||||
destroyPopupOnHide,
|
||||
visible: sPopupVisible,
|
||||
point: alignPoint ? point : null,
|
||||
action,
|
||||
align,
|
||||
animation: popupAnimation,
|
||||
getClassNameFromAlign: handleGetPopupClassFromAlign,
|
||||
stretch,
|
||||
getRootDomNode,
|
||||
mask,
|
||||
zIndex,
|
||||
transitionName: popupTransitionName,
|
||||
maskAnimation,
|
||||
maskTransitionName,
|
||||
getContainer,
|
||||
popupClassName,
|
||||
popupStyle,
|
||||
onAlign: $attrs.onPopupAlign || noop,
|
||||
...mouseProps,
|
||||
ref: this.savePopup,
|
||||
};
|
||||
return <Popup {...popupProps}>{getComponent(self, 'popup')}</Popup>;
|
||||
},
|
||||
|
||||
getContainer() {
|
||||
const { $props: props, dialogContext } = this;
|
||||
const popupContainer = document.createElement('div');
|
||||
// Make sure default popup container will never cause scrollbar appearing
|
||||
// https://github.com/react-component/trigger/issues/41
|
||||
popupContainer.style.position = 'absolute';
|
||||
popupContainer.style.top = '0';
|
||||
popupContainer.style.left = '0';
|
||||
popupContainer.style.width = '100%';
|
||||
const mountNode = props.getPopupContainer
|
||||
? props.getPopupContainer(findDOMNode(this), dialogContext)
|
||||
: props.getDocument().body;
|
||||
mountNode.appendChild(popupContainer);
|
||||
this.popupContainer = popupContainer;
|
||||
return popupContainer;
|
||||
},
|
||||
|
||||
setPopupVisible(sPopupVisible, event) {
|
||||
const { alignPoint, sPopupVisible: prevPopupVisible, onPopupVisibleChange } = this;
|
||||
this.clearDelayTimer();
|
||||
if (prevPopupVisible !== sPopupVisible) {
|
||||
if (!hasProp(this, 'popupVisible')) {
|
||||
this.setState({
|
||||
sPopupVisible,
|
||||
prevPopupVisible,
|
||||
});
|
||||
}
|
||||
onPopupVisibleChange && onPopupVisibleChange(sPopupVisible);
|
||||
}
|
||||
// Always record the point position since mouseEnterDelay will delay the show
|
||||
if (alignPoint && event) {
|
||||
this.setPoint(event);
|
||||
}
|
||||
},
|
||||
|
||||
setPoint(point) {
|
||||
const { alignPoint } = this.$props;
|
||||
if (!alignPoint || !point) return;
|
||||
|
||||
this.setState({
|
||||
point: {
|
||||
pageX: point.pageX,
|
||||
pageY: point.pageY,
|
||||
},
|
||||
});
|
||||
},
|
||||
handlePortalUpdate() {
|
||||
if (this.prevPopupVisible !== this.sPopupVisible) {
|
||||
this.afterPopupVisibleChange(this.sPopupVisible);
|
||||
}
|
||||
},
|
||||
delaySetPopupVisible(visible, delayS, event) {
|
||||
const delay = delayS * 1000;
|
||||
this.clearDelayTimer();
|
||||
if (delay) {
|
||||
const point = event ? { pageX: event.pageX, pageY: event.pageY } : null;
|
||||
this.delayTimer = requestAnimationTimeout(() => {
|
||||
this.setPopupVisible(visible, point);
|
||||
this.clearDelayTimer();
|
||||
}, delay);
|
||||
} else {
|
||||
this.setPopupVisible(visible, event);
|
||||
}
|
||||
},
|
||||
|
||||
clearDelayTimer() {
|
||||
if (this.delayTimer) {
|
||||
cancelAnimationTimeout(this.delayTimer);
|
||||
this.delayTimer = null;
|
||||
}
|
||||
},
|
||||
|
||||
clearOutsideHandler() {
|
||||
if (this.clickOutsideHandler) {
|
||||
this.clickOutsideHandler.remove();
|
||||
this.clickOutsideHandler = null;
|
||||
}
|
||||
|
||||
if (this.contextmenuOutsideHandler1) {
|
||||
this.contextmenuOutsideHandler1.remove();
|
||||
this.contextmenuOutsideHandler1 = null;
|
||||
}
|
||||
|
||||
if (this.contextmenuOutsideHandler2) {
|
||||
this.contextmenuOutsideHandler2.remove();
|
||||
this.contextmenuOutsideHandler2 = null;
|
||||
}
|
||||
|
||||
if (this.touchOutsideHandler) {
|
||||
this.touchOutsideHandler.remove();
|
||||
this.touchOutsideHandler = null;
|
||||
}
|
||||
},
|
||||
|
||||
createTwoChains(event) {
|
||||
let fn = () => {};
|
||||
const events = getEvents(this);
|
||||
if (this.childOriginEvents[event] && events[event]) {
|
||||
return this[`fire${event}`];
|
||||
}
|
||||
fn = this.childOriginEvents[event] || events[event] || fn;
|
||||
return fn;
|
||||
},
|
||||
|
||||
isClickToShow() {
|
||||
const { action, showAction } = this.$props;
|
||||
return action.indexOf('click') !== -1 || showAction.indexOf('click') !== -1;
|
||||
},
|
||||
|
||||
isContextmenuToShow() {
|
||||
const { action, showAction } = this.$props;
|
||||
return action.indexOf('contextmenu') !== -1 || showAction.indexOf('contextmenu') !== -1;
|
||||
},
|
||||
|
||||
isClickToHide() {
|
||||
const { action, hideAction } = this.$props;
|
||||
return action.indexOf('click') !== -1 || hideAction.indexOf('click') !== -1;
|
||||
},
|
||||
|
||||
isMouseEnterToShow() {
|
||||
const { action, showAction } = this.$props;
|
||||
return action.indexOf('hover') !== -1 || showAction.indexOf('mouseenter') !== -1;
|
||||
},
|
||||
|
||||
isMouseLeaveToHide() {
|
||||
const { action, hideAction } = this.$props;
|
||||
return action.indexOf('hover') !== -1 || hideAction.indexOf('mouseleave') !== -1;
|
||||
},
|
||||
|
||||
isFocusToShow() {
|
||||
const { action, showAction } = this.$props;
|
||||
return action.indexOf('focus') !== -1 || showAction.indexOf('focus') !== -1;
|
||||
},
|
||||
|
||||
isBlurToHide() {
|
||||
const { action, hideAction } = this.$props;
|
||||
return action.indexOf('focus') !== -1 || hideAction.indexOf('blur') !== -1;
|
||||
},
|
||||
forcePopupAlign() {
|
||||
if (this.$data.sPopupVisible && this._component && this._component.alignInstance) {
|
||||
this._component.alignInstance.forceAlign();
|
||||
}
|
||||
},
|
||||
fireEvents(type, e) {
|
||||
if (this.childOriginEvents[type]) {
|
||||
this.childOriginEvents[type](e);
|
||||
}
|
||||
const event = this.$props[type] || this.$attrs[type];
|
||||
if (event) {
|
||||
event(e);
|
||||
}
|
||||
},
|
||||
|
||||
close() {
|
||||
this.setPopupVisible(false);
|
||||
},
|
||||
},
|
||||
render() {
|
||||
const { sPopupVisible, $attrs } = this;
|
||||
const children = filterEmpty(getSlot(this));
|
||||
const { forceRender, alignPoint } = this.$props;
|
||||
|
||||
if (children.length > 1) {
|
||||
warning(false, 'Trigger children just support only one default', true);
|
||||
}
|
||||
const child = children[0];
|
||||
this.childOriginEvents = getEvents(child);
|
||||
const newChildProps = {
|
||||
key: 'trigger',
|
||||
};
|
||||
|
||||
if (this.isContextmenuToShow()) {
|
||||
newChildProps.onContextmenu = this.onContextmenu;
|
||||
} else {
|
||||
newChildProps.onContextmenu = this.createTwoChains('onContextmenu');
|
||||
}
|
||||
|
||||
if (this.isClickToHide() || this.isClickToShow()) {
|
||||
newChildProps.onClick = this.onClick;
|
||||
newChildProps.onMousedown = this.onMousedown;
|
||||
newChildProps[supportsPassive ? 'onTouchstartPassive' : 'onTouchstart'] = this.onTouchstart;
|
||||
} else {
|
||||
newChildProps.onClick = this.createTwoChains('onClick');
|
||||
newChildProps.onMousedown = this.createTwoChains('onMousedown');
|
||||
newChildProps[supportsPassive ? 'onTouchstartPassive' : 'onTouchstart'] =
|
||||
this.createTwoChains('onTouchstart');
|
||||
}
|
||||
if (this.isMouseEnterToShow()) {
|
||||
newChildProps.onMouseenter = this.onMouseenter;
|
||||
if (alignPoint) {
|
||||
newChildProps.onMousemove = this.onMouseMove;
|
||||
}
|
||||
} else {
|
||||
newChildProps.onMouseenter = this.createTwoChains('onMouseenter');
|
||||
}
|
||||
if (this.isMouseLeaveToHide()) {
|
||||
newChildProps.onMouseleave = this.onMouseleave;
|
||||
} else {
|
||||
newChildProps.onMouseleave = this.createTwoChains('onMouseleave');
|
||||
}
|
||||
|
||||
if (this.isFocusToShow() || this.isBlurToHide()) {
|
||||
newChildProps.onFocus = this.onFocus;
|
||||
newChildProps.onBlur = this.onBlur;
|
||||
} else {
|
||||
newChildProps.onFocus = this.createTwoChains('onFocus');
|
||||
newChildProps.onBlur = e => {
|
||||
if (e && (!e.relatedTarget || !contains(e.target, e.relatedTarget))) {
|
||||
this.createTwoChains('onBlur')(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
const childrenClassName = classNames(child && child.props && child.props.class, $attrs.class);
|
||||
if (childrenClassName) {
|
||||
newChildProps.class = childrenClassName;
|
||||
}
|
||||
const trigger = cloneElement(child, newChildProps);
|
||||
let portal;
|
||||
// prevent unmounting after it's rendered
|
||||
if (sPopupVisible || this._component || forceRender) {
|
||||
portal = (
|
||||
<Portal
|
||||
key="portal"
|
||||
children={this.getComponent()}
|
||||
getContainer={this.getContainer}
|
||||
didUpdate={this.handlePortalUpdate}
|
||||
></Portal>
|
||||
);
|
||||
}
|
||||
return [portal, trigger];
|
||||
},
|
||||
});
|
|
@ -0,0 +1,716 @@
|
|||
import { defineComponent, inject, provide, ref } from 'vue';
|
||||
import PropTypes from '../_util/vue-types';
|
||||
import contains from '../vc-util/Dom/contains';
|
||||
import raf from '../_util/raf';
|
||||
import {
|
||||
hasProp,
|
||||
getComponent,
|
||||
getEvents,
|
||||
filterEmpty,
|
||||
getSlot,
|
||||
findDOMNode,
|
||||
} from '../_util/props-util';
|
||||
import { requestAnimationTimeout, cancelAnimationTimeout } from '../_util/requestAnimationTimeout';
|
||||
import addEventListener from '../vc-util/Dom/addEventListener';
|
||||
import warning from '../_util/warning';
|
||||
import Popup from './Popup';
|
||||
import { getAlignFromPlacement, getAlignPopupClassName, noop } from './utils';
|
||||
import BaseMixin from '../_util/BaseMixin';
|
||||
import Portal from '../_util/Portal';
|
||||
import classNames from '../_util/classNames';
|
||||
import { cloneElement } from '../_util/vnode';
|
||||
import supportsPassive from '../_util/supportsPassive';
|
||||
|
||||
function returnEmptyString() {
|
||||
return '';
|
||||
}
|
||||
|
||||
function returnDocument(element) {
|
||||
if (element) {
|
||||
return element.ownerDocument;
|
||||
}
|
||||
return window.document;
|
||||
}
|
||||
const ALL_HANDLERS = [
|
||||
'onClick',
|
||||
'onMousedown',
|
||||
'onTouchstart',
|
||||
'onMouseenter',
|
||||
'onMouseleave',
|
||||
'onFocus',
|
||||
'onBlur',
|
||||
'onContextmenu',
|
||||
];
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Trigger',
|
||||
mixins: [BaseMixin],
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
action: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]).def([]),
|
||||
showAction: PropTypes.any.def([]),
|
||||
hideAction: PropTypes.any.def([]),
|
||||
getPopupClassNameFromAlign: PropTypes.any.def(returnEmptyString),
|
||||
onPopupVisibleChange: PropTypes.func.def(noop),
|
||||
afterPopupVisibleChange: PropTypes.func.def(noop),
|
||||
popup: PropTypes.any,
|
||||
popupStyle: PropTypes.object.def(() => ({})),
|
||||
prefixCls: PropTypes.string.def('rc-trigger-popup'),
|
||||
popupClassName: PropTypes.string.def(''),
|
||||
popupPlacement: PropTypes.string,
|
||||
builtinPlacements: PropTypes.object,
|
||||
popupTransitionName: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
||||
popupAnimation: PropTypes.any,
|
||||
mouseEnterDelay: PropTypes.number.def(0),
|
||||
mouseLeaveDelay: PropTypes.number.def(0.1),
|
||||
zIndex: PropTypes.number,
|
||||
focusDelay: PropTypes.number.def(0),
|
||||
blurDelay: PropTypes.number.def(0.15),
|
||||
getPopupContainer: PropTypes.func,
|
||||
getDocument: PropTypes.func.def(returnDocument),
|
||||
forceRender: PropTypes.looseBool,
|
||||
destroyPopupOnHide: PropTypes.looseBool.def(false),
|
||||
mask: PropTypes.looseBool.def(false),
|
||||
maskClosable: PropTypes.looseBool.def(true),
|
||||
// onPopupAlign: PropTypes.func.def(noop),
|
||||
popupAlign: PropTypes.object.def(() => ({})),
|
||||
popupVisible: PropTypes.looseBool,
|
||||
defaultPopupVisible: PropTypes.looseBool.def(false),
|
||||
maskTransitionName: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
||||
maskAnimation: PropTypes.string,
|
||||
stretch: PropTypes.string,
|
||||
alignPoint: PropTypes.looseBool, // Maybe we can support user pass position in the future
|
||||
autoDestroy: PropTypes.looseBool.def(false),
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
vcTriggerContext: inject('vcTriggerContext', {}),
|
||||
dialogContext: inject('dialogContext', null),
|
||||
popupRef: ref(null),
|
||||
triggerRef: ref(null),
|
||||
};
|
||||
},
|
||||
data() {
|
||||
const props = this.$props;
|
||||
let popupVisible;
|
||||
if (this.popupVisible !== undefined) {
|
||||
popupVisible = !!props.popupVisible;
|
||||
} else {
|
||||
popupVisible = !!props.defaultPopupVisible;
|
||||
}
|
||||
ALL_HANDLERS.forEach(h => {
|
||||
this[`fire${h}`] = e => {
|
||||
this.fireEvents(h, e);
|
||||
};
|
||||
});
|
||||
this.focusTime = null;
|
||||
this.clickOutsideHandler = null;
|
||||
this.contextmenuOutsideHandler1 = null;
|
||||
this.contextmenuOutsideHandler2 = null;
|
||||
this.touchOutsideHandler = null;
|
||||
this.attachId = null;
|
||||
return {
|
||||
prevPopupVisible: popupVisible,
|
||||
sPopupVisible: popupVisible,
|
||||
point: null,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
popupVisible(val) {
|
||||
if (val !== undefined) {
|
||||
this.prevPopupVisible = this.sPopupVisible;
|
||||
this.sPopupVisible = val;
|
||||
}
|
||||
},
|
||||
},
|
||||
created() {
|
||||
provide('vcTriggerContext', this);
|
||||
},
|
||||
deactivated() {
|
||||
this.setPopupVisible(false);
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this.updatedCal();
|
||||
});
|
||||
},
|
||||
|
||||
updated() {
|
||||
this.$nextTick(() => {
|
||||
this.updatedCal();
|
||||
});
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
this.clearDelayTimer();
|
||||
this.clearOutsideHandler();
|
||||
clearTimeout(this.mouseDownTimeout);
|
||||
raf.cancel(this.attachId);
|
||||
},
|
||||
methods: {
|
||||
updatedCal() {
|
||||
const props = this.$props;
|
||||
const state = this.$data;
|
||||
|
||||
// We must listen to `mousedown` or `touchstart`, edge case:
|
||||
// https://github.com/ant-design/ant-design/issues/5804
|
||||
// https://github.com/react-component/calendar/issues/250
|
||||
// https://github.com/react-component/trigger/issues/50
|
||||
if (state.sPopupVisible) {
|
||||
let currentDocument;
|
||||
if (!this.clickOutsideHandler && (this.isClickToHide() || this.isContextmenuToShow())) {
|
||||
currentDocument = props.getDocument(this.getRootDomNode());
|
||||
this.clickOutsideHandler = addEventListener(
|
||||
currentDocument,
|
||||
'mousedown',
|
||||
this.onDocumentClick,
|
||||
);
|
||||
}
|
||||
// always hide on mobile
|
||||
if (!this.touchOutsideHandler) {
|
||||
currentDocument = currentDocument || props.getDocument(this.getRootDomNode());
|
||||
this.touchOutsideHandler = addEventListener(
|
||||
currentDocument,
|
||||
'touchstart',
|
||||
this.onDocumentClick,
|
||||
supportsPassive ? { passive: false } : false,
|
||||
);
|
||||
}
|
||||
// close popup when trigger type contains 'onContextmenu' and document is scrolling.
|
||||
if (!this.contextmenuOutsideHandler1 && this.isContextmenuToShow()) {
|
||||
currentDocument = currentDocument || props.getDocument(this.getRootDomNode());
|
||||
this.contextmenuOutsideHandler1 = addEventListener(
|
||||
currentDocument,
|
||||
'scroll',
|
||||
this.onContextmenuClose,
|
||||
);
|
||||
}
|
||||
// close popup when trigger type contains 'onContextmenu' and window is blur.
|
||||
if (!this.contextmenuOutsideHandler2 && this.isContextmenuToShow()) {
|
||||
this.contextmenuOutsideHandler2 = addEventListener(
|
||||
window,
|
||||
'blur',
|
||||
this.onContextmenuClose,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.clearOutsideHandler();
|
||||
}
|
||||
},
|
||||
onMouseenter(e) {
|
||||
const { mouseEnterDelay } = this.$props;
|
||||
this.fireEvents('onMouseenter', e);
|
||||
this.delaySetPopupVisible(true, mouseEnterDelay, mouseEnterDelay ? null : e);
|
||||
},
|
||||
|
||||
onMouseMove(e) {
|
||||
this.fireEvents('onMousemove', e);
|
||||
this.setPoint(e);
|
||||
},
|
||||
|
||||
onMouseleave(e) {
|
||||
this.fireEvents('onMouseleave', e);
|
||||
this.delaySetPopupVisible(false, this.$props.mouseLeaveDelay);
|
||||
},
|
||||
|
||||
onPopupMouseenter() {
|
||||
this.clearDelayTimer();
|
||||
},
|
||||
|
||||
onPopupMouseleave(e) {
|
||||
if (
|
||||
e &&
|
||||
e.relatedTarget &&
|
||||
!e.relatedTarget.setTimeout &&
|
||||
contains(this.popupRef?.getPopupDomNode(), e.relatedTarget)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.delaySetPopupVisible(false, this.$props.mouseLeaveDelay);
|
||||
},
|
||||
|
||||
onFocus(e) {
|
||||
this.fireEvents('onFocus', e);
|
||||
// incase focusin and focusout
|
||||
this.clearDelayTimer();
|
||||
if (this.isFocusToShow()) {
|
||||
this.focusTime = Date.now();
|
||||
this.delaySetPopupVisible(true, this.$props.focusDelay);
|
||||
}
|
||||
},
|
||||
|
||||
onMousedown(e) {
|
||||
this.fireEvents('onMousedown', e);
|
||||
this.preClickTime = Date.now();
|
||||
},
|
||||
|
||||
onTouchstart(e) {
|
||||
this.fireEvents('onTouchstart', e);
|
||||
this.preTouchTime = Date.now();
|
||||
},
|
||||
|
||||
onBlur(e) {
|
||||
if (!contains(e.target, e.relatedTarget || document.activeElement)) {
|
||||
this.fireEvents('onBlur', e);
|
||||
this.clearDelayTimer();
|
||||
if (this.isBlurToHide()) {
|
||||
this.delaySetPopupVisible(false, this.$props.blurDelay);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onContextmenu(e) {
|
||||
e.preventDefault();
|
||||
this.fireEvents('onContextmenu', e);
|
||||
this.setPopupVisible(true, e);
|
||||
},
|
||||
|
||||
onContextmenuClose() {
|
||||
if (this.isContextmenuToShow()) {
|
||||
this.close();
|
||||
}
|
||||
},
|
||||
|
||||
onClick(event) {
|
||||
this.fireEvents('onClick', event);
|
||||
// focus will trigger click
|
||||
if (this.focusTime) {
|
||||
let preTime;
|
||||
if (this.preClickTime && this.preTouchTime) {
|
||||
preTime = Math.min(this.preClickTime, this.preTouchTime);
|
||||
} else if (this.preClickTime) {
|
||||
preTime = this.preClickTime;
|
||||
} else if (this.preTouchTime) {
|
||||
preTime = this.preTouchTime;
|
||||
}
|
||||
if (Math.abs(preTime - this.focusTime) < 20) {
|
||||
return;
|
||||
}
|
||||
this.focusTime = 0;
|
||||
}
|
||||
this.preClickTime = 0;
|
||||
this.preTouchTime = 0;
|
||||
// Only prevent default when all the action is click.
|
||||
// https://github.com/ant-design/ant-design/issues/17043
|
||||
// https://github.com/ant-design/ant-design/issues/17291
|
||||
if (
|
||||
this.isClickToShow() &&
|
||||
(this.isClickToHide() || this.isBlurToHide()) &&
|
||||
event &&
|
||||
event.preventDefault
|
||||
) {
|
||||
event.preventDefault();
|
||||
}
|
||||
if (event && event.domEvent) {
|
||||
event.domEvent.preventDefault();
|
||||
}
|
||||
const nextVisible = !this.$data.sPopupVisible;
|
||||
if ((this.isClickToHide() && !nextVisible) || (nextVisible && this.isClickToShow())) {
|
||||
this.setPopupVisible(!this.$data.sPopupVisible, event);
|
||||
}
|
||||
},
|
||||
onPopupMouseDown(...args) {
|
||||
const { vcTriggerContext = {} } = this;
|
||||
this.hasPopupMouseDown = true;
|
||||
|
||||
clearTimeout(this.mouseDownTimeout);
|
||||
this.mouseDownTimeout = setTimeout(() => {
|
||||
this.hasPopupMouseDown = false;
|
||||
}, 0);
|
||||
|
||||
if (vcTriggerContext.onPopupMouseDown) {
|
||||
vcTriggerContext.onPopupMouseDown(...args);
|
||||
}
|
||||
},
|
||||
|
||||
onDocumentClick(event) {
|
||||
if (this.$props.mask && !this.$props.maskClosable) {
|
||||
return;
|
||||
}
|
||||
const target = event.target;
|
||||
const root = this.getRootDomNode();
|
||||
const popupNode = this.getPopupDomNode();
|
||||
if (
|
||||
// mousedown on the target should also close popup when action is contextMenu.
|
||||
// https://github.com/ant-design/ant-design/issues/29853
|
||||
(!contains(root, target) || this.isContextMenuOnly()) &&
|
||||
!contains(popupNode, target) &&
|
||||
!this.hasPopupMouseDown
|
||||
) {
|
||||
this.close();
|
||||
}
|
||||
},
|
||||
getPopupDomNode() {
|
||||
// for test
|
||||
return this.popupRef?.getElement() || null;
|
||||
},
|
||||
|
||||
getRootDomNode() {
|
||||
const { getTriggerDOMNode } = this.$props;
|
||||
if (getTriggerDOMNode) {
|
||||
return getTriggerDOMNode(this.triggerRef);
|
||||
}
|
||||
|
||||
try {
|
||||
const domNode = findDOMNode(this.triggerRef);
|
||||
if (domNode) {
|
||||
return domNode;
|
||||
}
|
||||
} catch (err) {
|
||||
// Do nothing
|
||||
}
|
||||
return findDOMNode(this);
|
||||
},
|
||||
|
||||
handleGetPopupClassFromAlign(align) {
|
||||
const className = [];
|
||||
const props = this.$props;
|
||||
const {
|
||||
popupPlacement,
|
||||
builtinPlacements,
|
||||
prefixCls,
|
||||
alignPoint,
|
||||
getPopupClassNameFromAlign,
|
||||
} = props;
|
||||
if (popupPlacement && builtinPlacements) {
|
||||
className.push(getAlignPopupClassName(builtinPlacements, prefixCls, align, alignPoint));
|
||||
}
|
||||
if (getPopupClassNameFromAlign) {
|
||||
className.push(getPopupClassNameFromAlign(align));
|
||||
}
|
||||
return className.join(' ');
|
||||
},
|
||||
|
||||
getPopupAlign() {
|
||||
const props = this.$props;
|
||||
const { popupPlacement, popupAlign, builtinPlacements } = props;
|
||||
if (popupPlacement && builtinPlacements) {
|
||||
return getAlignFromPlacement(builtinPlacements, popupPlacement, popupAlign);
|
||||
}
|
||||
return popupAlign;
|
||||
},
|
||||
getComponent() {
|
||||
const self = this;
|
||||
const mouseProps = {};
|
||||
if (this.isMouseEnterToShow()) {
|
||||
mouseProps.onMouseenter = self.onPopupMouseenter;
|
||||
}
|
||||
if (this.isMouseLeaveToHide()) {
|
||||
mouseProps.onMouseleave = self.onPopupMouseleave;
|
||||
}
|
||||
mouseProps.onMousedown = this.onPopupMouseDown;
|
||||
mouseProps[supportsPassive ? 'onTouchstartPassive' : 'onTouchstart'] = this.onPopupMouseDown;
|
||||
const { handleGetPopupClassFromAlign, getRootDomNode, getContainer, $attrs } = self;
|
||||
const {
|
||||
prefixCls,
|
||||
destroyPopupOnHide,
|
||||
popupClassName,
|
||||
popupAnimation,
|
||||
popupTransitionName,
|
||||
popupStyle,
|
||||
mask,
|
||||
maskAnimation,
|
||||
maskTransitionName,
|
||||
zIndex,
|
||||
stretch,
|
||||
alignPoint,
|
||||
mobile,
|
||||
forceRender,
|
||||
} = self.$props;
|
||||
const { sPopupVisible, point } = this.$data;
|
||||
const align = this.getPopupAlign();
|
||||
const popupProps = {
|
||||
prefixCls,
|
||||
destroyPopupOnHide,
|
||||
visible: sPopupVisible,
|
||||
point: alignPoint ? point : null,
|
||||
align,
|
||||
animation: popupAnimation,
|
||||
getClassNameFromAlign: handleGetPopupClassFromAlign,
|
||||
stretch,
|
||||
getRootDomNode,
|
||||
mask,
|
||||
zIndex,
|
||||
transitionName: popupTransitionName,
|
||||
maskAnimation,
|
||||
maskTransitionName,
|
||||
getContainer,
|
||||
popupClassName,
|
||||
popupStyle,
|
||||
onAlign: $attrs.onPopupAlign || noop,
|
||||
...mouseProps,
|
||||
ref: 'popupRef',
|
||||
mobile,
|
||||
forceRender,
|
||||
};
|
||||
return <Popup {...popupProps}>{getComponent(self, 'popup')}</Popup>;
|
||||
},
|
||||
|
||||
attachParent(popupContainer) {
|
||||
raf.cancel(this.attachId);
|
||||
|
||||
const { getPopupContainer, getDocument } = this.$props;
|
||||
const domNode = this.getRootDomNode();
|
||||
|
||||
let mountNode;
|
||||
if (!getPopupContainer) {
|
||||
mountNode = getDocument(this.getRootDomNode()).body;
|
||||
} else if (domNode || getPopupContainer.length === 0) {
|
||||
// Compatible for legacy getPopupContainer with domNode argument.
|
||||
// If no need `domNode` argument, will call directly.
|
||||
// https://codesandbox.io/s/eloquent-mclean-ss93m?file=/src/App.js
|
||||
mountNode = getPopupContainer(domNode);
|
||||
}
|
||||
|
||||
if (mountNode) {
|
||||
mountNode.appendChild(popupContainer);
|
||||
} else {
|
||||
// Retry after frame render in case parent not ready
|
||||
this.attachId = raf(() => {
|
||||
this.attachParent(popupContainer);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
getContainer() {
|
||||
const { $props: props } = this;
|
||||
const { getDocument } = props;
|
||||
const popupContainer = getDocument(this.getRootDomNode()).createElement('div');
|
||||
// Make sure default popup container will never cause scrollbar appearing
|
||||
// https://github.com/react-component/trigger/issues/41
|
||||
popupContainer.style.position = 'absolute';
|
||||
popupContainer.style.top = '0';
|
||||
popupContainer.style.left = '0';
|
||||
popupContainer.style.width = '100%';
|
||||
this.attachParent(popupContainer);
|
||||
return popupContainer;
|
||||
},
|
||||
|
||||
setPopupVisible(sPopupVisible, event) {
|
||||
const { alignPoint, sPopupVisible: prevPopupVisible, onPopupVisibleChange } = this;
|
||||
this.clearDelayTimer();
|
||||
if (prevPopupVisible !== sPopupVisible) {
|
||||
if (!hasProp(this, 'popupVisible')) {
|
||||
this.setState({
|
||||
sPopupVisible,
|
||||
prevPopupVisible,
|
||||
});
|
||||
}
|
||||
onPopupVisibleChange && onPopupVisibleChange(sPopupVisible);
|
||||
}
|
||||
// Always record the point position since mouseEnterDelay will delay the show
|
||||
if (alignPoint && event && sPopupVisible) {
|
||||
this.setPoint(event);
|
||||
}
|
||||
},
|
||||
|
||||
setPoint(point) {
|
||||
const { alignPoint } = this.$props;
|
||||
if (!alignPoint || !point) return;
|
||||
|
||||
this.setState({
|
||||
point: {
|
||||
pageX: point.pageX,
|
||||
pageY: point.pageY,
|
||||
},
|
||||
});
|
||||
},
|
||||
handlePortalUpdate() {
|
||||
if (this.prevPopupVisible !== this.sPopupVisible) {
|
||||
this.afterPopupVisibleChange(this.sPopupVisible);
|
||||
}
|
||||
},
|
||||
delaySetPopupVisible(visible, delayS, event) {
|
||||
const delay = delayS * 1000;
|
||||
this.clearDelayTimer();
|
||||
if (delay) {
|
||||
const point = event ? { pageX: event.pageX, pageY: event.pageY } : null;
|
||||
this.delayTimer = requestAnimationTimeout(() => {
|
||||
this.setPopupVisible(visible, point);
|
||||
this.clearDelayTimer();
|
||||
}, delay);
|
||||
} else {
|
||||
this.setPopupVisible(visible, event);
|
||||
}
|
||||
},
|
||||
|
||||
clearDelayTimer() {
|
||||
if (this.delayTimer) {
|
||||
cancelAnimationTimeout(this.delayTimer);
|
||||
this.delayTimer = null;
|
||||
}
|
||||
},
|
||||
|
||||
clearOutsideHandler() {
|
||||
if (this.clickOutsideHandler) {
|
||||
this.clickOutsideHandler.remove();
|
||||
this.clickOutsideHandler = null;
|
||||
}
|
||||
|
||||
if (this.contextmenuOutsideHandler1) {
|
||||
this.contextmenuOutsideHandler1.remove();
|
||||
this.contextmenuOutsideHandler1 = null;
|
||||
}
|
||||
|
||||
if (this.contextmenuOutsideHandler2) {
|
||||
this.contextmenuOutsideHandler2.remove();
|
||||
this.contextmenuOutsideHandler2 = null;
|
||||
}
|
||||
|
||||
if (this.touchOutsideHandler) {
|
||||
this.touchOutsideHandler.remove();
|
||||
this.touchOutsideHandler = null;
|
||||
}
|
||||
},
|
||||
|
||||
createTwoChains(event) {
|
||||
let fn = () => {};
|
||||
const events = getEvents(this);
|
||||
if (this.childOriginEvents[event] && events[event]) {
|
||||
return this[`fire${event}`];
|
||||
}
|
||||
fn = this.childOriginEvents[event] || events[event] || fn;
|
||||
return fn;
|
||||
},
|
||||
|
||||
isClickToShow() {
|
||||
const { action, showAction } = this.$props;
|
||||
return action.indexOf('click') !== -1 || showAction.indexOf('click') !== -1;
|
||||
},
|
||||
|
||||
isContextMenuOnly() {
|
||||
const { action } = this.$props;
|
||||
return action === 'contextmenu' || (action.length === 1 && action[0] === 'contextmenu');
|
||||
},
|
||||
|
||||
isContextmenuToShow() {
|
||||
const { action, showAction } = this.$props;
|
||||
return action.indexOf('contextmenu') !== -1 || showAction.indexOf('contextmenu') !== -1;
|
||||
},
|
||||
|
||||
isClickToHide() {
|
||||
const { action, hideAction } = this.$props;
|
||||
return action.indexOf('click') !== -1 || hideAction.indexOf('click') !== -1;
|
||||
},
|
||||
|
||||
isMouseEnterToShow() {
|
||||
const { action, showAction } = this.$props;
|
||||
return action.indexOf('hover') !== -1 || showAction.indexOf('mouseenter') !== -1;
|
||||
},
|
||||
|
||||
isMouseLeaveToHide() {
|
||||
const { action, hideAction } = this.$props;
|
||||
return action.indexOf('hover') !== -1 || hideAction.indexOf('mouseleave') !== -1;
|
||||
},
|
||||
|
||||
isFocusToShow() {
|
||||
const { action, showAction } = this.$props;
|
||||
return action.indexOf('focus') !== -1 || showAction.indexOf('focus') !== -1;
|
||||
},
|
||||
|
||||
isBlurToHide() {
|
||||
const { action, hideAction } = this.$props;
|
||||
return action.indexOf('focus') !== -1 || hideAction.indexOf('blur') !== -1;
|
||||
},
|
||||
forcePopupAlign() {
|
||||
if (this.$data.sPopupVisible) {
|
||||
this.popupRef?.forceAlign();
|
||||
}
|
||||
},
|
||||
fireEvents(type, e) {
|
||||
if (this.childOriginEvents[type]) {
|
||||
this.childOriginEvents[type](e);
|
||||
}
|
||||
const event = this.$props[type] || this.$attrs[type];
|
||||
if (event) {
|
||||
event(e);
|
||||
}
|
||||
},
|
||||
|
||||
close() {
|
||||
this.setPopupVisible(false);
|
||||
},
|
||||
},
|
||||
render() {
|
||||
const { sPopupVisible, $attrs } = this;
|
||||
const children = filterEmpty(getSlot(this));
|
||||
const { forceRender, alignPoint, autoDestroy } = this.$props;
|
||||
|
||||
if (children.length > 1) {
|
||||
warning(false, 'Trigger children just support only one default', true);
|
||||
}
|
||||
const child = children[0];
|
||||
this.childOriginEvents = getEvents(child);
|
||||
const newChildProps = {
|
||||
key: 'trigger',
|
||||
};
|
||||
|
||||
if (this.isContextmenuToShow()) {
|
||||
newChildProps.onContextmenu = this.onContextmenu;
|
||||
} else {
|
||||
newChildProps.onContextmenu = this.createTwoChains('onContextmenu');
|
||||
}
|
||||
|
||||
if (this.isClickToHide() || this.isClickToShow()) {
|
||||
newChildProps.onClick = this.onClick;
|
||||
newChildProps.onMousedown = this.onMousedown;
|
||||
newChildProps[supportsPassive ? 'onTouchstartPassive' : 'onTouchstart'] = this.onTouchstart;
|
||||
} else {
|
||||
newChildProps.onClick = this.createTwoChains('onClick');
|
||||
newChildProps.onMousedown = this.createTwoChains('onMousedown');
|
||||
newChildProps[supportsPassive ? 'onTouchstartPassive' : 'onTouchstart'] =
|
||||
this.createTwoChains('onTouchstart');
|
||||
}
|
||||
if (this.isMouseEnterToShow()) {
|
||||
newChildProps.onMouseenter = this.onMouseenter;
|
||||
if (alignPoint) {
|
||||
newChildProps.onMousemove = this.onMouseMove;
|
||||
}
|
||||
} else {
|
||||
newChildProps.onMouseenter = this.createTwoChains('onMouseenter');
|
||||
}
|
||||
if (this.isMouseLeaveToHide()) {
|
||||
newChildProps.onMouseleave = this.onMouseleave;
|
||||
} else {
|
||||
newChildProps.onMouseleave = this.createTwoChains('onMouseleave');
|
||||
}
|
||||
|
||||
if (this.isFocusToShow() || this.isBlurToHide()) {
|
||||
newChildProps.onFocus = this.onFocus;
|
||||
newChildProps.onBlur = this.onBlur;
|
||||
} else {
|
||||
newChildProps.onFocus = this.createTwoChains('onFocus');
|
||||
newChildProps.onBlur = e => {
|
||||
if (e && (!e.relatedTarget || !contains(e.target, e.relatedTarget))) {
|
||||
this.createTwoChains('onBlur')(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
const childrenClassName = classNames(child && child.props && child.props.class, $attrs.class);
|
||||
if (childrenClassName) {
|
||||
newChildProps.class = childrenClassName;
|
||||
}
|
||||
const trigger = cloneElement(child, { ...newChildProps, ref: 'triggerRef' }, true, true);
|
||||
let portal;
|
||||
// prevent unmounting after it's rendered
|
||||
if (sPopupVisible || this.popupRef || forceRender) {
|
||||
portal = (
|
||||
<Portal
|
||||
key="portal"
|
||||
children={this.getComponent()}
|
||||
getContainer={this.getContainer}
|
||||
didUpdate={this.handlePortalUpdate}
|
||||
></Portal>
|
||||
);
|
||||
}
|
||||
if (!sPopupVisible && autoDestroy) {
|
||||
portal = null;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{portal}
|
||||
{trigger}
|
||||
</>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,72 @@
|
|||
import type { CSSProperties, TransitionProps } from 'vue';
|
||||
import type { VueNode } from '../_util/type';
|
||||
|
||||
/** Two char of 't' 'b' 'c' 'l' 'r'. Example: 'lt' */
|
||||
export type AlignPoint = string;
|
||||
|
||||
export interface AlignType {
|
||||
/**
|
||||
* move point of source node to align with point of target node.
|
||||
* Such as ['tr','cc'], align top right point of source node with center point of target node.
|
||||
* Point can be 't'(top), 'b'(bottom), 'c'(center), 'l'(left), 'r'(right) */
|
||||
points?: AlignPoint[];
|
||||
/**
|
||||
* offset source node by offset[0] in x and offset[1] in y.
|
||||
* If offset contains percentage string value, it is relative to sourceNode region.
|
||||
*/
|
||||
offset?: number[];
|
||||
/**
|
||||
* offset target node by offset[0] in x and offset[1] in y.
|
||||
* If targetOffset contains percentage string value, it is relative to targetNode region.
|
||||
*/
|
||||
targetOffset?: number[];
|
||||
/**
|
||||
* If adjustX field is true, will adjust source node in x direction if source node is invisible.
|
||||
* If adjustY field is true, will adjust source node in y direction if source node is invisible.
|
||||
*/
|
||||
overflow?: {
|
||||
adjustX?: boolean | number;
|
||||
adjustY?: boolean | number;
|
||||
};
|
||||
/**
|
||||
* Whether use css right instead of left to position
|
||||
*/
|
||||
useCssRight?: boolean;
|
||||
/**
|
||||
* Whether use css bottom instead of top to position
|
||||
*/
|
||||
useCssBottom?: boolean;
|
||||
/**
|
||||
* Whether use css transform instead of left/top/right/bottom to position if browser supports.
|
||||
* Defaults to false.
|
||||
*/
|
||||
useCssTransform?: boolean;
|
||||
ignoreShake?: boolean;
|
||||
}
|
||||
|
||||
export type BuildInPlacements = Record<string, AlignType>;
|
||||
|
||||
export type StretchType = string;
|
||||
|
||||
export type ActionType = string;
|
||||
|
||||
export type AnimationType = string;
|
||||
|
||||
export type TransitionNameType = string;
|
||||
|
||||
export interface Point {
|
||||
pageX: number;
|
||||
pageY: number;
|
||||
}
|
||||
|
||||
export interface CommonEventHandler {
|
||||
remove: () => void;
|
||||
}
|
||||
|
||||
export interface MobileConfig {
|
||||
/** Set popup motion. You can ref `rc-motion` for more info. */
|
||||
popupMotion?: TransitionProps;
|
||||
popupClassName?: string;
|
||||
popupStyle?: CSSProperties;
|
||||
popupRender?: (originNode: VueNode) => VueNode;
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
import type { AlignType, BuildInPlacements, AlignPoint } from '../interface';
|
||||
|
||||
function isPointsEq(a1: AlignPoint[], a2: AlignPoint[], isAlignPoint: boolean): boolean {
|
||||
if (isAlignPoint) {
|
||||
return a1[0] === a2[0];
|
||||
}
|
||||
return a1[0] === a2[0] && a1[1] === a2[1];
|
||||
}
|
||||
|
||||
export function getAlignFromPlacement(
|
||||
builtinPlacements: BuildInPlacements,
|
||||
placementStr: string,
|
||||
align: AlignType,
|
||||
): AlignType {
|
||||
const baseAlign = builtinPlacements[placementStr] || {};
|
||||
return {
|
||||
...baseAlign,
|
||||
...align,
|
||||
};
|
||||
}
|
||||
|
||||
export function getAlignPopupClassName(
|
||||
builtinPlacements: BuildInPlacements,
|
||||
prefixCls: string,
|
||||
align: AlignType,
|
||||
isAlignPoint: boolean,
|
||||
): string {
|
||||
const { points } = align;
|
||||
|
||||
const placements = Object.keys(builtinPlacements);
|
||||
|
||||
for (let i = 0; i < placements.length; i += 1) {
|
||||
const placement = placements[i];
|
||||
if (isPointsEq(builtinPlacements[placement].points, points, isAlignPoint)) {
|
||||
return `${prefixCls}-placement-${placement}`;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import type { AnimationType, TransitionNameType } from '../interface';
|
||||
|
||||
interface GetMotionProps {
|
||||
animation: AnimationType;
|
||||
transitionName: TransitionNameType;
|
||||
prefixCls: string;
|
||||
}
|
||||
|
||||
export function getMotion({ prefixCls, animation, transitionName }: GetMotionProps) {
|
||||
if (animation) {
|
||||
return {
|
||||
name: `${prefixCls}-${animation}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (transitionName) {
|
||||
return {
|
||||
name: transitionName,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
|
@ -1,39 +1,61 @@
|
|||
<template>
|
||||
<div>
|
||||
<demo />
|
||||
</div>
|
||||
<a-radio-group v-model:value="size">
|
||||
<a-radio-button value="large">Large</a-radio-button>
|
||||
<a-radio-button value="default">Default</a-radio-button>
|
||||
<a-radio-button value="small">Small</a-radio-button>
|
||||
</a-radio-group>
|
||||
<br />
|
||||
<br />
|
||||
<a-space direction="vertical">
|
||||
<a-select
|
||||
v-model:value="value1"
|
||||
:size="size"
|
||||
style="width: 200px"
|
||||
:options="options"
|
||||
></a-select>
|
||||
<!-- <a-select
|
||||
v-model:value="value2"
|
||||
:options="options"
|
||||
mode="multiple"
|
||||
:size="size"
|
||||
placeholder="Please select"
|
||||
style="width: 200px"
|
||||
@popupScroll="popupScroll"
|
||||
>
|
||||
</a-select>
|
||||
<a-select
|
||||
v-model:value="value3"
|
||||
:options="options"
|
||||
mode="tags"
|
||||
:size="size"
|
||||
placeholder="Please select"
|
||||
style="width: 200px"
|
||||
>
|
||||
</a-select> -->
|
||||
</a-space>
|
||||
</template>
|
||||
<script>
|
||||
import { defineComponent } from 'vue';
|
||||
import demo from '../v2-doc/src/docs/mentions/demo/index.vue';
|
||||
// import Affix from '../components/affix';
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from 'vue';
|
||||
export default defineComponent({
|
||||
components: {
|
||||
demo,
|
||||
// Affix,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
pStyle: {
|
||||
fontSize: '16px',
|
||||
color: 'rgba(0,0,0,0.85)',
|
||||
lineHeight: '24px',
|
||||
display: 'block',
|
||||
marginBottom: '16px',
|
||||
},
|
||||
pStyle2: {
|
||||
marginBottom: '24px',
|
||||
},
|
||||
setup() {
|
||||
const popupScroll = () => {
|
||||
console.log('popupScroll');
|
||||
};
|
||||
|
||||
return {
|
||||
popupScroll,
|
||||
size: ref('default'),
|
||||
value1: ref('t20'),
|
||||
value2: ref(['a1', 'b2']),
|
||||
value3: ref(['a1', 'b2']),
|
||||
options: [...Array(25)].map((_, i) => ({ value: (i + 10).toString(36) + (i + 1) })),
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
showDrawer() {
|
||||
this.visible = true;
|
||||
},
|
||||
onClose() {
|
||||
this.visible = false;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style>
|
||||
.ant-select-dropdown {
|
||||
/* top: 140px !important;
|
||||
left: 50px !important; */
|
||||
}
|
||||
</style>
|
||||
|
|
2
v2-doc
2
v2-doc
|
@ -1 +1 @@
|
|||
Subproject commit e5fb2accb9cf5e02e2fd0011310a70041b5ff7a1
|
||||
Subproject commit 7a7b52df8b3b69d8b1a8b8dcd96e1b0f7bb3f8c9
|
Loading…
Reference in New Issue