@react-vant-next/campaign
Version:
React Mobile UI Components based on Vant UI - Next Generation
241 lines (238 loc) • 9.85 kB
JavaScript
import { __rest } from 'tslib';
import { jsx, jsxs } from 'react/jsx-runtime';
import { useMergedState, useSetState, useIsomorphicLayoutEffect, useEventListener, useClickAway } from '@react-vant-next/hooks';
import { createNamespace, mergeProps, raf, throttle } from '@react-vant-next/utils';
import cls from 'clsx';
import React, { useState, useImperativeHandle } from 'react';
import FloatingBallItem$1 from './FloatingBallContext.js';
import FloatingBallItem from './FloatingBallItem.js';
import useFloatingTouch from './useFloatingTouch.js';
const TOUCH_DURATION = 0;
const TRANSITION_DURATION = 300;
const DEFAULT_ADSORB = { indent: 0.5, distance: 0 };
const [bem] = createNamespace("floating-ball");
function FloatingBall(_a) {
var _b, _c, _d, _e;
var { ref } = _a, p = __rest(_a, ["ref"]);
const props = mergeProps(p, {
adsorb: DEFAULT_ADSORB,
draggable: true,
menu: {},
offset: {
right: 0,
bottom: "30vh",
},
});
const timer = React.useRef(null);
const [position, setPosition] = useState("bottom right");
const [container, setContainer] = React.useState();
const touch = useFloatingTouch({
target: container,
offset: props.offset,
});
const [active, updateActive] = useMergedState({
value: (_b = props.menu) === null || _b === void 0 ? void 0 : _b.active,
defaultValue: (_c = props.menu) === null || _c === void 0 ? void 0 : _c.defaultActive,
});
const [state, updateState] = useSetState({
indenting: false,
duration: TOUCH_DURATION,
});
// 是否处于滚动缩进中
const isIndenting = state.indenting;
// 是否可拖动
const isDraggable = event => props.draggable
&& event.touches.length === 1
&& container
&& !props.disabled;
// 吸附属性
const adsorb = React.useMemo(() => {
if (typeof props.adsorb === "boolean") {
if (!props.adsorb)
return false;
return DEFAULT_ADSORB;
}
return Object.assign(Object.assign({}, DEFAULT_ADSORB), props.adsorb);
}, [props.adsorb]);
const validMenus = React.useMemo(() => {
var _a, _b;
return (props === null || props === void 0 ? void 0 : props.menu.items) && Array.isArray((_a = props.menu) === null || _a === void 0 ? void 0 : _a.items)
? (_b = props.menu) === null || _b === void 0 ? void 0 : _b.items.filter(Boolean).filter((_, i) => i < 5)
: [];
}, [(_d = props.menu) === null || _d === void 0 ? void 0 : _d.items]);
// 处理菜单
const renderMenus = React.useCallback(() => {
var _a, _b;
if (!validMenus.length)
return null;
const [position1, position2] = position.split(" ");
return (jsx("div", { className: cls(bem("menu", {
[(_a = props.menu) === null || _a === void 0 ? void 0 : _a.direction]: !!((_b = props.menu) === null || _b === void 0 ? void 0 : _b.direction),
[position1]: !!position1,
[position2]: !!position2,
}), `list-${Math.max(validMenus.length, 5)}`), children: validMenus.map((cld, i) => (jsx(FloatingBallItem, { children: cld }, i))) }));
}, [position, (_e = props.menu) === null || _e === void 0 ? void 0 : _e.direction, validMenus]);
// 获取容器在屏幕的哪一侧
const getSideWithRect = () => {
const rect = container.getBoundingClientRect();
const side = rect.left + rect.width / 2 > window.innerWidth / 2 ? "right" : "left";
return { rect, side };
};
// 更新 menu 的位置
const checkMenuPosition = () => {
if (container) {
const { rect: { left, top, width, height }, } = getSideWithRect();
const windowW = window.innerWidth;
const windowH = window.innerHeight;
if (left + width / 2 < windowW / 2) {
position.includes("right")
&& setPosition(state => state.replace("right", "left"));
}
else if (position.includes("left")) {
setPosition(state => state.replace("left", "right"));
}
if (top + height / 2 < windowH / 2) {
position.includes("bottom")
&& setPosition(state => state.replace("bottom", "top"));
}
else if (position.includes("top")) {
setPosition(state => state.replace("top", "bottom"));
}
}
};
const innerChange = (value) => {
var _a, _b;
updateActive(value);
(_b = (_a = props.menu) === null || _a === void 0 ? void 0 : _a.onChange) === null || _b === void 0 ? void 0 : _b.call(_a, value);
};
// 悬浮球点击事件
const handleBaseClick = () => {
// 是否禁用
if (props.disabled || !validMenus.length)
return;
innerChange(!active);
};
// 近边吸附
const checkPosition = () => {
const { side, rect } = getSideWithRect();
if (adsorb) {
const { distance } = adsorb;
const isRightSide = side === "right";
const x = isRightSide
? -distance
: -(window.innerWidth - rect.width) + +distance;
updateState({ duration: TRANSITION_DURATION });
touch.update({ deltaX: x });
}
};
const onTouchStart = (event) => {
if (!isDraggable(event) || isIndenting)
return;
updateState({ duration: TOUCH_DURATION });
touch.start(event);
};
const onTouchMove = (event) => {
if (!isDraggable(event) || isIndenting)
return;
touch.move(event);
if (typeof event.cancelable !== "boolean" || event.cancelable) {
event.preventDefault();
}
if (active)
innerChange(false);
};
const onTouchEnd = () => {
if (isIndenting)
return;
checkPosition();
checkMenuPosition();
};
useIsomorphicLayoutEffect(() => {
if (!active || !touch.ready)
return;
checkMenuPosition();
}, [touch.ready]);
useEventListener("touchmove", onTouchMove, {
target: container,
depends: [
container,
touch.deltaX,
touch.deltaY,
props.disabled,
props.draggable,
],
});
// 点击除悬浮球之外的地方自动收回悬浮球
useClickAway(container, () => {
innerChange(false);
});
// 实例方法
useImperativeHandle(ref, () => ({
open: () => {
if (!validMenus.length)
return;
// viod click away mix
raf(() => innerChange(true));
},
close: () => {
if (!validMenus.length)
return;
// viod click away mix
raf(() => innerChange(false));
},
}));
// 滚动时缩进
useIsomorphicLayoutEffect(() => {
if (props.disabled || !adsorb || !touch.ready)
return;
const onScroll = () => {
const { side, rect } = getSideWithRect();
const { indent, distance } = adsorb;
const isRightSide = side === "right";
const indentPx = rect.width * (isRightSide ? +indent : 1 - +indent);
const offsetX = isRightSide
? +indentPx
: -(window.innerWidth - indentPx);
updateState({
indenting: true,
duration: TRANSITION_DURATION,
});
innerChange(false);
touch.update({ deltaX: offsetX });
if (timer.current)
clearTimeout(timer.current);
timer.current = setTimeout(() => {
const x = isRightSide
? -distance
: -(window.innerWidth - rect.width) + +distance;
updateState({ indenting: false });
touch.update({ deltaX: x });
}, 600);
};
const handle = throttle(() => raf(onScroll), 300);
window.addEventListener("scroll", handle);
return () => window.removeEventListener("scroll", handle);
}, [touch.ready, container, adsorb, props.disabled]);
const indentClassName = React.useMemo(() => {
if (!adsorb)
return "";
if (state.indenting)
return adsorb.indentClassName;
return "";
}, [adsorb, state.indenting]);
const trackStyle = React.useMemo(() => (Object.assign(Object.assign({}, props.style), { transitionDuration: `${state.duration}ms`, transform: `translate3d(${touch.deltaX}px,${touch.deltaY}px, 0)` })), [props.style, state.duration, touch.deltaX, touch.deltaY]);
return (jsx(FloatingBallItem$1, { value: {
close: () => {
var _a, _b;
const closeable = (_b = (_a = props.menu) === null || _a === void 0 ? void 0 : _a.itemClickClose) !== null && _b !== void 0 ? _b : true;
if (closeable)
innerChange(false);
},
}, children: jsxs("div", { className: cls(props.className, indentClassName, bem({ active })), style: trackStyle, ref: setContainer, onTouchStart: onTouchStart, onTouchEnd: onTouchEnd, onTouchCancel: onTouchEnd, children: [renderMenus(), jsx("div", { className: cls(bem("base", {
[props.disabledClassName]: props.disabled,
})), onClick: handleBaseClick, children: typeof props.children === "function"
? props.children({ active, indenting: state.indenting })
: props.children })] }) }));
}
export { FloatingBall as default };
//# sourceMappingURL=FloatingBall.js.map