js 键盘监听 组合键

news2024/9/25 21:20:36

今天分享如何快速实现js快捷键监听

所需环境:

  • 浏览器
  • js

实现目标

  • mac/win兼容,一套代码,多个平台
  • 支持快捷键监听/单按键监听
  • 事件是否冒泡可设置
  • 使用方式简单
  • 快速挂载与卸载
  • 4行代码实现组合键监听

代码原理

把键盘监听事件挂载在document上,当键盘事件发生时,依次匹配事先订阅的快捷键/单按键事件,
如果有匹配到对应的订阅,则进行事件回调,并且停止键盘事件的回调。按下的按键进行转换,把mac/win的键盘差异进行了兼容

实现效果

在这里插入图片描述

js快捷键实战

核心代码

interface keyListener {
    keys: Array<string | ((event: KeyboardEvent) => boolean)>;
    // ALL 全部符合才算| ANY 匹配到任意一个就算
    matchType: 'ALL' | 'ANY';
    callback: (keyStr: string) => void;
    // 是否停止时间传播
    stop: boolean;
}

// 多平台键盘转换
const keyConvert = {
    Ctrl: ['Meta', 'Ctrl']
} as Record<string, Array<string>>;

const eventListeners: Array<keyListener> = [];
const downKeyList = {} as any;

export function addKeyboardEvent() {
    document.addEventListener('keydown', keyDown);
    document.addEventListener('keyup', keyUp);

    const handlers = {
        subscribe(keys: Array<string | ((event: KeyboardEvent) => boolean)>, callback: (keyStr: string) => void, stop = true) {
            eventListeners.push({
                keys,
                matchType: 'ALL',
                callback,
                stop
            });
            return handlers;
        },
        subscribeAny(keys: Array<string | ((event: KeyboardEvent) => boolean)>, callback: (keyStr: string) => void, stop = true) {
            eventListeners.push({
                keys,
                matchType: 'ANY',
                callback,
                stop
            });
            return handlers;
        }
    };
    return handlers;
}

export function removeKeyboardEvent() {
    document.removeEventListener('keydown', keyDown);
    document.removeEventListener('keyup', keyUp);
}

function keyUp(event: KeyboardEvent) {
    delete downKeyList[convertKey(event.key)];
}

function convertKey(key: string) {
    for (let keyConvertKey in keyConvert) {
        let convertList = keyConvert[keyConvertKey];
        if (convertList.includes(key)) {
            return keyConvertKey;
        }
    }
    return key;
}

function keyDown(event: KeyboardEvent) {
    // 如果需要输入框,则不监听组合键
    if ((event.target as HTMLElement).tagName === 'INPUT' || (event.target as HTMLElement).tagName === 'TEXTAREA') {
        // console.log('This event is triggered by an input or textarea!');
        return;
    }

    for (let eventListener of eventListeners) {
        let matchResult = false
        if (eventListener.matchType == 'ALL') {
            matchResult = matchAll(event, eventListener)
        } else {
            matchResult = matchAny(event, eventListener)
        }
        if (matchResult) {
            break
        }
    }
}

function matchAll(event: KeyboardEvent, keyListener: keyListener) {
    const {keys, callback, stop} = keyListener;
    let isTrigger = true;
    let keyStr = ''
    for (let key of keys) {
        if (key instanceof Function) {
            keyStr = keyStr + key.name + "+"
            if (!key(event)) {
                isTrigger = false;
                break;
            }
        } else {
            keyStr = keyStr + event.key + "+"

            if (key != event.key) {
                isTrigger = false;
                break;
            }
        }
    }
    if (isTrigger) {
        keyStr = keyStr.slice(0, -1);
        callback(keyStr);
        if (stop) {
            event.preventDefault();
            event.stopPropagation();
        }
        return true;
    }
    return false
}

function matchAny(event: KeyboardEvent, keyListener: keyListener) {
    const {keys, callback, stop} = keyListener;
    let isTrigger = false;
    let keyStr = ''
    for (let key of keys) {
        if (key instanceof Function) {
            if (key(event)) {
                keyStr = keyStr + key.name + "+"
                isTrigger = true;
            }
        } else {
            if (key == event.key) {
                keyStr = keyStr + event.key + "+"
                isTrigger = true;
            }
        }

        if (isTrigger) {
            keyStr = keyStr.slice(0, -1);
            callback(keyStr);
            if (stop) {
                event.preventDefault();
                event.stopPropagation();
            }
            return true
        }
    }
    return false
}

// 兼容macos & win
export function isCtrl(event: KeyboardEvent) {
    return event.ctrlKey || event.metaKey;
}

export function isShift(event: KeyboardEvent) {
    return event.shiftKey;
}

export function isCtrlShift(event: KeyboardEvent) {
    return (event.ctrlKey || event.metaKey) && event.shiftKey;
}

export function isDelete(event: KeyboardEvent) {
    return event.key == 'Backspace';
}

使用示例

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>MyPrint-快捷键示例</title>
    <style>
        :root {
            font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
            line-height: 1.5;
            font-weight: 400;
            
            color-scheme: light dark;
            color: rgba(255, 255, 255, 0.87);
            background-color: #242424;
            
            font-synthesis: none;
            text-rendering: optimizeLegibility;
            -webkit-font-smoothing: antialiased;
            -moz-osx-font-smoothing: grayscale;
        }
        
        body {
            display: flex;
            height: 100vh;
            place-items: center;
            width: 100%;
            min-height: 100vh;
            margin: 0 auto;
            text-align: center;
        }
        
        #keyboardShortcutsStr {
            width: 100%;
        }
    </style>
</head>
<body>
<div id="keyboardShortcutsStr"></div>
<script>
    function setKeyboardShortcutsStr(keyboardShortcutsStr) {
        document.querySelector("#keyboardShortcutsStr").innerHTML = keyboardShortcutsStr
    }
    
    var keyConvert = {
        Ctrl: ['Meta', 'Ctrl']
    };
    var eventListeners = [];
    var downKeyList = {};
    
    function addKeyboardEvent() {
        document.addEventListener('keydown', keyDown);
        document.addEventListener('keyup', keyUp);
        var handlers = {
            subscribe: function (keys, callback, stop) {
                if (stop === void 0) {
                    stop = true;
                }
                eventListeners.push({
                    keys: keys,
                    matchType: 'ALL',
                    callback: callback,
                    stop: stop
                });
                return handlers;
            },
            subscribeAny: function (keys, callback, stop) {
                if (stop === void 0) {
                    stop = true;
                }
                eventListeners.push({
                    keys: keys,
                    matchType: 'ANY',
                    callback: callback,
                    stop: stop
                });
                return handlers;
            }
        };
        return handlers;
    }
    
    function removeKeyboardEvent() {
        document.removeEventListener('keydown', keyDown);
        document.removeEventListener('keyup', keyUp);
    }
    
    function keyUp(event) {
        delete downKeyList[convertKey(event.key)];
    }
    
    function convertKey(key) {
        for (var keyConvertKey in keyConvert) {
            var convertList = keyConvert[keyConvertKey];
            if (convertList.includes(key)) {
                return keyConvertKey;
            }
        }
        return key;
    }
    
    function keyDown(event) {
        // 如果需要输入框,则不监听组合键
        if (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') {
            // console.log('This event is triggered by an input or textarea!');
            return;
        }
        for (var _i = 0, eventListeners_1 = eventListeners; _i < eventListeners_1.length; _i++) {
            var eventListener = eventListeners_1[_i];
            var matchResult = false;
            if (eventListener.matchType == 'ALL') {
                matchResult = matchAll(event, eventListener);
            } else {
                matchResult = matchAny(event, eventListener);
            }
            if (matchResult) {
                break;
            }
        }
    }
    
    function matchAll(event, keyListener) {
        var keys = keyListener.keys, callback = keyListener.callback, stop = keyListener.stop;
        var isTrigger = true;
        var keyStr = '';
        for (var _i = 0, keys_1 = keys; _i < keys_1.length; _i++) {
            var key = keys_1[_i];
            if (key instanceof Function) {
                keyStr = keyStr + key.name + "+";
                if (!key(event)) {
                    isTrigger = false;
                    break;
                }
            } else {
                keyStr = keyStr + event.key + "+";
                if (key != event.key) {
                    isTrigger = false;
                    break;
                }
            }
        }
        if (isTrigger) {
            keyStr = keyStr.slice(0, -1);
            callback(keyStr);
            if (stop) {
                event.preventDefault();
                event.stopPropagation();
            }
            return true;
        }
        return false;
    }
    
    function matchAny(event, keyListener) {
        var keys = keyListener.keys, callback = keyListener.callback, stop = keyListener.stop;
        var isTrigger = false;
        var keyStr = '';
        for (var _i = 0, keys_2 = keys; _i < keys_2.length; _i++) {
            var key = keys_2[_i];
            if (key instanceof Function) {
                if (key(event)) {
                    keyStr = keyStr + key.name + "+";
                    isTrigger = true;
                }
            } else {
                if (key == event.key) {
                    keyStr = keyStr + event.key + "+";
                    isTrigger = true;
                }
            }
            if (isTrigger) {
                keyStr = keyStr.slice(0, -1);
                callback(keyStr);
                if (stop) {
                    event.preventDefault();
                    event.stopPropagation();
                }
                return true;
            }
        }
        return false;
    }
    
    // 兼容macos & win
    function isCtrl(event) {
        return event.ctrlKey || event.metaKey;
    }
    
    function isShift(event) {
        return event.shiftKey;
    }
    
    function isCtrlShift(event) {
        return (event.ctrlKey || event.metaKey) && event.shiftKey;
    }
    
    function isDelete(event) {
        return event.key == 'Backspace';
    }
    
    
    addKeyboardEvent()
            // macos
            .subscribe([isCtrlShift, 'z'], (_keyStr) => {
                setKeyboardShortcutsStr('Ctrl+Shift+z 重做')
            })
            // win
            .subscribe([isCtrlShift, 'y'], () => {
                setKeyboardShortcutsStr('Ctrl+y')
            })
            .subscribe([isCtrl, 'z'], () => {
                setKeyboardShortcutsStr('Ctrl+z 撤销')
            })
            .subscribe([isCtrl, 'a'], () => {
                setKeyboardShortcutsStr('Ctrl+a 全选')
            })
            .subscribe([isCtrl, 'c'], () => {
                setKeyboardShortcutsStr('Ctrl+c 复制')
            })
            .subscribe([isCtrl, 'x'], () => {
                setKeyboardShortcutsStr('Ctrl+c 剪切')
            })
            .subscribe([isCtrl, 'v'], () => {
                setKeyboardShortcutsStr('Ctrl+v 粘贴')
            })
            .subscribe([isCtrl, 'd'], () => {
                setKeyboardShortcutsStr('Ctrl+d 副本')
            })
            .subscribe([isCtrl, 's'], () => {
                setKeyboardShortcutsStr('Ctrl+s 保存')
            })
            .subscribe([isCtrl, 'f'], () => {
                setKeyboardShortcutsStr('Ctrl+f 搜索')
            })
            .subscribe(['Tab'], () => {
                setKeyboardShortcutsStr('Tab切换')
            })
            
            .subscribe([isCtrlShift, 'ArrowUp'], () => {
                // console.log('ArrowUp')
            })
            .subscribe([isCtrlShift, 'ArrowDown'], () => {
                // console.log('ArrowDown')
            })
            .subscribe([isCtrlShift, 'ArrowLeft'], () => {
                // console.log('ArrowLeft')
            })
            .subscribe([isCtrlShift, 'ArrowRight'], () => {
                // console.log('ArrowRight')
            })
            
            .subscribe([isShift, 'ArrowUp'], () => {
                // console.log('ArrowUp')
            })
            .subscribe([isShift, 'ArrowDown'], () => {
            })
            .subscribe([isShift, 'ArrowLeft'], () => {
            })
            .subscribe([isShift, 'ArrowRight'], () => {
            })
            
            .subscribe([isCtrl, 'ArrowUp'], () => {
            })
            .subscribe([isCtrl, 'ArrowDown'], () => {
            })
            .subscribe([isCtrl, 'ArrowLeft'], () => {
            })
            .subscribe([isCtrl, 'ArrowRight'], () => {
            })
            
            .subscribe(['ArrowUp'], () => {
            })
            .subscribe(['ArrowDown'], () => {
            })
            .subscribe(['ArrowLeft'], () => {
            })
            .subscribe(['ArrowRight'], () => {
            })
            
            .subscribe([isDelete], () => {
                // console.log('ArrowRight')
            })
            .subscribeAny(['q', 'w', 'e', 'r', 't', 'a', 'b', 'c', 'd'], (keyStr) => {
                // console.log('ArrowRight')
                setKeyboardShortcutsStr(keyStr)
            }, false);
</script>
</body>
</html>

代码仓库

在线体验

代码仓库:github

代码仓库:gitee

实战项目:MyPrint

操作简单,组件丰富的一站式打印解决方案打印设计器

体验地址:前往

代码仓库:github

代码仓库:gitee

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2067493.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

c#-DataGridView控件实现分页

有时候我们需要进行分页显示&#xff0c;第一方面是在大数据量下可以降低卡顿&#xff0c;另一方面也是方便查找。 首先划重点&#xff0c;如果卡顿&#xff0c;不要用单元格填充的方式去刷新&#xff0c;用绑定数据源的方式比较高效&#xff01; 下面重点讲如何使用数据源绑定…

正式收官!阿里云携手优酷,用AI重塑影视IP创新边界

影视行业的新一轮创作风潮&#xff0c;将由AI掀起。 GPT和Sora等先进AI模型的出现&#xff0c;带动影视行业进入一场前所未有的创意变革。当前&#xff0c;在角色创作、脚本生成、营销策略等方面&#xff0c;AI已经展现了强大的潜力。而作为影视创作的“灵魂”&#xff0c;影视…

重新审视 ChatGPT 和 Elasticsearch:RAG 真正将应用程序紧密结合在一起

作者&#xff1a;来自 Elastic Jeff Vestal 关注博客 ChatGPT 和 Elasticsearch&#xff1a;OpenAI 遇到私人数据。 在此博客中&#xff0c;你将了解如何&#xff1a; 创建 Elasticsearch Serverless 项目创建推理端点以使用 ELSER 生成嵌入使用语义文本字段进行自动分块并调…

SpringBoot如何进行全局异常处理?

1.为什么需要全局异常处理&#xff1f; 在日常开发中&#xff0c;为了不抛出异常堆栈信息给前端页面&#xff0c;每次编写Controller层代码都要尽可能的catch住所有service层、dao层等异常&#xff0c;代码耦合性较高&#xff0c;且不美观&#xff0c;不利于后期维护。 应用场…

基于java的少儿编程网上报名系统+vue

TOC ssm006基于java的少儿编程网上报名系统vue 研究背景 近年来&#xff0c;随着网络技术的不断发展&#xff0c;越来越多人在网站查询各种信息&#xff0c;少儿编程网上报名系统对用户和管理员都有很大帮助&#xff0c;少儿编程网上报名系统通过和数据库管理系软件协作来实…

基于STM32开发的智能安防报警系统

目录 引言环境准备工作 硬件准备软件安装与配置系统设计 系统架构硬件连接代码实现 系统初始化传感器数据采集与处理报警控制与通知Wi-Fi通信与远程监控应用场景 家庭安防管理商铺和办公室的智能安防常见问题及解决方案 常见问题解决方案结论 1. 引言 随着智能家居和物联网技…

拟合的置信区间

目标图: 图片来源:Fig. 4e from Arwani, Ruth Theresia, et al. "Stretchable ionic–electronic bilayer hydrogel electronics enable in situ detection of solid-state epidermal biomarkers." Nature Materials (2024): 1-8. 1. 数据输入 假设原始数据如下:…

书生大模型实战营第三期基础岛第二课——8G 显存玩转书生大模型 Demo

8G 显存玩转书生大模型 Demo 基础任务进阶作业一&#xff1a;进阶作业二&#xff1a; 基础任务 使用 Cli Demo 完成 InternLM2-Chat-1.8B 模型的部署&#xff0c;并生成 300 字小故事&#xff0c;记录复现过程并截图。 创建conda环境 # 创建环境 conda create -n demo pytho…

协作新选择:即时白板在线白板软件分享

在团队合作中&#xff0c;产品经理扮演着至关重要的角色&#xff0c;他们不仅是产品与用户之间的纽带&#xff0c;更是产品性能和用户需求的桥梁。他们需要深入参与产品的研发过程&#xff0c;并与研发团队保持紧密的沟通。因此&#xff0c;产品经理需要一款高效的协作工具来提…

纯电SUV挑花眼了?看看这两款十多万的家用SUV谁更香

文/王俣祺 导语&#xff1a;随着新能源技术的日益成熟&#xff0c;现如今纯电汽车已经在市场上卖得风生水起。早些时候人们可能还会对纯电汽车抱有“续航焦虑”&#xff0c;但随着各个车型在电池容量以及能耗方面都迎来了进步&#xff0c;充电网络也日渐完善&#xff0c;选择一…

多商户多套部署需修改注意事项

同一台服务器上部署多个多商户项目&#xff0c;需要修改和调整的地方等。 一、修改代码中的端口号&#xff0c;需要两个项目不能使用同一个端口号&#xff0c;例如&#xff1a;A项目用&#xff1a;8324&#xff0c;B项目用&#xff1a;8325&#xff1b; 二、修改反向代理&…

认识泛型VS包装类

1.包装类 在 Java 中&#xff0c;由于基本类型不是继承自 Object &#xff0c;为了在泛型代码中可以支持基本类型&#xff0c; Java 给每个基本类型都对应了 一个包装类型。 ps:为什么需要包装类&#xff1f;说白了java就是面向对象编程的 比如&#xff1a;Java 的集合框架&am…

笔记整理—uboot启动过程(3)栈的二次设置以及常用名词解析,BL1部分完

前文说到了uboot的lowlevel_init都干了些什么&#xff0c;也就是经过了这项初期的低级启动&#xff0c;使得我们能在串口监视器上看见机器打印出的第一句话“OK”。当lowlevel_init结束后&#xff0c;uboot去做了另一件事情&#xff0c;那就是栈的再次设置。 第一次栈设置发生在…

昂科烧录器支持Analogix硅谷数模的USB-C端口控制器ANX7406

芯片烧录行业领导者-昂科技术近日发布最新的烧录软件更新及新增支持的芯片型号列表&#xff0c;其中Analogix硅谷数模的USB-C端口控制器ANX7406已经被昂科的通用烧录平台AP8000所支持。 ANX7406是一款USB Type-C™&#xff08;USB-C&#xff09;端口控制器&#xff0c;符合最新…

网络 通信

一、客户端接收(也可以bind) 1. socket socket 函数 用于创建一个套接字&#xff08;socket&#xff09;&#xff0c;这是网络通信的基础。 它的原型如下&#xff1a;int socket(int domain, int type, int protocol); 参数&#xff1a; domain&#xff1a;指定协议族&…

go-zero接入skywalking链路追踪

文章目录 Skywalking本地测试搭建项目引入dockerfile打包引入最后效果图 Skywalking本地测试搭建 这里用Docker搭建 #数据存储用ES&#xff0c;搭建ES docker run -d -p 9200:9200 -p 9300:9300 --name es -e "discovery.typesingle-node" -e ES_JAVA_OPTS"-X…

【C++】12.智能指针

在上一篇博客【C】11.异常中我们知道有些时候会造成内存空间的未释放从而导致内存泄漏&#xff0c;因此本篇博客的内容就是如何减少内存泄漏——智能指针。 一、RAII RAII&#xff08;Resource Acquisition Is Initialization&#xff09;是一种利用对象生命周期来控制程序资…

基于单片机的指纹识别考勤系统设计

本设计基于STC89C52为主控的指纹考勤系统&#xff0c;主要分为光学AS608指纹识别模块、LCD12864液晶模块、AT24C02存储芯片、DS1302时钟芯片模块、矩阵按键模块。AS608指纹模块进行指纹的采集&#xff1b;矩阵按键能实现对指纹的录入、删除、编号&#xff1b;AT24C02存储模块对…

如何使用ssm实现网上服装销售系统

TOC ssm047网上服装销售系统jsp 第一章 绪 论 1.1背景及意义 系统管理也都将通过计算机进行整体智能化操作&#xff0c;对于网上服装销售系统系统所牵扯的管理及数据保存都是非常多的&#xff0c;例如管理员&#xff1b;主页、个人中心、用户管理、商品分类管理、商品信息管…

[Meachines] [Easy] Optimum HFS文件管理2.3.x-RCE+MS16-032

信息收集 IP AddressOpening Ports10.10.10.8TCP:80 $ nmap -p- 10.10.10.8 --min-rate 1000 -sC -sV -Pn PORT STATE SERVICE VERSION 80/tcp open http HttpFileServer httpd 2.3 |_http-server-header: HFS 2.3 |_http-title: HFS / Service Info: OS: Windows; CP…