Vue通用自定义Resize组件

1.背景

项目基于Vue+Element-UI,新接到的需求是点击按钮能够从右侧弹出一个页面,并能控制该弹出页面的宽度。在Element-UI中有一个抽屉(Drawer)组件,支持点击从浏览器侧边弹出,但不能实现宽度的调整,于是自己想到实现一个通用的Resize组件,以后如果要使用时,可以直接引入。

2.具体实现

<template>
  <div
    :class="`resizer-${position}`"
    :style="{
      width: width,
      height: height,
      position: 'absolute',
      right: right,
      left: left,
      top: top,
      bottom: bottom,
      cursor: cursor,
    }"
    :id="uuid"
  />
</template>

<script>
// 定义每个方位的边框样式
const directionMap = {
  top: {
    width: '100%',
    height: '5px',
    top: 0,
    right: 'unset',
    bottom: 'unset',
    left: 0,
    cursor: 'ns-resize',
  },
  right: {
    width: '5px',
    height: '100%',
    top: 0,
    right: 0,
    bottom: 'unset',
    left: 'unset',
    cursor: 'ew-resize',
  },
  bottom: {
    width: '100%',
    height: '5px',
    top: 'unset',
    right: 'unset',
    bottom: 0,
    left: 0,
    cursor: 'ns-resize',
  },
  left: {
    width: '5px',
    height: '100%',
    top: 0,
    right: 'unset',
    bottom: 'unset',
    left: 0,
    cursor: 'ew-resize',
  },
  topLeft: {
    width: '12px',
    height: '12px',
    top: 0,
    right: 'unset',
    bottom: 'unset',
    left: 0,
    cursor: 'nw-resize',
  },
  topRight: {
    width: '12px',
    height: '12px',
    top: 0,
    right: 0,
    bottom: 'unset',
    left: 'unset',
    cursor: 'ne-resize',
  },
  bottomLeft: {
    width: '12px',
    height: '12px',
    top: 'unset',
    right: 'unset',
    bottom: 0,
    left: 0,
    cursor: 'sw-resize',
  },
  bottomRight: {
    width: '12px',
    height: '12px',
    top: 'unset',
    right: 0,
    bottom: 0,
    left: 'unset',
    cursor: 'se-resize',
  },
};
export default {
  props: {
    // 用于指定需要调整的节点
    target: {
      type: String,
    },
    // 用于指明组件是哪个方向的边框
    position: {
      type: String,
    },
    // 是否手动控制边框调整
    // 这里因为ElementUI的drawer通过size控制了宽度,需要手动计算size,也许是我不会用
    manual: {
      type: Boolean,
      default: false,
    },
    // 用于禁用某个方向的resize,防止组件自动调整宽度,影响布局
    disabledAxis: {
      type: String,
      default: '',
    },
  },
  computed: {
    isLeft() {
      return ['left', 'topLeft', 'bottomLeft'].includes(this.position);
    },
    isRight() {
      return ['right', 'topRight', 'bottomRight'].includes(this.position);
    },
    isAxisX() {
      return this.isLeft || this.isRight;
    },
    isTop() {
      return ['top', 'topLeft', 'topRight'].includes(this.position);
    },
    isBottom() {
      return ['bottom', 'bottomLeft', 'bottomRight'].includes(this.position);
    },
    isAxisY() {
      return this.isTop || this.isBottom;
    },
    isCorner() {
      return ['topLeft', 'topRight', 'bottomLeft', 'bottomRight'].includes(this.position);
    },
    enabledAxisX() {
      return !['x', 'all'].includes(this.disabledAxis);
    },
    enabledAxisY() {
      return !['y', 'all'].includes(this.disabledAxis);
    },
  },
  data() {
    return {
      ...directionMap[this.position],

      offset: {
        positionOnClickX: undefined,
        positionOnClickY: undefined,
      },
      targetDom: undefined,
      domData: {
        width: undefined,
        height: undefined,
        isPercentWidth: undefined,
        isPercentHeight: undefined,
      },
      modal: undefined,
      // 用于保证获取节点时,获取到的是组件内部的div
      uuid: this._.uniqueId('drawer-resizer'),
    };
  },
  mounted() {
    this.modal = this.generateModal();
    this.resizer = document.querySelector(`#${this.uuid}`);
    this.targetDom = document.querySelector(this.target);
    this.addListener();
  },
  methods: {
    generateModal() {
      const modal = document.createElement('div');
      modal.style.width = '100vw';
      modal.style.height = '100vh';
      modal.style.position = 'fixed';
      modal.style.top = '0';
      modal.style.left = '0';
      modal.style.cursor = this.cursor;
      modal.style['z-index'] = 9998;
      return modal;
    },
    mouseMoveHanlder(e) {
      this.calculateOffset(e);
    },
    mouseDownHanlder(e) {
      this.offset.positionOnClickX = e.clientX;
      this.offset.positionOnClickY = e.clientY;
      if (this.targetDom) {
        this.domData.width = this.targetDom.style.width.replace(/%|px/g, '');
        this.domData.height = this.targetDom.style.height.replace(/%|px/g, '');
      }
      this.resizer.style['z-index'] = 9999;
      this.modal.addEventListener('mousemove', this.mouseMoveHanlder);
      document.body.appendChild(this.modal);
    },
    mouseUpHanlder(e) {
      this.calculateOffset(e);
      this.resizer.style['z-index'] = undefined;
      this.offset = {
        positionOnClickX: undefined,
        positionOnClickY: undefined,
      };
      this.modal.removeEventListener('mousemove', this.mouseMoveHanlder);
      document.body.removeChild(this.modal);
    },
    calculateOffset(e) {
      const {
        positionOnClickX,
        positionOnClickY,
      } = this.offset;

      // 处理X轴
      // 当前鼠标的坐标
      const coordX = e.clientX;
      // 浏览器的宽度
      const windowWidth = window.innerWidth;
      let offsetX;
      // 鼠标移动的距离
      offsetX = positionOnClickX - coordX;
      // 向右移动鼠标,offsetX为负值,处理右边框的resize时,应取反
      if (this.isRight) {
        offsetX = -offsetX;
      }
      // 鼠标移动的距离与浏览器宽度的百分比
      const offsetPercentX = (offsetX / windowWidth) * 100;
      // 当前鼠标相对浏览器最左侧的百分比
      let coordPercentX;
      coordPercentX = (coordX / windowWidth) * 100;
      if (this.isLeft) {
        coordPercentX = 100 - coordPercentX;
      }


      let offsetY;
      let coordPercentY;
      const windowHeight = window.innerHeight;
      const coordY = e.clientY;
      offsetY = positionOnClickY - coordY;
      coordPercentY = (coordY / windowHeight) * 100;
      // 处理Y轴
      if (this.isBottom) {
        offsetY = -offsetY;
      }
      const offsetPercentY = (offsetY / windowHeight) * 100;

      const {
        position,
      } = this;

      const offset = {
        coordX,
        coordPercentX,
        offsetX,
        offsetPercentX,

        coordY,
        coordPercentY,
        offsetY,
        offsetPercentY,

        position,

      };
      if (this.target) {
        this.handlerDomStyle(offset);
      }
      if (this.manual) {
        this.$emit('offset', offset);
      }
    },
    handlerDomStyle(offset) {
      if (this.targetDom) {
        const {
          offsetX,
          offsetPercentX,
          offsetY,
          offsetPercentY,
        } = offset;
        const isPercentWidth = this.targetDom.style.width.includes('%');
        const isPercentHeight = this.targetDom.style.height.includes('%');
        const {
          width,
          height,
        } = this.domData;
        if (this.isAxisX && this.enabledAxisX) {
          if (isPercentWidth) {
            this.targetDom.style.width = `${Number(width) + Number(offsetPercentX)}%`;
          } else {
            this.targetDom.style.width = `${Number(width) + Number(offsetX)}px`;
          }
        }
        if (this.isAxisY && this.enabledAxisY) {
          if (isPercentHeight) {
            this.targetDom.style.height = `${Number(height) + Number(offsetPercentY)}%`;
          } else {
            this.targetDom.style.height = `${Number(height) + Number(offsetY)}px`;
          }
        }
      }
    },
    addListener() {
      if (this.resizer) {
        this.resizer.addEventListener('mousedown', this.mouseDownHanlder);
        this.modal.addEventListener('mouseup', this.mouseUpHanlder);
      }
    },
  },
};
</script>

3.改进思路

后来在npm上搜索了一下resize相关的包,发现可以将整个边框封入一个div中,直接做一个可以调整的div出来,也是一个不错的想法。另外,在target对象获取的时候,还可以使用$parent来获取父节点,这样是不是会减少dom获取带来的性能开销?还有组件销毁时,需要移除相应的事件。

一直在注重功能的实现,欠缺性能优化相关的知识点,如果你有好的意见,或发现其中的不足,还望指出。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,014评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,796评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,484评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,830评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,946评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,114评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,182评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,927评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,369评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,678评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,832评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,533评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,166评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,885评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,128评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,659评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,738评论 2 351

推荐阅读更多精彩内容

  • 前端开发面试题 面试题目: 根据你的等级和职位的变化,入门级到专家级,广度和深度都会有所增加。 题目类型: 理论知...
    怡宝丶阅读 2,574评论 0 7
  • ¥开启¥ 【iAPP实现进入界面执行逐一显】 〖2017-08-25 15:22:14〗 《//首先开一个线程,因...
    小菜c阅读 6,377评论 0 17
  • 找到fullcalendar.js, 找到代码为 isRTL:false,这句话 输入以下几句 monthName...
    迷你小小白阅读 1,646评论 0 1
  • 项目规范 1 2 3 4 5 6 8 9 10 11 12 13 在使用 axios 进行 ajax 请求的时候,...
    当然我没扯淡阅读 822评论 0 1
  • 摩纳哥王妃:女主人公在国家与事业的抉择中,勇敢的懂得了爱的真谛,真正的爱是责任,的确,就如影片中讲述的那样,每个人...
    大秦子阅读 685评论 0 0