import React, { useRef, forwardRef, useState, useEffect, useMemo } from "react"; import classNames from "classnames"; import { ReferenceObject } from "popper.js"; import { BubbleContent } from "../bubble"; import { Popover } from "../popover"; import { getPrecision } from "../_util/get-precision"; import { useConfig } from "../_util/config-context"; import { SliderTrackProps } from "./SliderProps"; import { noop } from "../_util/noop"; import { flatten } from "../_util/flatten"; import { SliderHandle } from "./SliderHandle"; import { StyledProps } from "../_type"; const defaultFormatter = number => number; const sort = (arr: [number, number]) => arr.sort((a, b) => a - b) as [number, number]; const getEvent = ( event: React.MouseEvent | React.TouchEvent | MouseEvent | TouchEvent ) => { if ("targetTouches" in event) { return event.targetTouches[0]; } return event; }; export const SliderTrack = React.forwardRef(function SliderTrack( { value, min = 0, max, step = 1, range, multiRangeMode = false, dotValues = [], rangeMode, marks = [], disabled, markValueOnly, enableTrackTip, after, vertical, onUpdate = noop, onChange = noop, tipFormatter = defaultFormatter, className, ...props }: SliderTrackProps, ref: React.Ref ) { const { classPrefix } = useConfig(); const offsetKey = vertical ? "bottom" : "left"; const boundingKey = vertical ? "height" : "width"; const [startValue, endValue] = value; const precision = useMemo(() => getPrecision(step), [step]); // 定位相关 hooks const startRef = useRef(null); const endRef = useRef(null); // 滑轨相关 hooks const scheduleRef = useRef(null); const mouseElement = useRef< ReferenceObject & { scrollTop: number; scrollLeft: number } >(null); const [mousePosition, setMousePosition] = useState(null); // 滑块相关 hooks const positionRef = useRef(0); const dragOffsetRef = useRef(0); const innerScheduleRef = useRef(null); const [dragging, setDragging] = useState(false); // const [hovering, setHovering] = useState(false); useEffect(() => removeDocumentEvents, []); // eslint-disable-line react-hooks/exhaustive-deps // 拖动过程中值 const [interactiveValue, setInteractiveValue] = useState(null); const interactivePosRef = useRef<"start" | "end">(null); const displayDotValues = getValidDotAndRanges(dotValues, range).filter( value => typeof value === "number" ); const total = Math.abs(max - min); const proportions = !rangeMode ? (getProportions() as any) : [sort(getProportions() as any)]; function calcProportion(num: number) { return ((num - min) / total) * 100; } function getProportions(): SliderTrackProps["range"] { if (!rangeMode && !multiRangeMode) { return [ [ 0, calcProportion( interactiveValue === null ? endValue : interactiveValue ), ], ]; } // 多段模式 if (!rangeMode && multiRangeMode) { const realRanges = getValidDotAndRanges(dotValues, range) .filter(item => Array.isArray(item)) .map(item => { return [calcProportion(item[0]), calcProportion(item[1])] as any; }); return realRanges; } // rangeMode if (interactiveValue === null || interactivePosRef.current === null) { return [calcProportion(startValue), calcProportion(endValue)]; } return interactivePosRef.current === "start" ? [calcProportion(interactiveValue), calcProportion(endValue)] : [calcProportion(startValue), calcProportion(interactiveValue)]; } function addDocumentMouseEvents() { document.addEventListener("mousemove", handleMouseMove); document.addEventListener("touchmove", handleMouseMove, { passive: false }); document.addEventListener("mouseup", handleMouseUp); document.addEventListener("touchend", handleMouseUp, { passive: false }); } function removeDocumentEvents() { document.removeEventListener("mousemove", handleMouseMove); document.removeEventListener("touchmove", handleMouseMove); document.removeEventListener("mouseup", handleMouseUp); document.removeEventListener("touchend", handleMouseUp); } function handleMouseUp(event) { event.preventDefault(); setDragging(false); removeDocumentEvents(); const value = getValueByPosition(positionRef.current); if (!rangeMode) { onChange([0, value], { event }); } else { onChange( sort( interactivePosRef.current === "start" ? [value, endValue] : [startValue, value] ), { event } ); } if (rangeMode) { interactivePosRef.current = null; } setInteractiveValue(null); } function handleMouseMove(event: MouseEvent | TouchEvent) { if (event.cancelable) { event.preventDefault(); } if (disabled) { return; } const position = vertical ? getEvent(event).pageY : getEvent(event).pageX; const value = getValueByPosition(position - dragOffsetRef.current); if (getValueByPosition(positionRef.current) !== value) { positionRef.current = position; setInteractiveValue(value); onUpdate( sort( interactivePosRef.current === "start" ? [value, endValue] : [startValue, value] ) ); } if (innerScheduleRef.current) { innerScheduleRef.current(); } } function handleMouseDown(event: React.MouseEvent | React.TouchEvent) { if (disabled) { return; } const position = vertical ? getEvent(event).pageY : getEvent(event).pageX; const value = getValueByPosition(position); if (rangeMode) { interactivePosRef.current = Math.abs(value - startValue) < Math.abs(value - endValue) ? "start" : "end"; } setInteractiveValue(value); onUpdate( sort( interactivePosRef.current === "start" ? [value, endValue] : [startValue, value] ) ); // 滑块拖动相关事件 setDragging(true); positionRef.current = position; dragOffsetRef.current = 0; removeDocumentEvents(); addDocumentMouseEvents(); } function handleMouseLeave(event: React.MouseEvent | React.TouchEvent) { event.preventDefault(); setMousePosition(null); } function getStartPos(): number { if (!startRef.current) { return 0; } const coords = startRef.current.getBoundingClientRect(); return ( (vertical ? window.pageYOffset : window.pageXOffset) + coords[offsetKey] ); } function getEndPos(): number { if (!endRef.current) { return 0; } const coords = endRef.current.getBoundingClientRect(); return ( (vertical ? window.pageYOffset : window.pageXOffset) + coords[offsetKey] ); } function getValidDotAndRanges( dotValues: SliderTrackProps["dotValues"], range: SliderTrackProps["range"] ) { return [...dotValues, ...range] .sort((a, b) => { if (Array.isArray(a) && Array.isArray(b)) { return a[0] > b[0] || (a[0] === b[0] && a[1] < b[1]) ? 1 : -1; } if (Array.isArray(a) && typeof b === "number") { return a[0] > b ? 1 : -1; } if (typeof a === "number" && Array.isArray(b)) { return a > b[0] ? 1 : -1; } return a > b ? 1 : -1; }) .reduce((pre, current) => { const len = pre.length; if (len === 0) { return [current]; } const tailItem = pre[len - 1]; // 最后元素和遍历元素均为数组 if (Array.isArray(tailItem) && Array.isArray(current)) { if (tailItem[1] >= current[0] && tailItem[1] <= current[1]) { pre[len - 1] = [tailItem[0], current[1]]; return pre; } if (tailItem[1] >= current[0] && tailItem[1] > current[1]) { return pre; } pre.push(current); } if (Array.isArray(tailItem) && typeof current === "number") { if (tailItem[1] >= current) { return pre; } pre.push(current); } if (typeof tailItem === "number" && Array.isArray(current)) { if (tailItem === current[0]) { pre[len - 1] = current; return pre; } pre.push(current); } if (typeof tailItem === "number" && typeof current === "number") { if (tailItem !== current) { pre.push(current); } } return pre; }, []); } function getValueByPosition(position: number) { // 垂直时上下相反 const [start, end] = sort([getStartPos(), getEndPos()]); const posRange = end - start; if (position < start) { position = start; // eslint-disable-line no-param-reassign } if (position > end) { position = end; // eslint-disable-line no-param-reassign } const proportion = (vertical ? end - position : position - start) / posRange; return getValueByProportion(proportion); } function getValueByProportion(proportion: number): number { const pow = 10 ** precision; let value = proportion * total; value -= ((value * pow) % (step * pow)) / pow; value += min; if (markValueOnly && marks.length) { // eslint-disable-next-line prefer-destructuring value = marks.reduce( (prev, cur) => Math.abs(cur.value - value) < Math.abs(prev.value - value) ? cur : prev, marks[0] ).value; } // 点模式的range可为多段 if (!rangeMode) { const valueRanges = getValidDotAndRanges(dotValues, range); const isInRangeOrDotValues = valueRanges.reduce((pre, current) => { return ( pre || (Array.isArray(current) && value >= current[0] && value <= current[1]) || (typeof current === "number" && current === value) ); }, false); if (!isInRangeOrDotValues && valueRanges.length) { value = flatten(valueRanges).reduce((pre, current) => { return Math.abs(value - pre) < Math.abs(value - current) ? pre : current; }); } } else { const [allowedStart, allowedEnd] = range as [number, number]; if (value < allowedStart) { value = allowedStart; } if (value > allowedEnd) { value = allowedEnd; } } return parseFloat(value.toFixed(precision)); } function handleMouseMoveForPopover( event: React.MouseEvent | React.TouchEvent ) { if (disabled) { return; } const position = vertical ? getEvent(event).pageY : getEvent(event).pageX; setMousePosition(position); setMouseElement(event.nativeEvent); if (scheduleRef.current) { scheduleRef.current(); } } function setMouseElement(event: MouseEvent | TouchEvent) { const clientX = Math.round(getEvent(event).clientX); const clientY = startRef.current.getBoundingClientRect().top; mouseElement.current = { clientWidth: 0, clientHeight: 0, getBoundingClientRect: () => ({ left: clientX, top: clientY, right: clientX, bottom: clientY, width: 0, height: 0, } as any), // IE10 下 Popper 读取这两个值计算 scrollTop: 0, scrollLeft: 0, }; } return (
{ scheduleRef.current = scheduleUpdate; const tips = tipFormatter(getValueByPosition(mousePosition)); return ( tips !== null && !!mousePosition && ( {tips} ) ); }} >
e.preventDefault()} > {/* 滑轨 */}
{proportions && proportions.map((proportion, index) => { return (
); })}
{/* 滑块 */} {rangeMode && ( )} {!rangeMode && multiRangeMode && displayDotValues.length > 0 && displayDotValues.map(value => (
))} {/* 标尺 */}
{marks .filter(({ value }) => value >= min && value <= max) .map(({ value, label }) => ( {label || value} ))}
{after}
); }); SliderTrack.displayName = "SliderTrack"; const StartFlag = forwardRef(({ style }, ref) => ( )); StartFlag.displayName = "SliderTrackStartFlag"; const EndFlag = forwardRef(({ style }, ref) => ( )); EndFlag.displayName = "SliderTrackEndFlag";