gmfe / Think

观麦前端团队的官方博客
68 stars 3 forks source link

Table接入全键盘总结 #76

Open liyatang opened 5 years ago

liyatang commented 5 years ago

背景

在信息录入场景下,如果涉及到大量数据录入,那肯定是一个头疼的事情。 之前网站信息录入是鼠标点点点,效率很低。 于是探索键盘快速录入的方案。目标做到类似 Excel 一样便捷,达到快速录入信息的目的。

于是有个同事折腾了一个月做了出来,对于一个新人来说很不错了。 同时存在了以下问题

于是重新思考了快速录入,重构了下。 以下是思路和些总结

思路

设计思想

理想中是代码是

Table
  - input
  - select
// 接入键盘能力
Wrap Table
  - Cell input
  - Cell select

想要获得键盘能力,提供一个 Wrap 包裹下 键盘要控制单元格,提供一个 Cell 包裹下

Cell 负责响应和反馈。监听键盘事件,把 方向、Enter、Tab 反馈给 Wrap。响应 Wrap 的 focus 事件。 Wrap 负责调度。接收来自 Cell 的反馈,然后做出下一个动作,其中包括 focus 到 Cell

思考

为什么不是 Wrap 监听键盘事件? 如果 Wrap 监听键盘事件,那逻辑处理也在 Wrap,Wrap 就需要关心具体 Cell 怎么处理键盘事件。但是 Cell 是复杂的,没法再 Wrap 层处理,情况太多。 所以键盘是在 Cell 处理的。

怎么通信? Wrap 和 Cell 是分开的,所以不能直接通过 props 通信。 那有什么好方法?想到原生事件 dispatchEvent addEventListener。 如果多个信息录入怎么确定事件是那一个? 给 Wrap 补个 id props,把 id 通过 Context 传递给 Cell。这样就能保证信息准确传达了。

怎么定位到 Cell ? 比如 Wrap 需要 focus 到某个 Cell,这就要求 Cell 有身份标识。这很好解决,给 Cell 定个 cellKey props 即可。cellKey 规则是 rowIndex_field。 你可能有疑问为什么是 field,而不是 columnIndex?因为表格的数据结构是 [{name, age}, {name, age}],并没有 columnIndex。 那在 CellName 按方向键右的时候怎么知道 focus 到 CellAge 呢?只能给 Wrap 额外的参数了columnKeys = [’name’, ‘age’]。这样就很方便定位到单元格了。

listLength 是什么? 下到最后一行继续下回增加一行,但是 Wrap 并清除有多少行,所以 给 Wrap 再加个参数 listLength 即可。

Cell 怎么实现 onFocus? 如果是 input 就直接 refInput.current.focus()。然而 Cell 的内部是任意的,是复杂的,比如 MoreSelect 组件。当然 focus 应该是组件的事情,所以 MoreSelect 需要实现 focus api,refMoreSelect.current.apiDoFocus() 即可。

至此,一个大体框架就出来了,具体的看代码吧 keyboard_table

其他

Event

监听事件用 keydown? 还是 keyup? 按住 keydown,会一直触发 keydown,更符合直觉。

使用 event.key,非废弃的 event.keyCode。为什么?

需要阻止默认行为? 需要。 比如右方向键调到下一个input,你灰发现光标会变为这样 ‘1|23’ (input 的文本是 123,光标是 |)。默认行为发生在代码之后 比如 input number,上下是数字增大增小。 文本内移动? selectionStart selectionEnd 判断。但是 input number 的值是 null。 why ?

focus

blur 不能冒泡,用 focusout focusout 产生了另外一个问题,就是 onClick 触发不了,因为 foucusout 之后隐藏掉了 UI,自然点击不到。 想到了2个解决方法

不是所有元素都可以focus,比如 input 可以,但是 div 是不可以。 可以加上 tabIndex

历史记录

首先想到的是 redux 的时光穿梭。 还有 mobx-state-tree

上面都是些系统级别的时光穿梭。本需求还用不到这么复杂的。想起之前关注的一个库 Javascript-Undo-Manage 他不是从数据层面上来做历史记录,而是从动作上,这个动作有两个行为 undo redo,具体是调用方实现。

看看代码看多简单

    const handleChange = event => {
    const oldValue = value
    const nowValue = event.target.value
    cellRef.current.apiAddUndo({
      undo: () => {
        onChangeValue(oldValue)
      },
      redo: () => {
        onChangeValue(nowValue)
      }
    })
    onChange(event)
  }

感悟

简单化,一切都可简单化。 沟通交流,相互提升认知。