element-ui 表格中输入框的键盘移动

310 阅读3分钟

防止出现为什么都是出现Vue3.x了还在用vue2.x言论,先提前打下预防针,一个老的稳定的系统是不会去做跨版本的升级操作,如要做也是重头开始从0开始

环境

  • Vue2.x
  • JavaScript
  • Element2.x

开始

之前接到一个需求,要求使用键盘方向键控制表格中的输入框焦点,接到这个需求我的第一想法,客户就这么懒吗?用鼠标点吧点吧就能解决的事件为什么非要用方向键控制?原来他们老系统上就可以使用方向键控制输入框焦点,他们的老系统使用的是CS架构跑在客户端,我们系统为BS跑在网页端。既然答应的客户那我们还能怎么帮,就去实现呗。

186812e697faeb6bafa24b836f502f30.gif

思路

既然要使用移动我们就得监听方向键事件,好在Vue2.x为我们提供了指令

  • @keydown.up 键盘上键
  • @keydown.down 键盘下键
  • @keydown.left 键盘左键
  • @keydown.right 键盘右键

由于是在表格里移动来获取输入框焦点,我们需要知道当前是在那个输入框内,需要定义几个参数来标明

  • vertical 垂直方向:判断是上移还是下移,移动几行(基本上都是一行一行移动),上移传参【-1】,下移传参【1】
  • horizontal 水平方法:判断是左移还是右移,移动几个单元格,左移传参【-1】,右移传参【1】
  • index 表格索引:判断是在表格那一行
  • columnFile 列名:判断是在哪一列

获取元素的方法

  • 使用Ref的方法获取输入框的获取焦点事件focus

v2-799528ecee37cbe8bc271762388b0131_720w.gif

编码

开编码前还需要分情况,就是这个表格是静态的还是动态的?为什么考虑这个,如果是静态水平方向移动几个单元格是可以手动直接写死,如果是动态的就需要动态去计算需要移动几个单元格,废话不说了直接开整。

QQ20240805-162017.png

先为每个输入框都绑定事件并取名move(vertical, horizontal, index, column)

  • @keydown.up
  • @keydown.down
  • @keydown.left
  • @keydown.right
<el-table :data="tableData" style="width: 100%" ref="tableRef" border>
  <el-table-column prop="name" label="姓名" width="180">
    <template slot-scope="scope">
      <el-input v-model="scope.row.name" placeholder="请输入内容"></el-input>
    </template>
  </el-table-column>

  <el-table-column prop="address" label="地址">
    <template slot-scope="scope">
      <el-input
        @keydown.up.native="move(-1, 0, scope.$index, 'address')"
        @keydown.down.native="move(1, 0, scope.$index, 'address')"
        @keydown.left.native="move(0, -1, scope.$index, 'address')"
        @keydown.right.native="move(0, 2, scope.$index, 'address')"
        v-model="scope.row.address"
        placeholder="请输入内容"
      ></el-input>
    </template>
  </el-table-column>

  <el-table-column prop="number" label="数量">
    <template slot-scope="scope">
      <el-input v-model="scope.row.number" placeholder="请输入内容"></el-input>
    </template>
  </el-table-column>

  <el-table-column prop="email" label="邮箱">
    <template slot-scope="scope">
      <el-input
        @keydown.up.native="move(-1, 0, scope.$index, 'email')"
        @keydown.down.native="move(1, 0, scope.$index, 'email')"
        @keydown.left.native="move(0, -2, scope.$index, 'email')"
        @keydown.right.native="move(0, 1, scope.$index, 'email')"
        v-model="scope.row.email"
        placeholder="请输入内容"
      ></el-input>
    </template>
  </el-table-column>

  <el-table-column prop="phoneNumber" label="手机号">
    <template slot-scope="scope">
      <el-input
        @keydown.up.native="move(-1, 0, scope.$index, 'phoneNumber')"
        @keydown.down.native="move(1, 0, scope.$index, 'phoneNumber')"
        @keydown.left.native="move(0, -1, scope.$index, 'phoneNumber')"
        @keydown.right.native="move(0, 0, scope.$index, 'phoneNumber')"
        v-model="scope.row.phoneNumber"
        placeholder="请输入内容"
      ></el-input>
    </template>
  </el-table-column>

  <el-table-column prop="phoneNumber" label="性别">
    <template slot-scope="scope">
      <el-input v-model="scope.row.sex" placeholder="请输入内容"></el-input>
    </template>
  </el-table-column>
</el-table>
export default {
  name: 'HelloWorld',
  data() {
    return {
      columns: [null, 'address', null, 'email', 'phoneNumber'],
      tableData: [
        { name: 'A', address: '地址A', number: 1, email: 'a.qq', phoneNumber: '', sex: '' },
        { name: 'B', address: '地址B', number: 2, email: 'b.qq', phoneNumber: '', sex: '' },
        { name: 'C', address: '地址C', number: 3, email: 'c.qq', phoneNumber: '', sex: '' },
        { name: 'D', address: '地址D', number: 4, email: '', phoneNumber: '', sex: '' },
        { name: 'E', address: '地址E', number: 5, email: '', phoneNumber: '', sex: '' },
      ],
    };
  },
  methods: {
    /**
     * 键盘方向键移动
     * @param {*} vertical 垂直方向
     * @param {*} horizontal 水平方法
     * @param {*} index 表格索引
     * @param {*} columnFile 列名
     */
    move(vertical, horizontal, index, columnFile) {
      const rowIndex = index + vertical;
      const columnIndex = this.columns.indexOf(columnFile) + horizontal;
      console.log(rowIndex, columnIndex, 'columnIndex');
      // 控制在上下左右移动时,在需要移动的输入框内
      if (rowIndex >= 0 && rowIndex < this.tableData.length && columnIndex >= 1) {
        this.$nextTick(() => {
          const el = this.$refs['tableRef'].$refs.bodyWrapper.querySelector(
            `tr:nth-child(${rowIndex + 1}) td:nth-child(${columnIndex + 1}) input`
          );
          el.focus();
          this.timeout = setTimeout(() => {
            el.select();
          });
        });
      }
    },
  },
};

代码解释

首先需要把表格想象成一个二维表格,左上角为(0,0),第一行第二列为(0,1)以此类推

  • 首先确定需要移动哪些列,在代码里的columns存放的就是需要移动的列
  • move函数
    • 第一个参数因为上下移动都是一行一行的,所有每个传参都是1,正负值是区分是往上移动,还是往下移动
    • 第二个参数左右移动可能存在跨行移动,基本的左右移动都是一列一列传参都是一,正负值是区分是往左移动,还是往右移动,像地址着向右移动需要跨一列,在传参的时候就是2
    • 第三个参数index表格索引主要是来计算当前移到是哪一行,通过传入的值与当前表格索引进行计算,得出光标在一行就是 x
    • 第四个参数columnFile列名,之前已经定义需要移动的列columns,通过传入的字段与columns做比较计算出是在哪一列,得出光标在一列就是 y
  • 为什么这样写rowIndex >= 0 && rowIndex < this.tableData.length && columnIndex >= 1
    • rowIndex >= 0 && rowIndex < this.tableData.length:控制在垂直方向上不会越界
    • columnIndex >= 1 :控制在水平方向上不会越界,至于这里为什么是 >=1,因为它是从第二列开始做移动,如果是第一列就改为0,如果是第三列就改为2
  • 获取元素就是一层一层去获取,目前我是用elementui,如果你是用的其它ui框架就找到相应的方法去获取

动态表格案例

每一列都是动态的,思路和上面的差不多

QQ20240807-155451.png

  <el-table :data="tableData" style="width: 100%" ref="report-table" border>
      <el-table-column type="selection" width="55" v-if="isSelection"></el-table-column>
      <template v-for="(item, index) in tableColumn">
        <el-table-column v-if="item.display" :label="item.title" :key="index" :prop="item.fieldName">
          <template slot-scope="scope">
            <el-input
              v-if="item.inputType === 'input'"
              v-model="scope.row[item.fieldName]"
              placeholder="请输入内容"
              @keydown.up.native="move(-1, 0, scope.$index, item.fieldName)"
              @keydown.down.native="move(1, 0, scope.$index, item.fieldName)"
              @keydown.left.native="
                move(
                  0,
                  nextMoveFile(tableColumn, tableNeedMoveFieldNames, item.fieldName, -1),
                  scope.$index,
                  item.fieldName
                )
              "
              @keydown.right.native="
                move(
                  0,
                  nextMoveFile(tableColumn, tableNeedMoveFieldNames, item.fieldName, 1),
                  scope.$index,
                  item.fieldName
                )
              "
            />
            <el-input
              v-else-if="item.inputType === 'textarea'"
              type="textarea"
              v-model="scope.row[item.fieldName]"
              placeholder="请输入内容"
              @keydown.up.native="move(-1, 0, scope.$index, item.fieldName)"
              @keydown.down.native="move(1, 0, scope.$index, item.fieldName)"
              @keydown.left.native="
                move(
                  0,
                  nextMoveFile(tableColumn, tableNeedMoveFieldNames, item.fieldName, -1),
                  scope.$index,
                  item.fieldName
                )
              "
              @keydown.right.native="
                move(
                  0,
                  nextMoveFile(tableColumn, tableNeedMoveFieldNames, item.fieldName, 1),
                  scope.$index,
                  item.fieldName
                )
              "
            />
            <span v-else>{{ scope.row[item.fieldName] }}</span>
          </template>
        </el-table-column>
      </template>
    </el-table>

动态表格左右移动

  • 由于表格是动态显示,它的列是不固定的所有要动态去计算,也是最重要的一步
/**
 * 获取下上两个字段中间相隔多少步长
 * @param {*} column 表格显示列
 * @param {*} needMoveFiles 需要移动的字段
 * @param {*} file 当前字段
 * @param {*} horizontal 水平移动 left:-1 right:1
 */
nextMoveFile(column, needMoveFiles, file, horizontal) {
  // 获取页面上显示的列表字段
  const showLists = column.filter((item) => item.display).map((item) => item.fieldName);
  // 需要移动的字段
  const needMoveFile = showLists.filter((item) => needMoveFiles.includes(item));

  // 当前按下去的字段
  const findNowFileIndex = needMoveFile.indexOf(file);

  // 移动到下一个字段
  const nextFile = needMoveFile[findNowFileIndex + horizontal];
  // 需要移动的步数,
  const nowFileIndex = showLists.indexOf(file); // 当前按下字段索引
  const nextFileIndex = showLists.indexOf(nextFile); // 移动到下一个字段的索引
  const step = nextFileIndex - nowFileIndex;
  return step;
}
  data() {
    return {
      isSelection: false,
      tableNeedMoveFieldNames: ['phoneNumber', 'email', 'address'],
      tableData: [
        { name: 'A', address: '地址A', number: 1, email: 'a.qq', phoneNumber: '', sex: '男' },
        { name: 'B', address: '地址B', number: 2, email: 'b.qq', phoneNumber: '', sex: '女' },
        { name: 'C', address: '地址C', number: 3, email: 'c.qq', phoneNumber: '', sex: '女' },
        { name: 'D', address: '地址D', number: 4, email: '', phoneNumber: '', sex: '男' },
        { name: 'E', address: '地址E', number: 5, email: '', phoneNumber: '', sex: '男' },
      ],
      // 模拟动态列
      tableColumn: [
        {
          title: '性别',
          fieldName: 'sex',
          width: '105',
          sort: 0,
          display: true,
        },
        {
          title: '手机号',
          fieldName: 'phoneNumber',
          width: '105',
          sort: 1,
          display: true,
          inputType: 'textarea',
        },
        {
          title: '邮箱',
          fieldName: 'email',
          width: '105',
          sort: 2,
          display: true,
          inputType: 'input',
        },
        {
          title: '数量',
          fieldName: 'number',
          width: '105',
          sort: 3,
          display: true,
        },
        {
          title: '地址',
          fieldName: 'address',
          width: '105',
          sort: 4,
          display: true,
          inputType: 'input',
        },
        {
          title: '姓名',
          fieldName: 'name',
          width: '105',
          sort: 5,
          display: true,
        },
      ],
    };
  }
  • 通过nextMoveFile方法计算出左右间隔之后,再去调用move方法实现的形式和静态没有区别

  • isSelection控制是否有多选列,如果有在左右移动加2,没有左右移动加一

/**
 * 键盘方向键移动
 * @param {*} vertical 垂直方向
 * @param {*} horizontal 水平方法
 * @param {*} index 表格索引
 * @param {*} columnFile 列名
 */
move(vertical, horizontal, index, columnFile) {
  let rowIndex = index + vertical;
  let columnIndex = 0;
  columnIndex = this.returnCloumnIndex(this.tableColumn, columnFile) + horizontal;
  if (rowIndex >= 0 && rowIndex < this.tableData.length && columnIndex >= 0)) {
	this.moveFocus('report-table', rowIndex + 1, columnIndex + (this.isSelection ? 2 : 1));
  }
},
/**
 * 输入框获取焦点
 * @param {*} refName 元素节点
 * @param {*} rowIndex 行索引
 * @param {*} columnIndex 列索引
 */
moveFocus(refName, rowIndex, columnIndex) {
  this.$nextTick(() => {
	const element =
	  this.$refs[refName].$refs.bodyWrapper.querySelector(
		`tr:nth-child(${rowIndex}) td:nth-child(${columnIndex}) input`
	  ) ||
	  this.$refs[refName].$refs.bodyWrapper.querySelector(
		`tr:nth-child(${rowIndex}) td:nth-child(${columnIndex}) textarea`
	  );

	if (!element) {
	  return;
	}
	element.focus();
  });
},

/**
 * 返回列索引
 * @param {*} column 表格显示列
 * @param {*} field 字段
 * @returns number
 */
returnCloumnIndex(column, field) {
  if (column.length && field) {
	return column
	  .filter((item) => item.display)
	  .map((item) => item.fieldName)
	  .indexOf(field);
  }
  return 0;
},

结尾

上面就是整个实现过程,只要思路清楚,其实现实起来还是比较容易的

完整代码地址