Capture and react to user-typed words. Make your website keyboard-friendly with minimal effort.
Brotkeys builds upon jaywcjaylove's hotkeys and depends on their release to offer the additional functionality of reacting not only to a given key press, but also to words, by specifying a javascript function that shall be executed.
- listen for specific words
- set a character that has to preceed any input
- be informed on start and end of user input, or at every character
- automatically generate an "open link" action for every anchor tag or a specific css class
Executes the given javascript function when the word is typed. There is no way to start a new word while typing the old word. So hello
will show a popup saying "general kenobi!", but if you start typing about
and then decide to switch to hello
, there won't be any reaction to abhello
.
<script src="./libs/jaywcjlove_hotkeys/hotkeys.min.js"></script>
<script src="./libs/lucidbrot_brotkeys/brotkeys.js"></script>
<script type="text/javascript">
// The HotkeyManager class is provided by Brotkeys.js
var manager;
var wordMap = new Map([
["hello", function(){alert("general kenobi!");}],
["about", function(){window.open("https://www.example.com/about", "_self");}],
["disable", function(){manager.disable();
console.log("No longer listening for words");}]
]);
manager = new HotkeyManager(wordMap, new Map([]));
manager.enable_f_mode(false); // explanation in next subsection
</script>
Brotkeys.js was developed with the idea of Vimium in mind. In short: press F to see what to type in order to open a link without using the mouse. See below for an example of how vimium works (the yellow things) and how a simple website is able to offer the same functionality with Brotkeys.js.
In order for this to work, there's a special option that is set to enabled by default: manager.enable_f_mode(true)
causes Brotkeys.js to only listen to words that are entered after they press F
. You can change that to some other character using manager.set_f_mode_character('g')
for example.
Here's what F_Mode is good for: Your user can abort typing one word and start typing another. For example by pressing F
instantly again after having pressed F
for the first time. Or by pressing Esc
anytime during F_Mode.
<script src="./libs/jaywcjlove_hotkeys/hotkeys.min.js"></script>
<script src="./libs/lucidbrot_brotkeys/brotkeys.js"></script>
<script type="text/javascript">
var manager;
// words of the form [f]abcdefg unless enable_f_mode is set to false
var wordMap = new Map([
// default settings
["f", function(){manager.abort_f_mode();}],
["d", function(){console.log("user disabled shortcuts"); manager.disable();}],
// my defined words
["asd", function(){alert("you typed asd");}],
["asdf", function(){alert("you typed asdf");}],
["qwer", function(){alert("you typed qwer");}],
]);
// single characters that can interrupt at any time during the word-typing mode
var interruptMap = new Map([
["escape", function(){manager.abort_f_mode();}],
]);
manager = new HotkeyManager(wordMap, interruptMap);
</script>
(Of course, Esc
is not the smartest choice if your users are already using Vimium, because it leaves the insert mode of Vimium.)
Register a listener function to be called whenever F_Mode is entered or left.
// please notify me on entering and leaving fmode
var notifyFModeFunc = function(entering){
if(entering){
StyleSwapper.showKeys(true, "LB-SS-swap1"); //important: this class must be defined in an _external_ css file.
} else {
StyleSwapper.showKeys(false, "LB-SS-swap1");
}
};
manager.setNotifyFModeFunction(notifyFModeFunc);
// To disable the notifyMe function:
// manager.setNotifyMeFunction(undefined);
Similarly, you can set a function to be called whenever the user is typing anything while in F_Mode. If you have turned F_Mode off, replace this sentence with "... whenever the user is typing anything. period.".
// please notify me on every keystroke instead of only at the end
var notifyFunc = function(current_word, remaining_words_possible){
var index_in_word = current_word.length;
console.log("---notify---\n["+index_in_word+"]Remaining words: "+remaining_words_possible+"\nCurrent word: "+current_word);
};
manager.setNotifyMeFunction(notifyFunc);
/*
Example result:
keys.html:331 ---notify---
[1]Remaining words: asd,asdf
Current word: a
keys.html:331 ---notify---
[2]Remaining words: asd,asdf
Current word: as
keys.html:331 ---notify---
[3]Remaining words:
Current word: asf
f not found in available word options. Leaving f_mode.
keys.html:331 ---notify---
[1]Remaining words: qwer
Current word: q
keys.html:331 ---notify---
[2]Remaining words: qwer
Current word: qw
keys.html:331 ---notify---
[3]Remaining words: qwer
Current word: qwe
keys.html:331 ---notify---
[4]Remaining words: qwer
Current word: qwer
*/
// To disable the notifyMe function:
// manager.setNotifyMeFunction(undefined);
// To disable f_mode functionality (fmode means that you have to first press f - or whatever character you designed - before I start accepting words) :
// manager.enable_f_mode(false);
You already have a webpage, filled with hundreds of links. You want for every link some link hint that displays a word, and when the user types that word it shall open the link.
If you want much control, read the rest of this readme, set up your manager, and then call manager.autogenerate(manager.GenerationEnum.tag_anchor, undefined, swap_class);
. If you want a quick start, do this:
<script src="./libs/jaywcjlove_hotkeys/hotkeys.min.js"></script>
<script src="./libs/lucidbrot_brotkeys/brotkeys.js"></script>
<script type="text/javascript">
brotkeys_autogenerate_manager_for_anchors();
</script>
Now you have exactly what I described above.
Of course, that does not sate your thirst for power, you want exactly that, but for other things. Here you go: simply give each of your HTML elements the same CSS class - I'll use class="some other classes BHK"
and then use
<script src="./libs/jaywcjlove_hotkeys/hotkeys.min.js"></script>
<script src="./libs/lucidbrot_brotkeys/brotkeys.js"></script>
<script type="text/javascript">
brotkeys_autogenerate_manager_for_class_tag("BHK")
</script>
And if you want even more control, here's what's lacking. In the way I just showed you, you do not have access to the HotkeyManager variable where you can change settings. If you want that, you could either use the implementation of the above-called functions as inspiration, or do it like I did in my sample page:
var manager;
// words of the form [f]abcdefg unless enable_f_mode is set to false
// DONT INCLUDE AN UPPERCASE X, because that is used to immediately abort f mode here.
var wordMap = new Map([
["secret", function(){window.open("https://eric.mink.li/src/php/ccount/click.php?id=sneric","_self");}],
]);
// single characters that can interrupt at any time during the word-typing mode
var interruptMap = new Map([
["X", function(){manager.abort_f_mode();}],
["D", function(){console.log("user disabled shortcuts"); manager.disable();}],
]);
manager = new HotkeyManager(wordMap, interruptMap);
manager.interrupt_caseInsensitivity = false;
manager.loadNeededJSCSSForStyleSwapping();
// please notify me on entering and leaving fmode simply by showing the link hints
// this is the simplest way to do this. for other options, see the examples in brotkeys.js#brotkeys_autogenerate_manager_for_anchors and brotkeys.js#brotkeys_autogenerate_manager_for_class_tag
var notifyFModeFunc = manager.genToggleKeysOnNotify();
manager.setNotifyFModeFunction(notifyFModeFunc);
manager.log_prefix = "[M] ";
manager.autogenerate(manager.GenerationEnum.tag_anchor);
In this sample, I first create wordMap
, a mapping from words to actions. "secret"
is something I added manually, and whenever somebody types that (while in fMode, since I did not deactivate fMode) it calls the javascript function in that map, which opens some other url.
The interruptMap
contains things that can always happen, while in fMode, and interrupt the other actions.
I create the manager with those two arrays as start setup, tell it that I want case sensitive interrupts. The call to loadNeededJSCSSForStyleSwapping();
is only neccessary when not using manager.genToggleKeysOnNotify()
but still using my css class - so it is not needed here. But it will usually not hurt to call it anyways, since it instantly returns if it has been called before on the same manager. Generally speaking, you probably never need this, but if something doesn't look correctly (or at all), try calling this.
genToggleKeysOnNotify
is a function that is tricky to explain, but in fact quite simple. setNotifyFModeFunction
takes as argument any function that reacts to toggling of the fMode. genToggleKeysOnNotify
generates such a function for you so you don't have to worry about this unless you want to use your own function instead. In that case, read the start of this readme instead, I think I have another example there.
The log prefix is just that, a prefix before any log messages.
And finally, the call to autogenerate
.
autogenerate
takes up to three arguments. The first is mandatory: Either manager.GenerationEnum.tag_anchor
or manager.GenerationEnum.class_tag
. This states what you want to autogenerate.
If you want to generate for anchors, you can pass undefined
as the second argument. Otherwise the second argument is used as your class name that will be operated on.
The third argument is replaced with manager.SWAP_CLASS_NAME_DEFAULT
if you're passing undefined
. It serves as the class name used for the link hint buttons that appear and disappear. Make sure it's the same as in genToggleKeysOnNotify
if you have passed something there - and undefined if not.
The same thing as autogenerate
, but only for all elements within a given container instead of for all elements within the document.
autogenerateWithinId(containerId, generationTarget, /*optional*/ css_class_name, /*optional*/ arbitraty_swap_class_name)
containerId
- String, ID of the container element
For the other arguments, see in the section on autogenerate, which is right above this one.
Sometimes, autogeneration will not traverse certain boundaries. For example, container.getElementById()
will never find any elements that are within an iframe that is somewhere in the container. If you want to support your iframe (let's say it has id ifrem
), you can do so by additionally running this after your other generations:
autogenerateWithinId("ifrem", manager.GenerationEnum.tag_anchor)
This example will add link hints to all <a/>
tags within that iframe. Make sure that it is run only once the iframe content has been fully loaded. It seems a defer
is not always enough, so use the iFrame's onload
attribute.
Currently, iFrame support is limited to iframes that can be found with document.getElementById()
- so no nested iFrames.
I don't know why you would do this, but that works as well. Just be aware that using hotkeys.unbind('*')
would break the behaviour of the Managers (but that's always the case, not just when using multiple managers).
// a manager
var man1 = new HotkeyManager(new Map([["free", function(){console.log("man1: you typed free");}]]), new Map([]));
// another manager
var man2 = new HotkeyManager(new Map([["tree", function(){console.log("man2: you typed tree");}]]), new Map([]));
To distinguish between them for debugging, it can help to set a prefix the internal log messages:
man2.log_prefix = "[M2]";
man1.log_prefix = "[M1]";
The HotkeyManager
has three settings regarding case sensitivity, which are all by default set to true (insensitive):
// config for key case insensitivity
this.fmode_caseInsensitivity = true;
this.interrupt_caseInsensitivity = true;
this.word_caseInsensitivity = true;
You should be able to set these values using direct qualified access.
The first line is whether F
and f
both trigger F_Mode.
The second line is whether characters in interruptMap are case insensitive.
The third line is whether capitalization in words matters. True if it does not matter.
See also further below about an obscure setting called ignore_ShiftAndCapslock_inWordMode
.
Simply provide your own css class and set manager.LINKHINT_STYLE_CLASS
to it.
The currently used one:
kbd.eric-reverse {
background-color: #fff;
color: #333;
border-style: dashed;
}
The Link Hints can either show up within the page content (text), or overlayed. By default, brotkeys.js uses overlayMode because that way, the content is not moved when the link hints appear. If you prefer the Link Hints to be directly embedded instead of using absolute positioning and a container element, you can do manager.setOverlayMode(false)
before generating the link hints.
It seems like a cool feature, defer
. But it makes it impossible for Brotkeys to figure out where it is stored. There are several ways to solve this problem, but the simplest way is not to use defer
.
The second simplest way is to tell Brotkey where it is stored:
<!-- main.html -->
<script src="./libs/jaywcjlove_hotkeys/hotkeys.min.js" defer></script>
<script src="./libs/lucidbrot_brotkeys/brotkeys.js" defer></script>
<script src="./keyjs/main.js" defer></script>
// main.js
// since we used defer in main.html for brotkeys, we have to tell brotkeys where it lives. Absolute, or relative to the HTML file where this js file is included in.
_brotkeysjs__src__path = './libs/lucidbrot_brotkeys/brotkeys.js';
This is only relevant if you are using autogenerate or anything else that uses loadNeededJSCSSForStyleSwapping.
If you want to know what's going on, without reading the actual code.
There is also the option this.ignore_ShiftAndCapslock_inWordMode = true
. If it is enabled, Brotkeys.js ignores entered shift and capslock keys themselves while within a word. Otherwise, eXample
and example
would not be the same word, even with case insensitivity on - because it would be eshiftXample
internally. And I don't think you could capture that without writing your own code instead of using Brotkeys.js. But you're welcome to try what happens if you set this to false.
This is where that setting is used internally: when we know that there was no word matching the latest key
:
if(counter == 0){
if(this.ignore_ShiftAndCapslock_inWordMode && (key=="shift" || key=="capslock")){
// ignore shift or capslock key if we're in word mode and it was not specified in the remaining possible words
this.log_verbose("ignoring "+key+" because there are no possible matches containing it and this.ignore_ShiftAndCapslock_inWordMode equals true");
this.current_link_word = this.current_link_word.slice(0, -(key.length)); // remove last character again
return;
}
this.log_verbose(key+" not found in available word options. Leaving f_mode.");
this.leave_f_mode();
return;
}
constructor(wordMap, interruptMap)
wordMap maps user-typed words to javascript functions. InterruptMap maps single characters that are always interrupting word entering to execute their own javascript function to those functions.
Do not modify this, it's used internally. However, reading is fine. It tells you what characters from the current word the user has already entered.
Again, if you overwrite this, maybe you break something. But you can read this and get one of the values as specified here:
this.ModeEnum = Object.freeze({"f_mode":1, "pre_f_mode":2, "all_disabled":3});
Use log_verbose
, log_error
, log_happy
as you please. No side effects apart from formatted writing to console.log()
.
Do what they say. Abort simply logs and then calls leave. These are used internally but should be ok to use from outside.
Overwrites leave_f_mode
, so if for some reason, you're overwriting that as well, consider this.
Does not unregister the listener to hotkeys.js - it simply does not act on the events until activated again using enable_f_mode
with either true or false as parameter. (True if you want F_Mode).
If you're disabling the manager with a user-typed-word-reaction, you want to also manager.leave_f_mode()
, else the link hints remain visible.
There's no harm in calling this, I think. But there's also no reason to. It's called from the constructor.
This loads the css needed for toggling the style of the link hints when hiding them.
Don't touch unless you've read the code.
log_happy(text){
/*
https://stackoverflow.com/a/21457293/2550406
(c) bartburkhardt cc-by-sa 3.0
*/
[code skipped]
}
log_happy
was originally provided under CC-BY-SA. However, if this is of concern to you, just remove the function and replace it with a simple console.log()
. Also, I don't think they care - after all, everybody steals from stackoverflow.
My own code is provided as-is without guarantees, under MIT Licence as provided in the LICENCE file.
I'd be happy to hear about it when you use my code though :) eric@mink.li
It's probably ugly. I've tried a little, but not too hard, and javascript does not offer the options I'm used to. This is not a "sorry", it's just a "I knew that it was ugly when I wrote it, I'm not that incompetent".