├── .babelrc ├── .github └── workflows │ └── actions.yml ├── .gitignore ├── .jscsrc ├── .node-version ├── .nvmrc ├── AUTHORS ├── LICENSE.txt ├── README.md ├── assets └── images │ ├── agent@2x.png │ ├── chat@2x.png │ ├── customer@2x.png │ └── progress@2x.png ├── bower.json ├── docs ├── Chariot.html ├── Overlay.html ├── Step.html ├── Tooltip.html ├── Tutorial.html ├── chariot.js.html ├── config.example.js.html ├── fonts │ ├── OpenSans-Bold-webfont.eot │ ├── OpenSans-Bold-webfont.svg │ ├── OpenSans-Bold-webfont.woff │ ├── OpenSans-BoldItalic-webfont.eot │ ├── OpenSans-BoldItalic-webfont.svg │ ├── OpenSans-BoldItalic-webfont.woff │ ├── OpenSans-Italic-webfont.eot │ ├── OpenSans-Italic-webfont.svg │ ├── OpenSans-Italic-webfont.woff │ ├── OpenSans-Light-webfont.eot │ ├── OpenSans-Light-webfont.svg │ ├── OpenSans-Light-webfont.woff │ ├── OpenSans-LightItalic-webfont.eot │ ├── OpenSans-LightItalic-webfont.svg │ ├── OpenSans-LightItalic-webfont.woff │ ├── OpenSans-Regular-webfont.eot │ ├── OpenSans-Regular-webfont.svg │ └── OpenSans-Regular-webfont.woff ├── global.html ├── index.html ├── overlay.js.html ├── scripts │ ├── linenumber.js │ └── prettify │ │ ├── Apache-License-2.0.txt │ │ ├── lang-css.js │ │ └── prettify.js ├── step.js.html ├── styles │ ├── jsdoc-default.css │ ├── prettify-jsdoc.css │ └── prettify-tomorrow.css ├── tooltip.js.html └── tutorial.js.html ├── example ├── chariot_screenshot.png ├── example-tooltip.css └── index.html ├── gulpfile.js ├── lib ├── chariot.js ├── constants.js ├── ie-shim.js ├── overlay.js ├── step.js ├── style.js ├── tooltip.js └── tutorial.js ├── package.json ├── release ├── chariot.css ├── chariot.js ├── chariot.min.css └── chariot.min.js ├── stylesheets ├── chariot.scss ├── example-tooltip.scss ├── overlay.scss └── tooltip.scss ├── test ├── chariot_test.js ├── libs │ └── style_test.js ├── step_test.js ├── test_helper.js ├── tooltip_test.js └── tutorial_test.js ├── testem.yml ├── webpack.config.babel.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"], 3 | "plugins": [ 4 | "add-module-exports" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.github/workflows/actions.yml: -------------------------------------------------------------------------------- 1 | name: repo-checks 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | main: 10 | name: nodejs 11 | runs-on: ubuntu-latest 12 | strategy: 13 | fail-fast: true 14 | matrix: 15 | task: ['jscs', 'sass', 'test', 'testem'] 16 | include: 17 | - task: jscs 18 | run: npm run jscs 19 | - task: sass 20 | run: npm rebuild node-sass 21 | - task: test 22 | run: node_modules/.bin/gulp compile-test 23 | - task: testem 24 | run: node_modules/.bin/testem ci 25 | steps: 26 | - uses: zendesk/checkout@v2 27 | - uses: zendesk/setup-node@v2.0.0 28 | with: 29 | node-version: '6' 30 | - name: ${{ matrix.task }} 31 | run: | 32 | npm install 33 | ${{ matrix.run }} 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /tmp 3 | /dist 4 | 5 | # dependencies 6 | /node_modules 7 | /bower_components 8 | 9 | # misc 10 | /.sass-cache 11 | npm-debug.log 12 | .DS_Store 13 | _site 14 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "excludeFiles": ["node_modules/**", "example/config.example.js", "package.json" , "dist/**"], 3 | "esnext": true, 4 | "additionalRules": [ "lib/jscs-rules/*.js" ], 5 | "disallowSpacesInsideArrayBrackets": "all", 6 | "disallowPaddingNewlinesInBlocks": true, 7 | "requireSpaceBeforeObjectValues": true, 8 | "requireCommaBeforeLineBreak": true, 9 | "requireBlocksOnNewline": 1, 10 | "disallowKeywordsOnNewLine": ["else"], 11 | "disallowNewlineBeforeBlockStatements": true, 12 | "requireSpacesInConditionalExpression": { 13 | "afterTest": true, 14 | "beforeConsequent": true, 15 | "afterConsequent": true, 16 | "beforeAlternate": true 17 | }, 18 | "disallowSpacesInCallExpression": true, 19 | "disallowEmptyBlocks": null, 20 | "requireSpacesInsideObjectBrackets": "all", 21 | "requireCurlyBraces": null, 22 | "requireLineFeedAtFileEnd": true, 23 | "disallowTrailingWhitespace": true, 24 | "disallowTrailingComma": true, 25 | "requireSpaceBeforeBlockStatements": true, 26 | "validateIndentation": 2, 27 | "validateParameterSeparator": ", ", 28 | "validateQuoteMarks": null, 29 | "requireSpaceBeforeKeywords": [ 30 | "else", 31 | "while", 32 | "catch" 33 | ], 34 | "requireSpaceAfterKeywords": [ 35 | "do", 36 | "for", 37 | "if", 38 | "else", 39 | "switch", 40 | "case", 41 | "try", 42 | "while", 43 | "with", 44 | "return" 45 | ], 46 | "requireSpaceBeforeBinaryOperators": [ 47 | "<", 48 | ">", 49 | "=", 50 | "+", 51 | "-", 52 | "/", 53 | "*", 54 | "==", 55 | "===", 56 | "!=", 57 | "!==" 58 | ], 59 | "requireSpaceAfterBinaryOperators": [ 60 | "<", 61 | ">", 62 | "=", 63 | ",", 64 | "+", 65 | "-", 66 | "/", 67 | "*", 68 | "==", 69 | "===", 70 | "!=", 71 | "!==" 72 | ], 73 | "requireSpaceBetweenArguments": true 74 | } 75 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 6 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 6 2 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Ronald Fung 2 | Herbert Siojo 3 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | * Copyright 2016 Zendesk Inc. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![repo-checks](https://github.com/zendesk/chariot-tooltips/workflows/repo-checks/badge.svg) 2 | ![example image](example/chariot_screenshot.png) 3 | 4 | # Chariot 5 | > Walkthroughs so easy, you might as well be flying in a chariot. 6 | 7 | A Javascript library for creating step-by-step tooltip tutorials, using a 8 | background overlay to bring focus to the elements you care about. 9 | 10 | # Demo 11 | 12 | Visit the [live demo](http://chariot.zendesk.com). 13 | 14 | # Motivation 15 | Tooltips are better at drawing focus to highlighted elements on a website 16 | when the background is dimmed out. 17 | 18 | Existing tooltip overlay solutions don't use overlay backgrounds, or if they 19 | do, they fail to consider when parent containers already have the CSS 20 | `z-index` property set. 21 | (A child element's `z-index` cannot override it's parent's `z-index`). 22 | 23 | # Features 24 | 25 | - Programmatic API and lifecycle callbacks 26 | - Overlay obscures the background, and your key elements are cloned over it 27 | - `z-index` is taken care of 28 | - Tooltips are animated into view 29 | - Browser support includes every sane browser and IE9+ 30 | 31 | # Usage 32 | 33 | `chariot.js` works in global, CommonJS and AMD contexts. 34 | 35 | Visit the [demo page](http://chariot.zendesk.com) for usage examples. 36 | 37 | # API 38 | 39 | If you're running the project locally, you can view the JSDoc-formatted 40 | documentation at 41 | [http://localhost:8080/docs/global.html](http://localhost:8080/docs/global.html). 42 | 43 | Or go to the example site: http://chariot.zendesk.com/docs 44 | 45 | # Development 46 | Checkout the `development` branch. 47 | git checkout development 48 | 49 | Install node packages. 50 | 51 | yarn install # if you use yarn, otherwise 52 | npm install 53 | 54 | Install the gulp cli. 55 | 56 | npm install -g gulp 57 | 58 | To run a webserver: 59 | 60 | gulp connect 61 | 62 | This will start a simple server that serves `index.html` at 63 | [http://localhost:8080/example/index.html](http://localhost:8080/example/index.html), 64 | and loads `chariot.js` onto the page. 65 | The task will also watch your changes and reloads the page as the files are updated. 66 | 67 | Run the following style-checker before pushing your branch. 68 | 69 | gulp style 70 | 71 | To automatically fix the style errors: 72 | 73 | gulp style-fix 74 | 75 | To update the generated docs: 76 | 77 | gulp js-doc 78 | 79 | # Testing 80 | 81 | ## Command Line 82 | To run test in command line, run: 83 | 84 | gulp test 85 | 86 | ## Browser 87 | If you want to test the same test suite in multiple browsers, run: 88 | 89 | gulp testem 90 | 91 | The browsers to test can be configured in `testem.yml`, currently it is configured to test in all major browsers (Firefox, Safari, Chrome) and PhantomJS. 92 | 93 | # Build 94 | Run the following to build `chariot.js` into thd `/dist` directory. 95 | 96 | gulp 97 | 98 | *Do not check in the `dist` directory. Release on github will contain the tarballs with compiled js/css.* 99 | 100 | # Release 101 | 102 | When you have merge in all your changes from your branch. Run the following **IN MASTER**: 103 | 104 | gulp release 105 | 106 | This gulp task will 107 | 108 | 1. Bump version in package.json, bower.json 109 | 1. Auto-generate documentation with js-doc 110 | 1. Package release into the `release/` folder 111 | 1. Commit the version bump changes in package.json, bower.json 112 | 1. Push the bump changes 113 | 1. Tag with the new version 114 | 115 | After releasing, update the relevant files in your project which uses ChariotJS. 116 | Update version in bower/npm, or copy release/chariot.[min.]js, 117 | release/chariot.[min.]css into your project's `vendor/` folder. 118 | 119 | # Copyright and License 120 | 121 | Copyright 2016, Zendesk Inc. Licensed under the Apache License Version 2.0, http://www.apache.org/licenses/LICENSE-2.0 122 | 123 | 124 | -------------------------------------------------------------------------------- /assets/images/agent@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zendesk/chariot-tooltips/0650103b2074a48f933ea21846606352da0fa3b7/assets/images/agent@2x.png -------------------------------------------------------------------------------- /assets/images/chat@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zendesk/chariot-tooltips/0650103b2074a48f933ea21846606352da0fa3b7/assets/images/chat@2x.png -------------------------------------------------------------------------------- /assets/images/customer@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zendesk/chariot-tooltips/0650103b2074a48f933ea21846606352da0fa3b7/assets/images/customer@2x.png -------------------------------------------------------------------------------- /assets/images/progress@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zendesk/chariot-tooltips/0650103b2074a48f933ea21846606352da0fa3b7/assets/images/progress@2x.png -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chariot-tooltips", 3 | "homepage": "chariot.zendesk.com", 4 | "authors": [ 5 | "Ronald Fung ", 6 | "Herbert Siojo " 7 | ], 8 | "description": "A library for creating beautifully simple onboarding tutorials.", 9 | "main": [ 10 | "release/chariot.js", 11 | "release/chariot.css" 12 | ], 13 | "dependencies": { 14 | "jquery": "2.0.0" 15 | }, 16 | "moduleType": [ 17 | "amd", 18 | "node", 19 | "es6", 20 | "globals" 21 | ], 22 | "keywords": [ 23 | "chariot", 24 | "onboarding", 25 | "tutorial", 26 | "tooltips" 27 | ], 28 | "ignore": [ 29 | "node_modules", 30 | "bower_components", 31 | "test", 32 | "tests", 33 | "**/.*" 34 | ], 35 | "repository": { 36 | "type": "git", 37 | "url": "git://github.com:zendesk/chariot-tooltips.git" 38 | }, 39 | "license": "LICENSE.txt" 40 | } 41 | -------------------------------------------------------------------------------- /docs/Overlay.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Class: Overlay 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Class: Overlay

21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | 29 |
30 | 31 |

Overlay

32 | 33 | 34 |
35 | 36 |
37 |
38 | 39 | 40 | 41 | 42 | 43 |

new Overlay()

44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 |
62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 |
Source:
89 |
92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 |
100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 |
118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 |

Methods

133 | 134 | 135 | 136 | 137 | 138 | 139 |

focusOnElement()

140 | 141 | 142 | 143 | 144 | 145 |
146 | Focuses on an element by resizing a transparent overlay to match its 147 | dimensions and changes the borders to be colored to obscure the main UI. 148 | This method is involved in the "transparent overlay" strategy. 149 |
150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 |
164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 |
Source:
191 |
194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 |
202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 |

showBackgroundOverlay()

224 | 225 | 226 | 227 | 228 | 229 |
230 | Shows a background overlay to obscure the main interface, and acts as the 231 | background for the cloned elements involved in the tutorial. 232 | This method is involved in the "clone element" strategy. 233 |
234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 |
248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 |
Source:
275 |
278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 |
286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 |

showTransparentOverlay()

308 | 309 | 310 | 311 | 312 | 313 |
314 | Shows a transparent overlay to prevent user from interacting with cloned 315 | elements. 316 |
317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 |
331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 |
Source:
358 |
361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 |
369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 |
392 | 393 |
394 | 395 | 396 | 397 | 398 |
399 | 400 | 403 | 404 |
405 | 406 |
407 | Documentation generated by JSDoc 3.4.3 on Fri Feb 10 2017 14:56:40 GMT+1100 (AEDT) 408 |
409 | 410 | 411 | 412 | 413 | -------------------------------------------------------------------------------- /docs/Step.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Class: Step 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Class: Step

21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | 29 |
30 | 31 |

Step

32 | 33 | 34 |
35 | 36 |
37 |
38 | 39 | 40 | 41 | 42 | 43 |

new Step(config, index, tutorial, overlay, delegateopt)

44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 |
Parameters:
58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 95 | 96 | 97 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 126 | 127 | 128 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 157 | 158 | 159 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 188 | 189 | 190 | 197 | 198 | 199 | 200 | 201 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 220 | 221 | 222 | 231 | 232 | 233 | 234 | 235 | 237 | 238 | 239 | 240 | 241 |
NameTypeAttributesDescription
config 88 | 89 | 90 | StepConfiguration 91 | 92 | 93 | 94 | 98 | 99 | 100 | 101 | 102 | 103 | The configuration for this step
index 119 | 120 | 121 | integer 122 | 123 | 124 | 125 | 129 | 130 | 131 | 132 | 133 | 134 | The index of this step within the current tutorial
tutorial 150 | 151 | 152 | Tutorial 153 | 154 | 155 | 156 | 160 | 161 | 162 | 163 | 164 | 165 | The Tutorial object corresponding to this Step
overlay 181 | 182 | 183 | Overlay 184 | 185 | 186 | 187 | 191 | 192 | 193 | 194 | 195 | 196 | The Overlay object displayed along with this 202 | Step
delegate 213 | 214 | 215 | ChariotDelegate 216 | 217 | 218 | 219 | 223 | 224 | <optional>
225 | 226 | 227 | 228 | 229 | 230 |
An optional delegate that responds to 236 | lifecycle callbacks
242 | 243 | 244 | 245 | 246 | 247 | 248 |
249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 |
Source:
276 |
279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 |
287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 |
305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 |
324 | 325 |
326 | 327 | 328 | 329 | 330 |
331 | 332 | 335 | 336 |
337 | 338 |
339 | Documentation generated by JSDoc 3.4.3 on Fri Feb 10 2017 14:56:40 GMT+1100 (AEDT) 340 |
341 | 342 | 343 | 344 | 345 | -------------------------------------------------------------------------------- /docs/Tooltip.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Class: Tooltip 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Class: Tooltip

21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | 29 |
30 | 31 |

Tooltip

32 | 33 | 34 |
35 | 36 |
37 |
38 | 39 | 40 | 41 | 42 | 43 |

new Tooltip(config, step, tutorial)

44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 |
Parameters:
58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 139 | 140 | 141 | 142 | 143 | 144 | 146 | 147 | 148 | 149 | 150 |
NameTypeDescription
config 86 | 87 | 88 | TooltipConfiguration 89 | 90 | 91 | 92 | The configuration for this tooltip
step 109 | 110 | 111 | Step 112 | 113 | 114 | 115 | The Step object displayed along with this tooltip
tutorial 132 | 133 | 134 | Tutorial 135 | 136 | 137 | 138 | The Tutorial object corresponding to this 145 | Tooltip
151 | 152 | 153 | 154 | 155 | 156 | 157 |
158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 |
Source:
185 |
188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 |
196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 |
214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 |
233 | 234 |
235 | 236 | 237 | 238 | 239 |
240 | 241 | 244 | 245 |
246 | 247 |
248 | Documentation generated by JSDoc 3.4.3 on Fri Feb 10 2017 14:56:40 GMT+1100 (AEDT) 249 |
250 | 251 | 252 | 253 | 254 | -------------------------------------------------------------------------------- /docs/chariot.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Source: chariot.js 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Source: chariot.js

21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
/* global
 30 | history, location
 31 | */
 32 | 
 33 | /* Please refer to example page to see how a typical configuration is structured */
 34 | 
 35 | import Tutorial from './tutorial';
 36 | 
 37 | require('./ie-shim');
 38 | 
 39 | let initialState = true;
 40 | 
 41 | class Chariot {
 42 |   /**
 43 |    * The master Chariot configuration dictionary can consist of multiple
 44 |    *  tutorial configurations.
 45 |    * @typedef ChariotConfiguration
 46 |    * @property {Object.<string, TutorialConfig>} config - The main configuration
 47 |    *  containing all tutorials.
 48 |    *
 49 |    */
 50 | 
 51 |   /**
 52 |    * The delegate optionally responds to lifecycle callbacks from Chariot.
 53 |    * @typedef ChariotDelegate
 54 |    * @property {Object} delegate - The object that responds to the
 55 |    *  following lifecycle callbacks.
 56 |    *
 57 |    * <ol>
 58 |    *   <li>willBeginTutorial</li>
 59 |    *   <li>The following are repeated for each step.</li>
 60 |    *   <ol>
 61 |    *     <li>willBeginStep</li>
 62 |    *     <li>willRenderOverlay</li>
 63 |    *     <li>didShowOverlay</li>
 64 |    *     <li>willRenderTooltip</li>
 65 |    *     <li>didRenderTooltip</li>
 66 |    *     <li>didFinishStep</li>
 67 |    *   </ol>
 68 |    *   <li>didFinishTutorial</li>
 69 |    * </ol>
 70 |    */
 71 | 
 72 |   /**
 73 |    * Called once before a tutorial begins.
 74 |    * @callback willBeginTutorial
 75 |    * @param {Tutorial} tutorial - The Tutorial object
 76 |    */
 77 | 
 78 |   /**
 79 |    * Called once after a tutorial is finished.
 80 |    * @callback didFinishTutorial tutorial
 81 |    * @param {Tutorial} tutorial - The Tutorial object
 82 |    * @param {boolean} forced - Indicates whether tutorial was forced to end
 83 |    */
 84 | 
 85 |   /**
 86 |    * Called once before each step begins.
 87 |    * Return a promise here if you have async callbacks you want resolved before
 88 |    * continuing.
 89 |    * @callback willBeginStep
 90 |    * @param {Step} step - The current Step object
 91 |    * @param {int} stepIndex - Index of current Step
 92 |    * @param {Tutorial} tutorial - The Tutorial object corresponding to this Step
 93 |    * @returns {Promise} [promise] Return a promise if you have async callbacks
 94 |    *   that must be resolved before continuing.
 95 |    */
 96 | 
 97 |   /**
 98 |    * Called once after each step is finished.
 99 |    * @callback didFinishStep
100 |    * @param {Step} step - The current Step object
101 |    * @param {int} stepIndex - Index of current Step
102 |    * @param {Tutorial} tutorial - The Tutorial object corresponding to this Step
103 |    * @returns {Promise} [promise] Return a promise if you have async callbacks
104 |    *   that must be resolved before continuing.
105 |    */
106 | 
107 |   /**
108 |    * Called once before each overlay is shown.
109 |    * @callback willShowOverlay
110 |    * @param {Overlay} overlay - The current Overlay object
111 |    * @param {int} stepIndex - Index of current Step
112 |    * @param {Tutorial} tutorial - The Tutorial object corresponding to this Step
113 |    * @returns {Promise} [promise] Return a promise if you have async callbacks
114 |    *   that must be resolved before continuing.
115 |    */
116 | 
117 |   /**
118 |    * Called once after each overlay is shown.
119 |    * @callback didShowOverlay
120 |    * @param {Overlay} overlay - The current Overlay object
121 |    * @param {int} stepIndex - Index of current Step
122 |    * @param {Tutorial} tutorial - The Tutorial object corresponding to this Step
123 |    * @returns {Promise} [promise] Return a promise if you have async callbacks
124 |    *   that must be resolved before continuing.
125 |    */
126 | 
127 |   /**
128 |    * Called once before each tooltip is rendered.
129 |    * @callback willRenderTooltip
130 |    * @param {Tooltip} tooltip - The current Tooltip object
131 |    * @param {int} stepIndex - Index of current Step
132 |    * @param {Tutorial} tutorial - The Tutorial object corresponding to this Step
133 |    * @returns {Promise} [promise] Return a promise if you have async callbacks
134 |    *   that must be resolved before continuing.
135 |    */
136 | 
137 |   /**
138 |    * Called once after each tooltip is rendered.
139 |    * @callback didRenderTooltip
140 |    * @param {Tooltip} tooltip - The current Tooltip object
141 |    * @param {int} stepIndex - Index of current Step
142 |    * @param {Tutorial} tutorial - The Tutorial object corresponding to this Step
143 |    * @returns {Promise} [promise] Return a promise if you have async callbacks
144 |    *   that must be resolved before continuing.
145 |    */
146 | 
147 |   /**
148 |    * @constructor
149 |    * @param {ChariotConfiguration} config - The main configuration for all
150 |    *  tutorials
151 |    * @param {ChariotDelegate} [delegate] - An optional delegate that responds to
152 |    *  lifecycle callbacks
153 |    */
154 |   constructor(config, delegate) {
155 |     this.config = config;
156 |     this.delegate = delegate;
157 |     this.tutorials = {};
158 |     this._readConfig(config);
159 |   }
160 | 
161 |   /**
162 |    * Sets the chariot delegate.
163 |    * @param {ChariotDelegate} [delegate] - An object that responds to
164 |    *  lifecycle callbacks
165 |    */
166 |   setDelegate(delegate) {
167 |     this.delegate = delegate;
168 |   }
169 | 
170 |   /**
171 |    * Starts a tutorial with the given name.
172 |    * Won't start a tutorial if one is currently running.
173 |    * @param {string} name - Name of the tutorial to start
174 |    * @returns {Tutorial} tutorial - The Tutorial object, or undefined if
175 |    *  another tutorial is currently active.
176 |    */
177 |   startTutorial(name) {
178 |     if (this.currentTutorial()) {
179 |       return;
180 |     }
181 |     const tutorial = this.tutorials[name];
182 |     tutorial.start();
183 |     return tutorial;
184 |   }
185 | 
186 |   /**
187 |    * Ends the current tutorial.
188 |    * @returns {undefined}
189 |    */
190 |   endTutorial() {
191 |     const tutorial = this.currentTutorial();
192 |     tutorial.end(true);
193 |   }
194 | 
195 |   /**
196 |    * Returns the current tutorial, if any.
197 |    * @returns {Tutorial} tutorial - The current tutorial, or null if none active
198 |    */
199 |   currentTutorial() {
200 |     for (let tutorialName in this.tutorials) {
201 |       let tutorial = this.tutorials[tutorialName];
202 |       if (tutorial.isActive()) return tutorial;
203 |     }
204 |   }
205 | 
206 |   /**
207 |    * Static method for creating a Tutorial object without needing to instantiate
208 |    * chariot with a large configuration and named tutorials.
209 |    * @param {TutorialConfiguration} config - The tutorial configuration
210 |    * @param {ChariotDelegate} [delegate] - An optional delegate that responds to
211 |    *  lifecycle callbacks
212 |    */
213 |   static createTutorial(config, delegate) {
214 |     return new Tutorial(config, '', delegate);
215 |   }
216 | 
217 |   /**
218 |    * Static method for creating and starting a Tutorial object without needing
219 |    * to instantiate chariot with a large configuration and named tutorials.
220 |    * @param {TutorialConfiguration} config - The tutorial configuration
221 |    * @param {ChariotDelegate} [delegate] - An optional delegate that responds to
222 |    *  lifecycle callbacks
223 |    */
224 |   static startTutorial(config, delegate) {
225 |     const tutorial = this.createTutorial(config, delegate);
226 |     tutorial.start();
227 |     return tutorial;
228 |   }
229 | 
230 |   toString() {
231 |     return `[Chariot - config: ${this.config}, tutorials: {this.tutorials}]`;
232 |   }
233 | 
234 |   //// PRIVATE
235 | 
236 |   _readConfig(config) {
237 |     if (!config || typeof config !== 'object') {
238 |       throw new Error(`Config must contains a tutorials hash.\n${this}`);
239 |     }
240 |     for (let tutorialName in config) {
241 |       this.tutorials[tutorialName] = new Tutorial(
242 |         config[tutorialName], tutorialName, this.delegate);
243 |     }
244 |   }
245 | }
246 | 
247 | export
248 | default Chariot;
249 | 
250 |
251 |
252 | 253 | 254 | 255 | 256 |
257 | 258 | 261 | 262 |
263 | 264 |
265 | Documentation generated by JSDoc 3.4.3 on Fri Feb 10 2017 14:56:40 GMT+1100 (AEDT) 266 |
267 | 268 | 269 | 270 | 271 | 272 | -------------------------------------------------------------------------------- /docs/config.example.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Source: config.example.js 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Source: config.example.js

21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
/**
 30 |  * The master Chariot configuration dictionary can consist of multiple tutorial
 31 |  *  configurations.
 32 |  * @typedef ChariotConfiguration
 33 |  * @property {Object.<string, TutorialConfig>} config - The main configuration
 34 |  *  containing all tutorials.
 35 |  *
 36 |  */
 37 | 
 38 | /** The tutorial configuration is where the steps of a tutorial are specified,
 39 |  * and also allows customization of the overlay style. A complete callback is
 40 |  * available for end of tutorial cleanup.
 41 |  *
 42 |  * @typedef TutorialConfiguration
 43 |  * @property {boolean} [shouldOverlay=true] - Setting to false will disable the
 44 |  * overlay that normally appears over the page and behind the tooltips.
 45 |  * @property {string} [overlayColor='rgba(255,255,255,0.7)'] - Overlay CSS color
 46 |  * @property {StepConfiguration[]} steps - An array of step configurations (see below).
 47 |  * @property {Tutorial-completeCallback} [complete] - Callback that is called
 48 |  * once the tutorial has gone through all steps.
 49 |  *
 50 |  */
 51 | 
 52 | /**
 53 |  * Callback is called after the tutorial has finished all steps.
 54 |  * @callback Tutorial-completeCallback
 55 |  */
 56 | 
 57 | /** The step configuration is where you specify which elements of the page will
 58 |  * be cloned and placed over the overlay.
 59 |  *
 60 |  * @typedef StepConfiguration
 61 |  * @property {TooltipConfiguration} tooltip - Tooltip configuration.
 62 |  * @property {Object.<string, string>} [selectors] - Contains arbitrarily-named
 63 |  *  keys with CSS selector values. These keys can be referenced from
 64 |  *  TooltipConfiguration.anchorElement.
 65 |  *  Note: Specifying a selector that lives within another specified selector will
 66 |  *  result in unpredictable behavior.
 67 |  * @property {Step-beforeCallback} [before] - Callback called once before step
 68 |  *  is rendered.
 69 |  * @property {Step-afterCallback} [after] - Callback called once after step is
 70 |  *  rendered.
 71 |  * @property {boolean} [highlightTransparentRegion=false] - Setting to true will
 72 |  *  result in elements being highlighted with a transparent square region,
 73 |  *  instead of the individual selected element. This is useful if you want to
 74 |  *  enable spotlight-like transitions between steps.
 75 |  *  Note: This value is ignored when selectors contain multiple keys.
 76 |  */
 77 | 
 78 | /**
 79 |  * Callback called once before step is rendered.
 80 |  * @callback Step-beforeCallback
 81 |  */
 82 | 
 83 | /**
 84 |  * Callback called once after step is rendered.
 85 |  * @callback Step-afterCallback
 86 |  */
 87 | 
 88 | /** The tooltip configuration allows you to specify which anchor element will
 89 |  * be pointed to by the tooltip, along with its position. A default template is
 90 |  * provided, which can be configured
 91 |  *
 92 |  * @typedef TooltipConfiguration
 93 |  * @property {string} anchorElement - Contains either (1) a key from
 94 |  *  StepConfiguration.selectors above, or (2) a CSS selector.
 95 |  * @property {string} position - Relatively positions the tooltip to the anchor
 96 |  *   element. Possible values: 'top' | 'left' | 'bottom' | 'right'
 97 |  * @property {number} [xOffset] - Value in pixels to offset the x-coordinate of
 98 |  *  the tooltip.
 99 |  * @property {number} [yOffset] - Value in pixels to offset the y-coordinate of
100 |  *  the tooltip.
101 |  * @property {Tooltip-renderCallback} [render] - (TODO) Renders a custom template,
102 |  *  thereby ignoring all other properties below.
103 |  * @property {string} [iconUrl] - Path to an image displayed above the title.
104 |  * @property {string} [title] - The title text of a toolip.
105 |  * @property {string|function} [body] - The body text of a tooltip, or a callback
106 |  *  that returns custom HTML.
107 |  * @property {string} [cta] - The text contained within the button.
108 |  * @property {Object} [attr] - HTML attributes to set on the tooltip.
109 |  * @property {Number} [arrowLength] - Distance between arrow tip and edge of
110 |  *  tooltip, not including border.  A value of 0 removes the arrow.
111 |  * @property {Tooltip-subtextCallback} [subtext] - Callback that returns subtext
112 |  *  content.
113 |  *
114 |  */
115 | 
116 | /**
117 |  * A function that provides step information and returns subtext content.
118 |  * @callback Tooltip-renderCallback
119 |  * @param {number} currentStep - The current step number
120 |  * @param {number} totalSteps - The total # of steps
121 |  * @returns {string} markup - The HTML markup that represents the subtext
122 |  */
123 | 
124 | /**
125 |  * A function that provides step information and returns subtext content.
126 |  * @callback Tooltip-subtextCallback
127 |  * @param {number} currentStep - The current step number
128 |  * @param {number} totalSteps - The total # of steps
129 |  * @returns {string} markup - The HTML markup that represents the subtext
130 |  */
131 | 
132 | var OnboardingConfig = {
133 |   ticketing: {
134 |     steps: [
135 |       {
136 |         selectors: {
137 |           assignee: "#input",
138 |           assignLabel: "#label"
139 |         },
140 |         tooltip: {
141 |           position: 'right',
142 |           title: 'Title',
143 |           text: 'Some text',
144 |           xOffset: '10',
145 |           yOffset: '10',
146 |           anchorElement: "assignee",
147 |           iconUrl: '/assets/whatever',
148 |           cta: 'Next',
149 |           subtext: function(currentStep, totalSteps) {
150 |             return `${currentStep} of ${totalSteps}`;
151 |           },
152 |           attr: { 'id': 'know_your_customer' }
153 |         },
154 |         before: function() {
155 |           // any arbitrary code to run before showing this step (after the timeout between steps)
156 |           // eg. populate an image outside of the #elem
157 |         }
158 |       },
159 |       {
160 |         selectors: {
161 |           assignee: "#input",
162 |           assignLabel: "#label"
163 |         },
164 |         tooltip: {
165 |           title: 'Title',
166 |           position: 'right',
167 |           text: 'Some text',
168 |           anchorElement: "assignee",
169 |           cta: 'Done',
170 |           arrowLength: 30
171 |         },
172 |         before: function() {
173 |           // any arbitrary code to run before showing this step (after the timeout between steps)
174 |           // eg. populate an image outside of the #elem
175 |         }
176 |       }
177 |     ],
178 |     complete: function() {
179 |     },
180 |     shouldOverlay: { opacity: 0.7, background: 'white' }
181 |   }
182 | };
183 | 
184 |
185 |
186 | 187 | 188 | 189 | 190 |
191 | 192 | 195 | 196 |
197 | 198 |
199 | Documentation generated by JSDoc 3.3.2 on Mon Aug 03 2015 09:59:14 GMT-0700 (PDT) 200 |
201 | 202 | 203 | 204 | 205 | 206 | -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Bold-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zendesk/chariot-tooltips/0650103b2074a48f933ea21846606352da0fa3b7/docs/fonts/OpenSans-Bold-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Bold-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zendesk/chariot-tooltips/0650103b2074a48f933ea21846606352da0fa3b7/docs/fonts/OpenSans-Bold-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/OpenSans-BoldItalic-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zendesk/chariot-tooltips/0650103b2074a48f933ea21846606352da0fa3b7/docs/fonts/OpenSans-BoldItalic-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-BoldItalic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zendesk/chariot-tooltips/0650103b2074a48f933ea21846606352da0fa3b7/docs/fonts/OpenSans-BoldItalic-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Italic-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zendesk/chariot-tooltips/0650103b2074a48f933ea21846606352da0fa3b7/docs/fonts/OpenSans-Italic-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Italic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zendesk/chariot-tooltips/0650103b2074a48f933ea21846606352da0fa3b7/docs/fonts/OpenSans-Italic-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Light-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zendesk/chariot-tooltips/0650103b2074a48f933ea21846606352da0fa3b7/docs/fonts/OpenSans-Light-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Light-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zendesk/chariot-tooltips/0650103b2074a48f933ea21846606352da0fa3b7/docs/fonts/OpenSans-Light-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/OpenSans-LightItalic-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zendesk/chariot-tooltips/0650103b2074a48f933ea21846606352da0fa3b7/docs/fonts/OpenSans-LightItalic-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-LightItalic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zendesk/chariot-tooltips/0650103b2074a48f933ea21846606352da0fa3b7/docs/fonts/OpenSans-LightItalic-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Regular-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zendesk/chariot-tooltips/0650103b2074a48f933ea21846606352da0fa3b7/docs/fonts/OpenSans-Regular-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zendesk/chariot-tooltips/0650103b2074a48f933ea21846606352da0fa3b7/docs/fonts/OpenSans-Regular-webfont.woff -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Home 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Home

21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |

30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 |
51 | 52 | 55 | 56 |
57 | 58 |
59 | Documentation generated by JSDoc 3.4.3 on Fri Feb 10 2017 14:56:40 GMT+1100 (AEDT) 60 |
61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /docs/overlay.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Source: overlay.js 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Source: overlay.js

21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
import Constants from './constants';
 30 | 
 31 | class Overlay {
 32 | 
 33 |   /**
 34 |    * @constructor
 35 |    *
 36 |    */
 37 |   constructor(config) {
 38 |     this.shouldOverlay = config.shouldOverlay === undefined ? true : config.shouldOverlay;
 39 |     this.overlayColor = config.overlayColor || 'rgba(255,255,255,0.8)';
 40 |     this.useTransparentOverlayStrategy = !!config.useTransparentOverlayStrategy;
 41 |     this._resizeHandler = null;
 42 |     this.disableCloneInteraction = config.disableCloneInteraction === undefined ? true : config.disableCloneInteraction;
 43 |   }
 44 | 
 45 |   isVisible() {
 46 |     return this.shouldOverlay === false;
 47 |   }
 48 | 
 49 |   render() {
 50 |     if (this.isVisible()) return;
 51 | 
 52 |     this.$document = $(document);
 53 |     let $body = $('body');
 54 |     let $overlay = this._createOverlay();
 55 |     $body.append($overlay);
 56 |     this.$overlay = $overlay;
 57 | 
 58 |     if (this.disableCloneInteraction) {
 59 |       const $transparentOverlay = this._createTransparentOverlay();
 60 |       $body.append($transparentOverlay);
 61 |       this.$transparentOverlay = $transparentOverlay;
 62 |     }
 63 |   }
 64 | 
 65 |   isTransparentOverlayStrategy() {
 66 |     return this.useTransparentOverlayStrategy;
 67 |   }
 68 | 
 69 |   // The following 2 methods are part of the "clone element" strategy
 70 | 
 71 |   /**
 72 |    * Shows a background overlay to obscure the main interface, and acts as the
 73 |    * background for the cloned elements involved in the tutorial.
 74 |    * This method is involved in the "clone element" strategy.
 75 |    */
 76 |   showBackgroundOverlay() {
 77 |     // Remove the resize handler that might exist from focusOnElement
 78 |     // (Note: take care to not call this after cloning elements, because they
 79 |     //  have their own window resize handlers)
 80 |     let $window = $(window);
 81 | 
 82 |     this.$overlay.css({
 83 |       background: this.overlayColor,
 84 |       border: 'none'
 85 |     });
 86 | 
 87 |     this._resizeOverlayToFullScreen();
 88 |     this._resizeHandler = this._resizeOverlayToFullScreen.bind(this);
 89 |   }
 90 | 
 91 |   /**
 92 |    * Shows a transparent overlay to prevent user from interacting with cloned
 93 |    * elements.
 94 |    */
 95 |   showTransparentOverlay() {
 96 |     this.$transparentOverlay.show();
 97 |   }
 98 | 
 99 |   /**
100 |    * Focuses on an element by resizing a transparent overlay to match its
101 |    * dimensions and changes the borders to be colored to obscure the main UI.
102 |    * This method is involved in the "transparent overlay" strategy.
103 |    */
104 |   focusOnElement($element) {
105 |     // Hide overlay from showTransparentOverlay
106 |     this.$transparentOverlay.hide();
107 | 
108 |     this._resizeOverlayToElement($element);
109 |     this._resizeHandler = this._resizeOverlayToElement.bind(this, $element);
110 |   }
111 | 
112 |   resize() {
113 |     this._resizeHandler();
114 |   }
115 | 
116 |   tearDown() {
117 |     this.$overlay.remove();
118 |     if (this.$transparentOverlay) {
119 |       this.$transparentOverlay.remove();
120 |     }
121 |   }
122 | 
123 |   toString() {
124 |     return `[Overlay - shouldOverlay: ${this.shouldOverlay}, ` +
125 |       `overlayColor: ${this.overlayColor}]`;
126 |   }
127 | 
128 |   //// PRIVATE
129 | 
130 |   _createOverlay() {
131 |     let $overlay = $("<div class='chariot-overlay'></div>");
132 |     $overlay.css({ 'z-index': Constants.OVERLAY_Z_INDEX });
133 |     return $overlay;
134 |   }
135 | 
136 |   _createTransparentOverlay() {
137 |     let $transparentOverlay = $("<div class='chariot-transparent-overlay'></div>");
138 |     $transparentOverlay.css({
139 |       'z-index': Constants.CLONE_Z_INDEX + 1,
140 |       width: this._documentWidth(),
141 |       height: this._documentHeight()
142 |     });
143 |     return $transparentOverlay;
144 |   }
145 | 
146 |   // Used for clone element strategy
147 |   _resizeOverlayToFullScreen() {
148 |     this.$overlay.css({
149 |       width: this._documentWidth(),
150 |       height: this._documentHeight()
151 |     });
152 |   }
153 | 
154 |   _documentWidth() {
155 |     const body = document.body;
156 |     const html = document.documentElement;
157 |     return Math.max(html.scrollWidth, html.offsetWidth, html.clientWidth,
158 |       body.scrollWidth, body.offsetWidth);
159 |   }
160 | 
161 |   _documentHeight() {
162 |     const body = document.body;
163 |     const html = document.documentElement;
164 |     return Math.max(html.scrollHeight, html.offsetHeight, html.clientHeight,
165 |       body.scrollHeight, body.offsetHeight);
166 |   }
167 | 
168 |   // Used for transparent overlay strategy
169 |   _resizeOverlayToElement($element) {
170 |     // First position the overlay
171 |     let offset = $element.offset();
172 | 
173 |     // Then resize it
174 |     let borderStyles = `solid ${this.overlayColor}`;
175 |     let $document = this.$document;
176 |     let docWidth = $document.outerWidth();
177 |     let docHeight = $document.outerHeight();
178 | 
179 |     let width = $element.outerWidth();
180 |     let height = $element.outerHeight();
181 | 
182 |     let leftWidth = offset.left;
183 |     let rightWidth = docWidth - (offset.left + width);
184 |     let topWidth = offset.top;
185 |     let bottomWidth = docHeight - (offset.top + height);
186 | 
187 |     this.$overlay.css({
188 |       background: 'transparent',
189 |       width, height,
190 |       'border-left': `${leftWidth}px ${borderStyles}`,
191 |       'border-top': `${topWidth}px ${borderStyles}`,
192 |       'border-right': `${rightWidth}px ${borderStyles}`,
193 |       'border-bottom': `${bottomWidth}px ${borderStyles}`
194 |     });
195 |   }
196 | }
197 | 
198 | export default Overlay;
199 | 
200 |
201 |
202 | 203 | 204 | 205 | 206 |
207 | 208 | 211 | 212 |
213 | 214 |
215 | Documentation generated by JSDoc 3.4.3 on Fri Feb 10 2017 14:56:40 GMT+1100 (AEDT) 216 |
217 | 218 | 219 | 220 | 221 | 222 | -------------------------------------------------------------------------------- /docs/scripts/linenumber.js: -------------------------------------------------------------------------------- 1 | /*global document */ 2 | (function() { 3 | var source = document.getElementsByClassName('prettyprint source linenums'); 4 | var i = 0; 5 | var lineNumber = 0; 6 | var lineId; 7 | var lines; 8 | var totalLines; 9 | var anchorHash; 10 | 11 | if (source && source[0]) { 12 | anchorHash = document.location.hash.substring(1); 13 | lines = source[0].getElementsByTagName('li'); 14 | totalLines = lines.length; 15 | 16 | for (; i < totalLines; i++) { 17 | lineNumber++; 18 | lineId = 'line' + lineNumber; 19 | lines[i].id = lineId; 20 | if (lineId === anchorHash) { 21 | lines[i].className += ' selected'; 22 | } 23 | } 24 | } 25 | })(); 26 | -------------------------------------------------------------------------------- /docs/scripts/prettify/Apache-License-2.0.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /docs/scripts/prettify/lang-css.js: -------------------------------------------------------------------------------- 1 | PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\f\r ]+/,null," \t\r\n "]],[["str",/^"(?:[^\n\f\r"\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*"/,null],["str",/^'(?:[^\n\f\r'\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*'/,null],["lang-css-str",/^url\(([^"')]*)\)/i],["kwd",/^(?:url|rgb|!important|@import|@page|@media|@charset|inherit)(?=[^\w-]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*)\s*:/i],["com",/^\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//],["com", 2 | /^(?:<\!--|--\>)/],["lit",/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],["lit",/^#[\da-f]{3,6}/i],["pln",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i],["pun",/^[^\s\w"']+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[["kwd",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[["str",/^[^"')]+/]]),["css-str"]); 3 | -------------------------------------------------------------------------------- /docs/scripts/prettify/prettify.js: -------------------------------------------------------------------------------- 1 | var q=null;window.PR_SHOULD_USE_CONTINUATION=!0; 2 | (function(){function L(a){function m(a){var f=a.charCodeAt(0);if(f!==92)return f;var b=a.charAt(1);return(f=r[b])?f:"0"<=b&&b<="7"?parseInt(a.substring(1),8):b==="u"||b==="x"?parseInt(a.substring(2),16):a.charCodeAt(1)}function e(a){if(a<32)return(a<16?"\\x0":"\\x")+a.toString(16);a=String.fromCharCode(a);if(a==="\\"||a==="-"||a==="["||a==="]")a="\\"+a;return a}function h(a){for(var f=a.substring(1,a.length-1).match(/\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\[0-3][0-7]{0,2}|\\[0-7]{1,2}|\\[\S\s]|[^\\]/g),a= 3 | [],b=[],o=f[0]==="^",c=o?1:0,i=f.length;c122||(d<65||j>90||b.push([Math.max(65,j)|32,Math.min(d,90)|32]),d<97||j>122||b.push([Math.max(97,j)&-33,Math.min(d,122)&-33]))}}b.sort(function(a,f){return a[0]-f[0]||f[1]-a[1]});f=[];j=[NaN,NaN];for(c=0;ci[0]&&(i[1]+1>i[0]&&b.push("-"),b.push(e(i[1])));b.push("]");return b.join("")}function y(a){for(var f=a.source.match(/\[(?:[^\\\]]|\\[\S\s])*]|\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\\d+|\\[^\dux]|\(\?[!:=]|[()^]|[^()[\\^]+/g),b=f.length,d=[],c=0,i=0;c=2&&a==="["?f[c]=h(j):a!=="\\"&&(f[c]=j.replace(/[A-Za-z]/g,function(a){a=a.charCodeAt(0);return"["+String.fromCharCode(a&-33,a|32)+"]"}));return f.join("")}for(var t=0,s=!1,l=!1,p=0,d=a.length;p=5&&"lang-"===b.substring(0,5))&&!(o&&typeof o[1]==="string"))c=!1,b="src";c||(r[f]=b)}i=d;d+=f.length;if(c){c=o[1];var j=f.indexOf(c),k=j+c.length;o[2]&&(k=f.length-o[2].length,j=k-c.length);b=b.substring(5);B(l+i,f.substring(0,j),e,p);B(l+i+j,c,C(b,c),p);B(l+i+k,f.substring(k),e,p)}else p.push(l+i,b)}a.e=p}var h={},y;(function(){for(var e=a.concat(m), 9 | l=[],p={},d=0,g=e.length;d=0;)h[n.charAt(k)]=r;r=r[1];n=""+r;p.hasOwnProperty(n)||(l.push(r),p[n]=q)}l.push(/[\S\s]/);y=L(l)})();var t=m.length;return e}function u(a){var m=[],e=[];a.tripleQuotedStrings?m.push(["str",/^(?:'''(?:[^'\\]|\\[\S\s]|''?(?=[^']))*(?:'''|$)|"""(?:[^"\\]|\\[\S\s]|""?(?=[^"]))*(?:"""|$)|'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$))/,q,"'\""]):a.multiLineStrings?m.push(["str",/^(?:'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$)|`(?:[^\\`]|\\[\S\s])*(?:`|$))/, 10 | q,"'\"`"]):m.push(["str",/^(?:'(?:[^\n\r'\\]|\\.)*(?:'|$)|"(?:[^\n\r"\\]|\\.)*(?:"|$))/,q,"\"'"]);a.verbatimStrings&&e.push(["str",/^@"(?:[^"]|"")*(?:"|$)/,q]);var h=a.hashComments;h&&(a.cStyleComments?(h>1?m.push(["com",/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,q,"#"]):m.push(["com",/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\n\r]*)/,q,"#"]),e.push(["str",/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,q])):m.push(["com",/^#[^\n\r]*/, 11 | q,"#"]));a.cStyleComments&&(e.push(["com",/^\/\/[^\n\r]*/,q]),e.push(["com",/^\/\*[\S\s]*?(?:\*\/|$)/,q]));a.regexLiterals&&e.push(["lang-regex",/^(?:^^\.?|[!+-]|!=|!==|#|%|%=|&|&&|&&=|&=|\(|\*|\*=|\+=|,|-=|->|\/|\/=|:|::|;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|[?@[^]|\^=|\^\^|\^\^=|{|\||\|=|\|\||\|\|=|~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\s*(\/(?=[^*/])(?:[^/[\\]|\\[\S\s]|\[(?:[^\\\]]|\\[\S\s])*(?:]|$))+\/)/]);(h=a.types)&&e.push(["typ",h]);a=(""+a.keywords).replace(/^ | $/g, 12 | "");a.length&&e.push(["kwd",RegExp("^(?:"+a.replace(/[\s,]+/g,"|")+")\\b"),q]);m.push(["pln",/^\s+/,q," \r\n\t\xa0"]);e.push(["lit",/^@[$_a-z][\w$@]*/i,q],["typ",/^(?:[@_]?[A-Z]+[a-z][\w$@]*|\w+_t\b)/,q],["pln",/^[$_a-z][\w$@]*/i,q],["lit",/^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i,q,"0123456789"],["pln",/^\\[\S\s]?/,q],["pun",/^.[^\s\w"-$'./@\\`]*/,q]);return x(m,e)}function D(a,m){function e(a){switch(a.nodeType){case 1:if(k.test(a.className))break;if("BR"===a.nodeName)h(a), 13 | a.parentNode&&a.parentNode.removeChild(a);else for(a=a.firstChild;a;a=a.nextSibling)e(a);break;case 3:case 4:if(p){var b=a.nodeValue,d=b.match(t);if(d){var c=b.substring(0,d.index);a.nodeValue=c;(b=b.substring(d.index+d[0].length))&&a.parentNode.insertBefore(s.createTextNode(b),a.nextSibling);h(a);c||a.parentNode.removeChild(a)}}}}function h(a){function b(a,d){var e=d?a.cloneNode(!1):a,f=a.parentNode;if(f){var f=b(f,1),g=a.nextSibling;f.appendChild(e);for(var h=g;h;h=g)g=h.nextSibling,f.appendChild(h)}return e} 14 | for(;!a.nextSibling;)if(a=a.parentNode,!a)return;for(var a=b(a.nextSibling,0),e;(e=a.parentNode)&&e.nodeType===1;)a=e;d.push(a)}var k=/(?:^|\s)nocode(?:\s|$)/,t=/\r\n?|\n/,s=a.ownerDocument,l;a.currentStyle?l=a.currentStyle.whiteSpace:window.getComputedStyle&&(l=s.defaultView.getComputedStyle(a,q).getPropertyValue("white-space"));var p=l&&"pre"===l.substring(0,3);for(l=s.createElement("LI");a.firstChild;)l.appendChild(a.firstChild);for(var d=[l],g=0;g=0;){var h=m[e];A.hasOwnProperty(h)?window.console&&console.warn("cannot override language handler %s",h):A[h]=a}}function C(a,m){if(!a||!A.hasOwnProperty(a))a=/^\s*=o&&(h+=2);e>=c&&(a+=2)}}catch(w){"console"in window&&console.log(w&&w.stack?w.stack:w)}}var v=["break,continue,do,else,for,if,return,while"],w=[[v,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"], 18 | "catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],F=[w,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"],G=[w,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"], 19 | H=[G,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"],w=[w,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"],I=[v,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"], 20 | J=[v,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"],v=[v,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"],K=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/,N=/\S/,O=u({keywords:[F,H,w,"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END"+ 21 | I,J,v],hashComments:!0,cStyleComments:!0,multiLineStrings:!0,regexLiterals:!0}),A={};k(O,["default-code"]);k(x([],[["pln",/^[^]*(?:>|$)/],["com",/^<\!--[\S\s]*?(?:--\>|$)/],["lang-",/^<\?([\S\s]+?)(?:\?>|$)/],["lang-",/^<%([\S\s]+?)(?:%>|$)/],["pun",/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\S\s]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\S\s]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\S\s]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]), 22 | ["default-markup","htm","html","mxml","xhtml","xml","xsl"]);k(x([["pln",/^\s+/,q," \t\r\n"],["atv",/^(?:"[^"]*"?|'[^']*'?)/,q,"\"'"]],[["tag",/^^<\/?[a-z](?:[\w-.:]*\w)?|\/?>$/i],["atn",/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^\s"'>]*(?:[^\s"'/>]|\/(?=\s)))/],["pun",/^[/<->]+/],["lang-js",/^on\w+\s*=\s*"([^"]+)"/i],["lang-js",/^on\w+\s*=\s*'([^']+)'/i],["lang-js",/^on\w+\s*=\s*([^\s"'>]+)/i],["lang-css",/^style\s*=\s*"([^"]+)"/i],["lang-css",/^style\s*=\s*'([^']+)'/i],["lang-css", 23 | /^style\s*=\s*([^\s"'>]+)/i]]),["in.tag"]);k(x([],[["atv",/^[\S\s]+/]]),["uq.val"]);k(u({keywords:F,hashComments:!0,cStyleComments:!0,types:K}),["c","cc","cpp","cxx","cyc","m"]);k(u({keywords:"null,true,false"}),["json"]);k(u({keywords:H,hashComments:!0,cStyleComments:!0,verbatimStrings:!0,types:K}),["cs"]);k(u({keywords:G,cStyleComments:!0}),["java"]);k(u({keywords:v,hashComments:!0,multiLineStrings:!0}),["bsh","csh","sh"]);k(u({keywords:I,hashComments:!0,multiLineStrings:!0,tripleQuotedStrings:!0}), 24 | ["cv","py"]);k(u({keywords:"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["perl","pl","pm"]);k(u({keywords:J,hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["rb"]);k(u({keywords:w,cStyleComments:!0,regexLiterals:!0}),["js"]);k(u({keywords:"all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes", 25 | hashComments:3,cStyleComments:!0,multilineStrings:!0,tripleQuotedStrings:!0,regexLiterals:!0}),["coffee"]);k(x([],[["str",/^[\S\s]+/]]),["regex"]);window.prettyPrintOne=function(a,m,e){var h=document.createElement("PRE");h.innerHTML=a;e&&D(h,e);E({g:m,i:e,h:h});return h.innerHTML};window.prettyPrint=function(a){function m(){for(var e=window.PR_SHOULD_USE_CONTINUATION?l.now()+250:Infinity;p=0){var k=k.match(g),f,b;if(b= 26 | !k){b=n;for(var o=void 0,c=b.firstChild;c;c=c.nextSibling)var i=c.nodeType,o=i===1?o?b:c:i===3?N.test(c.nodeValue)?b:o:o;b=(f=o===b?void 0:o)&&"CODE"===f.tagName}b&&(k=f.className.match(g));k&&(k=k[1]);b=!1;for(o=n.parentNode;o;o=o.parentNode)if((o.tagName==="pre"||o.tagName==="code"||o.tagName==="xmp")&&o.className&&o.className.indexOf("prettyprint")>=0){b=!0;break}b||((b=(b=n.className.match(/\blinenums\b(?::(\d+))?/))?b[1]&&b[1].length?+b[1]:!0:!1)&&D(n,b),d={g:k,h:n,i:b},E(d))}}p 2 | 3 | 4 | 5 | JSDoc: Source: step.js 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Source: step.js

21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
import debounce from 'lodash.debounce';
 30 | import Tooltip from './tooltip';
 31 | import Constants from './constants';
 32 | import Style from './style';
 33 | 
 34 | let MAX_ATTEMPTS = 100;
 35 | let DOM_QUERY_DELAY = 100;
 36 | 
 37 | let Promise = require('es6-promise').Promise;
 38 | 
 39 | class Step {
 40 | 
 41 |   /** The step configuration is where you specify which elements of the page
 42 |    * will be cloned and placed over the overlay. These elements are the
 43 |    * what appear as highlighted to the user.
 44 |    *
 45 |    * @typedef StepConfiguration
 46 |    * @property {TooltipConfiguration} tooltip - Tooltip configuration.
 47 |    * @property {Object.<string, string>|string[]|string} [selectors] -
 48 |    *  Object with arbitrarily-named keys and CSS selector values.
 49 |    *  These keys can then be referenced from <code>TooltipConfiguration.anchorElement.</code>
 50 |    *  Or, an array of selector strings if named keys are not required.
 51 |    *  Or, a string if only one selector is required.<br/>
 52 |    *  Notes: Specifying a selector that targets another specified selector
 53 |    *  will result in unpredictable behavior.<br/>
 54 |    *  Specifying multiple selectors will effectively cause
 55 |    *  <code>Tutorial.useTransparentOverlayStrategy == false.</code>
 56 |    */
 57 | 
 58 |   /**
 59 |    * @constructor
 60 |    * @param {StepConfiguration} config - The configuration for this step
 61 |    * @param {integer} index - The index of this step within the current tutorial
 62 |    * @param {Tutorial} tutorial - The Tutorial object corresponding to this Step
 63 |    * @param {Overlay} overlay - The Overlay object displayed along with this
 64 |    *  Step
 65 |    * @param {ChariotDelegate} [delegate] - An optional delegate that responds to
 66 |    *  lifecycle callbacks
 67 |    */
 68 |   constructor(config = {}, index, tutorial, overlay, delegate) {
 69 |     this.tutorial = tutorial;
 70 |     this.index = index;
 71 |     this.overlay = overlay;
 72 |     this.delegate = delegate || {};
 73 | 
 74 |     if (!config.selectors) {
 75 |       throw new Error('selectors must be present in Step configuration\n' +
 76 |         this);
 77 |     } else if (typeof config.selectors === 'string') {
 78 |       this.selectors = { 0: config.selectors };
 79 |     } else if (Object.prototype.toString.call(config.selectors) === '[object Object]') {
 80 |       if (!Object.keys(config.selectors).length) {
 81 |         throw new Error('selectors must be present in Step configuration\n' +
 82 |           this);
 83 |       }
 84 |       this.selectors = config.selectors;
 85 |     } else if (Array.isArray(config.selectors)) {
 86 |       const selectorsMap = {};
 87 |       config.selectors.forEach((val, idx) => {
 88 |         selectorsMap[idx] = val;
 89 |       });
 90 |       this.selectors = selectorsMap;
 91 |     } else {
 92 |       throw new Error('selectors must be an object, array, or string');
 93 |     }
 94 | 
 95 |     this._resizeTimeout = null;
 96 | 
 97 |     this._elementMap = {};
 98 |     for (let selectorName in this.selectors) {
 99 |       this._elementMap[selectorName] = {};
100 |     }
101 | 
102 |     this.tooltip = new Tooltip(config.tooltip, this, tutorial);
103 |   }
104 | 
105 |   render() {
106 |     Promise.resolve().then(() => {
107 |       if (this.delegate.willBeginStep) {
108 |         return this.delegate.willBeginStep(
109 |           this, this.index, this.tutorial);
110 |       }
111 |     }).then(() => {
112 |       if (this.delegate.willShowOverlay) {
113 |         return this.delegate.willShowOverlay(
114 |           this.overlay, this.index, this.tutorial);
115 |       }
116 |     }).then(() => {
117 |       // Show a temporary background overlay while we wait for elements
118 |       this.overlay.showBackgroundOverlay();
119 |       return this._waitForElements();
120 |     }).then(() => {
121 |       // Render the overlay
122 |       if (this.overlay.isTransparentOverlayStrategy() &&
123 |           Object.keys(this.selectors).length === 1) {
124 |         this._singleTransparentOverlayStrategy();
125 |       } else {
126 |         this._clonedElementStrategy();
127 |       }
128 |     }).then(() => {
129 |       if (this.delegate.didShowOverlay) {
130 |         return this.delegate.didShowOverlay(
131 |           this.overlay, this.index, this.tutorial);
132 |       }
133 |     }).then(() => {
134 |       if (this.delegate.willRenderTooltip) {
135 |         return this.delegate.willRenderTooltip(
136 |           this.tooltip, this.index, this.tutorial);
137 |       }
138 |     }).then(() => {
139 |       this._renderTooltip();
140 |       if (this.delegate.didRenderTooltip) {
141 |         return this.delegate.didRenderTooltip(
142 |           this.tooltip, this.index, this.tutorial);
143 |       }
144 |     }).then(() => {
145 |       // Resize the overlay in case the tooltip extended the width/height of DOM
146 |       this.overlay.resize();
147 | 
148 |       // Setup resize handler
149 |       this._resizeHandler = debounce(() => {
150 |         for (let selectorName in this.selectors) {
151 |           let elementInfo = this._elementMap[selectorName];
152 |           if (elementInfo.clone) {
153 |             let $element = elementInfo.element;
154 |             let $clone = elementInfo.clone;
155 |             Style.clearCachedStylesForElement($element);
156 |             this._applyComputedStyles($clone, $element);
157 |             this._positionClone($clone, $element);
158 |           }
159 |         }
160 |         this.tooltip.reposition();
161 |         this.overlay.resize();
162 |       }, 50);
163 |       $(window).on('resize', this._resizeHandler);
164 |     }).catch(error => {
165 |       console.log(error);
166 |       this.tutorial.tearDown();
167 |     });
168 |   }
169 | 
170 |   _moveTo(action) {
171 |     Promise.resolve().then(() => {
172 |       if (this.delegate.didFinishStep) {
173 |         return this.delegate.didFinishStep(
174 |           this, this.index, this.tutorial);
175 |       }
176 |     }).then(() => {
177 |       action();
178 |     }).catch(error => {
179 |       console.log(error);
180 |       action();
181 |     });
182 |   }
183 | 
184 |   previous() {
185 |     this._moveTo(() => this.tutorial.next(this.index - 1));
186 |   }
187 | 
188 |   next() {
189 |     this._moveTo(() => this.tutorial.next());
190 |   }
191 | 
192 |   getClonedElement(selectorName) {
193 |     let elementInfo = this._elementMap[selectorName];
194 |     if (!elementInfo) return;
195 |     return elementInfo.clone;
196 |   }
197 | 
198 |   tearDown() {
199 |     let $window = $(window);
200 |     for (let selectorName in this.selectors) {
201 |       let selector = this.selectors[selectorName]
202 |       // Remove computed styles
203 |       Style.clearCachedStylesForElement($(selector));
204 |       let elementInfo = this._elementMap[selectorName];
205 |       if (elementInfo.clone) {
206 |         // Remove cloned elements
207 |         elementInfo.clone.remove();
208 |       }
209 |     }
210 |     this.tooltip.tearDown();
211 | 
212 |     $window.off('resize', this._resizeHandler);
213 |   }
214 | 
215 |   prepare() {
216 |     // FIX: This method currently always prepares for the clone strategy,
217 |     // regardless of the value of useTransparentOverlayStrategy.
218 |     // Perhaps add a check or rename this method, once the coupling to
219 |     // this.tutorial.prepare() is removed
220 |     for (let selectorName in this.selectors) {
221 |       let selector = this.selectors[selectorName]
222 |       this._computeStyles($(selector));
223 |     }
224 |   }
225 | 
226 |   toString() {
227 |     return `[Step - index: ${this.index}, ` +
228 |       `selectors: ${JSON.stringify(this.selectors)}]`;
229 |   }
230 | 
231 |   //// PRIVATE
232 | 
233 |   _singleTransparentOverlayStrategy() {
234 |     // Only use an overlay
235 |     let selectorName = Object.keys(this.selectors)[0];
236 |     let $element =  this._elementMap[selectorName].element;
237 |     this.overlay.focusOnElement($element);
238 |   }
239 | 
240 |   _clonedElementStrategy() {
241 |     // Clone elements if multiple selectors
242 |     this._cloneElements(this.selectors);
243 |     if (this.overlay.disableCloneInteraction) {
244 |       this.overlay.showTransparentOverlay();
245 |     }
246 |   }
247 | 
248 |   _renderTooltip() {
249 |     this.tooltip.render();
250 |   }
251 | 
252 |   _waitForElements() {
253 |     let promises = [];
254 |     for (let selectorName in this.selectors) {
255 |       let promise = new Promise((resolve, reject) => {
256 |         this._waitForElement(selectorName, 0, resolve, reject);
257 |       });
258 |       promises.push(promise);
259 |     }
260 | 
261 |     return Promise.all(promises);
262 |   }
263 | 
264 |   _waitForElement(selectorName, numAttempts, resolve, reject) {
265 |     let selector = this.selectors[selectorName];
266 |     let element = $(selector);
267 |     if (element.length == 0) {
268 |       ++numAttempts;
269 |       if (numAttempts == MAX_ATTEMPTS) {
270 |         reject(`Selector not found: ${selector}`);
271 |       } else {
272 |         window.setTimeout(() => {
273 |           this._waitForElement(selectorName, numAttempts, resolve, reject);
274 |         }, DOM_QUERY_DELAY);
275 |       }
276 |     } else {
277 |       this._elementMap[selectorName].element = element;
278 |       resolve();
279 | 
280 |       // TODO: fire event when element is ready. Tutorial will listen and call
281 |       // prepare() on all steps
282 |     }
283 |   }
284 | 
285 |   _computeStyles($element) {
286 |     Style.getComputedStylesFor($element[0]);
287 |     $element.children().toArray().forEach(child => {
288 |       this._computeStyles($(child));
289 |     });
290 |   }
291 | 
292 |   _cloneElements(selectors) {
293 |     if (this.overlay.isVisible()) return;
294 | 
295 |     setTimeout(() => {
296 |       this.tutorial.prepare();
297 |     }, 0);
298 |     for (let selectorName in selectors) {
299 |       let clone = this._cloneElement(selectorName);
300 |       this._elementMap[selectorName].clone = clone;
301 |     }
302 |   }
303 | 
304 |   _cloneElement(selectorName) {
305 |     let $element = this._elementMap[selectorName].element;
306 |     if ($element.length == 0) { return null; }
307 | 
308 |     let $clone = $element.clone();
309 |     $('body').append($clone);
310 |     this._applyComputedStyles($clone, $element);
311 |     this._positionClone($clone, $element);
312 | 
313 |     return $clone;
314 |   }
315 | 
316 |   _applyComputedStyles($clone, $element) {
317 |     if (!$element.is(":visible")) {
318 |       return;
319 |     }
320 |     $clone.addClass('chariot-clone');
321 |     Style.cloneStyles($element, $clone);
322 |     if (this.overlay.disableCloneInteraction) {
323 |       $clone.css('pointer-events', 'none');
324 |     }
325 |     let clonedChildren = $clone.children().toArray();
326 |     $element.children().toArray().forEach((child, index) => {
327 |       this._applyComputedStyles($(clonedChildren[index]), $(child));
328 |     });
329 |   }
330 | 
331 |   _positionClone($clone, $element) {
332 |     $clone.css({
333 |       'z-index': Constants.CLONE_Z_INDEX,
334 |       position: 'absolute'
335 |     });
336 |     $clone.offset($element.offset());
337 |   }
338 | }
339 | 
340 | export default Step;
341 | 
342 |
343 |
344 | 345 | 346 | 347 | 348 |
349 | 350 | 353 | 354 |
355 | 356 |
357 | Documentation generated by JSDoc 3.4.3 on Fri Feb 10 2017 14:56:40 GMT+1100 (AEDT) 358 |
359 | 360 | 361 | 362 | 363 | 364 | -------------------------------------------------------------------------------- /docs/styles/jsdoc-default.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Open Sans'; 3 | font-weight: normal; 4 | font-style: normal; 5 | src: url('../fonts/OpenSans-Regular-webfont.eot'); 6 | src: 7 | local('Open Sans'), 8 | local('OpenSans'), 9 | url('../fonts/OpenSans-Regular-webfont.eot?#iefix') format('embedded-opentype'), 10 | url('../fonts/OpenSans-Regular-webfont.woff') format('woff'), 11 | url('../fonts/OpenSans-Regular-webfont.svg#open_sansregular') format('svg'); 12 | } 13 | 14 | @font-face { 15 | font-family: 'Open Sans Light'; 16 | font-weight: normal; 17 | font-style: normal; 18 | src: url('../fonts/OpenSans-Light-webfont.eot'); 19 | src: 20 | local('Open Sans Light'), 21 | local('OpenSans Light'), 22 | url('../fonts/OpenSans-Light-webfont.eot?#iefix') format('embedded-opentype'), 23 | url('../fonts/OpenSans-Light-webfont.woff') format('woff'), 24 | url('../fonts/OpenSans-Light-webfont.svg#open_sanslight') format('svg'); 25 | } 26 | 27 | html 28 | { 29 | overflow: auto; 30 | background-color: #fff; 31 | font-size: 14px; 32 | } 33 | 34 | body 35 | { 36 | font-family: 'Open Sans', sans-serif; 37 | line-height: 1.5; 38 | color: #4d4e53; 39 | background-color: white; 40 | } 41 | 42 | a, a:visited, a:active { 43 | color: #0095dd; 44 | text-decoration: none; 45 | } 46 | 47 | a:hover { 48 | text-decoration: underline; 49 | } 50 | 51 | header 52 | { 53 | display: block; 54 | padding: 0px 4px; 55 | } 56 | 57 | tt, code, kbd, samp { 58 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 59 | } 60 | 61 | .class-description { 62 | font-size: 130%; 63 | line-height: 140%; 64 | margin-bottom: 1em; 65 | margin-top: 1em; 66 | } 67 | 68 | .class-description:empty { 69 | margin: 0; 70 | } 71 | 72 | #main { 73 | float: left; 74 | width: 70%; 75 | } 76 | 77 | article dl { 78 | margin-bottom: 40px; 79 | } 80 | 81 | section 82 | { 83 | display: block; 84 | background-color: #fff; 85 | padding: 12px 24px; 86 | border-bottom: 1px solid #ccc; 87 | margin-right: 30px; 88 | } 89 | 90 | .variation { 91 | display: none; 92 | } 93 | 94 | .signature-attributes { 95 | font-size: 60%; 96 | color: #aaa; 97 | font-style: italic; 98 | font-weight: lighter; 99 | } 100 | 101 | nav 102 | { 103 | display: block; 104 | float: right; 105 | margin-top: 28px; 106 | width: 30%; 107 | box-sizing: border-box; 108 | border-left: 1px solid #ccc; 109 | padding-left: 16px; 110 | } 111 | 112 | nav ul { 113 | font-family: 'Lucida Grande', 'Lucida Sans Unicode', arial, sans-serif; 114 | font-size: 100%; 115 | line-height: 17px; 116 | padding: 0; 117 | margin: 0; 118 | list-style-type: none; 119 | } 120 | 121 | nav ul a, nav ul a:visited, nav ul a:active { 122 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 123 | line-height: 18px; 124 | color: #4D4E53; 125 | } 126 | 127 | nav h3 { 128 | margin-top: 12px; 129 | } 130 | 131 | nav li { 132 | margin-top: 6px; 133 | } 134 | 135 | footer { 136 | display: block; 137 | padding: 6px; 138 | margin-top: 12px; 139 | font-style: italic; 140 | font-size: 90%; 141 | } 142 | 143 | h1, h2, h3, h4 { 144 | font-weight: 200; 145 | margin: 0; 146 | } 147 | 148 | h1 149 | { 150 | font-family: 'Open Sans Light', sans-serif; 151 | font-size: 48px; 152 | letter-spacing: -2px; 153 | margin: 12px 24px 20px; 154 | } 155 | 156 | h2, h3.subsection-title 157 | { 158 | font-size: 30px; 159 | font-weight: 700; 160 | letter-spacing: -1px; 161 | margin-bottom: 12px; 162 | } 163 | 164 | h3 165 | { 166 | font-size: 24px; 167 | letter-spacing: -0.5px; 168 | margin-bottom: 12px; 169 | } 170 | 171 | h4 172 | { 173 | font-size: 18px; 174 | letter-spacing: -0.33px; 175 | margin-bottom: 12px; 176 | color: #4d4e53; 177 | } 178 | 179 | h5, .container-overview .subsection-title 180 | { 181 | font-size: 120%; 182 | font-weight: bold; 183 | letter-spacing: -0.01em; 184 | margin: 8px 0 3px 0; 185 | } 186 | 187 | h6 188 | { 189 | font-size: 100%; 190 | letter-spacing: -0.01em; 191 | margin: 6px 0 3px 0; 192 | font-style: italic; 193 | } 194 | 195 | table 196 | { 197 | border-spacing: 0; 198 | border: 0; 199 | border-collapse: collapse; 200 | } 201 | 202 | td, th 203 | { 204 | border: 1px solid #ddd; 205 | margin: 0px; 206 | text-align: left; 207 | vertical-align: top; 208 | padding: 4px 6px; 209 | display: table-cell; 210 | } 211 | 212 | thead tr 213 | { 214 | background-color: #ddd; 215 | font-weight: bold; 216 | } 217 | 218 | th { border-right: 1px solid #aaa; } 219 | tr > th:last-child { border-right: 1px solid #ddd; } 220 | 221 | .ancestors { color: #999; } 222 | .ancestors a 223 | { 224 | color: #999 !important; 225 | text-decoration: none; 226 | } 227 | 228 | .clear 229 | { 230 | clear: both; 231 | } 232 | 233 | .important 234 | { 235 | font-weight: bold; 236 | color: #950B02; 237 | } 238 | 239 | .yes-def { 240 | text-indent: -1000px; 241 | } 242 | 243 | .type-signature { 244 | color: #aaa; 245 | } 246 | 247 | .name, .signature { 248 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 249 | } 250 | 251 | .details { margin-top: 14px; border-left: 2px solid #DDD; } 252 | .details dt { width: 120px; float: left; padding-left: 10px; padding-top: 6px; } 253 | .details dd { margin-left: 70px; } 254 | .details ul { margin: 0; } 255 | .details ul { list-style-type: none; } 256 | .details li { margin-left: 30px; padding-top: 6px; } 257 | .details pre.prettyprint { margin: 0 } 258 | .details .object-value { padding-top: 0; } 259 | 260 | .description { 261 | margin-bottom: 1em; 262 | margin-top: 1em; 263 | } 264 | 265 | .code-caption 266 | { 267 | font-style: italic; 268 | font-size: 107%; 269 | margin: 0; 270 | } 271 | 272 | .prettyprint 273 | { 274 | border: 1px solid #ddd; 275 | width: 80%; 276 | overflow: auto; 277 | } 278 | 279 | .prettyprint.source { 280 | width: inherit; 281 | } 282 | 283 | .prettyprint code 284 | { 285 | font-size: 100%; 286 | line-height: 18px; 287 | display: block; 288 | padding: 4px 12px; 289 | margin: 0; 290 | background-color: #fff; 291 | color: #4D4E53; 292 | } 293 | 294 | .prettyprint code span.line 295 | { 296 | display: inline-block; 297 | } 298 | 299 | .prettyprint.linenums 300 | { 301 | padding-left: 70px; 302 | -webkit-user-select: none; 303 | -moz-user-select: none; 304 | -ms-user-select: none; 305 | user-select: none; 306 | } 307 | 308 | .prettyprint.linenums ol 309 | { 310 | padding-left: 0; 311 | } 312 | 313 | .prettyprint.linenums li 314 | { 315 | border-left: 3px #ddd solid; 316 | } 317 | 318 | .prettyprint.linenums li.selected, 319 | .prettyprint.linenums li.selected * 320 | { 321 | background-color: lightyellow; 322 | } 323 | 324 | .prettyprint.linenums li * 325 | { 326 | -webkit-user-select: text; 327 | -moz-user-select: text; 328 | -ms-user-select: text; 329 | user-select: text; 330 | } 331 | 332 | .params .name, .props .name, .name code { 333 | color: #4D4E53; 334 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 335 | font-size: 100%; 336 | } 337 | 338 | .params td.description > p:first-child, 339 | .props td.description > p:first-child 340 | { 341 | margin-top: 0; 342 | padding-top: 0; 343 | } 344 | 345 | .params td.description > p:last-child, 346 | .props td.description > p:last-child 347 | { 348 | margin-bottom: 0; 349 | padding-bottom: 0; 350 | } 351 | 352 | .disabled { 353 | color: #454545; 354 | } 355 | -------------------------------------------------------------------------------- /docs/styles/prettify-jsdoc.css: -------------------------------------------------------------------------------- 1 | /* JSDoc prettify.js theme */ 2 | 3 | /* plain text */ 4 | .pln { 5 | color: #000000; 6 | font-weight: normal; 7 | font-style: normal; 8 | } 9 | 10 | /* string content */ 11 | .str { 12 | color: #006400; 13 | font-weight: normal; 14 | font-style: normal; 15 | } 16 | 17 | /* a keyword */ 18 | .kwd { 19 | color: #000000; 20 | font-weight: bold; 21 | font-style: normal; 22 | } 23 | 24 | /* a comment */ 25 | .com { 26 | font-weight: normal; 27 | font-style: italic; 28 | } 29 | 30 | /* a type name */ 31 | .typ { 32 | color: #000000; 33 | font-weight: normal; 34 | font-style: normal; 35 | } 36 | 37 | /* a literal value */ 38 | .lit { 39 | color: #006400; 40 | font-weight: normal; 41 | font-style: normal; 42 | } 43 | 44 | /* punctuation */ 45 | .pun { 46 | color: #000000; 47 | font-weight: bold; 48 | font-style: normal; 49 | } 50 | 51 | /* lisp open bracket */ 52 | .opn { 53 | color: #000000; 54 | font-weight: bold; 55 | font-style: normal; 56 | } 57 | 58 | /* lisp close bracket */ 59 | .clo { 60 | color: #000000; 61 | font-weight: bold; 62 | font-style: normal; 63 | } 64 | 65 | /* a markup tag name */ 66 | .tag { 67 | color: #006400; 68 | font-weight: normal; 69 | font-style: normal; 70 | } 71 | 72 | /* a markup attribute name */ 73 | .atn { 74 | color: #006400; 75 | font-weight: normal; 76 | font-style: normal; 77 | } 78 | 79 | /* a markup attribute value */ 80 | .atv { 81 | color: #006400; 82 | font-weight: normal; 83 | font-style: normal; 84 | } 85 | 86 | /* a declaration */ 87 | .dec { 88 | color: #000000; 89 | font-weight: bold; 90 | font-style: normal; 91 | } 92 | 93 | /* a variable name */ 94 | .var { 95 | color: #000000; 96 | font-weight: normal; 97 | font-style: normal; 98 | } 99 | 100 | /* a function name */ 101 | .fun { 102 | color: #000000; 103 | font-weight: bold; 104 | font-style: normal; 105 | } 106 | 107 | /* Specify class=linenums on a pre to get line numbering */ 108 | ol.linenums { 109 | margin-top: 0; 110 | margin-bottom: 0; 111 | } 112 | -------------------------------------------------------------------------------- /docs/styles/prettify-tomorrow.css: -------------------------------------------------------------------------------- 1 | /* Tomorrow Theme */ 2 | /* Original theme - https://github.com/chriskempson/tomorrow-theme */ 3 | /* Pretty printing styles. Used with prettify.js. */ 4 | /* SPAN elements with the classes below are added by prettyprint. */ 5 | /* plain text */ 6 | .pln { 7 | color: #4d4d4c; } 8 | 9 | @media screen { 10 | /* string content */ 11 | .str { 12 | color: #718c00; } 13 | 14 | /* a keyword */ 15 | .kwd { 16 | color: #8959a8; } 17 | 18 | /* a comment */ 19 | .com { 20 | color: #8e908c; } 21 | 22 | /* a type name */ 23 | .typ { 24 | color: #4271ae; } 25 | 26 | /* a literal value */ 27 | .lit { 28 | color: #f5871f; } 29 | 30 | /* punctuation */ 31 | .pun { 32 | color: #4d4d4c; } 33 | 34 | /* lisp open bracket */ 35 | .opn { 36 | color: #4d4d4c; } 37 | 38 | /* lisp close bracket */ 39 | .clo { 40 | color: #4d4d4c; } 41 | 42 | /* a markup tag name */ 43 | .tag { 44 | color: #c82829; } 45 | 46 | /* a markup attribute name */ 47 | .atn { 48 | color: #f5871f; } 49 | 50 | /* a markup attribute value */ 51 | .atv { 52 | color: #3e999f; } 53 | 54 | /* a declaration */ 55 | .dec { 56 | color: #f5871f; } 57 | 58 | /* a variable name */ 59 | .var { 60 | color: #c82829; } 61 | 62 | /* a function name */ 63 | .fun { 64 | color: #4271ae; } } 65 | /* Use higher contrast and text-weight for printable form. */ 66 | @media print, projection { 67 | .str { 68 | color: #060; } 69 | 70 | .kwd { 71 | color: #006; 72 | font-weight: bold; } 73 | 74 | .com { 75 | color: #600; 76 | font-style: italic; } 77 | 78 | .typ { 79 | color: #404; 80 | font-weight: bold; } 81 | 82 | .lit { 83 | color: #044; } 84 | 85 | .pun, .opn, .clo { 86 | color: #440; } 87 | 88 | .tag { 89 | color: #006; 90 | font-weight: bold; } 91 | 92 | .atn { 93 | color: #404; } 94 | 95 | .atv { 96 | color: #060; } } 97 | /* Style */ 98 | /* 99 | pre.prettyprint { 100 | background: white; 101 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 102 | font-size: 12px; 103 | line-height: 1.5; 104 | border: 1px solid #ccc; 105 | padding: 10px; } 106 | */ 107 | 108 | /* Specify class=linenums on a pre to get line numbering */ 109 | ol.linenums { 110 | margin-top: 0; 111 | margin-bottom: 0; } 112 | 113 | /* IE indents via margin-left */ 114 | li.L0, 115 | li.L1, 116 | li.L2, 117 | li.L3, 118 | li.L4, 119 | li.L5, 120 | li.L6, 121 | li.L7, 122 | li.L8, 123 | li.L9 { 124 | /* */ } 125 | 126 | /* Alternate shading for lines */ 127 | li.L1, 128 | li.L3, 129 | li.L5, 130 | li.L7, 131 | li.L9 { 132 | /* */ } 133 | -------------------------------------------------------------------------------- /docs/tutorial.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Source: tutorial.js 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Source: tutorial.js

21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
import Step from './step';
 30 | import Overlay from './overlay';
 31 | const Promise = require('es6-promise').Promise;
 32 | import Constant from './constants';
 33 | 
 34 | const DEFAULT_SCROLL_ANIMATION_DURATION = 500;
 35 | 
 36 | class Tutorial {
 37 |   /**
 38 |    * <p>The tutorial configuration is where the steps of a tutorial are specified,
 39 |    * and also allows customization of the overlay style.
 40 |    * If optional configuration parameters are not required, the steps property
 41 |    * array can be passed in directly as the configuration.</p>
 42 |    *
 43 |    * <p>Notes on implementation:</p>
 44 |    * <p>The elements defined in each step of a tutorial via
 45 |    * StepConfiguration.selectors are highlighted using transparent overlays.</p>
 46 |    * <p>These elements areare overlaid using one of two strategies:</p>
 47 |    * <ol>
 48 |    *   <li>Semi-transparent overlay with a transparent section cut out over the
 49 |    *    element</li>
 50 |    *   <li>Selected elements are cloned and placed above a transparent overlay</li>
 51 |    * </ol>
 52 |    *
 53 |    * <p>#1 is more performant, but issues arise when an element is not rectangularly-
 54 |    * shaped, or when it has `:before` or `:after`
 55 |    * pseudo-selectors that insert new DOM elements that protrude out of the
 56 |    * main element.</p>
 57 |    * <p>#2 is slower because of deep CSS style cloning, but it will correctly render
 58 |    * the entire element in question, regardless of shape or size.</p>
 59 |    * </p>However, there are edge cases where Firefox
 60 |    * will not clone CSS <code>margin</code> attribute of children elements.
 61 |    * In those cases, the delegate callbacks should be utilized to fix.
 62 |    * Note however, that #2 is always chosen if multiple selectors are specified in
 63 |    * StepConfiguration.selectors.</p>
 64 |    *
 65 |    * @typedef TutorialConfiguration
 66 |    * @property {StepConfiguration[]} steps - An array of step configurations (see below).
 67 |    *  Note that this property can be passed as the configuration if the
 68 |    *  optional params below are not used.
 69 |    * @property {integer} [zIndex=20] - Sets the base z-index value used by this tutorial
 70 |    * @property {boolean} [shouldOverlay=true] - Setting to false will disable the
 71 |    * overlay that normally appears over the page and behind the tooltips.
 72 |    * @property {string} [overlayColor='rgba(255,255,255,0.7)'] - Overlay CSS color
 73 |    * @property {boolean} [disableCloneInteraction=true] - Setting to false will allow the
 74 |    * user to interact with the cloned element (eg. click on links)
 75 |    * @property {Tutorial-onCompleteCallback} [onComplete] - Callback that is called
 76 |    * once the tutorial has gone through all steps.
 77 |    * @property {boolean} [useTransparentOverlayStrategy=false] - <p>
 78 |    *  Setting to true will use an implementation that does not rely on
 79 |    *  cloning highlighted elements.<br/>
 80 |    *  Note: This value is ignored if multiple selectors are
 81 |    *  specified in <code>StepConfiguration.selectors</code>.</p>
 82 |    *  <p><code>useTransparentOverlayStrategy</code> focuses on an element by
 83 |    *  resizing a transparent overlay to match its dimensions and changes the
 84 |    *  borders to be colored to obscure the main UI.<p>
 85 | 
 86 |     <h4>Strategy Details</h4>
 87 |     <p>
 88 |       By default, a tutorial is displayed with a semi-transparent overlay
 89 |       that hides background content and highlights the selected element(s) for the
 90 |       current step of the tutorial.
 91 |     </p>
 92 |     This is achieved by one of two exclusive strategies:
 93 |     <ol>
 94 |       <li>
 95 |         An overlay div with a semi-transparent border but with a transparent center
 96 |         equal in size to the selected element.<br/>
 97 |         Example: A 50x50 div is the selected element, so the overlay's transparent center
 98 |         is 50x50, and its semi-transparent border fills the rest of the viewport.
 99 |       </li>
100 |       <li>
101 |         A completely semi-transparent overlay fills the entire viewport, and the
102 |         selected element(s) is cloned and placed on top of this overlay, using
103 |         <code>z-index</code>.
104 |       </li>
105 |     </ol>
106 | 
107 |     <p>Both strategies have pros & cons.</p>
108 |     1. Clone strategy (default)
109 |     <p>
110 |       <strong>Pros:</strong> It will correctly render the entire element in question,
111 |       regardless of shape or size.
112 |     </p>
113 |     <p>
114 |       <strong>Cons:</strong> Slow because of the deep-cloning involved with CSS styling. The more
115 |       children elements that exist, the slower each step will take to render.
116 |       (This can be improved over time by pre-caching the next step in advance.)
117 |       There are also edge cases where Firefox will not clone the
118 |       CSS `margin` attribute of children elements.
119 |       <br/>
120 |       In those cases, the callbacks <code>Step.beforeCallback</code> and
121 |       <code>Step.afterCallback</code> can be used to properly restore the margin.
122 |     </p>
123 | 
124 |     2. Background overlay with transparent center and semi-transparent border
125 |     <p>
126 |       <strong>Pros:</strong>: More performant than the clone strategy because styles are not being cloned.
127 |     </p>
128 |     <p>
129 |       <strong>Cons:</strong> When an element is not rectangular in shape, or
130 |       when it has <code>:before</code> or <code>:after</code> pseudo-selectors
131 |       that insert new DOM elements that protrude out of the main element,
132 |       the transparent center will either reveal or occlude sections of
133 |       the element.
134 |     </p>
135 | 
136 |     Note: The clone strategy is always chosen if multiple selectors are
137 |     specified in <code>StepConfiguration.selectors</code>.
138 | 
139 | 
140 |    * @property {boolean} [animateTooltips=true] - Enables tooltip bouncing at the
141 |    *  beginning and end of each step.
142 |    * @property {boolean} [animateScrolling=true] -
143 |    *  <p>If the next tooltip is not completely within the client bounds, this
144 |    *  property animates the scrolling of the viewport until the next tooltip
145 |    *  is centered.</p>
146 |    *  <p>If false, the viewport is not scrolled.</p
147 |    * @property {integer} [scrollAnimationDuration=500] - Specifies the duration
148 |    *  of the scroll animation above, in milliseconds.
149 |    *  Ignored if <code>animateScrolling</code> is false.
150 |    * @property {boolean} [allowSteppingBackward=false] - Enables returning to previous steps.
151 |    */
152 | 
153 |   /**
154 |    * @constructor
155 |    * @param {TutorialConfiguration} config - The configuration for this tutorial
156 |    * @param {string} [name] - Name of the tutorial
157 |    * @param {ChariotDelegate} [delegate] - An optional delegate that responds to
158 |    *  lifecycle callbacks
159 |    */
160 |   constructor(config, name, delegate) {
161 |     this.name = name;
162 |     this.delegate = delegate || {};
163 |     this.steps = [];
164 | 
165 |     const configType = Object.prototype.toString.call(config);
166 |     let stepConfigs, overlayConfig;
167 |     if (configType === '[object Array]') {
168 |       stepConfigs = config;
169 |       overlayConfig = {};
170 |       this.animateTooltips = true;
171 |       this.animateScrolling = true;
172 |       this.scrollAnimationDuration = DEFAULT_SCROLL_ANIMATION_DURATION;
173 |       this.allowSteppingBackward = false;
174 |     } else if (configType === '[object Object]') {
175 |       if (!Array.isArray(config.steps)) {
176 |         throw new Error(`steps must be an array.\n${this}`);
177 |       }
178 |       this.zIndex = config.zIndex;
179 |       this.useTransparentOverlayStrategy = config.useTransparentOverlayStrategy;
180 |       this.animateTooltips = config.animateTooltips === undefined ? true : config.animateTooltips;
181 |       this.animateScrolling = config.animateScrolling === undefined ? true : config.animateScrolling;
182 |       this.scrollAnimationDuration = config.scrollAnimationDuration || DEFAULT_SCROLL_ANIMATION_DURATION;
183 |       this.allowSteppingBackward = config.allowSteppingBackward;
184 |       stepConfigs = config.steps;
185 |       overlayConfig = config;
186 |     } else {
187 |       throw new Error('config must be an object or array');
188 |     }
189 | 
190 |     this.overlay = new Overlay(overlayConfig);
191 |     stepConfigs.forEach((stepConfig, index) => {
192 |       this.steps.push(new Step(stepConfig, index, this, this.overlay, this.delegate));
193 |     });
194 |     this._prepared = false;
195 |     this._isActive = false;
196 |   }
197 | 
198 |   /**
199 |    * Indicates if this tutorial is currently active.
200 |    * return {boolean}
201 |    */
202 |   isActive() {
203 |     return this._isActive;
204 |   }
205 | 
206 |   /**
207 |    * Starts the tutorial and marks itself as active.
208 |    * @returns {Promise}
209 |    */
210 |   start() {
211 |     if (this.zIndex !== null) {
212 |       Constant.reload({ overlayZIndex: this.zIndex });
213 |     }
214 |     if (this.steps.length === 0) {
215 |       throw new Error(`steps should not be empty.\n${this}`);
216 |       return;
217 |     }
218 | 
219 |     this._isActive = true;
220 |     // render overlay first to avoid willBeingTutorial delay overlay showing up
221 |     this.overlay.render();
222 |     return Promise.resolve().then(() => {
223 |       if (this.delegate.willBeginTutorial) {
224 |         return this.delegate.willBeginTutorial(this);
225 |       }
226 |     }).then(() => {
227 |       this.currentStep = this.steps[0];
228 |       this.currentStep.render();
229 |     }).catch(() => {
230 |       this.tearDown();
231 |     });
232 |   }
233 | 
234 |   /**
235 |    * Prepares each step of the tutorial, to speedup rendering.
236 |    * @returns {undefined}
237 |    */
238 |   prepare() {
239 |     if (this._prepared) return;
240 |     this.steps.forEach(step => {
241 |       step.prepare();
242 |       this._prepared = true;
243 |     });
244 |   }
245 | 
246 |   /**
247 |    * Advances to the next step in the tutorial, or ends tutorial if no more
248 |    * steps.
249 |    *
250 |    * @param {integer|Step} [step] - If step is an integer, advances to that
251 |    *  step. If step is a Step instance, that step
252 |    *  If no argument is passed in, the current step's index is incremented to
253 |    *  determine the next step.
254 |    * @returns {undefined}
255 |    */
256 |   next(step) {
257 |     let currentStepIndex = -1;
258 |     if (step === undefined || step === null) {
259 |       currentStepIndex = this.steps.indexOf(this.currentStep);
260 |       if (currentStepIndex < 0) {
261 |         throw new Error('step not found');
262 |       } else if (currentStepIndex === this.steps.length - 1) {
263 |         this.end();
264 |         return;
265 |       }
266 |     }
267 | 
268 |     if (this.currentStep) {
269 |       this.currentStep.tearDown();
270 |     }
271 | 
272 |     let nextStep;
273 |     if (step !== undefined && step !== null) {
274 |       if (typeof step === 'number') {
275 |         if (step < 0 || step >= this.steps.length) {
276 |           throw new Error(`step is outside bounds of steps array (length: ${this.steps.length})`);
277 |         }
278 |         nextStep = this.steps[step];
279 |       } else if (Object.prototype.toString.call(step) === '[object Object]') {
280 |         nextStep = step;
281 |       } else {
282 |         throw new Error('step arg must be number or object');
283 |       }
284 |     } else {
285 |       nextStep = this.steps[currentStepIndex + 1];
286 |     }
287 | 
288 |     this.currentStep = nextStep;
289 |     nextStep.render();
290 |   }
291 | 
292 |   /**
293 |    * Returns the one-indexed (human-friendly) step number.
294 |    *
295 |    * @param {Step} step - The step instance for which we want the index
296 |    * @returns {integer} stepNum - The one-indexed step number
297 |    */
298 |   stepNum(step) {
299 |     if (step === null) return null;
300 |     return this.steps.indexOf(step) + 1;
301 |   }
302 | 
303 |   /**
304 |    * Tears down the internal overlay and tears down each individual step
305 |    * Nulls out internal references.
306 |    * @returns {undefined}
307 |    */
308 |   tearDown() {
309 |     this._prepared = false;
310 |     this.overlay.tearDown();
311 |     this.steps.forEach(step => {
312 |       step.tearDown();
313 |     });
314 |     this.currentStep = null;
315 |   }
316 | 
317 |   /**
318 |    * Retrieves the Step object at index.
319 |    * @returns {Step} step
320 |    */
321 |   getStep(index) {
322 |     return this.steps[index];
323 |   }
324 | 
325 |   /**
326 |    * Ends the tutorial by tearing down all the steps (and associated tooltips,
327 |    * overlays).
328 |    * Also marks itself as inactive.
329 |    * @param {boolean} [forced=false] - Indicates whether tutorial was forced to
330 |    *  end
331 |    * @returns {undefined}
332 |    */
333 |   end(forced = false) {
334 |     // Note: Order matters.
335 |     this.tearDown();
336 | 
337 |     return Promise.resolve().then(() => {
338 |       if (this.delegate.didFinishTutorial) {
339 |         return this.delegate.didFinishTutorial(this, forced);
340 |       }
341 |     }).then(() => {
342 |       this._isActive = false;
343 |     });
344 |   }
345 | 
346 |   toString() {
347 |     return `[Tutorial - active: ${this._isActive}, ` +
348 |       `useTransparentOverlayStrategy: ${this.useTransparentOverlayStrategy}, ` +
349 |       `steps: ${this.steps}, overlay: ${this.overlay}]`;
350 |   }
351 | 
352 | }
353 | 
354 | export default Tutorial;
355 | 
356 |
357 |
358 | 359 | 360 | 361 | 362 |
363 | 364 | 367 | 368 |
369 | 370 |
371 | Documentation generated by JSDoc 3.4.3 on Fri Feb 10 2017 14:56:40 GMT+1100 (AEDT) 372 |
373 | 374 | 375 | 376 | 377 | 378 | -------------------------------------------------------------------------------- /example/chariot_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zendesk/chariot-tooltips/0650103b2074a48f933ea21846606352da0fa3b7/example/chariot_screenshot.png -------------------------------------------------------------------------------- /example/example-tooltip.css: -------------------------------------------------------------------------------- 1 | .chariot-tooltip { 2 | background-color: #fff; 3 | padding: 30px; 4 | width: 220px; 5 | text-align: center; 6 | box-shadow: 0 0 5px 0 rgba(31, 28, 28, 0.3); 7 | border: 1px solid #ddd; 8 | color: #999; } 9 | .chariot-tooltip .chariot-tooltip-icon { 10 | width: 52px; 11 | height: 52px; 12 | margin: auto; } 13 | .chariot-tooltip .chariot-tooltip-icon img { 14 | width: 52px; 15 | height: 52px; } 16 | .chariot-tooltip .chariot-tooltip-header { 17 | font-size: 18px; 18 | line-height: 18px; 19 | font-weight: 500; 20 | color: #555; 21 | padding: 5px 0; } 22 | .chariot-tooltip .chariot-tooltip-content { 23 | padding: 5px 0; } 24 | .chariot-tooltip .chariot-tooltip-content p { 25 | font-size: 14px; 26 | font-weight: 300; 27 | color: #999; 28 | padding-bottom: 15px; } 29 | .chariot-tooltip .chariot-btn-row { 30 | padding-top: 5px; } 31 | .chariot-tooltip .chariot-btn-row .btn { 32 | font-size: 13px; 33 | font-weight: 400; 34 | color: #78A300; 35 | background-color: transparent; 36 | border: 1px solid #78A300; 37 | border-radius: 3px; 38 | height: 36px; 39 | padding: 0 20px; 40 | cursor: pointer; } 41 | .chariot-tooltip .chariot-btn-row .btn-next { 42 | color: #fff; 43 | background-color: #78A300; 44 | border: none; } 45 | .chariot-tooltip .chariot-btn-row .btn-previous { 46 | margin-right: 5px; } 47 | .chariot-tooltip .chariot-btn-row .btn-previous:hover { 48 | color: #fff; 49 | background-color: #78A300; } 50 | .chariot-tooltip .chariot-btn-row .chariot-tooltip-subtext { 51 | float: left; 52 | color: #ddd; 53 | font-size: 13px; 54 | padding-top: 10px; } 55 | .chariot-tooltip-arrow { 56 | background: #fff; } 57 | .chariot-tooltip-arrow-left { 58 | border-left: 1px solid #ddd; 59 | border-bottom: 1px solid #ddd; 60 | box-shadow: -2px 2px 2px 0 rgba(31, 28, 28, 0.1); } 61 | .chariot-tooltip-arrow-right { 62 | border-right: 1px solid #ddd; 63 | border-top: 1px solid #ddd; 64 | box-shadow: 2px -2px 2px 0 rgba(31, 28, 28, 0.1); } 65 | .chariot-tooltip-arrow-top { 66 | border-left: 1px solid #ddd; 67 | border-top: 1px solid #ddd; 68 | box-shadow: -2px -2px 4px 0 rgba(31, 28, 28, 0.1); } 69 | .chariot-tooltip-arrow-bottom { 70 | border-right: 1px solid #ddd; 71 | border-bottom: 1px solid #ddd; 72 | box-shadow: 2px 2px 4px 0 rgba(31, 28, 28, 0.1); } 73 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 |
58 |
59 |
60 |
61 |
62 | 63 |
64 | 187 | 188 | 189 | 190 | 191 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const glob = require('glob'); 3 | const browserify = require('browserify'); 4 | const babelify = require('babelify'); 5 | const source = require('vinyl-source-stream'); 6 | const connect = require('gulp-connect'); 7 | const run = require('gulp-run'); 8 | const mocha = require('gulp-mocha'); 9 | const testem = require('gulp-testem'); 10 | const sass = require('gulp-sass'); 11 | const autoprefixer = require('gulp-autoprefixer'); 12 | const uglify = require('gulp-uglify'); 13 | const clean = require('gulp-clean'); 14 | const cleanCSS = require('gulp-clean-css'); 15 | const runSequence = require('run-sequence'); 16 | const rename = require('gulp-rename'); 17 | const shell = require('gulp-shell'); 18 | const jscs = require('gulp-jscs'); 19 | const bump = require('gulp-bump'); 20 | const prompt = require('gulp-prompt'); 21 | const git = require('gulp-git'); 22 | const fs = require('fs'); 23 | const insert = require('gulp-insert'); 24 | 25 | const babel = require('gulp-babel'); 26 | const del = require('del'); 27 | const exec = require('child_process').exec; 28 | const eslint = require('gulp-eslint'); 29 | 30 | const projectName = 'chariot'; 31 | 32 | const paths = { 33 | allSrcJs: 'lib/**/*.js', 34 | distDir: 'dist', 35 | releaseDir: 'release', 36 | gulpFile: 'gulpfile.babel.js', 37 | webpackFile: 'webpack.config.babel.js' 38 | }; 39 | 40 | gulp.task("default", ['js', 'sass']); 41 | 42 | //################ DEV #################### 43 | 44 | gulp.task('watch', ['js:watch', 'sass:watch']); 45 | 46 | gulp.task('js', function() { 47 | return browserify({ 48 | entries: './lib/chariot.js', 49 | standalone: 'chariot' 50 | }) 51 | .transform(babelify, { "presets": ["es2015"], "plugins": [ "add-module-exports" ] }) 52 | .bundle() 53 | .pipe(source(projectName + '.js')) 54 | .pipe(gulp.dest('./dist/javascripts')) 55 | .pipe(connect.reload()) 56 | }); 57 | 58 | gulp.task('js:watch', function() { 59 | return gulp.watch(paths.allSrcJs, ['js']); 60 | }); 61 | 62 | gulp.task('sass', function() { 63 | return gulp.src('./stylesheets/chariot.scss') 64 | .pipe(sass().on('error', sass.logError)) 65 | .pipe(rename(projectName + '.css')) 66 | .pipe(autoprefixer({ 67 | browsers: ['last 3 versions'], 68 | cascade: false 69 | })) 70 | .pipe(gulp.dest('./dist/stylesheets')) 71 | .pipe(connect.reload()) 72 | }); 73 | 74 | gulp.task('sass:watch', function() { 75 | gulp.watch('./stylesheets/**/*.scss', ['sass']); 76 | }); 77 | 78 | //################ TEST #################### 79 | 80 | gulp.task('connect', ['js', 'sass', 'watch'], function() { 81 | connect.server({ 82 | root: [__dirname], 83 | livereload: true 84 | }); 85 | }); 86 | 87 | gulp.task('test', function() { 88 | return gulp.src(['test/**/*.js'], { read: false }) 89 | .pipe(mocha({ 90 | compilers: { 91 | js: babel 92 | } 93 | })) 94 | }); 95 | 96 | gulp.task('testem', ['compile-test'], function() { 97 | gulp.src(['']) 98 | .pipe(testem({ 99 | configFile: 'testem.yml' 100 | })); 101 | }); 102 | 103 | gulp.task("compile-test", function() { 104 | glob("./test/**/*.js", function(er, files) { 105 | return browserify({ 106 | entries: files, 107 | debug: true 108 | }) 109 | .transform(babelify) 110 | .bundle() 111 | .pipe(source('test.js')) 112 | .pipe(gulp.dest('./dist/test')) 113 | }); 114 | }); 115 | 116 | //################ BUILD #################### 117 | 118 | gulp.task('js-doc', shell.task([ 119 | './node_modules/.bin/jsdoc lib/* --destination docs/' 120 | ])); 121 | 122 | gulp.task('style', function () { 123 | return gulp.src(['lib/**', 'test/**']) 124 | .pipe(jscs()); 125 | }); 126 | 127 | gulp.task('style-fix', function () { 128 | return gulp.src(['lib/**', 'test/**'], { base: './' }) 129 | .pipe(jscs({ 130 | configPath: '.jscsrc', 131 | fix: true 132 | })) 133 | .pipe(gulp.dest('./')); 134 | }); 135 | 136 | function getVersion() { 137 | return JSON.parse(fs.readFileSync('package.json', 'utf8')).version; 138 | } 139 | 140 | gulp.task('release', function(cb) { 141 | return runSequence( 142 | 'bump', 143 | 'build-release', 144 | 'git-tag', 145 | cb 146 | ); 147 | }); 148 | 149 | gulp.task('git-tag', function(cb) { 150 | var version = getVersion(); 151 | return gulp.src(["release", "package.json", "bower.json"]) 152 | .pipe(git.commit('bump version')) 153 | .pipe( 154 | prompt.prompt({ 155 | type: 'input', 156 | name: 'message', 157 | message: "A simple message for tagging v" + version + ":" 158 | }, function(res) { 159 | var message = res.message; 160 | return git.tag("v" + version, message, {args: '-a'}, function(err){ 161 | if (!err) { 162 | console.log("Successfully tagged v" + version); 163 | git.push('origin', "master", {args: '--tags'}, function(err){ 164 | if(err){ 165 | throw err; 166 | } 167 | }); 168 | } 169 | else { 170 | throw err; 171 | } 172 | }); 173 | }) 174 | ); 175 | }); 176 | 177 | gulp.task('build-release',function(cb){ 178 | return runSequence( 179 | 'style-fix', 180 | 'js-doc', 181 | ['clean'], 182 | ['js-minify', 'css-minify'], 183 | 'prepend-version', 184 | 'copy-dist', 185 | cb 186 | ); 187 | }); 188 | 189 | function releaseHeader() { 190 | var license = fs.readFileSync('LICENSE.txt', 'utf8'), 191 | packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8')), 192 | version = packageJson.version, 193 | repository = packageJson.repository, 194 | name = packageJson.name, 195 | description = packageJson.description; 196 | 197 | return [ "/**", 198 | [" *", name, "v"+version, "-", description].join(" "), 199 | " *", 200 | [" *", repository].join(" "), 201 | " *", 202 | license, 203 | " */\n" 204 | ].join('\n') 205 | 206 | } 207 | 208 | gulp.task('prepend-version', function(){ 209 | return gulp.src(['dist/javascripts/' + projectName + '.js', 210 | 'dist/stylesheets/' + projectName + '.css'], {base: 'dist/'}) 211 | .pipe(insert.prepend(releaseHeader())) 212 | .pipe(gulp.dest('dist/')) 213 | }) 214 | 215 | gulp.task('copy-dist', function(){ 216 | return gulp.src(['dist/javascripts/'+projectName+'.js' 217 | ,'dist/javascripts/'+projectName+'.min.js' 218 | ,'dist/stylesheets/'+projectName+'.css' 219 | ,'dist/stylesheets/'+projectName+'.min.css' 220 | ]) 221 | .pipe(gulp.dest('release/')); 222 | }) 223 | 224 | gulp.task('js-minify', ['js'], function() { 225 | return gulp.src('./dist/javascripts/' + projectName + '.js') 226 | .pipe(uglify()) 227 | .pipe(rename(projectName + '.min.js')) 228 | .pipe(gulp.dest('./dist/javascripts')); 229 | }) 230 | gulp.task('css-minify', ['sass'], function() { 231 | return gulp.src('./dist/stylesheets/**/*') 232 | .pipe(cleanCSS({ compatibility: 'ie8' })) 233 | .pipe(rename(projectName + '.min.css')) 234 | .pipe(gulp.dest('./dist/stylesheets')); 235 | }) 236 | 237 | gulp.task('clean', function () { 238 | return del([paths.distDir, paths.releaseDir]); 239 | }); 240 | 241 | var versionBumpType; 242 | gulp.task('bump-prompt', function(cb) { 243 | return gulp.src(['./package.json']) 244 | .pipe(prompt.prompt({ 245 | type: 'checkbox', 246 | name: 'bump', 247 | message: 'What would you like to bump? Choose one, or leave empty to skip', 248 | choices: ['patch', 'minor', 'major'] 249 | }, function(res) { 250 | if (res.bump.length > 1) { 251 | throw Error("Y U NO FOLLOW INSTRUCTIONS!"); 252 | } 253 | versionBumpType = res.bump[0] 254 | })); 255 | }) 256 | 257 | gulp.task('bump', ['bump-prompt'], function() { 258 | return gulp.src(['./package.json']) 259 | .pipe(bump({ type: versionBumpType })) 260 | .pipe(gulp.dest('./')); 261 | }); 262 | 263 | gulp.task('lint', () => 264 | gulp.src([ 265 | paths.allSrcJs, 266 | paths.gulpFile, 267 | paths.webpackFile 268 | ]) 269 | .pipe(eslint()) 270 | .pipe(eslint.format()) 271 | .pipe(eslint.failAfterError()) 272 | ); 273 | -------------------------------------------------------------------------------- /lib/chariot.js: -------------------------------------------------------------------------------- 1 | /* global 2 | history, location 3 | */ 4 | 5 | /* Please refer to example page to see how a typical configuration is structured */ 6 | 7 | import Tutorial from './tutorial'; 8 | 9 | require('./ie-shim'); 10 | 11 | let initialState = true; 12 | 13 | class Chariot { 14 | /** 15 | * The master Chariot configuration dictionary can consist of multiple 16 | * tutorial configurations. 17 | * @typedef ChariotConfiguration 18 | * @property {Object.} config - The main configuration 19 | * containing all tutorials. 20 | * 21 | */ 22 | 23 | /** 24 | * The delegate optionally responds to lifecycle callbacks from Chariot. 25 | * @typedef ChariotDelegate 26 | * @property {Object} delegate - The object that responds to the 27 | * following lifecycle callbacks. 28 | * 29 | *
    30 | *
  1. willBeginTutorial
  2. 31 | *
  3. The following are repeated for each step.
  4. 32 | *
      33 | *
    1. willBeginStep
    2. 34 | *
    3. willRenderOverlay
    4. 35 | *
    5. didShowOverlay
    6. 36 | *
    7. willRenderTooltip
    8. 37 | *
    9. didRenderTooltip
    10. 38 | *
    11. didFinishStep
    12. 39 | *
    40 | *
  5. didFinishTutorial
  6. 41 | *
42 | */ 43 | 44 | /** 45 | * Called once before a tutorial begins. 46 | * @callback willBeginTutorial 47 | * @param {Tutorial} tutorial - The Tutorial object 48 | */ 49 | 50 | /** 51 | * Called once after a tutorial is finished. 52 | * @callback didFinishTutorial tutorial 53 | * @param {Tutorial} tutorial - The Tutorial object 54 | * @param {boolean} forced - Indicates whether tutorial was forced to end 55 | */ 56 | 57 | /** 58 | * Called once before each step begins. 59 | * Return a promise here if you have async callbacks you want resolved before 60 | * continuing. 61 | * @callback willBeginStep 62 | * @param {Step} step - The current Step object 63 | * @param {int} stepIndex - Index of current Step 64 | * @param {Tutorial} tutorial - The Tutorial object corresponding to this Step 65 | * @returns {Promise} [promise] Return a promise if you have async callbacks 66 | * that must be resolved before continuing. 67 | */ 68 | 69 | /** 70 | * Called once after each step is finished. 71 | * @callback didFinishStep 72 | * @param {Step} step - The current Step object 73 | * @param {int} stepIndex - Index of current Step 74 | * @param {Tutorial} tutorial - The Tutorial object corresponding to this Step 75 | * @returns {Promise} [promise] Return a promise if you have async callbacks 76 | * that must be resolved before continuing. 77 | */ 78 | 79 | /** 80 | * Called once before each overlay is shown. 81 | * @callback willShowOverlay 82 | * @param {Overlay} overlay - The current Overlay object 83 | * @param {int} stepIndex - Index of current Step 84 | * @param {Tutorial} tutorial - The Tutorial object corresponding to this Step 85 | * @returns {Promise} [promise] Return a promise if you have async callbacks 86 | * that must be resolved before continuing. 87 | */ 88 | 89 | /** 90 | * Called once after each overlay is shown. 91 | * @callback didShowOverlay 92 | * @param {Overlay} overlay - The current Overlay object 93 | * @param {int} stepIndex - Index of current Step 94 | * @param {Tutorial} tutorial - The Tutorial object corresponding to this Step 95 | * @returns {Promise} [promise] Return a promise if you have async callbacks 96 | * that must be resolved before continuing. 97 | */ 98 | 99 | /** 100 | * Called once before each tooltip is rendered. 101 | * @callback willRenderTooltip 102 | * @param {Tooltip} tooltip - The current Tooltip object 103 | * @param {int} stepIndex - Index of current Step 104 | * @param {Tutorial} tutorial - The Tutorial object corresponding to this Step 105 | * @returns {Promise} [promise] Return a promise if you have async callbacks 106 | * that must be resolved before continuing. 107 | */ 108 | 109 | /** 110 | * Called once after each tooltip is rendered. 111 | * @callback didRenderTooltip 112 | * @param {Tooltip} tooltip - The current Tooltip object 113 | * @param {int} stepIndex - Index of current Step 114 | * @param {Tutorial} tutorial - The Tutorial object corresponding to this Step 115 | * @returns {Promise} [promise] Return a promise if you have async callbacks 116 | * that must be resolved before continuing. 117 | */ 118 | 119 | /** 120 | * @constructor 121 | * @param {ChariotConfiguration} config - The main configuration for all 122 | * tutorials 123 | * @param {ChariotDelegate} [delegate] - An optional delegate that responds to 124 | * lifecycle callbacks 125 | */ 126 | constructor(config, delegate) { 127 | this.config = config; 128 | this.delegate = delegate; 129 | this.tutorials = {}; 130 | this._readConfig(config); 131 | } 132 | 133 | /** 134 | * Sets the chariot delegate. 135 | * @param {ChariotDelegate} [delegate] - An object that responds to 136 | * lifecycle callbacks 137 | */ 138 | setDelegate(delegate) { 139 | this.delegate = delegate; 140 | } 141 | 142 | /** 143 | * Starts a tutorial with the given name. 144 | * Won't start a tutorial if one is currently running. 145 | * @param {string} name - Name of the tutorial to start 146 | * @returns {Tutorial} tutorial - The Tutorial object, or undefined if 147 | * another tutorial is currently active. 148 | */ 149 | startTutorial(name) { 150 | if (this.currentTutorial()) { 151 | return; 152 | } 153 | const tutorial = this.tutorials[name]; 154 | tutorial.start(); 155 | return tutorial; 156 | } 157 | 158 | /** 159 | * Ends the current tutorial. 160 | * @returns {undefined} 161 | */ 162 | endTutorial() { 163 | const tutorial = this.currentTutorial(); 164 | tutorial.end(true); 165 | } 166 | 167 | /** 168 | * Returns the current tutorial, if any. 169 | * @returns {Tutorial} tutorial - The current tutorial, or null if none active 170 | */ 171 | currentTutorial() { 172 | for (let tutorialName in this.tutorials) { 173 | let tutorial = this.tutorials[tutorialName]; 174 | if (tutorial.isActive()) return tutorial; 175 | } 176 | } 177 | 178 | /** 179 | * Static method for creating a Tutorial object without needing to instantiate 180 | * chariot with a large configuration and named tutorials. 181 | * @param {TutorialConfiguration} config - The tutorial configuration 182 | * @param {ChariotDelegate} [delegate] - An optional delegate that responds to 183 | * lifecycle callbacks 184 | */ 185 | static createTutorial(config, delegate) { 186 | return new Tutorial(config, '', delegate); 187 | } 188 | 189 | /** 190 | * Static method for creating and starting a Tutorial object without needing 191 | * to instantiate chariot with a large configuration and named tutorials. 192 | * @param {TutorialConfiguration} config - The tutorial configuration 193 | * @param {ChariotDelegate} [delegate] - An optional delegate that responds to 194 | * lifecycle callbacks 195 | */ 196 | static startTutorial(config, delegate) { 197 | const tutorial = this.createTutorial(config, delegate); 198 | tutorial.start(); 199 | return tutorial; 200 | } 201 | 202 | toString() { 203 | return `[Chariot - config: ${this.config}, tutorials: {this.tutorials}]`; 204 | } 205 | 206 | //// PRIVATE 207 | 208 | _readConfig(config) { 209 | if (!config || typeof config !== 'object') { 210 | throw new Error(`Config must contains a tutorials hash.\n${this}`); 211 | } 212 | for (let tutorialName in config) { 213 | this.tutorials[tutorialName] = new Tutorial( 214 | config[tutorialName], tutorialName, this.delegate); 215 | } 216 | } 217 | } 218 | 219 | export 220 | default Chariot; 221 | -------------------------------------------------------------------------------- /lib/constants.js: -------------------------------------------------------------------------------- 1 | class Constant { 2 | constructor(options={}) { 3 | this.OVERLAY_Z_INDEX = options.overlayZIndex || 20; 4 | this.CLONE_Z_INDEX = this.OVERLAY_Z_INDEX + 1; 5 | this.TOOLTIP_Z_INDEX = this.CLONE_Z_INDEX + 1; 6 | } 7 | 8 | reload(options={}) { 9 | this.OVERLAY_Z_INDEX = options.overlayZIndex || 20; 10 | this.CLONE_Z_INDEX = this.OVERLAY_Z_INDEX + 1; 11 | this.TOOLTIP_Z_INDEX = this.CLONE_Z_INDEX + 1; 12 | } 13 | } 14 | 15 | export default new Constant(); 16 | -------------------------------------------------------------------------------- /lib/ie-shim.js: -------------------------------------------------------------------------------- 1 | // this shim is to fix IE & Firefox's problem where 2 | // getComputedStyle().cssText returns an empty string rather than a 3 | // string of computed CSS styles for the element 4 | if (typeof navigator !== "undefined" && navigator.userAgent.match(/msie|windows|firefox/i)) { 5 | Node.prototype.getComputedCSSText = function() { 6 | var s = []; 7 | var cssTranslation = { "cssFloat": "float" } 8 | var computedStyle = document.defaultView.getComputedStyle(this); 9 | for (var propertyName in computedStyle) { 10 | if ("string" == typeof(computedStyle[propertyName]) && 11 | computedStyle[propertyName] != "") { 12 | var translatedName = cssTranslation[propertyName] || propertyName; 13 | s[s.length] = (translatedName.replace(/[A-Z]/g, function(x) { 14 | return "-" + (x.toLowerCase()) 15 | })) + ": " + computedStyle[propertyName]; 16 | } 17 | } 18 | 19 | return s.join('; ') + ";"; 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /lib/overlay.js: -------------------------------------------------------------------------------- 1 | import Constants from './constants'; 2 | 3 | class Overlay { 4 | 5 | /** 6 | * @constructor 7 | * 8 | */ 9 | constructor(config) { 10 | this.shouldOverlay = config.shouldOverlay === undefined ? true : config.shouldOverlay; 11 | this.overlayColor = config.overlayColor || 'rgba(255,255,255,0.8)'; 12 | this.useTransparentOverlayStrategy = !!config.useTransparentOverlayStrategy; 13 | this._resizeHandler = null; 14 | this.disableCloneInteraction = config.disableCloneInteraction === undefined ? true : config.disableCloneInteraction; 15 | } 16 | 17 | isVisible() { 18 | return this.shouldOverlay === false; 19 | } 20 | 21 | render() { 22 | if (this.isVisible()) return; 23 | 24 | this.$document = $(document); 25 | let $body = $('body'); 26 | let $overlay = this._createOverlay(); 27 | $body.append($overlay); 28 | this.$overlay = $overlay; 29 | 30 | if (this.disableCloneInteraction) { 31 | const $transparentOverlay = this._createTransparentOverlay(); 32 | $body.append($transparentOverlay); 33 | this.$transparentOverlay = $transparentOverlay; 34 | } 35 | } 36 | 37 | isTransparentOverlayStrategy() { 38 | return this.useTransparentOverlayStrategy; 39 | } 40 | 41 | // The following 2 methods are part of the "clone element" strategy 42 | 43 | /** 44 | * Shows a background overlay to obscure the main interface, and acts as the 45 | * background for the cloned elements involved in the tutorial. 46 | * This method is involved in the "clone element" strategy. 47 | */ 48 | showBackgroundOverlay() { 49 | // Remove the resize handler that might exist from focusOnElement 50 | // (Note: take care to not call this after cloning elements, because they 51 | // have their own window resize handlers) 52 | let $window = $(window); 53 | 54 | this.$overlay.css({ 55 | background: this.overlayColor, 56 | border: 'none' 57 | }); 58 | 59 | this._resizeOverlayToFullScreen(); 60 | this._resizeHandler = this._resizeOverlayToFullScreen.bind(this); 61 | } 62 | 63 | /** 64 | * Shows a transparent overlay to prevent user from interacting with cloned 65 | * elements. 66 | */ 67 | showTransparentOverlay() { 68 | this.$transparentOverlay.show(); 69 | } 70 | 71 | /** 72 | * Focuses on an element by resizing a transparent overlay to match its 73 | * dimensions and changes the borders to be colored to obscure the main UI. 74 | * This method is involved in the "transparent overlay" strategy. 75 | */ 76 | focusOnElement($element) { 77 | // Hide overlay from showTransparentOverlay 78 | this.$transparentOverlay.hide(); 79 | 80 | this._resizeOverlayToElement($element); 81 | this._resizeHandler = this._resizeOverlayToElement.bind(this, $element); 82 | } 83 | 84 | resize() { 85 | this._resizeHandler(); 86 | } 87 | 88 | tearDown() { 89 | this.$overlay.remove(); 90 | if (this.$transparentOverlay) { 91 | this.$transparentOverlay.remove(); 92 | } 93 | } 94 | 95 | toString() { 96 | return `[Overlay - shouldOverlay: ${this.shouldOverlay}, ` + 97 | `overlayColor: ${this.overlayColor}]`; 98 | } 99 | 100 | //// PRIVATE 101 | 102 | _createOverlay() { 103 | let $overlay = $("
"); 104 | $overlay.css({ 'z-index': Constants.OVERLAY_Z_INDEX }); 105 | return $overlay; 106 | } 107 | 108 | _createTransparentOverlay() { 109 | let $transparentOverlay = $("
"); 110 | $transparentOverlay.css({ 111 | 'z-index': Constants.CLONE_Z_INDEX + 1, 112 | width: this._documentWidth(), 113 | height: this._documentHeight() 114 | }); 115 | return $transparentOverlay; 116 | } 117 | 118 | // Used for clone element strategy 119 | _resizeOverlayToFullScreen() { 120 | this.$overlay.css({ 121 | width: this._documentWidth(), 122 | height: this._documentHeight() 123 | }); 124 | } 125 | 126 | _documentWidth() { 127 | const body = document.body; 128 | const html = document.documentElement; 129 | return Math.max(html.scrollWidth, html.offsetWidth, html.clientWidth, 130 | body.scrollWidth, body.offsetWidth); 131 | } 132 | 133 | _documentHeight() { 134 | const body = document.body; 135 | const html = document.documentElement; 136 | return Math.max(html.scrollHeight, html.offsetHeight, html.clientHeight, 137 | body.scrollHeight, body.offsetHeight); 138 | } 139 | 140 | // Used for transparent overlay strategy 141 | _resizeOverlayToElement($element) { 142 | // First position the overlay 143 | let offset = $element.offset(); 144 | 145 | // Then resize it 146 | let borderStyles = `solid ${this.overlayColor}`; 147 | let $document = this.$document; 148 | let docWidth = $document.outerWidth(); 149 | let docHeight = $document.outerHeight(); 150 | 151 | let width = $element.outerWidth(); 152 | let height = $element.outerHeight(); 153 | 154 | let leftWidth = offset.left; 155 | let rightWidth = docWidth - (offset.left + width); 156 | let topWidth = offset.top; 157 | let bottomWidth = docHeight - (offset.top + height); 158 | 159 | this.$overlay.css({ 160 | background: 'transparent', 161 | width, height, 162 | 'border-left': `${leftWidth}px ${borderStyles}`, 163 | 'border-top': `${topWidth}px ${borderStyles}`, 164 | 'border-right': `${rightWidth}px ${borderStyles}`, 165 | 'border-bottom': `${bottomWidth}px ${borderStyles}` 166 | }); 167 | } 168 | } 169 | 170 | export default Overlay; 171 | -------------------------------------------------------------------------------- /lib/step.js: -------------------------------------------------------------------------------- 1 | import debounce from 'lodash.debounce'; 2 | import Tooltip from './tooltip'; 3 | import Constants from './constants'; 4 | import Style from './style'; 5 | 6 | let MAX_ATTEMPTS = 100; 7 | let DOM_QUERY_DELAY = 100; 8 | 9 | let Promise = require('es6-promise').Promise; 10 | 11 | class Step { 12 | 13 | /** The step configuration is where you specify which elements of the page 14 | * will be cloned and placed over the overlay. These elements are the 15 | * what appear as highlighted to the user. 16 | * 17 | * @typedef StepConfiguration 18 | * @property {TooltipConfiguration} tooltip - Tooltip configuration. 19 | * @property {Object.|string[]|string} [selectors] - 20 | * Object with arbitrarily-named keys and CSS selector values. 21 | * These keys can then be referenced from TooltipConfiguration.anchorElement. 22 | * Or, an array of selector strings if named keys are not required. 23 | * Or, a string if only one selector is required.
24 | * Notes: Specifying a selector that targets another specified selector 25 | * will result in unpredictable behavior.
26 | * Specifying multiple selectors will effectively cause 27 | * Tutorial.useTransparentOverlayStrategy == false. 28 | */ 29 | 30 | /** 31 | * @constructor 32 | * @param {StepConfiguration} config - The configuration for this step 33 | * @param {integer} index - The index of this step within the current tutorial 34 | * @param {Tutorial} tutorial - The Tutorial object corresponding to this Step 35 | * @param {Overlay} overlay - The Overlay object displayed along with this 36 | * Step 37 | * @param {ChariotDelegate} [delegate] - An optional delegate that responds to 38 | * lifecycle callbacks 39 | */ 40 | constructor(config = {}, index, tutorial, overlay, delegate) { 41 | this.tutorial = tutorial; 42 | this.index = index; 43 | this.overlay = overlay; 44 | this.delegate = delegate || {}; 45 | 46 | if (!config.selectors) { 47 | throw new Error('selectors must be present in Step configuration\n' + 48 | this); 49 | } else if (typeof config.selectors === 'string') { 50 | this.selectors = { 0: config.selectors }; 51 | } else if (Object.prototype.toString.call(config.selectors) === '[object Object]') { 52 | if (!Object.keys(config.selectors).length) { 53 | throw new Error('selectors must be present in Step configuration\n' + 54 | this); 55 | } 56 | this.selectors = config.selectors; 57 | } else if (Array.isArray(config.selectors)) { 58 | const selectorsMap = {}; 59 | config.selectors.forEach((val, idx) => { 60 | selectorsMap[idx] = val; 61 | }); 62 | this.selectors = selectorsMap; 63 | } else { 64 | throw new Error('selectors must be an object, array, or string'); 65 | } 66 | 67 | this._resizeTimeout = null; 68 | 69 | this._elementMap = {}; 70 | for (let selectorName in this.selectors) { 71 | this._elementMap[selectorName] = {}; 72 | } 73 | 74 | this.tooltip = new Tooltip(config.tooltip, this, tutorial); 75 | } 76 | 77 | render() { 78 | Promise.resolve().then(() => { 79 | if (this.delegate.willBeginStep) { 80 | return this.delegate.willBeginStep( 81 | this, this.index, this.tutorial); 82 | } 83 | }).then(() => { 84 | if (this.delegate.willShowOverlay) { 85 | return this.delegate.willShowOverlay( 86 | this.overlay, this.index, this.tutorial); 87 | } 88 | }).then(() => { 89 | // Show a temporary background overlay while we wait for elements 90 | this.overlay.showBackgroundOverlay(); 91 | return this._waitForElements(); 92 | }).then(() => { 93 | // Render the overlay 94 | if (this.overlay.isTransparentOverlayStrategy() && 95 | Object.keys(this.selectors).length === 1) { 96 | this._singleTransparentOverlayStrategy(); 97 | } else { 98 | this._clonedElementStrategy(); 99 | } 100 | }).then(() => { 101 | if (this.delegate.didShowOverlay) { 102 | return this.delegate.didShowOverlay( 103 | this.overlay, this.index, this.tutorial); 104 | } 105 | }).then(() => { 106 | if (this.delegate.willRenderTooltip) { 107 | return this.delegate.willRenderTooltip( 108 | this.tooltip, this.index, this.tutorial); 109 | } 110 | }).then(() => { 111 | this._renderTooltip(); 112 | if (this.delegate.didRenderTooltip) { 113 | return this.delegate.didRenderTooltip( 114 | this.tooltip, this.index, this.tutorial); 115 | } 116 | }).then(() => { 117 | // Resize the overlay in case the tooltip extended the width/height of DOM 118 | this.overlay.resize(); 119 | 120 | // Setup resize handler 121 | this._resizeHandler = debounce(() => { 122 | for (let selectorName in this.selectors) { 123 | let elementInfo = this._elementMap[selectorName]; 124 | if (elementInfo.clone) { 125 | let $element = elementInfo.element; 126 | let $clone = elementInfo.clone; 127 | Style.clearCachedStylesForElement($element); 128 | this._applyComputedStyles($clone, $element); 129 | this._positionClone($clone, $element); 130 | } 131 | } 132 | this.tooltip.reposition(); 133 | this.overlay.resize(); 134 | }, 50); 135 | $(window).on('resize', this._resizeHandler); 136 | }).catch(error => { 137 | console.log(error); 138 | this.tutorial.tearDown(); 139 | }); 140 | } 141 | 142 | _moveTo(action) { 143 | Promise.resolve().then(() => { 144 | if (this.delegate.didFinishStep) { 145 | return this.delegate.didFinishStep( 146 | this, this.index, this.tutorial); 147 | } 148 | }).then(() => { 149 | action(); 150 | }).catch(error => { 151 | console.log(error); 152 | action(); 153 | }); 154 | } 155 | 156 | previous() { 157 | this._moveTo(() => this.tutorial.next(this.index - 1)); 158 | } 159 | 160 | next() { 161 | this._moveTo(() => this.tutorial.next()); 162 | } 163 | 164 | getClonedElement(selectorName) { 165 | let elementInfo = this._elementMap[selectorName]; 166 | if (!elementInfo) return; 167 | return elementInfo.clone; 168 | } 169 | 170 | tearDown() { 171 | let $window = $(window); 172 | for (let selectorName in this.selectors) { 173 | let selector = this.selectors[selectorName] 174 | // Remove computed styles 175 | Style.clearCachedStylesForElement($(selector)); 176 | let elementInfo = this._elementMap[selectorName]; 177 | if (elementInfo.clone) { 178 | // Remove cloned elements 179 | elementInfo.clone.remove(); 180 | } 181 | } 182 | this.tooltip.tearDown(); 183 | 184 | $window.off('resize', this._resizeHandler); 185 | } 186 | 187 | prepare() { 188 | // FIX: This method currently always prepares for the clone strategy, 189 | // regardless of the value of useTransparentOverlayStrategy. 190 | // Perhaps add a check or rename this method, once the coupling to 191 | // this.tutorial.prepare() is removed 192 | for (let selectorName in this.selectors) { 193 | let selector = this.selectors[selectorName] 194 | this._computeStyles($(selector)); 195 | } 196 | } 197 | 198 | toString() { 199 | return `[Step - index: ${this.index}, ` + 200 | `selectors: ${JSON.stringify(this.selectors)}]`; 201 | } 202 | 203 | //// PRIVATE 204 | 205 | _singleTransparentOverlayStrategy() { 206 | // Only use an overlay 207 | let selectorName = Object.keys(this.selectors)[0]; 208 | let $element = this._elementMap[selectorName].element; 209 | this.overlay.focusOnElement($element); 210 | } 211 | 212 | _clonedElementStrategy() { 213 | // Clone elements if multiple selectors 214 | this._cloneElements(this.selectors); 215 | if (this.overlay.disableCloneInteraction) { 216 | this.overlay.showTransparentOverlay(); 217 | } 218 | } 219 | 220 | _renderTooltip() { 221 | this.tooltip.render(); 222 | } 223 | 224 | _waitForElements() { 225 | let promises = []; 226 | for (let selectorName in this.selectors) { 227 | let promise = new Promise((resolve, reject) => { 228 | this._waitForElement(selectorName, 0, resolve, reject); 229 | }); 230 | promises.push(promise); 231 | } 232 | 233 | return Promise.all(promises); 234 | } 235 | 236 | _waitForElement(selectorName, numAttempts, resolve, reject) { 237 | let selector = this.selectors[selectorName]; 238 | let element = $(selector); 239 | if (element.length == 0) { 240 | ++numAttempts; 241 | if (numAttempts == MAX_ATTEMPTS) { 242 | reject(`Selector not found: ${selector}`); 243 | } else { 244 | window.setTimeout(() => { 245 | this._waitForElement(selectorName, numAttempts, resolve, reject); 246 | }, DOM_QUERY_DELAY); 247 | } 248 | } else { 249 | this._elementMap[selectorName].element = element; 250 | resolve(); 251 | 252 | // TODO: fire event when element is ready. Tutorial will listen and call 253 | // prepare() on all steps 254 | } 255 | } 256 | 257 | _computeStyles($element) { 258 | Style.getComputedStylesFor($element[0]); 259 | $element.children().toArray().forEach(child => { 260 | this._computeStyles($(child)); 261 | }); 262 | } 263 | 264 | _cloneElements(selectors) { 265 | if (this.overlay.isVisible()) return; 266 | 267 | setTimeout(() => { 268 | this.tutorial.prepare(); 269 | }, 0); 270 | for (let selectorName in selectors) { 271 | let clone = this._cloneElement(selectorName); 272 | this._elementMap[selectorName].clone = clone; 273 | } 274 | } 275 | 276 | _cloneElement(selectorName) { 277 | let $element = this._elementMap[selectorName].element; 278 | if ($element.length == 0) { return null; } 279 | 280 | let $clone = $element.clone(); 281 | $('body').append($clone); 282 | this._applyComputedStyles($clone, $element); 283 | this._positionClone($clone, $element); 284 | 285 | return $clone; 286 | } 287 | 288 | _applyComputedStyles($clone, $element) { 289 | if (!$element.is(":visible")) { 290 | return; 291 | } 292 | $clone.addClass('chariot-clone'); 293 | Style.cloneStyles($element, $clone); 294 | if (this.overlay.disableCloneInteraction) { 295 | $clone.css('pointer-events', 'none'); 296 | } 297 | let clonedChildren = $clone.children().toArray(); 298 | $element.children().toArray().forEach((child, index) => { 299 | this._applyComputedStyles($(clonedChildren[index]), $(child)); 300 | }); 301 | } 302 | 303 | _positionClone($clone, $element) { 304 | $clone.css({ 305 | 'z-index': Constants.CLONE_Z_INDEX, 306 | position: 'absolute' 307 | }); 308 | $clone.offset($element.offset()); 309 | } 310 | } 311 | 312 | export default Step; 313 | -------------------------------------------------------------------------------- /lib/style.js: -------------------------------------------------------------------------------- 1 | const CHARIOT_COMPUTED_STYLE_CLASS_PREFIX = 'chariot_computed_styles'; 2 | 3 | class Style { 4 | static calculateLeft($tooltip, $anchor, xOffsetTooltip, position, arrowOffset) { 5 | let offset = 0; 6 | switch (position) { 7 | case 'left': 8 | offset = $anchor.offset().left - $tooltip.outerWidth() + xOffsetTooltip - arrowOffset; 9 | break; 10 | case 'right': 11 | offset = $anchor.offset().left + $anchor.outerWidth() + xOffsetTooltip + arrowOffset; 12 | break; 13 | case 'top': 14 | case 'bottom': 15 | offset = $anchor.offset().left + $anchor.outerWidth() / 2 - 16 | $tooltip.outerWidth() / 2 + xOffsetTooltip; 17 | break; 18 | default: 19 | break; 20 | } 21 | return offset; 22 | } 23 | 24 | static calculateTop($tooltip, $anchor, yOffsetTooltip, position, arrowOffset) { 25 | let offset = 0; 26 | switch (position) { 27 | case 'top': 28 | offset = $anchor.offset().top - $tooltip.outerHeight() + yOffsetTooltip - arrowOffset; 29 | break; 30 | case 'bottom': 31 | offset = $anchor.offset().top + $anchor.outerHeight() + yOffsetTooltip + arrowOffset; 32 | break; 33 | case 'left': 34 | case 'right': 35 | offset = $anchor.offset().top + $anchor.outerHeight() / 2 - 36 | $tooltip.outerHeight() / 2 + yOffsetTooltip; 37 | break; 38 | default: 39 | break; 40 | } 41 | return offset; 42 | } 43 | 44 | static getComputedStylesFor(element) { 45 | if (element._chariotComputedStyles) { 46 | return element._chariotComputedStyles; 47 | } else { 48 | return this._cacheStyleFor(element); 49 | } 50 | } 51 | 52 | static cloneStyles($element, $clone) { 53 | let start = new Date().getTime(); 54 | let cssText = this.getComputedStylesFor($element[0]); 55 | $clone[0].style.cssText = cssText; 56 | 57 | // Fixes IE border box boxing model 58 | if (navigator.userAgent.match(/msie|windows/i)) { 59 | this._ieBoxModelStyleFix('width', $clone, cssText); 60 | this._ieBoxModelStyleFix('height', $clone, cssText); 61 | } 62 | //this._clonePseudoStyle($element, $clone, 'before'); 63 | //this._clonePseudoStyle($element, $clone, 'after'); 64 | } 65 | 66 | static clearCachedStylesForElement($element) { 67 | if (!$element || !$element.length) return; 68 | $element[0]._chariotComputedStyles = null; 69 | $element.children().each((index, child) => { 70 | this.clearCachedStylesForElement($(child)); 71 | }); 72 | } 73 | 74 | static _ieBoxModelStyleFix(style, $ele, cssText) { 75 | let match = cssText.match(new RegExp(`; ${style}: ([^;]*)`)); 76 | let value = (match && match.length > 1) ? parseInt(match[1]) : 0; 77 | if (value !== 0 && !isNaN(value)) { 78 | $ele[style](value); 79 | } 80 | } 81 | 82 | static _generateUniqueClassName(prefix = 'class') { 83 | return `${prefix}_${Math.floor(Math.random() * 1000000)}`; 84 | } 85 | 86 | // NOTE: unused currently 87 | static _clonePseudoStyle($element, $clone, pseudoClass) { 88 | let pseudoStyle = window.getComputedStyle($element[0], `:${pseudoClass}` ); 89 | if (pseudoStyle.content && pseudoStyle.content !== '') { 90 | let className = this._generateUniqueClassName(); 91 | $clone.addClass(className); 92 | document.styleSheets[0].insertRule(`.${className}::${pseudoClass} { 93 | ${pseudoStyle.cssText}; content: ${pseudoStyle.content}; }`, 94 | 0); 95 | } 96 | } 97 | 98 | /* 99 | Known issues: 100 | - FF bug does not correctly copy CSS margin values 101 | (https://bugzilla.mozilla.org/show_bug.cgi?id=381328) 102 | - IE9 does not implement getComputedCSSText() 103 | */ 104 | static _cacheStyleFor(element) { 105 | // check for IE getComputedCSSText() 106 | let computedStyles; 107 | if (navigator.userAgent.match(/msie|windows|firefox/i)) { 108 | computedStyles = element.getComputedCSSText(); 109 | } else { 110 | computedStyles = document.defaultView.getComputedStyle(element).cssText; 111 | } 112 | 113 | Object.defineProperty(element, '_chariotComputedStyles', { 114 | value: computedStyles, 115 | enumerable: false, 116 | writable: true, 117 | configurable: false 118 | }); 119 | 120 | return computedStyles; 121 | } 122 | } 123 | export default Style; 124 | -------------------------------------------------------------------------------- /lib/tutorial.js: -------------------------------------------------------------------------------- 1 | import Step from './step'; 2 | import Overlay from './overlay'; 3 | const Promise = require('es6-promise').Promise; 4 | import Constant from './constants'; 5 | 6 | const DEFAULT_SCROLL_ANIMATION_DURATION = 500; 7 | 8 | class Tutorial { 9 | /** 10 | *

The tutorial configuration is where the steps of a tutorial are specified, 11 | * and also allows customization of the overlay style. 12 | * If optional configuration parameters are not required, the steps property 13 | * array can be passed in directly as the configuration.

14 | * 15 | *

Notes on implementation:

16 | *

The elements defined in each step of a tutorial via 17 | * StepConfiguration.selectors are highlighted using transparent overlays.

18 | *

These elements areare overlaid using one of two strategies:

19 | *
    20 | *
  1. Semi-transparent overlay with a transparent section cut out over the 21 | * element
  2. 22 | *
  3. Selected elements are cloned and placed above a transparent overlay
  4. 23 | *
24 | * 25 | *

#1 is more performant, but issues arise when an element is not rectangularly- 26 | * shaped, or when it has `:before` or `:after` 27 | * pseudo-selectors that insert new DOM elements that protrude out of the 28 | * main element.

29 | *

#2 is slower because of deep CSS style cloning, but it will correctly render 30 | * the entire element in question, regardless of shape or size.

31 | *

However, there are edge cases where Firefox 32 | * will not clone CSS margin attribute of children elements. 33 | * In those cases, the delegate callbacks should be utilized to fix. 34 | * Note however, that #2 is always chosen if multiple selectors are specified in 35 | * StepConfiguration.selectors.

36 | * 37 | * @typedef TutorialConfiguration 38 | * @property {StepConfiguration[]} steps - An array of step configurations (see below). 39 | * Note that this property can be passed as the configuration if the 40 | * optional params below are not used. 41 | * @property {integer} [zIndex=20] - Sets the base z-index value used by this tutorial 42 | * @property {boolean} [shouldOverlay=true] - Setting to false will disable the 43 | * overlay that normally appears over the page and behind the tooltips. 44 | * @property {string} [overlayColor='rgba(255,255,255,0.7)'] - Overlay CSS color 45 | * @property {boolean} [disableCloneInteraction=true] - Setting to false will allow the 46 | * user to interact with the cloned element (eg. click on links) 47 | * @property {Tutorial-onCompleteCallback} [onComplete] - Callback that is called 48 | * once the tutorial has gone through all steps. 49 | * @property {boolean} [useTransparentOverlayStrategy=false] -

50 | * Setting to true will use an implementation that does not rely on 51 | * cloning highlighted elements.
52 | * Note: This value is ignored if multiple selectors are 53 | * specified in StepConfiguration.selectors.

54 | *

useTransparentOverlayStrategy focuses on an element by 55 | * resizing a transparent overlay to match its dimensions and changes the 56 | * borders to be colored to obscure the main UI.

57 | 58 |

Strategy Details

59 |

60 | By default, a tutorial is displayed with a semi-transparent overlay 61 | that hides background content and highlights the selected element(s) for the 62 | current step of the tutorial. 63 |

64 | This is achieved by one of two exclusive strategies: 65 |
    66 |
  1. 67 | An overlay div with a semi-transparent border but with a transparent center 68 | equal in size to the selected element.
    69 | Example: A 50x50 div is the selected element, so the overlay's transparent center 70 | is 50x50, and its semi-transparent border fills the rest of the viewport. 71 |
  2. 72 |
  3. 73 | A completely semi-transparent overlay fills the entire viewport, and the 74 | selected element(s) is cloned and placed on top of this overlay, using 75 | z-index. 76 |
  4. 77 |
78 | 79 |

Both strategies have pros & cons.

80 | 1. Clone strategy (default) 81 |

82 | Pros: It will correctly render the entire element in question, 83 | regardless of shape or size. 84 |

85 |

86 | Cons: Slow because of the deep-cloning involved with CSS styling. The more 87 | children elements that exist, the slower each step will take to render. 88 | (This can be improved over time by pre-caching the next step in advance.) 89 | There are also edge cases where Firefox will not clone the 90 | CSS `margin` attribute of children elements. 91 |
92 | In those cases, the callbacks Step.beforeCallback and 93 | Step.afterCallback can be used to properly restore the margin. 94 |

95 | 96 | 2. Background overlay with transparent center and semi-transparent border 97 |

98 | Pros:: More performant than the clone strategy because styles are not being cloned. 99 |

100 |

101 | Cons: When an element is not rectangular in shape, or 102 | when it has :before or :after pseudo-selectors 103 | that insert new DOM elements that protrude out of the main element, 104 | the transparent center will either reveal or occlude sections of 105 | the element. 106 |

107 | 108 | Note: The clone strategy is always chosen if multiple selectors are 109 | specified in StepConfiguration.selectors. 110 | 111 | 112 | * @property {boolean} [animateTooltips=true] - Enables tooltip bouncing at the 113 | * beginning and end of each step. 114 | * @property {boolean} [animateScrolling=true] - 115 | *

If the next tooltip is not completely within the client bounds, this 116 | * property animates the scrolling of the viewport until the next tooltip 117 | * is centered.

118 | *

If false, the viewport is not scrolled.

animateScrolling is false. 122 | * @property {boolean} [allowSteppingBackward=false] - Enables returning to previous steps. 123 | */ 124 | 125 | /** 126 | * @constructor 127 | * @param {TutorialConfiguration} config - The configuration for this tutorial 128 | * @param {string} [name] - Name of the tutorial 129 | * @param {ChariotDelegate} [delegate] - An optional delegate that responds to 130 | * lifecycle callbacks 131 | */ 132 | constructor(config, name, delegate) { 133 | this.name = name; 134 | this.delegate = delegate || {}; 135 | this.steps = []; 136 | 137 | const configType = Object.prototype.toString.call(config); 138 | let stepConfigs, overlayConfig; 139 | if (configType === '[object Array]') { 140 | stepConfigs = config; 141 | overlayConfig = {}; 142 | this.animateTooltips = true; 143 | this.animateScrolling = true; 144 | this.scrollAnimationDuration = DEFAULT_SCROLL_ANIMATION_DURATION; 145 | this.allowSteppingBackward = false; 146 | } else if (configType === '[object Object]') { 147 | if (!Array.isArray(config.steps)) { 148 | throw new Error(`steps must be an array.\n${this}`); 149 | } 150 | this.zIndex = config.zIndex; 151 | this.useTransparentOverlayStrategy = config.useTransparentOverlayStrategy; 152 | this.animateTooltips = config.animateTooltips === undefined ? true : config.animateTooltips; 153 | this.animateScrolling = config.animateScrolling === undefined ? true : config.animateScrolling; 154 | this.scrollAnimationDuration = config.scrollAnimationDuration || DEFAULT_SCROLL_ANIMATION_DURATION; 155 | this.allowSteppingBackward = config.allowSteppingBackward; 156 | stepConfigs = config.steps; 157 | overlayConfig = config; 158 | } else { 159 | throw new Error('config must be an object or array'); 160 | } 161 | 162 | this.overlay = new Overlay(overlayConfig); 163 | stepConfigs.forEach((stepConfig, index) => { 164 | this.steps.push(new Step(stepConfig, index, this, this.overlay, this.delegate)); 165 | }); 166 | this._prepared = false; 167 | this._isActive = false; 168 | } 169 | 170 | /** 171 | * Indicates if this tutorial is currently active. 172 | * return {boolean} 173 | */ 174 | isActive() { 175 | return this._isActive; 176 | } 177 | 178 | /** 179 | * Starts the tutorial and marks itself as active. 180 | * @returns {Promise} 181 | */ 182 | start() { 183 | if (this.zIndex !== null) { 184 | Constant.reload({ overlayZIndex: this.zIndex }); 185 | } 186 | if (this.steps.length === 0) { 187 | throw new Error(`steps should not be empty.\n${this}`); 188 | return; 189 | } 190 | 191 | this._isActive = true; 192 | // render overlay first to avoid willBeingTutorial delay overlay showing up 193 | this.overlay.render(); 194 | return Promise.resolve().then(() => { 195 | if (this.delegate.willBeginTutorial) { 196 | return this.delegate.willBeginTutorial(this); 197 | } 198 | }).then(() => { 199 | this.currentStep = this.steps[0]; 200 | this.currentStep.render(); 201 | }).catch(() => { 202 | this.tearDown(); 203 | }); 204 | } 205 | 206 | /** 207 | * Prepares each step of the tutorial, to speedup rendering. 208 | * @returns {undefined} 209 | */ 210 | prepare() { 211 | if (this._prepared) return; 212 | this.steps.forEach(step => { 213 | step.prepare(); 214 | this._prepared = true; 215 | }); 216 | } 217 | 218 | /** 219 | * Advances to the next step in the tutorial, or ends tutorial if no more 220 | * steps. 221 | * 222 | * @param {integer|Step} [step] - If step is an integer, advances to that 223 | * step. If step is a Step instance, that step 224 | * If no argument is passed in, the current step's index is incremented to 225 | * determine the next step. 226 | * @returns {undefined} 227 | */ 228 | next(step) { 229 | let currentStepIndex = -1; 230 | if (step === undefined || step === null) { 231 | currentStepIndex = this.steps.indexOf(this.currentStep); 232 | if (currentStepIndex < 0) { 233 | throw new Error('step not found'); 234 | } else if (currentStepIndex === this.steps.length - 1) { 235 | this.end(); 236 | return; 237 | } 238 | } 239 | 240 | if (this.currentStep) { 241 | this.currentStep.tearDown(); 242 | } 243 | 244 | let nextStep; 245 | if (step !== undefined && step !== null) { 246 | if (typeof step === 'number') { 247 | if (step < 0 || step >= this.steps.length) { 248 | throw new Error(`step is outside bounds of steps array (length: ${this.steps.length})`); 249 | } 250 | nextStep = this.steps[step]; 251 | } else if (Object.prototype.toString.call(step) === '[object Object]') { 252 | nextStep = step; 253 | } else { 254 | throw new Error('step arg must be number or object'); 255 | } 256 | } else { 257 | nextStep = this.steps[currentStepIndex + 1]; 258 | } 259 | 260 | this.currentStep = nextStep; 261 | nextStep.render(); 262 | } 263 | 264 | /** 265 | * Returns the one-indexed (human-friendly) step number. 266 | * 267 | * @param {Step} step - The step instance for which we want the index 268 | * @returns {integer} stepNum - The one-indexed step number 269 | */ 270 | stepNum(step) { 271 | if (step === null) return null; 272 | return this.steps.indexOf(step) + 1; 273 | } 274 | 275 | /** 276 | * Tears down the internal overlay and tears down each individual step 277 | * Nulls out internal references. 278 | * @returns {undefined} 279 | */ 280 | tearDown() { 281 | this._prepared = false; 282 | this.overlay.tearDown(); 283 | this.steps.forEach(step => { 284 | step.tearDown(); 285 | }); 286 | this.currentStep = null; 287 | } 288 | 289 | /** 290 | * Retrieves the Step object at index. 291 | * @returns {Step} step 292 | */ 293 | getStep(index) { 294 | return this.steps[index]; 295 | } 296 | 297 | /** 298 | * Ends the tutorial by tearing down all the steps (and associated tooltips, 299 | * overlays). 300 | * Also marks itself as inactive. 301 | * @param {boolean} [forced=false] - Indicates whether tutorial was forced to 302 | * end 303 | * @returns {undefined} 304 | */ 305 | end(forced = false) { 306 | // Note: Order matters. 307 | this.tearDown(); 308 | 309 | return Promise.resolve().then(() => { 310 | if (this.delegate.didFinishTutorial) { 311 | return this.delegate.didFinishTutorial(this, forced); 312 | } 313 | }).then(() => { 314 | this._isActive = false; 315 | }); 316 | } 317 | 318 | toString() { 319 | return `[Tutorial - active: ${this._isActive}, ` + 320 | `useTransparentOverlayStrategy: ${this.useTransparentOverlayStrategy}, ` + 321 | `steps: ${this.steps}, overlay: ${this.overlay}]`; 322 | } 323 | 324 | } 325 | 326 | export default Tutorial; 327 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chariot-tooltips", 3 | "main": "release/chariot.js", 4 | "description": "A JavaScript library for creating beautiful in product tutorials", 5 | "version": "1.0.3", 6 | "repository": "https://github.com/zendesk/chariot-tooltips", 7 | "engines": { 8 | "node": ">=6" 9 | }, 10 | "author": "Herbert Siojo ", 11 | "devDependencies": { 12 | "babel": "^6.5.2", 13 | "babel-loader": "^6.2.7", 14 | "babel-plugin-add-module-exports": "^0.2.1", 15 | "babel-plugin-transform-es2015-modules-commonjs": "^6.18.0", 16 | "babel-preset-es2015": "^6.18.0", 17 | "babel-preset-latest": "^6.16.0", 18 | "babel-register": "^6.18.0", 19 | "babelify": "^7.0.0", 20 | "browserify": "^10.2.6", 21 | "chai": "^3.0.0", 22 | "del": "^2.2.2", 23 | "es5-shim": "^4.1.7", 24 | "es6-promise": "^2.3.0", 25 | "es6-shim": "^0.32.2", 26 | "eslint": "^3.9.1", 27 | "eslint-config-airbnb": "^12.0.0", 28 | "eslint-plugin-import": "^2.1.0", 29 | "eslint-plugin-jsx-a11y": "^2.2.3", 30 | "eslint-plugin-react": "^6.5.0", 31 | "glob": "^5.0.13", 32 | "gulp": "^3.9.1", 33 | "gulp-autoprefixer": "^2.3.1", 34 | "gulp-babel": "^6.1.2", 35 | "gulp-bump": "^0.3.1", 36 | "gulp-clean": "^0.3.1", 37 | "gulp-clean-css": "^2.0.13", 38 | "gulp-connect": "^2.2.0", 39 | "gulp-eslint": "^3.0.1", 40 | "gulp-git": "^1.2.4", 41 | "gulp-insert": "^0.4.0", 42 | "gulp-jscs": "^2.0.0", 43 | "gulp-mocha": "^2.1.2", 44 | "gulp-prompt": "^0.1.2", 45 | "gulp-rename": "^1.2.2", 46 | "gulp-run": "^1.6.8", 47 | "gulp-sass": "^2.0.3", 48 | "gulp-shell": "^0.4.2", 49 | "gulp-testem": "0.0.1", 50 | "gulp-uglify": "^1.2.0", 51 | "gulp-util": "^3.0.6", 52 | "jquery": ">3.0.0", 53 | "jscs": "^2.0.0", 54 | "jsdoc": "^3.3.2", 55 | "mocha": "^2.2.5", 56 | "node-sass": "^3.13.0", 57 | "phantomjs": "^2.1.7", 58 | "run-sequence": "^1.1.1", 59 | "sinon": "^1.15.4", 60 | "testem": "^0.8.5", 61 | "vinyl-source-stream": "^1.1.0", 62 | "webpack": "^1.13.3", 63 | "webpack-dev-server": "^1.16.2", 64 | "webpack-stream": "^3.2.0" 65 | }, 66 | "dependencies": { 67 | "lodash.debounce": "^3.1.1" 68 | }, 69 | "eslintConfig": { 70 | "extends": "airbnb", 71 | "plugins": [ 72 | "import" 73 | ] 74 | }, 75 | "scripts": { 76 | "jscs-fix": "node_modules/.bin/jscs lib/** test/** -x", 77 | "jscs": "node_modules/.bin/jscs lib/** test/**", 78 | "ci": "node_modules/.bin/gulp compile-test && node_modules/.bin/testem ci" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /release/chariot.css: -------------------------------------------------------------------------------- 1 | /** 2 | * chariot-tooltips v1.0.3 - A JavaScript library for creating beautiful in product tutorials 3 | * 4 | * https://github.com/zendesk/chariot-tooltips 5 | * 6 | * Copyright 2016 Zendesk Inc. 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | 20 | */ 21 | .chariot-tooltip { 22 | position: absolute; } 23 | .chariot-tooltip .left { 24 | float: left; } 25 | .chariot-tooltip .right { 26 | float: right; } 27 | .chariot-tooltip .center { 28 | margin-left: auto; 29 | margin-right: auto; } 30 | .chariot-tooltip-arrow { 31 | position: absolute; 32 | -ms-transform: rotate(45deg); 33 | transform: rotate(45deg); 34 | -ms-transform-origin: 50% 50% 0; 35 | transform-origin: 50% 50% 0; } 36 | 37 | .animate-appear-top { 38 | animation: appear-top 0.4s ease-in-out; 39 | animation-fill-mode: forwards; } 40 | 41 | .animate-appear-right { 42 | animation: appear-right 0.4s ease-in-out; 43 | animation-fill-mode: forwards; } 44 | 45 | .animate-appear-bottom { 46 | animation: appear-bottom 0.4s ease-in-out; 47 | animation-fill-mode: forwards; } 48 | 49 | .animate-appear-left { 50 | animation: appear-left 0.4s ease-in-out; 51 | animation-fill-mode: forwards; } 52 | 53 | @keyframes appear-top { 54 | 0% { 55 | margin-top: -10px; 56 | opacity: 0; } 57 | 40% { 58 | margin-top: 2.5px; } 59 | 100% { 60 | margin-top: 0px; 61 | opacity: 1; } } 62 | 63 | @keyframes appear-right { 64 | 0% { 65 | margin-left: 10px; 66 | opacity: 0; } 67 | 40% { 68 | margin-left: -2.5px; } 69 | 100% { 70 | margin-left: 0px; 71 | opacity: 1; } } 72 | 73 | @keyframes appear-bottom { 74 | 0% { 75 | margin-top: 10px; 76 | opacity: 0; } 77 | 40% { 78 | margin-top: -2.5px; } 79 | 100% { 80 | margin-top: 0px; 81 | opacity: 1; } } 82 | 83 | @keyframes appear-left { 84 | 0% { 85 | margin-left: -10px; 86 | opacity: 0; } 87 | 40% { 88 | margin-left: 2.5px; } 89 | 100% { 90 | margin-left: 0px; 91 | opacity: 1; } } 92 | 93 | .chariot-overlay { 94 | position: absolute; 95 | top: 0; 96 | left: 0; 97 | width: 100%; 98 | height: 100%; } 99 | 100 | .chariot-transparent-overlay { 101 | position: absolute; 102 | top: 0; 103 | left: 0; 104 | width: 100%; 105 | height: 100%; 106 | background: transparent; } 107 | -------------------------------------------------------------------------------- /release/chariot.min.css: -------------------------------------------------------------------------------- 1 | .chariot-overlay,.chariot-transparent-overlay{position:absolute;top:0;left:0;width:100%;height:100%}.chariot-tooltip{position:absolute}.chariot-tooltip .left{float:left}.chariot-tooltip .right{float:right}.chariot-tooltip .center{margin-left:auto;margin-right:auto}.chariot-tooltip-arrow{position:absolute;-ms-transform:rotate(45deg);transform:rotate(45deg);-ms-transform-origin:50% 50% 0;transform-origin:50% 50% 0}.animate-appear-top{animation:appear-top .4s ease-in-out;animation-fill-mode:forwards}.animate-appear-right{animation:appear-right .4s ease-in-out;animation-fill-mode:forwards}.animate-appear-bottom{animation:appear-bottom .4s ease-in-out;animation-fill-mode:forwards}.animate-appear-left{animation:appear-left .4s ease-in-out;animation-fill-mode:forwards}@keyframes appear-top{0%{margin-top:-10px;opacity:0}40%{margin-top:2.5px}100%{margin-top:0;opacity:1}}@keyframes appear-right{0%{margin-left:10px;opacity:0}40%{margin-left:-2.5px}100%{margin-left:0;opacity:1}}@keyframes appear-bottom{0%{margin-top:10px;opacity:0}40%{margin-top:-2.5px}100%{margin-top:0;opacity:1}}@keyframes appear-left{0%{margin-left:-10px;opacity:0}40%{margin-left:2.5px}100%{margin-left:0;opacity:1}}.chariot-transparent-overlay{background:0 0} -------------------------------------------------------------------------------- /stylesheets/chariot.scss: -------------------------------------------------------------------------------- 1 | @import 'tooltip'; 2 | @import 'overlay'; 3 | -------------------------------------------------------------------------------- /stylesheets/example-tooltip.scss: -------------------------------------------------------------------------------- 1 | $white: #fff; 2 | $greyish-brown: #555; 3 | $warm-grey: #999; 4 | $border-color: #ddd; 5 | $arrow-shadow-color: rgba(31, 28, 28, 0.1); 6 | $shadow-color: rgba(31, 28, 28, 0.3); 7 | 8 | .chariot-tooltip { 9 | background-color: $white; 10 | padding: 30px; 11 | width: 220px; 12 | text-align: center; 13 | box-shadow: 0 0 5px 0 $shadow-color; 14 | border: 1px solid $border-color; 15 | color: $warm-grey; 16 | 17 | .chariot-tooltip-icon { 18 | width: 52px; 19 | height: 52px; 20 | margin: auto; 21 | 22 | img { 23 | width: 52px; 24 | height: 52px; 25 | } 26 | } 27 | 28 | .chariot-tooltip-header { 29 | font-size: 18px; 30 | line-height: 18px; 31 | font-weight: 500; 32 | color: $greyish-brown; 33 | padding: 5px 0; 34 | } 35 | 36 | .chariot-tooltip-content { 37 | padding: 5px 0; 38 | 39 | p { 40 | font-size: 14px; 41 | font-weight: 300; 42 | color: $warm-grey; 43 | padding-bottom: 15px; 44 | } 45 | } 46 | 47 | .chariot-btn-row { 48 | padding-top: 5px; 49 | 50 | .btn { 51 | font-size: 13px; 52 | font-weight: 400; 53 | color: #78A300; 54 | background-color: transparent; 55 | border: 1px solid #78A300; 56 | border-radius: 3px; 57 | height: 36px; 58 | padding: 0 20px; 59 | 60 | } 61 | 62 | .btn-next { 63 | color: $white; 64 | background-color: #78A300; 65 | border: none; 66 | 67 | &:hover { 68 | background-color: #78A300; 69 | } 70 | 71 | } 72 | 73 | 74 | .chariot-tooltip-subtext { 75 | float: left; 76 | color: #ddd; 77 | font-size: 13px; 78 | padding-top: 10px; 79 | } 80 | } 81 | 82 | .chariot-btn-row-subtext { 83 | text-align: right; 84 | } 85 | 86 | &-arrow { 87 | background: $white; 88 | } 89 | 90 | &-arrow-left { 91 | border-left: 1px solid $border-color; 92 | border-bottom: 1px solid $border-color; 93 | box-shadow: -2px 2px 2px 0 $arrow-shadow-color; 94 | } 95 | 96 | &-arrow-right { 97 | border-right: 1px solid $border-color; 98 | border-top: 1px solid $border-color; 99 | box-shadow: 2px -2px 2px 0 $arrow-shadow-color; 100 | } 101 | 102 | &-arrow-top { 103 | border-left: 1px solid $border-color; 104 | border-top: 1px solid $border-color; 105 | box-shadow: -2px -2px 4px 0 $arrow-shadow-color; 106 | } 107 | 108 | &-arrow-bottom { 109 | border-right: 1px solid $border-color; 110 | border-bottom: 1px solid $border-color; 111 | box-shadow: 2px 2px 4px 0 $arrow-shadow-color; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /stylesheets/overlay.scss: -------------------------------------------------------------------------------- 1 | .chariot-overlay { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | height: 100%; 7 | } 8 | 9 | .chariot-transparent-overlay { 10 | position: absolute; 11 | top: 0; 12 | left: 0; 13 | width: 100%; 14 | height: 100%; 15 | background: transparent; 16 | } 17 | -------------------------------------------------------------------------------- /stylesheets/tooltip.scss: -------------------------------------------------------------------------------- 1 | .chariot-tooltip { 2 | // Keep this otherwise tooltip height is unpredictable 3 | position: absolute; 4 | 5 | .left { 6 | float: left; 7 | } 8 | 9 | .right { 10 | float: right; 11 | } 12 | 13 | .center { 14 | margin-left: auto; 15 | margin-right: auto; 16 | } 17 | 18 | &-arrow { 19 | position: absolute; 20 | transform: rotate(45deg); 21 | transform-origin: 50% 50% 0; 22 | } 23 | } 24 | 25 | $directions: top, right, bottom, left; 26 | 27 | @each $direction in $directions { 28 | .animate-appear-#{$direction} { 29 | animation: appear-#{$direction} 0.4s ease-in-out; 30 | animation-fill-mode: forwards; 31 | } 32 | } 33 | 34 | @each $direction in $directions { 35 | $margin-direction: null; 36 | @if ($direction == left) or ($direction == right) { 37 | $margin-direction: left; 38 | } @else { 39 | $margin-direction: top; 40 | } 41 | 42 | $reverse: 1; 43 | @if ($direction == left) or ($direction == top) { 44 | $reverse: -1; 45 | } 46 | 47 | @keyframes appear-#{$direction} { 48 | 0% { 49 | margin-#{$margin-direction}: #{10 * $reverse}px; 50 | opacity: 0; 51 | } 52 | 40% { 53 | margin-#{$margin-direction}: #{-2.5 * $reverse}px; 54 | } 55 | 100% { 56 | margin-#{$margin-direction}: 0px; 57 | opacity: 1; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /test/chariot_test.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import sinon from 'sinon'; 3 | import Chariot from '../lib/chariot' 4 | 5 | let helper = require('./test_helper'); 6 | let assert = require("assert"); 7 | let expect = chai.expect; 8 | 9 | describe('Chariot', function() { 10 | let delegate = null; 11 | let tutorialName = "tutorialName"; 12 | let config = helper.fixtures.configFixture; 13 | 14 | describe('#startTutorial', function() { 15 | it('returns null when there is a current tutorial', function() { 16 | let chariot = new Chariot(config, delegate); 17 | let tutorial = chariot.startTutorial(tutorialName); 18 | assert.equal(chariot.startTutorial("differentTutorial"), null); 19 | }); 20 | 21 | it('returns the started tutorial when no current tutorial', function() { 22 | let chariot = new Chariot(config, delegate); 23 | let tutorial = chariot.startTutorial(tutorialName); 24 | assert.equal(tutorial.name, tutorialName); 25 | }); 26 | }); 27 | 28 | describe('#endTutorial', function() { 29 | it('ends the current tutorial', function() { 30 | let chariot = new Chariot(config, delegate); 31 | let tutorial = chariot.startTutorial(tutorialName); 32 | sinon.stub(tutorial, ('end')); 33 | chariot.endTutorial(); 34 | expect(tutorial.end.called).to.be.true; 35 | }); 36 | }); 37 | 38 | describe('#currentTutorial', function() { 39 | it('returns null when no tutorial has started', function() { 40 | let chariot = new Chariot({}, delegate); 41 | assert.equal(chariot.currentTutorial(), null); 42 | }); 43 | 44 | it('returns the current tutorial if started', function() { 45 | let chariot = new Chariot(config, delegate); 46 | let tutorial = chariot.startTutorial(tutorialName); 47 | assert.equal(chariot.currentTutorial(), tutorial); 48 | }); 49 | }); 50 | 51 | describe('#Chariot.createTutorial', function() { 52 | it('returns a tutorial with a valid config', function() { 53 | let tutorial = Chariot.createTutorial(config[tutorialName], delegate); 54 | expect(tutorial).to.not.equal(null); 55 | }); 56 | }); 57 | 58 | describe('#Chariot.startTutorial', function() { 59 | it('returns and starts a tutorial with a valid config', function() { 60 | let stub = { start: () => '' }; 61 | sinon.stub(Chariot, 'createTutorial').returns(stub); 62 | sinon.stub(stub, 'start'); 63 | let tutorial = Chariot.startTutorial(config[tutorialName], delegate); 64 | expect(tutorial).to.equal(stub); 65 | expect(stub.start.called).to.be.true; 66 | }); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /test/libs/style_test.js: -------------------------------------------------------------------------------- 1 | require('../test_helper'); 2 | import Style from '../../lib/style'; 3 | import chai from 'chai'; 4 | import sinon from 'sinon'; 5 | let expect = chai.expect; 6 | 7 | describe('Styles', function() { 8 | context('caclulate left position', function() { 9 | let tooltip = { outerWidth: () => 20 }; 10 | let anchor = { 11 | offset: () => ({ left: 100 }), 12 | outerWidth: () => 200 13 | }; 14 | let xOffsetTooltip = 5, arrowOffset = 5; 15 | 16 | describe("anchor to right", function() { 17 | it("with offset", function() { 18 | let offset = Style.calculateLeft(tooltip, anchor, xOffsetTooltip, 'right', 0); 19 | expect(offset).to.be.equal(100 + 200 + xOffsetTooltip); 20 | }); 21 | 22 | it("with offset and arrowOffset", function() { 23 | let offset = Style.calculateLeft(tooltip, anchor, xOffsetTooltip, 'right', arrowOffset); 24 | expect(offset).to.be.equal(100 + 200 + xOffsetTooltip + arrowOffset); 25 | }); 26 | }); 27 | 28 | describe("anchor to left", function() { 29 | it("with offset", function() { 30 | let offset = Style.calculateLeft(tooltip, anchor, xOffsetTooltip, 'left', 0); 31 | expect(offset).to.be.equal(100 - 20 + xOffsetTooltip); 32 | }); 33 | 34 | it("with offset and arrowOffset", function() { 35 | let offset = Style.calculateLeft( 36 | tooltip, anchor, xOffsetTooltip, 'left', arrowOffset); 37 | expect(offset).to.be.equal(100 - 20 + xOffsetTooltip - arrowOffset); 38 | }); 39 | }); 40 | 41 | describe("anchor to top/bottom", function() { 42 | it("with offset", function() { 43 | let topOffset = Style.calculateLeft(tooltip, anchor, xOffsetTooltip, 'top', 0); 44 | let bottomOffset = Style.calculateLeft( 45 | tooltip, anchor, xOffsetTooltip, 'bottom', 0); 46 | let expected = 100 + 200 / 2 - 20 / 2 + xOffsetTooltip; 47 | expect(topOffset).to.be.equal(expected); 48 | expect(bottomOffset).to.be.equal(expected); 49 | }); 50 | 51 | it("with arrowOffset not factored in", function() { 52 | let topOffset = Style.calculateLeft( 53 | tooltip, anchor, xOffsetTooltip, 'bottom', arrowOffset); 54 | let bottomOffset = Style.calculateLeft( 55 | tooltip, anchor, xOffsetTooltip, 'bottom', 0); 56 | let expected = 100 + 200 / 2 - 20 / 2 + xOffsetTooltip; 57 | expect(topOffset).to.be.equal(expected); 58 | expect(bottomOffset).to.be.equal(expected); 59 | }); 60 | }); 61 | }); 62 | 63 | context('calculate top position', function() { 64 | let tooltip = { outerHeight: () => 20 }; 65 | let anchor = { 66 | offset: () => ({ top: 100 }), 67 | outerHeight: () => 200 68 | }; 69 | let yOffsetTooltip = 5, arrowOffset = 5; 70 | 71 | describe("anchor to top", function() { 72 | it("with offset", function() { 73 | let offset = Style.calculateTop(tooltip, anchor, yOffsetTooltip, 'bottom', 0); 74 | expect(offset).to.be.equal(100 + 200 + yOffsetTooltip); 75 | }); 76 | 77 | it("with offset and arrowOffset", function() { 78 | let offset = Style.calculateTop( 79 | tooltip, anchor, yOffsetTooltip, 'bottom', arrowOffset); 80 | expect(offset).to.be.equal(100 + 200 + yOffsetTooltip + arrowOffset); 81 | }); 82 | }); 83 | 84 | describe("anchor to bottom", function() { 85 | it("with offset", function() { 86 | let offset = Style.calculateTop(tooltip, anchor, yOffsetTooltip, 'top', 0); 87 | expect(offset).to.be.equal(100 - 20 + yOffsetTooltip); 88 | }); 89 | 90 | it("with offset and arrowOffset", function() { 91 | let offset = Style.calculateTop( 92 | tooltip, anchor, yOffsetTooltip, 'top', arrowOffset); 93 | expect(offset).to.be.equal(100 - 20 + yOffsetTooltip - arrowOffset); 94 | }); 95 | }); 96 | 97 | describe("anchor to left/right", function() { 98 | it("with offset", function() { 99 | let topOffset = Style.calculateTop(tooltip, anchor, yOffsetTooltip, 'left', 0); 100 | let bottomOffset = Style.calculateTop( 101 | tooltip, anchor, yOffsetTooltip, 'right', 0); 102 | let expected = 100 + 200 / 2 - 20 / 2 + yOffsetTooltip; 103 | expect(topOffset).to.be.equal(expected); 104 | expect(bottomOffset).to.be.equal(expected); 105 | }); 106 | 107 | it("with arrowOffset not factored in", function() { 108 | let topOffset = Style.calculateTop( 109 | tooltip, anchor, yOffsetTooltip, 'left', arrowOffset); 110 | let bottomOffset = Style.calculateTop( 111 | tooltip, anchor, yOffsetTooltip, 'right', arrowOffset); 112 | let expected = 100 + 200 / 2 - 20 / 2 + yOffsetTooltip; 113 | expect(topOffset).to.be.equal(expected); 114 | expect(bottomOffset).to.be.equal(expected); 115 | }); 116 | }); 117 | }); 118 | }); 119 | -------------------------------------------------------------------------------- /test/step_test.js: -------------------------------------------------------------------------------- 1 | var assert = require("assert"); 2 | require('./test_helper'); 3 | import Step from '../lib/step'; 4 | import Tutorial from '../lib/tutorial'; 5 | import Style from '../lib/style'; 6 | import chai from 'chai'; 7 | import sinon from 'sinon'; 8 | const expect = chai.expect; 9 | 10 | 11 | describe('Step', function() { 12 | let stepConfiguration = { 13 | name: 'Internal or Public?', 14 | selectors: { 15 | assignee: "#input", 16 | assignLabel: "#label" 17 | }, 18 | tooltip: { 19 | position: 'right', // 'top' | 'left' | 'bottom' | 'right' 20 | text: 'Some text', 21 | xOffsetTooltip: '10', 22 | yOffsetTooltip: '10', 23 | anchorElement: "assignee", 24 | iconUrl: '/assets/whatever' 25 | }, 26 | cta: 'Next' 27 | }; 28 | 29 | context('constructor', function() { 30 | let chariot = {}, 31 | step = null, 32 | tutorial = null, 33 | overlay = null; 34 | 35 | before(function() { 36 | tutorial = new Tutorial({ steps: [] }, 'tutorialName'); 37 | step = new Step(stepConfiguration, 0, tutorial, overlay); 38 | }); 39 | 40 | it('reads selectors', function() { 41 | expect(step.selectors).to.equal(stepConfiguration.selectors); 42 | }); 43 | 44 | it('reads tutorial', function() { 45 | expect(step.tutorial).to.equal(tutorial); 46 | }); 47 | }); 48 | 49 | context('getClonedElement', function() { 50 | let step = null, 51 | tutorial = {}, 52 | overlay = null; 53 | 54 | before(function() { 55 | step = new Step(stepConfiguration, 0, tutorial, overlay); 56 | }); 57 | 58 | it('returns null for invalid selectorName', function() { 59 | let result = step.getClonedElement('random'); 60 | expect(result).to.equal(undefined); 61 | }); 62 | 63 | it('returns undefined for selectorName that has not been cloned', function() { 64 | let result = step.getClonedElement('assignee'); 65 | expect(result).to.equal(undefined); 66 | }); 67 | 68 | it('returns clone for selectorName', function() { 69 | let selectorName = 'assignee'; 70 | let clone = 'clone'; 71 | step._elementMap[selectorName].clone = clone; 72 | let result = step.getClonedElement(selectorName); 73 | expect(result).to.equal(clone); 74 | }); 75 | }); 76 | 77 | context('tearDown', function() { 78 | let chariot = {}, 79 | step = null, 80 | tutorial = null, 81 | overlay = null; 82 | 83 | before(function() { 84 | tutorial = new Tutorial({ steps: [] }, 'tutorialName'); 85 | step = new Step(stepConfiguration, 0, tutorial, overlay); 86 | sinon.stub(step.tooltip, ('tearDown')); 87 | }); 88 | 89 | it('clears cached styles', function() { 90 | sinon.stub(Style, 'clearCachedStylesForElement'); 91 | step.tearDown(); 92 | expect(Style.clearCachedStylesForElement.calledTwice).to.be.true; 93 | }); 94 | 95 | it('removes all clone elements', function() { 96 | for (let selectorName in stepConfiguration.selectors) { 97 | let clone = { remove: () => {} }; 98 | sinon.spy(clone, 'remove'); 99 | step._elementMap[selectorName].clone = clone; 100 | } 101 | 102 | step.tearDown(); 103 | for (let selectorName in stepConfiguration.selectors) { 104 | expect(step._elementMap[selectorName].clone.remove.calledOnce).to.be.true; 105 | } 106 | }); 107 | 108 | it('tears down tooltip', function() { 109 | expect(step.tooltip.tearDown.called).to.be.true; 110 | }); 111 | }); 112 | }); 113 | -------------------------------------------------------------------------------- /test/test_helper.js: -------------------------------------------------------------------------------- 1 | require('es5-shim'); 2 | require('es6-shim'); 3 | 4 | const stepFixture = { 5 | selectors: { 6 | assignee: "test" 7 | }, 8 | tooltip: { 9 | position: 'left' 10 | } 11 | }; 12 | 13 | const tutorialFixture = { 14 | steps: [stepFixture] 15 | }; 16 | 17 | const configFixture = { 18 | tutorialName: tutorialFixture 19 | }; 20 | 21 | export default { 22 | fixtures: { 23 | configFixture, tutorialFixture, stepFixture 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /test/tooltip_test.js: -------------------------------------------------------------------------------- 1 | require('./test_helper'); 2 | import Tooltip from '../lib/tooltip'; 3 | import chai from 'chai'; 4 | import sinon from 'sinon'; 5 | import Style from '../lib/style'; 6 | import Constants from '../lib/constants'; 7 | let expect = chai.expect; 8 | 9 | describe('Tooltip', function() { 10 | let configuration = { 11 | position: 'right', // 'top' | 'left' | 'bottom' | 'right' 12 | text: 'Some text', 13 | xOffsetTooltip: '10', 14 | yOffsetTooltip: '10', 15 | offsetArrow: '5', 16 | anchorElement: "assignee", 17 | width: '10', 18 | height: '10', 19 | iconUrl: '/assets/whatever', 20 | title: 'Title', 21 | cta: 'Next', 22 | subtext: () => 'foobar', 23 | attr: {}, 24 | arrowLength: 10 25 | }, 26 | step = { selectors: {} }, 27 | tutorial = new Object({ 28 | stepNum: () => 1, 29 | steps: [step] 30 | }), 31 | tooltip = null; 32 | 33 | beforeEach(function() { 34 | tooltip = new Tooltip(configuration, step, tutorial); 35 | }); 36 | 37 | context('constructor', function() { 38 | it('reads step', function() { 39 | expect(tooltip.step).to.equal(step); 40 | }); 41 | it('reads tutorial', function() { 42 | expect(tooltip.tutorial).to.equal(tutorial); 43 | }); 44 | it('reads position', function() { 45 | expect(tooltip.position).to.equal(configuration.position); 46 | }); 47 | it('reads text', function() { 48 | expect(tooltip.text).to.equal(configuration.text); 49 | }); 50 | it('reads xOffsetTooltip', function() { 51 | expect(tooltip.xOffsetTooltip).to.equal(parseInt(configuration.xOffsetTooltip)); 52 | }); 53 | it('reads xOffsetTooltip', function() { 54 | expect(tooltip.yOffsetTooltip).to.equal(parseInt(configuration.yOffsetTooltip)); 55 | }); 56 | it('reads offsetArrow', function() { 57 | expect(tooltip.offsetArrow).to.equal(parseInt(configuration.offsetArrow)); 58 | }); 59 | it('reads width', function() { 60 | expect(tooltip.width).to.equal(parseInt(configuration.width)); 61 | }); 62 | it('reads height', function() { 63 | expect(tooltip.height).to.equal(parseInt(configuration.height)); 64 | }); 65 | it('reads iconUrl', function() { 66 | expect(tooltip.iconUrl).to.equal(configuration.iconUrl); 67 | }); 68 | it('reads title', function() { 69 | expect(tooltip.title).to.equal(configuration.title); 70 | }); 71 | it('reads attr', function() { 72 | expect(tooltip.attr).to.equal(configuration.attr); 73 | }); 74 | it('reads arrowLength', function() { 75 | expect(tooltip.arrowLength).to.equal(configuration.arrowLength); 76 | }); 77 | }); 78 | 79 | context('render', function() { 80 | it('sets css styles', function() { 81 | let css = sinon.stub().returns(); 82 | let $markup = $('
'); 83 | sinon.stub(tooltip, '_createTooltipTemplate').returns($markup); 84 | sinon.stub(tooltip, '_getAnchorElement').returns({}); 85 | sinon.stub(Style, "calculateTop").returns(0); 86 | sinon.stub(Style, "calculateLeft").returns(0); 87 | tooltip.render(); 88 | 89 | expect($markup.css('position')).to.equal('absolute'); 90 | expect($markup.css('top')).to.equal("0px"); 91 | expect($markup.css('left')).to.equal("0px"); 92 | expect(parseInt($markup.css('z-index'))).to.equal( 93 | parseInt(Constants.TOOLTIP_Z_INDEX)); 94 | }); 95 | }); 96 | 97 | context('tearDown', function() { 98 | it('tearDown with null DOM element', function() { 99 | tooltip.$tooltip = null; 100 | expect(tooltip.tearDown.bind(tooltip)).not.to.throw(Error); 101 | }); 102 | 103 | it('tearDown removes element', function() { 104 | let $tooltip = { remove: () => ({}) }; 105 | tooltip.$tooltip = $tooltip; 106 | sinon.spy($tooltip, 'remove'); 107 | let $tooltipArrow = { remove: () => ({}) }; 108 | tooltip.$tooltipArrow = $tooltipArrow; 109 | sinon.spy($tooltipArrow, 'remove'); 110 | tooltip.tearDown(); 111 | expect($tooltip.remove.calledOnce).to.be.true; 112 | }); 113 | }); 114 | 115 | context('_createTooltipTemplate', function() { 116 | let template = null; 117 | 118 | beforeEach(function() { 119 | template = tooltip._createTooltipTemplate(); 120 | }); 121 | 122 | it('uses elements from config to create markup', function() { 123 | let templateHtml = template.html(); 124 | expect(tooltip.cta).to.equal(configuration.cta); 125 | expect(tooltip.subtext).to.equal(configuration.subtext); 126 | expect(templateHtml.includes(tooltip.title)).to.be.true; 127 | expect(templateHtml.includes(tooltip.text)).to.be.true; 128 | expect(templateHtml.includes(tooltip.cta)).to.be.true; 129 | expect(templateHtml.includes(tooltip.iconUrl)).to.be.true; 130 | expect(templateHtml.includes(tooltip.subtext())).to.be.true; 131 | }); 132 | 133 | it('includes step number in CSS class and data attribute', function() { 134 | expect(template.attr('class').includes('chariot-step')).to.be.true; 135 | expect(template.attr('data-step-order').includes(1)).to.be.true; 136 | }); 137 | }); 138 | }); 139 | -------------------------------------------------------------------------------- /test/tutorial_test.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import sinon from 'sinon'; 3 | import { fixtures } from './test_helper'; 4 | import assert from 'assert'; 5 | import Tutorial from '../lib/tutorial' 6 | 7 | const expect = chai.expect; 8 | const stepFixture = fixtures.stepFixture; 9 | 10 | describe('Tutorial', function() { 11 | let tutorial; 12 | 13 | it('reads config', function() { 14 | tutorial = new Tutorial({ steps: [] }, 'test'); 15 | assert.equal(typeof tutorial, 'object'); 16 | }); 17 | 18 | describe('next', function() { 19 | before(function() { 20 | tutorial = new Tutorial({ steps: [stepFixture, stepFixture, stepFixture] }); 21 | }); 22 | 23 | it('renders the next step when no args passed', function() { 24 | tutorial.currentStep = tutorial.getStep(0); 25 | const stepSpy1 = sinon.spy(tutorial.currentStep, 'tearDown'); 26 | const stepSpy2 = sinon.spy(tutorial.getStep(1), 'render'); 27 | tutorial.next(); 28 | expect(stepSpy1.called).to.be.true; 29 | expect(stepSpy2.called).to.be.true; 30 | }); 31 | 32 | it('ends tutorial when currentStep is last step', function() { 33 | tutorial.currentStep = tutorial.getStep(2); 34 | const spy = sinon.stub(tutorial, 'end'); 35 | tutorial.next(); 36 | expect(spy.called).to.be.true; 37 | }); 38 | 39 | describe('when an arg is passed', function () { 40 | let tearDownSpy, endSpy; 41 | 42 | before(function() { 43 | tutorial = new Tutorial({ steps: [stepFixture, stepFixture, stepFixture] }); 44 | tutorial.currentStep = tutorial.getStep(0); 45 | tearDownSpy = sinon.spy(tutorial.currentStep, 'tearDown'); 46 | }); 47 | 48 | it('tears down current step and advances to step when step arg is an integer', function() { 49 | tutorial.next(2); 50 | expect(tearDownSpy.called).to.be.true; 51 | assert.equal(tutorial.currentStep, tutorial.getStep(2)); 52 | }); 53 | 54 | it('tears down current step and advances to step when arg is a Step instance', function() { 55 | const nextStep = tutorial.getStep(1); 56 | tutorial.next(nextStep); 57 | expect(tearDownSpy.called).to.be.true; 58 | assert.equal(tutorial.currentStep, nextStep); 59 | }); 60 | }); 61 | }); 62 | 63 | describe('stepNum', function() { 64 | it('returns null with null argument', function() { 65 | const tutorial = new Tutorial({ steps: [] }); 66 | assert.equal(tutorial.stepNum(null), null); 67 | }); 68 | 69 | it('returns index + 1 for valid step', function() { 70 | const tutorial = new Tutorial({ steps: [stepFixture] }); 71 | const step = tutorial.getStep(0); 72 | assert.equal(tutorial.stepNum(step), 1); 73 | }); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /testem.yml: -------------------------------------------------------------------------------- 1 | { 2 | "framework": "mocha", 3 | "src_files": [ 4 | "node_modules/jquery/dist/jquery.js", 5 | "dist/test/test.js", 6 | ], 7 | "launch_in_ci": [ 8 | "PhantomJS" 9 | ], 10 | "launch_in_dev": [ 11 | "PhantomJS", 12 | "Chrome" 13 | ] 14 | } 15 | 16 | -------------------------------------------------------------------------------- /webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | export default { 2 | output: { 3 | filename: 'client-bundle.js', 4 | }, 5 | devtool: 'source-map', 6 | module: { 7 | loaders: [ 8 | { 9 | test: /\.jsx?$/, 10 | loader: 'babel-loader', 11 | exclude: [/node_modules/], 12 | }, 13 | ], 14 | }, 15 | resolve: { 16 | extensions: ['', '.js', '.jsx'], 17 | }, 18 | }; 19 | --------------------------------------------------------------------------------