GenieJS 🧞
A JavaScript library committed to improving user experience by empowering users to interact with web apps using the keyboard (better than cryptic shortcuts).
Genie |ˈjēnē| (noun): a spirit of Arabian folklore, as traditionally depicted imprisoned within a bottle or oil lamp, and capable of granting wishes when summoned.
Old links:
- Demo (Demo with React and Downshift)
- Tests
- Genie Workshop - Terrific way to learn how to use genie right in your browser.
- API Docs
- Chrome Extension
The problem
You want to enable users to power through your application with the keyboard, but you're limited on the kinds of reasonable keyboard shortcuts you can use.
This solution
GenieJS is a library to emulate the same kind of behavior seen in apps like Alfred. Essentially, you register actions associated with keywords. Then you can request the genie to perform that action based on the best keyword match for a given keyword.
Over time, the genie will learn the actions more associated with specific keywords and those will be come first when a list of matching actions is requested. If that didn't make sense, don't worry, hopefully the tutorial, tests, and demo will help explain how it works.
- Vernacular
- How to use it
- API
- Special Wish Actions
- About Matching Priority
- About Optimistic Anticipation
- About Context
- Enabling & Disabling
- Merging Wishes
- Inspiration
- Other Solutions
- Issues
- Contributors ✨
- LICENSE
Vernacular
Wish: An object with an id, action, and magic words.
Action: What to call when this wish is to be executed.
Magic Word: Keywords for a wish used to match it with given magic words.
On Deck: The second wish of preference for a certain magic word which will be King of the Hill if chosen again.
King of the Hill: The wish which gets preference for a certain magic word until the On Deck wish is chosen again (it then becomes On Deck).
How to use it
If you're using RequireJS then you can simply
require('path/to/genie')
. Or you could simply include the regular script tag:
<!-- This will place `genie` on the global namespace for your delight. -->
genie
is a function with a few useful functions as properties of genie
. The
flow of using GenieJS is simple:
/* Register wishes */// One magic wordvar trashWish = // Multiple magic wordsvar vacuumWish = /* Get wishes based on magic word matches */genie // returns [vacuumWish];genie // returns [trashWish, vacuumWish]; // Make wish based on wish object or id of wish objectgenie // logs: 'Yes! I love taking out the trash!'genie // logs: 'Can NOT wait to get that dust out of that carpet!'
So far it doesn't look too magical, but the true magic comes in the form of genie giving preference to wishes that were recently chosen with a given keyword. To do this, you need to provide genie with a magic word to associate the wish with, like so:
genie // logs as abovegenie // returns [vacuumWish, trashWish]; <-- Notice difference from above
As you'll notice, the order of the two wishes is changed because genie gave
preference to the vacuumWish
because the last time makeWish
was called with
the the 'out'
magic word, vacuumWish
was the wish given.
This behavior simulates apps such as Alfred which is the goal of this library!
API
Genie is undergoing an overhaul on the API documentation using autodocs. It is still being worked on, but you can see that documentation here.
Below you can see full documentation. It's just less enjoyable to read...
Objects
There are a few internal objects you may want to be aware of:
var wishObject = id: 'string' data: timesMade: total: 0 magicWords: 'Magic Word': 1 context: all: 'string' any: 'string' none: 'string' keywords: 'string' {} var enteredMagicWords = h: wishes: 'wishid1' 'wishid2' e: l: l: o: wishes: 'wishid3' 'wishid4' p: wishes: 'wishid5' 'wishid2' var pathContext = paths: 'string' regexes: /regex/gi contexts: 'The context to apply'
You have the following api to use at your discretion:
/* * If no id is provided, one will be auto-generated via the previousId + 1 * Genie adds a "timesMade" property to the "data" property * This is incremented every time the wish is made * (when the action is called) * You can also provide genie with an array of these objects * to register all of them at once. * Returns the wish object. */
Special Wish Actions
There are some actions that are common use cases, so genie helps with these (currently only one special wish action):
Navigation
You for the action of the wish you can provide either a string (URL) or an object with a destination property (URL). If the action is an object this gives you a few options:
- openNewTab - If truthy, this will open the URL using '_blank'. Otherwise opens in the current window.
- That's all for now... any other ideas?
About Matching Priority
The wishes returned from getMatchingWishes
are ordered with the following
priority
- King of the Hill for the given
magicWords
(genie optimistically anticipates this as well) - On Deck for the given
magicWords
(also optimistically anticipated) - If the given magic word is equal to any magic words of a wish
- If the given magic word is the start to any magic word of a wish (i.e. 'he' in 'hello');
- If the given magic word is the start to any word in a magic word (i.e. 'wo' in 'hello world');
- If the given magic word is contained in any magic words of a wish
- If the given magic word is an acronym of any magic words of a wish
- If the given magic word matches the order of characters in any magic words of a wish.
Just trust the genie. He knows best. And if you think otherwise, let me know or (even better) contribute :)
About Optimistic Anticipation
Genie keeps track of which wishes were executed with which magic words so it
knows which wish is "King of the Hill" and "On Deck." But it's not a simple
string-to-string comparison. If I have a wish with the magic words of
'Do laundry'
and another with 'Laundry stinks
' then make the 'Do laundry
'
wish with 'laundry
', I would have to type the entire word 'laundry
' before
'Do laundry'
came up to the top. So genie will anticipate that what I'm typing
to be 'laundry'
until I type something that renders this impossible (like if I
type 'lan'
, it will anticipate 'laundry
' until I type the 'n'
and keep
'Do laundry'
at the top until I do).
This is possible because the structure of object that genie uses to keep track of entered magic words:
"enteredMagicWords":
If you're curious, look in the code :-)
About Context
Genie has a concept of context that allows you to switch between sets of wishes
easily. It's a toss up between context and the matching algorithm on which is
more complex but hopefully I can explain it well enough for you! Each wish is
given the default context which is universe
unless one is provided when it is
registered. Wishes will only behave normally in getMatchingWishes
and
makeWish
when they are in context.
The easiest way to think of a wish context is that it is structured like so:
all: 'context1' 'context2' any: 'context3' 'context4' none: 'context5' 'context6'
If you set a wish's context to a string or array of strings, it behaves like so:
// what you set:var wish = console // logs {any: ['context', 'context2']}
There are a few ways for a wish to definitely be in context:
- Genie's current context is the default context
- The wish's context is the default context (does not apply if it simply contains the default context)
- The wish's context is equal to the current context
If none of these are true, then these things must be true for the wish to be in context:
- Genie's context does not contain any of the wish's
context.none
contexts if it exists. - Genie's context contains at least one of the wish's
context.any
contexts if it exists. - Genie's context contains all of the wish's
context.all
contexts if it exists.
Checkout the tests for #context to see more how this works. Here's a simple demonstration:
// Simple stuff // Before setting context, genie.context is defaultwish0context // returns the default contextwish1context = 'context1'wish2context = 'context1' 'context2'wish3context = 'context3' genie // returns [wish0, wish1, wish2, wish3] geniegenie // returns [wish0, wish1, wish2] geniegenie // returns [wish0, wish2] geniegenie // returns [wish0, wish3] geniegenie // returns [wish0, wish1, wish2] geniegenie // returns [wish0, wish1, wish2, wish3]
// Complex stuff geniecontext = 'context1' 'context2' 'context3' 'context4' wish0context // returns the default contextwish1context = any: 'context2' 'context5'wish2context = none: 'context3' 'context5'wish3context = all: 'context1' 'context5' genie // returns [wish0, wish1] geniegenie // returns [wish0, wish1, wish3] geniegenie // returns [wish0, wish1, wish2] genie // resets genie's context to defaultgenie // returns [wish0, wish2]
Path Context
A big use case for context is to have a url path (or route) represent the
context for genie. For example, if you have an email app, you can have the
/index
and the /message/:id
routes which would have different contexts.
Instead of managing this yourself, genie can help you a little. Genie will not
watch the URL for you, so you have to do that yourself. This is by design. At
any time, you can call genie.updatePathContext(window.location.pathname)
and
genie will update the context based on an internal variable called
_pathContexts
. You have control over what's in this array using the
genie.addPathContext(pathContext)
and the
genie.removePathContext(pathContext)
methods. A pathContext
object looks
like this:
paths: string || array of strings | regexes: regex || array of regexes | contexts: string || array of strings | required
The contexts
variable is special and is associated with the regexes variable.
The easiest way to describe this is via an example:
If I have a pathContext object like this:
regexes: /\/pizza\//gi /\/pizza\//gi contexts: 'a-page-{{1}}'
Then, when I call genie.updatePathContext('/pizza/1234')
it will match this
pathContext and genie will automatically change a-page-{{1}}
to a-page-1234
.
The 1
in a-page-{{1}}
represents the group that is matched on the path in
the regex. It will replace the digit in {{\d}}
with the group that's matched
(Note: in true JavaScript form, group 0 represents the entire match string,
hence, 1 is the first group in parentheses).
Enabling & Disabling
To give you a little more control, you can enable and disable genie globally.
All genie functions go through a check to make sure genie is enabled. If it is
enabled, everything works as expected. If it is disabled, then genie will return
an empty object/array/string depending on what the function you're calling is
expecting. This behavior is to prevent the need to do null/undefined checking
everywhere you use genie
and can be disabled as well via the returnOnDisabled
function.
Merging Wishes
To persist the user's experience, you may want to store the result of
genie.options()
in localStorage
or even a database associated with the user.
Then after you have registered all the wishes for the user you load the options
by calling genie.options({wishes: usersOptions})
. The problem with this is
that usersOptions
wont have the actions for wishes, so this would overwrite
the wishes with a bunch that don't have actions.
To prevent this, by default when you call genie.options
genie will merge the
wishes. So any new wishes provided will either overwrite wishes with the same
ID, but preserve the action of the old version if the new version doesn't have
an action already. It will also preserve wishes which existed before and don't
have matching ids.
To completely overwrite the existing wishes, simply pass in noWishMerge
along
with the wishes.
Note: Genie provides direct access to the mergeWishes
function as well.
Inspiration
I built this after I was trying to add keyboard shortcuts to an application at work and ran out of letters that made sense. I was heavily inspired by Alfred.
Other Solutions
Similar solutions we know of:
If you are aware of other solutions please make a pull request and add it here!
Issues
Looking to contribute? Look for the [Good First Issue][good-first-issue] label.
🐛 Bugs
Please file an issue for bugs, missing documentation, or unexpected behavior.
[See Bugs][bugs]
💡 Feature Requests
Please file an issue to suggest new features. Vote on feature requests by adding a 👍. This helps maintainers prioritize what to work on.
[See Feature Requests][requests]
Contributors ✨
Thanks goes to these people (emoji key):
Kent C. Dodds 💻 📖 🚇 ⚠️ 📢 |
swyx 📖 |
Justin Dorfman 🔍 |
Michaël De Boey 💻 |
This project follows the all-contributors specification. Contributions of any kind welcome!
LICENSE
MIT