├── .gitattributes ├── .prettierignore ├── .huskyrc.js ├── .doclets.yml ├── .prettierrc.js ├── other └── logo.png ├── .gitignore ├── jest.config.js ├── .travis.yml ├── LICENSE ├── package.json ├── .all-contributorsrc ├── CONTRIBUTING.md ├── CHANGELOG.md ├── src ├── __tests__ │ └── index.js └── index.js └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | dist 4 | -------------------------------------------------------------------------------- /.huskyrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('kcd-scripts/husky') 2 | -------------------------------------------------------------------------------- /.doclets.yml: -------------------------------------------------------------------------------- 1 | dir: src 2 | articles: 3 | - About: README.md 4 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('kcd-scripts/prettier') 2 | -------------------------------------------------------------------------------- /other/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kentcdodds/genie/HEAD/other/logo.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | dist 4 | .DS_Store 5 | 6 | # these cause more harm than good 7 | # when working with contributors 8 | package-lock.json 9 | yarn.lock 10 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const jest = require('kcd-scripts/jest') 2 | 3 | module.exports = { 4 | ...jest, 5 | coverageThreshold: { 6 | global: { 7 | statements: 78, 8 | branches: 71, 9 | lines: 78, 10 | functions: 78, 11 | }, 12 | }, 13 | testEnvironment: 'jsdom', 14 | } 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: npm 3 | notifications: 4 | email: false 5 | node_js: 6 | - 10.0.0 7 | - 12 8 | - 14 9 | - node 10 | install: npm install 11 | script: 12 | - npm run validate 13 | - npx codecov@3 14 | branches: 15 | only: 16 | - master 17 | - beta 18 | 19 | jobs: 20 | include: 21 | - stage: release 22 | node_js: 14 23 | script: kcd-scripts travis-release 24 | if: fork = false 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2017 Kent C. Dodds 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "geniejs", 3 | "version": "0.0.0-semantically-released", 4 | "description": "A JavaScript library committed to improving user experience by empowering users to interact with web apps using the keyboard (better than cryptic shortcuts).", 5 | "main": "dist/geniejs.cjs.js", 6 | "module": "dist/geniejs.es.js", 7 | "jsnext:main": "dist/geniejs.es.js", 8 | "keywords": [], 9 | "author": "Kent C. Dodds (https://kentcdodds.com)", 10 | "license": "MIT", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/kentcdodds/genie" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/kentcdodds/genie/issues" 17 | }, 18 | "homepage": "https://github.com/kentcdodds/genie#readme", 19 | "files": [ 20 | "dist" 21 | ], 22 | "scripts": { 23 | "build": "kcd-scripts build --bundle --environment BUILD_NAME:genie", 24 | "lint": "kcd-scripts lint", 25 | "setup": "npm install && npm run validate -s", 26 | "test": "kcd-scripts test", 27 | "test:update": "npm test -- --updateSnapshot --coverage", 28 | "validate": "kcd-scripts validate" 29 | }, 30 | "dependencies": {}, 31 | "devDependencies": { 32 | "kcd-scripts": "^5.6.0" 33 | }, 34 | "eslintConfig": { 35 | "extends": "./node_modules/kcd-scripts/eslint.js", 36 | "rules": { 37 | "max-lines": "off", 38 | "max-lines-per-function": "off" 39 | } 40 | }, 41 | "eslintIgnore": [ 42 | "node_modules", 43 | "coverage", 44 | "dist" 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "genie", 3 | "projectOwner": "kentcdodds", 4 | "imageSize": 100, 5 | "commit": false, 6 | "contributorsPerLine": 7, 7 | "repoHost": "https://github.com", 8 | "repoType": "github", 9 | "skipCi": false, 10 | "files": [ 11 | "README.md" 12 | ], 13 | "contributors": [ 14 | { 15 | "login": "kentcdodds", 16 | "name": "Kent C. Dodds", 17 | "avatar_url": "https://avatars.githubusercontent.com/u/1500684?v=3", 18 | "profile": "https://kentcdodds.com", 19 | "contributions": [ 20 | "code", 21 | "doc", 22 | "infra", 23 | "test", 24 | "talk" 25 | ] 26 | }, 27 | { 28 | "login": "sw-yx", 29 | "name": "swyx", 30 | "avatar_url": "https://avatars1.githubusercontent.com/u/6764957?v=4", 31 | "profile": "https://twitter.com/swyx", 32 | "contributions": [ 33 | "doc" 34 | ] 35 | }, 36 | { 37 | "login": "jdorfman", 38 | "name": "Justin Dorfman", 39 | "avatar_url": "https://avatars1.githubusercontent.com/u/398230?v=4", 40 | "profile": "https://stackshare.io/jdorfman/decisions", 41 | "contributions": [ 42 | "fundingFinding" 43 | ] 44 | }, 45 | { 46 | "login": "MichaelDeBoey", 47 | "name": "Michaël De Boey", 48 | "avatar_url": "https://avatars3.githubusercontent.com/u/6643991?v=4", 49 | "profile": "https://michaeldeboey.be", 50 | "contributions": [ 51 | "code" 52 | ] 53 | } 54 | ] 55 | } 56 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for being willing to contribute! 4 | 5 | **Working on your first Pull Request?** You can learn how from this _free_ 6 | series [How to Contribute to an Open Source Project on GitHub][egghead] 7 | 8 | ## Project setup 9 | 10 | 1. Fork and clone the repo 11 | 2. `$ npm install` to install dependencies 12 | 3. `$ npm run validate` to validate you've got it working 13 | 4. Create a branch for your PR 14 | 15 | > Tip: Keep your `master` branch pointing at the original repository and make 16 | > pull requests from branches on your fork. To do this, run: 17 | > 18 | > ``` 19 | > git remote add upstream https://github.com/kentcdodds/genie 20 | > git fetch upstream 21 | > git branch --set-upstream-to=upstream/master master 22 | > ``` 23 | > 24 | > This will add the original repository as a "remote" called "upstream," Then 25 | > fetch the git information from that remote, then set your local `master` 26 | > branch to use the upstream master branch whenever you run `git pull`. Then you 27 | > can make all of your pull request branches based on this `master` branch. 28 | > Whenever you want to update your version of `master`, do a regular `git pull`. 29 | 30 | ## Committing and Pushing changes 31 | 32 | Please make sure to run the tests before you commit your changes. You can run 33 | `npm run test:update` which will update any snapshots that need updating. Make 34 | sure to include those changes (if they exist) in your commit. 35 | 36 | ## Help needed 37 | 38 | Please checkout the [the open issues][issues] 39 | 40 | Also, please watch the repo and respond to questions/bug reports/feature 41 | requests! Thanks! 42 | 43 | 44 | [egghead]: https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github 45 | [issues]: https://github.com/kentcdodds/generator-kcd-oss/issues 46 | 47 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Latest 2 | 3 | # 0.5.0 4 | 5 | - Remove all lamps 6 | - Modernize the tooling of the project 7 | 8 | # 0.4.0 9 | 10 | - Pulling in ux-genie. 11 | - Reorganizing some of the project. 12 | - Creating slots for lamps made to work with other frameworks and libraries. 13 | - Starting tests for ux-genie. 14 | - Bug Fix: Path Contexts would only match the first group in the regex. One 15 | character change. 16 | 17 | # 0.3.0 18 | 19 | - Breaking changes: Changed how wishes are registered. Now you can only register 20 | with objects and arrays of objects. You can't register by parameters. There 21 | were just too many parameters, and everyone using this was using 22 | objects/arrays anyway. Also changing context a tad internally. Now all context 23 | will be created as an object. Though a string or array can still be provided, 24 | it will be assigned to the `context.any` property. 25 | - Update to the matching algorithm to improve suggestions. 26 | - Genie now adds a `data` property to all wishes with a `timesMade` object 27 | indicating how many times that wish had been made (how many times the action 28 | was called) total as well as with specific magic words. 29 | - Added `getWishesWithContext(context, type, wishContextTypes)` 30 | - Added `getWishesInContext(context)` 31 | - Added `getWish(id)` 32 | - Added 33 | `overrideMatchingAlgorithm(function(wishes, magicWord, enteredMagicWords){})`. 34 | See README. 35 | - Adding autodocs 36 | - Added `restoreMatchingAlgorithm()` 37 | - Seriously documented some functions. 38 | - Adding jshint to the build. Some errors corrected. 39 | - Old tests fixed/removed 40 | - Updated README accordingly 41 | 42 | # 0.2.5 43 | 44 | - Big improvement to context. Added complex context functionality allowing for 45 | more fine control over what wishes are in context and which are not. 46 | 47 | # 0.2.4 48 | 49 | - Bug fix: if a wish is deregistered, it is removed from the entered magic 50 | words, but the entry in the entered magic words remained even if it was empty. 51 | 52 | # 0.2.3 53 | 54 | - Added the pathContext feature 55 | - Added some internal helpers 56 | - Added this changelog file :) 57 | -------------------------------------------------------------------------------- /src/__tests__/index.js: -------------------------------------------------------------------------------- 1 | import genie from '../' 2 | 3 | beforeEach(prepForTest) 4 | 5 | describe('#', () => { 6 | beforeEach(prepForTest) 7 | test('should register with an object', () => { 8 | genie({ 9 | magicWords: 'magicWord', 10 | action() {}, 11 | }) 12 | const wishData = { 13 | name: 'value', 14 | } 15 | const maximalObject = genie({ 16 | id: 'coolId', 17 | context: ['gandpa', 'child', 'grandchild'], 18 | data: wishData, 19 | magicWords: ['magic', 'word'], 20 | action() {}, 21 | }) 22 | const allWishes = genie.getMatchingWishes() 23 | expect(allWishes).toHaveLength(2) 24 | expect(maximalObject.id).toBe('coolId') 25 | expect(maximalObject.context.any).toHaveLength(3) 26 | expect(maximalObject.data).toBe(wishData) 27 | expect(maximalObject.magicWords).toHaveLength(2) 28 | }) 29 | 30 | test('should register with an array', () => { 31 | genie([ 32 | { 33 | magicWords: 'wish1', 34 | action() {}, 35 | }, 36 | { 37 | magicWords: 'wish2', 38 | action() {}, 39 | }, 40 | { 41 | magicWords: 'wish3', 42 | action() {}, 43 | }, 44 | ]) 45 | 46 | const allWishes = genie.getMatchingWishes() 47 | expect(allWishes).toHaveLength(3) 48 | }) 49 | 50 | test('should overwrite wish with duplicateId', () => { 51 | const wishOne = genie( 52 | fillInWish({ 53 | id: 'id', 54 | }), 55 | ) 56 | const wishTwo = genie( 57 | fillInWish({ 58 | id: 'id', 59 | }), 60 | ) 61 | const allWishes = genie.getMatchingWishes() 62 | expect(allWishes).toHaveLength(1) 63 | expect(wishTwo).toBe(allWishes[0]) 64 | expect(wishOne).not.toBe(allWishes[0]) 65 | }) 66 | 67 | test('should not have any wishes registered prior to registration', () => { 68 | const allWishes = genie.getMatchingWishes() 69 | expect(allWishes).toHaveLength(0) 70 | }) 71 | 72 | test('should have one wish registered upon registration and zero upon deregistration', () => { 73 | const wish = genie(fillInWish()) 74 | let allWishes = genie.getMatchingWishes() 75 | expect(allWishes).toHaveLength(1) 76 | genie.deregisterWish(wish) 77 | allWishes = genie.getMatchingWishes() 78 | expect(allWishes).toHaveLength(0) 79 | }) 80 | 81 | test('should make the last wish registered without giving magic words', () => { 82 | genie(fillInWish()) 83 | const wishToBeMade = genie(fillInWish()) 84 | const allWishes = genie.getMatchingWishes() 85 | expect(allWishes).toHaveLength(2) 86 | const madeWish = genie.makeWish() 87 | expect(wishToBeMade.data.timesMade.total).toBe(1) 88 | expect(madeWish).toBe(wishToBeMade) 89 | }) 90 | 91 | test('should return empty object and not register a wish when genie is disabled', () => { 92 | genie.enabled(false) 93 | const emptyObject = genie(fillInWish()) 94 | expect(Object.keys(emptyObject)).toHaveLength(0) 95 | let allWishes = genie.getMatchingWishes() 96 | expect(allWishes).toHaveLength(0) 97 | 98 | genie.enabled(true) 99 | genie.returnOnDisabled(false) 100 | genie.enabled(false) 101 | const nullObject = genie(fillInWish()) 102 | 103 | expect(nullObject).toBeNull() 104 | allWishes = genie.getMatchingWishes() 105 | expect(allWishes).toBeNull() 106 | }) 107 | 108 | describe('special wish types', () => { 109 | describe('navigation wish type', () => { 110 | test('should navigate when a wish is made whose action is a string', () => { 111 | const destination = `${window.location.href}#success` 112 | const moveToSuccess = genie({ 113 | magicWords: 'Add success hash', 114 | action: destination, 115 | }) 116 | genie.makeWish(moveToSuccess) 117 | expect(window.location.href).toBe(destination) 118 | // Cannot really test the new tab action, but if I could, it would look like this: 119 | // wish = genie({ 120 | // magicWords: 'Open GenieJS repo', 121 | // action: { 122 | // destination: 'http://www.github.com/kentcdodds/genie', 123 | // openNewTab: true 124 | // } 125 | // }); 126 | // genie.makeWish(wish); 127 | }) 128 | }) 129 | }) 130 | }) 131 | 132 | describe('#reset', () => { 133 | beforeEach(prepForTest) 134 | test('should reset all options', () => { 135 | const originalOptions = genie.options() 136 | 137 | const wish = genie(fillInWish()) 138 | genie.makeWish(wish) 139 | 140 | expect(genie.options()).not.toBe(originalOptions) 141 | genie.reset() 142 | }) 143 | }) 144 | 145 | // End registration 146 | 147 | describe('#deregisterWish #deregisterWishesWithContex', () => { 148 | beforeEach(prepForTest) 149 | const allWishCount = 5 150 | let wish1, allWishes 151 | beforeEach(done => { 152 | wish1 = genie(fillInWish()) 153 | genie( 154 | fillInWish({ 155 | context: 'context1', 156 | }), 157 | ) 158 | genie( 159 | fillInWish({ 160 | context: 'context1', 161 | }), 162 | ) 163 | genie( 164 | fillInWish({ 165 | context: 'context2', 166 | }), 167 | ) 168 | genie( 169 | fillInWish({ 170 | context: 'context2', 171 | }), 172 | ) 173 | done() 174 | }) 175 | test('should have one less wish when a wish is deregistered', () => { 176 | allWishes = genie.getMatchingWishes() 177 | expect(allWishes).toHaveLength(allWishCount) 178 | genie.deregisterWish(wish1) 179 | 180 | allWishes = genie.getMatchingWishes() 181 | expect(allWishes).toHaveLength(allWishCount - 1) 182 | }) 183 | 184 | test('should remove only wishes in a given context (excluding the default context) when deregisterWishesWithContext is called', () => { 185 | allWishes = genie.getMatchingWishes() 186 | expect(allWishes).toHaveLength(allWishCount) 187 | genie.deregisterWishesWithContext('context1') 188 | 189 | allWishes = genie.getMatchingWishes() 190 | expect(allWishes).toHaveLength(allWishCount - 2) 191 | }) 192 | }) 193 | // End #deregisterWish #deregisterWishesWithContex 194 | 195 | describe('#context #addContext #removeContext', () => { 196 | beforeEach(prepForTest) 197 | let defaultContextWish, complexContextNone 198 | beforeEach(done => { 199 | defaultContextWish = genie(fillInWish()) 200 | genie( 201 | fillInWish({ 202 | context: 'context1', 203 | }), 204 | ) 205 | genie( 206 | fillInWish({ 207 | context: 'context2', 208 | }), 209 | ) 210 | genie( 211 | fillInWish({ 212 | context: ['context3'], 213 | }), 214 | ) 215 | genie( 216 | fillInWish({ 217 | context: ['context1', 'context2', 'context3'], 218 | }), 219 | ) 220 | genie( 221 | fillInWish({ 222 | context: { 223 | all: ['context1', 'context2', 'context3'], 224 | }, 225 | }), 226 | ) 227 | genie( 228 | fillInWish({ 229 | context: { 230 | any: ['context1', 'context3', 'context5'], 231 | }, 232 | }), 233 | ) 234 | complexContextNone = genie( 235 | fillInWish({ 236 | context: { 237 | none: ['context1', 'context2'], 238 | }, 239 | }), 240 | ) 241 | genie( 242 | fillInWish({ 243 | context: { 244 | all: ['context1', 'context3'], 245 | any: ['context4', 'context5'], 246 | none: ['context2'], 247 | }, 248 | }), 249 | ) 250 | done() 251 | }) 252 | test('should have all wishes when genie.context is default', () => { 253 | expect(genie.getMatchingWishes()).toHaveLength(9) 254 | }) 255 | 256 | test('should have only wishes with default context when genie.context is not default', () => { 257 | genie.context('different-context') 258 | const matchingWishes = genie.getMatchingWishes() 259 | expect(matchingWishes).toHaveLength(2) 260 | expect(matchingWishes[0]).toBe(complexContextNone) 261 | expect(matchingWishes[1]).toBe(defaultContextWish) 262 | }) 263 | 264 | test('should have only in context wishes (including default context wishes) when genie.context is not default', () => { 265 | genie.context('context1') 266 | expect(genie.getMatchingWishes()).toHaveLength(4) 267 | }) 268 | 269 | test('should be able to have multiple contexts', () => { 270 | genie.context(['context1', 'context2']) 271 | expect(genie.getMatchingWishes()).toHaveLength(5) 272 | }) 273 | 274 | test('should be able to add a string context', () => { 275 | genie.context('context1') 276 | genie.addContext('context2') 277 | expect(genie.getMatchingWishes()).toHaveLength(5) 278 | }) 279 | 280 | test('should be able to add an array of contexts', () => { 281 | genie.context('context1') 282 | genie.addContext(['context2', 'context3']) 283 | expect(genie.getMatchingWishes()).toHaveLength(7) 284 | }) 285 | 286 | test('should be able to remove string context', () => { 287 | genie.context(['context1', 'context2']) 288 | expect(genie.getMatchingWishes()).toHaveLength(5) 289 | 290 | genie.removeContext('context1') 291 | expect(genie.getMatchingWishes()).toHaveLength(3) 292 | }) 293 | 294 | test('should be able to remove an array of contexts', () => { 295 | genie.context(['context1', 'context2', 'context3']) 296 | expect(genie.getMatchingWishes()).toHaveLength(7) 297 | 298 | genie.removeContext(['context1', 'context2']) 299 | expect(genie.getMatchingWishes()).toHaveLength(5) 300 | }) 301 | 302 | test('should be able to manage complex contexts', () => { 303 | genie.context(['context1', 'context3', 'context5']) 304 | const allWishes = genie.getMatchingWishes() 305 | expect(allWishes).toHaveLength(6) 306 | }) 307 | }) // end #context #addContext #removeContext 308 | 309 | describe('#getMatchingWishes', () => { 310 | beforeEach(prepForTest) 311 | let wishes, wishesArray 312 | beforeEach(() => { 313 | wishes = {} 314 | wishesArray = [] 315 | wishes.equal = genie( 316 | fillInWish({ 317 | magicWords: 'tTOtc', // equal 318 | }), 319 | ) 320 | wishesArray.push(wishes.equal) 321 | 322 | wishes.equal2 = genie( 323 | fillInWish({ 324 | magicWords: ['First Magic Word', 'tTOtc'], // equal 2nd magic word 325 | }), 326 | ) 327 | wishesArray.push(wishes.equal2) 328 | 329 | wishes.contains = genie( 330 | fillInWish({ 331 | magicWords: 'The ttotc container', // contains 332 | }), 333 | ) 334 | wishesArray.push(wishes.contains) 335 | 336 | wishes.contains2 = genie( 337 | fillInWish({ 338 | magicWords: ['First Magic Word', 'The ttotc container'], // contains 2nd magic word 339 | }), 340 | ) 341 | wishesArray.push(wishes.contains2) 342 | 343 | wishes.acronym = genie( 344 | fillInWish({ 345 | magicWords: 'The Tail of Two Cities', // acronym 346 | }), 347 | ) 348 | wishesArray.push(wishes.acronym) 349 | 350 | wishes.acronym2 = genie( 351 | fillInWish({ 352 | magicWords: ['First Magic Word', 'The Tail of Two Cities'], // acronym 2nd magic word 353 | }), 354 | ) 355 | wishesArray.push(wishes.acronym2) 356 | 357 | wishes.match = genie( 358 | fillInWish({ 359 | magicWords: 'The Tail of Forty Cities', // match 360 | }), 361 | ) 362 | wishesArray.push(wishes.match) 363 | 364 | wishes.match2 = genie( 365 | fillInWish({ 366 | magicWords: ['First Magic Word', 'The Tail of Forty Cities'], // match 2nd magic word 367 | }), 368 | ) 369 | wishesArray.push(wishes.match2) 370 | 371 | wishes.noMatch = genie( 372 | fillInWish({ 373 | magicWords: 'no match', 374 | }), 375 | ) 376 | wishesArray.push(wishes.noMatch) 377 | }) 378 | 379 | test('should return wishes in order of most recently registered when not given any params', () => { 380 | const allWishes = genie.getMatchingWishes() 381 | let j = wishesArray.length - 1 382 | for (let i = 0; i < allWishes.length && j >= 0; i++, j--) { 383 | expect(allWishes[i]).toBe(wishesArray[j]) 384 | } 385 | }) 386 | 387 | test('should match equal, contains, acronym, and then match with the second magic word match coming after the first', () => { 388 | const ttotcAcronym = 'ttotc' 389 | 390 | // Even though they were registered in reverse order, the matching should follow this pattern 391 | const matchingWishes = genie.getMatchingWishes(ttotcAcronym) 392 | expect(matchingWishes).toHaveLength(wishesArray.length - 1) 393 | expect(matchingWishes[0]).toBe(wishes.equal) 394 | expect(matchingWishes[1]).toBe(wishes.equal2) 395 | expect(matchingWishes[2]).toBe(wishes.contains) 396 | expect(matchingWishes[3]).toBe(wishes.contains2) 397 | expect(matchingWishes[4]).toBe(wishes.acronym) 398 | expect(matchingWishes[5]).toBe(wishes.acronym2) 399 | expect(matchingWishes[6]).toBe(wishes.match) 400 | expect(matchingWishes[7]).toBe(wishes.match2) 401 | }) 402 | 403 | // eslint-disable-next-line max-statements 404 | test('should match entered magic words before anything else', () => { 405 | genie.makeWish(wishes.contains, 'tt') 406 | // This shouldn't affect the results of a search with more text 407 | 408 | genie.makeWish(wishes.match, 'ttot') 409 | let matchingWishes = genie.getMatchingWishes('ttot') 410 | expect(matchingWishes).toHaveLength(wishesArray.length - 1) 411 | expect(matchingWishes[0]).toBe(wishes.match) 412 | expect(matchingWishes[1]).toBe(wishes.equal) // starts with (not equal) in this case 413 | expect(matchingWishes[2]).toBe(wishes.equal2) // starts with 2 in this case 414 | expect(matchingWishes[3]).toBe(wishes.contains) 415 | expect(matchingWishes[4]).toBe(wishes.contains2) 416 | expect(matchingWishes[5]).toBe(wishes.acronym) 417 | expect(matchingWishes[6]).toBe(wishes.acronym2) 418 | expect(matchingWishes[7]).toBe(wishes.match2) 419 | 420 | genie.makeWish(wishes.match2, 'ttotc') 421 | matchingWishes = genie.getMatchingWishes('ttot') 422 | expect(matchingWishes).toHaveLength(wishesArray.length - 1) 423 | expect(matchingWishes[0]).toBe(wishes.match) 424 | expect(matchingWishes[1]).toBe(wishes.match2) 425 | expect(matchingWishes[2]).toBe(wishes.equal) // starts with in this case 426 | expect(matchingWishes[3]).toBe(wishes.equal2) // starts with 2 in this case 427 | expect(matchingWishes[4]).toBe(wishes.contains) 428 | expect(matchingWishes[5]).toBe(wishes.contains2) 429 | expect(matchingWishes[6]).toBe(wishes.acronym) 430 | expect(matchingWishes[7]).toBe(wishes.acronym2) 431 | 432 | matchingWishes = genie.getMatchingWishes('ttotc') 433 | expect(matchingWishes).toHaveLength(wishesArray.length - 1) 434 | expect(matchingWishes[0]).toBe(wishes.match2) 435 | expect(matchingWishes[1]).toBe(wishes.equal) 436 | expect(matchingWishes[2]).toBe(wishes.equal2) 437 | expect(matchingWishes[3]).toBe(wishes.contains) 438 | expect(matchingWishes[4]).toBe(wishes.contains2) 439 | expect(matchingWishes[5]).toBe(wishes.acronym) 440 | expect(matchingWishes[6]).toBe(wishes.acronym2) 441 | expect(matchingWishes[7]).toBe(wishes.match) 442 | }) 443 | 444 | test('should follow the rules with "on deck" and "king of the hill"', () => { 445 | const fred = wishes.contains2 446 | const ethel = wishes.acronym2 447 | const lucy = wishes.match2 448 | 449 | const wordToGet = 'magic' 450 | 451 | // In opposite order of their registration 452 | let mertzMatch = genie.getMatchingWishes(wordToGet) 453 | expect(mertzMatch[0]).toBe(lucy) 454 | expect(mertzMatch[1]).toBe(ethel) 455 | expect(mertzMatch[2]).toBe(fred) 456 | 457 | // In order of called then opposite registration 458 | genie.makeWish(ethel, wordToGet) 459 | mertzMatch = genie.getMatchingWishes(wordToGet) 460 | expect(mertzMatch[0]).toBe(ethel) 461 | expect(mertzMatch[1]).toBe(lucy) 462 | expect(mertzMatch[2]).toBe(fred) 463 | 464 | /* 465 | * More complicated here. A specific wish must be called 466 | * twice in a row for a specific magic word for it to 467 | * be lucy if that magic word already has a wish 468 | * associated with it. So lucy must be called with 469 | * 'mertz' twice in a row before she can take the 470 | * top spot. 471 | */ 472 | genie.makeWish(lucy, wordToGet) 473 | mertzMatch = genie.getMatchingWishes(wordToGet) 474 | expect(mertzMatch[0]).toBe(ethel) 475 | expect(mertzMatch[1]).toBe(lucy) 476 | expect(mertzMatch[2]).toBe(fred) 477 | 478 | /* 479 | * Lucy is now king of the hill. 480 | * Ethel is second, and fred isn't 481 | * even on the hill at all... 482 | */ 483 | genie.makeWish(lucy, wordToGet) 484 | mertzMatch = genie.getMatchingWishes(wordToGet) 485 | expect(mertzMatch[0]).toBe(lucy) 486 | expect(mertzMatch[1]).toBe(ethel) 487 | expect(mertzMatch[2]).toBe(fred) 488 | 489 | /* 490 | * De-registering lucy and making a 491 | * wish with fred will place ethel as 492 | * king of the hill and fred as second 493 | */ 494 | genie.deregisterWish(lucy) 495 | genie.makeWish(fred, wordToGet) 496 | mertzMatch = genie.getMatchingWishes(wordToGet) 497 | expect(mertzMatch).toHaveLength(3) 498 | expect(mertzMatch[0]).toBe(ethel) 499 | expect(mertzMatch[1]).toBe(fred) 500 | }) 501 | }) 502 | 503 | describe('#enabled', () => { 504 | describe('returnOnDisabled behavior when disabled', () => { 505 | beforeEach(prepForTest) 506 | test('should cause functions to return a reasonable empty return when enabled', () => { 507 | genie.enabled(false) 508 | expect(genie()).toEqual({}) 509 | expect(genie.getMatchingWishes()).toEqual([]) 510 | expect(genie.makeWish()).toEqual({}) 511 | expect(genie.options()).toEqual({}) 512 | expect(genie.deregisterWish()).toEqual({}) 513 | expect(genie.reset()).toEqual({}) 514 | expect(genie.context()).toEqual([]) 515 | expect(genie.revertContext()).toEqual([]) 516 | expect(genie.restoreContext()).toEqual([]) 517 | expect(genie.enabled()).toBe(false) 518 | expect(genie.returnOnDisabled()).toBe(true) 519 | }) 520 | test('should cause functions to return a reasonable empty return when disabled', () => { 521 | genie.returnOnDisabled(false) 522 | genie.enabled(false) 523 | 524 | expect(genie()).toBeNull() 525 | expect(genie.getMatchingWishes()).toBeNull() 526 | expect(genie.makeWish()).toBeNull() 527 | expect(genie.options()).toBeNull() 528 | expect(genie.deregisterWish()).toBeNull() 529 | expect(genie.reset()).toBeNull() 530 | expect(genie.context()).toBeNull() 531 | expect(genie.revertContext()).toBeNull() 532 | expect(genie.restoreContext()).toBeNull() 533 | expect(genie.enabled()).toBe(false) // Special case. Enabled always runs. 534 | expect(genie.returnOnDisabled()).toBeNull() 535 | }) 536 | }) 537 | }) 538 | 539 | // UTIL FUNCTIONS 540 | 541 | function fillInWish(defaults) { 542 | defaults = defaults || {} 543 | if (typeof defaults === 'string') { 544 | defaults = { 545 | magicWords: defaults, 546 | } 547 | } 548 | return createWishWithDefaults(defaults) 549 | } 550 | 551 | function createWishWithDefaults(defaults) { 552 | return { 553 | id: defaults.id, 554 | context: defaults.context, 555 | data: defaults.data, 556 | magicWords: defaults.magicWords || 'magicWords', 557 | action: defaults.action || function defaultAction() {}, 558 | } 559 | } 560 | 561 | function prepForTest(done) { 562 | genie.reset() 563 | genie.restoreContext() 564 | genie.enabled(true) 565 | genie.returnOnDisabled(true) 566 | 567 | if (done) { 568 | done() 569 | } 570 | } 571 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

GenieJS 🧞

3 | 4 | genie logo 5 | 6 |

A JavaScript library committed to improving user experience by empowering users to interact with web apps using the keyboard (better than cryptic shortcuts).

7 |

Genie |ˈjēnē| (noun): a spirit of Arabian folklore, as traditionally depicted imprisoned 8 | within a bottle or oil lamp, and capable of granting wishes when summoned.

9 |
10 | 11 | --- 12 | 13 | 14 | [![Build Status][build-badge]][build] 15 | [![Code Coverage][coverage-badge]][coverage] 16 | [![version][version-badge]][package] 17 | [![downloads][downloads-badge]][npmtrends] 18 | [![MIT License][license-badge]][license] 19 | 20 | 21 | [![All Contributors](https://img.shields.io/badge/all_contributors-4-orange.svg?style=flat-square)](#contributors) 22 | 23 | [![PRs Welcome][prs-badge]][prs] 24 | [![Code of Conduct][coc-badge]][coc] 25 | 26 | 27 | > Old links: 28 | > 29 | > - [Demo](http://kentcdodds.github.io/genie/) 30 | > ([Demo with React and Downshift](https://codesandbox.io/s/jrlkrxwgl)) 31 | > - [Tests](http://kentcdodds.github.io/genie/tests/testrunner.html) 32 | > - [Genie Workshop](http://kentcdodds.github.io/genie/workshop) - Terrific way 33 | > to learn how to use genie right in your browser. 34 | > - [API Docs](http://kentcdodds.github.io/genie/autodoc) 35 | > - [Chrome Extension](https://chrome.google.com/webstore/detail/genies-lamp/pimmaneflgfbknjkjdlnffagpgfeklko) 36 | 37 | ## The problem 38 | 39 | You want to enable users to power through your application with the keyboard, 40 | but you're limited on the kinds of reasonable keyboard shortcuts you can use. 41 | 42 | ## This solution 43 | 44 | GenieJS is a library to emulate the same kind of behavior seen in apps like 45 | [Alfred](http://www.alfredapp.com/). Essentially, you register actions 46 | associated with keywords. Then you can request the genie to perform that action 47 | based on the best keyword match for a given keyword. 48 | 49 | Over time, the genie will learn the actions more associated with specific 50 | keywords and those will be come first when a list of matching actions is 51 | requested. If that didn't make sense, don't worry, hopefully the tutorial, 52 | tests, and demo will help explain how it works. 53 | 54 | 55 | 56 | 57 | - [Vernacular](#vernacular) 58 | - [How to use it](#how-to-use-it) 59 | - [API](#api) 60 | - [Objects](#objects) 61 | - [Special Wish Actions](#special-wish-actions) 62 | - [Navigation](#navigation) 63 | - [About Matching Priority](#about-matching-priority) 64 | - [About Optimistic Anticipation](#about-optimistic-anticipation) 65 | - [About Context](#about-context) 66 | - [Path Context](#path-context) 67 | - [Enabling & Disabling](#enabling--disabling) 68 | - [Merging Wishes](#merging-wishes) 69 | - [Inspiration](#inspiration) 70 | - [Other Solutions](#other-solutions) 71 | - [Issues](#issues) 72 | - [🐛 Bugs](#-bugs) 73 | - [💡 Feature Requests](#-feature-requests) 74 | - [Contributors ✨](#contributors-) 75 | - [LICENSE](#license) 76 | 77 | 78 | 79 | ## Vernacular 80 | 81 | _Wish_: An object with an id, action, and magic words. 82 | 83 | _Action_: What to call when this wish is to be executed. 84 | 85 | _Magic Word_: Keywords for a wish used to match it with given magic words. 86 | 87 | _On Deck_: The second wish of preference for a certain magic word which will be 88 | King of the Hill if chosen again. 89 | 90 | _King of the Hill_: The wish which gets preference for a certain magic word 91 | until the On Deck wish is chosen again (it then becomes On Deck). 92 | 93 | ## How to use it 94 | 95 | If you're using [RequireJS](http://requirejs.org/) then you can simply 96 | `require('path/to/genie')`. Or you could simply include the regular script tag: 97 | 98 | ```html 99 | 100 | 101 | ``` 102 | 103 | `genie` is a function with a few useful functions as properties of `genie`. The 104 | flow of using GenieJS is simple: 105 | 106 | ```javascript 107 | /* Register wishes */ 108 | // One magic word 109 | var trashWish = genie({ 110 | magicWords: 'Take out the trash', 111 | action: function () { 112 | console.log('Yes! I love taking out the trash!') 113 | }, 114 | }) 115 | // Multiple magic words 116 | var vacuumWish = genie({ 117 | magicWords: ['Get dust out of the carpet', 'vacuum'], 118 | action: function () { 119 | console.log('Can NOT wait to get that dust out of that carpet!') 120 | }, 121 | }) 122 | 123 | /* Get wishes based on magic word matches */ 124 | genie.getMatchingWishes('vacuum') // returns [vacuumWish]; 125 | genie.getMatchingWishes('out') // returns [trashWish, vacuumWish]; 126 | 127 | // Make wish based on wish object or id of wish object 128 | genie.makeWish(trashWish.id) // logs: 'Yes! I love taking out the trash!' 129 | genie.makeWish(vacuumWish) // logs: 'Can NOT wait to get that dust out of that carpet!' 130 | ``` 131 | 132 | So far it doesn't look too magical, but the true magic comes in the form of 133 | genie giving preference to wishes that were recently chosen with a given 134 | keyword. To do this, you need to provide genie with a magic word to associate 135 | the wish with, like so: 136 | 137 | ```javascript 138 | genie.makeWish(vacuumWish, 'out') // logs as above 139 | genie.getMatchingWishes('out') // returns [vacuumWish, trashWish]; <-- Notice difference from above 140 | ``` 141 | 142 | As you'll notice, the order of the two wishes is changed because genie gave 143 | preference to the `vacuumWish` because the last time `makeWish` was called with 144 | the the `'out'` magic word, `vacuumWish` was the wish given. 145 | 146 | This behavior simulates apps such as [Alfred](http://www.alfredapp.com/) which 147 | is the goal of this library! 148 | 149 | ## API 150 | 151 | Genie is undergoing an overhaul on the API documentation using 152 | [autodocs](https://github.com/dtao/autodoc). It is still being worked on, but 153 | you can see that documentation 154 | [here](http://kentcdodds.github.io/genie/autodoc). 155 | 156 | Below you can see full documentation. It's just less enjoyable to read... 157 | 158 | ### Objects 159 | 160 | There are a few internal objects you may want to be aware of: 161 | 162 | ```javascript 163 | var wishObject = { 164 | id: 'string', 165 | data: { 166 | timesMade: { 167 | total: 0, 168 | magicWords: { 169 | 'Magic Word': 1, 170 | }, 171 | }, 172 | }, 173 | context: { 174 | all: ['string'], 175 | any: ['string'], 176 | none: ['string'], 177 | }, 178 | keywords: ['string'], 179 | action: function (wish, magicWord) {}, 180 | } 181 | 182 | var enteredMagicWords = { 183 | h: { 184 | wishes: ['wishid1', 'wishid2'], 185 | e: { 186 | l: { 187 | l: { 188 | o: { 189 | wishes: ['wishid3', 'wishid4'], 190 | }, 191 | }, 192 | p: { 193 | wishes: ['wishid5', 'wishid2'], 194 | }, 195 | }, 196 | }, 197 | }, 198 | } 199 | 200 | var pathContext = { 201 | paths: ['string'], 202 | regexes: [/regex/gi], 203 | contexts: 'The context to apply', 204 | } 205 | ``` 206 | 207 | You have the following api to use at your discretion: 208 | 209 | ```javascript 210 | /* 211 | * If no id is provided, one will be auto-generated via the previousId + 1 212 | * Genie adds a "timesMade" property to the "data" property 213 | * This is incremented every time the wish is made 214 | * (when the action is called) 215 | * You can also provide genie with an array of these objects 216 | * to register all of them at once. 217 | * Returns the wish object. 218 | */ 219 | genie({ 220 | id: string | optional, 221 | data: object | optional, 222 | context: string || [string] || { 223 | all: [string], 224 | any: [string], 225 | none: [string] 226 | } | optional, 227 | action: function | required, 228 | magicWords: string || [string] | required 229 | }); 230 | 231 | /* 232 | * Removes the wish from the registered wishes and the enteredMagicWords 233 | * Returns the deregisteredWish 234 | */ 235 | genie.deregisterWish(id [string || wishObject | required]); 236 | 237 | /* 238 | * Removes all wishes which have any of the given context(s). 239 | */ 240 | genie.deregisterWishesWithContext(context [string || array | required]); 241 | 242 | /* 243 | * calls options() with default options and returns the old options 244 | */ 245 | genie.reset(); 246 | 247 | /* 248 | * Returns an array of wishes which match in order: 249 | * 1. Most recently made wishes with the given magicWord 250 | * 2. Following the order of their initial registration 251 | */ 252 | genie.getMatchingWishes(magicWord [string | required]); 253 | 254 | /* 255 | * Replace genie's matching algorithm with your own. 256 | * Gives you three parameters: 257 | * - wishes (all genie wishes) 258 | * - magicWord (the magicWord to match) 259 | * - context (genie's current context) 260 | * - enteredMagicWords (genie's current enteredMagicWords object) 261 | */ 262 | genie.overrideMatchingAlgorithm(function(wishes, magicWord, context, enteredMagicWords) { 263 | // Your matching code. 264 | }); 265 | 266 | /* 267 | * Sets the matching algorithm back to genie's default algorithm. 268 | */ 269 | genie.restoreMatchingAlgorithm(); 270 | 271 | /* 272 | * Executes the given wish's action. 273 | * If a magicWord is provided, adds the given wish to the enteredMagicWords 274 | * to be given preferential treatment of order in the array returned 275 | * by the getMatchingWishes method. 276 | * Returns the wish object. 277 | */ 278 | genie.makeWish(id [string || wishObject | required], magicWord [string | optional]); 279 | 280 | /* 281 | * Returns wishes in the given context. This is what would be returned in the 282 | * case genie's context were the given context and you called 283 | * genie.getMatchingWishes(); (no args). 284 | * If no context is provided, all wishes are returned. 285 | * Returns wishes as an array 286 | */ 287 | genie.getWishesInContext(context [string || array | optional]); 288 | 289 | 290 | /* 291 | * Get wishes which have {type} of {context} in their context.{wishContextType} 292 | * type can be: 'any', 'all', or 'none' and refers to the context given 293 | * wishContextType can be a string or array with: 'any', 'all', or 'none' 294 | * and refers to the which of the wish's contexts to search. If not provided 295 | * genie combines all three. If a wish doesn't have a context object, but an 296 | * array or string instead, it treats that as if it were 'any' (as expected). 297 | * 298 | * This is a pretty complicated api, so here are a few examples 299 | * ex. getWishesWithContext('animal', 'none', 'all') 300 | * This would find all wishes which have no don't have the context 'animal' in their 'all' 301 | * context property 302 | * ex. getWishesWithContext('fred') 303 | * This would find all wishes which have the context 'fred' in any of their contexts 304 | * (including none). 305 | * ex. getWishesWithContext('tom', 'any', ['all', 'none']) 306 | * This would find all wishes which have the context 'tom' in their 'all' or 'none' contexts. 307 | * ex. getWishesWithContext(['tom', 'fred'], 'all') 308 | * This would find all wishes which have both the context 'tom' and 'fred' in any of their contexts. 309 | * ex. getWishesWithContext(['orange', 'apple'], 'none', 'any') 310 | * This would find all wishes which do not have 'orange' or 'apple' in their 'any' context. 311 | */ 312 | genie.getWishesWithContext(context [string || array | required], type [string | optional | 'any'], wishContextTypes) { 313 | 314 | /* 315 | * Get a specific wish by an id. 316 | * If the id is an array, returns an array 317 | * of wishes with the same order as the 318 | * given array. 319 | * Note: If the id does not correspond to 320 | * a registered wish, it will be undefined 321 | */ 322 | genie.getWish(id [string || array | required]); 323 | 324 | /* 325 | * Allows you to set the attributes of genie and returns the current genie options. 326 | * 1. wishes: All wishes (wishObject described above) currently registered 327 | * 2. noWishMerge: Instead of adding wishes, replace the current list of wishes with the given list. 328 | * More about this below... 329 | * 3. previousId: The number used to auto-generate wish Ids if an id is not 330 | * provided when a wish is registered. 331 | * 4. enteredMagicWords: All magicWords which have been associated with wishes 332 | * to give preferential treatment in the order of wishes returned by getMatchingWishes 333 | * 5. context: The current context of the genie. See below about how context affects wishes 334 | * 6. enabled: Control whether genie's functions will actually run 335 | * 7. returnOnDisabled: If enabled is set to false and this is true, will return an empty 336 | * object/array/string to prevent the need to do null/undefined checking wherever genie 337 | * is used. 338 | */ 339 | genie.options({ 340 | wishes: object | optional, 341 | noWishMerge: boolean | optional, 342 | previousId: number | optional, 343 | enteredMagicWords: object | optional, 344 | context: string | optional, 345 | enabled: boolean | optional, 346 | returnOnDisabled: boolean | optional 347 | }); 348 | 349 | /* 350 | * Merges the given wishes with existing wishes. (See Merging Wishes below) 351 | */ 352 | genie.mergeWishes(wishes); 353 | 354 | /* 355 | * Sets and returns the current context to newContext if provided 356 | * Also sets an internal variable: _previousContext for the revertContext function 357 | */ 358 | genie.context(newContext [string || array | optional]); 359 | 360 | /* 361 | * Adds the context(s) to genie's current context 362 | */ 363 | genie.addContext(newContext [string || array | optional]); 364 | 365 | /* 366 | * Removes the context(s) to genie's current context 367 | */ 368 | genie.removeContext(newContext [string || array | optional]); 369 | 370 | /* 371 | * Sets and returns the current context to the default context: ['universe'] 372 | */ 373 | genie.restoreContext(); 374 | 375 | /* Sets and returns the current context to the previous context 376 | * The previous context is updated when context, addContext, 377 | * removeContext, restoreContext, and revertContext are called. 378 | */ 379 | genie.revertContext(); 380 | 381 | /* See more about how this works below in the context section 382 | * This will update the current context with the given path 383 | * noDeregister is used to prevent this from deregistering 384 | * wishes which are not in the path's context. 385 | */ 386 | genie.updatePathContext(path, noDeregister); 387 | 388 | /* 389 | * Adds a path context (or array of them) to genie's _pathContext array 390 | */ 391 | genie.addPathContext(pathContext); 392 | 393 | /* 394 | * Removes a path context (or array of them) from genie's _pathContext array 395 | */ 396 | genie.removePathContext(pathContext); 397 | 398 | /* 399 | * Sets and returns the enabled state 400 | */ 401 | genie.enabled(boolean | optional); 402 | 403 | /* 404 | * Sets and returns the returnOnDisabled state 405 | */ 406 | genie.returnOnDisabled(boolean | optional); 407 | ``` 408 | 409 | ## Special Wish Actions 410 | 411 | There are some actions that are common use cases, so genie helps with these 412 | (currently only one special wish action): 413 | 414 | ### Navigation 415 | 416 | You for the action of the wish you can provide either a string (URL) or an 417 | object with a destination property (URL). If the action is an object this gives 418 | you a few options: 419 | 420 | - openNewTab - If truthy, this will open the URL using '\_blank'. Otherwise 421 | opens in the current window. 422 | - That's all for now... any [other](https://github.com/kentcdodds/genie/pulls) 423 | [ideas?](https://github.com/kentcdodds/genie/issues) 424 | 425 | ## About Matching Priority 426 | 427 | The wishes returned from `getMatchingWishes` are ordered with the following 428 | priority 429 | 430 | 1. King of the Hill for the given `magicWords` (genie optimistically anticipates 431 | this as well) 432 | 2. On Deck for the given `magicWords` (also optimistically anticipated) 433 | 3. If the given magic word is equal to any magic words of a wish 434 | 4. If the given magic word is the start to any magic word of a wish (i.e. 'he' 435 | in 'hello'); 436 | 5. If the given magic word is the start to any word in a magic word (i.e. 'wo' 437 | in 'hello world'); 438 | 6. If the given magic word is contained in any magic words of a wish 439 | 7. If the given magic word is an acronym of any magic words of a wish 440 | 8. If the given magic word matches the order of characters in any magic words of 441 | a wish. 442 | 443 | Just trust the genie. He knows best. And if you think otherwise, 444 | [let me know](https://github.com/kentcdodds/genie/issues) or (even better) 445 | [contribute](https://github.com/kentcdodds/genie/pulls) :) 446 | 447 | ## About Optimistic Anticipation 448 | 449 | Genie keeps track of which wishes were executed with which magic words so it 450 | knows which wish is "King of the Hill" and "On Deck." But it's not a simple 451 | string-to-string comparison. If I have a wish with the magic words of 452 | `'Do laundry'` and another with `'Laundry stinks`' then make the `'Do laundry`' 453 | wish with `'laundry`', I would have to type the entire word `'laundry`' before 454 | `'Do laundry'` came up to the top. So genie will anticipate that what I'm typing 455 | to be `'laundry'` until I type something that renders this impossible (like if I 456 | type `'lan'`, it will anticipate `'laundry`' until I type the `'n'` and keep 457 | `'Do laundry'` at the top until I do). 458 | 459 | This is possible because the structure of object that genie uses to keep track 460 | of entered magic words: 461 | 462 | ```json 463 | "enteredMagicWords": { 464 | "w": { 465 | "i": { 466 | "s": { 467 | "h": { 468 | "wishes": [ 469 | "g-4", 470 | "g-3" 471 | ] 472 | } 473 | }, 474 | "wishes": [ 475 | "g-5" 476 | ] 477 | } 478 | } 479 | } 480 | ``` 481 | 482 | If you're curious, look in the code :-) 483 | 484 | ## About Context 485 | 486 | Genie has a concept of context that allows you to switch between sets of wishes 487 | easily. It's a toss up between context and the matching algorithm on which is 488 | more complex but hopefully I can explain it well enough for you! Each wish is 489 | given the default context which is `universe` unless one is provided when it is 490 | registered. Wishes will only behave normally in `getMatchingWishes` and 491 | `makeWish` when they are in context. 492 | 493 | The easiest way to think of a wish context is that it is structured like so: 494 | 495 | ```javascript 496 | { 497 | all: ['context1', 'context2'], 498 | any: ['context3', 'context4'], 499 | none: ['context5', 'context6'] 500 | } 501 | ``` 502 | 503 | If you set a wish's context to a string or array of strings, it behaves like so: 504 | 505 | ```javascript 506 | // what you set: 507 | var wish = genie({ 508 | context: ['context1', 'context2'], 509 | }) 510 | 511 | console.log(wish.context) // logs {any: ['context', 'context2']} 512 | ``` 513 | 514 | There are a few ways for a wish to **definitely** be in context: 515 | 516 | 1. Genie's current context is the default context 517 | 2. The wish's context is the default context (does not apply if it simply 518 | contains the default context) 519 | 3. The wish's context is equal to the current context 520 | 521 | If none of these are true, then these things must be true for the wish to be in 522 | context: 523 | 524 | 1. Genie's context does **not** contain any of the wish's `context.none` 525 | contexts if it exists. 526 | 2. Genie's context contains **at least one** of the wish's `context.any` 527 | contexts if it exists. 528 | 3. Genie's context contains **all** of the wish's `context.all` contexts if it 529 | exists. 530 | 531 | Checkout [the tests](src/__tests__/index.js) for #context to see more how this 532 | works. Here's a simple demonstration: 533 | 534 | ```javascript 535 | // Simple stuff 536 | 537 | // Before setting context, genie.context is default 538 | wish0.context // returns the default context 539 | wish1.context = 'context1' 540 | wish2.context = ['context1', 'context2'] 541 | wish3.context = 'context3' 542 | 543 | genie.getMatchingWishes() // returns [wish0, wish1, wish2, wish3] 544 | 545 | genie.context('context1') 546 | genie.getMatchingWishes() // returns [wish0, wish1, wish2] 547 | 548 | genie.context('context2') 549 | genie.getMatchingWishes() // returns [wish0, wish2] 550 | 551 | genie.context('context3') 552 | genie.getMatchingWishes() // returns [wish0, wish3] 553 | 554 | genie.context(['context1', 'context2']) 555 | genie.getMatchingWishes() // returns [wish0, wish1, wish2] 556 | 557 | genie.context(['context1', 'context3']) 558 | genie.getMatchingWishes() // returns [wish0, wish1, wish2, wish3] 559 | ``` 560 | 561 | ```javascript 562 | // Complex stuff 563 | 564 | genie.context = ['context1', 'context2', 'context3', 'context4'] 565 | 566 | wish0.context // returns the default context 567 | wish1.context = { 568 | any: ['context2', 'context5'], 569 | } 570 | wish2.context = { 571 | none: ['context3', 'context5'], 572 | } 573 | wish3.context = { 574 | all: ['context1', 'context5'], 575 | } 576 | 577 | genie.getMatchingWishes() // returns [wish0, wish1] 578 | 579 | genie.context(['context5', 'context1']) 580 | genie.getMatchingWishes() // returns [wish0, wish1, wish3] 581 | 582 | genie.context(['context2']) 583 | genie.getMatchingWishes() // returns [wish0, wish1, wish2] 584 | 585 | genie.restoreContext() // resets genie's context to default 586 | genie.getMatchingWishes() // returns [wish0, wish2] 587 | ``` 588 | 589 | ### Path Context 590 | 591 | A big use case for context is to have a url path (or route) represent the 592 | context for genie. For example, if you have an email app, you can have the 593 | `/index` and the `/message/:id` routes which would have different contexts. 594 | Instead of managing this yourself, genie can help you a little. Genie will not 595 | watch the URL for you, so you have to do that yourself. This is by design. At 596 | any time, you can call `genie.updatePathContext(window.location.pathname)` and 597 | genie will update the context based on an internal variable called 598 | `_pathContexts`. You have control over what's in this array using the 599 | `genie.addPathContext(pathContext)` and the 600 | `genie.removePathContext(pathContext)` methods. A `pathContext` object looks 601 | like this: 602 | 603 | ```javascript 604 | { 605 | paths: string || array of strings | optional (either this or regexes), 606 | regexes: regex || array of regexes | optional (either this or paths), 607 | contexts: string || array of strings | required 608 | } 609 | ``` 610 | 611 | The `contexts` variable is special and is associated with the regexes variable. 612 | The easiest way to describe this is via an example: 613 | 614 | If I have a pathContext object like this: 615 | 616 | ```javascript 617 | { 618 | regexes: [ 619 | /\/pizza\/(-\d+|\d+)/gi, 620 | /\/pizza\/(pepperoni)/gi 621 | ], 622 | contexts: 'a-page-{{1}}' 623 | } 624 | ``` 625 | 626 | Then, when I call `genie.updatePathContext('/pizza/1234')` it will match this 627 | pathContext and genie will automatically change `a-page-{{1}}` to `a-page-1234`. 628 | 629 | The `1` in `a-page-{{1}}` represents the group that is matched on the path in 630 | the regex. It will replace the digit in `{{\d}}` with the group that's matched 631 | (Note: in true JavaScript form, group 0 represents the entire match string, 632 | hence, 1 is the first group in parentheses). 633 | 634 | ## Enabling & Disabling 635 | 636 | To give you a little more control, you can enable and disable genie globally. 637 | All genie functions go through a check to make sure genie is enabled. If it is 638 | enabled, everything works as expected. If it is disabled, then genie will return 639 | an empty object/array/string depending on what the function you're calling is 640 | expecting. This behavior is to prevent the need to do null/undefined checking 641 | everywhere you use `genie` and can be disabled as well via the returnOnDisabled 642 | function. 643 | 644 | ## Merging Wishes 645 | 646 | To persist the user's experience, you may want to store the result of 647 | `genie.options()` in `localStorage` or even a database associated with the user. 648 | Then after you have registered all the wishes for the user you load the options 649 | by calling `genie.options({wishes: usersOptions})`. The problem with this is 650 | that `usersOptions` wont have the actions for wishes, so this would overwrite 651 | the wishes with a bunch that don't have actions. 652 | 653 | To prevent this, by default when you call `genie.options` genie will merge the 654 | wishes. So any new wishes provided will either overwrite wishes with the same 655 | ID, but preserve the action of the old version if the new version doesn't have 656 | an action already. It will also preserve wishes which existed before and don't 657 | have matching ids. 658 | 659 | To completely overwrite the existing wishes, simply pass in `noWishMerge` along 660 | with the wishes. 661 | 662 | Note: Genie provides direct access to the `mergeWishes` function as well. 663 | 664 | ## Inspiration 665 | 666 | I built this after I was trying to add keyboard shortcuts to an application at 667 | work and ran out of letters that made sense. I was heavily inspired by 668 | [Alfred](http://www.alfredapp.com/). 669 | 670 | ## Other Solutions 671 | 672 | Similar solutions we know of: 673 | 674 | - https://github.com/mixmaxhq/frecency 675 | 676 | If you are aware of other solutions please [make a pull request][prs] and add it 677 | here! 678 | 679 | ## Issues 680 | 681 | _Looking to contribute? Look for the [Good First Issue][good-first-issue] 682 | label._ 683 | 684 | ### 🐛 Bugs 685 | 686 | Please file an issue for bugs, missing documentation, or unexpected behavior. 687 | 688 | [**See Bugs**][bugs] 689 | 690 | ### 💡 Feature Requests 691 | 692 | Please file an issue to suggest new features. Vote on feature requests by adding 693 | a 👍. This helps maintainers prioritize what to work on. 694 | 695 | [**See Feature Requests**][requests] 696 | 697 | ## Contributors ✨ 698 | 699 | Thanks goes to these people ([emoji key][emojis]): 700 | 701 | 702 | 703 | 704 | 705 | 706 | 707 | 708 | 709 | 710 |
Kent C. Dodds
Kent C. Dodds

💻 📖 🚇 ⚠️ 📢
swyx
swyx

📖
Justin Dorfman
Justin Dorfman

🔍
Michaël De Boey
Michaël De Boey

💻
711 | 712 | 713 | 714 | This project follows the [all-contributors][all-contributors] specification. 715 | Contributions of any kind welcome! 716 | 717 | ## LICENSE 718 | 719 | MIT 720 | 721 | 722 | [npm]: https://www.npmjs.com 723 | [node]: https://nodejs.org 724 | [build-badge]: https://img.shields.io/travis/kentcdodds/genie.svg?style=flat-square 725 | [build]: https://travis-ci.org/kentcdodds/genie 726 | [coverage-badge]: https://img.shields.io/codecov/c/github/kentcdodds/genie.svg?style=flat-square 727 | [coverage]: https://codecov.io/github/kentcdodds/genie 728 | [version-badge]: https://img.shields.io/npm/v/genie.svg?style=flat-square 729 | [package]: https://www.npmjs.com/package/genie 730 | [downloads-badge]: https://img.shields.io/npm/dm/genie.svg?style=flat-square 731 | [npm-trends]: https://www.npmtrends.com/genie 732 | [license-badge]: https://img.shields.io/npm/l/genie.svg?style=flat-square 733 | [license]: https://github.com/kentcdodds/genie/blob/master/other/LICENSE 734 | [prs-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square 735 | [prs]: http://makeapullrequest.com 736 | [coc-badge]: https://img.shields.io/badge/code%20of-conduct-ff69b4.svg?style=flat-square 737 | [coc]: https://github.com/kentcdodds/genie/blob/master/other/CODE_OF_CONDUCT.md 738 | [emojis]: https://github.com/kentcdodds/all-contributors#emoji-key 739 | [all-contributors]: https://github.com/kentcdodds/all-contributors 740 | 741 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @name genie 3 | * @fileOverview A JavaScript library committed to improving 4 | * user experience by empowering users to interact with web 5 | * apps using the keyboard (better than cryptic shortcuts). 6 | * 7 | * http://www.github.com/kentcdodds/genie 8 | * 9 | * **Note:** This documentation was generated with 10 | * [autodocs](https://github.com/dtao/autodoc). Both autodocs and 11 | * this documentation are still under development. Any issues 12 | * you find about content can be assigned to 13 | * [genie issues](http://www.github.com/kentcdodds/genie) 14 | * any docs related issues can be assigned to 15 | * [autodocs issues](https://github.com/dtao/autodoc/issues). 16 | * 17 | * @license genie.js may be freely distributed under the MIT license. 18 | * @copyright (c) 2017 Kent C. Dodds 19 | * @author Kent C. Dodds 20 | */ 21 | 22 | const genie = _passThrough(registerWish, {}) 23 | let _wishes = [] 24 | let _previousId = 0 25 | let _enteredMagicWords = {} 26 | const _defaultContext = ['universe'] 27 | let _originalMatchingAlgorithm = function _originalMatchingAlgorithm() {} 28 | let _context = _defaultContext 29 | const _pathContexts = [] 30 | let _previousContext = _defaultContext 31 | let _enabled = true 32 | let _returnOnDisabled = true 33 | const _contextRegex = /\{\{(\d+)\}\}/g 34 | 35 | /** 36 | * The _matchRankMap 37 | * @typedef {object} _matchRankMap 38 | * @property {number} equals - 5 39 | * @property {number} startsWith - 4 40 | * @property {number} wordStartsWith - 3 41 | * @property {number} contains - 2 42 | * @property {number} acronym - 1 43 | * @property {number} matches - 0 44 | * @property {number} noMatch - -1 45 | * @readonly 46 | * @private 47 | */ 48 | const _matchRankMap = { 49 | equals: 5, 50 | startsWith: 4, 51 | wordStartsWith: 3, 52 | contains: 2, 53 | acronym: 1, 54 | matches: 0, 55 | noMatch: -1, 56 | } 57 | 58 | /** 59 | * The context of a wish 60 | * @typedef {object} context 61 | * @property {Array.} any 62 | * @property {Array.} all 63 | * @property {Array.} none 64 | * @public 65 | */ 66 | 67 | /** 68 | * Wish action callback definition 69 | * @callback WishAction 70 | * @param {wish} wish 71 | * @param {string} magicWord 72 | * @public 73 | */ 74 | 75 | /** 76 | * The wish object 77 | * @typedef {object} wish 78 | * @property {string} id - Unique identifier for the wish. 79 | * @property {context} context - The context of the wish. Can be given as a 80 | * string or array. In which case it is assigned to the wish's context.any property. 81 | * @property {{timesMade: {total: number, magicWords: {string}}}} data - Any 82 | * data you wish to associate with the wish. 83 | * Genie adds a 'timesMade' property with total and magicWords 84 | * properties to keep track of how often a wish is made with a 85 | * given magicWord. 86 | * @property {Array.} magicWords - Used to match this wish on genie.getMatchingWishes 87 | * @property {WishAction} action - The action to be performed when genie.makeWish is 88 | * called with this wish. 89 | * @public 90 | */ 91 | 92 | /** 93 | * Wish Ids 94 | * @typedef {string|Array.} wishIds 95 | * @public 96 | */ 97 | 98 | /** 99 | * A path context 100 | * @typedef {object} PathContext 101 | * @property {Array.} regexes 102 | * @property {Array.} paths 103 | * @property {Array.} contexts 104 | * @public 105 | */ 106 | 107 | /** 108 | * Letter in an entered magic words 109 | * @typedef {object} EnteredMagicLetter 110 | * @property {EnteredMagicLetter} 111 | * @property {Array.} wishIds 112 | */ 113 | 114 | /** 115 | * The object used for genie's matching algorithm. 116 | * @typedef {object} EnteredMagicWords 117 | * @property {EnteredMagicLetter} 118 | * @property {Array.} wishIds 119 | */ 120 | 121 | /** 122 | * **Note:** This is actually assigned to the `genie` variable, so 123 | * it is called like so: `genie({wish: object});` 124 | * 125 | * Creates and registers a new wish with the given pseudo wish(es). 126 | * @param {object|Array.} wish pseudo wish(es) 127 | * @returns {wish|Array.} The registered wish or array of wishes. 128 | * @public 129 | */ 130 | function registerWish(wish) { 131 | if (_isArray(wish)) { 132 | const wishesRegistered = [] 133 | _each(wish, w => { 134 | wishesRegistered.push(registerWish(w)) 135 | }) 136 | return wishesRegistered 137 | } else { 138 | const newWish = _createWish(wish) 139 | const existingWishIndex = _getWishIndexById(newWish.id) 140 | if (existingWishIndex < 0) { 141 | _wishes.push(newWish) 142 | } else { 143 | _wishes[existingWishIndex] = newWish 144 | } 145 | 146 | return newWish 147 | } 148 | } 149 | 150 | /** 151 | * Creates a new wish object. 152 | * @param {object} wish 153 | * @returns {wish} New wish object 154 | * @private 155 | */ 156 | function _createWish(wish) { 157 | const id = wish.id || `g-${_previousId++}` 158 | const newWish = { 159 | id, 160 | context: _createContext(wish.context), 161 | data: wish.data || {}, 162 | magicWords: _arrayify(wish.magicWords), 163 | action: _createAction(wish.action), 164 | } 165 | newWish.data.timesMade = { 166 | total: 0, 167 | magicWords: {}, 168 | } 169 | return newWish 170 | } 171 | 172 | /** 173 | * Transforms the given context to a context object. 174 | * @param {object|string|Array.} context 175 | * @returns {context} 176 | * @private 177 | */ 178 | function _createContext(context) { 179 | let newContext = context || _defaultContext 180 | if (_isString(newContext) || _isArray(newContext)) { 181 | newContext = { 182 | any: _arrayify(newContext), 183 | } 184 | } else { 185 | newContext = _arrayizeContext(context) 186 | } 187 | return newContext 188 | } 189 | 190 | /** 191 | * Makes all the context properties arrays. 192 | * @param {object|string|Array.} context 193 | * @returns {context} 194 | * @private 195 | */ 196 | function _arrayizeContext(context) { 197 | function checkAndAdd(type) { 198 | if (context[type]) { 199 | context[type] = _arrayify(context[type]) 200 | } 201 | } 202 | checkAndAdd('all') 203 | checkAndAdd('any') 204 | checkAndAdd('none') 205 | return context 206 | } 207 | 208 | /** 209 | * Transforms the given action into an action 210 | * callback. 211 | * @param {Function|object|string} action 212 | * @returns {WishAction} 213 | * @private 214 | */ 215 | function _createAction(action) { 216 | if (_isString(action)) { 217 | action = { 218 | destination: action, 219 | } 220 | } 221 | if (_isObject(action)) { 222 | action = (function () { 223 | const openNewTab = action.openNewTab 224 | const destination = action.destination 225 | return function () { 226 | if (openNewTab) { 227 | window.open(destination, '_blank') 228 | } else { 229 | window.location.href = destination 230 | } 231 | } 232 | })() 233 | } 234 | 235 | return action 236 | } 237 | 238 | /** 239 | * Deregisters the given wish. Removes it from the registry 240 | * and from the _enteredMagicWords map. 241 | * This will delete an _enteredMagicWords listing if this 242 | * is the only wish in the list. 243 | * @param {object|string} wish The wish to deregister 244 | * @returns {wish} The deregistered wish 245 | * @public 246 | */ 247 | function deregisterWish(wish) { 248 | let indexOfWish = _wishes.indexOf(wish) 249 | if (!indexOfWish) { 250 | _each(_wishes, (aWish, index) => { 251 | // the given parameter could be an id. 252 | if (wish === aWish.id || wish.id === aWish.id) { 253 | indexOfWish = index 254 | wish = aWish 255 | return false 256 | } 257 | }) 258 | } 259 | 260 | _wishes.splice(indexOfWish, 1) 261 | _removeWishIdFromEnteredMagicWords(wish.id) 262 | return wish 263 | } 264 | 265 | /** 266 | * Iterates through _enteredMagicWords and removes 267 | * all instances of this id. If this leaves the letter 268 | * empty it removes the letter. 269 | * @param {string} id 270 | * @private 271 | */ 272 | function _removeWishIdFromEnteredMagicWords(id) { 273 | function removeIdFromWishes(charObj, parent, charObjName) { 274 | _each(charObj, (childProp, propName) => { 275 | if (propName === 'wishes') { 276 | const index = childProp.indexOf(id) 277 | if (index !== -1) { 278 | childProp.splice(index, 1) 279 | } 280 | if (!childProp.length) { 281 | delete charObj[propName] 282 | } 283 | } else { 284 | removeIdFromWishes(childProp, charObj, propName) 285 | } 286 | }) 287 | const keepCharObj = _getPropFromPosterity(charObj, 'wishes').length > 0 288 | if (!keepCharObj && parent && charObjName) { 289 | delete parent[charObjName] 290 | } 291 | } 292 | removeIdFromWishes(_enteredMagicWords) 293 | } 294 | 295 | /** 296 | * Convenience method which calls getWishesWithContext and passes the arguments 297 | * which are passed to this function. Then deregisters each of these. 298 | * @param {string|Array.} context The context the lookup 299 | * @param {string} [type='any'] 'all', 'any', or 'none' referring to the 300 | * context parameter 301 | * @param {string|Array.} [wishContextTypes=['any', 'all', 'none']] 302 | * The type of wish contexts to compare 303 | * @returns {Array.} the deregistered wishes. 304 | * @public 305 | */ 306 | function deregisterWishesWithContext(context, type, wishContextTypes) { 307 | const deregisteredWishes = getWishesWithContext( 308 | context, 309 | type, 310 | wishContextTypes, 311 | ) 312 | _each(deregisteredWishes, (wish, i) => { 313 | deregisteredWishes[i] = deregisterWish(wish) 314 | }) 315 | return deregisteredWishes 316 | } 317 | 318 | /** 319 | * Get wishes in a specific context. If no context 320 | * is provided, all wishes are returned. 321 | * Think of this as, if genie were in the given 322 | * context, what would be returned if I called 323 | * genie.getMatchingWishes()? 324 | * @param {string|Array.} context The context(s) 325 | * to check wishes against. 326 | * @returns {Array.} The wish's which are in 327 | * the given context. 328 | * @public 329 | */ 330 | function getWishesInContext(context) { 331 | context = context || _defaultContext 332 | const wishesInContext = [] 333 | _each(_wishes, wish => { 334 | if ( 335 | _contextIsDefault(context) || 336 | _contextIsDefault(wish.context) || 337 | _wishInThisContext(wish, context) 338 | ) { 339 | wishesInContext.push(wish) 340 | } 341 | }) 342 | return wishesInContext 343 | } 344 | 345 | /** 346 | * Get wishes which have {type} of {context} in their context.{wishContextType} 347 | * @param {string|Array.} context The context the lookup 348 | * @param {string} [type='any'] 'all', 'any', or 'none' referring to the 349 | * context parameter 350 | * @param {string|Array.} [wishContextTypes=['any', 'all', 'none']] 351 | * The type of wish contexts to compare 352 | * @returns {Array.} 353 | * @public 354 | */ 355 | function getWishesWithContext(context, type, wishContextTypes) { 356 | const wishesWithContext = [] 357 | type = type || 'any' 358 | _each(_wishes, wish => { 359 | const wishContext = _getWishContext(wish, wishContextTypes) 360 | 361 | if ( 362 | !_isEmpty(wishContext) && 363 | ((type === 'all' && _arrayContainsAll(wishContext, context)) || 364 | (type === 'none' && _arrayContainsNone(wishContext, context)) || 365 | (type === 'any' && _arrayContainsAny(wishContext, context))) 366 | ) { 367 | wishesWithContext.push(wish) 368 | } 369 | }) 370 | return wishesWithContext 371 | } 372 | 373 | /** 374 | * Gets the wish context based on the wishContextTypes. 375 | * @param {object} wish 376 | * @param {string|Array.} wishContextTypes 377 | * @returns {Array.} 378 | * @private 379 | */ 380 | function _getWishContext(wish, wishContextTypes) { 381 | let wishContext = [] 382 | wishContextTypes = wishContextTypes || ['all', 'any', 'none'] 383 | 384 | wishContextTypes = _arrayify(wishContextTypes) 385 | _each(wishContextTypes, wishContextType => { 386 | if (wish.context[wishContextType]) { 387 | wishContext = wishContext.concat(wish.context[wishContextType]) 388 | } 389 | }) 390 | 391 | return wishContext 392 | } 393 | 394 | /** 395 | * Get a specific wish by an id. 396 | * If the id is an array, returns an array 397 | * of wishes with the same order as the 398 | * given array. 399 | * Note: If the id does not correspond to 400 | * a registered wish, it will be undefined 401 | * @param {wishIds} id The id(s) to get wishes for 402 | * @returns {wish|Array.|null} The wish object(s) 403 | * @public 404 | */ 405 | function getWish(id) { 406 | if (_isArray(id)) { 407 | const wishes = [] 408 | _each(_getWishIndexById(id), index => { 409 | wishes.push(_wishes[index]) 410 | }) 411 | return wishes 412 | } else { 413 | const index = _getWishIndexById(id) 414 | if (index > -1) { 415 | return _wishes[index] 416 | } else { 417 | return null 418 | } 419 | } 420 | } 421 | 422 | /** 423 | * Gets a wish from the _wishes array by its ID 424 | * @param {wishIds} id 425 | * @returns {wish|Array.} 426 | * @private 427 | */ 428 | function _getWishIndexById(id) { 429 | let wishIndex = -1 430 | if (_isArray(id)) { 431 | const wishIndexes = [] 432 | _each(id, wishId => { 433 | wishIndexes.push(_getWishIndexById(wishId)) 434 | }) 435 | return wishIndexes 436 | } else { 437 | _each(_wishes, (aWish, index) => { 438 | if (aWish.id === id) { 439 | wishIndex = index 440 | return false 441 | } 442 | }) 443 | return wishIndex 444 | } 445 | } 446 | 447 | /** 448 | * Sets genie's options to the default options 449 | * @returns {GenieOptions} Genie's old options 450 | * @public 451 | */ 452 | function reset() { 453 | const oldOptions = options() 454 | options({ 455 | wishes: [], 456 | noWishMerge: true, 457 | previousId: 0, 458 | enteredMagicWords: {}, 459 | context: _defaultContext, 460 | previousContext: _defaultContext, 461 | enabled: true, 462 | }) 463 | return oldOptions 464 | } 465 | 466 | /** 467 | * Uses the given magic word to return an intelligently sorted 468 | * list of wishes which are in context and match the magic word 469 | * (based on their own magic words and genie's enteredMagicWords) 470 | * @param {?string} [magicWord=''] The magic word to match against 471 | * @returns {Array.} wishes The matching wishes. 472 | * @public 473 | */ 474 | function getMatchingWishes(magicWord) { 475 | magicWord = (_isNullOrUndefined(magicWord) 476 | ? '' 477 | : `${magicWord}` 478 | ).toLowerCase() 479 | const allWishIds = _getWishIdsInEnteredMagicWords(magicWord) 480 | const allWishes = getWish(allWishIds) 481 | const matchingWishes = _filterInContextWishes(allWishes) 482 | 483 | const otherMatchingWishIds = _sortWishesByMatchingPriority( 484 | _wishes, 485 | allWishIds, 486 | magicWord, 487 | ) 488 | const otherWishes = getWish(otherMatchingWishIds) 489 | return matchingWishes.concat(otherWishes) 490 | } 491 | 492 | /** 493 | * Climbs down the chain with the _enteredMagicWords object to find 494 | * where the word ends and then gets the 'wish' property from 495 | * the posterity at that point in the _enteredMagicWords object. 496 | * @param {string} word 497 | * @returns {Array.} 498 | * @private 499 | */ 500 | function _getWishIdsInEnteredMagicWords(word) { 501 | const startingCharWishesObj = _climbDownChain( 502 | _enteredMagicWords, 503 | word.split(''), 504 | ) 505 | if (startingCharWishesObj) { 506 | return _getPropFromPosterity(startingCharWishesObj, 'wishes', true) 507 | } else { 508 | return [] 509 | } 510 | } 511 | 512 | /** 513 | * Returns a filtered array of the wishes which are in context. 514 | * @param {Array.} wishes - the wishes to filter 515 | * @returns {Array.} wishes - the wishes which are in context 516 | * @private 517 | */ 518 | function _filterInContextWishes(wishes) { 519 | const inContextWishes = [] 520 | _each(wishes, wish => { 521 | if (wish && _wishInContext(wish)) { 522 | inContextWishes.push(wish) 523 | } 524 | }) 525 | return inContextWishes 526 | } 527 | 528 | /** 529 | * Climbs down an object's properties based on the given 530 | * array of properties. (obj[props[0]][props[1]][props[2]] etc.) 531 | * @param {*} obj - the object to climb down. 532 | * @param {Array.} props - the ordered list of properties 533 | * to climb down with 534 | * @returns {*} 535 | * @private 536 | * @examples 537 | * _climbDownChain({a: { b: {c: {d: 'hello'}}}}, ['a', 'b', 'c', 'd']) // => 'hello' 538 | */ 539 | function _climbDownChain(obj, props) { 540 | let finalObj = obj 541 | props = _arrayify(props) 542 | const madeItAllTheWay = _each(props, prop => { 543 | if (finalObj.hasOwnProperty(prop)) { 544 | finalObj = finalObj[prop] 545 | return true 546 | } else { 547 | return false 548 | } 549 | }) 550 | if (madeItAllTheWay) { 551 | return finalObj 552 | } else { 553 | return null 554 | } 555 | } 556 | 557 | /** 558 | * Iterates through all child properties of the given object 559 | * and if it has the given property, it will add that property 560 | * to the array that's returned at the end. 561 | * @param {*} objToStartWith 562 | * @param {string} prop 563 | * @param {boolean} [unique=false] 564 | * @returns {Array.} 565 | * @private 566 | * @examples 567 | * _getPropFromPosterity({a: {p: 1, b: {p: 2}}}, 'p') // => [1,2] 568 | */ 569 | function _getPropFromPosterity(objToStartWith, prop, unique) { 570 | let values = [] 571 | function loadValues(obj) { 572 | if (obj[prop]) { 573 | const propsToAdd = _arrayify(obj[prop]) 574 | _each(propsToAdd, propToAdd => { 575 | if (!unique || !_contains(values, propToAdd)) { 576 | values.push(propToAdd) 577 | } 578 | }) 579 | } 580 | _each(obj, (oProp, oPropName) => { 581 | if (oPropName !== prop && !_isPrimitive(oProp)) { 582 | values = values.concat(loadValues(oProp)) 583 | } 584 | }) 585 | } 586 | loadValues(objToStartWith) 587 | return values 588 | } 589 | 590 | /** 591 | * A matchPriority for a wish 592 | * @typedef {object} MatchPriority 593 | * @property {number} matchType - based on _matchRankMap 594 | * @property {number} magicWordIndex - the index of the magic word 595 | * in the wish's array of magic words. 596 | */ 597 | 598 | /** 599 | * Takes the given wishes and sorts them by how well they match the givenMagicWord. 600 | * The wish must be in context, and they follow the order defined in the 601 | * {@link "#_matchRankMap"} 602 | * @param {*} wishes 603 | * @param {Array.} currentMatchingWishIds 604 | * @param givenMagicWord 605 | * @returns {Array.} 606 | * @private 607 | */ 608 | function _sortWishesByMatchingPriority( 609 | wishes, 610 | currentMatchingWishIds, 611 | givenMagicWord, 612 | ) { 613 | const matchPriorityArrays = [] 614 | let returnedIds = [] 615 | 616 | _each( 617 | wishes, 618 | wish => { 619 | if (_wishInContext(wish)) { 620 | const matchPriority = _bestMagicWordsMatch( 621 | wish.magicWords, 622 | givenMagicWord, 623 | ) 624 | _maybeAddWishToMatchPriorityArray( 625 | wish, 626 | matchPriority, 627 | matchPriorityArrays, 628 | currentMatchingWishIds, 629 | ) 630 | } 631 | }, 632 | true, 633 | ) 634 | 635 | _each( 636 | matchPriorityArrays, 637 | matchTypeArray => { 638 | if (matchTypeArray) { 639 | _each(matchTypeArray, magicWordIndexArray => { 640 | if (magicWordIndexArray) { 641 | returnedIds = returnedIds.concat(magicWordIndexArray) 642 | } 643 | }) 644 | } 645 | }, 646 | true, 647 | ) 648 | return returnedIds 649 | } 650 | 651 | /** 652 | * Gets the best magic words match of the wish's magic words 653 | * @param {string|Array.} wishesMagicWords 654 | * @param {string} givenMagicWord 655 | * @returns {MatchPriority} 656 | * @private 657 | */ 658 | function _bestMagicWordsMatch(wishesMagicWords, givenMagicWord) { 659 | const bestMatch = { 660 | matchType: _matchRankMap.noMatch, 661 | magicWordIndex: -1, 662 | } 663 | _each(wishesMagicWords, (wishesMagicWord, index) => { 664 | const matchRank = _stringsMatch(wishesMagicWord, givenMagicWord) 665 | if (matchRank > bestMatch.matchType) { 666 | bestMatch.matchType = matchRank 667 | bestMatch.magicWordIndex = index 668 | } 669 | return bestMatch.matchType !== _matchRankMap.equals 670 | }) 671 | return bestMatch 672 | } 673 | 674 | /** 675 | * Gives a _matchRankMap score based on 676 | * how well the two strings match. 677 | * @param {string} magicWord 678 | * @param {string} givenMagicWord 679 | * @returns {number} 680 | * @private 681 | */ 682 | function _stringsMatch(magicWord, givenMagicWord) { 683 | /* jshint maxcomplexity:8 */ 684 | magicWord = `${magicWord}`.toLowerCase() 685 | 686 | // too long 687 | if (givenMagicWord.length > magicWord.length) { 688 | return _matchRankMap.noMatch 689 | } 690 | 691 | // equals 692 | if (magicWord === givenMagicWord) { 693 | return _matchRankMap.equals 694 | } 695 | 696 | // starts with 697 | if (magicWord.indexOf(givenMagicWord) === 0) { 698 | return _matchRankMap.startsWith 699 | } 700 | 701 | // word starts with 702 | if (magicWord.indexOf(` ${givenMagicWord}`) !== -1) { 703 | return _matchRankMap.wordStartsWith 704 | } 705 | 706 | // contains 707 | if (magicWord.indexOf(givenMagicWord) !== -1) { 708 | return _matchRankMap.contains 709 | } else if (givenMagicWord.length === 1) { 710 | // If the only character in the given magic word 711 | // isn't even contained in the magic word, then 712 | // it's definitely not a match. 713 | return _matchRankMap.noMatch 714 | } 715 | 716 | // acronym 717 | if (_getAcronym(magicWord).indexOf(givenMagicWord) !== -1) { 718 | return _matchRankMap.acronym 719 | } 720 | 721 | return _stringsByCharOrder(magicWord, givenMagicWord) 722 | } 723 | 724 | /** 725 | * Generates an acronym for a string. 726 | * 727 | * @param {string} string 728 | * @returns {string} 729 | * @private 730 | * @examples 731 | * _getAcronym('i love candy') // => 'ilc' 732 | * _getAcronym('water-fall in the spring-time') // => 'wfitst' 733 | */ 734 | function _getAcronym(string) { 735 | let acronym = '' 736 | const wordsInString = string.split(' ') 737 | _each(wordsInString, wordInString => { 738 | const splitByHyphenWords = wordInString.split('-') 739 | _each(splitByHyphenWords, splitByHyphenWord => { 740 | acronym += splitByHyphenWord.substr(0, 1) 741 | }) 742 | }) 743 | return acronym 744 | } 745 | 746 | /** 747 | * Returns a _matchRankMap.matches or noMatch score based on whether 748 | * the characters in the givenMagicWord are found in order in the 749 | * magicWord 750 | * @param {string} magicWord 751 | * @param {string} givenMagicWord 752 | * @returns {number} 753 | * @private 754 | */ 755 | function _stringsByCharOrder(magicWord, givenMagicWord) { 756 | let charNumber = 0 757 | 758 | function _findMatchingCharacter(matchChar, string) { 759 | let found = false 760 | for (let j = charNumber; j < string.length; j++) { 761 | const stringChar = string[j] 762 | if (stringChar === matchChar) { 763 | found = true 764 | charNumber = j + 1 765 | break 766 | } 767 | } 768 | return found 769 | } 770 | 771 | for (let i = 0; i < givenMagicWord.length; i++) { 772 | const matchChar = givenMagicWord[i] 773 | const found = _findMatchingCharacter(matchChar, magicWord) 774 | if (!found) { 775 | return _matchRankMap.noMatch 776 | } 777 | } 778 | return _matchRankMap.matches 779 | } 780 | 781 | /** 782 | * If the wish has a matchType which is not equal to the _matchRankMap.noMatch 783 | * and it is not contained in the currentMatchingWishIds, then it is added to 784 | * the matchPriorityArrays based on the matchPriority. 785 | * @param {wish} wish 786 | * @param {MatchPriority} matchPriority 787 | * @param {Array.} matchPriorityArrays 788 | * @param {wishIds} currentMatchingWishIds 789 | * @private 790 | */ 791 | function _maybeAddWishToMatchPriorityArray( 792 | wish, 793 | matchPriority, 794 | matchPriorityArrays, 795 | currentMatchingWishIds, 796 | ) { 797 | const indexOfWishInCurrent = currentMatchingWishIds.indexOf(wish.id) 798 | if (matchPriority.matchType !== _matchRankMap.noMatch) { 799 | if (indexOfWishInCurrent === -1) { 800 | _getMatchPriorityArray(matchPriorityArrays, matchPriority).push(wish.id) 801 | } 802 | } else if (indexOfWishInCurrent !== -1) { 803 | // remove current matching wishIds if it doesn't match 804 | currentMatchingWishIds.splice(indexOfWishInCurrent, 1) 805 | } 806 | } 807 | 808 | /** 809 | * Creates a spot in the given array for the matchPriority 810 | * @param {Array.} arry 811 | * @param {MatchPriority} matchPriority 812 | * @returns {Array.} 813 | * @private 814 | */ 815 | function _getMatchPriorityArray(arry, matchPriority) { 816 | arry[matchPriority.matchType] = arry[matchPriority.matchType] || [] 817 | const matchTypeArray = arry[matchPriority.matchType] 818 | const matchPriorityArray = (matchTypeArray[matchPriority.magicWordIndex] = 819 | matchTypeArray[matchPriority.magicWordIndex] || []) 820 | return matchPriorityArray 821 | } 822 | 823 | /** 824 | * Take the given wish/wish id and call it's action 825 | * method if it is in context. 826 | * @param {wish|string} [wish] If null, then the first wish 827 | * to come back from getMatchingWishes(magicWord) will be made. 828 | * @param {string} magicWord The words to match the wish to. 829 | * This is used for 830 | * 1. Getting a wish if none is provided 831 | * 2. Passed as an argument to `wish.action` 832 | * 3. Updating the enteredMagicWords to improve future matching 833 | * @returns {wish} The wish which was made. 834 | * @public 835 | */ 836 | function makeWish(wish, magicWord) { 837 | magicWord = (!!magicWord ? `${magicWord}` : '').toLowerCase() 838 | wish = _convertToWishObjectFromNullOrId(wish, magicWord) 839 | 840 | if (!_wishCanBeMade(wish)) { 841 | return null 842 | } 843 | 844 | _executeWish(wish, magicWord) 845 | 846 | if (!_isNullOrUndefined(magicWord)) { 847 | _updateEnteredMagicWords(wish, magicWord) 848 | } 849 | return wish 850 | } 851 | 852 | /** 853 | * Convert the given wish argument to a valid wish object. 854 | * It could be an ID, or null. 855 | * @param {wish|string} [wish] An id, wish object, or null. 856 | * @param {string} magicWord Used if wish is null to lookup 857 | * the nearest matching wish to be used. 858 | * @returns {wish} The wish object 859 | * @private 860 | */ 861 | function _convertToWishObjectFromNullOrId(wish, magicWord) { 862 | let wishObject = wish 863 | // Check if it may be a wish object 864 | if (!_isObject(wishObject)) { 865 | wishObject = getWish(wish) 866 | } 867 | if (_isNullOrUndefined(wishObject)) { 868 | const matchingWishes = getMatchingWishes(magicWord) 869 | if (matchingWishes.length > 0) { 870 | wishObject = matchingWishes[0] 871 | } 872 | } 873 | return wishObject 874 | } 875 | 876 | /** A wish is non-executable if it 877 | * - doesn't exist 878 | * - doesn't have an action 879 | * - wish is not in context 880 | * @param {wish} wish The wish to check 881 | * @returns {boolean} Whether the wish can be made. 882 | * @private 883 | * @examples 884 | * _wishCanBeMade(null) // => false 885 | * _wishCanBeMade(undefined) // => false 886 | * _wishCanBeMade({}) // => false 887 | * _wishCanBeMade({action: function(){}}) // => false 888 | * _wishCanBeMade({action: function(){}, context: {any:'universe'}}) // => true 889 | */ 890 | function _wishCanBeMade(wish) { 891 | return !!wish && !_isNullOrUndefined(wish.action) && _wishInContext(wish) 892 | } 893 | 894 | /** 895 | * Calls the wish's action with the wish and 896 | * magic word as the parameters and iterates 897 | * the timesMade properties. 898 | * 899 | * @param {wish} wish 900 | * @param {string} magicWord 901 | * @private 902 | */ 903 | function _executeWish(wish, magicWord) { 904 | wish.action(wish, magicWord) 905 | const timesMade = wish.data.timesMade 906 | timesMade.total++ 907 | timesMade.magicWords[magicWord] = timesMade.magicWords[magicWord] || 0 908 | timesMade.magicWords[magicWord]++ 909 | } 910 | 911 | /** 912 | * Returns true if the given context is the default context. 913 | * @param {string|Array.|context} context 914 | * @returns {boolean} contextIsDefault 915 | * @private 916 | * @examples 917 | * _contextIsDefault(_defaultContext[0]) // => true 918 | * _contextIsDefault(_defaultContext) // => true 919 | * _contextIsDefault(_defaultContext.concat(['1', '2', '3'])) // => true 920 | * _contextIsDefault('something else') // => false 921 | */ 922 | function _contextIsDefault(context) { 923 | if (!_isObject(context)) { 924 | context = _arrayify(context) 925 | } 926 | if (_isArray(context) && context.length === 1) { 927 | return context[0] === _defaultContext[0] 928 | } else if (context.any && context.any.length === 1) { 929 | return context.any[0] === _defaultContext[0] 930 | } else { 931 | return false 932 | } 933 | } 934 | 935 | /** 936 | * There are a few ways for a wish to be in context: 937 | * 1. Genie's context is equal to the default context 938 | * 2. The wish's context is equal to the default context 939 | * 3. The wish's context is equal to genie's context 940 | * 4. The wish is _wishInThisContext(_context) 941 | * @param {wish} wish 942 | * @returns {boolean} wishInContext 943 | * @private 944 | */ 945 | function _wishInContext(wish) { 946 | return ( 947 | _contextIsDefault(_context) || 948 | _contextIsDefault(wish.context) || 949 | wish.context === _context || 950 | _wishInThisContext(wish, _context) 951 | ) 952 | } 953 | 954 | /** 955 | * This will get the any, all, and none constraints for the 956 | * wish's context. If a constraint is not present, it is 957 | * considered passing. The exception being if the wish has 958 | * no context (each context property is not present). In 959 | * this case, it is not in context. 960 | * These things must be true for the wish to be in the given context: 961 | * 1. any: genie's context contains any of these. 962 | * 2. all: genie's context contains all of these. 963 | * 3. none: genie's context contains none of these. 964 | * 965 | * @param {wish} wish 966 | * @param {string|Array.} theContexts 967 | * @returns {boolean} wishInThisContext 968 | * @private 969 | */ 970 | function _wishInThisContext(wish, theContexts) { 971 | const any = wish.context.any || [] 972 | const all = wish.context.all || [] 973 | const none = wish.context.none || [] 974 | 975 | const containsAny = _isEmpty(any) || _arrayContainsAny(theContexts, any) 976 | const containsAll = 977 | theContexts.length >= all.length && _arrayContainsAll(theContexts, all) 978 | const wishNoneContextNotContainedInContext = _arrayContainsNone( 979 | theContexts, 980 | none, 981 | ) 982 | 983 | const wishContextConstraintsMet = 984 | containsAny && containsAll && wishNoneContextNotContainedInContext 985 | 986 | return wishContextConstraintsMet 987 | } 988 | 989 | /** 990 | * Updates the _enteredMagicWords map. Steps: 991 | * 1. Get or create a spot for the magic word in the map. 992 | * 2. If the wish is the first element in the map already, 993 | * do nothing. (return) 994 | * 3. If the wish already exists in the map, remove it. 995 | * 4. If the wish was not already the second element, 996 | * set is as the second element. If it was, set it 997 | * as the first element. 998 | * @param {wish} wish 999 | * @param {string} magicWord 1000 | * @private 1001 | */ 1002 | function _updateEnteredMagicWords(wish, magicWord) { 1003 | // Reset entered magicWords order. 1004 | const spotForWishes = _createSpotInEnteredMagicWords( 1005 | _enteredMagicWords, 1006 | magicWord, 1007 | ) 1008 | spotForWishes.wishes = spotForWishes.wishes || [] 1009 | const existingIndex = spotForWishes.wishes.indexOf(wish.id) 1010 | if (existingIndex !== 0) { 1011 | _repositionWishIdInEnteredMagicWordsArray( 1012 | wish.id, 1013 | spotForWishes.wishes, 1014 | existingIndex, 1015 | ) 1016 | } 1017 | } 1018 | 1019 | /** 1020 | * Recursively creates a new object property if one does not exist 1021 | * for each character in the chars string. 1022 | * @param {object} spot 1023 | * @param {string} chars 1024 | * @returns {object} - the final object. 1025 | * @private 1026 | */ 1027 | function _createSpotInEnteredMagicWords(spot, chars) { 1028 | const firstChar = chars.substring(0, 1) 1029 | const remainingChars = chars.substring(1) 1030 | const nextSpot = (spot[firstChar] = spot[firstChar] || {}) 1031 | if (remainingChars) { 1032 | return _createSpotInEnteredMagicWords(nextSpot, remainingChars) 1033 | } else { 1034 | return nextSpot 1035 | } 1036 | } 1037 | 1038 | /** 1039 | * Updates the order of wish ids based on "king of the hill" 1040 | * and "on deck" concepts. Meaning, for a wish to be placed 1041 | * in front, it needs to be "on deck" which is the second 1042 | * position. If it is not already on deck then it will be 1043 | * placed in the second position. If it is on deck then it 1044 | * will replace the king of the hill and the king of the hill 1045 | * will be placed in the second position (on deck). 1046 | * @param {string} id 1047 | * @param {Array} arry 1048 | * @param {number} existingIndex 1049 | * @private 1050 | */ 1051 | function _repositionWishIdInEnteredMagicWordsArray(id, arry, existingIndex) { 1052 | if (existingIndex !== -1) { 1053 | // If it already exists, remove it before re-adding it in the correct spot 1054 | arry.splice(existingIndex, 1) 1055 | } 1056 | if (existingIndex !== 1 && arry.length > 0) { 1057 | // If it's not "on deck" then put it in the first slot and set the King of the Hill to be the id to go first. 1058 | const first = arry[0] 1059 | arry[0] = id 1060 | id = first 1061 | } 1062 | arry.unshift(id) 1063 | } 1064 | 1065 | /** 1066 | * Gets the context paths that should be added based on the 1067 | * given path and the context paths that should be removed 1068 | * based ont he given path 1069 | * @param {string} path 1070 | * @returns {{add: Array, remove: Array}} 1071 | * @private 1072 | */ 1073 | function _getContextsFromPath(path) { 1074 | const allContexts = { 1075 | add: [], 1076 | remove: [], 1077 | } 1078 | _each(_pathContexts, pathContext => { 1079 | let contextAdded = false 1080 | const contexts = pathContext.contexts 1081 | const regexes = pathContext.regexes 1082 | const paths = pathContext.paths 1083 | 1084 | _each(regexes, regex => { 1085 | regex.lastIndex = 0 1086 | const matches = regex.exec(path) 1087 | if (matches && matches.length > 0) { 1088 | const contextsToAdd = [] 1089 | _each(contexts, context => { 1090 | const replacedContext = context.replace( 1091 | _contextRegex, 1092 | (match, group) => { 1093 | return matches[group] 1094 | }, 1095 | ) 1096 | contextsToAdd.push(replacedContext) 1097 | }) 1098 | allContexts.add = allContexts.add.concat(contextsToAdd) 1099 | contextAdded = true 1100 | } 1101 | return !contextAdded 1102 | }) 1103 | 1104 | if (!contextAdded) { 1105 | _each(paths, pathToTry => { 1106 | if (path === pathToTry) { 1107 | allContexts.add = allContexts.add.concat(contexts) 1108 | contextAdded = true 1109 | } 1110 | return !contextAdded 1111 | }) 1112 | if (!contextAdded) { 1113 | allContexts.remove = allContexts.remove.concat(contexts) 1114 | } 1115 | } 1116 | }) 1117 | return allContexts 1118 | } 1119 | 1120 | /** 1121 | * Gets all the pathContext.contexts that are regex contexts and matches 1122 | * those to genie's contexts. Returns all the matching contexts. 1123 | * @returns {Array} 1124 | * @private 1125 | */ 1126 | function _getContextsMatchingRegexPathContexts() { 1127 | const regexContexts = [] 1128 | _each(_pathContexts, pathContext => { 1129 | const contexts = pathContext.contexts 1130 | 1131 | _each(contexts, context => { 1132 | if (_contextRegex.test(context)) { 1133 | // context string is a regex context 1134 | const replaceContextRegex = context.replace(_contextRegex, '.+?') 1135 | 1136 | _each(_context, currentContext => { 1137 | if (new RegExp(replaceContextRegex).test(currentContext)) { 1138 | regexContexts.push(currentContext) 1139 | } 1140 | }) 1141 | } 1142 | }) 1143 | }) 1144 | return regexContexts 1145 | } 1146 | 1147 | // Helpers // 1148 | /** 1149 | * returns the obj in array form if it is not one already 1150 | * @param {*} obj 1151 | * @returns {Array.<*>} 1152 | * @private 1153 | * @examples 1154 | * _arrayify('hello') // => ['hello'] 1155 | * _arrayify() // => [] 1156 | * _arrayify(['you', 'rock']) // => ['you', 'rock'] 1157 | * _arrayify({x: 3, y: 'sup'}) // => [{x: 3, y: 'sup'}] 1158 | */ 1159 | function _arrayify(obj) { 1160 | if (!obj) { 1161 | return [] 1162 | } else if (_isArray(obj)) { 1163 | return obj 1164 | } else { 1165 | return [obj] 1166 | } 1167 | } 1168 | 1169 | /** 1170 | * Adds items to the arry from the obj only if it 1171 | * is not in the arry already 1172 | * @param {Array.<*>} arry 1173 | * @param {*|Array.<*>} obj 1174 | * @returns {Array.<*>} arry 1175 | * @private 1176 | * @examples 1177 | * _addUniqueItems(1, 2) // => [1,2] 1178 | * _addUniqueItems(1, [2,3]) // => [1,2,3] 1179 | * _addUniqueItems([1,2], 3) // => [1,2,3] 1180 | * _addUniqueItems([1,2], [3,4]) // => [1,2,3,4] 1181 | * _addUniqueItems([1,2], [3,1]) // => [1,2,3] 1182 | * _addUniqueItems([1,2], [1,2]) // => [1,2] 1183 | * _addUniqueItems([1,2], [1,2]) // => [1,2] 1184 | * _addUniqueItems([1,2,3], [1,2,1,2,3]) // => [1,2,3] 1185 | */ 1186 | function _addUniqueItems(arry, obj) { 1187 | obj = _arrayify(obj) 1188 | arry = _arrayify(arry) 1189 | _each(obj, o => { 1190 | if (arry.indexOf(o) < 0) { 1191 | arry.push(o) 1192 | } 1193 | }) 1194 | return arry 1195 | } 1196 | 1197 | /** 1198 | * Removes all instances of items in the given obj 1199 | * from the given arry. 1200 | * @param {Array.<*>} arry 1201 | * @param {*|Array.<*>} obj 1202 | * @returns {Array.<*>} arry 1203 | * @private 1204 | * @examples 1205 | * _removeItems(1, 2) // => [1] 1206 | * _removeItems(1, [2,3]) // => [1] 1207 | * _removeItems([1,2], 3) // => [1,2] 1208 | * _removeItems([1,2], [3,4]) // => [1,2] 1209 | * _removeItems([1,2], [3,1]) // => [2] 1210 | * _removeItems([1,2], [1,2]) // => [] 1211 | * _removeItems([1,2,1,2,3], [2,3]) // => [1,1] 1212 | */ 1213 | function _removeItems(arry, obj) { 1214 | arry = _arrayify(arry) 1215 | obj = _arrayify(obj) 1216 | let i = 0 1217 | 1218 | while (i < arry.length) { 1219 | if (_contains(obj, arry[i])) { 1220 | arry.splice(i, 1) 1221 | } else { 1222 | i++ 1223 | } 1224 | } 1225 | return arry 1226 | } 1227 | 1228 | /** 1229 | * Returns true if arry1 contains any of arry2's elements 1230 | * @param {*|Array.<*>} arry1 1231 | * @param {*|Array.<*>} arry2 1232 | * @returns {boolean} 1233 | * @private 1234 | * @examples 1235 | * _arrayContainsAny(1, 2) // => false 1236 | * _arrayContainsAny([1], 2) // => false 1237 | * _arrayContainsAny(1, [2]) // => false 1238 | * _arrayContainsAny([2], [2]) // => true 1239 | * _arrayContainsAny([1,2], [2]) // => true 1240 | */ 1241 | function _arrayContainsAny(arry1, arry2) { 1242 | arry1 = _arrayify(arry1) 1243 | arry2 = _arrayify(arry2) 1244 | for (let i = 0; i < arry2.length; i++) { 1245 | if (_contains(arry1, arry2[i])) { 1246 | return true 1247 | } 1248 | } 1249 | return false 1250 | } 1251 | 1252 | /** 1253 | * Returns true if arry1 does not contain any of arry2's elements 1254 | * @param {*|Array.<*>} arry1 1255 | * @param {*|Array.<*>} arry2 1256 | * @returns {boolean} 1257 | * @private 1258 | */ 1259 | function _arrayContainsNone(arry1, arry2) { 1260 | arry1 = _arrayify(arry1) 1261 | arry2 = _arrayify(arry2) 1262 | for (let i = 0; i < arry2.length; i++) { 1263 | if (_contains(arry1, arry2[i])) { 1264 | return false 1265 | } 1266 | } 1267 | return true 1268 | } 1269 | 1270 | /** 1271 | * Returns true if arry1 contains all of arry2's elements 1272 | * @param {*|Array.<*>} arry1 1273 | * @param {*|Array.<*>} arry2 1274 | * @returns {boolean} 1275 | * @private 1276 | */ 1277 | function _arrayContainsAll(arry1, arry2) { 1278 | arry1 = _arrayify(arry1) 1279 | arry2 = _arrayify(arry2) 1280 | for (let i = 0; i < arry2.length; i++) { 1281 | if (!_contains(arry1, arry2[i])) { 1282 | return false 1283 | } 1284 | } 1285 | return true 1286 | } 1287 | 1288 | /** 1289 | * Whether an object has an index in an array 1290 | * @param {Array.<*>} arry 1291 | * @param {*} obj 1292 | * @returns {boolean} 1293 | * @private 1294 | */ 1295 | function _contains(arry, obj) { 1296 | return arry.indexOf(obj) > -1 1297 | } 1298 | 1299 | /** 1300 | * Whether the given object is empty. 1301 | * @param obj 1302 | * @returns {boolean} 1303 | * @private 1304 | * @examples 1305 | * _isEmpty() // => true 1306 | * _isEmpty(null) // => true 1307 | * _isEmpty(undefined) // => true 1308 | * _isEmpty('') // => true 1309 | * _isEmpty({}) // => true 1310 | * _isEmpty([]) // => true 1311 | * _isEmpty(1) // => false 1312 | * _isEmpty('a') // => false 1313 | * _isEmpty(['a', 'b']) // => false 1314 | * _isEmpty({a: 'b'}) // => false 1315 | */ 1316 | function _isEmpty(obj) { 1317 | if (_isNullOrUndefined(obj)) { 1318 | return true 1319 | } else if (_isArray(obj)) { 1320 | return obj.length === 0 1321 | } else if (_isString(obj)) { 1322 | return obj === '' 1323 | } else if (_isPrimitive(obj)) { 1324 | return false 1325 | } else if (_isObject(obj)) { 1326 | return Object.keys(obj).length < 1 1327 | } else { 1328 | return false 1329 | } 1330 | } 1331 | /** 1332 | * @callback eachCallback 1333 | * @param {*} item - the current array item 1334 | * @param {number|string} indexOrName - the index or property name of the item 1335 | * @param {Array.<*>} array - the whole array 1336 | */ 1337 | 1338 | /** 1339 | * Iterates through each own property of obj and calls the fn on it. 1340 | * If obj is an array: fn(val, index, obj) 1341 | * If obj is an obj: fn(val, propName, obj) 1342 | * @param {object|Array.<*>} obj - the object or array to iterate through 1343 | * @param {Function} fn - the function called each iteration. 1344 | * @param {boolean} [reverse=false] - whether to iterate 1345 | * through the array in reverse order. 1346 | * @returns {boolean} - whether the loop broke early 1347 | * @private 1348 | * @examples 1349 | * _each({a: 1, b: 'hello'}, callback) // => calls callback 2 times 1350 | * _each([1,2], callback) // => calls callback 2 times 1351 | * _each([], callback) // => calls callback 0 times 1352 | */ 1353 | function _each(obj, fn, reverse) { 1354 | if (_isPrimitive(obj)) { 1355 | obj = _arrayify(obj) 1356 | } 1357 | if (_isArray(obj)) { 1358 | return _eachArray(obj, fn, reverse) 1359 | } else { 1360 | return _eachProperty(obj, fn) 1361 | } 1362 | } 1363 | 1364 | /** 1365 | * If reverse is true, calls _eachArrayReverse(arry, fn) 1366 | * otherwise calls _eachArrayForward(arry, fn) 1367 | * @param {Array.<*>} arry 1368 | * @param {eachCallback} fn 1369 | * @param {boolean} [reverse=false] - whether to iterate 1370 | * through the array in reverse order. 1371 | * @returns {boolean} - whether the loop broke early 1372 | * @private 1373 | */ 1374 | function _eachArray(arry, fn, reverse) { 1375 | if (reverse === true) { 1376 | return _eachArrayReverse(arry, fn) 1377 | } else { 1378 | return _eachArrayForward(arry, fn) 1379 | } 1380 | } 1381 | 1382 | /** 1383 | * Iterates through the array and calls the given function 1384 | * in reverse order. 1385 | * @param {Array.<*>} arry 1386 | * @param {eachCallback} fn 1387 | * @returns {boolean} - whether the loop broke early 1388 | * @private 1389 | */ 1390 | function _eachArrayReverse(arry, fn) { 1391 | let ret = true 1392 | for (let i = arry.length - 1; i >= 0; i--) { 1393 | ret = fn(arry[i], i, arry) 1394 | if (ret === false) { 1395 | break 1396 | } 1397 | } 1398 | return ret 1399 | } 1400 | 1401 | /** 1402 | * Iterates through the array and calls the given function 1403 | * @param {Array.<*>} arry 1404 | * @param {Function} fn 1405 | * @returns {boolean} - whether the loop broke early 1406 | * @private 1407 | */ 1408 | function _eachArrayForward(arry, fn) { 1409 | let ret = true 1410 | for (let i = 0; i < arry.length; i++) { 1411 | ret = fn(arry[i], i, arry) 1412 | if (ret === false) { 1413 | break 1414 | } 1415 | } 1416 | return ret 1417 | } 1418 | 1419 | /** 1420 | * Iterates through each property and calls the callback 1421 | * if the object hasOwnProperty. 1422 | * @param {object} obj 1423 | * @param {Function} fn 1424 | * @returns {boolean} 1425 | * @private 1426 | */ 1427 | function _eachProperty(obj, fn) { 1428 | let ret = true 1429 | for (const prop in obj) { 1430 | if (obj.hasOwnProperty(prop)) { 1431 | ret = fn(obj[prop], prop, obj) 1432 | if (ret === false) { 1433 | break 1434 | } 1435 | } 1436 | } 1437 | return ret 1438 | } 1439 | 1440 | /** 1441 | * @param {*} obj 1442 | * @returns {boolean} 1443 | * @private 1444 | * @examples 1445 | * _isArray([1]) // => true 1446 | * _isArray({x: 1}) // => false 1447 | * _isArray() // => false 1448 | */ 1449 | function _isArray(obj) { 1450 | return obj instanceof Array 1451 | } 1452 | 1453 | /** 1454 | * @param {*} obj 1455 | * @returns {boolean} 1456 | * @private 1457 | * @examples 1458 | * _isString('') // => true 1459 | * _isString({}) // => false 1460 | * _isString([]) // => false 1461 | * _isString(1) // => false 1462 | * _isString(true) // => false 1463 | */ 1464 | function _isString(obj) { 1465 | return typeof obj === 'string' 1466 | } 1467 | 1468 | /** 1469 | * @param {*} obj 1470 | * @returns {boolean} 1471 | * @private 1472 | * @examples 1473 | * _isObject({}) // => true 1474 | * _isObject([]) // => true 1475 | * _isObject(1) // => false 1476 | * _isObject('a') // => false 1477 | * _isObject(true) // => false 1478 | */ 1479 | function _isObject(obj) { 1480 | return typeof obj === 'object' 1481 | } 1482 | 1483 | /** 1484 | * @param {*} obj 1485 | * @returns {boolean} 1486 | * @private 1487 | * @examples 1488 | * _isPrimitive('string') // => true 1489 | * _isPrimitive(1) // => true 1490 | * _isPrimitive(true) // => true 1491 | * _isPrimitive(undefined) // => true 1492 | * _isPrimitive(null) // => false 1493 | * _isPrimitive({}) // => false 1494 | * _isPrimitive([]) // => false 1495 | */ 1496 | function _isPrimitive(obj) { 1497 | switch (typeof obj) { 1498 | case 'string': 1499 | case 'number': 1500 | case 'boolean': 1501 | case 'undefined': 1502 | return true 1503 | default: 1504 | return false 1505 | } 1506 | } 1507 | 1508 | /** 1509 | * @param {*|Array.<*>} obj 1510 | * @returns {boolean} 1511 | * @private 1512 | * @examples 1513 | * _isUndefined() // => true 1514 | * _isUndefined(undefined) // => true 1515 | * _isUndefined(null) // => false 1516 | * _isUndefined({}) // => false 1517 | * _isUndefined(1) // => false 1518 | * _isUndefined(false) // => false 1519 | * _isUndefined('defined') // => false 1520 | * _isUndefined('defined') // => false 1521 | * _isUndefined(['defined', undefined]) // => true 1522 | * _isUndefined([undefined, undefined]) // => true 1523 | */ 1524 | function _isUndefined(obj) { 1525 | if (_isArray(obj)) { 1526 | return !_each(obj, o => { 1527 | return !_isUndefined(o) 1528 | }) 1529 | } else { 1530 | return typeof obj === 'undefined' 1531 | } 1532 | } 1533 | 1534 | /** 1535 | * @param {*} obj 1536 | * @returns {boolean} 1537 | * @private 1538 | * @examples 1539 | * _isNull(null) // => true 1540 | * _isNull() // => false 1541 | * _isNull(1) // => false 1542 | * _isNull('hello') // => false 1543 | * _isNull(true) // => false 1544 | * _isNull(['hello', null]) // => true 1545 | * _isNull([null, null]) // => true 1546 | */ 1547 | function _isNull(obj) { 1548 | if (_isArray(obj)) { 1549 | return !_each(obj, o => { 1550 | return !_isNull(o) 1551 | }) 1552 | } else { 1553 | return obj === null 1554 | } 1555 | } 1556 | 1557 | /** 1558 | * @param {*} obj 1559 | * @returns {boolean} 1560 | * @private 1561 | * _isNullOrUndefined(null) // => true 1562 | * _isNullOrUndefined() // => true 1563 | * _isNullOrUndefined(undefined) // => true 1564 | * _isNullOrUndefined(1) // => false 1565 | * _isNullOrUndefined({}) // => false 1566 | * _isNullOrUndefined('hello') // => false 1567 | * _isNullOrUndefined(true) // => false 1568 | * _isNullOrUndefined(['hello', null]) // => true 1569 | * _isNullOrUndefined([undefined, null]) // => true 1570 | * _isNullOrUndefined(false) // => false 1571 | * _isNullOrUndefined('defined') // => false 1572 | * _isNullOrUndefined('defined') // => false 1573 | * _isNullOrUndefined(['defined', undefined]) // => true 1574 | * _isNullOrUndefined([null, undefined]) // => true 1575 | */ 1576 | function _isNullOrUndefined(obj) { 1577 | return _isNull(obj) || _isUndefined(obj) 1578 | } 1579 | 1580 | /** 1581 | * @typedef {object} GenieOptions 1582 | * @property {Array.} wishes - All wishes registered with genie 1583 | * @property {number} previousId - The number used to generate an 1584 | * id for a newly registered wish 1585 | * @property {object} enteredMagicWords - An exploded object of letters 1586 | * to wishes and letters. 1587 | * @property {Array.} context - an array of all of genie's current contexts 1588 | * @property {Array.} previousContext - genie's most recent context 1589 | * @property {boolean} enabled - whether genie is enabled 1590 | * @property {boolean} returnOnDisabled - whether genie will return an 1591 | * empty object when it is disabled. 1592 | * @public 1593 | */ 1594 | 1595 | /** 1596 | * An api into genie's options 1597 | * The opts argument can have the properties of GenieOptions 1598 | * as well as the following property: 1599 | * - noWishMerge: boolean - Using this will simply assign the 1600 | * given wishes to genie's _wishes variable. If falsy, then 1601 | * genie.mergeWishes is called with the wishes. 1602 | * 1603 | * @param {object} [opts] - if not given, simply returns the options 1604 | * @returns {GenieOptions} 1605 | * @public 1606 | */ 1607 | function options(opts) { 1608 | /* jshint maxcomplexity:8 */ 1609 | if (opts) { 1610 | _updateWishesWithOptions(opts) 1611 | _previousId = isNaN(opts.previousId) ? _previousId : opts.previousId 1612 | _enteredMagicWords = opts.enteredMagicWords || _enteredMagicWords 1613 | _context = opts.context || _context 1614 | _previousContext = opts.previousContext || _previousContext 1615 | _enabled = opts.enabled || _enabled 1616 | _returnOnDisabled = opts.returnOnDisabled || _returnOnDisabled 1617 | } 1618 | return { 1619 | wishes: _wishes, 1620 | previousId: _previousId, 1621 | enteredMagicWords: _enteredMagicWords, 1622 | context: _context, 1623 | previousContext: _previousContext, 1624 | enabled: _enabled, 1625 | returnOnDisabled: _returnOnDisabled, 1626 | } 1627 | } 1628 | 1629 | /** 1630 | * This will override the matching algorithm ({@link #getMatchingWishes}) 1631 | * You wont need to change how you interface with 1632 | * {@link #getMatchingWishes} at all by using this. 1633 | * @param fn {Function} The new function. Should accept wishes array, 1634 | * magicWord string, and enteredMagicWords object. 1635 | * @public 1636 | */ 1637 | function overrideMatchingAlgorithm(fn) { 1638 | genie.getMatchingWishes = _passThrough(magicWord => { 1639 | return fn(_wishes, magicWord, _context, _enteredMagicWords) 1640 | }, []) 1641 | } 1642 | 1643 | /** 1644 | * This will set the matching algorithm back to the original 1645 | * @returns {Function} The old matching algorithm 1646 | * @public 1647 | */ 1648 | function restoreMatchingAlgorithm() { 1649 | const oldMatchingAlgorithm = genie.getMatchingWishes 1650 | genie.getMatchingWishes = _originalMatchingAlgorithm 1651 | return oldMatchingAlgorithm 1652 | } 1653 | 1654 | /** 1655 | * If wishes are present, will update them based on options given. 1656 | * @param {object} opts 1657 | * @private 1658 | */ 1659 | function _updateWishesWithOptions(opts) { 1660 | if (opts.wishes) { 1661 | if (opts.noWishMerge) { 1662 | _wishes = opts.wishes 1663 | } else { 1664 | mergeWishes(opts.wishes) 1665 | } 1666 | } 1667 | } 1668 | 1669 | /** 1670 | * Merges the given wishes with genie's current wishes. 1671 | * Iterates through the wishes: If the wish does not have 1672 | * an action, and the wish's id is registered with genie, 1673 | * genie will assign the registered wish's action to 1674 | * the new wish's action property. 1675 | * Next, if the new wish has an action, it is registered 1676 | * with genie based on its wishId 1677 | * @param {Array.} wishes Array of wish objects 1678 | * @returns {Array.} All of genie's wishes 1679 | * @public 1680 | */ 1681 | function mergeWishes(wishes) { 1682 | _each(wishes, newWish => { 1683 | let wishIndex = -1 1684 | let existingWish = null 1685 | _each(_wishes, (aWish, aWishIndex) => { 1686 | if (aWish.id === newWish.id) { 1687 | existingWish = aWish 1688 | wishIndex = aWishIndex 1689 | return false 1690 | } 1691 | }) 1692 | if (!newWish.action && existingWish) { 1693 | newWish.action = existingWish.action 1694 | } 1695 | if (newWish.action) { 1696 | _wishes[wishIndex] = newWish 1697 | } 1698 | }) 1699 | return _wishes 1700 | } 1701 | 1702 | /** 1703 | * Set's then returns genie's current context. 1704 | * If no context is provided, simply acts as getter. 1705 | * If a context is provided, genie's previous context 1706 | * is set to the context before it is assigned 1707 | * to the given context. 1708 | * @param {string|Array.} newContext The context to set genie's context to. 1709 | * @returns {Array.} The new context 1710 | * @public 1711 | */ 1712 | function context(newContext) { 1713 | if (!_isUndefined(newContext)) { 1714 | _previousContext = _context 1715 | if (!_isArray(newContext)) { 1716 | newContext = [newContext] 1717 | } 1718 | _context = newContext 1719 | } 1720 | return _context 1721 | } 1722 | 1723 | /** 1724 | * Adds the new context to genie's current context. 1725 | * Genie's context will maintain uniqueness, so don't 1726 | * worry about overloading genie's context with 1727 | * duplicates. 1728 | * @param {string|Array.} newContext The context to add 1729 | * @returns {Array} Genie's new context 1730 | * @public 1731 | */ 1732 | function addContext(newContext) { 1733 | if (newContext && newContext.length) { 1734 | _previousContext = _context 1735 | _addUniqueItems(_context, newContext) 1736 | } 1737 | return _context 1738 | } 1739 | 1740 | /** 1741 | * Removes the given context 1742 | * @param {string|Array.} contextToRemove The context to remove 1743 | * @returns {Array.} Genie's new context 1744 | * @public 1745 | */ 1746 | function removeContext(contextToRemove) { 1747 | if (contextToRemove && contextToRemove.length) { 1748 | _previousContext = _context 1749 | _removeItems(_context, contextToRemove) 1750 | if (_isEmpty(context)) { 1751 | _context = _defaultContext 1752 | } 1753 | } 1754 | return _context 1755 | } 1756 | 1757 | /** 1758 | * Changes genie's context to _previousContext 1759 | * @returns {Array.} The new context 1760 | * @public 1761 | */ 1762 | function revertContext() { 1763 | return context(_previousContext) 1764 | } 1765 | 1766 | /** 1767 | * Changes context to _defaultContext 1768 | * @returns {Array.} The new context 1769 | * @public 1770 | */ 1771 | function restoreContext() { 1772 | return context(_defaultContext) 1773 | } 1774 | 1775 | /** 1776 | * Updates genie's context based on the given path 1777 | * @param {string} path the path to match 1778 | * @param {boolean} [noDeregister] Do not deregister wishes 1779 | * which are no longer in context 1780 | * @returns {Array.} The new context 1781 | * @public 1782 | */ 1783 | function updatePathContext(path, noDeregister) { 1784 | if (path) { 1785 | const allContexts = _getContextsFromPath(path) 1786 | const contextsToAdd = allContexts.add 1787 | let contextsToRemove = _getContextsMatchingRegexPathContexts() 1788 | contextsToRemove = contextsToRemove.concat(allContexts.remove) 1789 | 1790 | removeContext(contextsToRemove) 1791 | 1792 | if (!noDeregister) { 1793 | // There's no way to prevent users of genie from adding wishes that already exist in genie 1794 | // so we're completely removing them here 1795 | deregisterWishesWithContext(contextsToRemove) 1796 | } 1797 | 1798 | addContext(contextsToAdd) 1799 | } 1800 | return _context 1801 | } 1802 | 1803 | /** 1804 | * Add a path context to genie's pathContexts 1805 | * @param {Array.} pathContexts The path context to add 1806 | * @returns {Array.} The new path contexts 1807 | * @public 1808 | */ 1809 | function addPathContext(pathContexts) { 1810 | _each(pathContexts, pathContext => { 1811 | if (pathContext.paths) { 1812 | pathContext.paths = _arrayify(pathContext.paths) 1813 | } 1814 | 1815 | if (pathContext.regexes) { 1816 | pathContext.regexes = _arrayify(pathContext.regexes) 1817 | } 1818 | 1819 | if (pathContext.contexts) { 1820 | pathContext.contexts = _arrayify(pathContext.contexts) 1821 | } 1822 | }) 1823 | _addUniqueItems(_pathContexts, pathContexts) 1824 | return _pathContexts 1825 | } 1826 | 1827 | /** 1828 | * Removes the given path contexts from genie's path contexts 1829 | * @param {Array.} pathContext - The path context 1830 | * object to remove. Must be equal to the object that was added 1831 | * previously. No support for ids etc. 1832 | * @returns {Array.} Genie's new path context 1833 | * @public 1834 | */ 1835 | function removePathContext(pathContext) { 1836 | _removeItems(_pathContexts, pathContext) 1837 | return _pathContexts 1838 | } 1839 | 1840 | /** 1841 | * Set/get genie's enabled state 1842 | * @param {boolean} [newState] The new state for `enabled` 1843 | * @returns {boolean} Genie's enabled state 1844 | * @public 1845 | */ 1846 | function enabled(newState) { 1847 | if (newState !== undefined) { 1848 | _enabled = newState 1849 | } 1850 | return _enabled 1851 | } 1852 | 1853 | /** 1854 | * Set/get genie's returnOnDisabled state 1855 | * This defines whether genie will return an empty 1856 | * object when it is disabled. Useful for when you 1857 | * want to disable genie, but don't want to do 1858 | * null checks in your code everywhere you use genie. 1859 | * @param {boolean} [newState] The new state for `returnOnDisabled` 1860 | * @returns {boolean} Genie's returnOnDisabled state 1861 | * @public 1862 | */ 1863 | function returnOnDisabled(newState) { 1864 | if (newState !== undefined) { 1865 | _returnOnDisabled = newState 1866 | } 1867 | return _returnOnDisabled 1868 | } 1869 | 1870 | /** 1871 | * Used to hijack public api functions for the 1872 | * enabled feature 1873 | * @param {Function} fn 1874 | * @param {*} emptyRetObject 1875 | * @returns {Function} 1876 | * @private 1877 | */ 1878 | function _passThrough(fn, emptyRetObject) { 1879 | // eslint-disable-next-line babel/no-invalid-this 1880 | const _thusly = this 1881 | return function hijackedFunction() { 1882 | if (_enabled || fn === enabled) { 1883 | // eslint-disable-next-line prefer-rest-params 1884 | return fn.apply(_thusly, arguments) 1885 | } else if (_returnOnDisabled) { 1886 | return emptyRetObject 1887 | } else { 1888 | return null 1889 | } 1890 | } 1891 | } 1892 | 1893 | genie.deregisterWish = _passThrough(deregisterWish, {}) 1894 | genie.deregisterWishesWithContext = _passThrough( 1895 | deregisterWishesWithContext, 1896 | [], 1897 | ) 1898 | genie.getMatchingWishes = _passThrough(getMatchingWishes, []) 1899 | genie.overrideMatchingAlgorithm = _passThrough(overrideMatchingAlgorithm, {}) 1900 | genie.restoreMatchingAlgorithm = _passThrough(restoreMatchingAlgorithm, {}) 1901 | genie.getWishesInContext = _passThrough(getWishesInContext, []) 1902 | genie.getWishesWithContext = _passThrough(getWishesWithContext, []) 1903 | genie.getWish = _passThrough(getWish, {}) 1904 | genie.makeWish = _passThrough(makeWish, {}) 1905 | genie.reset = _passThrough(reset, {}) 1906 | genie.options = _passThrough(options, {}) 1907 | genie.mergeWishes = _passThrough(mergeWishes, {}) 1908 | genie.context = _passThrough(context, []) 1909 | genie.addContext = _passThrough(addContext, []) 1910 | genie.removeContext = _passThrough(removeContext, []) 1911 | genie.revertContext = _passThrough(revertContext, []) 1912 | genie.restoreContext = _passThrough(restoreContext, []) 1913 | genie.updatePathContext = _passThrough(updatePathContext, []) 1914 | genie.addPathContext = _passThrough(addPathContext, []) 1915 | genie.removePathContext = _passThrough(removePathContext, []) 1916 | genie.enabled = _passThrough(enabled, false) 1917 | genie.returnOnDisabled = _passThrough(returnOnDisabled, true) 1918 | genie.version = '0.4.0' 1919 | 1920 | _originalMatchingAlgorithm = genie.getMatchingWishes 1921 | 1922 | export default genie 1923 | 1924 | // TODO: enable all of these rules... 1925 | /* 1926 | eslint 1927 | valid-jsdoc:0, 1928 | no-shadow:0, 1929 | func-names:0, 1930 | consistent-return:0, 1931 | complexity:[2, 10], 1932 | no-multi-assign:0, 1933 | no-negated-condition:0 1934 | */ 1935 | --------------------------------------------------------------------------------