Jelajahi Sumber

新增自动滑动

方正 7 tahun lalu
induk
melakukan
0f3ac78bac
2 mengubah file dengan 533 tambahan dan 0 penghapusan
  1. 467 0
      src/lib/dragger/base.tsx
  2. 66 0
      src/lib/dragger/type.ts

+ 467 - 0
src/lib/dragger/base.tsx

@@ -0,0 +1,467 @@
+import * as React from "react";
+import { int, innerHeight, innerWidth, outerHeight, outerWidth, parseBounds } from '../utils';
+import { DraggerProps } from "./type";
+
+const doc = document
+
+type ListenType = 'drag' | 'resize';
+
+var speed = 0;
+var timer: any = 0;
+var autoScrolling = false;
+var autoY = 0;
+var currentHeight = 0;
+
+const ScollPostion = () => {
+    var t = 0, l = 0, w = 0, h = 0;
+    if (document.documentElement && document.documentElement.scrollTop) {
+        t = document.documentElement.scrollTop;
+        l = document.documentElement.scrollLeft;
+        w = document.documentElement.scrollWidth;
+        h = document.documentElement.scrollHeight;
+    } else if (document.body) {
+        t = document.body.scrollTop;
+        l = document.body.scrollLeft;
+        w = document.body.scrollWidth;
+        h = document.body.scrollHeight;
+    }
+    return {
+        top: t,
+        left: l,
+        width: w,
+        height: h
+    };
+}
+
+export class Dragger extends React.Component<DraggerProps, {}> {
+    parent: any;
+    self: any;
+
+    ListenMove = (type: ListenType) => {
+        /** 保证用户在移动元素的时候不会选择到元素内部的东西 */
+        doc.body.style.userSelect = 'none'
+        if (type === 'resize') {
+            doc.addEventListener('mouseup', this.onResizeEnd);
+            doc.addEventListener('mousemove', this.onResizing);
+            return
+        }
+        doc.addEventListener('mousemove', this.move);
+        doc.addEventListener('mouseup', this.onDragEnd);
+    }
+    unListenMove = (type: ListenType) => {
+        doc.body.style.userSelect = ''
+        if (type === 'resize') {
+            doc.removeEventListener('mousemove', this.onResizing)
+            doc.removeEventListener('mouseup', this.onResizeEnd)
+            return
+        }
+        doc.removeEventListener('mousemove', this.move)
+        doc.removeEventListener('mouseup', this.onDragEnd)
+    }
+
+    constructor(props: DraggerProps) {
+        super(props)
+        this.parent = null;
+        this.self = null;
+    }
+    /**
+     * 初始变量设置
+     */
+    static defaultProps = {
+        allowX: true,
+        allowY: true,
+        isUserMove: true
+    }
+
+    state = {
+        /** x轴位移,单位是px */
+        x: this.props.x || 0,
+
+        /** y轴位移,单位是px */
+        y: this.props.y || 0,
+
+        /**鼠标点击元素的原始位置,单位是px */
+        originX: 0,
+        originY: 0,
+
+        isUserMove: true,
+
+        /**已经移动的位移,单位是px */
+        lastX: 0,
+        lastY: 0,
+
+        /**堆叠的层级 */
+        zIndex: 1,
+
+        w: this.props.w || 0,
+        h: this.props.h || 0,
+
+        lastW: 0,
+        lastH: 0
+    }
+
+
+
+    move = (event: any) => {
+
+        let { lastX, lastY } = this.state;
+        /*  event.client - this.state.origin 表示的是移动的距离,
+        *   elX表示的是原来已经有的位移
+        */
+        let deltaX, deltaY;
+        if (event.type.indexOf('mouse') >= 0) {
+            deltaX = (event as MouseEvent).clientX - this.state.originX + lastX
+            deltaY = (event as MouseEvent).clientY - this.state.originY + lastY
+        } else {
+            deltaX = (event as TouchEvent).touches[0].clientX - this.state.originX + lastX
+            deltaY = (event as TouchEvent).touches[0].clientY - this.state.originY + lastY
+        }
+
+        const { bounds } = this.props
+        if (bounds) {
+            /**
+            * 如果用户指定一个边界,那么在这里处理
+            */
+            let NewBounds = typeof bounds !== 'string' ? parseBounds(bounds) : bounds;
+
+            /**
+             * 网格式移动范围设定,永远移动 n 的倍数
+             * 注意:设定移动范围的时候,一定要在判断bounds之前,否则会造成bounds不对齐
+             */
+            const { grid } = this.props
+            if (Array.isArray(grid) && grid.length === 2) {
+                deltaX = Math.round(deltaX / grid[0]) * grid[0]
+                deltaY = Math.round(deltaY / grid[1]) * grid[1]
+            }
+
+            if (this.props.bounds === 'parent') {
+                NewBounds = {
+                    left: int(this.parent.style.paddingLeft) + int(this.self.style.marginLeft) - this.self.offsetLeft,
+                    top: int(this.parent.style.paddingTop) + int(this.self.style.marginTop) - this.self.offsetTop,
+                    right: innerWidth(this.parent) - outerWidth(this.self) - this.self.offsetLeft +
+                        int(this.parent.style.paddingRight) - int(this.self.style.marginRight),
+                    bottom: innerHeight(this.parent) - outerHeight(this.self) - this.self.offsetTop +
+                        int(this.parent.style.paddingBottom) - int(this.self.style.marginBottom)
+                }
+            }
+
+            /**
+             * 保证不超出右边界和底部
+             * keep element right and bot can not cross the bounds
+             */
+            if (NewBounds !== 'parent') deltaX = Math.min(deltaX, NewBounds.right)
+            if (NewBounds !== 'parent') deltaY = Math.min(deltaY, NewBounds.bottom)
+
+
+            /**
+             * 保证不超出左边和上边
+             * keep element left and top can not cross the bounds
+             */
+            if (NewBounds !== 'parent') deltaX = Math.max(deltaX, NewBounds.left)
+            if (NewBounds !== 'parent') deltaY = Math.max(deltaY, NewBounds.top)
+        }
+
+
+        /**如果设置了x,y限制 */
+        deltaX = this.props.allowX ? deltaX : 0
+        deltaY = this.props.allowY ? deltaY : 0
+
+        /**
+         * 调整手感 
+         * 无论是向上移动还是向下移动,全部都是根据重力中心
+         * */
+        const height = (this.refs['dragger'] as HTMLDivElement).getClientRects()[0].height;
+        const upNdown = this.state.y - deltaY;
+        const fixY = deltaY + (upNdown >= 0 ? 0 : height / 2);
+        if (!autoScrolling) {
+            /**移动时回调,用于外部控制 */
+
+            if (this.props.onMove) this.props.onMove(event, deltaX, fixY)
+
+            // console.log('del', deltaY);
+            this.setState({
+                x: deltaX,
+                y: deltaY,
+            })
+        }
+        if (autoScrolling && speed < 0) {
+            clearInterval(timer);
+            timer = 0;
+            this.setState({//恢复的时候,要设定所有数据是当前的数据
+                y: this.state.y,
+                lastY: this.state.y,
+                originY: event.clientY,
+                originX: event.clientX
+            })
+            if (this.props.onMove) this.props.onMove(event, this.state.x, this.state.y)
+            window.scrollTo(0, ScollPostion().top);
+            autoScrolling = false;
+            speed = 0;
+            autoY = 0;
+        }
+        if (autoScrolling) {
+
+            const check = event.clientY - this.state.originY;
+            speed = check / 3;
+
+            this.setState({
+                x: deltaX,
+                lastY: this.state.y,
+                originY: event.clientY,
+                originX: event.clientX
+            })
+        }
+
+        if (!autoScrolling && event.clientY > 650) {
+            const totalHeight = document.body.clientHeight - window.innerHeight
+            console.log(totalHeight)
+            autoScrolling = true;
+            currentHeight = ScollPostion().top;
+            timer = setInterval(() => {
+                autoY += speed;
+                if (this.state.y > document.body.clientHeight - 200) clearInterval(timer);
+                window.scrollTo(0, currentHeight + autoY);
+
+                this.setState({
+                    y: this.state.y + speed,
+                    lastY: this.state.y + speed,
+                    originY: event.clientY,
+                    originX: event.clientX
+                })
+                if (this.props.onMove) this.props.onMove(event, this.state.x, this.state.y + speed)
+            })
+        }
+
+    }
+
+    onDragStart = (event: any) => {
+
+
+        if (this.props.handle) {
+            if (event.target.id !== 'dragact-handle') return
+        }
+
+        /**
+         * 把监听事件的回掉函数,绑定在document上
+         * 当设置边界的时候,用户鼠标会离开元素的范围
+         * 绑定在document上可以使得其依旧能够监听
+         * 如果绑定在元素上,则鼠标离开元素,就不会再被监听了
+         */
+        if (event.type.indexOf('mouse') >= 0) {
+            this.ListenMove('drag');
+        } else {
+            doc.addEventListener('touchmove', this.move)
+            doc.addEventListener('touchend', this.onDragEnd)
+        }
+
+        if (this.props.bounds === 'parent' &&
+            /**为了让 这段代码不会重复执行 */
+            (typeof this.parent === 'undefined' || this.parent === null)) {
+            /**
+             * 在这里我们将父节点缓存下来,保证当用户鼠标离开拖拽区域时,我们仍然能获取到父节点
+             * what we do here is 
+             * making sure that we still can retrieve our parent when user's mouse left this node.
+             */
+
+            this.parent = (event as any).currentTarget.offsetParent //todo
+
+            /**
+             * 我们自己
+             * ourself
+             */
+            this.self = event.currentTarget
+        }
+
+        this.props.onDragStart && this.props.onDragStart(this.state.x, this.state.y)
+
+
+        let originX, originY;
+        if (event.type.indexOf('mouse') >= 0) {
+            originX = (event as MouseEvent).clientX
+            originY = (event as MouseEvent).clientY
+        } else {
+            originX = (event as TouchEvent).touches[0].clientX
+            originY = (event as TouchEvent).touches[0].clientY
+        }
+
+        // console.log(originY, this.state.y)
+
+
+        this.setState({
+            originX: originX,
+            originY: originY,
+            lastX: this.state.x,
+            lastY: this.state.y,
+            zIndex: 10
+        })
+    }
+
+    onDragEnd = (event: any) => {
+        /** 取消用户选择限制,用户可以重新选择 */
+        clearInterval(timer);
+        speed = 0;
+        autoScrolling = false;
+        autoY = 0;
+        currentHeight = 0;
+
+
+        this.parent = null
+        this.self = null
+
+        if (event.type.indexOf('mouse') >= 0) {
+            this.unListenMove('drag');
+        } else {
+            doc.removeEventListener('touchmove', this.move)
+            doc.removeEventListener('touchend', this.onDragEnd)
+        }
+
+        this.setState({
+            zIndex: 1
+        })
+
+        this.props.onDragEnd && this.props.onDragEnd(event, this.state.x, this.state.y)
+    }
+
+    onResizeStart = (event: React.MouseEvent<HTMLSpanElement>) => {
+        /** 保证用户在移动元素的时候不会选择到元素内部的东西 */
+
+        this.ListenMove('resize');
+
+        let originX, originY;
+        originX = event.clientX
+        originY = event.clientY
+
+        this.props.onResizeStart && this.props.onResizeStart(event, this.state.w, this.state.h);
+
+        this.setState({
+            originX: originX,
+            originY: originY,
+            zIndex: 2,
+            lastW: this.state.w,
+            lastH: this.state.h
+        })
+        event.stopPropagation();
+    }
+    onResizing = (event: any) => {
+        /*  event.client - this.state.origin 表示的是移动的距离,
+        *   elX表示的是原来已经有的位移
+        */
+
+        let deltaX, deltaY;
+        if (event.type.indexOf('mouse') >= 0) {
+            deltaX = (event as MouseEvent).clientX - this.state.originX
+            deltaY = (event as MouseEvent).clientY - this.state.originY
+        } else {
+            deltaX = (event as TouchEvent).touches[0].clientX - this.state.originX
+            deltaY = (event as TouchEvent).touches[0].clientY - this.state.originY
+        }
+        /**移动时回调,用于外部控制 */
+
+        this.props.onResizing && this.props.onResizing(event, this.state.w, this.state.h);
+
+        this.setState({
+            w: deltaX + this.state.lastW,
+            h: deltaY + this.state.lastH
+        })
+
+    }
+    onResizeEnd = (event: any) => {
+        this.unListenMove('resize');
+        this.props.onResizeEnd && this.props.onResizeEnd(event, this.state.w, this.state.h);
+    }
+    componentWillReceiveProps(nextProps: DraggerProps) {
+        /**
+         * 外部props 改变的时候更新元素的内部位置
+         * 这个api设计其实很不好
+         * 以后可能会修改掉
+         */
+        const { isUserMove } = nextProps
+        if (!isUserMove) {
+
+            if (typeof nextProps.x === 'number' &&
+                typeof nextProps.y === 'number') {
+                this.setState({
+                    x: nextProps.x,
+                    y: nextProps.y,
+                    lastX: nextProps.x,
+                    lastY: nextProps.y,
+                    w: nextProps.w,
+                    h: nextProps.h
+                })
+            }
+        }
+    }
+
+    mixin = () => {
+        var dragMix = {};
+        if (this.props.canDrag === void 666 || this.props.canDrag === true) {
+            dragMix = {
+                onMouseDown: this.onDragStart,
+                onTouchStart: this.onDragStart,
+                onTouchEnd: this.onDragEnd,
+                onMouseUp: this.onDragEnd
+            }
+        }
+
+        var resizeMix = {}
+        if (this.props.canResize === void 666 || this.props.canDrag === true) {
+            resizeMix = {
+                onMouseDown: this.onResizeStart,
+                onMouseUp: this.onResizeEnd
+            }
+        }
+
+        return {
+            dragMix, resizeMix
+        };
+    }
+
+    render() {
+
+        var { x, y, w, h } = this.state
+        var { style, className, canResize } = this.props
+        if (!this.props.isUserMove) {
+            /**当外部设置其props的x,y初始属性的时候,我们在这里设置元素的初始位移 */
+            x = this.props.x ? this.props.x : 0;
+            y = this.props.y ? this.props.y : 0;
+            if (style) {
+                w = style.width ? style.width : w;
+                h = style.height ? style.height : h;
+            }
+        }
+        if (style) {
+            //使得初始化的时候,不会有从0-1缩放动画
+            w = w === 0 ? style.width : w;
+            h = h === 0 ? style.height : h;
+        }
+        const { dragMix, resizeMix } = this.mixin();
+
+        /**主要是为了让用户定义自己的className去修改css */
+        const fixedClassName = typeof className === 'undefined' ? '' : className + ' '
+        return (
+            <div className={`${fixedClassName}WrapDragger`}
+                ref={'dragger'}
+                style={{
+                    ...style,
+                    touchAction: 'none!important',
+                    transform: `translate(${x}px,${y}px)`,
+                    width: w,
+                    height: h,
+                }}
+                {...dragMix}
+            >
+                {this.props.children ? React.Children.only(this.props.children) : null}
+                {canResize !== false ?
+                    <span
+                        {...resizeMix}
+                        style={{
+                            position: 'absolute',
+                            width: 10, height: 10, right: 2, bottom: 2, cursor: 'se-resize',
+                            borderRight: '2px solid rgba(15,15,15,0.2)',
+                            borderBottom: '2px solid rgba(15,15,15,0.2)'
+                        }}
+                    /> : null}
+            </div>
+        )
+    }
+}

+ 66 - 0
src/lib/dragger/type.ts

@@ -0,0 +1,66 @@
+export interface Bound {
+    left: number,
+    top: number,
+    right: number,
+    bottom: number
+}
+
+export interface DraggerProps {
+
+    className?: string;
+
+    /**
+    * 给予元素一个x,y的初始位置,单位是px
+    */
+    x?: number,
+    y?: number,
+
+    /** 
+     * 拖动范围限制
+     * 如果不规定范围,那么子元素就可以随意拖动不受限制
+     * 1.可以提供自定义的范围限制
+     * 2.也可以提供父类为边框的范围限制(string === parent)
+     */
+    bounds?: Bound | 'parent',
+
+    /**
+         * 以网格的方式移动,每次移动并不是平滑的移动
+         * [20,30],鼠标x轴方向移动了20 px ,y方向移动了30 px,整个子元素才会移动
+         */
+    grid?: [number, number],
+
+
+    /**只允许移动x轴 */
+    /**只允许移动y轴 */
+    allowX?: Boolean,
+    allowY?: Boolean,
+
+
+    /**
+    * 是否由用户移动
+    * 可能是通过外部props改变
+    */
+    isUserMove?: Boolean,
+
+    /**
+     * 生命周期回调
+     */
+    onDragStart?: (x: number, y: number) => void,
+    onMove?: (event: MouseEvent | TouchEvent, x: number, y: number) => void,
+    onDragEnd?: (event: MouseEvent | TouchEvent, x: number, y: number) => void,
+
+    onResizeStart?: (event: any, x: number, y: number) => void,
+    onResizing?: (event: MouseEvent | TouchEvent, x: number, y: number) => void
+    onResizeEnd?: (event: MouseEvent | TouchEvent, x: number, y: number) => void
+
+    style?: React.CSSProperties,
+
+    w?: number,
+    h?: number,
+
+    handle?: Boolean;
+
+    canDrag?: Boolean;
+
+    canResize?: Boolean;
+}