一、前言
Vue项目中有许多表格需要能快捷键操作,以提高操作速度。如果直接使用DOM方法进行操作的话,性能较差,且Vue并不推荐直接操作DOM对象。了解到vue-direction-key插件解决了这样一个问题,因此想手写一个类似的指令。
那我们先了解一下vue-direction-key的基本使用
二、vue-direction-key的基本使用
1. 下载
npm i vue-direction-key -S
2.在入口文件main.js中引入插件
import Direction from 'vue-direction-key'
Vue.use(Direction)
基本用法
1. 在需要的input框或者el-input框上绑定指定
- v-direction的值是当前表格所在的坐标位置
不绑定指令的input框,键盘事件会跳过当前单元格
<el-table :data="tableData" border style="width: 100%">
<el-table-column prop="code" label="商品编号" width="180">
<template slot-scope="{ row, $index }">
<el-input v-model="row.code" v-direction="{ x: 0, y: $index }"></el-input>
</template>
</el-table-column>
<el-table-column prop="name" label="商品" width="180">
<template slot-scope="{ row, $index }">
<el-input v-model="row.name" v-direction="{ x: 1, y: $index }"></el-input>
</template>
</el-table-column>
<el-table-column prop="barcodes" label="商品条码">
<template slot-scope="{ row }">
<!-- 可以不绑定指令,不绑定指令的时候就会跳过该单元格 -->
<el-input v-model="row.barcodes"></el-input>
</template>
</el-table-column>
<el-table-column prop="specification" label="规格">
<template slot-scope="{ row, $index }">
<el-input v-model="row.specification" v-direction="{ x: 3, y: $index }"></el-input>
</template>
</el-table-column>
</el-table>
...
<script>
export default {
data() {
return {
tableData: [
{
code: '20220513001',
name: '连衣裙',
barcodes: '52132124511',
specification: '红色'
},
{
code: '20220513001',
name: '连衣裙',
barcodes: '52132124511',
specification: '红色'
},
{
code: '20220513001',
name: '连衣裙',
barcodes: '52132124511',
specification: '红色'
},
]
}
}
}
</script>
2. 在生命周期钩子中监听键盘事件,并做对应操作
可以在ceated或mounted中处理,官网推荐在created
direction的移动方法中可以接收两个参数,表示对应点的坐标,默认为当前聚焦元素的坐标
- 比如:direction.next(1,2),移动到x=1,y=2坐标的右边也就是,x=2,y=2
created() {
// 在源码中,这个代码相当于是在获取操作键盘的实例,它具有上移,下移等方法
let direction = this.$getDirection()
// 监听键盘操作事件,内部处理就是document.addEventListener
direction.on('keyup', function (e, val) {
// e是event对象,val是当前操作的元素的坐标,如:{x: 0, y: 0}
if (e.key === 'ArrowUp') { // 这里的键盘事件可以自己指定
console.log('按下 ↑ 键')
direction.previousLine() // 上移
} else if (e.key === 'ArrowRight' || e.key === 'Enter') {
console.log('按下 → 键,或 Enter 键')
direction.next() // 右移
} else if (e.key === 'ArrowDown') {
console.log('按下 ↓ 键')
direction.nextLine() // 下移
} else if (e.key === 'ArrowLeft') {
console.log('按下 ← 键')
direction.previous() // 左移
}
})
},
效果图:
多组件共存情况的使用
- 比如:direction.next(1,2),移动到x=1,y=2坐标的右边也就是,x=2,y=2
比如有多个表格,每个表格对应一个坐标体系
- 这种情况,在指令中增加参数来区别,如:v-direction:a=”{x: 0, y: 0}”
- 然后在this.$getDirection(‘a’)中传入这个参数
以下案例是官网的案例
<input type="text" v-direction:a="{x: 0, y: 0}">
<input type="text" v-direction:a="{x: 1, y: 0}">
<input type="text" v-direction:b="{x: 0, y: 0}">
<input type="text" v-direction:b="{x: 1, y: 0}">
created: function () {
let a = this.$getDirection('a')
a.on('keyup', function (e, val) {
if (e.keyCode === 39) {
a.next()
}
if (e.keyCode === 37) {
a.previous()
}
if (e.keyCode === 38) {
a.previousLine()
}
if (e.keyCode === 40) {
a.nextLine()
}
})
let b = this.$getDirection('b')
b.on('keyup', function (e, val) {
if (e.keyCode === 39) {
b.next()
}
if (e.keyCode === 37) {
b.previous()
}
if (e.keyCode === 38) {
b.previousLine()
}
if (e.keyCode === 40) {
b.nextLine()
}
})
}
三、手写实现
本次实现不考虑以下问题:
- 多组件共存的情况
- 未使用插件的形式,主要是实现指令和操作类
- 实例操作对象的方法参数,是当前操作元素
- 不对方法监听进行封装,直接在created中使用document.addEventListener
思路分析
通过指令收集表格的坐标体系,采用二维数组的方式收集,比如以下二维数组:
let arr = [
[A, B, C], // 第一行,如:A的坐标就是{x:0,y:0}
[D, E, F] // 第二行
]
在表格坐标体系中的位置如下:
定义一个操作类,并将其实例挂载到Vue原型上
- 自定义指令,定义操作类
```javascript
import Vue from ‘vue’
// 声明一个数组,用于存储需要键盘操作的input元素,这是一个二维数组
const nodeArr = []
// 自定义指令
Vue.directive(‘direction’, {
inserted: function (el, binding) {
// 获取绑定自定义指令的元素的坐标
if (!nodeArr[binding.value.y]) {
} if (el.tagName !== ‘input’) {nodeArr[binding.value.y] = []
} // 将元素,和其坐标,保存在二维数组的对应位置 nodeArr[binding.value.y][binding.value.x] = {// 如果元素不是input,则找其子元素中的input
el = el.querySelector('input')
} } })el,
value: binding.value // 指令的值{x:??,y:??}
// 定义一个操作类,该类接收需要操作的二维数组 class Direction { constructor(nodeArr) { this.nodeArr = nodeArr this.x = 0 this.y = 0 } // x轴上前进,右移,接收当前元素作为参数 next(target) { // 获取当前元素的X和Y,以这个目标元素作为参照点 this.getXandY(target) // 处理坐标 // X轴上加1,如果超长换行,最后一个不做处理 if (this.x < this.nodeArr[this.y].length - 1) { this.x += 1 } else { this.y += 1 if (this.y < this.nodeArr.length) { this.x = 0 } else { console.log(‘到底了’) return } } // 获取下一个node const node = this.nodeArr[this.y][this.x] if (node && node.el) { node.el.focus() // 下一个node获取焦点 } else { this.next(null) // 下一个元素为空,直接跳过 } } // x轴上左移 previous(target) { this.getXandY(target) // X轴上减1,到第一个时回退到上一行,到头时不做操作 if (this.x > 0) { this.x -= 1 } else { this.y -= 1 if (this.y >= 0) { this.x = this.nodeArr[this.y].length - 1 } else { console.log(‘到头了’) return } } const node = this.nodeArr[this.y][this.x] if (node && node.el) { node.el.focus() // 下一个node获取焦点 } else { this.previous(null) // 下一个元素为空,直接跳过 } } // 下移 nextLine(target) { this.getXandY(target) // y轴上加1,到底后不做操作 if (this.y < this.nodeArr.length - 1) { this.y += 1 } else { console.log(‘Y轴到底了’) return } const node = this.nodeArr[this.y][this.x] if (node && node.el) { node.el.focus() // 下一个node获取焦点 } else { this.nextLine(null) // 下一个元素为空,直接跳过 } } // 上移 previousLine(target) { this.getXandY(target) // y轴上减1,到头后不做操作 if (this.y > 0) { this.y -= 1 } else { console.log(‘Y轴到头了’) return } const node = this.nodeArr[this.y][this.x] if (node && node.el) { node.el.focus() // 下一个node获取焦点 } else { this.previousLine(null) // 下一个元素为空,直接跳过 } } // 获取目标元素的X和Y getXandY(target) { if (target) { this.nodeArr.forEach(list => { list.forEach(node => { if (node.el === target) { this.x = node.value.x this.y = node.value.y } }) }) } } } // 向外暴露操作类的实例 export default new Direction(nodeArr)
<a name="tuvix"></a>
#### main.js中引入,并挂载到VUE原型上
```javascript
import Direction from './directives/direction-key'
Vue.prototype.$direction = Direction
使用:
<template>
<div id="app">
<el-table :data="tableData" border style="width: 100%">
<el-table-column prop="code" label="商品编号" width="180">
<template slot-scope="{ row, $index }">
<!-- 类readonlyClass,用于处理input框失去焦点时,不显示外框 -->
<!-- @focus="row.active = 0" @blur="row.active = 11",都是用于处理样式的,与主代码无关 -->
<el-input v-model="row.code" :class="{ readonlyClass: row.active !== 0 }" @focus="row.active = 0" @blur="row.active = 11" v-direction="{ x: 0, y: $index }"></el-input>
</template>
</el-table-column>
<el-table-column prop="name" label="商品" width="180">
<template slot-scope="{ row, $index }">
<el-input v-model="row.name" :class="{ readonlyClass: row.active !== 1 }" @focus="row.active = 1" @blur="row.active = 11" v-direction="{ x: 1, y: $index }"></el-input>
</template>
</el-table-column>
<el-table-column prop="barcodes" label="商品条码">
<template slot-scope="{ row }">
<!-- 这里没有绑定v-direction ,键盘操作事件时会跳过该单元格-->
<el-input v-model="row.barcodes" :class="{ readonlyClass: row.active !== 2 }" @focus="row.active = 2" @blur="row.active = 11"></el-input>
</template>
</el-table-column>
<el-table-column prop="specification" label="规格">
<template slot-scope="{ row, $index }">
<el-input v-model="row.specification" :class="{ readonlyClass: row.active !== 3 }" @focus="row.active = 3" @blur="row.active = 11" v-direction="{ x: 3, y: $index }"></el-input>
</template>
</el-table-column>
<el-table-column prop="warehouse" label="仓库">
<template slot-scope="{ row, $index }">
<el-input v-model="row.warehouse" :class="{ readonlyClass: row.active !== 4 }" @focus="row.active = 4" @blur="row.active = 11" v-direction="{ x: 4, y: $index }"></el-input>
</template>
</el-table-column>
<el-table-column prop="count" label="数量">
<template slot-scope="{ row, $index }">
<el-input v-model="row.count" :class="{ readonlyClass: row.active !== 5 }" @focus="row.active = 5" @blur="row.active = 11" v-direction="{ x: 5, y: $index }"></el-input>
</template>
</el-table-column>
<el-table-column prop="stock" label="库存">
<template slot-scope="{ row, $index }">
<el-input v-model="row.stock" :class="{ readonlyClass: row.active !== 6 }" @focus="row.active = 6" @blur="row.active = 11" v-direction="{ x: 6, y: $index }"></el-input>
</template>
</el-table-column>
<el-table-column prop="unit" label="单位">
<template slot-scope="{ row, $index }">
<el-input v-model="row.unit" :class="{ readonlyClass: row.active !== 7 }" @focus="row.active = 7" @blur="row.active = 11" v-direction="{ x: 7, y: $index }"></el-input>
</template>
</el-table-column>
<el-table-column prop="weight" label="重量">
<template slot-scope="{ row, $index }">
<el-input v-model="row.weight" :class="{ readonlyClass: row.active !== 8 }" @focus="row.active = 8" @blur="row.active = 11" v-direction="{ x: 8, y: $index }"></el-input>
</template>
</el-table-column>
<el-table-column prop="price" label="单价">
<template slot-scope="{ row, $index }">
<el-input v-model="row.price" :class="{ readonlyClass: row.active !== 9 }" @focus="row.active = 9" @blur="row.active = 11" v-direction="{ x: 9, y: $index }"></el-input>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script>
export default {
// 在created中监听键盘事件
created() {
document.addEventListener('keyup', e => {
if (e.key === 'ArrowUp') {
console.log('按下 ↑ 键')
// 传入当前元素,作为参照点
this.$direction.previousLine(e.target)
} else if (e.key === 'ArrowRight' || e.key === 'Enter') {
console.log('按下 → 键,或 Enter 键')
this.$direction.next(e.target)
} else if (e.key === 'ArrowDown') {
console.log('按下 ↓ 键')
this.$direction.nextLine(e.target)
} else if (e.key === 'ArrowLeft') {
console.log('按下 ← 键')
this.$direction.previous(e.target)
}
})
},
data() {
return {
tableData: [
{
code: '20220513001',
name: '连衣裙',
barcodes: '52132124511',
specification: '红色',
warehouse: '主仓库',
count: 5,
stock: '53件',
unit: '件',
weight: '0.3kg',
price: 120,
active: 1 // 1商品编号获取焦点,11是没有单元格被激活。用于处理获取焦点或失去焦点时候的样式。
},
{
code: '20220513001',
name: '连衣裙',
barcodes: '52132124518',
specification: '红色',
warehouse: '主仓库',
count: 5,
stock: '53件',
unit: '件',
weight: '0.3kg',
price: 120,
active: 11
},
{
code: '20220513001',
name: '连衣裙',
barcodes: '52132124511',
specification: '红色',
warehouse: '主仓库',
count: 5,
stock: '53件',
unit: '件',
weight: '0.3kg',
price: 120,
active: 11
},
{
code: '20220513001',
name: '连衣裙',
barcodes: '52132124511',
specification: '红色',
warehouse: '主仓库',
count: 5,
stock: '53件',
unit: '件',
weight: '0.3kg',
price: 120,
active: 11
},
{
code: '20220513001',
name: '连衣裙',
barcodes: '52132124511',
specification: '红色',
warehouse: '主仓库',
count: 5,
stock: '53件',
unit: '件',
weight: '0.3kg',
price: 120,
active: 11
},
{
code: '20220513001',
name: '连衣裙',
barcodes: '52132124511',
specification: '红色',
warehouse: '主仓库',
count: 5,
stock: '53件',
unit: '件',
weight: '0.3kg',
price: 120,
active: 11
}
]
}
}
}
</script>
<style>
.el-table__body-wrapper .cell {
padding: 0 0 !important;
line-height: normal !important;
}
.el-table__body-wrapper .el-table__cell {
padding: 0 0 !important;
}
.el-table__body-wrapper .el-input input {
border-radius: 0;
}
.el-table__body-wrapper .el-input.readonlyClass input {
background: transparent;
border-color: transparent;
}
</style>