✏️
ougege
  • README
  • Docs
    • index
    • Articles
      • AI
        • 体验Chrome AI
        • 体验Cloudflare Workers AI
        • 体验deepseek
      • CSS
        • CSS优化-PurgeCSS
        • 实用效果
        • 开发常用样式
      • Deepin
        • deepin20安装mysql
        • deepin使用tensorflow入门机器学习
        • deepin安装cuda和cuDNN
        • deepin安装lamp
        • deepin安装nvidia驱动
        • deepin安装oh my Zsh
        • deepin安装p7zip
        • deepin换源
        • 安装deepin系统后要做的事
      • Docker
        • CI/CD搭建配置
        • deepin搭建docker环境
        • docker安装和使用gitlab
        • docker搭建nginx+php环境
      • Essay
        • IOS申请邓白氏编码
        • Markdown-Mermaid
        • Markdown Use
        • webview白屏的问题查找和修复
        • 前端开发对接问题和解决办法汇总
        • 国务院机构改革方案
        • 国家级智库
        • 实用网站推荐
        • 常用Markdown数学公式语法
        • 强烈推荐前端要安装的vscode扩展
        • 新建销售计划-页面卡死问题分析
        • 海淘入坑手册
        • 竞品研究
        • 足球知识速成
      • Git
        • GitBook安装和常用命令
        • GitKraken免费版本
        • Git安装和配置
        • Git异常处理
        • Git Worktree使用
        • 前端工程化相关的实用git命令
      • JS
        • ESM模块导出方式对比
        • Emoji多端统一处理
        • JS发布订阅模式
        • JS性能优化
        • JS标准内置对象
        • JS链式调用原理
        • Promise介绍和使用
        • Range的使用
        • Vue+Oauth登录实现
        • Vue实现富文本插入Emoji
        • chrome扩展入门
        • es5新特性
        • es6常用特性
        • es常用片段
        • uniapp使用eslint校验代码
        • 与移动端通信
        • 优秀js库moment
        • 使用vue-socketio
        • 实现一个中间件
        • 小程序webview调试
        • 常用snippets
        • 常用正则
        • 常用的设计模式
        • 微信jssdk封装使用
        • 浏览器宏任务和微任务
        • 浏览器的5种Observer
        • 深入理解赋值、浅拷贝、深拷贝
        • 解析vue指令clickoutside源码
        • 键盘事件与KeyBoardWrapper交互
        • 高德地图常用方法封装
        • 高阶函数片段
      • Network
        • 使用Lighthouse分析前端性能
        • 前后端启用https
        • 宝塔nginx安装ngx_brotli
        • 比较gz与br加载速度
        • 浏览器https提示不安全
        • 浏览器提示HSTS
        • 简单使用tcpdump
        • 静态资源gzip优化
      • Node
        • CommonJS模块导出方式对比
        • Taro command not found 多平台解决方案
        • koa使用和API实现
        • node安装报错Unexpected-token
        • 使用nvm和nrm
        • 使用uniapp给小程序添加云函数
        • 使用verdaccio搭建本地npm仓库
        • 使用vue-cli搭建vue项目
        • 安装Node.js和npm配置
        • 编译成cjs和mjs的思路解析
        • 让你的npmPackage同时支持cjs和mjs
        • 通过GithubAction将内容部署到vps
      • Python
        • Python源管理
        • Python版本管理
        • mitmproxy抓包
        • 微信公众平台开发爬坑经历
      • Shell
        • Ubuntu安装deepin桌面环境
        • Ubuntu安装flatpak软件
        • Ubuntu安装wireshark
        • Ubuntu常见问题汇总
        • dell G3装系统无法识别第二块硬盘
        • linux下virtualbox用gho还原系统
        • mysql常用命令
        • navicat连接一键集成环境的mysql
        • nginx常用命令
        • pm2常用命令
        • virtualbox虚拟机和宿主机互相复制粘贴
        • vps内资源通过mega快传到本地
        • vps报错temporary failure in name resolution
        • vscode修改文件监控数
        • windows+linux双系统引导修复
        • zsh常用插件和命令
        • 一键搭建ChatGPT web版
        • 使用V2ray,CloudFlare Warp解锁GPT
        • 使用vscode进行java开发
        • 利用zx和SSHKey发布代码到服务器
        • 反爬虫一些方案总结和尝试
        • 安装1Panel
        • 安装Bt面板
        • 安装Ubuntu22.04后要做的事
        • 无显示器linux设置默认分辨率
        • 特别实用的shell命令
        • 解决linux安装xmind缺少依赖报错
      • Standards
        • CSS格式化之stylelint
        • CSS规范
        • HTML规范
        • JS规范
        • commit规范
        • 使用husky+commitlint规范代码提交
        • 使用semantic-release自动管理版本号
        • 命名规范
        • 图片规范
        • 版本编号规范
      • Wall
        • 科学上网-Cloudflare-Pages
        • 科学上网-Cloudflare-Warp
        • 科学上网-Geph
        • 科学上网-RackNerd
        • 科学上网-Slicehosting
        • 科学上网-Surfshark
        • 科学上网-Tor
        • 科学上网-XX-NET
        • 科学上网-heroku
        • 科学上网-shadowsock
        • 科学上网-v2ray使用
        • 科学上网-v2ray搭建
        • 科学上网-浏览器代理
        • 科学上网-让终端走代理
      • Windows
        • SourceTree破解免登录(windows版)
        • git bash交互提示符不工作
        • nexus 7 2013 wifi 刷机
        • tree命令生成文件目录
        • 利用charles抓包app
        • 安装Openssl
        • 安装msi文件报错2503和2502
        • 神器vimium使用说明
        • 自用host
        • 解决win10扩展出来的屏幕模糊
        • 解决安装Adobe Air时发生错误
    • Snippets
      • zsh
        • docker
        • extract
        • git-commit
        • git
        • mysql-macports
        • npm
        • nvm
        • pip
        • pm2
        • systemd
        • ubuntu
        • vscode
Powered by GitBook
On this page
  • 场景
  • 原理
  • KeyBoard.js
  • KeyBoardWrapper.vue
  • 使用

Was this helpful?

  1. Docs
  2. Articles
  3. JS

键盘事件与KeyBoardWrapper交互

场景

用遥控器控制网页浏览

原理

  1. 实现一个 KeyBoard.js 类库

  2. 创造一个 KeyBoardWrapper 组件,每个组件生成对应的坐标信息和唯一 id

  3. KeyBoardWrapper 组件生成的实例交由 KeyBoard 维护

  4. 执行 keyup 事件分发,结合四条边坐标算出最近的实例

  5. 操作该实例上的方法

KeyBoard.js

const KeyBoard = {
  allNode: [], // 页面所有keyBoardWrapper节点
  allNodePos: [], // 所有实例的中心坐标信息
  isInDialog: false, // 当前focus是否处在dialog状态
  addDialogNodePos: [], // 处于dialog状态所有实例的中心坐标信息
  fitDirectionNodePos: [], // 符合当前操作方向的中心坐标信息
  perfectNodeInfo: {}, // 方向操作后最佳的节点信息
  currentOperateNodeInfo: {}, // 当前正在操作的节点信息
  currentOperateVNode: {}, // 当前正在操作的vNode
  scaleLen: 10, // 这里要加入模糊距离,暂定10px,因为在选中状态,scale大于1,当前块的左侧可能比符合匹配的块右侧更小
  // 解键盘的key值
  keyup (e) {
    let obj = e.srcElement.dataset
    this.main(e.keyCode, obj)
  },
  // click进入,找到最近满足的节点
  click (e) {
    // 对path冒泡
    let obj = {} 
    for (let i = 0; i < e.path.length; i++) {
      if (e.path[i].dataset && e.path[i].dataset.kbwid) {
        obj = e.path[i].dataset
        break
      }
    }
    if (obj.kbwid) {
      this.excute(obj.kbwid)
    } else {
      console.log('未找到节点')
    }
  },
  // 主方法: 传递节点,发射事件
  main (keyCode, obj) {
    switch (keyCode) {
      // 上
      case 38:
      // 下
      case 40:
      // 左
      case 37:
      // 右
      case 39:
        this.commonFn(keyCode, obj)
        break
      // 返回
      case 8:
        this.clickBackAndEnter(obj)
        this.currentOperateVNode.back()
        break
      // enter
      case 13:
        this.clickBackAndEnter(obj)
        this.currentOperateVNode.enter()
        break
      default:
        break
    }
  },
  // 统一方法: 上下左右
  commonFn (keyCode, obj) {
    // 找出当前正在操作的节点信息
    this.currentOperateNodeInfo = this.findCurrentOperateNodeInfo(obj.kbwid)
    // 找出当前正在操作的vNode
    this.currentOperateVNode = this.findCurrentOperateVNode(obj.kbwid)
    // 找出符合当前操作方向的所有节点
    if (this.isInDialog) {
      this.fitDirectionNodePos = this.findFixDirectionNodePos(keyCode, this.addDialogNodePos, this.currentOperateNodeInfo)
    } else {
      this.fitDirectionNodePos = this.findFixDirectionNodePos(keyCode, this.allNodePos, this.currentOperateNodeInfo)
    }
    // 计算并更新符合方向所有节点与当前节点位置关系
    this.fitDirectionNodePos = this.calAllNodeDistance(keyCode, this.fitDirectionNodePos, this.currentOperateNodeInfo)
    // 找出符合当前操作方向最优节点的kbwid
    let perfectId = ''
    if (this.fitDirectionNodePos.length > 0) {
      perfectId = this.findPerfectVnodeId(this.fitDirectionNodePos)
    } else {
      perfectId = this.currentOperateNodeInfo.kbwid
    }
    console.log(perfectId)
    console.log(this.allNode)
    console.log(this.allNodePos)
    console.log(this.fitDirectionNodePos)
    // 操作最优节点执行动作
    this.excute(perfectId)
  },
  // back和enter点击事件
  clickBackAndEnter (obj) {
    // 找出当前正在操作的节点信息
    this.currentOperateNodeInfo = this.findCurrentOperateNodeInfo(obj.kbwid)
    // 找出当前正在操作的vNode
    this.currentOperateVNode = this.findCurrentOperateVNode(obj.kbwid)
    this.currentOperateVNode.isActive = true
    this.currentOperateVNode.$el.focus()
  },
  // 找到对应的实例执行方法
  excute (perfectId) {
    let vNode = {}
    for (let i = 0; i < this.allNode.length; i++) {
      if (this.allNode[i].kbwid === perfectId) {
        vNode = this.allNode[i]
        vNode.isActive = true
        vNode.$el.focus()
      } else {
        vNode = this.allNode[i]
        vNode.isActive = false
      }
    }
  },
  // 每次keyBoardWrapper创建时传入自身实例
  create (e) {
    this.allNode.push(e)
    const pos = this.storePos(e)
    this.allNodePos.push(pos)
    if (e.isInDialog) {
      this.addDialogNodePos.push(pos)
    }
  },
  // 每次keyBoardWrapper销毁时
  destroy (e) {
    // 从数组中删除自身实例
    for (let i = 0; i < this.allNode.length; i++) {
      if (this.allNode[i].kbwid === e.kbwid) {
        this.allNode.splice(i, 1)
        break
      }
    }
    // 删除自身坐标
    for (let j = 0; j < this.allNodePos.length; j++) {
      if (this.allNodePos[j].kbwid === e.kbwid) {
        this.allNodePos.splice(j, 1)
        break
      }
    }
    // 删除带isInDialog标记坐标
    if (e.isInDialog) {
      for (let j = 0; j < this.addDialogNodePos.length; j++) {
        if (this.addDialogNodePos[j].kbwid === e.kbwid) {
          this.addDialogNodePos.splice(j, 1)
          break
        }
      }
    }
  },
  // 所有页面默认第一个实例设置为active
  firstSetActive () {
    if (this.allNode.length > 0) {
      this.allNode[0].isActive = true
      this.allNode[0].$el.focus()
    }
  },
  // 存储所有实例坐标信息
  // 新方式使用4条边的中点坐标
  storePos (e) {
    const el = e.$el
    const offsetWidth = el.offsetWidth
    const offsetHeight = el.offsetHeight
    const offsetTop = el.offsetTop
    const offsetLeft = el.offsetLeft
    const kbwid = e.kbwid
    // 上边中点
    const upCenter = { x: offsetLeft + offsetWidth / 2, y: offsetTop }
    // 下边中点
    const downCenter = { x: offsetLeft + offsetWidth / 2, y: offsetTop + offsetHeight }
    // 左边中点
    const leftCenter = { x: offsetLeft, y: offsetTop + offsetHeight / 2 }
    // 右边中点
    const rightCenter = { x: offsetLeft + offsetWidth, y: offsetTop + offsetHeight / 2 }
    const pos = { upCenter, downCenter, leftCenter, rightCenter, kbwid }
    return pos
  },
  // 计算俩点间距离(勾股定理)
  calTwoPointDistance (a, b) {
    const straightLen_1 = Math.abs(a.x - b.x)
    const straightLen_2 = Math.abs(a.y - b.y)
    const slashLen = Math.sqrt(Math.pow(straightLen_1, 2) + Math.pow(straightLen_2, 2))
    return slashLen
  },
  // 找出当前正在操作的节点
  findCurrentOperateNodeInfo (kbwid) {
    let currentOperateNodeInfo = {}
    for (let i = 0; i < this.allNodePos.length; i++) {
      if (this.allNodePos[i].kbwid === kbwid) {
        currentOperateNodeInfo = this.allNodePos[i]
        break
      }
    }
    return currentOperateNodeInfo
  },
  // 找出当前正早操作的vNode
  findCurrentOperateVNode (kbwid) {
    let currentOperateVNode = {}
    for (let i = 0; i < this.allNode.length; i++) {
      if (this.allNode[i].kbwid === kbwid) {
        currentOperateVNode = this.allNode[i]
        break
      }
    }
    return currentOperateVNode
  },
  // 计算所有节点与当前节点的位置关系
  calAllNodeDistance (keyCode, arr, currentOperateNodeInfo) {
    console.log(arr)
    console.log(currentOperateNodeInfo)
    switch (keyCode) {
      // 上
      case 38:
        return arr.map((e) => {
          e.distance = this.calTwoPointDistance(e.downCenter, currentOperateNodeInfo.upCenter)
          return e
        })
        break
      // 下
      case 40:
        return arr.map((e) => {
          e.distance = this.calTwoPointDistance(e.upCenter, currentOperateNodeInfo.downCenter)
          return e
        })
        break
      // 左
      case 37:
        return arr.map((e) => {
          e.distance = this.calTwoPointDistance(e.rightCenter, currentOperateNodeInfo.leftCenter)
          return e
        })
        break
      // 右
      case 39:
        return arr.map((e) => {
          e.distance = this.calTwoPointDistance(e.leftCenter, currentOperateNodeInfo.rightCenter)
          return e
        })
        break
      default:
        break
    }
  },
  // 找出距离最优的实例id
  findPerfectVnodeId (arr) {
    let smDis = arr[0].distance
    let smId = arr[0].kbwid
    for (let i = 0; i < arr.length; i++) {
      if (arr[i].distance < smDis) {
        smDis = arr[i].distance
        smId = arr[i].kbwid
      }
    }
    return smId
  },
  // 找出符合当前操作方向的所有中心坐标
  // 注意:这里要加入模糊距离,暂定20px,因为在选中状态,当前块的左侧可能比符合匹配的块右侧更小
  findFixDirectionNodePos (keyCode, allNodePos, currentOperateNodeInfo) {
    let fitDirectionNodePos = []
    switch (keyCode) {
      // 上
      case 38:
        for (let i = 0; i < allNodePos.length; i++) {
          if (allNodePos[i].downCenter.y < currentOperateNodeInfo.upCenter.y + this.scaleLen) {
            fitDirectionNodePos.push(allNodePos[i])
          }
        }
        break
      // 下
      case 40:
        for (let i = 0; i < allNodePos.length; i++) {
          if (allNodePos[i].upCenter.y > currentOperateNodeInfo.downCenter.y - this.scaleLen) {
            fitDirectionNodePos.push(allNodePos[i])
          }
        }
        break
      // 左
      case 37:
        for (let i = 0; i < allNodePos.length; i++) {
          if (allNodePos[i].rightCenter.x < currentOperateNodeInfo.leftCenter.x + this.scaleLen) {
            fitDirectionNodePos.push(allNodePos[i])
          }
        }
        break
      // 右
      case 39:
        for (let i = 0; i < allNodePos.length; i++) {
          if (allNodePos[i].leftCenter.x > currentOperateNodeInfo.rightCenter.x - this.scaleLen ) {
            fitDirectionNodePos.push(allNodePos[i])
          }
        }
        break
      default:
        break
    }
    return fitDirectionNodePos
  },
  // 标记是否处在dialog状态: 用于控制此时上下左右的区域限制
  changeDialogStatus (e) {
    this.isInDialog = e
  }
}
export { KeyBoard }

KeyBoardWrapper.vue

<template>
  <div 
    :class="isActive ? 'active component keyBoardWrapper' : 'component keyBoardWrapper'"
    :style="myStyle"
    :tabindex="keyIdx"
    :data-kbwid="kbwid">
    <!-- 插槽:显示自定义内容 -->
    <slot></slot>
  </div>
</template>

<script>
import { config, utilFn, KeyBoard } from '../../untils/index'
export default {
  props: {
    item: {
      type: Object,
      default: {}
    },
    keyIdx: {
      type: Number,
      default: 0
    },
    // postcss不支持行内样式
    myStyle: {
      type: String,
      default: ''
    },
    isInDialog: {
      type: Boolean,
      default: false
    }
  },
  components: {},
  computed: {},
  watch: {
    'isActive': function (newObj) {
      this.vNodeFocus(newObj)
    }
  },
  data () {
    return {
      staticHost: config.staticHost,
      defaultImg: config.defaultImg,
      assets: config.defaultImg.assets,
      kbwid: '',
      isActive: false
    }
  },
  beforeCreate () {},
  created () {},
  beforeMount () {},
  mounted () {
    this.kbwid = utilFn.createKeyWrapperId()
    // 将实例传入KeyBoard
    KeyBoard.create(this)
    // 通知keyBoard是否处在dialog
    KeyBoard.changeDialogStatus(this.isInDialog)
  },
  beforeUpdate () {},
  updated () {},
  beforeDestroy () {},
  destroyed () {
    // 将KeyBoard对应实例销毁
    KeyBoard.destroy(this)
  },
  methods: {
    // 确认
    enter (e) {
      // 使用$parent传值
      this.transToParent()
      this.$emit('enter', null)
    },
    // 返回
    back (e) {
      this.$emit('back', e)
    },
    // 节点focus
    vNodeFocus (e) {
      // 这里只有当前节点和下一节点会触发watch isActive, 
      if (e) {
        // 得焦
        this.transToParent()
        this.$emit('focus', { e, keyIdx: this.keyIdx })
      } else {
        // 失焦
        this.$emit('blur', { e, keyIdx: this.keyIdx })
      }
    },
    // 给父组件传值
    transToParent () {
      let currentOperateObj = {}
      if (this.$parent.hasOwnProperty('currentOperateObj')) {
        currentOperateObj = {...this.item, kbwid: this.kbwid }
      }
      this.$parent.currentOperateObj = currentOperateObj
    }
  }
}
</script>

<style lang='scss' scoped>
</style>

使用

<KeyBoardWrapper
  v-for="(item, index) in beaforeOptions"
  :item="item"
  :key="index"
  :keyIdx="index"
  :myStyle="beaforeStyle"
  @enter="beaforeEnter"
  @back="beaforeBack">
  <div class="group">
    <span :class="'iconfont ' + item.icon "></span>
    <span class="tips">{{item.label}}</span>
  </div>
</KeyBoardWrapper>
Previous解析vue指令clickoutside源码Next高德地图常用方法封装

Last updated 10 months ago

Was this helpful?