├── examples └── simple-test │ ├── .gitignore │ ├── packages │ └── tutorials │ ├── .meteor │ ├── release │ ├── identifier │ ├── platforms │ ├── .gitignore │ ├── .id │ ├── packages │ ├── .finished-upgraders │ └── versions │ ├── client │ ├── testtut.css │ ├── testtut.js │ └── testtut.html │ └── readme.md ├── docs ├── highlight_1.png └── highlight_2.png ├── .gitignore ├── tutorial.styl ├── package.js ├── eventEmitter.coffee ├── LICENSE ├── templates.html ├── helpers.coffee ├── drags.js ├── History.md ├── README.md └── tutorial.coffee /examples/simple-test/.gitignore: -------------------------------------------------------------------------------- 1 | private/ 2 | -------------------------------------------------------------------------------- /examples/simple-test/packages/tutorials: -------------------------------------------------------------------------------- 1 | ../.. -------------------------------------------------------------------------------- /examples/simple-test/.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@1.3.4.4 2 | -------------------------------------------------------------------------------- /examples/simple-test/.meteor/identifier: -------------------------------------------------------------------------------- 1 | 1w1mmd91be9fkz1cwtb4h -------------------------------------------------------------------------------- /examples/simple-test/.meteor/platforms: -------------------------------------------------------------------------------- 1 | server 2 | browser 3 | -------------------------------------------------------------------------------- /examples/simple-test/.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | dev_bundle 2 | local 3 | -------------------------------------------------------------------------------- /docs/highlight_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TurkServer/meteor-tutorials/HEAD/docs/highlight_1.png -------------------------------------------------------------------------------- /docs/highlight_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TurkServer/meteor-tutorials/HEAD/docs/highlight_2.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .versions 2 | 3 | lib-cov 4 | *.seed 5 | *.log 6 | *.csv 7 | *.dat 8 | *.out 9 | *.pid 10 | *.gz 11 | 12 | pids 13 | logs 14 | results 15 | 16 | npm-debug.log 17 | node_modules 18 | .build* 19 | -------------------------------------------------------------------------------- /examples/simple-test/client/testtut.css: -------------------------------------------------------------------------------- 1 | /* CSS declarations go here */ 2 | 3 | body { 4 | padding-top: 40px; 5 | font-family: 'Helvetica Neue', 'Helvetica', 'Lucida Grande', 'Arial', sans-serif !important; 6 | } 7 | 8 | * { 9 | box-sizing: border-box; 10 | } -------------------------------------------------------------------------------- /examples/simple-test/.meteor/.id: -------------------------------------------------------------------------------- 1 | # This file contains a token that is unique to your project. 2 | # Check it into your repository along with the rest of this directory. 3 | # It can be used for purposes such as: 4 | # - ensuring you don't accidentally deploy one app on top of another 5 | # - providing package authors with aggregated statistics 6 | 7 | 58fgs4176k3cv15hcw3x 8 | -------------------------------------------------------------------------------- /examples/simple-test/.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # 3 | # 'meteor add' and 'meteor remove' will edit this file for you, 4 | # but you can also edit it by hand. 5 | 6 | meteor 7 | webapp 8 | spacebars 9 | check 10 | tracker 11 | templating 12 | underscore 13 | session 14 | standard-minifier-css 15 | standard-minifier-js 16 | 17 | # This pulls in DDP/mongo 18 | # hot-code-push 19 | 20 | mizzao:tutorials 21 | iron:router 22 | twbs:bootstrap 23 | -------------------------------------------------------------------------------- /examples/simple-test/.meteor/.finished-upgraders: -------------------------------------------------------------------------------- 1 | # This file contains information which helps Meteor properly upgrade your 2 | # app when you run 'meteor update'. You should check it into version control 3 | # with your project. 4 | 5 | notices-for-0.9.0 6 | notices-for-0.9.1 7 | 0.9.4-platform-file 8 | notices-for-facebook-graph-api-2 9 | 1.2.0-standard-minifiers-package 10 | 1.2.0-meteor-platform-split 11 | 1.2.0-cordova-changes 12 | 1.2.0-breaking-changes 13 | 1.3.0-split-minifiers-package 14 | -------------------------------------------------------------------------------- /examples/simple-test/readme.md: -------------------------------------------------------------------------------- 1 | testtut 2 | ==== 3 | Please pardon the badly named repository. lol. It was a quick decision to quickly build this demo to show [@mizzao](http://github.com/mizzao). Never expected this to be referenced. DEMO: [click here](http://testtut.meteor.com) 4 | 5 | 6 | features 7 | ==== 8 | * Hook up with iron-router 9 | * multiple tutorials for different pages 10 | * Use of Session to check if tutorial is enabled 11 | * Hook up 'finish' button on the 'words' page to turn off the tutorial by changing the Session variable 12 | 13 | contributions 14 | ==== 15 | * feel free to fork and adjust this tutorial -------------------------------------------------------------------------------- /tutorial.styl: -------------------------------------------------------------------------------- 1 | @import 'nib' 2 | 3 | // The darkness of the spotlight shadow 4 | opacity = 0.66 5 | 6 | .spotlight 7 | display: block; 8 | position: absolute; 9 | // The position values are animated and moved 10 | top: 0; 11 | left: 0; 12 | // Don't want these so we can have a total spot where needed 13 | // bottom: auto; 14 | // right: auto; 15 | border-radius: 10px 16 | box-shadow: 0px 0px 5px 5px rgba(0,0,0,opacity) inset, 0px 0px 0px 4000px rgba(0,0,0,opacity) 17 | pointer-events: none; 18 | z-index: 1000 19 | 20 | .modal-dialog.positioned 21 | position: absolute 22 | // Ideal value is above OpenLayers' controls of 1000+x, below bootstrap modal dialog of 1050 23 | z-index: 1049 24 | 25 | .modal-footer.compact 26 | margin-top: 0 27 | 28 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: "mizzao:tutorials", 3 | summary: "Create super cool animated tutorials for your Meteor app", 4 | version: "0.6.8", 5 | git: "https://github.com/mizzao/meteor-tutorials.git" 6 | }); 7 | 8 | Package.onUse(function (api) { 9 | api.versionsFrom("1.2.0.1"); 10 | 11 | api.use(['jquery', 'coagmano:stylus@1.0.0', 'coffeescript'], 'client'); 12 | api.use(['ui', 'templating'], 'client'); 13 | 14 | // Weak dependencies on the most popular bootstrap packages 15 | api.use("twbs:bootstrap@3.3.5", 'client', {weak: true}); 16 | api.use("nemo64:bootstrap@3.3.5_2", 'client', {weak: true}); 17 | 18 | api.addFiles('templates.html', 'client'); 19 | api.addFiles('tutorial.styl', 'client'); 20 | 21 | api.addFiles('eventEmitter.coffee', 'client'); 22 | api.addFiles('drags.js', 'client'); 23 | 24 | api.addFiles('tutorial.coffee', 'client'); 25 | api.addFiles('helpers.coffee', 'client'); 26 | 27 | api.export('EventEmitter', 'client'); 28 | }); 29 | -------------------------------------------------------------------------------- /eventEmitter.coffee: -------------------------------------------------------------------------------- 1 | # Simple EventEmitter for the client 2 | # Adapted from https://github.com/arunoda/meteor-streams/blob/master/lib/ev.js 3 | 4 | class EventEmitter 5 | constructor: -> 6 | @handlers = {} 7 | 8 | emit: (event) -> 9 | args = Array::slice.call(`arguments`, 1) 10 | handler.apply(@, args) for handler in @handlers[event] if @handlers[event] 11 | return 12 | 13 | on: (event, callback) -> 14 | @handlers[event] = [] unless @handlers[event] 15 | @handlers[event].push(callback) 16 | return 17 | 18 | once: (event, callback) -> 19 | @on event, onetimeCallback = -> 20 | callback.apply(@, `arguments`) 21 | @removeListener(event, onetimeCallback) 22 | return 23 | return 24 | 25 | removeListener: (event, callback) -> 26 | if @handlers[event] 27 | index = @handlers[event].indexOf(callback) 28 | @handlers[event].splice(index, 1) 29 | return 30 | 31 | removeAllListeners: (event) -> 32 | @handlers[event] = `undefined` 33 | return 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Andrew Mao 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/simple-test/.meteor/versions: -------------------------------------------------------------------------------- 1 | babel-compiler@6.8.4 2 | babel-runtime@0.1.9_1 3 | base64@1.0.9 4 | blaze@2.1.8 5 | blaze-tools@1.0.9 6 | boilerplate-generator@1.0.9 7 | caching-compiler@1.0.6 8 | caching-html-compiler@1.0.6 9 | check@1.2.3 10 | coffeescript@1.1.3 11 | deps@1.0.12 12 | diff-sequence@1.0.6 13 | ecmascript@0.4.7 14 | ecmascript-runtime@0.2.12 15 | ejson@1.0.12 16 | html-tools@1.0.10 17 | htmljs@1.0.10 18 | id-map@1.0.8 19 | iron:controller@1.0.12 20 | iron:core@1.0.11 21 | iron:dynamic-template@1.0.12 22 | iron:layout@1.0.12 23 | iron:location@1.0.11 24 | iron:middleware-stack@1.1.0 25 | iron:router@1.0.13 26 | iron:url@1.0.11 27 | jquery@1.11.9 28 | logging@1.0.14 29 | meteor@1.1.16 30 | minifier-css@1.1.13 31 | minifier-js@1.1.13 32 | mizzao:tutorials@0.6.7 33 | modules@0.6.5 34 | modules-runtime@0.6.5 35 | mongo-id@1.0.5 36 | observe-sequence@1.0.12 37 | promise@0.7.3 38 | random@1.0.10 39 | reactive-dict@1.1.8 40 | reactive-var@1.0.10 41 | routepolicy@1.0.11 42 | session@1.1.6 43 | spacebars@1.0.12 44 | spacebars-compiler@1.0.12 45 | standard-minifier-css@1.0.8 46 | standard-minifier-js@1.0.8 47 | stylus@2.512.4 48 | templating@1.1.13 49 | templating-tools@1.0.4 50 | tracker@1.0.14 51 | twbs:bootstrap@3.3.6 52 | ui@1.0.11 53 | underscore@1.0.9 54 | webapp@1.2.10 55 | webapp-hashing@1.0.9 56 | -------------------------------------------------------------------------------- /templates.html: -------------------------------------------------------------------------------- 1 | 20 | 21 | 43 | -------------------------------------------------------------------------------- /examples/simple-test/client/testtut.js: -------------------------------------------------------------------------------- 1 | // client code 2 | Router.configure({ 3 | layoutTemplate: 'hello' 4 | }); 5 | 6 | Router.map(function(){ 7 | this.route('home', {path: '/'}); 8 | this.route('pics'); 9 | this.route('words'); 10 | }); 11 | 12 | Session.set('tutorialEnabled', true); 13 | var emitter = new EventEmitter(); 14 | 15 | Template.home.helpers({ 16 | tutorialEnabled: function() { 17 | return Session.get('tutorialEnabled') 18 | } 19 | }); 20 | 21 | Template.words.events = { 22 | "click .words": function() { 23 | emitter.emit("wordsClick"); 24 | } 25 | }; 26 | 27 | Template.words.helpers({ 28 | tutorialEnabled: function() { 29 | return Session.get('tutorialEnabled') 30 | } 31 | }); 32 | 33 | var homeTutorialSteps = [ 34 | { 35 | template: Template.tutorial_step1, 36 | onLoad: function() { console.log("The tutorial has started!"); } 37 | }, 38 | { 39 | template: Template.tutorial_step2, 40 | spot: ".nav" 41 | } 42 | ]; 43 | 44 | var wordsTutorialSteps = [ 45 | { 46 | template: Template.tutorial_step3, 47 | spot: ".words", 48 | require: { 49 | event: "wordsClick" 50 | } 51 | }, 52 | { 53 | template: Template.tutorial_step4, 54 | spot: "body" 55 | } 56 | ]; 57 | 58 | Template.home.helpers({ 59 | options: { 60 | steps: homeTutorialSteps, 61 | onFinish: function(){ 62 | Router.go('words'); 63 | } 64 | } 65 | }); 66 | 67 | Template.words.helpers({ 68 | options: { 69 | steps: wordsTutorialSteps, 70 | emitter: emitter, 71 | onFinish: function() { 72 | console.log("Finish clicked!"); 73 | Meteor.setTimeout( function () { 74 | // Test debouncing 75 | Session.set('tutorialEnabled', false); 76 | }, 1000); 77 | } 78 | } 79 | }); 80 | -------------------------------------------------------------------------------- /helpers.coffee: -------------------------------------------------------------------------------- 1 | Template.tutorial.helpers 2 | tutorialManager: -> Template.instance().tm = new TutorialManager(@) 3 | 4 | Template.tutorial.rendered = -> 5 | $spot = @$(".spotlight") 6 | $modal = @$(".modal-dialog") 7 | 8 | # Add resizer on first render 9 | @resizer = => 10 | [spotCSS, modalCSS] = @tm.getPositions() 11 | # Don't animate, just move 12 | $spot.css(spotCSS) 13 | $modal.css(modalCSS) 14 | 15 | # attach a window resize handler 16 | $(window).on('resize', @resizer) 17 | 18 | # Make modal draggable so it can be moved out of the way if necessary 19 | $modal.drags 20 | handle: ".modal-footer" 21 | 22 | Template.tutorial.destroyed = -> 23 | # Take off the resize watcher 24 | $(window).off('resize', @resizer) if @resizer 25 | @resizer = null 26 | 27 | Template.tutorial.helpers 28 | content: -> 29 | # Run load function, if any 30 | # Don't run it reactively in case it accesses reactive variables 31 | if (func = @currentLoadFunc())? 32 | Deps.nonreactive(func) 33 | 34 | tutorialInstance = Template.instance() 35 | 36 | # Move things where they should go, after the template renders 37 | Meteor.defer => 38 | # Animate spotlight and modal to appropriate positions 39 | $spot = tutorialInstance.$(".spotlight") 40 | $modal = tutorialInstance.$(".modal-dialog") 41 | # Move things where they should go 42 | [spotCSS, modalCSS] = @getPositions() 43 | $spot.animate(spotCSS) 44 | $modal.animate(modalCSS) 45 | return 46 | 47 | # Template will render with tutorial as the data context 48 | # This function is reactive; the above will run whenever the context changes 49 | return @currentTemplate() 50 | 51 | Template._tutorial_buttons.events = 52 | "click .action-tutorial-back": -> @prev() 53 | "click .action-tutorial-next": -> @next() 54 | "click .action-tutorial-finish": -> @finish() 55 | -------------------------------------------------------------------------------- /drags.js: -------------------------------------------------------------------------------- 1 | var touchSupported = 'ontouchend' in document; 2 | 3 | // Modified from http://css-tricks.com/snippets/jquery/draggable-without-jquery-ui/ 4 | // so that we don't have an unnecessary dependency on jQuery UI 5 | 6 | // However, this code is significantly less shitty 7 | 8 | (function($) { 9 | $.fn.drags = function(options) { 10 | options = $.extend({ 11 | handle: null, 12 | cursor: 'move', 13 | draggingClass: 'dragging' 14 | }, options); 15 | 16 | var $handle = this, 17 | $drag = this; 18 | 19 | if( options.handle ) { 20 | $handle = $(options.handle); 21 | } 22 | 23 | $handle 24 | .css('cursor', options.cursor) 25 | .on("mousedown", function(e) { 26 | var x = $drag.offset().left - e.pageX, 27 | y = $drag.offset().top - e.pageY; 28 | 29 | $(document.documentElement) 30 | .on('mousemove.drags', function(e) { 31 | $drag.offset({ 32 | left: x + e.pageX, 33 | top: y + e.pageY 34 | }); 35 | }) 36 | .one('mouseup', function() { 37 | $(this).off('mousemove.drags'); 38 | }); 39 | 40 | // disable selection 41 | e.preventDefault(); 42 | }); 43 | 44 | if( touchSupported ) { 45 | initTouchDrag($handle[0]); 46 | } 47 | }; 48 | })(jQuery); 49 | 50 | // Make touch events work: 51 | // http://stackoverflow.com/a/6362527/586086 52 | function touchHandler(event) { 53 | var touch = event.changedTouches[0]; 54 | 55 | var simulatedEvent = document.createEvent("MouseEvent"); 56 | simulatedEvent.initMouseEvent({ 57 | touchstart: "mousedown", 58 | touchmove: "mousemove", 59 | touchend: "mouseup" 60 | }[event.type], true, true, window, 1, 61 | touch.screenX, touch.screenY, 62 | touch.clientX, touch.clientY, false, 63 | false, false, false, 0, null); 64 | 65 | touch.target.dispatchEvent(simulatedEvent); 66 | // Clicks should continue to work 67 | if( event.type === "touchmove" ) event.preventDefault(); 68 | } 69 | 70 | function initTouchDrag(element) { 71 | element.addEventListener("touchstart", touchHandler, true); 72 | element.addEventListener("touchmove", touchHandler, true); 73 | element.addEventListener("touchend", touchHandler, true); 74 | element.addEventListener("touchcancel", touchHandler, true); 75 | } 76 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | ## vNEXT 2 | 3 | * Ensure that modal width is small enough for screens below 560px (e.g. mobile). 4 | * Update documentation for Meteor 1.0+. 5 | 6 | ## v0.6.8 7 | 8 | * Meteor's stylus package is deprecated, switch to the coagmano:stylus package 9 | 10 | ## v0.6.7 11 | 12 | * Update versions for Meteor 1.2. 13 | * Fix a potential race condition in loading the tutorial template. (#11) 14 | 15 | ## v0.6.6 16 | 17 | * Use weak dependencies on Bootstrap packages for more flexibility. 18 | 19 | ## v0.6.5 20 | 21 | * Switch to the official Bootstrap 3 package, `twbs:bootstrap`. 22 | 23 | ## v0.6.4 24 | 25 | * Switch to a better supported bootstrap package version. 26 | * Update template helper syntax for the new API. 27 | 28 | ## v0.6.3 29 | 30 | * **Update for Meteor 0.9.** 31 | 32 | ## v0.6.2 33 | 34 | * Make sure to run `onLoad` functions non-reactively, in case they access reactive variables. 35 | * Set z-index of dialog modal to be just below that of standard Bootstrap 3 modals. 36 | * Debounce the "Finish" button so that it can't be mashed repeatedly. 37 | 38 | ## v0.6.1 39 | 40 | * Basic support of touch dragging for repositioning the tutorial dialog. 41 | * Tutorial options can now take an `id` field. A tutorial with a given `id` will preserve its current step across a hot code reload (but not a hard reload, as this step is stored in `Session`.) 42 | 43 | ## v0.6.0 44 | 45 | * Now using Bootstrap 3. **Don't try to use this with something that also pulls in Bootstrap 2**. 46 | * Made an `EventEmitter` implementation available on the client. See the example app. 47 | * Improved standalone draggable code for the tutorial dialog. 48 | 49 | ## v0.5.0 50 | 51 | * Preliminary support for Meteor 0.8.0 (Blaze). 52 | * Removed dependency on jQuery UI as it was only used to make the modal draggable. 53 | 54 | **NOTE**: This package will be moving to Bootstrap 3. This is the last Bootstrap 2 release. 55 | 56 | ## v0.4.0 57 | 58 | * Improved width/height computations to support some SVG selections. 59 | * Added support for requiring actions to proceed in tutorial steps. 60 | * Allowed both string and direct function references for templates, for more flexible construction of steps. 61 | * Fixed a minor bug with the dialog not being draggable in IE. 62 | 63 | ## v0.3.0 64 | 65 | * Allow for full dimming (`spot: null`) in addition to full brightness (with `spot: "body"`). 66 | * Add the option to specify a finish function. **Note**: breaking changes with previous format. 67 | 68 | ## v0.2.0 69 | 70 | * Added dependency on jQuery UI. 71 | -------------------------------------------------------------------------------- /examples/simple-test/client/testtut.html: -------------------------------------------------------------------------------- 1 | 2 | testtut 3 | 4 | 5 | 6 | 7 | 8 | 23 | 24 | 33 | 34 | 47 | 48 | 56 | 57 | 58 | 62 | 63 | 72 | 73 | 83 | 84 | 87 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | meteor-tutorials 2 | ================ 3 | 4 | ## What's this do? 5 | 6 | Easily create super cool animated tutorials for your Meteor app. This package gives you a dirt easy way to make multi-step tutorials that spotlight multiple parts of your user interface. What a great way to show off how awesome your Meteor app is! 7 | 8 | [![Animated example](https://j.gifs.com/W6QO44.gif)](https://www.youtube.com/watch?v=smax46TNPPk) 9 | 10 | [See a demo](http://testtut.meteor.com/). 11 | 12 | Here's one of my apps. Among other things, it has a list of online users (provided by the [user-status package](https://github.com/mizzao/meteor-user-status)) and a chat room. As users go through the tutorial, which has several steps, the tutorial explains different parts of the user interface to them. 13 | 14 | ![Spotlighting the user list](docs/highlight_1.png) 15 | 16 | Later on, the chat room is shown: 17 | 18 | ![Spotlighting the user list](docs/highlight_2.png) 19 | 20 | The awesomeness of this tutorial is that it's completely autogenerated and moves with your code. All you have to do is specify the templates for each step and a selector for what should be spotlighted, and the spotlight and modal positions are computed automatically. This means that you won't have to hardcode anything or do much maintenance when your app changes. Best of all, it's animated and looks great! 21 | 22 | ## Install 23 | 24 | Install with Meteorite: 25 | 26 | ``` 27 | meteor add mizzao:tutorials 28 | ``` 29 | 30 | ## Usage 31 | 32 | First, specify some templates for your tutorial. Easy as pie: 33 | 34 | ```html 35 | 38 | 39 | 42 | ``` 43 | 44 | Next, define the steps of your tutorial in a helper accessible by whatever template is drawing the tutorial. 45 | 46 | ```js 47 | tutorialSteps = [ 48 | { 49 | template: Template.tutorial_step1, 50 | onLoad: function() { console.log("The tutorial has started!"); } 51 | }, 52 | { 53 | template: Template.tutorial_step2, 54 | spot: ".myElement, .otherElement", 55 | require: { 56 | event: "something-emitted", 57 | validator: function(args) { ... } 58 | } 59 | } 60 | ]; 61 | 62 | Template.foo.helpers({ 63 | options: { 64 | id: "myCoolTutorial", 65 | steps: tutorialSteps, 66 | emitter: new EventEmitter(), 67 | onFinish: function() { /* make the tutorial disappear */ } 68 | } 69 | }); 70 | ``` 71 | 72 | The `id` field of the options is optional. If provided, it preserves the current step of the tutorial across a hot code reload by saving it in a `Session` variable. You will probably find this very useful when testing your tutorial. 73 | 74 | The steps of the tutorial should be an array of objects, which take the following form: 75 | 76 | - `template`: (**required**) The template that should be displayed in the modal for this step. You can specify this either directly as a `Template.foo` object, or as a string like `"foo"`. 77 | - `spot`: (*optional*) jQuery selector of elements to highlight (can be a single selector or separated by commas). If multiple elements are selected, the tutorial automatically calculates a box that will fit around all of them. 78 | - `onLoad`: (*optional*) a function that will run whenever the tutorial hits this step. Helpful if you need to make sure your interface is in a certain state before displaying the tutorial contents. 79 | - `require`: (*optional*) an object with an `event` argument and an optional `validator` function to listen for a required user or other action to happen before the **Next** or **Finish** buttons appear. Used in conjunction with an `EventEmitter` instance, as below. 80 | 81 | Now, just call the `tutorial` helper with your `steps` from a template whose [offset parent](http://api.jquery.com/offsetParent/) is the same size as the body. This is necessary because the tutorial content is absolutely positioned relative to the window. 82 | 83 | ```html 84 |