原生JS 實現框選功能(Vue)

效果

image

分析該過程,可拆分紅兩個步驟:css

  • 鼠標框選一段區域
  • 判斷框選區域包含的 checkbox,進行對應處理

HTML 和 CSS 佈局

該佈局基於 element-ui,一個父容器 box,裏面一個 mask div,一個 el-checkbox-group 塊。其中父容器設置 position: relative; 子 mask 容器設置 position: absolute;而且其寬、高、偏移值根據鼠標當前位置動態計算vue

<div class="box" >
  <div class="mask" v-show="is_show_mask" :style="'width:'+mask_width+'left:'+mask_left+'height:'+mask_height+'top:'+mask_top">
  </div>
  <el-checkbox-group>
    <el-checkbox>
    </el-checkbox>
  </el-checkbox-group>
</div>

.box {
  width: 800px;
  margin: 20px auto;
  position: relative;
  overflow: hidden;
  user-select: none;
  .mask {
    position: absolute;
    background: #409eff;
    opacity: 0.4;
  }
}

JS 實現框選

該部分邏輯實際上可拆分爲 4 個步驟:node

  • 鼠標按下 mousedown,記錄當前起點座標 start_x,start_y,並綁定 mousemove、mouseup 事件
  • 鼠標移動 mousemove,實時更新終點座標 end_x,end_y,便可框選矩形大小、位置
  • 鼠標鬆開 mouseup,移除 mousemove、mouseup 事件
  • 根據框選矩形大小、位置,計算在範圍內的 checkbox 數量,進行對應處理

1.給 box 綁定 mousedown 事件typescript

<div class="box" @mousedown="handleMouseDown"></div>

handleMouseDown(event: any) {
  this.start_x = event.clientX;
  this.start_y = event.clientY;

  document.body.addEventListener('mousemove', this.handleMouseMove);
  document.body.addEventListener('mouseup', this.handleMouseUp);
}

2.mousemove 事件,比較簡單,只是更新 end_x,end_y 座標element-ui

handleMouseMove(event: MouseEvent) {
  this.end_x = event.clientX;
  this.end_y = event.clientY;
}

3.mouseup 事件,移除 mousemove、mouseup 事件,並調用判斷方法bash

handleMouseUp() {
  document.body.removeEventListener('mousemove', this.handleMouseMove);
  document.body.removeEventListener('mouseup', this.handleMouseUp);
  this.handleDomSelect();
  this.resSetXY();
}

4.處理框選邏輯dom

難點是如何判斷元素是否被框選住ide

問題可轉化爲 框選矩形是否與 checkbox 矩形 相交或者包含在內,即兩矩形是否相交或者存在包含關係佈局

假定矩形 A1 左上角座標爲 (x1,y1);矩形寬度爲 width1,高度爲 height1;ui

假定矩形 A2 左上角座標爲 (x2,y2);矩形寬度爲 width2,高度爲 height2;

畫圖分析,只看水平方向:
x軸方向不相交

x軸方向相交

由圖能夠得出,x 方向上:
令 maxX = Math.max(x1 + width1, x2 + width2)
令 minX = Math.max(x1, x2)

若相交或包含則必知足:maxX - minX <= width1 + width2;

同理能夠容易獲得 y 軸相交的判斷

使用 Element.getBoundingClientRect()獲取 dom 元素位置信息

Element.getBoundingClientRect() 方法返回元素的大小及其相對於視口的位置。返回值是一個 DOMRect 對象,返回的結果是包含完整元素的最小矩形,而且擁有 left, top, right, bottom, x, y, width, 和 height 這幾個以像素爲單位的只讀屬性用於描述整個邊框。

該部分邏輯以下,比較簡單

collide(rect1: any, rect2: any): boolean {
  const maxX: number = Math.max(rect1.x + rect1.width, rect2.x + rect2.width);
  const maxY: number = Math.max(rect1.y + rect1.height, rect2.y + rect2.height);
  const minX: number = Math.min(rect1.x, rect2.x);
  const minY: number = Math.min(rect1.y, rect2.y);
  if (maxX - minX <= rect1.width + rect2.width && maxY - minY <= rect1.height + rect2.height) {
    return true;
  } else {
    return false;
  }
}

難點已經攻破,遍歷 checkbox 集合,每一個 checkbox 都執行上面的矩形相交判斷,並進行相應的勾選處理,此處再也不多累述

完整代碼

<template>
  <div class="box" @mousedown="handleMouseDown">
    <div class="mask" v-show="is_show_mask" :style="'width:'+mask_width+'left:'+mask_left+'height:'+mask_height+'top:'+mask_top">
    </div>
    <el-checkbox-group v-model="select_list">
      <el-checkbox v-for="(item,index) in data_list" :label="item.city_id" :key="index">
        <p @click.stop.prevent> {{item.city_name}}</p>
      </el-checkbox>
    </el-checkbox-group>
  </div>
</template>

<script lang="ts">
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Vue, Component } from 'vue-property-decorator';

interface City {
  city_id: number;
  city_name: string;
}

@Component({})
export default class Debug extends Vue {
  data_list: Array<City> = [
    { city_id: 35, city_name: '香港特別行政區' },
    { city_id: 34, city_name: '北京市' },
    { city_id: 33, city_name: '江蘇省' },
    { city_id: 32, city_name: '吉林省' },
    { city_id: 31, city_name: '內蒙古自治區' },
  ];

  select_list: Array<number> = [];

  is_show_mask = false;

  box_screen_left = 0;

  box_screen_top = 0;

  start_x = 0;

  start_y = 0;

  end_x = 0;

  end_y = 0;

  get mask_width() {
    return `${Math.abs(this.end_x - this.start_x)}px;`;
  }

  get mask_height() {
    return `${Math.abs(this.end_y - this.start_y)}px;`;
  }

  get mask_left() {
    return `${Math.min(this.start_x, this.end_x) - this.box_screen_left}px;`;
  }

  get mask_top() {
    return `${Math.min(this.start_y, this.end_y) - this.box_screen_top}px;`;
  }

  mounted() {
    const dom_box: any = document.querySelector('.box');
    this.box_screen_left = dom_box.getBoundingClientRect().left;
    this.box_screen_top = dom_box.getBoundingClientRect().top;
  }

  /* 方法 */
  handleMouseDown(event: any) {
    if (event.target.tagName === 'SPAN') return false;

    this.is_show_mask = true;
    this.start_x = event.clientX;
    this.start_y = event.clientY;
    this.end_x = event.clientX;
    this.end_y = event.clientY;

    document.body.addEventListener('mousemove', this.handleMouseMove);
    document.body.addEventListener('mouseup', this.handleMouseUp);
  }

  handleMouseMove(event: MouseEvent) {
    this.end_x = event.clientX;
    this.end_y = event.clientY;
  }

  handleMouseUp() {
    document.body.removeEventListener('mousemove', this.handleMouseMove);
    document.body.removeEventListener('mouseup', this.handleMouseUp);
    this.is_show_mask = false;
    this.handleDomSelect();
    this.resSetXY();
  }

  handleDomSelect() {
    const dom_mask: any = window.document.querySelector('.mask');
    const rect_select = dom_mask.getClientRects()[0];

    const add_list: Array<number> = [];
    const del_list: Array<number> = [];
    document.querySelectorAll('.el-checkbox-group .el-checkbox').forEach((node, index) => {
      const rects = node.getClientRects()[0];
      if (this.collide(rects, rect_select) === true) {
        if (this.select_list.includes(this.data_list[index].city_id)) {
          del_list.push(this.data_list[index].city_id);
        } else {
          add_list.push(this.data_list[index].city_id);
        }
      }
    });
    this.select_list = this.select_list.concat(add_list).filter((item: number) => !del_list.includes(item));
  }

  // eslint-disable-next-line class-methods-use-this
  collide(rect1: any, rect2: any): boolean {
    const maxX: number = Math.max(rect1.x + rect1.width, rect2.x + rect2.width);
    const maxY: number = Math.max(rect1.y + rect1.height, rect2.y + rect2.height);
    const minX: number = Math.min(rect1.x, rect2.x);
    const minY: number = Math.min(rect1.y, rect2.y);
    if (maxX - minX <= rect1.width + rect2.width && maxY - minY <= rect1.height + rect2.height) {
      return true;
    } else {
      return false;
    }
  }

  resSetXY() {
    this.start_x = 0;
    this.start_y = 0;
    this.end_x = 0;
    this.end_y = 0;
  }
}
</script>

<style lang="scss" scoped>
.box {
  width: 800px;
  margin: 20px auto;
  position: relative;
  overflow: hidden;
  user-select: none;
  .mask {
    position: absolute;
    background: #409eff;
    opacity: 0.4;
  }
  .el-checkbox-group {
    overflow: auto;
    .el-checkbox {
      width: 137px;
      margin: 0 20px 10px 0;
      float: left;
      ::v-deep .el-checkbox__input {
        padding-top: 3px;
        vertical-align: top;
      }
      p {
        width: 110px;
        white-space: pre-wrap;
      }
    }
  }
}
</style>

END

相關文章
相關標籤/搜索