vue学习(6)自定义指令详解及常见自定义指令

1、钩子函数

bind : 只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。(常用)

inserted : 被绑定元素插入父节点时调用(保证父节点存在,但不一定已被插入文档中)。(常用)

update : 所在组件的VNode更新时调用,但是可能发生在其子VNode更新之前。 指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新(详细的钩子函数参数下见)。(基本没咋用)

componentUpdated : 指令所在组件的VNode 及其子 VNode 全部更新后调用。(基本没咋用)

unbind : 只调用一次,指令与元素解绑时调用。(常用)

2、钩子函数参数

el : 指令所绑定的元素,可以用来直接操作DOM。(常用)

binding : 一个对象,包含以下属性:(常用)

name : 指令名,不包括 v- 前缀。

value : 指令表达式的最终返回结果

oldValue :指令表达式的最终返回结果前一个值,仅在 update 和 componentUpdated 钩子中可用。
expression :表达式。

arg :传给指令的参数,可选。例如 v-copy:foo 中,参数为 "foo"。

modifiers :一个包含修饰符的对象。例如:v-copy:foo.a 中的a

vnode :Vue 编译生成的虚拟节点。(基本没咋用)

oldVnode :上一个虚拟节点,仅在 update和 componentUpdated 钩子中可用。(基本没咋用)

3、Vue插件机制 install

// directive.js
import directive0 from './directive0'
import directive1 from './directive1'
import directive2 from './directive2'

const install = (Vue) => {
  Vue.directive('directive0', copy)
  Vue.directive('directive1', copy)
  Vue.directive('directive2', copy)
}

export default {
  install
}

// main.js
import directive from './directive'

Vue.use(directive)

4、局部自定义注册

<template>
    <div v-drag-dialog></div>
</template>
export default {
    directives: {
        dragDialog: {
            bind(xxx){}
        }
    }
}

// 或者
import drag from '@/directives/drag'
export default {
    directives: {
        dragDialog: drag
    }
}

5、常见自定义指令

1、复制输入框的值 v-copy(已实测可行

调用:

<template>
      <el-input v-model="url" readonly>
          <template slot="append">
              <el-button icon="el-icon-copy-document" v-copy="url">复制</el-button>
          </template>
      </el-input> 
</template>
<script>
export default {
    data () {
        return {
            url: 'xxx'
        }
    }
}
</script>

代码:

const copy = {
  /**
   * 初始化
   * @param {DOM} el 指令所绑定的元素DOM节点
   * @param {*} value 指令的绑定值 即 v-copy="value" 的value值
   */
  bind(el, { value }) {
      // 给元素赋值一个$value值,即指令绑定的值
      el.$value = value
      el.handler = () => {
          // 如果可复制的值为空的时候,给出提示;
          if (!el.$value) {
              console.log('无复制内容')
              return
          }
          // 动态创建 textarea 标签
          const textarea = document.createElement('textarea');
          // 将该 textarea 设为 readonly 防止 IOS 下自动唤起键盘,同时将 textarea 移除可视区域
          textarea.readOnly = 'readonly';
          textarea.style.position = 'absolute';
          textarea.style.left = '-9999px';
          // 将要copy的值赋值给textarea 标签的value属性
          textarea.value = el.$value;
          // 将textarea 插入到body中
          document.body.appendChild(textarea);
          // 选中值并复制
          textarea.select()
          const result = document.execCommand('Copy');
          if (result) {
              console.log('复制成功');
          }
          document.body.removeChild(textarea);
      }
      // 绑定点击事件,点击的时候copy值
      el.addEventListener('click', el.handler);
  },
  // 当传递进来的值更新的时候触发
  componentUpdated(el, { value }) {
      el.$value = value;
  },
  // 指令与元素解绑的时候,移除事件绑定
  unbind(el) {
      el.removeEventListener('click', el.handler);
  }
}

export default copy;

2、长按屏幕 v-longpress已实测可行

调用:直接v-longpress则默认长按1秒执行函数,当然也可以自己传参,单位为毫秒

<template>
    <div>
        <el-button v-longpress:2000="longpress">长按</el-button>
    </div>
</template>

<script>
export default {
    methods: {
        longpress() {
            // xxx
        }
    }
}
</script>

 代码:

/**
 * 1. 创建一个计时器,n s 后执行函数
 * 2. 当用户按下按钮时触发 mousedown 事件,启动计时器;
 *    用户松开按钮时调用 mouseout 事件。
 * 3. 如果 mouseup 事件 n s内被触发,就清除计时器,当作一个普通的点击事件。
 * 4. 如果 计时器没有在n s内清除,则判定为移除长按,可以执行关联的函数。
 * 5. 在移动端要考虑 touchstart, touchend 事件。
 */
 const longpress = {
  bind (el, binding) {
      if (typeof binding.value !== 'function') {
          throw 'callback must be a function'
      }
      const time = binding.arg || 1000 // n秒后执行,默认1秒
      let pressTimer = null
      // 创建计时器(1s后执行函数)
      let start = e => {
          if (e.type === 'click' && e.button !== 0) return
          if (pressTimer === null) {
              pressTimer = setTimeout(() => {
                  handler()
              }, time)
          }
      }
      // 取消计时器
      let cancel = e => {
          if (pressTimer !== null) {
              clearTimeout(pressTimer)
              pressTimer = null
          }
      }
      // 运行函数
      const handler = e => {
          binding.value()
      }
      // 添加事件监听器
      el.addEventListener('mousedown', start)
      el.addEventListener('touchstart', start)
      // 取消计时器
      el.addEventListener('click', cancel)
      el.addEventListener('mouseout', cancel)
      el.addEventListener('touchend', cancel)
      el.addEventListener('touchcancel', cancel)
  },
  // 当传递进来的值更新的时候触发
  componentUpdated(el, { value }) {
      el.$value = value
  },
  // 指令与元素解绑的时候,移除事件绑定
  unbind(el) {
      el.removeEventListener('click', el.handler)
  }
}

export default longpress

3、 防抖 v-debounce 已实测可行

调用:分为立即/非立即防抖,传参方式不同

<template>
    <div>
        <el-button v-debounce="{ event: debounce, delay: 500, immediate: true}">立即/非立即防抖</el-button>
        <el-button v-debounce="debounce">常规防抖</el-button>
    </div>
</template>

<script>export default {
    methods: {
        debounce() {
            // xxx
        }
    }
}
</script>

代码:

// 在指定的时间段内,多次点击只会执行一次
const debounce = {
  inserted(el ,binding) {
      let timer
      el.addEventListener('click', () => {
          if (timer) {
            clearTimeout(timer)
          }
          // 传值为 v-debounce="{ event: func, delay: 500, immediate: true}"
          if (typeof binding.value !== 'function') {
            const { event, delay = 500, immediate = false } = binding.value
            if (immediate) {
              let now = !timer
              timer = setTimeout(() => {
                timer = null
              }, delay)
              now && event()
            } else {
              timer = setTimeout(() => {
                timer = null
                event()
              }, delay)
            }
          } else {
            // 传值为 v-debounce="func"
            timer = setTimeout(() => {
              binding.value() // 延迟执行回调方法
            }, 1000)
          }
      })
  }
}
export default debounce

4、 禁止输入框发表情及特殊字符 v-emoji目前暂不知道怎么用,无法测试

调用:

<template>
    <div>
        <input v-model="name" v-emoji />
    </div>
</template>
<script>
export default {
    data () {
        return {
            name: ''
        }
    }
}
</script>

代码:


let findEle = (parent, type) => {
    return parent.tagName.toLowerCase() === type ? parent : parent.querySelector(type)
}
const trigger = (el, type) => {
    // 创建一个指定类型的事件
    const e = document.createEvent('HTMLEvents') 
    // 定义事件名为 type
    e.initEvent(type, true, true)
    // 触发对象可以是任何元素或其他事件目标
    el.dispatchEvent(e)
}
const emoji = {
    bind: function (el, binding) {
      // 正则规则可根据需求自定义
      var regRule = /[^\u4E00-\u9FA5|\d|\a-zA-Z|\r\n\s,.?!,。?!…—&$=()-+/*{}[\]]|\s/g
      let $inp = findEle(el, 'input')
      el.$inp = $inp
      $inp.handle = function () {
        let val = $inp.value
        $inp.value = val.replace(regRule, '')
        trigger($inp, 'input')
      }
      $inp.addEventListener('keyup', $inp.handle)
    },
    unbind: function (el) {
      el.$inp.removeEventListener('keyup', el.$inp.handle)
    },
}
  
export default emoji

5、页面水印 v-waterMarker已实测可行

调用:

<template>
    <div style="width: 100%;height:200px" v-copy="{name:'林大大版权所有',color:'rgba(100, 100, 100, 0.4)',width: 100, height: 100}"></div>
</template>

 代码:

/**
 * @param {*} name 文字
 * @param {*} width 文字宽度
 * @param {*} height 文字高度
 * @param {*} color 文字颜色
 */
const waterMarker = {
  bind(el, binding) {
    const { name, width, height, color } = binding.value
    var can = document.createElement('canvas')
    el.appendChild(can)
    can.width = width
    can.height = height
    can.style.display = 'none'
    var cans = can.getContext('2d')
    cans.rotate((-20 * Math.PI) / 180)
    cans.fillStyle = color || 'rgba(180, 180, 180, 0.3)'
    cans.textAlign = 'left'
    cans.textBaseline = 'Middle'
    cans.fillText(name, can.width / 10, can.height / 2)
    el.style.backgroundImage = 'url(' + can.toDataURL('image/png') + ')'
  }
}

export default waterMarker

6、拖拽 v-draggable已实测可行

调用:针对于el-dialog的拖动

<template>
   <el-dialog v-draggable>
   </el-dialog>
</template>

代码:

const draggable = {
  bind(el, binding, vnode) {
    const dialogHeaderEl = el.querySelector('.el-dialog__header') // 点击能拖动的地方
    const dragDom = el.querySelector('.el-dialog') // 被拖动的dom
    dialogHeaderEl.style.cssText += ';cursor:move;'
    dragDom.style.cssText += ';top:0px;'

    // 获取原有属性 ie dom元素.currentStyle 火狐谷歌 window.getComputedStyle(dom元素, null);
    const getStyle = (function() {
      if (window.document.currentStyle) {
        return (dom, attr) => dom.currentStyle[attr]
      } else {
        return (dom, attr) => getComputedStyle(dom, false)[attr]
      }
    })()
    dialogHeaderEl.onmousedown = (e) => {
      // 鼠标按下,计算当前元素距离可视区的距离
      const disX = e.clientX - dialogHeaderEl.offsetLeft
      const disY = e.clientY - dialogHeaderEl.offsetTop

      const dragDomWidth = dragDom.offsetWidth
      const dragDomHeight = dragDom.offsetHeight

      const screenWidth = document.body.clientWidth
      const screenHeight = document.body.clientHeight

      const minDragDomLeft = dragDom.offsetLeft
      const maxDragDomLeft = screenWidth - dragDom.offsetLeft - dragDomWidth

      const minDragDomTop = dragDom.offsetTop
      const maxDragDomTop = screenHeight - dragDom.offsetTop - dragDomHeight

      // 获取到的值带px 正则匹配替换
      let styL = getStyle(dragDom, 'left')
      let styT = getStyle(dragDom, 'top')

      if (styL.includes('%')) {
        styL = +document.body.clientWidth * (+styL.replace(/\%/g, '') / 100)
        styT = +document.body.clientHeight * (+styT.replace(/\%/g, '') / 100)
      } else {
        styL = +styL.replace(/\px/g, '')
        styT = +styT.replace(/\px/g, '')
      }
      document.onmousemove = function(e) {
        // 通过事件委托,计算移动的距离
        let left = e.clientX - disX
        let top = e.clientY - disY

        // 边界处理
        if (-(left) > minDragDomLeft) {
          left = -minDragDomLeft
        } else if (left > maxDragDomLeft) {
          left = maxDragDomLeft
        }

        if (-(top) > minDragDomTop) {
          top = -minDragDomTop
        } else if (top > maxDragDomTop) {
          top = maxDragDomTop
        }
        // 移动当前元素
        dragDom.style.cssText += `;left:${left + styL}px;top:${top + styT}px;`
      }
      document.onmouseup = function(e) {
        document.onmousemove = null
        document.onmouseup = null
      }
    }
  }
}

export default draggable

 7、权限控制 v-permission已实测可行

调用:其实传什么值 和 如何验证都是你可控制的,由你自己逻辑来定

<template>
    <el-button v-permission="5">不被展示</el-button>
    <el-button v-permission="1">展示</el-button>
</template>

代码:

/**
 * 实现你有无权限的地方,返回true则展示,返回false则隐藏,下面的1234则是举例子
 * @param {*} key 
 * @returns 
 */
function checkPermission(key) {
  let arr = [1,2,3,4]
  return arr.indexOf(key) > -1
}
 
const permission = {
  inserted: function (el, binding) {
    let permission = binding.value // 获取到 v-permission的值
    if (permission) {
      let hasPermission = checkPermission(permission)
      if (!hasPermission) {
        // 没有权限 移除Dom元素
        el.parentNode && el.parentNode.removeChild(el)
      }
    }
  },
}
 
export default permission

8、 超出宽度隐藏显示文本 v-overflow-tooltip(不推荐使用

调用:(我个人觉得没啥必要用这个,你自己写个css样式就可以了)

<template>
    <el-button v-overflow-tooltip>超出实体宽度隐藏展示,鼠标移上来展示全部</el-button>
</template>

代码:

/**
 * 超出设置宽度显示文字提示指令
 * 用法:v-overflow-tooltip / v-overflow-tooltip:width
 * width 可选
 * 只要当dom元素内容超出设置的宽度时,超出文字省略号显示,鼠标画上去有全部文字提示
*/
export default {
  name: 'overflow-tooltip',
  bind (el, binding) {
    const width = binding.arg
    if (width) {
      el.style.width = `${width}px`
    }
    const style = {
      whiteSpace: 'nowrap',
      overflow: 'hidden',
      textOverflow: 'ellipsis'
    }
    setStyle(el, style)
  },
  inserted (el, binding) {
    addTooltip(el, binding)
  },
  unbind (el) {
    if (!el.tooltip) return
    el.removeEventListener('mouseenter', el.elMouseEnterHandler)
    el.removeEventListener('mouseleave', el.elMouseOutHandler)
    el.tooltip.destroy()
  }
}
 
function addTooltip (el, binding) {
  el.oldOffsetWidth = el.offsetWidth
  if (!el.textWidth) {
    // 计算文本宽度
    const range = document.createRange()
    range.setStart(el, 0)
    range.setEnd(el, el.childNodes.length)
    const rangeWidth = range.getBoundingClientRect().width
    const padding = (parseInt(getStyle(el, 'paddingLeft'), 10) || 0) +
        (parseInt(getStyle(el, 'paddingRight'), 10) || 0)
    const textWidth = rangeWidth + padding
    el.textWidth = textWidth
  }
 
  // 监听元素宽度变化
  const resizeObserver = new ResizeObserver(entry => {
    const target = entry[0].target
    el.oldOffsetWidth !== target.offsetWidth && addTooltip(el, binding)
  })
  resizeObserver.observe(el)
 
  // Math.max(el.offsetWidth, binding.arg) 处理offsetWidth不是设置宽度时的情况
  if (el.textWidth > Math.max(el.offsetWidth, binding.arg || 0)) {
    let tooltip = null
 
    const elMouseEnterHandler = el.elMouseEnterHandler = debounce((event) => {
      if (!tooltip) {
        const tooltipContent = el.innerText || el.textContent
        tooltip = new Tooltip()
        tooltip.create(tooltipContent)
        el.tooltip = tooltip
      }
      // 400为tootip最大宽度
      tooltip.show(event, Math.min(el.textWidth, 400))
    }, 300)
    const elMouseOutHandler = el.elMouseOutHandler = debounce(() => {
      tooltip && tooltip.hide()
    }, 300)
 
    el.addEventListener('mouseenter', elMouseEnterHandler)
    el.addEventListener('mouseleave', elMouseOutHandler)
  } else {
    el.tooltip && el.tooltip.destroy()
    el.elMouseEnterHandler && el.removeEventListener('mouseenter', el.elMouseEnterHandler)
    el.elMouseOutHandler && el.removeEventListener('mouseleave', el.elMouseOutHandler)
  }
}
 
function debounce(fn, delay = 500) {
  let timer
  return function() {
    const th = this
    const args = arguments
    if (timer) {
      clearTimeout(timer)
    }
    timer = setTimeout(function() {
      timer = null
      fn.apply(th, args)
    }, delay)
  }
}
 
const SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g
const MOZ_HACK_REGEXP = /^moz([A-Z])/
const ieVersion = Number(document.documentMode)
const camelCase = function(name) {
  return name.replace(SPECIAL_CHARS_REGEXP, function(_, separator, letter, offset) {
    return offset ? letter.toUpperCase() : letter
  }).replace(MOZ_HACK_REGEXP, 'Moz$1')
}
 
const getStyle = ieVersion < 9 ? function(element, styleName) {
  if (!element || !styleName) return null
  styleName = camelCase(styleName)
  if (styleName === 'float') {
    styleName = 'styleFloat'
  }
  try {
    switch (styleName) {
      case 'opacity':
        try {
          return element.filters.item('alpha').opacity / 100
        } catch (e) {
          return 1.0
        }
      default:
        return (element.style[styleName] || element.currentStyle ? element.currentStyle[styleName] : null)
    }
  } catch (e) {
    return element.style[styleName]
  }
} : function(element, styleName) {
  if (!element || !styleName) return null
  styleName = camelCase(styleName)
  if (styleName === 'float') {
    styleName = 'cssFloat'
  }
  try {
    var computed = document.defaultView.getComputedStyle(element, '')
    return element.style[styleName] || computed ? computed[styleName] : null
  } catch (e) {
    return element.style[styleName]
  }
}
 
function setStyle(element, styleName, value) {
  if (!element || !styleName) return
 
  if (typeof styleName === 'object') {
    for (const prop in styleName) {
      if (styleName.hasOwnProperty(prop)) {
        setStyle(element, prop, styleName[prop])
      }
    }
  } else {
    styleName = camelCase(styleName)
    if (styleName === 'opacity' && ieVersion < 9) {
      element.style.filter = isNaN(value) ? '' : 'alpha(opacity=' + value * 100 + ')'
    } else {
      element.style[styleName] = value
    }
  }
}
 
class Tooltip {
  constructor () {
    this.id = 'autoToolTip'
    this.styleId = 'autoToolTipStyle'
    this.tooltipContent = ''
    this.styleElementText = `
      #autoToolTip {
        display: none;
        position: absolute;
        border-radius: 4px;
        padding: 10px;
        z-index: 99999;
        font-size: 12px;
        line-height: 1.2;
        min-width: 10px;
        max-width: 400px;
        word-break: break-word;
        color: #fff;
        background: #303133;
        transform-origin: center top;
      }
      #autoToolTip #arrow::after {
        content: " ";
        border-width: 5px;
        position: absolute;
        display: block;
        width: 0;
        height: 0;
        border-color: transparent;
        border-style: solid;
        bottom: -10px;
        left: calc(50% - 5px);
        border-top-color: #303133;
      }
    `
    this.tooltipElement = null
    this.styleElement = null
    this.showStatus = false
  }
 
  create (tooltipContent) {
    this.tooltipContent = tooltipContent
    const autoToolTip = document.querySelector('#' + this.id)
    // 同时只添加一个
    if (autoToolTip) {
      this.tooltipElement = autoToolTip
      return
    }
 
    const styleElement = document.createElement('style')
    styleElement.id = this.styleId
    styleElement.innerHTML = this.styleElementText
    document.head.append(styleElement)
    this.styleElement = styleElement
 
    const element = document.createElement('div')
    element.id = this.id
 
    const arrowElement = document.createElement('div')
    arrowElement.id = 'arrow'
    element.append(arrowElement)
 
    document.body.append(element)
    this.tooltipElement = element
  }
 
  show (event, textWidth) {
    if (this.showStatus) return
 
    const targetElement = event.target
    const targetElementRect = targetElement.getBoundingClientRect()
    const { left, top, width } = targetElementRect
 
    this.showStatus = true
    this.removeTextNode()
    this.tooltipElement.insertAdjacentText('afterbegin', this.tooltipContent)
    const style = {
      left: `${left - (textWidth + 20 - width) / 2}px`,
      top: `${top - 38}px`,
      display: 'block'
    }
    setStyle(this.tooltipElement, style)
  }
 
  hide () {
    const style = {
      left: '0px',
      top: '0px',
      display: 'none'
    }
    setStyle(this.tooltipElement, style)
 
    this.removeTextNode()
    this.showStatus = false
  }
 
  removeTextNode () {
    const { firstChild } = this.tooltipElement
    if (Object.prototype.toString.call(firstChild) === '[object Text]') {
      this.tooltipElement.removeChild(firstChild)
    }
  }
 
  destroy () {
    const { tooltipElement, styleElement } = this
    tooltipElement && tooltipElement.remove()
    styleElement && styleElement.remove()
  }
}

 9、输入框输入限制 v-limit-input(可用但有bug导致栈内存溢出,暂时不推荐使用

调用:

<template>
   <el-input v-limit-input:digit />
</template>

代码:

/**
 * 用法:v-limit-input:digit 只允许输入数字
 * v-limip-input:reg="your reg expression" 支持传正则表达式,处理一些特殊的场景
 */
const copy = {
  bind (el, binding, vnode, oldvnode) {
    const typeMap = {
      // 只输入数字
      digit: /\D/g,
      // 只输入正整数
      positiveInteger: /^(0+)|\D+/g,
      // 只输入基本中文
      chinese: /[^\u4E00-\u9FA5]/g,
      // 只输入中文英文字母
      chineseAlphabet: /[^\u4E00-\u9FA5A-Za-z]/g,
      // 只输入大写字母及数字
      uppercaseLetterDigit: /[^A-Z0-9]/g,
      // 只输入字母及数字
      letterDigit: /[^0-9a-zA-Z]/,
      // 只输入合法的金额格式
      price: /(\d+)(\.\d{0,2})?/
    }
    const { arg, value } = binding
    console.log(binding);
    if (!arg) {
      throw Error('one arg is required')
    }
    if (arg && !typeMap.hasOwnProperty(arg)) {
      throw Error('arg is not in typeMap')
    }
    if (arg === 'reg' && !value) {
      throw Error('reg arg requires a reg expression value')
    }
    const tagName = el.tagName.toLowerCase()
    const input = tagName === 'input' ? el : el.querySelector('input')
    const regKey = arg || (arg === 'reg' && value)
    // 输入法气泡窗弹出,开始拼写
    el.compositionstartHandler = function () {
      el.inputLocking = true
    }
    // 输入法气泡窗关闭,输入结束
    el.compositionendHandler = function () {
      el.inputLocking = false
      input.dispatchEvent(new Event('input'))
    }
    el.inputHandler = function (e) {
      if (el.inputLocking) return
      const oldValue = e.target.value
      const newValue = oldValue.replace(typeMap[regKey], '')
      // price 正则在safar报错,导致页面无法打开,新增的判断
      if (regKey === 'price') {
        const rege = /(\d+)(\.\d{0,2})?/
        const target = e.target
        if (rege.test(target.value)) {
          const value = target.value.match(rege)[0]
          if (value.split('.').length === 1 && target.value === value) {
            input.value = Number(value)
          } else if (target.value !== value) {
            input.value = value
            input.dispatchEvent(new Event('input')) // 通知v-model更新
          }
        } else {
          input.value = ''
          input.dispatchEvent(new Event('input'))
        }
      } else {
        // 判断是否需要更新,避免进入死循环
        if (newValue !== oldValue) {
          input.value = newValue
          input.dispatchEvent(new Event('input')) // 通知v-model更新
        }
      }
    }
    input.addEventListener('compositionstart', el.compositionstartHandler)
    input.addEventListener('compositionend', el.compositionendHandler)
    input.addEventListener('input', el.inputHandler)
  },
  unbind (el) {
    const tagName = el.tagName.toLowerCase()
    const input = tagName === 'input' ? el : el.querySelector('input')
    input.removeEventListener('compositionstart', el.compositionstartHandler)
    input.removeEventListener('compositionend', el.compositionendHandler)
    input.removeEventListener('input', el.inputHandler)
  }
}
 export default copy

10、v-drag(已实测可行

调用:(和v-draggable有点像,但是后者用于el-table拖动,前者通用于普通div在父节点范围内拖动)

<template>
  <div class="parent">
    <div
      class="child"
      v-drag
    />
  </div>
</template>

代码:

const drag = {
  bind(el, binding, vnode) {
    const curDom = el; // 当前元素
    curDom.style.cursor = 'grab'
    curDom.onmousedown = (e) => {
      // 鼠标按下,计算当前元素距离可视区的距离
      const disX = e.clientX - curDom.offsetLeft;
      const disY = e.clientY - curDom.offsetTop;
      document.onmousemove = (e) => {
        e.preventDefault();
        if (e.stopPropagation) {
          e.stopPropagation();
        } else {
          e.cancelBubble = true;
        }
        curDom.style.cursor = 'grabbing'
        // 计算移动的距离
        const l = e.clientX - disX;
        const t = e.clientY - disY;
        curDom.style.left = l + 'px'
        curDom.style.top = t + 'px'
      }
      document.onmouseup = (e) => {
        curDom.style.cursor = 'grab'
        document.onmousemove = null;
        document.onmouseup = null;
      }
      // return false不加的话相当于onmouseup失效
      return false
    };
  }
}

export default drag

11、表格大数据量滚动加载 v-load-more(已实测可行

代码:

import {
  VNodeDirective
} from 'vue'
let timeout;
/** 设置表格滚动区间 */
const setRowScrollArea = (topNum, showRowNum, binding) => {
  if (timeout) {
    clearTimeout(timeout);
  }
  timeout = setTimeout(() => {
    binding.value.call(null, topNum, topNum + showRowNum);
  });
};
const loadMore= {
  bind(el: Element, _binding) {
    setTimeout(() => {
      // 创建虚拟滚动条
      const selectWrap = el.querySelector('.el-table__body-wrapper');
      const selectTbody = selectWrap.querySelector('table tbody');
      const createElementTR = document.createElement('tr');
      createElementTR.id = 'virtual-scroll'
      selectTbody.append(createElementTR); // 先行将虚拟滚动条加入进来
    })
  },
  componentUpdated(el: Element, binding: VNodeDirective, vnode, oldVnode) {
    setTimeout(() => {
      const dataSize = vnode.data.attrs['data-size'];
      const oldDataSize = oldVnode.data.attrs['data-size'];
      // 当数量相同时,表明当前未发生更新,减少后续操作
      if (dataSize === oldDataSize) {
        return;
      }
      const selectWrap = el.querySelector('.el-table__body-wrapper');
      const selectTbody = selectWrap.querySelector('table tbody');
      const selectRow = selectWrap.querySelector('table tr');
      // 当一行都没有,说明无数据渲染,但一般逻辑都不会进入这里
      if (!selectRow) {
        return;
      }
      const rowHeight = selectRow.clientHeight;
      // 能够在当前显示区的展示条数,本项目就是11条
      const showRowNum = Math.round(selectWrap.clientHeight / rowHeight);
      const createElementTRHeight = (dataSize - showRowNum) * rowHeight;
      const createElementTR = selectTbody.querySelector('#virtual-scroll')
      // 监听滚动后事件
      selectWrap.addEventListener('scroll', function() {
        let topPx = this.scrollTop;
        let topNum = Math.round(topPx / rowHeight);
        const minTopNum = dataSize - showRowNum;
        if (topNum > minTopNum) {
          topNum = minTopNum;
        }
        if (topNum < 0) {
          topNum = 0;
          topPx = 0;
        }
        selectTbody.setAttribute('style', `transform: translateY(${topPx}px)`);
        // 本来触底的话,应该设置为0,但是触底后 就没有滚动条了
        createElementTR.setAttribute('style', `height: ${createElementTRHeight - topPx > 0 ? createElementTRHeight - topPx : rowHeight}px;`);
        setRowScrollArea(topNum, showRowNum, binding);
      })
    });
  }
}

export default loadMore

调用方式:

        <el-table
            data={visibleResult.value} // 可视区域的数据
            {...{ directives: [{ name: 'load-more', value: methods.loadMore }] }}
        >
        </el-table>
  /** 表格上展示的数据 */
  const visibleResult = computed(() => {
    return result.value.filter((_item, index) => {
      if (index < curStartIndex.value) {
        return false;
      } else if (index > curEndIndex.value) {
        return false;
      } else {
        return true;
      }
    });
  })

const methods = {
    /**
     * 懒加载回调
     * @param startIndex 区段位置开始索引
     * @param endIndex 区段位置结束索引
     */
    loadMore(startIndex: number, endIndex: number) {
      curStartIndex.value = startIndex
      curEndIndex.value = endIndex
    },
}

12、 监听dom宽高变化 v-resize(已实测可行

调用:

<template>
    <div v-resize="resize"></div>
</template>

<script>
export default {
    methods: {
        resize(dom) {
            const {width, height} = dom; // 变化后的dom宽高
        }
    }
}
</script>

代码:

export default {
  bind(el, binding) {
    let width = '';
    let height = '';
    function isReize() {
      const style = document.defaultView.getComputedStyle(el);
      if (width !== style.width || height !== style.height) {
        binding.value({ width: style.width, height: style.height });
      }
      width = style.width;
      height = style.height;
    }
    el.__resizeInterval__ = setInterval(isReize, 300);
  },
  unbind(el) {
    clearInterval(el.__resizeInterval__);
  }
}

---持续更新--- 

  • 4
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值