留言板

-- 鼠标移动卡片实战

课程概要

本课程主要讲解鼠标移动卡片,在日常网页中,鼠标类型事件最为常用,包含鼠标按下、弹上、移动、离开等。通过本课程完成一个留言板,可以通过鼠标拖动里面的留言,大家将对页面的鼠标类型事件的应用有更深入的了解。

主要包含以下功能:

  • 随机分布卡片
  • 点击卡片可以拖动

实现原理

当点击某项卡片时候,把当前鼠标的位置,以及卡片的位置存储起来。当鼠标滑动的时候,根据当前计算鼠标于之前位置的距离计算出卡片的偏移值,然后设置卡片的位置让其发生移动。

知识点

本课程涉及到的主要知识点有:

  • Math.floor

向下舍入

Math.floor(4.6) // 4 
  • Math.random

返回大于等于0小于1的随机数

Math.random()  // 0.5892502788505454
  • 其他 Math 相关方法
Math.PI //3.141592653589793 ( π 的值 )
Math.ceil(4.3)  // 5 (向上舍入)
Math.round(4.5) // 5 (四舍五入)
  • event.pageX、event.pageY

在绑定鼠标事件的回调函数中的事件对象,我们可以获取到当前鼠标距离文档的位置

let element = document.getElementById('element-id'');

实现步骤

  1. HTML & CSS
  2. 构建单例对象PAGE
  3. 随机添加内容
  4. 鼠标点击存储数据并开锁
  5. 鼠标移动根据前后数据移动卡片
  6. 鼠标回弹清除数据并关锁

HTML & CSS

  1. HTML
<div style="height: 400px;"></div>
<div class="card-list-contaienr" id="card-list">
    <div class="card-item">haha</div>
</div>
<div style="height: 400px;"></div>
  1. CSS
*{
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
.card-list-contaienr{
  position: relative;
  width: 800px;
  height: 400px;
  background: #f5f5f5;
  margin: 0 auto;
  overflow: hidden;
}
.card-item{
  position: absolute;
  top: 50px;
  left: 50px;
  width: 100px;
  height: 100px;
  background-color: #ccc;
  padding: 10px;
  color: #fff;
}

构建单例对象PAGE

  1. 构建 PAGE , 及 PAGE.init, PAGE.bind
  2. 定义 PAGE.data
  • backgroundColors 卡片背景色
  • defaultDatas 卡片内容数据
  • itemWidth 卡片宽度
  • itemHeight 卡片高度
  • paddingOffset 卡片移动距离边缘的距离
  • zIndex: 卡片层级
  • item: 当前点击/移动的卡片元素
  • itemOffsetTop 卡片距离顶部的距离
  • itemOffsetLeft 卡片距离左部的距离
  • pageX 鼠标点击X轴位置
  • pageY 鼠标点击Y轴位置
  • isLock 事件锁
  1. 调用 PAGE.init 方法
let cardList = [{name:'Jax'},{name:'Joye'},{name:'Jimmy'},{name:'Jay'}];
let backgroundColors = ['#f50','#2db7f5','#87d068','#108ee9'];
const PAGE = {
  data: {
    backgroundColors: backgroundColors,
    defaultDatas: cardList,
    itemWidth: 100,
    itemHeight: 100,
    paddingOffset: 50,
    zIndex: 0,
    item: null,
    itemOffsetTop: null,
    itemOffsetLeft: null,
    pageX: null,
    pageY: null,
    isLock: true,
  },
  init: function() {
    this.bind();
  },
  bind: function() {

  }
}

PAGE.init();

随机添加内容

  1. 创建 PAGE.setDefaultData 方法用于设置所有卡片
  2. 创建 PAGE.addCart 方法用于设置单个卡片
  3. 在 PAGE.init 中调用 PAGE.setDefaultData
  4. 在 PAGE.setDefaultData 中,获取 PAGE.data.defaultDatas 循环调用 PAGE.addCart
  5. 在 PAGE.addCart 中,获取 card-list 卡片容器元素的高度、宽度
  6. 从 PAGE.data 中获取卡片项目的宽度、高度、以及距离边框的距离值
  7. 根据容器、项目、距边框距离计算出卡片距离顶部以及左部的最大偏移值
  8. 创建 PAGE.randomBetween 随机数方法,接受两个参数,返回两参数之间的随机数
  9. 调用 PAGE.randomBetween 获取距离容器边缘顶部、左部偏移值的随机数
  10. 设置 PAGE.data.zIndex 增加一级,并获取增加后的值
  11. 根据 zIndex 除余按顺序获取背景色的值
  12. 创建新的卡片元素 card-item 并为其赋值,设置 className 及样式属性
  13. 把新创建的元素添加到 card-list 中
let cardList = [{name:'Jax'},{name:'Joye'},{name:'Jimmy'},{name:'Jay'}];
let backgroundColors = ['#f50','#2db7f5','#87d068','#108ee9'];
const PAGE = {
  data: {
    backgroundColors: backgroundColors,
    defaultDatas: cardList,
    itemWidth: 100,
    itemHeight: 100,
    paddingOffset: 50,
    zIndex: 0,
    item: null,
    itemOffsetTop: null,
    itemOffsetLeft: null,
    pageX: null,
    pageY: null,
    isLock: true,
  },
  init: function() {
    3.PAGE.init 中调用 PAGE.setDefaultData
    this.setDefaultData();
    this.bind();
  },
  bind: function() {

  },
  // 1. 创建 PAGE.setDefaultData 方法用于设置所有卡片
  setDefaultData: function() {
    // 4. 获取 PAGE.data.defaultDatas 循环调用 PAGE.addCart
    PAGE.data.defaultDatas.forEach( data => PAGE.addCart(data.name));
  },
  // 2. 创建 PAGE.addCart 方法用于设置单个卡片
  addCart: function() {
    // 5. 在 PAGE.addCart 中,获取 card-list 卡片容器元素的高度、宽度
    let cardList = document.getElementById('card-list');
    let containerWidth = cardList.offsetWidth;
    let containerHeight = cardList.offsetHeight;
    // 6. 从 PAGE.data 中获取卡片项目的宽度、高度、以及距离边框的距离值
    let itemWidth  = PAGE.data.itemWidth;
    let itemHeight = PAGE.data.itemHeight;
    let paddingOffset = PAGE.data.paddingOffset;
    // 7. 根据容器、项目、距边框距离计算出卡片距离顶部以及左部的最大偏移值
    let maxWidth  = containerWidth - itemWidth - paddingOffset;
    let maxHeight = containerHeight - itemHeight - paddingOffset;
    // 9. 调用 PAGE.randomBetween 获取距离容器边缘顶部、左部偏移值的随机数
    let randomTop = PAGE.randomBetween(paddingOffset,maxHeight);
    let randomLeft = PAGE.randomBetween(paddingOffset,maxWidth);
    // 10. 设置 PAGE.data.zIndex 增加一级,并获取增加后的值
    let zIndex = ++PAGE.data.zIndex;
    // 11. 根据 zIndex 除余按顺序获取背景色的值
    let backgroundColors = PAGE.data.backgroundColors;
    let backgroundColor = backgroundColors[zIndex%backgroundColors.length];
    // 12. 创建新的卡片元素 card-item 并为其赋值,设置 className 及样式属性
    let cartItem = document.createElement('div');
    cartItem.setAttribute('class', 'card-item');
    cartItem.innerText = name;
    let styleStr = `
        z-index:${zIndex};
        background:${backgroundColor};
        top:${randomTop}px;
        left:${randomLeft}px;`;
    cartItem.setAttribute('style', styleStr);
    // 13. 把新创建的元素添加到 card-list 中
    cardList.appendChild(cartItem);
  },
  // 8. 创建 PAGE.randomBetween 随机数方法,接受两个参数,返回两参数之间的随机数
  randomBetween:function(min,max){
    return Math.floor(Math.random() * (max - min) + min);
  }
}

PAGE.init();

鼠标点击存储数据并开锁

  1. 添加 PAGE.handleMouseDown 方法
  2. 获取当前点击的元素,为其层级设置到当前最高
  3. 获取当前元素内容距离容器顶部、左部的偏移量、以及鼠标点击的XY坐标轴数据存储到 PAGE.data 中
  4. 存储当前点击元素
  5. 开锁
  6. 添加 PAGE.onEventLister 委托绑定方法
  7. 在 PAGE.bind 中委托 card-list 绑定点击 card-item 触发 PAGE.handleMouseDown
const PAGE = {
  ...,
  bind: function() {
    // 7. 在 PAGE.bind 中委托 card-list 绑定点击 card-item 触发 PAGE.handleMouseDown
    let cartList = document.getElementById('card-list');
    this.onEventLister(cartList, 'mousedown', 'card-item', this.handleMouseDown);
  },
  // 6. 添加 PAGE.onEventLister 委托绑定方法
  onEventLister: function(parentNode,action,childClassName,callback) {
    parentNode.addEventListener(action,function(e){
      e.target.className.indexOf(childClassName) >= 0 && callback(e);
    })
  },
  // 1. 添加 PAGE.handleMouseDown 方法
  handleMouseDown: function(e) {
    // 2. 获取当前点击的元素,为其层级设置到当前最高
    let item = e.target;
    item.style.zIndex = ++ PAGE.data.zIndex;
    // 3. 获取当前元素内容距离容器顶部、左部的偏移量、以及鼠标点击的XY坐标轴数据
    // 存储到 PAGE.data 中
    PAGE.data.itemOffsetTop = item.offsetTop;
    PAGE.data.itemOffsetLeft = item.offsetLeft;
    PAGE.data.pageX = e.pageX;
    PAGE.data.pageY = e.pageY;
    // 4. 存储当前点击元素
    PAGE.data.item = item;
    // 5. 开锁
    PAGE.data.isLock = false;
  },
  ...
}

PAGE.init();

鼠标移动根据前后数据移动卡片

  1. 定义 PAGE.handleMouseMove 当鼠标移动触发的事件
  2. 如果当前锁为开的状态执行以下代码
  3. 获取容器、卡片项目的宽高和距离容器边缘的偏移量计算出最大值
  4. 根据当前鼠标坐标于存储鼠标坐标对比计算出偏移量计算出卡片的距离边缘偏移量
  5. 设置偏移量的最大和最小值,最大不超过最大值,最少不少于边缘值。
  6. 为存储的元素设置偏移值
  7. 在 PAGE.bind 为 window 绑定鼠标移动事件触发 PAGE.handleMouseMove
const PAGE = {
  ...,
  bind: function() {
    let cartList = document.getElementById('card-list');
    this.onEventLister(cartList, 'mousedown', 'card-item', this.handleMouseDown);
    // 7. 为 window 绑定鼠标移动事件触发 PAGE.handleMouseMove
    window.addEventListener('mousemove',this.handleMouseMove);
  },
  ...,
  // 1. 定义 PAGE.handleMouseMove 当鼠标移动触发的事件
  handleMouseMove: function(e) {
    // 2. 如果当前锁为开的状态执行以下代码
    if(!PAGE.data.isLock){
      // 3. 获取容器、卡片项目的宽高和距离容器边缘的偏移量计算出最大值
      let cardList = document.getElementById('card-list');
      let containerWidth = cardList.offsetWidth;
      let containerHeight = cardList.offsetHeight;
      let itemWidth  = PAGE.data.itemWidth;
      let itemHeight = PAGE.data.itemHeight;
      let paddingOffset = PAGE.data.paddingOffset;
      // 4. 根据当前鼠标坐标于存储鼠标坐标对比计算出偏移量计算出卡片的距离边缘偏移量
      let maxWidth  = containerWidth - itemWidth - paddingOffset;
      let maxHeight = containerHeight - itemHeight - paddingOffset;
      let translateX = e.pageX - PAGE.data.pageX + PAGE.data.itemOffsetLeft;
      let translateY = e.pageY - PAGE.data.pageY + PAGE.data.itemOffsetTop;
      // 5. 设置偏移量的最大和最小值,最大不超过最大值,最少不少于边缘值。
      translateX = translateX > maxWidth ? maxWidth : translateX;
      translateY = translateY > maxHeight ? maxHeight : translateY;
      translateX = translateX < paddingOffset ? paddingOffset : translateX;
      translateY = translateY < paddingOffset ? paddingOffset : translateY;
      // 6. 为存储的元素设置偏移值
      PAGE.data.item.style.left = translateX + 'px';
      PAGE.data.item.style.top = translateY + 'px';
    }
  },
  ...
}

PAGE.init();

鼠标回弹清除数据并关锁

  1. 定义 PAGE.handleMouseUp 当鼠标回弹触发的事件
  2. 设置关锁,这样鼠标移动时候的事件就不会触发了
  3. 在 PAGE.bind 为 window 绑定鼠标回弹触发 PAGE.handleMouseMove
const PAGE = {
  ...,
  bind: function() {
    let cartList = document.getElementById('card-list');
    this.onEventLister(cartList, 'mousedown', 'card-item', this.handleMouseDown);
    window.addEventListener('mousemove',this.handleMouseMove);
    // 3. 为 window 绑定鼠标回弹触发 PAGE.handleMouseMove
    window.addEventListener('mouseup',this.handleMouseUp);
  },
  ...,
  // 1. 定义 PAGE.handleMouseUp 当鼠标回弹触发的事件
  handleMouseUp: function() {
    // 2. 设置关锁,这样鼠标移动时候的事件就不会触发了
    PAGE.data.isLock = true
  },
  ...
}

PAGE.init();

代码示例

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>MessageBorad</title>
  <style type="text/css">
    *{
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }
    .card-list-contaienr{
      position: relative;
      width: 800px;
      height: 400px;
      background: #f5f5f5;
      margin: 0 auto;
      overflow: hidden;
    }
    .card-item{
      position: absolute;
      width: 100px;
      height: 100px;
      background-color: #ccc;
      padding: 10px;
      color: #fff;
    }
  </style>
</head>
<body>
  <div style="height: 400px;"></div>
  <div class="card-list-contaienr" id="card-list">
      <div class="card-item" style="top:50px;left: 50px;">haha</div>
  </div>
  <div style="height: 400px;"></div>
  <script src="https://lib.baomitu.com/jquery/3.3.1/jquery.min.js"></script>
  <script type="text/javascript">
    // 定义卡片内容
    let cardList = [{name:'Jax'},{name:'Joye'},{name:'Jimmy'},{name:'Jay'}];
    let backgroundColors = ['#f50','#2db7f5','#87d068','#108ee9'];
    const PAGE = {
      data: {
        backgroundColors: backgroundColors,
        defaultDatas: cardList,
        itemWidth: 100,
        itemHeight: 100,
        paddingOffset: 50,
        zIndex: 0,
        item: null,
        itemOffsetTop: null,
        itemOffsetLeft: null,
        pageX: null,
        pageY: null,
        isLock: true,
      },
      init: function() {
        this.setDefaultData();
        this.bind();
      },
      bind: function() {
        let cartList = document.getElementById('card-list');
        this.onEventLister(cartList, 'mousedown', 'card-item', this.handleMouseDown);
        window.addEventListener('mousemove',this.handleMouseMove);
        window.addEventListener('mouseup',this.handleMouseUp);
      },
      onEventLister: function(parentNode,action,childClassName,callback) {
        parentNode.addEventListener(action,function(e){
          e.target.className.indexOf(childClassName) >= 0 && callback(e);
        })
      },
      handleMouseDown: function(e) {
        let item = e.target;
        item.style.zIndex = ++ PAGE.data.zIndex;
        PAGE.data.itemOffsetTop = item.offsetTop;
        PAGE.data.itemOffsetLeft = item.offsetLeft;
        PAGE.data.pageX = e.pageX;
        PAGE.data.pageY = e.pageY;
        PAGE.data.item = item;
        PAGE.data.isLock = false;
      },
      handleMouseMove: function(e) {
        if(!PAGE.data.isLock){
          let cardList = document.getElementById('card-list');
          let containerWidth = cardList.offsetWidth;
          let containerHeight = cardList.offsetHeight;
          let itemWidth  = PAGE.data.itemWidth;
          let itemHeight = PAGE.data.itemHeight;
          let paddingOffset = PAGE.data.paddingOffset;
          let maxWidth  = containerWidth - itemWidth - paddingOffset;
          let maxHeight = containerHeight - itemHeight - paddingOffset;
          let translateX = e.pageX - PAGE.data.pageX + PAGE.data.itemOffsetLeft;
          let translateY = e.pageY - PAGE.data.pageY + PAGE.data.itemOffsetTop;
          translateX = translateX > maxWidth ? maxWidth : translateX;
          translateY = translateY > maxHeight ? maxHeight : translateY;
          translateX = translateX < paddingOffset ? paddingOffset : translateX;
          translateY = translateY < paddingOffset ? paddingOffset : translateY;
          PAGE.data.item.style.left = translateX + 'px';
          PAGE.data.item.style.top = translateY + 'px';
        }
      },
      handleMouseUp: function() {
        PAGE.data.isLock = true
      },
      setDefaultData: function() {
        PAGE.data.defaultDatas.forEach( data => PAGE.addCart(data.name));
      },
      addCart: function(name) {
        let cardList = document.getElementById('card-list');
        let containerWidth = cardList.offsetWidth;
        let containerHeight = cardList.offsetHeight;
        let itemWidth  = PAGE.data.itemWidth;
        let itemHeight = PAGE.data.itemHeight;
        let paddingOffset = PAGE.data.paddingOffset;
        let maxWidth  = containerWidth - itemWidth - paddingOffset;
        let maxHeight = containerHeight - itemHeight - paddingOffset;
        let randomTop = PAGE.randomBetween(paddingOffset,maxHeight);
        let randomLeft = PAGE.randomBetween(paddingOffset,maxWidth);
        let zIndex = ++PAGE.data.zIndex;
        let backgroundColors = PAGE.data.backgroundColors;
        let backgroundColor = backgroundColors[zIndex%backgroundColors.length];
        let cartItem = document.createElement('div');
        cartItem.setAttribute('class', 'card-item');
        cartItem.innerText = name;
        let styleStr = `
          z-index:${zIndex};
          background:${backgroundColor};
          top:${randomTop}px;
          left:${randomLeft}px;`;
        cartItem.setAttribute('style', styleStr);
        cardList.appendChild(cartItem);
      },
      randomBetween:function(min,max){
        return Math.floor(Math.random() * (max - min) + min);
      }
    }

    PAGE.init();
  </script>
</body>
</html>