bind : 只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。(常用)
inserted : 被绑定元素插入父节点时调用(保证父节点存在,但不一定已被插入文档中)。(常用)
update : 所在组件的VNode更新时调用,但是可能发生在其子VNode更新之前。 指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新(详细的钩子函数参数下见)。(基本没咋用)
componentUpdated : 指令所在组件的VNode 及其子 VNode 全部更新后调用。(基本没咋用)
unbind : 只调用一次,指令与元素解绑时调用。(常用)
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 钩子中可用。(基本没咋用)
// 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)
调用:
复制
代码:
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;
调用:直接v-longpress则默认长按1秒执行函数,当然也可以自己传参,单位为毫秒
长按
代码:
/**
* 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
调用:分为立即/非立即防抖,传参方式不同
立即/非立即防抖
常规防抖
代码:
// 在指定的时间段内,多次点击只会执行一次
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
调用:
代码:
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
调用:
代码:
/**
* @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
调用:针对于el-dialog的拖动
代码:
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
调用:其实传什么值 和 如何验证都是你可控制的,由你自己逻辑来定
不被展示
展示
代码:
/**
* 实现你有无权限的地方,返回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
调用:(我个人觉得没啥必要用这个,你自己写个css样式就可以了)
超出实体宽度隐藏展示,鼠标移上来展示全部
代码:
/**
* 超出设置宽度显示文字提示指令
* 用法: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()
}
}
调用:
代码:
/**
* 用法: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
调用:(和v-draggable有点像,但是后者用于el-table拖动,前者通用于普通div在父节点范围内拖动)
代码:
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
代码:
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
调用方式:
/** 表格上展示的数据 */
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
},
}
---持续更新---