├── .gitignore ├── Gruntfile.js ├── LICENSE.md ├── Procfile ├── README.md ├── app.js ├── bin └── www ├── package.json ├── public ├── images │ ├── aboutme.png │ ├── apple.png │ ├── banana.png │ ├── carrot.png │ ├── caution.png │ └── tab-cola.png ├── javascripts │ ├── config.js │ ├── lib │ │ ├── addPrefixedClass.js │ │ ├── add_view_to_slides.js │ │ ├── behavior.tabs.js │ │ ├── get_nested_view.js │ │ ├── get_view_for_slide.js │ │ └── router.js │ ├── main.js │ ├── slides │ │ ├── donts.js │ │ ├── routing.js │ │ └── tabs.js │ ├── template │ │ └── slides.html │ └── view │ │ ├── slide.js │ │ └── tabbed.js └── stylesheets │ └── style.styl ├── routes └── index.js └── views ├── error.jade ├── index.jade └── layout.jade /.gitignore: -------------------------------------------------------------------------------- 1 | public/javascripts/vendor/ 2 | public/stylesheets/style.css 3 | 4 | ### SublimeText template 5 | # workspace files are user-specific 6 | *.sublime-workspace 7 | 8 | # project files should be checked into the repository, unless a significant 9 | # proportion of contributors will probably not be using SublimeText 10 | # *.sublime-project 11 | 12 | 13 | ### Node template 14 | # Logs 15 | logs 16 | *.log 17 | 18 | # Runtime data 19 | pids 20 | *.pid 21 | *.seed 22 | 23 | # Directory for instrumented libs generated by jscoverage/JSCover 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | coverage 28 | 29 | # Compiled binary addons (http://nodejs.org/api/addons.html) 30 | build/Release 31 | 32 | # Dependency directory 33 | node_modules 34 | 35 | # Users Environment Variables 36 | .lock-wscript 37 | 38 | 39 | ### OSX template 40 | .DS_Store 41 | .AppleDouble 42 | .LSOverride 43 | 44 | # Icon must end with two \r 45 | Icon 46 | 47 | # Thumbnails 48 | ._* 49 | 50 | # Files that might appear on external disk 51 | .Spotlight-V100 52 | .Trashes 53 | 54 | # Directories potentially created on remote AFP share 55 | .AppleDB 56 | .AppleDesktop 57 | Network Trash Folder 58 | Temporary Items 59 | .apdisk 60 | 61 | 62 | ### JetBrains template 63 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm 64 | 65 | ## Directory-based project format 66 | .idea/ 67 | /*.iml 68 | 69 | ## File-based project format 70 | *.ipr 71 | *.iws 72 | 73 | ## Additional for IntelliJ 74 | out/ 75 | 76 | # generated by mpeltonen/sbt-idea plugin 77 | .idea_modules/ -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var unwrap = require('unwrap'); 3 | var modRewrite = require('connect-modrewrite'); 4 | 5 | module.exports = function(grunt) { 6 | 7 | require('load-grunt-tasks')(grunt); 8 | 9 | // Project configuration. 10 | grunt.initConfig({ 11 | pkg: grunt.file.readJSON('package.json'), 12 | 13 | connect: { 14 | server: { 15 | options: { 16 | keepalive: true, 17 | livereload: true, 18 | port: 9000, 19 | hostname: '*', 20 | }, 21 | } 22 | }, 23 | 24 | jshint: { 25 | options: { 26 | jshintrc: '.jshintrc', 27 | reporter: require('jshint-stylish') 28 | }, 29 | all: { 30 | files: [{ 31 | expand: true, 32 | src: [ 33 | 'src/**/*.js', 34 | 'spec/**/*.js', 35 | 'demo/**/*.js', 36 | // Exclude the following 37 | '!demo/js/highlight/**/*', 38 | '!spec/setup/node.js', 39 | '!spec/setup/require_helper.js', 40 | ] 41 | }] 42 | }, 43 | src: { 44 | files: [{ 45 | expand: true, 46 | src: [ 47 | 'src/**/*.js' 48 | ] 49 | }] 50 | }, 51 | spec: { 52 | files: [{ 53 | expand: true, 54 | src: [ 55 | 'spec/**/*.js', 56 | // Exclude the following 57 | '!spec/setup/node.js', 58 | '!spec/setup/require_helper.js', 59 | ] 60 | }] 61 | }, 62 | demo: { 63 | files: [{ 64 | expand: true, 65 | src: [ 66 | 'demo/**/*.js', 67 | // Exclude the following 68 | '!**/js/highlight/**/*', 69 | ] 70 | }] 71 | }, 72 | changed: { 73 | src: '' 74 | } 75 | }, 76 | 77 | watch: { 78 | js: { 79 | files: [ 80 | 'src/**/*.js', 81 | 'spec/**/*.js', 82 | 'demo/**/*.js', 83 | // Exclude the following 84 | '!foo/**/*', 85 | ], 86 | tasks: ['jshint:changed'], 87 | options: { 88 | spawn: false 89 | } 90 | }, 91 | } 92 | 93 | }); 94 | 95 | // define the js task 96 | grunt.registerTask('dev', ['watch:js']); 97 | 98 | grunt.registerTask('verify-bower', function () { 99 | if (!grunt.file.isDir('./bower_components')) { 100 | grunt.fail.warn('Missing bower components. You should run `bower install` before.'); 101 | } 102 | }); 103 | 104 | grunt.registerTask('test', 'Run the unit tests.', ['mochaTest']); 105 | 106 | // For the quick version of jshinting 107 | grunt.event.on('watch', function(action, filepath) { 108 | grunt.config('jshint.changed.src', filepath); 109 | }); 110 | 111 | }; 112 | 113 | //module.exports = function(grunt) { 114 | // 115 | // grunt.initConfig({ 116 | // pkg: grunt.file.readJSON('package.json'), 117 | // 118 | // requirejs: { 119 | // build: { 120 | // options: {} 121 | // } 122 | // }, 123 | // jshint: { 124 | // options: { 125 | // jshintrc: '.jshintrc', 126 | // reporter: require('jshint-stylish') 127 | // }, 128 | // all: { 129 | // files: [{ 130 | // expand: true, 131 | // src: [ 132 | // 'src/**/*.js', 133 | // // Exclude the following 134 | // '!foo/**/*', 135 | // ] 136 | // }] 137 | // }, 138 | // changed: { 139 | // src: '' 140 | // } 141 | // }, 142 | // watch: { 143 | // js: { 144 | // files: [ 145 | // 'src/**/*.js', 146 | // // Exclude the following 147 | // '!foo/**/*', 148 | // ], 149 | // tasks: ['jshint:changed'], 150 | // options: { 151 | // spawn: false 152 | // } 153 | // }, 154 | // } 155 | // }); 156 | // 157 | // // define the js task 158 | // grunt.registerTask('js', ['watch:js']); 159 | // 160 | // // we can add other tasks to this default task 161 | // // as we modify other parts of our build process 162 | // grunt.registerTask('default', ['js']); 163 | // 164 | // // For the quick version of jshinting 165 | // grunt.event.on('watch', function(action, filepath) { 166 | // grunt.config('jshint.changed.src', filepath); 167 | // }); 168 | // 169 | //}; 170 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) {{{year}}} {{{fullname}}} 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node ./bin/www -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # scoped-router 2 | 3 | This project is a slide presentation which demonstrates a method for showing Marionette views in a tabbed user interface, using a custom Marionette router that operates within a 'scope' in order to track history and link directly to any tab in the view hierarchy. 4 | 5 | # to view the slides locally 6 | 7 | Clone this repo, then do: 8 | 9 | `npm i` 10 | 11 | `npm start` 12 | 13 | `open http://localhost:9000` 14 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var path = require('path'); 3 | var favicon = require('serve-favicon'); 4 | var logger = require('morgan'); 5 | var cookieParser = require('cookie-parser'); 6 | var bodyParser = require('body-parser'); 7 | 8 | var routes = require('./routes/index'); 9 | 10 | var app = express(); 11 | 12 | // view engine setup 13 | app.set('views', path.join(__dirname, 'views')); 14 | app.set('view engine', 'jade'); 15 | 16 | // uncomment after placing your favicon in /public 17 | //app.use(favicon(__dirname + '/public/favicon.ico')); 18 | app.use(logger('dev')); 19 | app.use(bodyParser.json()); 20 | app.use(bodyParser.urlencoded({ extended: false })); 21 | app.use(cookieParser()); 22 | app.use(require('stylus').middleware(path.join(__dirname, 'public'))); 23 | app.use(express.static(path.join(__dirname, 'public'))); 24 | 25 | app.use('/', routes); 26 | 27 | // catch 404 and forward to error handler 28 | app.use(function(req, res, next) { 29 | var err = new Error('Not Found'); 30 | err.status = 404; 31 | next(err); 32 | }); 33 | 34 | // error handlers 35 | 36 | // development error handler 37 | // will print stacktrace 38 | if (app.get('env') === 'development') { 39 | app.use(function(err, req, res, next) { 40 | res.status(err.status || 500); 41 | res.render('error', { 42 | message: err.message, 43 | error: err 44 | }); 45 | }); 46 | } 47 | 48 | // production error handler 49 | // no stacktraces leaked to user 50 | app.use(function(err, req, res, next) { 51 | res.status(err.status || 500); 52 | res.render('error', { 53 | message: err.message, 54 | error: {} 55 | }); 56 | }); 57 | 58 | 59 | module.exports = app; 60 | -------------------------------------------------------------------------------- /bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var debug = require('debug')('scoped-router-node'); 3 | var app = require('../app'); 4 | var copy = require('copy-files'); 5 | require('colors'); 6 | 7 | app.set('port', process.env.PORT || 9000); 8 | 9 | copy({ 10 | files: { 11 | 'backbone.js': 'node_modules/backbone/backbone.js', 12 | 'backbone.marionette.js': 'node_modules/backbone.marionette/lib/backbone.marionette.js', 13 | 'backbone.radio.js': 'node_modules/backbone.radio/build/backbone.radio.js', 14 | 'jquery.js': 'node_modules/jquery/dist/jquery.js', 15 | 'underscore.js': 'node_modules/underscore/underscore.js', 16 | 'require.js': 'node_modules/requirejs/require.js', 17 | 'text.js': 'node_modules/requirejs-text/text.js' 18 | }, 19 | dest: 'public/javascripts/vendor' 20 | }, function (err) { 21 | console.log('All JS libs copied, Pascal!'); 22 | }); 23 | 24 | 25 | 26 | var server = app.listen(app.get('port'), function() { 27 | console.log(' _______________________'.magenta); 28 | console.log('()===( (@===()'.magenta); 29 | console.log(' \'______________________\'|'.magenta); 30 | console.log(' | |'.magenta); 31 | console.log(' | Pascal\'s |'.magenta); 32 | console.log(' | awesome app |'.magenta); 33 | console.log(' | is now |'.magenta); 34 | console.log(' | running! |'.magenta); 35 | console.log(' | |'.magenta); 36 | console.log(' | |'.magenta); 37 | console.log(' | love, |'.magenta); 38 | console.log(' | Channing Tatum |'.magenta); 39 | console.log(' _)_____________________|'.magenta); 40 | console.log('()===( (@===()'.magenta); 41 | console.log(' \'----------------------\''.magenta); 42 | console.log(' '.magenta); 43 | 44 | 45 | console.log('Check out Pascal\'s app at http://localhost:' + server.address().port); 46 | console.log('Press Ctrl+C to stop.'); 47 | 48 | debug('Express server listening on port ' + server.address().port); 49 | }); 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scoped-router", 3 | "version": "0.0.0", 4 | "private": true, 5 | "author": "Pascal ", 6 | "license": "MIT", 7 | "scripts": { 8 | "start": "nodemon ./bin/www" 9 | }, 10 | "dependencies": { 11 | "express": "^4.11.0", 12 | "body-parser": "^1.10.1", 13 | "colors": "^1.0.3", 14 | "cookie-parser": "~1.3.3", 15 | "morgan": "~1.5.1", 16 | "serve-favicon": "^2.2.0", 17 | "debug": "~2.1.1", 18 | "jade": "^1.9.0", 19 | "stylus": "^0.49.3", 20 | "backbone": "^1.1.2", 21 | "backbone.marionette": "^2.3.1", 22 | "backbone.radio": "^0.8.4", 23 | "copy-files": "^0.1.0", 24 | "jquery": "^3.0.0", 25 | "requirejs": "^2.1.15", 26 | "requirejs-text": "^2.0.12", 27 | "underscore": "^1.7.0" 28 | }, 29 | "devDependencies": { 30 | "nodemon": "^1.2.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /public/images/aboutme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pascalpp/scoped-router/0d037fbb3438c0b60dcf7377058a1fba61be4b9e/public/images/aboutme.png -------------------------------------------------------------------------------- /public/images/apple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pascalpp/scoped-router/0d037fbb3438c0b60dcf7377058a1fba61be4b9e/public/images/apple.png -------------------------------------------------------------------------------- /public/images/banana.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pascalpp/scoped-router/0d037fbb3438c0b60dcf7377058a1fba61be4b9e/public/images/banana.png -------------------------------------------------------------------------------- /public/images/carrot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pascalpp/scoped-router/0d037fbb3438c0b60dcf7377058a1fba61be4b9e/public/images/carrot.png -------------------------------------------------------------------------------- /public/images/caution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pascalpp/scoped-router/0d037fbb3438c0b60dcf7377058a1fba61be4b9e/public/images/caution.png -------------------------------------------------------------------------------- /public/images/tab-cola.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pascalpp/scoped-router/0d037fbb3438c0b60dcf7377058a1fba61be4b9e/public/images/tab-cola.png -------------------------------------------------------------------------------- /public/javascripts/config.js: -------------------------------------------------------------------------------- 1 | requirejs.config({ 2 | baseUrl: '/javascripts', 3 | paths: { 4 | 'jquery': 'vendor/jquery', 5 | 'backbone': 'vendor/backbone', 6 | 'marionette': 'vendor/backbone.marionette', 7 | 'backbone.radio': 'vendor/backbone.radio', 8 | 'underscore': 'vendor/underscore', 9 | 'text': 'vendor/text' 10 | }, 11 | 12 | shim: { 13 | underscore: { 14 | exports: '_' 15 | } 16 | } 17 | }); 18 | 19 | define(function(require) { 20 | 'use strict'; 21 | 22 | require('main'); 23 | }); -------------------------------------------------------------------------------- /public/javascripts/lib/addPrefixedClass.js: -------------------------------------------------------------------------------- 1 | /* 2 | $.addPrefixedClass 3 | DOM helper for setting a mutually-exclusive prefixed classname on a node 4 | e.g. $('.profile.type-foo').setPrefixedClassname('type', 'bar') will become .profile.type-bar 5 | */ 6 | (function(root, factory) { 7 | // Set up appropriately for the environment. Start with AMD. 8 | if (typeof define === 'function' && define.amd) { 9 | define(['jquery'], function($) { 10 | factory($); 11 | }); 12 | 13 | // or, using global jquery 14 | } else { 15 | factory((root.jQuery || root.Zepto || root.ender || root.$)); 16 | } 17 | 18 | }(this, function($){ 19 | 'use strict'; 20 | $.fn.addPrefixedClass = function(prefix, suffix) { 21 | var regex = new RegExp(prefix+'-.+'), 22 | classname = prefix+'-'+suffix; 23 | 24 | return this.each(function() { 25 | var $this = $(this); 26 | if ($this.hasClass(classname)) return; // don't apply if already applied 27 | 28 | var classes = $this.attr('class') || ''; 29 | classes = $.map(classes.split(' '), function(c) { 30 | if (c && ! regex.test(c)) return c; 31 | }); 32 | classes.push(classname); 33 | 34 | $this.attr('class', classes.join(' ')); 35 | }); 36 | }; 37 | $.fn.removePrefixedClass = function(prefix) { 38 | var regex = new RegExp(prefix+'-.+'); 39 | 40 | return this.each(function() { 41 | var $this = $(this); 42 | 43 | var classes = $this.attr('class') || ''; 44 | classes = $.map(classes.split(' '), function(c) { 45 | if (c && ! regex.test(c)) return c; 46 | }); 47 | 48 | $this.attr('class', classes.join(' ')); 49 | }); 50 | }; 51 | 52 | })); 53 | -------------------------------------------------------------------------------- /public/javascripts/lib/add_view_to_slides.js: -------------------------------------------------------------------------------- 1 | define(function(require) { 2 | 'use strict'; 3 | 4 | var getView = require('lib/get_view_for_slide'); 5 | 6 | /* helper method for demo presentation */ 7 | var addViews = function(slides) { 8 | _.each(slides, function(slide) { 9 | if (! slide.view) { 10 | slide.view = getView(slide.id); 11 | } 12 | }); 13 | }; 14 | 15 | return addViews; 16 | 17 | }); 18 | -------------------------------------------------------------------------------- /public/javascripts/lib/behavior.tabs.js: -------------------------------------------------------------------------------- 1 | define(function(require) { 2 | 'use strict'; 3 | 4 | /* MODULE DEPENDENCIES */ 5 | var Backbone = require('backbone'); 6 | var Marionette = require('marionette'); 7 | var Router = require('./router'); 8 | 9 | /* 10 | NOTE: don't look too closely at this code :) 11 | I plan to publish this behavior in its own repo, but I have a bit of refactoring to do before I release it. 12 | */ 13 | 14 | 15 | var controller; 16 | 17 | var Controller = Marionette.Controller.extend({ 18 | initialize: function() { 19 | //console.log('controller initialize'); 20 | this.cid = _.uniqueId('controller'); 21 | this.scope_model = new Backbone.Model(); 22 | }, 23 | setCurrentTabIdForScope: function(scope, tab_id) { 24 | if (! scope) return; 25 | if (! tab_id) throw 'tab_id required'; 26 | this.scope_model.set(scope, tab_id); 27 | }, 28 | getCurrentTabIdForScope: function(scope) { 29 | return this.scope_model.get(scope) || ''; 30 | }, 31 | }); 32 | 33 | 34 | var Tab = Backbone.Model.extend({ 35 | defaults: { 36 | destroy: true, 37 | label: '', 38 | viewOptions: {}, 39 | visible: true, 40 | }, 41 | 42 | initialize: function() { 43 | // inherit superclass initialize 44 | Backbone.Model.prototype.initialize.apply(this, arguments); 45 | 46 | // if view attribute is not a generator, set destroy to false 47 | // so view can be re-used. not advisable for most use cases. 48 | //this.set('destroy', _.isFunction(this.get('view'))); 49 | //log(this.get('id'), this.get('destroy')); 50 | 51 | this.on('invalid', function() { 52 | console.error(this.get('id'), this.validationError); 53 | }); 54 | 55 | }, 56 | getView: function() { 57 | var View = this.get('view'); 58 | var view = new View(this.get('viewOptions')); 59 | view.$el.addClass('tab tab-'+this.get('id')); 60 | return view; 61 | }, 62 | validate: function(attrs) { 63 | if (! attrs.id) { 64 | return 'Tab requires an id.'; 65 | } 66 | 67 | if (! attrs.view) { 68 | return 'Tab requires a view.'; 69 | } 70 | 71 | if (! (attrs.view.prototype instanceof Backbone.View)) { 72 | return 'Tab view must be a Backbone.View'; 73 | } 74 | 75 | if (! _.isObject(attrs.viewOptions)) { 76 | return 'Tab viewOptions must be an object'; 77 | } 78 | 79 | if (_.isUndefined(attrs.label)) { 80 | return 'Tab requires a label.'; 81 | } 82 | } 83 | }); 84 | 85 | var TabList = Backbone.Collection.extend({ 86 | model: Tab 87 | }); 88 | 89 | var ButtonBarItemView = Marionette.ItemView.extend({ 90 | tagName: 'span', 91 | className: 'button', 92 | template: _.template('<%= label %>'), 93 | triggers: { 94 | 'click': 'item:click' 95 | } 96 | }); 97 | var ButtonBarView = Marionette.CollectionView.extend({ 98 | tagName: 'div', 99 | className: 'buttonbar', 100 | childView: ButtonBarItemView, 101 | initialize: function(options) { 102 | if (! options.tabs) throw new Error('Tabnav requires tabs.'); 103 | this.tabs = options.tabs; 104 | this.listenTo(this.tabs.model, 'change:current_tab_id', this.setActiveItem); 105 | this.listenTo(this, 'render', this.setActiveItem); 106 | // experimental support for tab.visible property 107 | this.listenTo(this.collection, 'change:visible', this.onChangeTabVisible); 108 | window.foobuttons = this; // DNR 109 | }, 110 | onEvent: function() { 111 | //console.log(arguments); 112 | }, 113 | childEvents: { 114 | 'item:click': 'onClickItem' 115 | }, 116 | onClickItem: function(child) { 117 | this.tabs.setCurrentTabId(child.model.get('id')); 118 | }, 119 | setActiveItem: function() { 120 | //console.log(this.tabs.options.scope, 'setActiveItem'); 121 | this.$('.active').removeClass('active'); 122 | var tab = this.tabs.getCurrentTab(); 123 | if (tab) { 124 | var view = this.children.findByModel(tab); 125 | view.$el.addClass('active'); 126 | } 127 | }, 128 | 129 | // experimental support for tab.visible property 130 | showCollection: function() { 131 | // override parent method to ignore tabs with visible:false 132 | var ChildView; 133 | var visible_children = this.collection.where({ visible: true }); 134 | 135 | _(visible_children).each(function(child, index) { 136 | ChildView = this.getChildView(child); 137 | this.addChild(child, ChildView, index); 138 | }, this); 139 | }, 140 | // experimental support for tab.visible property 141 | onChangeTabVisible: function(tab, visible, options) { 142 | if (visible) { 143 | // some hidden tab is now visible 144 | // re-render rather than try to figure out where to insert it 145 | this.render(); 146 | } else { 147 | // remove the view for this tab 148 | // and show the previous adjacent tab, or the first tab 149 | var previous_tab_index = this.collection.indexOf(tab) - 1 || 0; 150 | var view = this.children.findByModel(tab); 151 | view.remove(); 152 | this.tabs.showTabByIndex(previous_tab_index); 153 | } 154 | } 155 | }); 156 | 157 | var MenuItemView = Marionette.ItemView.extend({ 158 | tagName: 'option', 159 | template: _.template('<%= label %>'), 160 | initialize: function() { 161 | this.$el.attr('value', this.model.cid); 162 | } 163 | }); 164 | var MenuView = Marionette.CollectionView.extend({ 165 | tagName: 'select', 166 | className: 'tabmenu', 167 | childView: MenuItemView, 168 | initialize: function(options) { 169 | if (! options.tabs) throw new Error('Tabnav requires tabs.'); 170 | this.tabs = options.tabs; 171 | this.collection = options.tabs.collection; 172 | this.listenTo(this.tabs.model, 'change:current_tab_id', this.setActiveItem); 173 | this.listenTo(this, 'render', this.setActiveItem); 174 | }, 175 | events: { 176 | 'change': 'onSelectItem' 177 | }, 178 | onRender: function() { 179 | this.$el.prepend(''); 180 | }, 181 | onSelectItem: function() { 182 | var cid = this.$el.val(); 183 | var tab = this.collection.get(cid); 184 | this.tabs.setCurrentTabId(tab.get('id')); 185 | }, 186 | setActiveItem: function() { 187 | var tab = this.tabs.getCurrentTab(); 188 | if (tab && tab.cid) this.$el.val(tab.cid); 189 | } 190 | }); 191 | 192 | 193 | var Tabs = Marionette.Behavior.extend({ 194 | 195 | initialize: function() { 196 | this.options = this.options || {}; 197 | 198 | var demodelay = localStorage.getItem('demodelay') || 0; // DNR 199 | 200 | // validate options 201 | _.defaults(this.options, { 202 | tabs: [], 203 | routing: true, 204 | show_initial_tab: true, 205 | initial_tab_id: '', 206 | wraparound: false 207 | }); 208 | if (! this.options.region) throw new Error('Tabs behavior requires a region'); 209 | 210 | if (demodelay) this.options.show_initial_tab = false; // DNR 211 | 212 | this.cid = _.uniqueId('tabs'); 213 | //console.log('tabs init', this.options.scope, this.cid); 214 | this.view.tabs = this; 215 | 216 | 217 | // set up central controller 218 | if (! controller) controller = new Controller(); 219 | 220 | // if this.options.initial_tab_id isn't set explicitly, 221 | // use the last tab shown for this scope, if defined by a previous tab instance 222 | if (! this.options.initial_tab_id) { 223 | var initial_tab_id = controller.getCurrentTabIdForScope(this.options.scope); 224 | if (initial_tab_id) this.options.initial_tab_id = initial_tab_id; 225 | } 226 | 227 | // this.initializeRouter(); 228 | _.bindAll(this, 'initializeRouter'); // DNR - for demo 229 | _.delay(this.initializeRouter, demodelay); // DNR - for demo 230 | 231 | // set up view model and tablist 232 | this.model = new Backbone.Model({ current_tab_id: this.options.initial_tab_id }); 233 | this.collection = new TabList(); 234 | 235 | _.bindAll(this, 'autoShowFirstTab'); 236 | this.autoShowFirstTab = _.debounce(this.autoShowFirstTab, 20); 237 | 238 | window.footabs = this; // DNR 239 | }, 240 | 241 | initializeRouter: function() { 242 | if (this.options.routing) { 243 | this.router = new Router({ 244 | controller: this, 245 | scope: this.options.scope, 246 | appRoutes: { 247 | '(:tab_id)(/)(*params)': 'routeTabId' 248 | } 249 | }); 250 | } 251 | }, 252 | 253 | onShow: function() { 254 | //console.log('tabs show'); 255 | 256 | // set up model events 257 | this.listenTo(this.model, 'change:current_tab_id', this.onChangeCurrentTabId); 258 | this.on('next', this.showNextTab); 259 | this.on('previous', this.showPreviousTab); 260 | 261 | this.addTabs(this.options.tabs); 262 | 263 | if (this.options.nav) this.createNav(); 264 | 265 | _.defer(this.autoShowFirstTab); 266 | }, 267 | 268 | onDestroy: function() { 269 | //console.log('tabs destroy'); 270 | if (this.router) this.router.destroy(); 271 | }, 272 | 273 | onChangeCurrentTabId: function(model, tab_id, options) { 274 | //console.log('onChangeCurrentTabId', arguments); 275 | this.showCurrentTab(options); 276 | controller.setCurrentTabIdForScope(this.options.scope, this.model.get('current_tab_id')); 277 | }, 278 | 279 | addTab: function(tab) { 280 | if (! (tab instanceof Tab)) tab = new Tab(tab); 281 | if (! tab.isValid()) return tab.validationError; 282 | 283 | this.collection.add(tab); 284 | 285 | }, 286 | 287 | addTabs: function(tabs) { 288 | _.each(tabs, function(tab) { 289 | this.addTab(tab); 290 | }, this); 291 | }, 292 | 293 | getTabById: function(tab_id) { 294 | return this.collection.findWhere({id:tab_id}); 295 | }, 296 | 297 | 298 | getCurrentTab: function() { 299 | var tab_id = this.model.get('current_tab_id'); 300 | return this.getTabById(tab_id); 301 | }, 302 | 303 | showCurrentTab: function(options) { 304 | //console.log('showCurrentTab', this.options.scope, options); 305 | options = options || {}; 306 | 307 | var tab = this.getCurrentTab(); 308 | 309 | if (tab && tab.isValid()) { 310 | if (tab.get('shown')) return; //console.log('tab already showing'); 311 | 312 | var last_tab_id = this.model.get('last_tab_id'), 313 | last_tab = this.getTabById(last_tab_id); 314 | if (last_tab) { 315 | options.preventDestroy = ! last_tab.get('destroy'); 316 | } 317 | 318 | var tab_view = tab.getView(); 319 | this.view.triggerMethod('before:show:tab', tab_view); 320 | this.options.region.show(tab_view, options); 321 | tab.set('shown', true); 322 | this.listenTo(tab_view, 'destroy', function() { 323 | tab.unset('shown'); 324 | }); 325 | this.view.triggerMethod('show:tab', tab_view); 326 | this.setHistory(options); 327 | } 328 | }, 329 | 330 | routeTabId: function(tab_id) { 331 | this.setCurrentTabId(tab_id); 332 | }, 333 | 334 | setCurrentTabId: function(tab_id, options) { 335 | options = options || {}; 336 | var tab = this.getTabById(tab_id); 337 | if (tab && tab.isValid()) { 338 | var last_tab_id = this.model.get('current_tab_id'); 339 | this.model.set('last_tab_id', last_tab_id, options); 340 | this.model.set('current_tab_id', tab_id, options); 341 | } 342 | }, 343 | 344 | showTabById: function(tab_id, options) { 345 | this.setCurrentTabId(tab_id, options); 346 | }, 347 | 348 | showTabByIndex: function(n) { 349 | if (this.options.wraparound) { 350 | if (n < 0) n = this.collection.length - 1; 351 | if (n > this.collection.length - 1) n = 0; 352 | } else { 353 | n = Math.max(0, n); 354 | n = Math.min(n, this.collection.length - 1); 355 | } 356 | var tab = this.collection.at(n); 357 | this.setCurrentTabId(tab.get('id')); 358 | }, 359 | 360 | showNextTab: function() { 361 | var tab = this.getCurrentTab(), 362 | index = this.collection.indexOf(tab); 363 | 364 | index++; 365 | this.showTabByIndex(index); 366 | }, 367 | 368 | showPreviousTab: function() { 369 | var tab = this.getCurrentTab(), 370 | index = this.collection.indexOf(tab); 371 | 372 | index--; 373 | this.showTabByIndex(index); 374 | }, 375 | 376 | autoShowFirstTab: function() { 377 | if (this.options.show_initial_tab) { 378 | if (this.model.get('current_tab_id')) { 379 | // DNR TODO this is causing tabs to render twice in some cases e.g. style tab of edit panel 380 | // but without it, no tab is shown when closing and reopening the edit panel 381 | // fixed by tracking 'shown' state on tab model. not perfect but will do for now 382 | this.showCurrentTab(); 383 | } else { 384 | var first_tab = this.collection.first(); 385 | if (first_tab) { 386 | this.setCurrentTabId(first_tab.get('id'), {replace: true}); 387 | } 388 | } 389 | } 390 | }, 391 | 392 | createNav: function() { 393 | _.defaults(this.options.nav, { 394 | view: ButtonBarView, 395 | viewOptions: {}, 396 | }); 397 | if (! this.options.nav.region) throw new Error('Tab nav requires a region'); 398 | 399 | var navViewOptions = _.extend(this.options.nav.viewOptions, { 400 | tabs: this, 401 | collection: this.collection 402 | }); 403 | 404 | this.nav = new this.options.nav.view(navViewOptions); 405 | 406 | this.options.nav.region.show(this.nav); 407 | 408 | }, 409 | 410 | setHistory: function(options) { 411 | if (! this.router) return; 412 | var tab_id = this.model.get('current_tab_id'); 413 | this.router.navigate(tab_id, options); 414 | } 415 | 416 | 417 | }); 418 | 419 | 420 | // define publicly accessible entities 421 | Tabs.ButtonBarView = ButtonBarView; 422 | Tabs.MenuView = MenuView; 423 | return Tabs; 424 | 425 | }); 426 | -------------------------------------------------------------------------------- /public/javascripts/lib/get_nested_view.js: -------------------------------------------------------------------------------- 1 | define(function(require) { 2 | 'use strict'; 3 | 4 | var Template = require('text!template/slides.html'); 5 | var getView = require('lib/get_view_for_slide'); 6 | 7 | /* helper method for demo presentation */ 8 | var getNestedView = function(id, NestedView) { 9 | var View = getView(id).extend({ 10 | regions: { 11 | 'nested_region': '.nested-region' 12 | }, 13 | onRender: function() { 14 | var nested_view = new NestedView(); 15 | this.nested_region.show(nested_view); 16 | } 17 | }); 18 | return View; 19 | }; 20 | 21 | return getNestedView; 22 | 23 | }); 24 | -------------------------------------------------------------------------------- /public/javascripts/lib/get_view_for_slide.js: -------------------------------------------------------------------------------- 1 | define(function(require) { 2 | 'use strict'; 3 | 4 | var Template = require('text!template/slides.html'); 5 | var SlideView = require('view/slide'); 6 | 7 | /* helper method for demo presentation */ 8 | var getView = function(id) { 9 | var template = $(Template).filter('script.'+id); 10 | if (! template || ! template.length) throw 'No template found for ' + id; 11 | var View = SlideView.extend({ 12 | template: _.template(template.html()) 13 | }); 14 | return View; 15 | }; 16 | 17 | return getView; 18 | 19 | }); 20 | -------------------------------------------------------------------------------- /public/javascripts/lib/router.js: -------------------------------------------------------------------------------- 1 | define(function(require) { 2 | 'use strict'; 3 | 4 | /* MODULE DEPENDENCIES */ 5 | var Backbone = require('backbone'); 6 | var Marionette = require('marionette'); 7 | 8 | 9 | // override Backbone.History.prototype.route 10 | // to allow for handlers to have a router associated with them 11 | // this allows us to remove routes dynamically e.g. in router.destroy (see below) 12 | Backbone.History.prototype.route = function(route, callback, router) { 13 | this.handlers.unshift({router: router, route: route, callback: callback}); 14 | }; 15 | 16 | 17 | var Router = Marionette.AppRouter.extend({ 18 | 19 | initialize: function(options) { 20 | Marionette.AppRouter.prototype.initialize.apply(this, arguments); 21 | 22 | _.bindAll(this, 'checkState'); 23 | // initialize is called first in the Marionette.AppRouter constructor, before appRoutes have been processed 24 | // have to defer checkState so that rest of constructor can complete first 25 | _.defer(this.checkState); 26 | }, 27 | 28 | checkState: function() { 29 | if (Backbone.History.started) { 30 | // if this router was initialized after history started, 31 | // it might have added a route that need to be triggered 32 | // this tells Backbone.history to check the current fragment for any matching routes 33 | Backbone.history.loadUrl(); 34 | // TODO DNR need to do ^this in a way that *only* checks this router's routes 35 | } else { 36 | // console.log('Backbone.history not started yet'); 37 | if (! Backbone.history) Backbone.history = new Backbone.History(); 38 | Backbone.history.start({ pushState: true }); 39 | } 40 | }, 41 | 42 | // override Backbone.Router.route method 43 | // the only difference is that we pass a reference to `this` to Backbone.history.route 44 | // so this router's routes can be removed onDestroy 45 | // need to audit this when upgrading Backbone `upgrade:backbone:audit` 46 | route: function(route, name, callback) { 47 | if (!_.isRegExp(route)) route = this._routeToRegExp(route); 48 | if (_.isFunction(name)) { 49 | callback = name; 50 | name = ''; 51 | } 52 | if (!callback) callback = this[name]; 53 | var router = this; 54 | Backbone.history.route(route, function(fragment) { 55 | var args = router._extractParameters(route, fragment); 56 | router.execute(callback, args); 57 | router.trigger.apply(router, ['route:' + name].concat(args)); 58 | router.trigger('route', name, args); 59 | Backbone.history.trigger('route', router, name, args); 60 | }, this); 61 | return this; 62 | }, 63 | 64 | // override Marionette.AppRouter.prototype._addAppRoute 65 | // to prepend `scope` to all routes 66 | _addAppRoute: function(controller, route, methodName) { 67 | if (this.options.scope) { 68 | route = this.options.scope + '/' + route; 69 | } 70 | Marionette.AppRouter.prototype._addAppRoute.apply(this, [controller, route, methodName]); 71 | }, 72 | 73 | // override Marionette.AppRouter.prototype.navigate 74 | // to prevent out-of-scope URL changes, and other automated niceties 75 | navigate: function(fragment, options) { 76 | /* jshint maxcomplexity: 11 */ 77 | options = options || {}; 78 | 79 | var current_fragment = Backbone.history.fragment, 80 | scope = this.options.scope, 81 | defaults = {}, 82 | re; 83 | 84 | /* jshint maxdepth: 4 */ 85 | if (scope) { 86 | if (fragment) { 87 | // prepend scope to fragment 88 | fragment = scope + '/' + fragment; 89 | 90 | // if current URL ends in scope, this is additive, so set replace: true 91 | // we only do this if options.replace isn't already set to false (using _.defaults below) 92 | re = new RegExp(scope+'/?$', 'i'); 93 | if (current_fragment.match(re)) { 94 | defaults.replace = true; 95 | } 96 | 97 | } else { 98 | // fragment is empty, use scope as fragment 99 | // this allows you to pass an empty string to reset URL to scope 100 | fragment = scope; 101 | } 102 | 103 | // validate that requested URL is in scope 104 | // use `force: true` to override this scope check 105 | re = new RegExp(scope, 'i'); 106 | if (! current_fragment.match(re)) { 107 | if (! options.force) { 108 | console.error('Tab URL is out of scope.', scope, current_fragment); 109 | return this; 110 | } 111 | } 112 | } 113 | 114 | // if URL already exists in fragment, don't overwrite URL 115 | // use `force: true` to force overwrite 116 | re = new RegExp(fragment); 117 | if (current_fragment.match(re)) { 118 | // console.log('URL is already in place, no need to write'); 119 | if (! options.force) return this; 120 | } 121 | 122 | // if URL is the same, but case is different, just replace the history state 123 | // e.g. navigate to /mixedcase/collections, URL will be updated to /MixedCase/collections 124 | // but it won't add an additional state to the history, so back button still works 125 | re = new RegExp(fragment+'/?$', 'i'); 126 | if (current_fragment.match(re)) { 127 | defaults.replace = true; 128 | } 129 | 130 | // apply our defaults to the passed options 131 | // e.g. if replace is explicitly set to false, then we can't override it 132 | _.defaults(options, defaults); 133 | 134 | return Marionette.AppRouter.prototype.navigate.apply(this, [fragment, options]); 135 | }, 136 | 137 | // custom destroy method 138 | // removes any handlers that belong to this router 139 | destroy: function() { 140 | // console.log('destroy router for scope', this.options.scope); 141 | // console.log('Backbone.history.handlers.length', Backbone.history.handlers.length); 142 | Backbone.history.handlers = _.filter(Backbone.history.handlers, function(handler) { 143 | return (handler.router !== this); 144 | }, this); 145 | // console.log('Backbone.history.handlers.length', Backbone.history.handlers.length); 146 | } 147 | 148 | }); 149 | 150 | return Router; 151 | 152 | }); 153 | -------------------------------------------------------------------------------- /public/javascripts/main.js: -------------------------------------------------------------------------------- 1 | define(function(require) { 2 | 3 | var Marionette = require('marionette'); 4 | var Radio = require('backbone.radio'); 5 | var Tabs = require('lib/behavior.tabs'); 6 | var TabbedView = require('view/tabbed'); 7 | var SlideView = require('view/slide'); 8 | var TabsSlide = require('slides/tabs'); 9 | var RoutingSlide = require('slides/routing'); 10 | var DontsSlide = require('slides/donts'); 11 | var getSlide = require('lib/get_view_for_slide'); 12 | var Template = require('text!template/slides.html'); 13 | require('lib/addPrefixedClass'); 14 | 15 | var AboutMeSlide = SlideView.extend({ 16 | template: _.template($(Template).filter('script.aboutme').html()) 17 | }); 18 | 19 | var main_channel = Backbone.Radio.channel('main'); 20 | 21 | var MainView = TabbedView.extend({ 22 | initialize: function() { 23 | TabbedView.prototype.initialize.apply(this, arguments); 24 | main_channel.comply('show:scope', this.showScope, this); 25 | main_channel.comply('toggle:tab', this.toggleTab, this); 26 | }, 27 | showScope: function(which) { 28 | if (which) { 29 | this.$el.addPrefixedClass('scope', which); 30 | } else { 31 | this.$el.removePrefixedClass('scope'); 32 | } 33 | }, 34 | toggleTab: function(id) { 35 | var tab = this.tabs.collection.findWhere({'id': id}) 36 | if (! tab) return; 37 | var visible = tab.get('visible'); 38 | tab.set('visible', ! visible); 39 | }, 40 | tabOptions: function() { 41 | return { 42 | scope: '', 43 | tabs: [ 44 | { id: 'intro', label: 'Intro', view: getSlide('intro') }, 45 | { id: 'aboutme', label: 'About Me', view: AboutMeSlide }, 46 | { id: 'tabbed-views', label: 'Tabbed Views', view: TabsSlide }, 47 | { id: 'routing', label: 'Routing', view: RoutingSlide }, 48 | ] 49 | }; 50 | } 51 | }); 52 | 53 | 54 | var main_region = new Marionette.Region({ 55 | el: '.main-region' 56 | }); 57 | 58 | var main_view = new MainView(); 59 | 60 | // create the app 61 | var app = new Marionette.Application(); 62 | 63 | // define global regions 64 | app.addRegions({ 65 | region: '.main-region' 66 | }); 67 | 68 | app.addInitializer(function(options) { 69 | app.region.show(main_view); 70 | }); 71 | 72 | app.start(); 73 | 74 | 75 | }); 76 | -------------------------------------------------------------------------------- /public/javascripts/slides/donts.js: -------------------------------------------------------------------------------- 1 | define(function(require) { 2 | 3 | var TabbedView = require('view/tabbed'); 4 | var Template = require('text!template/slides.html'); 5 | var SlideView = require('view/slide'); 6 | var getView = require('lib/get_view_for_slide'); 7 | var addViews = require('lib/add_view_to_slides'); 8 | 9 | 10 | var slides = [ 11 | { id: 'dont1', label: 'Don’t One' }, 12 | { id: 'dont2', label: 'Don’t Two' }, 13 | ]; 14 | 15 | addViews(slides); 16 | 17 | var DontsView = TabbedView.extend({ 18 | tabOptions: { 19 | scope: 'donts', 20 | tabs: slides 21 | } 22 | }); 23 | 24 | return DontsView; 25 | 26 | }); 27 | -------------------------------------------------------------------------------- /public/javascripts/slides/routing.js: -------------------------------------------------------------------------------- 1 | define(function(require) { 2 | 3 | var TabbedView = require('view/tabbed'); 4 | var Template = require('text!template/slides.html'); 5 | var SlideView = require('view/slide'); 6 | var getView = require('lib/get_view_for_slide'); 7 | var getNestedView = require('lib/get_nested_view'); 8 | var addViews = require('lib/add_view_to_slides'); 9 | 10 | var main_channel = Backbone.Radio.channel('main'); 11 | 12 | var backbone_slides = [ 13 | { id: 'appstart', label: 'App Initialization' }, 14 | { id: 'apprunning', label: 'While Your App Is Running' }, 15 | { id: 'limitations', label: 'Limitations' }, 16 | ]; 17 | 18 | addViews(backbone_slides); 19 | 20 | var BackboneNestedView = TabbedView.extend({ 21 | tabOptions: { 22 | scope: 'routing/backbone', 23 | tabs: backbone_slides 24 | } 25 | }); 26 | 27 | var BackboneView = getNestedView('backbone', BackboneNestedView); 28 | 29 | var NeedsView = getView('needs').extend({ 30 | events: { 31 | 'mouseover li.scope': 'showScope', 32 | }, 33 | showScope: function(e) { 34 | var li = $(e.target).closest('li'); 35 | var scope = li.data('scope'); 36 | main_channel.command('show:scope', scope); 37 | }, 38 | hideScope: function() { 39 | main_channel.command('show:scope'); 40 | }, 41 | onBeforeDestroy: function() { 42 | this.hideScope(); 43 | } 44 | }); 45 | 46 | 47 | var ScopesView = getView('scopes').extend({ 48 | events: { 49 | 'mouseover li span': 'showScope', 50 | 'mouseover li': 'showScope', 51 | 'mouseout li': 'hideScope', 52 | }, 53 | showScope: function(e) { 54 | var li = $(e.target).closest('li'); 55 | li.addClass('highlight'); 56 | var index = li.index() + 1; 57 | main_channel.command('show:scope', index); 58 | }, 59 | hideScope: function() { 60 | this.$('ul .highlight').removeClass('highlight'); 61 | main_channel.command('show:scope'); 62 | }, 63 | onBeforeDestroy: function() { 64 | this.hideScope(); 65 | } 66 | }); 67 | 68 | var RouterView = getView('router').extend({ 69 | onRender: function() { 70 | this.$el.addClass('code-annotations'); 71 | var code = $(Template).filter('script.router-code-sample').html(); 72 | this.$('pre').html(code.trim()); 73 | this.bindUIElements(); 74 | }, 75 | ui: { 76 | codenotes: 'pre .note', 77 | sidenotes: '.sidenotes .note' 78 | }, 79 | events: { 80 | 'mouseover pre .note': 'showNote', 81 | 'mouseout pre .note': 'hideNotes' 82 | }, 83 | showNote: function(e) { 84 | e.stopPropagation(); 85 | this.hideNotes(); 86 | var note = $(e.target).addClass('active').data('note'); 87 | if (! note) return; 88 | this.ui.sidenotes.filter('.note-'+note).show(); 89 | }, 90 | hideNotes: function() { 91 | this.ui.codenotes.removeClass('active'); 92 | this.ui.sidenotes.hide(); 93 | } 94 | }); 95 | 96 | var BehaviorCodeSampleView = getView('tabs-code').extend({ 97 | onRender: function() { 98 | this.$el.addClass('code-annotations'); 99 | var code = $(Template).filter('script.tabs-code-sample').html(); 100 | this.$('pre').html(code.trim()); 101 | this.bindUIElements(); 102 | }, 103 | ui: { 104 | codenotes: 'pre .note', 105 | sidenotes: '.sidenotes .note' 106 | }, 107 | events: { 108 | 'mouseover pre .note': 'showNote', 109 | 'mouseout pre .note': 'hideNotes' 110 | }, 111 | showNote: function(e) { 112 | e.stopPropagation(); 113 | this.hideNotes(); 114 | var note = $(e.target).addClass('active').data('note'); 115 | if (! note) return; 116 | this.ui.sidenotes.filter('.note-'+note).show(); 117 | }, 118 | hideNotes: function() { 119 | this.ui.codenotes.removeClass('active'); 120 | this.ui.sidenotes.hide(); 121 | } 122 | }); 123 | 124 | var solution_slides = [ 125 | { id: 'overview', label: 'Overview' }, 126 | { id: 'scopes', label: 'Scopes', view: ScopesView }, 127 | { id: 'router', label: 'Router', view: RouterView }, 128 | { id: 'behavior', label: 'View Behavior', view: BehaviorCodeSampleView }, 129 | { id: 'initialize', label: 'Router.initialize' }, 130 | { id: 'createroute', label: 'Router.route' }, 131 | { id: 'navigate', label: 'Router.navigate' }, 132 | { id: 'destroy', label: 'View.destroy' }, 133 | ]; 134 | 135 | addViews(solution_slides); 136 | 137 | var SolutionNestedView = TabbedView.extend({ 138 | tabOptions: { 139 | scope: 'routing/solution', 140 | tabs: solution_slides 141 | } 142 | }); 143 | 144 | var SolutionView = getNestedView('solution', SolutionNestedView); 145 | 146 | 147 | var routing_slides = [ 148 | { id: 'backbone', label: 'Backbone Routing', view: BackboneView }, 149 | { id: 'needs', label: 'What We Need', view: NeedsView }, 150 | { id: 'solution', label: 'Our Solution', view: SolutionView }, 151 | { id: 'caution', label: 'Caution' }, 152 | { id: 'tryit', label: 'Try It Out' }, 153 | ]; 154 | 155 | addViews(routing_slides); 156 | 157 | 158 | var RoutingSlide = TabbedView.extend({ 159 | tabOptions: { 160 | scope: 'routing', 161 | tabs: routing_slides 162 | } 163 | }); 164 | 165 | return RoutingSlide; 166 | 167 | }); 168 | -------------------------------------------------------------------------------- /public/javascripts/slides/tabs.js: -------------------------------------------------------------------------------- 1 | define(function(require) { 2 | 3 | var Radio = require('backbone.radio'); 4 | var TabbedView = require('view/tabbed'); 5 | var Template = require('text!template/slides.html'); 6 | var getView = require('lib/get_view_for_slide'); 7 | var addViews = require('lib/add_view_to_slides'); 8 | 9 | var channel = Backbone.Radio.channel('tabs'); 10 | var main_channel = Backbone.Radio.channel('main'); 11 | 12 | var nested_slides = [ 13 | { id: 'apple', label: 'Apple' }, 14 | { id: 'banana', label: 'Banana' }, 15 | { id: 'carrot', label: 'Carrot' }, 16 | ]; 17 | 18 | addViews(nested_slides); 19 | 20 | var NestedView = TabbedView.extend({ 21 | tabOptions: { 22 | scope: 'tabbed-views/anatomy', 23 | tabs: nested_slides 24 | } 25 | }); 26 | 27 | 28 | var AnatomyView = getView('anatomy'); 29 | 30 | AnatomyView = AnatomyView.extend({ 31 | regions: { 32 | 'nested_region': '.nested-region' 33 | }, 34 | onRender: function() { 35 | var nested_view = new NestedView(); 36 | this.nested_region.show(nested_view); 37 | } 38 | }); 39 | 40 | 41 | var GoalsView = getView('goals').extend({ 42 | events: { 43 | 'click .dynamically': 'toggleTab' 44 | }, 45 | toggleTab: function() { 46 | main_channel.command('toggle:tab', 'routing'); 47 | } 48 | }); 49 | 50 | var slides = [ 51 | { id: 'goals', label: 'Goals', view: GoalsView }, 52 | { id: 'anatomy', label: 'Anatomy', view: AnatomyView }, 53 | ]; 54 | 55 | addViews(slides); 56 | 57 | var TabsView = TabbedView.extend({ 58 | tabOptions: { 59 | scope: 'tabbed-views', 60 | tabs: slides 61 | }, 62 | initialize: function() { 63 | TabbedView.prototype.initialize.apply(this, arguments); 64 | channel.comply('toggle:tab', this.toggleTab, this) 65 | }, 66 | toggleTab: function() { 67 | var tab = this.tabs.collection.findWhere({'id':'examples'}) 68 | var visible = tab.get('visible'); 69 | tab.set('visible', ! visible); 70 | } 71 | 72 | }); 73 | 74 | return TabsView; 75 | 76 | }); 77 | -------------------------------------------------------------------------------- /public/javascripts/template/slides.html: -------------------------------------------------------------------------------- 1 | 10 | 11 | 21 | 22 | 33 | 34 | 41 | 42 | 43 | 65 | 83 | 102 | 103 | 104 | 113 | 114 | 149 | 150 | 177 | 178 | 182 | 183 | 193 | 203 | 204 | 219 | 220 | 230 | 231 | 234 | 235 | 244 | 245 | 257 | 283 | 284 | 306 | 307 | 314 | 315 | 322 | 323 | 332 | 333 | 341 | 342 | 343 | 357 | 358 | 367 | 368 | 369 | 372 | 373 | 376 | 377 | 378 | -------------------------------------------------------------------------------- /public/javascripts/view/slide.js: -------------------------------------------------------------------------------- 1 | define(function(require) { 2 | 3 | var Marionette = require('marionette'); 4 | 5 | var show_notes = true; 6 | 7 | /* helper view class for demo presentation */ 8 | var SlideView = Marionette.LayoutView.extend({ 9 | className: 'slide', 10 | onShow: function() { 11 | this.logNotes(); 12 | }, 13 | logNotes: function() { 14 | if (! show_notes) return; 15 | var notes = this.$('.notes'); 16 | if (! notes.length) return; 17 | 18 | notes = notes.text().trim().split('\n'); 19 | 20 | console.log(''); 21 | console.log(''); 22 | console.log('—————————————————————————————————————'); 23 | console.log(this.$('h1,h2').first().text()); 24 | console.log(''); 25 | _.each(notes, function(note) { 26 | console.log(note.trim()); 27 | }); 28 | } 29 | }); 30 | 31 | return SlideView; 32 | 33 | }); 34 | -------------------------------------------------------------------------------- /public/javascripts/view/tabbed.js: -------------------------------------------------------------------------------- 1 | define(function(require) { 2 | 3 | var Marionette = require('marionette'); 4 | var Tabs = require('lib/behavior.tabs'); 5 | require('lib/addPrefixedClass'); 6 | 7 | 8 | var tabbed_views = []; 9 | 10 | /* keyboard helper method for demo presentation */ 11 | /* enables left/right arrow keys to step through slides */ 12 | function handleKeyPress(e) { 13 | var tabbed_view = _.last(tabbed_views); 14 | if (! tabbed_view) return; 15 | 16 | var current_tab = tabbed_view.tabs.model.get('current_tab_id'); 17 | 18 | switch(e.which) { 19 | case 37: 20 | var first_tab = tabbed_view.tabs.collection.first().get('id'); 21 | if (first_tab === current_tab) { 22 | if (tabbed_views.length > 1) { 23 | tabbed_views.pop(); 24 | handleKeyPress(e); 25 | } 26 | } else { 27 | tabbed_view.triggerMethod('previous'); 28 | } 29 | break; 30 | case 39: 31 | var last_tab = tabbed_view.tabs.collection.last().get('id'); 32 | if (last_tab === current_tab) { 33 | if (tabbed_views.length > 1) { 34 | tabbed_views.pop(); 35 | handleKeyPress(e); 36 | } 37 | } else { 38 | tabbed_view.triggerMethod('next'); 39 | } 40 | break; 41 | default: 42 | // console.log(e.which); 43 | } 44 | } 45 | 46 | $(window).on('keyup', handleKeyPress); 47 | 48 | 49 | /* helper view class for demo presentation */ 50 | var TabbedView = Marionette.LayoutView.extend({ 51 | className: 'tabbed', 52 | template: _.template('
'), 53 | regions: { 54 | nav: '.tab-nav', 55 | content: '.tab-content', 56 | }, 57 | initialize: function() { 58 | tabbed_views.push(this); 59 | this.listenTo(this.tabs.model, 'change:current_tab_id', this.onChangeTab); 60 | }, 61 | onBeforeDestroy: function() { 62 | tabbed_views = _.filter(tabbed_views, function(view) { 63 | return (view.cid !== this.cid); 64 | }, this); 65 | }, 66 | onChangeTab: function(model, tab) { 67 | $('body').addPrefixedClass('current-tab', tab); 68 | }, 69 | behaviors: function() { 70 | var tab_options = _.extend({ 71 | behaviorClass: Tabs, 72 | region: this.content, 73 | tabs: [], 74 | nav: { 75 | region: this.nav 76 | } 77 | }, _.result(this, 'tabOptions')); 78 | 79 | return { 80 | Tabs: tab_options 81 | }; 82 | } 83 | }); 84 | 85 | return TabbedView; 86 | 87 | }); 88 | -------------------------------------------------------------------------------- /public/stylesheets/style.styl: -------------------------------------------------------------------------------- 1 | body 2 | font-family: Georgia, Serif 3 | font-size: 20px 4 | padding: 20px 5 | line-height: 1.3 6 | background-color: #dde 7 | color: #000 8 | 9 | a, span.link 10 | cursor: pointer 11 | color: #33c 12 | text-decoration: none 13 | 14 | span.link 15 | -webkit-user-select: none 16 | user-select: none 17 | 18 | 19 | h1, h2, h3, h4 20 | font-family: Helvetica, Sans-Serif 21 | margin: 0 22 | margin-bottom: 20px 23 | h1 24 | font-size: 200% 25 | h2 26 | font-size: 180% 27 | 28 | 29 | p 30 | max-width: 600px 31 | 32 | ul li 33 | margin-bottom: 0.5em 34 | font-size: 1.8em 35 | li 36 | margin-top: 0.5em 37 | font-size: 0.8em 38 | 39 | 40 | .buttonbar 41 | display: flex 42 | margin-left: 20px 43 | 44 | .buttonbar .button 45 | font-family: Helvetica, Sans-Serif 46 | background-color: #eee 47 | min-width: 30px 48 | text-align: center 49 | padding: 8px 15px 50 | margin-right: 5px 51 | box-sizing: border-box 52 | cursor: pointer 53 | border-radius: 5px 54 | border-bottom-left-radius: 0 55 | border-bottom-right-radius: 0 56 | font-size: 14px 57 | line-height: 24px 58 | -webkit-user-select: none 59 | user-select: none 60 | border: 3px solid rgba(0,0,0,0.1) 61 | border-bottom: none 62 | 63 | .buttonbar .button.active 64 | background-color: #fff 65 | cursor: default 66 | font-weight: bold 67 | font-size: 20px 68 | padding: 10px 15px 69 | margin-top: -2px 70 | margin-bottom: -3px 71 | border-color: rgba(0,0,0,0.2) 72 | 73 | 74 | .tab 75 | background-color: #fff 76 | padding: 30px 77 | border-radius: 5px 78 | border: 3px solid rgba(0,0,0,0.2) 79 | 80 | .tab .tab 81 | border-color: rgba(0,0,0,0.2) 82 | background-color: #ffe 83 | min-height: 500px 84 | .tab .buttonbar .button 85 | border-color: rgba(0,0,0,0.1) 86 | .tab .buttonbar .button.active 87 | background-color: #ffe 88 | border-color: rgba(0,0,0,0.2) 89 | 90 | .tab .tab .tab 91 | padding: 30px 92 | min-height: 0px 93 | font-size: 18px 94 | 95 | .tab .tab .tab 96 | background-color: #fff 97 | min-height: 400px 98 | .tab .tab .buttonbar .button.active 99 | background-color: #fff 100 | 101 | 102 | 103 | 104 | .tab .notes 105 | display: none 106 | 107 | .tab-intro 108 | justify-content: center 109 | align-items: center 110 | height: 600px 111 | overflow: hidden 112 | 113 | .tab-intro > div 114 | font-size: 36px 115 | background-position: left top 116 | background-repeat: no-repeat 117 | background-image: url(/images/tab-cola.png) 118 | padding-left: 320px 119 | padding-top: 50px 120 | height: 400px 121 | margin-left: -80px 122 | 123 | 124 | .tab-intro p 125 | max-width: none 126 | margin: 10px 0 127 | color: #666 128 | 129 | .tab-intro p.instructions 130 | font-size: 24px 131 | margin-top: 40px 132 | line-height: 1.8 133 | 134 | 135 | .tab-aboutme 136 | align-items: center 137 | padding: 30px !important 138 | 139 | .tab-aboutme img 140 | width: 100% 141 | height: auto 142 | max-width: 1100px 143 | border: 1px solid rgba(0,0,0,0.3) 144 | border-radius: 3px 145 | 146 | 147 | .slide 148 | display: flex 149 | flex-direction: column 150 | justify-content: start 151 | align-content: space-between 152 | padding: 35px 50px 50px 153 | 154 | .tab-anatomy 155 | padding-bottom: 30px 156 | 157 | .tab-anatomy .nested-region 158 | margin-left: -15px 159 | margin-top: -50px 160 | 161 | .tab-anatomy .tab 162 | min-height: 400px 163 | padding-right: 270px !important 164 | background-position: top right 165 | background-repeat: no-repeat 166 | 167 | .tab-anatomy .tab-apple 168 | background-image: url(/images/apple.png) 169 | .tab-anatomy .tab-banana 170 | background-image: url(/images/banana.png) 171 | .tab-anatomy .tab-carrot 172 | background-position: top right 173 | background-image: url(/images/carrot.png) 174 | 175 | .tab-anatomy .buttonbar 176 | padding: 10px 177 | margin: 0 178 | margin-left: 560px 179 | margin-bottom: -20px 180 | width: 270px 181 | position: relative 182 | z-index: 1 183 | 184 | .tab-anatomy .buttonbar:hover 185 | box-shadow: 0 0 0 4px red; 186 | 187 | .tab-anatomy .tab-content 188 | width: 860px 189 | padding: 10px 190 | position: relative 191 | 192 | .tab-anatomy .tab-content:hover 193 | box-shadow: 0 0 0 4px red; 194 | 195 | code, .code 196 | font-family: Andale Mono, monospace 197 | 198 | code 199 | background-color: #fee; 200 | padding: 0 5px 201 | margin: 0 5px 202 | 203 | pre 204 | tab-size: 4 205 | font-size: 120% 206 | font-family: Andale Mono, monospace 207 | 208 | .tab-carrot pre 209 | tab-size: 2 210 | margin-left: 15px 211 | margin-top: -20px 212 | 213 | .tab-tabbed-views .tab-code pre 214 | font-size: 100% 215 | 216 | .tab-needs li.scope 217 | cursor: pointer 218 | 219 | .tab-scopes ul 220 | margin: 0 221 | 222 | .tab-scopes li 223 | cursor: pointer 224 | margin: 0 225 | padding: 20px 0 226 | 227 | .tab-scopes .url 228 | font-size: 36px 229 | line-height: 36px 230 | white-space: nowrap 231 | display: none 232 | margin: 30px 0 233 | 234 | .tab-scopes .highlight 235 | .scope 236 | position: relative 237 | background-color: rgba(255,204,204,0.5) 238 | box-shadow: 0 0 0 2px rgba(255,0,0,0.5) 239 | .param 240 | position: relative 241 | background-color: rgba(204,204,255,0.5) 242 | box-shadow: 0 0 0 2px rgba(0,0,255,0.5) 243 | 244 | .tab-scopes .url 245 | .scope:before 246 | content: 'scope' 247 | position: absolute 248 | top: -36px 249 | left: 2px 250 | font-size: 22px 251 | .param:before 252 | content: 'parameter' 253 | position: absolute 254 | top: -36px 255 | left: 2px 256 | font-size: 22px 257 | .ignored 258 | opacity: 0.3 259 | 260 | .tab-scopes .url.scope0 261 | display: block 262 | .scope-1 263 | .tab-scopes .url.scope0 264 | display: none 265 | .tab-scopes .url.scope1 266 | display: block 267 | .scope-2 268 | .tab-scopes .url.scope0 269 | display: none 270 | .tab-scopes .url.scope2 271 | display: block 272 | .scope-3 273 | .tab-scopes .url.scope0 274 | display: none 275 | .tab-scopes .url.scope3 276 | display: block 277 | 278 | .tabbed.scope-1 279 | padding: 10px 280 | margin: -10px 281 | box-shadow: 0 0 0px 10px rgba(255,0,0,0.5) 282 | 283 | .tabbed.scope-2 .tabbed.tab-routing 284 | box-shadow: inset 0 0 0px 10px rgba(255,0,0,0.5) 285 | 286 | .tabbed.scope-3 .tab-solution .tabbed 287 | padding: 10px 288 | margin: -10px 289 | box-shadow: 0 0 0px 10px rgba(255,0,0,0.5) 290 | 291 | .tab-code pre 292 | background-color: #fff 293 | padding: 20px 294 | margin: -20px 295 | margin-top: -10px 296 | border: 1px solid #eee 297 | 298 | 299 | .code-annotations 300 | 301 | pre .note 302 | box-shadow: inset 0 0 0 2px rgba(255,0,0,0.05) 303 | cursor: crosshair 304 | pre .note.active 305 | background-color: #fee 306 | pre .note .note 307 | box-shadow: none 308 | pre .note:hover .note 309 | box-shadow: inset 0 0 0 2px rgba(255,0,0,0.05) 310 | 311 | 312 | pre .note-defineviews 313 | width: 590px 314 | pre .note-regions 315 | width: 500px 316 | pre .note-behaviors 317 | width: 890px 318 | pre .note-automaticforthetabs 319 | width: 680px 320 | padding: 10px 321 | margin: -10px 322 | pre .note-handlers 323 | width: 520px 324 | 325 | .sidenotes 326 | position: relative 327 | 328 | .sidenotes .content 329 | position: absolute 330 | left: 700px 331 | top: 0px 332 | right: 0px 333 | font-size: 24px 334 | 335 | .note 336 | display: none 337 | position: absolute 338 | left: 0 339 | right: 0 340 | padding: 10px 341 | padding-left: 15px 342 | border: 2px solid #fcc 343 | border-radius: 5px 344 | .note:before 345 | display: block 346 | content: "" 347 | position: absolute 348 | right: 100% 349 | top: 20px 350 | width: 50px 351 | border: 1px solid #fcc 352 | 353 | .note-importtabsbehavior 354 | top: 10px 355 | .note-importtabsbehavior:before 356 | width: 132px 357 | .note-defineviews 358 | top: 90px 359 | .note-defineviews:before 360 | width: 130px 361 | .note-tabbedview 362 | top: 180px 363 | .note-tabbedview:before 364 | width: 65px 365 | .note-regions 366 | top: 220px 367 | .note-regions:before 368 | top: 50px 369 | width: 200px 370 | .note-behaviors 371 | top: 350px 372 | left: 240px 373 | .note-tabregion 374 | top: 430px 375 | left: 240px 376 | .note-navoptions 377 | top: 460px 378 | left: 240px 379 | .note-tabslist 380 | top: 560px 381 | left: 240px 382 | .note-tabscope 383 | top: 620px 384 | left: 240px 385 | .note-tabscope:before 386 | top: 80px 387 | 388 | .note-automaticforthetabs 389 | display: block 390 | left: -20px 391 | top: 20px 392 | .note-automaticforthetabs:before 393 | display: none 394 | .note-scoperouter 395 | top: 12px 396 | .note-scope 397 | top: 170px 398 | left: -120px 399 | .note-scope:before 400 | top: 30px 401 | width: 163px 402 | .note-foo 403 | top: 226px 404 | left: -120px 405 | .note-foo:before 406 | top: 30px 407 | width: 176px 408 | .note-bar 409 | top: 255px 410 | left: -120px 411 | .note-bar:before 412 | width: 36px 413 | top: 30px 414 | .note-handlers 415 | top: 400px 416 | left: -120px 417 | .note-handlers:before 418 | width: 60px 419 | 420 | 421 | 422 | .tab-caution 423 | background-position: top right 424 | background-repeat: no-repeat 425 | background-image: url(/images/caution.png) 426 | padding-right: 700px 427 | 428 | .tab-tryit 429 | pre 430 | line-height: 40px 431 | background-color: #fff 432 | padding: 15px 20px 433 | margin: -20px 434 | margin-top: 10px 435 | border: 1px solid rgba(0,0,0,0.1) 436 | p 437 | font-size: 36px 438 | 439 | -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | 4 | /* Serve index.html for all GET requests for any URL */ 5 | router.get('*', function(req, res) { 6 | res.render('index', { title: 'Tabbed Views and Scoped Routing' }); 7 | }); 8 | 9 | module.exports = router; 10 | -------------------------------------------------------------------------------- /views/error.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1= message 5 | h2= error.status 6 | pre #{error.stack} 7 | -------------------------------------------------------------------------------- /views/index.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | .main-region 5 | 6 | script(data-main="/javascripts/config", src="/javascripts/vendor/require.js") -------------------------------------------------------------------------------- /views/layout.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | title= title 5 | meta(charset='utf-8') 6 | meta(http-equiv='X-UA-Compatible', content='IE=edge') 7 | meta(name='viewport', content='width=device-width, initial-scale=1') 8 | link(rel='stylesheet', href='/stylesheets/style.css') 9 | body 10 | block content --------------------------------------------------------------------------------