从键盘快捷键中获得智慧

2022-02-21 05:50:53

在JavaScript/TypeScript中管理键盘快捷键真是一件糟糕的事情。让';假设你有以下快捷键(假设你在macOS上):

文件addEventListener(";keydown";,(事件:KeyboardEvent)=>;{if(event.metaKey&;!event.altKey&;!event.ctrlKey&;!event.shiftKey&;&;event.code==="KeyL"){doThingA();}if(event.metaKey&;event.ctrlKey&;event.altKey&;event.shiftKey&;event.code===&;34;KeyL&;34;){doThingB();}if(event.metaKey&;event.ctrlKey&;event.shiftKey&;event.altKey&;event.code===&;34;KeyL&;34;){doThingC();}});

文件addEventListener(";keydown";,(事件:KeyboardEvent)=>;{if(event.metaKey&;!event.altKey&;&;&;event.code==="KeyL"){if(event.ctrlKey){if(event.shiftKey){doThingC();}else{doThingB();}else{doThingA();}}});

因此,你的开箱即用的选择是让它变得混乱或冗长。好消息!结果是';第三个选项:位标志。

我需要在一个辅助项目I';我正在工作,所以我用位标志实现了自己的小键盘快捷键库。我尊敬的同事马特·梅希姆(Matt Mayhem,饰演坏阴影,没有明日男孩的名声)正在审查我的拉车请求,想知道所有的&;和|操作员,这就是这个帖子存在的原因。

在深入研究位移位、位标志和位运算符之前,让';Let’我们都跳上小车去二进制交叉点,谈谈基数为2的数字。

我们中的大多数人都在以10为基数(或十进制)的宇宙中工作,我';我懒得解释,所以我';我会让别人做的。二进制数用2进制表示,这只是一种花哨的说法,表示它只使用两个符号:0和1。

我';我将交替使用10进制和十进制,但它们指的是同一件事。

二进制计数有点奇怪,所以让';让我们来谈谈这种奇怪。我们';我们将使用JavaScript';s parseInt()函数以获取二进制数的十进制表示形式。第一个参数是二进制数字符串,第二个参数是基数。要获得十进制值,I';m使用基数为2(对于基数为2的数字系统)。

如果你猜到了5,给自己买杯饮料吧!如果你觉得101=5听起来像胡说八道,请容忍我。从右到左计算二进制数。从右向左移动时,该位的十进制表示为:

找到每一位的十进制值,然后将它们相加。所以这里';这就是把101变成5的方法:

最右边的那一列代表每一位的十进制值。因为第0位和第2位是1,所以将1乘以2⁰ (1)和2²(4),将它们相加得到5。

JavaScript使用32位有符号整数进行逐位操作。这意味着任何在引擎盖下旋转的位都会对二进制数进行操作,如下所示:

我可以为你节省一些计算时间,并向你保证这个数字是32x1。每个1代表一个位,即";32位";来自。32位有符号整数的最小值为-2147483648,最大值为2147483647。但如果你要运行这个:

它注销了4294967295。你怎么能一直到4294967295?那';这很简单,第一位是";符号位";(以及最高有效位),因此将其翻转为1会使数字无符号。自从我们';我们不再处理负数了,我们只是把所有的数据都加上2147483648。最小值-2147483648变为0,最大值2147483647变为4294967295。

既然你对二进制数有了更好的理解(希望如此),那就让';s关于位移位和位标志的说唱。

我们需要将四个修改键表示为位标志。我';我在这里使用打字脚本,所以我';我将使用枚举。我';我也使用Electron,这个应用程序只在macOS上运行,所以我';我将用macOS术语来表示每个修饰语。

枚举Mod{Command=1<;<;1,//2在base-10控件中=1<;<;2,//4在base-10选项中=1<;<;3,//8在base-10中Shift=1<;<;4,//16在base-10}

左移位运算符(<;<;)将第一个操作数向左移位指定位数。向左移位的多余位将被丢弃。零位从右边移入。

因此,如果我们要注销它们的base-2表示中的每一个修饰符,这里';这就是我们';d得到:

比如parseInt()、JavaScript';函数可以使用一个可选的基数参数,将数字转换为指定的基数。

您可以看到0的数量直接对应于<&书信电报;Mod enum中的运算符。

那又怎样';这一切的意义是什么?好吧,让';让我们切换到10垒一分钟。你需要检查多个正确或错误的条件。使用位标志的好处是,这些Mod值的任何组合都不会等于同一个数字:

清单还在继续(相信我)。这些标志的每个可能组合都不会与另一个组合重叠。例如,你';我永远不会遇到值可能是(Mod.Command+Mod.Option)或(Mod.Option+Mod.Shift)的问题。

那里';这实际上是我们的比特数问题';重新转换(即1、2、3和4),但我们';我稍后再谈。

现在我们';我们已经设置好了修改器,让';下面介绍如何将它们与位运算符一起使用。

我';我只关心AND,OR,AND运算符,而不是我的目的,但如果您需要的话,XOR是存在的。位运算符可以用真值表来描述。如果你没有';我不想点击这里的链接';下面是一个简短的概述:

真值表是数学中用来进行逻辑运算的数学表。它包括布尔代数或布尔函数。它主要用于根据输入值确定复合语句是真是假。

链接站点上的真相表有点难以掌握,所以让';让我们为每个运算符检查真值表';我们关心的是。

假设有两位a和b。and位运算符的真值表如下所示:

如果将0替换为false,将1替换为true,并将&;为&&;,它将映射到:

安慰日志(假和假);//假控制台。日志(假与真);//假控制台。日志(真与假);//假控制台。日志(真与真);//符合事实的

如果将0替换为false,将1替换为true,将|替换为| |,它将映射到:

安慰日志(假| |假);//假控制台。日志(假| |真);//trueconsole。日志(真| |假);//trueconsole。日志(真| |真);//符合事实的

所以我们有一个Mod bit flag enum和一些操作符,可以用来调整位。让';让我们将其应用于键盘快捷键。

目前,我们';我们以后将只关注修饰符,并担心非修饰符键(例如字母、数字、符号)。让';s创建一个名为areKeysDown的函数,其中包含一个事件和一个组合参数。事件是一个键盘事件,组合是一个与我们的位标志相对应的数字。

就所需的功能而言,我们希望检查特定的按键组合是否已关闭。它';It’重要的是要注意,当且仅当那些精确的键被按下时,函数才会返回true。因此,如果命令键关闭,areKeysDown(事件,Mod.Command)将返回true,如果Command+Control关闭,则返回false。

函数areKeysDown(事件:KeyboardEvent,combo:number):布尔值{let keyCode=combo;if((combo&;Mod.Command)==Mod.Command){if(!event.metaKey){return false;}else{keyCode=keyCode&;~Mod.Command;}else{if(event.metaKey){return false;}}/。。。其余的Mod处理程序…//当我们添加非修改键时,这将发生变化:return keyCode===0;}

在macOS上,事件。metaKey表示命令键(⌘) 事件已经结束。altKey表示选项键(⌥) 事件已经结束。ctrlKey表示控制键(⌃) 这件事已经结束了。shiftKey表示Shift键已按下。

文件addEventListener(";keydown";,(事件:KeyboardEvent)=>;{//如果event.metaKey=true,则注销true,其他修饰符为false,//并且没有按下其他键:console.log(areKeysDown(event,Mod.Command));};

让';让我们谈谈。在第一行中,我创建了一个局部变量keyCode,它被赋予combo的值。这个想法是,我们一步一步地通过每个修饰符,清除键码中的位标志,直到我们到达函数的底部。去掉所有Mod标志后,您';re left键代码代表按下的字母或数字。

好极了我们';我们遇到了第一个位运算符AND。那么这条线在做什么?好吧,根据MDN文档中关于和操作员的描述:

按位AND运算符(&;)在两个操作数的对应位均为1的每个位位置返回1。

常数a=5;//000000000000000000101常数b=3;//00000000000000000000000011//;在两个值中都是1。/a&b0000000000000000000000001//这就是它只注销1的原因^

const combo=Mod。命令;//00000000000000000000000010 const compare=combo&;摩登派青年命令;//00000000000000000000000000000010//这转换为://if 00000000000000000000000010/==00000000000000000000000000000010//这是真的!所以我们知道命令修改器被按下了。如果(compare==Mod.Command){/…}

如果((组合和修改命令)==Mod。命令){if(!event.metaKey){return false;}else{keyCode=keyCode&;~Mod.Command;}

if(!event.metaKey)语句非常明显。如果我们';重新检查命令修饰符,它不是';t按下,函数返回false。另一个是事情变得辛辣的地方。我';m使用AND和NOT运算符清除Mod。来自keyCode的命令。

达夫那是什么意思?对于32位整数,它将所有的1变成0,0变成1。

但是等一下,不是吗';这难道不意味着在我们不这么做之后,它的价值将是巨大的吗?你是对的,但是不要';不要忘记这是一个两步操作:

const combo=Mod。命令;//00000000000000000000000010 const not=~Mod。命令;//11111111111111111111101控制台。日志((组合和非组合)。toString(2));//00000000000000000000000000/^Don';别忘了还有!

按位AND运算符(&;)在两个操作数的对应位均为1的每个位位置返回1。

组合中没有位置,也没有两个位都为1的变量,因此其输出为0。最后一条else语句强制要求当且仅当按下指定的确切组合时函数返回true。如果你';重新检查国防部。控件关闭且用户按下Control+Command时,函数返回false。

函数areKeyDown(事件:KeyboardEvent,combo:number):布尔值{let keyCode=combo;if((combo&;Mod.Command)==Mod.Command){/…}否则//Command键被按下,但我们';重新显式检查//if _not u down,因此我们返回false:if(event.metaKey){return false;}}/。。。其余的Mod处理程序…//当我们添加非修改键时,这将发生变化:return keyCode===0;}

这一切都很好,但不是很有用。如何检查多个修改器?我';我很高兴你这么问!

让';s在areKeysDown函数中添加第二个修饰符if语句,用于检查控制:

函数areKeysDown(事件:KeyboardEvent,combo:number):布尔值{let keyCode=combo;if((combo&;Mod.Command)==Mod.Command){if(!event.metaKey){return false;}else{keyCode=keyCode&;~Mod.Command;}else{if(event.metaKey){return false;}如果((组合和模块控制)==Mod。控件){if(!event.ctrlKey){return false;}else{keyCode=keyCode&;~Mod.Control;}else{if(event.ctrlKey){return false;}}/。。。其余的Mod处理程序…//当我们添加非修改键时,这将发生变化:return keyCode===0;}

为了检查多个修饰符,我们';我们将给出最后一个按位运算符:OR。我们的函数调用如下所示:

文件addEventListener(";keydown";,(事件:KeyboardEvent)=>;{//如果event.metaKey=true,event.ctrlKey=true,则注销true,//其他修饰符为false,并且没有按下其他键:console.log(areKeysDown(event,Mod.Command | Mod.Control));};

按位OR运算符(|)在其中一个或两个操作数的对应位为1的每个位位置返回1。

常数a=5;//000000000000000000101常数b=3;//00000000000000000000000011//这些是1或0^^^控制台。日志(a | b);//000000000000000000000000111//所以它把它们都变成了1^^^

文件addEventListener(";keydown";,(事件:KeyboardEvent)=>;{//如果event.metaKey=true,event.ctrlKey=true,则注销true,//其他修饰符为false,并且没有按下其他键:console.log(areKeysDown(event,Mod.Command | Mod.Control));};函数areKeysDown(事件:KeyboardEvent,combo:number):布尔{//Mod.Command=00000000000000000000000000000010//Mod.Control=000000000000000000000000000000100//combo=000000000000000000000000000000110//如果((combo&;Mod.Command)==Mod Command),则将任意数字中的位设置为1^^^{//Mod.Command=00000000000000000000000010//combo=000000000000000000000000110//combo&;Mod.Command=00000000000000000000000000000010//Mod.Command_u和u00000000000000000010//==00000000000000000000000010?//是的!所以我们知道Mod.Command在com中bo参数}

文件addEventListener(";keydown";,(事件:KeyboardEvent)=>;{console.log(areKeysDown(事件,Mod.Command | Mod.Control));};函数areKeysDown(事件:KeyboardEvent,combo:number):布尔值{//Mod.Command=00000000000000000000000000000010//Mod.Control=000000000000000000000000000000100//combo=000000000000000000000000110//将任意数字中的1位设置为1^^^让keyCode=combo;如果((combo&;Mod.Command)==Mod。Command){//参见前面的示例(我们知道它是真的)…如果(!event.metaKey){return false;}else{/~Mod.Command=11111111111111111111111101//keyCode仍然是combo=000000000000000000000000110 keyCode=keyCode&;~Mod.Command;//000000000000000000000000100//两个数字中的唯一位位置,值为1^}else{/…}如果((组合和模块控制)==Mod。(控制){//Mod.Control=000000000000000000000000100//combo=000000000000000000000000110//combo&;Mod.Control=000000000000000000000000000000100//只有命令_和uCombo为1的位置^//如果(!event.ctrlKey){return false;}否则{//~Mod.Control=111111111111111111111111011//keyCode=combo w/o Mod.Command=000000000000000000000000000000100 keyCode=keyCode&;~Mod.Control;//00000000000000000000000000000000000000}//keyCode现在是0,所以我们返回true!return keyCode==0;}

因此';这就是我们处理多个修饰语的方式。但这仍然没有';我们无法获得所需的所有功能。字母、数字、箭头键等呢。?让';接下来我们来报道。

这就是事情开始变得有点棘手的地方。KeyboardEvent的keyCode属性已被弃用一段时间,不再推荐使用。

不推荐使用的键盘事件。keyCode只读属性表示与系统和实现相关的数字代码,用于标识按键的未修改值。

它';因为它是';s是一个数字,所以字母a的键码是65。通过执行以下操作,可以检查是否按下了Command+A:

文件addEventListener(";keydown";,(事件:KeyboardEvent)=>;{//如果按了";A";则注销65次:console.log(event.keyCode);//如果按了event.metaKey=true和";A";则注销true(仅当按下这些键时):console.log(areKeysDown(event,Mod.Command | 65));

我想不赞成的主要原因是国际键盘布局的问题。MDN建议您使用KeyboardEvent。代码,这很好,但它只需要一点额外的工作。

我用每个非修饰键创建了一个枚举,以使areKeysDown函数更具可读性。如果keyCode属性不是';t不推荐,看起来是这样的:

枚举键{LetterA=65,LetterB=66,LetterC=67,LetterD=68,//……等等……}

文件addEventListener(";keydown";,(事件:KeyboardEvent)=>;{console.log(areKeysDown(事件,Mod.Command | Key.LetterA));};函数areKeyDown(事件:KeyboardEvent,combo:number):布尔值{let keyCode=combo;if((combo&;Mod.Command)==Mod.Command){/..keyCode=keyCode&;~Mod.Command;//keyCode=65=Key.letta/../事件keyCode=65=Key。LetterA=keyCode,所以我们返回true:return事件。keyCode===keyCode;}

枚举键{LetterA=1,LetterB,LetterC,LetterD,//……等等……}

我从1开始,而不是0,因为0将是清除所有修饰符的结果。没有办法区分";只按下修改器";和";一些修饰语加上字母A被按下";。

现在我们只需要一个表来将密钥枚举映射到相应的KeyboardEvent。代码值:

const codeByKeyTable:记录<;键,字符串>;={[Key.LetterA]:";Key.LetterB]:";Key";[Key.LetterC]:";Key";[Key.LetterD]:";KeyD";/…等等。}

文件addEventListener(";keydown";,(事件:KeyboardEvent)=>;{//Logs out";KeyA";当您按下字母";A";:console.log(event.code);};

如果你';你想知道我为什么用KeyboardEvent。代码而不是键盘事件。钥匙,它';这是因为键盘事件。Code返回的值不是';t根据键盘布局或修改键的状态进行更改。键盘事件。键根据修改键的状态返回不同的值,这将大大破坏我们所有的代码。

文件addEventListener(";keydown";,(事件:KeyboardEvent)=>;{console.log(areKeysDown(事件,Mod.Command | Key.LetterA));};函数areKeyDown(事件:KeyboardEvent,combo:number):布尔值{let keyCode=combo;if((combo&;Mod.Command)==Mod.Command){/…keyCode=keyCode&;Mod.Comma

......