vue 常用自定义指令

目录

说明

 自定义指令

节流

防抖

复制

拖拽

水印

权限控制

移除隐藏

长按

input限制输入

select组件上拉加载


参考:分享8个非常实用的Vue自定义指令

说明

/**
 * 自定义指令对象可以接收的钩子函数
 * bind             只调用一次,指令第一次绑定到元素时调用
 * inserted         被绑定元素插入父节点时调用
 * update           所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前
 * componentUpdated 指令所在组件的 VNode 及其子 VNode 全部更新后调用
 * unbind           只调用一次,指令与元素解绑时调用
 */

/**
 * 自定义指令钩子函数参数 (binding参数包含内容)
 * name:指令名,不包括 v- 前缀。
 * value:指令的绑定值,例如:v-my-directive="1 + 1" 中,绑定值为 2。
 * oldValue:指令绑定的前一个值,仅在 update 和 componentUpdated 钩子中可用。无论值是否改变都可用。
 * expression:字符串形式的指令表达式。例如 v-my-directive="1 + 1" 中,表达式为 "1 + 1"。
 * arg:传给指令的参数,可选。例如 v-my-directive:foo 中,参数为 "foo"。
 * modifiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true }。
 */

详细使用及参数说明请参见vue官方文档-自定义指令。 

使用

import Vue from 'vue'

import copy from './copy.js'
import longpress from './longpress.js'
import { debounce, throttle } from './debounce.js'
import emoji from './emoji.js'
import permission from './permission'
import waterMarker from './waterMarker'
import draggable from './draggable'
import inputLimit from './inputLimit.js'
import dialogDrag from './dialogDrag.js'
import removeZero from './removeZero.js'
import overflowTip from './overflowTooltip.js'

const directives = {
  copy,
  longpress,
  debounce,
  throttle,
  emoji,
  waterMarker,
  permission,
  draggable,
  inputLimit,
  dialogDrag,
  removeZero,
  overflowTip
}

Object.keys(directives).forEach(key => Vue.directive(key, directives[key]))

 自定义指令

节流

/**
 * 使用
 * <el-button v-throttle:500="handlerClick">节流按钮throttle</el-button>
 */
export const throttle = {
  inserted (el, binding) {
    if (typeof binding.value !== 'function') {
      throw new Error('指令的参数必须是函数')
    }
    let flg = false
    let timerName
    let time = 500
    if(binding.arg) {
      time = parseInt(binding.arg)
    }
    el.handle = () => {
      if (flg) return
      flg = true
      binding.value()
      timerName = setTimeout(() => {
        flg = false
      }, time)
    }
    el.addEventListener('click', el.handle)
  },
  unbind(el,binding) {
    el.removeEventListener('click',el.handle)
  }
}

防抖

/**
 * 使用:
 * <el-input v-debounce="handlerInput" v-model="inputValue"></el-input>
 */
const findEle = (parent, type) => {
  return parent.tagName.toLowerCase() === type ? parent : parent.querySelector(type)
}

export const debounce = {
  inserted: function (el, binding) {
    if (typeof binding.value !== 'function') {
      throw new Error('指令的参数必须是函数')
    }
    let timer
    const $inp = findEle(el, 'input')
    el.$inp = $inp
    el.handle = () => {
      if (timer) {
        clearTimeout(timer)
      }
      timer = setTimeout(() => {
        binding.value()
      }, 1000)
    }
    el.addEventListener('input', el.handle)
  },
  unbind: function (el) {
    el.$inp.removeEventListener('input', el.$inp.handle)
  }
}

复制

/**
 * 使用
 * <div v-copy="copyText">
 *    <el-button v-copy="copyText">复制</el-button>
 *    {
   
   {copyText}}
 * </div>
 */
const copy = {
  bind (el, binding, vnode, oldVnode) {
    const { value } = binding
    el.$value = value
    el.handler = () => {
      if (!el.$value) {
        // 值为空的时候,给出提示。可根据项目UI仔细设计
        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('复制成功') // 可根据项目UI仔细设计
      }
      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

拖拽

/**
 * 使用
 * <el-button v-draggable>拖拽按钮</el-button>
 */
const draggable = {
  bind(el,binding) {
    el.style.cursor = 'move'
  },
  inserted: function (el) {
    el.onmousedown = function (e) {
      // el.offsetTop/Left 元素到父元素上边/左边的距离
      let disx = e.pageX - el.offsetLeft
      let disy = e.pageY - el.offsetTop
      document.onmousemove = function (e) {
        let x = e.pageX - disx
        let y = e.pageY - disy
        let maxX = document.body.clientWidth - parseInt(window.getComputedStyle(el).width)
        let maxY = document.body.clientHeight - parseInt(window.getComputedStyle(el).height)
        if (x < 0) {
          x = 0
        } else if (x > maxX) {
          x = maxX
        }

        if (y < 0) {
          y = 0
        } else if (y > maxY) {
          y = maxY
        }

        el.style.left = x + 'px'
        el.style.top = y + 'px'
      }
      document.onmouseup = function () {
        document.onmousemove = document.onmouseup = null
      }
    }
  },
}
export default draggable

水印

/**
 * 使用:
 * <div v-waterMarker="{text:'lzg版权所有',textColor:'rgba(180, 180, 180, 0.4)'}"></div>
 */
function addWaterMarker (str, el, font, textColor) {
  // 水印文字,父元素,字体,文字颜色
  var can = document.createElement('canvas')
  el.appendChild(can)
  can.width = 200
  can.height = 150
  can.style.display = 'none'
  var cans = can.getContext('2d')
  cans.rotate((-20 * Math.PI) / 180)
  cans.font = font || '16px Microsoft JhengHei'
  cans.fillStyle = textColor || 'rgba(180, 180, 180, 0.3)'
  cans.textAlign = 'left'
  cans.textBaseline = 'Middle'
  cans.fillText(str, can.width / 10, can.height / 2)
  el.style.backgroundImage = 'url(' + can.toDataURL('image/png') + ')'
}

const waterMarker = {
  bind: function (el, binding) {
    addWaterMarker(binding.value.text, el, binding.value.font, binding.value.textColor)
  }
}

export default waterMarker

权限控制

/**
 * 使用: str 标志当前元素展示权限
 * <el-button v-permission="str">按钮1</el-button>
 */
function checkArray(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 = checkArray(permission)
      if (!hasPermission) {
        // 没有权限 移除Dom元素
        el.parentNode && el.parentNode.removeChild(el)
      }
    }
  },
}

export default permission

移除隐藏

/**
 * 超出设置宽度显示文字提示指令
 * 用法: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()
  }
}

长按

/**
 * 长按指令,使用:
 * longpressFn 长按后执行的方法
 * <el-button v-longpress="longpressFn">长按</el-button>
 */
const longpress = {
  bind: function (el, binding, vNode) {
    if (typeof binding.value !== 'function') {
      throw new Error('callback must be a function')
    }
    // 定义变量
    let pressTimer = null
    // 创建计时器( 2秒后执行函数 )
    const start = (e) => {
      if (e.type === 'click' && e.button !== 0) {
        return
      }
      if (pressTimer === null) {
        pressTimer = setTimeout(() => {
          handler()
        }, 1500)
      }
    }
    // 取消计时器
    const cancel = (e) => {
      if (pressTimer !== null) {
        clearTimeout(pressTimer)
        pressTimer = null
      }
    }
    // 运行函数
    const handler = (e) => {
      binding.value(e)
    }
    // 添加事件监听器
    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

input限制输入

/**
 * 用法:v-limit-input:digit 只允许输入数字
 * v-limip-input:reg="your reg expression" 支持传正则表达式,处理一些特殊的场景
 */
export default {
  name: 'limit-input',
  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})?/
      // price: /([^0-9.])|((?<=\d+\.\d{2})\d+)|((?<=^0)0+)|(^0(?=[1-9]))|((?<=\.\d*)\.)|(^\.)/g
    }
    const { arg, value } = 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)
  }
}

select组件上拉加载

/**
 * element select添加上拉加载更多指令
 * 使用:
 * v-el-select-loadmore="执行的事件名"
 */
export default {
  name: 'el-select-loadmore',
  bind (el, binding) {
    // 获取element-ui定义好的scroll盒子
    const SELECTWRAP_DOM = el.querySelector('.el-select-dropdown .el-select-dropdown__wrap');
    SELECTWRAP_DOM.addEventListener('scroll', function () {
      const CONDITION = this.scrollHeight - this.scrollTop <= (this.clientHeight + 2);
      if (CONDITION) {
        binding.value()
      }
    })
  }
}
<!-- 使用 select组件需使用form-item包裹,且需设置样式 -->
<FormItem style="padding-right: 10px; transform: translate(-10px, 0px)">
    <el-select v-model.lazy="queryParams.platformName" v-el-select-loadmore="loRaLoadMoreFn" >
        <el-option v-for="item in loraNumList" 
            :value="item.platformName" 
            :label="item.platformName" 
            :key="item.appKey"
        ></el-option>
    </el-select>
</FormItem>

猜你喜欢

转载自blog.csdn.net/lwx931449660/article/details/124136652
今日推荐