JS的拖拽和拖放api

67 阅读1分钟
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        body,ul{
            margin: 0;
            padding: 0;
        }
        ul,ul li{
            list-style-type: none;
        }
        body{
            height: 1800px;
        }
        #list{
            /* height: 150px; */
            /* overflow-x: hidden; */
            width: 400px;
            margin: 0 auto;
            margin-top: 100px;
            border: 1px dashed blue;
            padding: 10px;
            box-sizing: border-box;
        }

        .item{
            padding: 10px;
        }
        .item:not(:last-child){
            margin-bottom: 10px;
        }

        .ani {
            animation: ani 3s ease-in-out 1s both;
        }

        @keyframes ani {
            from {
                color: red;
            }
            to{
                color: blue;
            }
        }

    </style>
</head>
<body>
    <div id="list">
        <ul>
            <li id="item1" class="item" draggable="true" style="background: linear-gradient(to right, red, blue);">1水电费水电费,收到烦死了,乐山大佛</li>
            <li class="item" draggable="true" style="background-color: rgb(18, 206, 96);">2</li>
            <li class="item" draggable="true" style="background-color: rgb(163, 68, 226);">3</li>
            <li class="item" draggable="true" style="background-color: rgb(241, 135, 73);">4</li>
            <li class="item" draggable="true" style="background-color: rgb(56, 129, 240);">5</li>
        </ul>
    </div>
    <script>
        /**
         * 理解目标元素
         * 拖拽事件中 dragStart dragEnd drag。event.target就是被拖拽元素
         * 拖放事件中 dragEnter dragOver drop。event.target就是拖放区域元素
         * 
        */

        /**
         * 为什么dragover要阻止默认事件?
         * dragover事件在拖动元素经过拖放元素时触发,这时候需要阻止默认行为以允许目标元素接收拖放元素。
         * 为什么drop要阻止默认事件?
         * 因为如果拖的是系统文夹,需要在dragover和drop事件中,同时阻止游览器打开文件夹,这一默认行为
         * */
        const items = document.querySelectorAll('.item');

        const list = document.querySelector('#list');

        Function.prototype.expandMethod = function(name,handle){
            !this.prototype[name] && (this.prototype[name] = handle)
            return this;
        }

        /**
         * 触底事件
         * 如果是整体页面的滚动,containerEl请指定为html,
         * 其它情况下,为滚动容器本身
         * 
        */
        HTMLElement.expandMethod('onReachBottom', function(callback, options = { threshold: 1 }) {
            let containerEl = this;
            // 是否是html标签
            const isHtmlTag = (el) => Object.prototype.toString.call(el) === '[object HTMLHtmlElement]'
            const throttle = (handle, wait = 500) => {
                let timer ;
                return function(...args){
                    //第一个计时器入栈后,只有等到其被执行完成后,才能执行其他计时器
                    if(timer){return}
                    timer = setTimeout(()=>{
                        handle.apply(this, args);
                        timer = null;
                    }, wait);
                }
            }
            //onReachBottom的副作用,就是load事件会产生的一个影响 
            window.addEventListener('load', () => {
                const reachbottom = new CustomEvent("reachbottom", {
                    detail:{
                        ...options
                    }
                });
                containerEl.addEventListener("reachbottom", (e) => {
                    let target = e.target;
                    const threshold = e.detail.threshold;
                    if(target === window) {
                        target = document.documentElement;
                    }
                    // 触底条件
                    if((target.clientHeight + target.scrollTop) * threshold >= target.scrollHeight) {
                        typeof callback === 'function' && callback.call(e.target, e);
                    }
                });
                (isHtmlTag(containerEl) ? window : containerEl).addEventListener('scroll', throttle((e) => containerEl.dispatchEvent(reachbottom)));
            })
        })

        /**
         * refEl: 参考元素 
        */
        HTMLElement.expandMethod('getRect', function(refEl) {
            const el = this;
            const elRect = el.getBoundingClientRect();
            const refElRect = refEl.getBoundingClientRect();
            return {
                top: elRect.top - refElRect.top,
                bottom: elRect.bottom - refElRect.top,
                left: elRect.left - refElRect.left,
                right: elRect.right - refElRect.left,
                height: elRect.height,
                width: elRect.width,
                // x: elRect.left - refElRect.left,
                // y: elRect.right - refElRect.left
            }
        });
        // const rect = document.querySelector('#item1').getRect(document.querySelector('#list'));

        function each(arr, callback) {
            const keys = Object.keys(arr);
            for(var i = 0; i < keys.length; i++) { 
                if(callback.call(arr, arr[i], i) === false) {
                    break;
                }
            }
        }
        
        function on(els, eventName, callback) {
            if(!Reflect.has(els, 'length')) {
                els = [els]
            }
            each([...els], (el) => el.addEventListener(eventName, callback))
        }

        function insertAfter(newNode, targetNode) {
            let parent = targetNode.parentNode;
            if(parent.lastElementChild === targetNode) {
                target.parentNode.appendChild(newNode)
            }else {
                parent.insertBefore(newNode, targetNode.nextElementSibling)
            }
        }
        // vue2中的Flip原理,实现动画       
        // https://aerotwist.com/blog/flip-your-animations/
        // animation api的 polyfill: https://github.com/web-animations/web-animations-js
        function ani(els) {
            const oldRects = [...els].map((el) => el.getBoundingClientRect())
            return {
                play() {
                    const lastRects = [...els].map((el) => el.getBoundingClientRect());
                    each(lastRects, (lastRect, i) => {
                        const y = oldRects[i].top - lastRect.top;
                        if(y) {
                            const ani = els[i].animate([
                                {
                                    transform: `translateY(${y}px)`
                                },
                                {
                                    transform: `translateY(0)`
                                }
                            ], {
                                duration: 300,
                                easing: 'cubic-bezier(0,0,0.32,1)',
                            })
                            ani.finished.then(() => oldRects[i] = els[i].getBoundingClientRect())
                        }
                    })
                }
            }
        }

        /**
         * 判断Y轴方向
         * lastY: 最新的y轴坐标。鼠标第一次按下时clientY
         * */ 
        function initDirectionY(lastY) {
            let direction = 'none' // 方向
            return function(curY) {
                curY - lastY > 0 && (direction = 'down')
                curY - lastY < 0 && (direction = 'up')
                // 更新Y坐标
                lastY = curY;
                return direction
            }
        }

        let source = null; // 源元素(当前被拖拽的元素)
        let target = null; // 目标元素(拖放区元素)
        let getDirectionY; // 获取y轴方向的方法
        let directionY = 'none';
        let flip = null;
        // 利用事件委托,子元素触发事件,冒泡到父元素上
        on(list, 'dragstart', (e) => {
            console.log('dragestart', e);
            source = e.target;
            e.dataTransfer.effectAllowed = 'move';
            getDirectionY = initDirectionY(e.clientY);
            flip = ani(items);
        })

        // 拖拽时的方法
        // on(items, 'drag', (e) => {});

        on(list, 'dragend', (e) => {
            console.log('dragend', e);
        });

        on(list, 'dragenter', (e) => {
            e.preventDefault();
            if(e.target === source || e.target.getAttribute('draggable') !== "true") {return}
            directionY = getDirectionY(e.clientY)
            target = e.target;
            if(directionY === 'up') { // 向上
                target.insertAdjacentElement('beforebegin', source);
                // target.parentNode.insertBefore(source, target)
            }else {
                // insertAfter(source, target)
                // 将source新节点,插入到target后面
                target.insertAdjacentElement('afterend', source);
            }
            flip.play();
        });

        on(list, 'dragover', (e) => {
            // 一定要禁用
            e.preventDefault();
        })
7
        on(list, 'drop', (e) => {
            // 一定要禁用
            e.preventDefault();
            // console.log('drop', e)
        });

        document.querySelector('#list').onReachBottom((e) => {
            // console.log(e)
        });
    </script>
</body>
</html>