2 |
3 | # whenipress
4 | A gorgeous, simple, tiny JavaScript package to add keyboard bindings into your application.
5 |
6 | ## Table of Contents
7 | * [Features](#features)
8 | * [Installation](#installation)
9 | * [Why use?](#why-use-whenipress)
10 | * [Usage](#using-whenipress)
11 | - [Simple key presses](#listening-for-key-presses)
12 | - [Key combos](#listening-for-key-combinations)
13 | - [Alternatives to `then`](#alternatives-to-then)
14 | - [Stop listening for a single binding](#stop-listening-for-a-single-key-binding)
15 | - [Stop listening for all bindings](#stop-listening-for-all-key-bindings)
16 | - [Retrieve registered bindings](#retrieve-a-list-of-every-registered-key-binding)
17 | - [Listen for an event just once](#listening-for-an-event-just-once)
18 | - [Create keybinding groups](#creating-keybinding-groups)
19 | - [Listen for double taps](#listening-for-double-taps)
20 | - [Listen for keys being released](#listening-for-when-keys-are-released)
21 | - [Keybindings and form elements](#keybindings-and-form-elements)
22 | - [Scoping your bindings to elements](#scoping-your-bindings-to-elements)
23 | * [Extending whenipress](#extending-whenipress)
24 | - [Registering plugins](#registering-plugins)
25 | - [Plugin syntax](#plugin-syntax)
26 | * [Initialising your plugin](#initialising-your-plugin)
27 | * [Listen for new bindings](#listen-for-when-a-new-binding-is-registered)
28 | * [Listen for a stopped binding](#listen-for-when-a-binding-is-stopped)
29 | * [Listen for when all bindings are stopped](#listen-for-when-all-bindings-are-stopped)
30 | * [Hook in before a handler is fired](#hook-in-before-an-event-is-handler-is-fired)
31 | * [Hook in after a handler is fired](#hook-in-after-an-event-has-been-handled)
32 | * [Plugin options](#plugin-options)
33 | * [Stopping plugins](#stopping-plugins)
34 |
35 |
36 | ## Features
37 | - A simple, intuitive syntax for adding keyboard shortcuts for key presses and key combinations.
38 | - Takes the complexity out of key codes and values, allowing you to mix and match to your heart's content.
39 | - Teeny and tiny and dependency free - just 1.4kB minified & gzipped.
40 | - Stores all of your keyboard combinations under a single keydown and keyup listener, improving your app's performance.
41 | - Provides advanced functionality, such as listening for double tapping keys and only listening for a keyboard event once.
42 | - Stores all your key bindings in one place, allowing you to have access to every binding in your application.
43 | - Allows for key groups using the `group` function, making your code more readable and powerful.
44 | - Provides a hook to be notified when a keyboard shortcut has been released.
45 | - Includes a powerful plugin syntax for extending the base functionality of whenipress.
46 |
47 | ## Installation
48 | Whenipress is available via npm: `npm i whenipress`.
49 | You should then require it as a module in your main JavaScript file.
50 |
51 | ```javascript
52 | import whenipress from 'whenipress/whenipress';
53 |
54 | whenipress('a', 'b', 'c').then(e => console.log('Nice key combo!'));
55 | ```
56 |
57 | But you can equally use it via a CDN:
58 |
59 | ```html
60 |
61 |
64 | ```
65 |
66 | ## Why use whenipress?
67 | Keyboard shortcuts are often an add-on in most web applications. Why? Usually, because it can be pretty complicated to
68 | add them in JavaScript. The `keydown` and `keyup` events are pretty low level stuff, and require a fair bit of abstraction
69 | to make adding shortcuts a simple task.
70 |
71 | Say hello to whenipress. We've done all the abstraction for you, and provided the functionality you'll need in a
72 | much simpler, easier to manage way. Getting started is as simple as calling the global `whenipress` method, passing
73 | in the key-combo you want to listen for. Check out our guide below...
74 |
75 | ## Using whenipress
76 | What follows is an in depth look at all the juicy functionality offered to you out of the box by whenipress. Enjoy!
77 |
78 | ### Listening for key presses
79 | So, how do you get started? After you've installed the package using one of the methods described in the 'getting started'
80 | section, you can get to registering your first keyboard shortcut. Let's imagine we want to register a shortcut on the '/'
81 | key that will focus the global search bar in our web application. We've already set up a method, `focusGlobalSearchBar()`,
82 | that will actually focus the input for us. We just need to wire it up to our shortcut. Check it out:
83 |
84 | ```javascript
85 | whenipress('/').then(event => focusGlobalSearchBar())
86 | ```
87 |
88 | ### Listening for key combinations
89 | And we're done. Yeah, it's that easy. However, that's also pretty easy to set up in vanilla JavaScript, right? What isn't
90 | so easy to wire up are key combinations. There is no way in native JavaScript to listen for multiple keys at the same time.
91 | Fret not, we have you covered here too. Let's imagine that, when the 'left control' key is pressed in combination with
92 | the 'n' key, we want to redirect to a page where a new CRUD entry can be added. Once again, we've already set up a method,
93 | `redirectToCreateForm()`, that will do the redirecting. Here's how we wire it up:
94 |
95 | ```javascript
96 | whenipress('ControlLeft', 'n').then(event => redirectToCreateForm())
97 | ```
98 |
99 | Pretty nice, right? We can pass any number of keys or key codes into the `whenipress` method to set up complex and
100 | powerful shortcuts.
101 |
102 | ### Alternatives to `then`
103 | Because `then` is used in JavaScript promises, some of you may wish to use a different syntax to avoid any confusion.
104 | Whenipress aliases `then` to `do` and `run`, so you can use those instead if you prefer.
105 |
106 | ```javascript
107 | // This...
108 | whenipress('a').then(e => alert('e pressed!'))
109 |
110 | // Is the same as this...
111 | whenipress('a').do(e => alert('e pressed!'))
112 |
113 | // And this...
114 | whenipress('a').run(e => alert('e pressed!'))
115 | ```
116 |
117 | ### Stop listening for a single key binding
118 | Sometimes, you'll want to disable a key binding. No problem! When you create the key binding, you'll be returned a
119 | reference to it. You can call the `stop` method on that reference at any time to stop listening.
120 |
121 | ```javascript
122 | var nKeyPress = whenipress('n').then(e => console.log('You pressed n'));
123 |
124 | nKeyPress.stop();
125 | ```
126 |
127 | Even better, the related event listener will be completely removed from the DOM, keeping performance snappy.
128 |
129 | ### Stop listening for all key bindings
130 | If you wish to stop listening for all registered key bindings, you can call the `stopAll` method on the global
131 | `whenipress` instance.
132 |
133 | ```javascript
134 | whenipress('A', 'B', 'C').then(e => console.log('Foo'));
135 | whenipress('T', 'A', 'N').then(e => console.log('Bar'));
136 |
137 | whenipress().stopAll();
138 | ```
139 |
140 | ### Retrieve a list of every registered key binding
141 | Because all key bindings are stored in a single location, it is possible to retrieve them programmatically at any time.
142 | This is super useful in whenipress plugins, where you can't be sure which key bindings have been registered.
143 |
144 | ```javascript
145 | whenipress('n', 'e', 's').then(e => console.log('Foo'));
146 | whenipress('l', 'i', 'h').then(e => console.log('Bar'));
147 |
148 | whenipress().bindings() // Will return [['n', 'e', 's'], ['l', 'i', 'h']]
149 | ```
150 |
151 | ### Listening for an event just once
152 | Only want to register a key binding for a single press? Just add the `once` modifier!
153 |
154 | ```javascript
155 | whenipress('z').then(e => console.log("z was pressed")).once();
156 | ```
157 |
158 | The event listener will be removed the first time it is fired. You can place the `once` modifier before the `then`
159 | call if you wish.
160 |
161 | ### Creating keybinding groups
162 | Whenipress supports key groups for easily adding modifiers without having to repeat yourself over and over.
163 |
164 | ```javascript
165 | whenipress().group('Shift', () => {
166 | whenipress('b').then(e => console.log('Shift + b pressed'));
167 | whenipress('c').then(e => console.log('Shift + c pressed'));
168 | });
169 | ```
170 |
171 | ### Listening for double taps
172 | Want to listen for keys pressed twice in quick succession? We have you covered. You can even alter the timeout between
173 | key presses.
174 |
175 | ```javascript
176 | whenipress('a').twiceRapidly().then(e => console.log('You just double pressed the a key'));
177 |
178 | // Use a 300ms timeout
179 | whenipress('a').twiceRapidly(300).then(e => console.log('You just double pressed the a key'));
180 | ```
181 |
182 | ### Listening for when keys are released
183 | The `then` callback you provide whenipress will be fired as soon as all keys in the binding are pressed down at the same
184 | time. Sometimes, however, you'll want to listen for when the keys are released too. No sweat here!
185 |
186 | ```javascript
187 | whenipress('a', 'b', 'c')
188 | .then(e => {
189 | console.log('Keys are pressed!');
190 | })
191 | .whenReleased(e => {
192 | console.log('Keys are released!');
193 | });
194 | ```
195 |
196 | ### Keybindings and form elements
197 | By default, whenipress will ignore keybindings on form elements like inputs, textareas, and select boxes so that you
198 | don't have unexpected side effects in your application. To override this functionality and cause a keybinding to
199 | fire even on these form elements, you may tag `evenOnForms` on to the end of your binding registration.
200 |
201 | ```javascript
202 | whenipress('LeftShift', 'KeyA').then(e => alert("I work, even in inputs, textareas and selects!")).evenOnForms()
203 | ```
204 |
205 | ### Scoping your bindings to elements
206 | Sometimes, you may only want a keyboard event to fire if a node or children within that node are currently in focus.
207 | For example, you may have a sidebar menu where, only when opened, you would like the escape key to close the menu for you.
208 |
209 | Whenipress allows you to do this using the `whileFocusIsWithin` method. This method accepts a query selector or an Element.
210 |
211 | ```javascript
212 | whenipress('Escape').whileFocusIsWithin('#slideout-menu').then(e => closeMenu())
213 |
214 | // Or...
215 | whenipress('Escape').whileFocusIsWithin(document.querySelector('#slideout-menu')).then(e => closeMenu())
216 | ```
217 |
218 | Whenipress will make sure that the `#slideout-menu` or one of its descendents has focus before executing your callback.
219 |
220 |
221 | ## Extending whenipress
222 | Whenipress was created to be extended. Whilst it offers tons of functionality out of the box, it can do so much more with a plugin.
223 | What follows is a brief guide on how to get started creating your own plugins for whenipress.
224 |
225 | > Created a great plugin that you think would benefit the community? Create an issue for it and we'll link you here!
226 |
227 | ### Registering plugins
228 | To register a plugin in your application, whenipress provides a `use` method.
229 |
230 | ```javascript
231 | import whenipress from 'whenipress/whenipress'
232 | import plugin from 'awesomeplugin/plugin'
233 | import anotherPlugin from 'thegreatplugin/plugin'
234 |
235 | whenipress().use(plugin, anotherPlugin)
236 | ```
237 |
238 | ### Plugin syntax
239 | Whenipress plugins are essentially JSON objects. The properties on that JSON object will be called by whenipress during
240 | different stages of the process, allowing you to hook in and perform any functionality you can think of. You do not
241 | need to include every hook, only the ones you're interested in using for your plugin.
242 |
243 | What follows is a list of available hooks.
244 |
245 | #### Initialising your plugin
246 | If you need to perform a setup step in your plugin, you should use the `mounted` hook. It is called when your plugin
247 | is first registered by the user. This receives the global `whenipress` instance as a parameter.
248 | You should use this in your plugin instead of calling `whenipress` as the end user may have aliased `whenipress` under
249 | a different name.
250 |
251 | ```javascript
252 | const myPlugin = {
253 | mounted: globalInstance => {
254 | alert('Hello world!')
255 | globalInstance.register('a', 'b', 'c').then(e => alert('You pressed a, b and c'))
256 | }
257 | }
258 | ```
259 |
260 | Note that in a plugin, we can register new keyboard bindings using the `register` method on the globalInstance.
261 |
262 | #### Listen for when a new binding is registered
263 | If you want to be notified every time a new key combination is registered with whenipress, you can use the `bindingRegistered`
264 | hook. It will receive the binding that was registered as the first parameter and the global `whenipress` instance as the second
265 | parameter. You should use this in your plugin instead of calling `whenipress` as the end user may have aliased `whenipress` under
266 | a different name.
267 |
268 | ```javascript
269 | const myPlugin = {
270 | bindingRegistered: (binding, globalInstance) => {
271 | alert(`I'm now listening for any time you press ${binding.join(" + ")}.`)
272 | }
273 | }
274 | ```
275 |
276 | Note that it is not guaranteed the user has initialised your plugin prior to creating their bindings. If you need to ensure
277 | you have all bindings, you should iterate over registered bindings in your plugin's mounted method.
278 |
279 | #### Listen for when a binding is stopped
280 | If you wish to be notified of when a binding has been removed from whenipress, you can use the `bindingStopped` hook.
281 | It will receive the binding that was stopped as the first parameter and the global `whenipress` instance as the second
282 | parameter. You should use this in your plugin instead of calling `whenipress` as the end user may have aliased `whenipress` under
283 | a different name.
284 |
285 | ```javascript
286 | const myPlugin = {
287 | bindingStopped: (binding, globalInstance) => {
288 | alert(`You are no longer listening for ${binding.join(" + ")}.`)
289 | }
290 | }
291 | ```
292 |
293 | #### Listen for when all bindings are stopped
294 | To be informed when all bindings in the application are stopped, you should use the `allBindingsStopped` hook.
295 | This receives the global `whenipress` instance as a parameter.
296 | You should use this in your plugin instead of calling `whenipress` as the end user may have aliased `whenipress` under
297 | a different name.
298 |
299 | ```javascript
300 | const myPlugin = {
301 | allBindingsStopped: globalInstance => {
302 | alert(`Do not go gently into that good night...`)
303 | }
304 | }
305 | ```
306 |
307 | #### Hook in before an event is handler is fired
308 | It may be useful to perform an action just before a keyboard shortcut is handled. Say hello to the `beforeBindingHandled` hook.
309 | It will receive the binding that is to be handled as the first parameter and the global `whenipress` instance as the second
310 | parameter. You should use this in your plugin instead of calling `whenipress` as the end user may have aliased `whenipress` under
311 | a different name.
312 |
313 | ```javascript
314 | const myPlugin = {
315 | beforeBindingHandled: (binding, globalInstance) => {
316 | alert(`You just pressed ${binding.join(" + ")}, but I got here first.`)
317 | }
318 | }
319 | ```
320 |
321 | You can actually prevent the handler from ever firing by returning `false` in you hook. This is useful if your plugin
322 | adds conditional functionality.
323 |
324 | ```javascript
325 | const myPlugin = {
326 | beforeBindingHandled: (binding, globalInstance) => {
327 | if (userHasDisabledKeyboardShortcuts()) {
328 | return false;
329 | }
330 | }
331 | }
332 | ```
333 |
334 | #### Hook in after an event has been handled
335 | You may wish to know when a keyboard binding has been handled. You can use the `afterBindingHandled` hook for this.
336 | It will receive the binding that has been handled as the first parameter and the global `whenipress` instance as the second
337 | parameter. You should use this in your plugin instead of calling `whenipress` as the end user may have aliased `whenipress` under
338 | a different name.
339 |
340 | ```javascript
341 | const myPlugin = {
342 | afterBindingHandled: (binding, globalInstance) => {
343 | alert(`You just pressed ${binding.join(" + ")}. It has been handled, but now I'm going to do something as well.`)
344 | }
345 | }
346 | ```
347 |
348 | #### Plugin options
349 | Whenipress provides a unified method of handling custom options to users of your plugin. To do so, register an `options`
350 | field in your plugin JSON. You can include anything in here that you want, but be sure to let your users know in your
351 | plugin's documentation.
352 |
353 | In our example below, we have decided to provide to options to the user, `urlsToSkip` and `skipAllUrls`. These are
354 | completely unique to your plugin and you're in charge of managing them.
355 |
356 | ```javascript
357 | const myPlugin = {
358 | options: {
359 | urlsToSkip: [],
360 | skipAllUrls: false
361 | }
362 | }
363 | ```
364 |
365 | Now, when your plugin is registered, these options can be overridden by the user.
366 |
367 | ```javascript
368 | import whenipress from 'whenipress/whenipress'
369 | import myPlugin from 'awesomeplugin/myPlugin'
370 |
371 | whenipress().use(whenipress().pluginWithOptions(myPlugin, { skipAllUrls: true }))
372 | ```
373 |
374 | #### Stopping plugins
375 | If you wish to stop all plugins running, you may use the `flushPlugins` method.
376 |
377 | ```javascript
378 | whenipress().flushPlugins()
379 | ```
380 | ## Contributors ✨
381 |
382 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
383 |
384 |
385 |
386 |
387 |