UNPKG

@react-vant-next/campaign

Version:

React Mobile UI Components based on Vant UI - Next Generation

241 lines (238 loc) 9.85 kB
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