Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.
Unless otherwise indicated, these slides are © 2013-2014 Pivotal Software, Inc. and licensed under a
Creative Commons Attr...
Unless otherwise indicated, these slides are © 2013-2014 Pivotal Software, Inc. and licensed under a
Creative Commons Attr...
Unless otherwise indicated, these slides are © 2013-2014 Pivotal Software, Inc. and licensed under a
Creative Commons Attr...
Unless otherwise indicated, these slides are © 2013-2014 Pivotal Software, Inc. and licensed under a
Creative Commons Attr...
Unless otherwise indicated, these slides are © 2013-2014 Pivotal Software, Inc. and licensed under a
Creative Commons Attr...
Unless otherwise indicated, these slides are © 2013-2014 Pivotal Software, Inc. and licensed under a
Creative Commons Attr...
Unless otherwise indicated, these slides are © 2013-2014 Pivotal Software, Inc. and licensed under a
Creative Commons Attr...
Unless otherwise indicated, these slides are © 2013-2014 Pivotal Software, Inc. and licensed under a
Creative Commons Attr...
Unless otherwise indicated, these slides are © 2013-2014 Pivotal Software, Inc. and licensed under a
Creative Commons Attr...
Unless otherwise indicated, these slides are © 2013-2014 Pivotal Software, Inc. and licensed under a
Creative Commons Attr...
Unless otherwise indicated, these slides are © 2013-2014 Pivotal Software, Inc. and licensed under a
Creative Commons Attr...
Unless otherwise indicated, these slides are © 2013-2014 Pivotal Software, Inc. and licensed under a
Creative Commons Attr...
Unless otherwise indicated, these slides are © 2013-2014 Pivotal Software, Inc. and licensed under a
Creative Commons Attr...
Unless otherwise indicated, these slides are © 2013-2014 Pivotal Software, Inc. and licensed under a
Creative Commons Attr...
Unless otherwise indicated, these slides are © 2013-2014 Pivotal Software, Inc. and licensed under a
Creative Commons Attr...
Unless otherwise indicated, these slides are © 2013-2014 Pivotal Software, Inc. and licensed under a
Creative Commons Attr...
Unless otherwise indicated, these slides are © 2013-2014 Pivotal Software, Inc. and licensed under a
Creative Commons Attr...
Unless otherwise indicated, these slides are © 2013-2014 Pivotal Software, Inc. and licensed under a
Creative Commons Attr...
Unless otherwise indicated, these slides are © 2013-2014 Pivotal Software, Inc. and licensed under a
Creative Commons Attr...
Unless otherwise indicated, these slides are © 2013-2014 Pivotal Software, Inc. and licensed under a
Creative Commons Attr...
Unless otherwise indicated, these slides are © 2013-2014 Pivotal Software, Inc. and licensed under a
Creative Commons Attr...
Unless otherwise indicated, these slides are © 2013-2014 Pivotal Software, Inc. and licensed under a
Creative Commons Attr...
Unless otherwise indicated, these slides are © 2013-2014 Pivotal Software, Inc. and licensed under a
Creative Commons Attr...
Demo
Unless otherwise indicated, these slides are
© 2013-2014 Pivotal Software, Inc. and licensed under a
Creative Commons...
Unless otherwise indicated, these slides are © 2013-2014 Pivotal Software, Inc. and licensed under a
Creative Commons Attr...
Unless otherwise indicated, these slides are © 2013-2014 Pivotal Software, Inc. and licensed under a
Creative Commons Attr...
Upcoming SlideShare
Loading in …5
×

Introducing RaveJS: Zero-config JavaScript applications

3,976 views

Published on

Introducing RaveJS. Rave eliminates configuration, machinery, and complexity. Stop configuring and tweaking file watchers, minifiers, and transpilers just to get to a runnable app. Instead, go from zero to "hello world" in 30 seconds. In the next 30 seconds, easily add capabilities and frameworks to your application simply by installing *Rave Extensions* and *Rave Starter* packages from npm and Bower, the leading JavaScript package managers. Finally, install additional *Rave Extension* packages to apply your favorite build, deploy, and testing patterns.

https://github.com/RaveJS

Published in: Technology

Introducing RaveJS: Zero-config JavaScript applications

  1. 1. Unless otherwise indicated, these slides are © 2013-2014 Pivotal Software, Inc. and licensed under a Creative Commons Attribution-NonCommercial license: http://creativecommons.org/licenses/by-nc/3.0/ RaveJS: Zero-config app development John Hann, JavaScript Barbarian, Pivotal @unscriptable
  2. 2. Unless otherwise indicated, these slides are © 2013-2014 Pivotal Software, Inc. and licensed under a Creative Commons Attribution-NonCommercial license: http://creativecommons.org/licenses/by-nc/3.0/ I work for Pivotal's Frameworks and Runtimes group 2
  3. 3. Unless otherwise indicated, these slides are © 2013-2014 Pivotal Software, Inc. and licensed under a Creative Commons Attribution-NonCommercial license: http://creativecommons.org/licenses/by-nc/3.0/ I work on the cujoJS Toolkit - cujojs.com 3
  4. 4. Unless otherwise indicated, these slides are © 2013-2014 Pivotal Software, Inc. and licensed under a Creative Commons Attribution-NonCommercial license: http://creativecommons.org/licenses/by-nc/3.0/ 4 JavaScript is awesome Architecture Language Tooling • Package managers • Minifiers / optimizers • Bundlers / builders • Pre-processors • CI • SASS/SCSS, LESS, Stylus --> CSS • Dart --> Javascript • ES6 --> ES5 • Transpile all the things! • SPA, AOP, DI, IOC • MV-WTF • Modules, components • Linters, unit testers, integration testers
  5. 5. Unless otherwise indicated, these slides are © 2013-2014 Pivotal Software, Inc. and licensed under a Creative Commons Attribution-NonCommercial license: http://creativecommons.org/licenses/by-nc/3.0/ More is more 5  More sophistication  More complexity  More machinery  More configuration  More maintenance
  6. 6. Unless otherwise indicated, these slides are © 2013-2014 Pivotal Software, Inc. and licensed under a Creative Commons Attribution-NonCommercial license: http://creativecommons.org/licenses/by-nc/3.0/ “JavaScript needs a build step.” WTF? 6
  7. 7. Unless otherwise indicated, these slides are © 2013-2014 Pivotal Software, Inc. and licensed under a Creative Commons Attribution-NonCommercial license: http://creativecommons.org/licenses/by-nc/3.0/ What happened to the good ol' days? <!doctype html> <html lang="en"> <head> <script src="easy.js"></script> <link href="easy.css" type="stylesheet"/> </head> <body> <div class="container">click me</div> </body> </html> 7
  8. 8. Unless otherwise indicated, these slides are © 2013-2014 Pivotal Software, Inc. and licensed under a Creative Commons Attribution-NonCommercial license: http://creativecommons.org/licenses/by-nc/3.0/ Problem: HTML forces crappy JavaScript  You have to choose • Simple HTML or • Architecturally sound code and best practices  Doing it the "right way" requires • Too much boilerplate, configuration, and setup  Too much work to create • Apps • Prototypes and experiments • Demos and tutorials 8
  9. 9. Unless otherwise indicated, these slides are © 2013-2014 Pivotal Software, Inc. and licensed under a Creative Commons Attribution-NonCommercial license: http://creativecommons.org/licenses/by-nc/3.0/ Example: recent project ~400 LOC in Gruntfile.js ~70 LOC in RequireJS main.js >100 LOC in karma configs 9
  10. 10. Unless otherwise indicated, these slides are © 2013-2014 Pivotal Software, Inc. and licensed under a Creative Commons Attribution-NonCommercial license: http://creativecommons.org/licenses/by-nc/3.0/ 'use strict'; module.exports = function (grunt) {   // Load grunt tasks automatically   require('load-grunt-tasks') (grunt);   // Time how long tasks take. Can help when optimizing build times   require('time-grunt') (grunt);   // Define the configuration for all the tasks   grunt.initConfig({     // Project settings     xd: {       app: 'app',       dist: 'dist'     },     // Set bower task's targetDir to use app directory     bower: {       options: {         targetDir: '<%= xd.app %>/lib'       },       // Provide install target       install: {}     },     // Watches files for changes and runs tasks based on the changed files     watch: {       files: ['<%= xd.app %>/ **/*', '*.js', '.jshintrc'],       tasks: ['build'],       livereload: {         options: {           livereload: '<%= connect.options.livereload %>'         },         files: ['<%= xd.app %>/**/*', '*.js', '.jshintrc']       }     },     protractor: {       options: {         //configFile: "test/ protractor.conf.js", // Default config file         keepAlive: true, // If false, the grunt process stops when the test fails.         noColor: false, // If true, protractor will not use colors in its output.         args: {           specs: [             './test/e2e/**/ *.spec.js'           ],           baseUrl: 'http:// localhost:8000',           chromeDriver: 'node_modules/protractor/ selenium/chromedriver'         }       },       run: {       }     },     // The actual grunt server settings     connect: {       options: {         port: 8000,         // Set to '0.0.0.0' to access the server from outside.         hostname: '0.0.0.0',         livereload: 35729       },       livereload: {         options: {           open: true,           base: [             '.tmp',             '<%= xd.app %>'           ],           middleware: function (connect, options) {             if (! Array.isArray(options.base)) {               options.base = [options.base];             }             var middlewares = [require('grunt-connect- proxy/lib/ utils').proxyRequest];             options.base.forE ach(function (base) {               grunt.log.warn( base);               middlewares.pus h(connect.static(base));             });             return middlewares;           }         }       },       test: {         options: {           port: 9001,           base: [             '.tmp',             'test',             '<%= xd.app %>'           ]         }       },       dist: {         options: {           base: '<%= xd.dist %>'         }       },       proxies: [         {           context: ['/batch', '/job', '/modules', '/ streams'],           host: 'localhost',           port: 9393,           changeOrigin: true         }       ]     },     // Make sure code styles are up to par and there are no obvious mistakes     jshint: {       options: {         jshintrc: '.jshintrc',         reporter: require('jshint-stylish')       },       all: [         'Gruntfile.js',         '<%= xd.app %>/ scripts/{,**/}*.js'       ],       test: {         options: {           jshintrc: 'test/.jshintrc'         },         src: ['test/spec/ {,*/}*.js']       }     },     less: {       dist: {         files: {           '<%= xd.app %>/ styles/main.css': ['<%= xd.app %>/styles/main.less']         },         options: {           sourceMap: true,           sourceMapFilename: '<%= xd.app %>/styles/ main.css.map',           sourceMapBasepath: '<%= xd.app %>/',           sourceMapRootpath: '/'         }       }     },     // Empties folders to start fresh     clean: {       dist: {         files: [           {             dot: true,             src: [               '.tmp',               '<%= xd.dist %>/*'             ]           }         ]       },       server: '.tmp'     },     // Add vendor prefixed styles     autoprefixer: {       options: {         browsers: ['last 1 version']       },       dist: {         files: [           {             expand: true,             cwd: '.tmp/ styles/',             src: '{,*/} *.css',             dest: '.tmp/ styles/'           }         ]       }     }, // imagemin: { // dist: { // files: [ // { // expand: true, // cwd: '<%= xd.app %>/images', // src: '{,*/}*. {png,jpg,jpeg,gif}', // dest: '<%= xd.dist %>/images' // } // ] // } // },     // Renames files for browser caching purposes     rev: {       dist: {         files: {           src: [             // TODO: commenting out js files for now.             // '<%= xd.dist %>/scripts/{,*/}*.js',             '<%= xd.dist %>/ styles/{,*/}*.css',             '<%= xd.dist %>/ images/{,*/}*. {png,jpg,jpeg,gif}',             '<%= xd.dist %>/ fonts/*'           ]         }       }     },     // Reads HTML for usemin blocks to enable smart builds that automatically     // concat, minify and revision files. Creates configurations in memory so     // additional tasks can operate on them     useminPrepare: {       html: '<%= xd.app %>/ index.html',       options: {         dest: '<%= xd.dist %>'       }     },     // Performs rewrites based on rev and the useminPrepare configuration     usemin: {       html: ['<%= xd.dist %>/ {,*/}*.html'],       css: ['<%= xd.dist %>/ styles/{,*/}*.css'],       options: {         assetsDirs: ['<%= xd.dist %>', '<%= xd.dist %>/ images']       }     },     htmlmin: {       dist: {         options: {           collapseWhitespace: true,           collapseBooleanAttr ibutes: true,           removeCommentsFromC DATA: true,           removeOptionalTags: true         },         files: [           {             expand: true,             cwd: '<%= xd.dist %>',             src: ['*.html', 'views/{,*/}*.html'],             dest: '<%= xd.dist %>'           }         ]       }     },     // Allow the use of non- minsafe AngularJS files. Automatically makes it     // minsafe compatible so Uglify does not destroy the ng references // ngmin: { // dist: { // files: [ // { // expand: true, // cwd: '.tmp/ concat/js', // src: '*.js', // dest: '.tmp/ concat/js' // } // ] // } // },     // Copies remaining files to places other tasks can use     copy: {       dist: {         files: [           {             expand: true,             dot: true,             cwd: '<%= xd.app %>',             dest: '<%= xd.dist %>',             src: [               '*. {ico,png,txt}',               '*.html',               'views/{,*/} *.html',               'lib/**/*',               'scripts/**/*',               'fonts/*',               'images/*'             ]           }         ]       },       styles: {         expand: true,         cwd: '<%= xd.app %>/ styles',         dest: '.tmp/styles/',         src: '{,*/}*.css'       },       testfiles: {         files: [           { src: 'test/ people.txt', dest: '/tmp/xd- tests/people.txt' }         ]       }     },     // Run some tasks in parallel to speed up the build process     concurrent: {       server: [         'copy:styles'       ],       test: [         'copy:styles'       ],       dist: [         // TODO: copy:styles copies .css files into .tmp         // TODO: hence probably not to include copy:styles in here.         // 'copy:styles' 10 Gruntfile.js
  11. 11. Unless otherwise indicated, these slides are © 2013-2014 Pivotal Software, Inc. and licensed under a Creative Commons Attribution-NonCommercial license: http://creativecommons.org/licenses/by-nc/3.0/ require.config({   paths: {     domReady: '../lib/requirejs- domready/domReady',     angular: '../lib/angular/angular',     jquery: '../lib/jquery/jquery',     bootstrap: '../lib/bootstrap/ bootstrap',     ngResource: '../lib/angular- resource/angular-resource',     uiRouter: '../lib/angular-ui-router/ angular-ui-router',     cgBusy: '../lib/angular-busy/ angular-busy',     ngGrowl: '../lib/angular-growl/ angular-growl',     angularHighlightjs: '../lib/angular- highlightjs/angular-highlightjs',     highlightjs: '../lib/highlightjs/ highlight.pack'   },   shim: {     angular: {       deps: ['bootstrap'],       exports: 'angular'     },     bootstrap: {       deps: ['jquery']     },     'uiRouter': {       deps: ['angular']     },     'ngResource': {       deps: ['angular']     },     'cgBusy': {       deps: ['angular']     },     'ngGrowl': {       deps: ['angular']     },     'angularHighlightjs': {       deps: ['angular', 'highlightjs']     }   } }); define([   'require',   'angular',   'app',   './routes' ], function (require, angular) {   'use strict';   require(['domReady!'], function (document) {     console.log('Start angular application.');     angular.bootstrap(document, ['xdAdmin']);   });   require(['jquery', 'bootstrap'], function () {     console.log('Loaded Twitter Bootstrap.');     updateGrowl();     $(window).on('scroll resize', function () {       updateGrowl();     });   });   function updateGrowl() {     var bodyScrollTop = $ ('body').scrollTop();     var navHeight = $ ('nav').outerHeight();     if (bodyScrollTop > navHeight) {       $('.growl').css('top', 10);     } else if (bodyScrollTop >= 0) {       var distance = navHeight - bodyScrollTop;       $('.growl').css('top', distance + 10);     }   } }); 11 RequireJS main.js
  12. 12. Unless otherwise indicated, these slides are © 2013-2014 Pivotal Software, Inc. and licensed under a Creative Commons Attribution-NonCommercial license: http://creativecommons.org/licenses/by-nc/3.0/ module.exports = function(config) {   config.set({     // base path, that will be used to resolve files and exclude     basePath: '',     // testing framework to use (jasmine/mocha/qunit/...)     frameworks: ['ng-scenario'],     // list of files / patterns to load in the browser     files: [       'test/e2e/*.js',       'test/e2e/**/*.js'     ],     // list of files / patterns to exclude     exclude: [],     // web server port     port: 7070,     // level of logging     // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG     logLevel: config.LOG_INFO,     // enable / disable watching file and executing tests whenever any file changes     autoWatch: false,     // Start these browsers, currently available:     // - Chrome     // - ChromeCanary     // - Firefox     // - Opera     // - Safari (only Mac)     // - PhantomJS     // - IE (only Windows)     browsers: ['PhantomJS'],     // Continuous Integration mode     // if true, it capture browsers, run tests and exit     singleRun: true,     // Uncomment the following lines if you are using grunt's server to run the tests     proxies: {         '/': 'http://localhost: 8000/'     },     // // URL root prevent conflicts with the site root     urlRoot: '/_karma_/'   }); }; module.exports = function (config) {   'use strict';   config.set({     // base path, that will be used to resolve files and exclude     basePath: '',     // testing framework to use (jasmine/mocha/qunit/...)     frameworks: ['jasmine'],     // list of files / patterns to load in the browser     files: [       'app/lib/angular/ angular.js',       'app/lib/angular-mocks/ angular-mocks.js',       'app/lib/angular-resource/ angular-resource.js',       'app/lib/angular-cookies/ angular-cookies.js',       'app/lib/angular-sanitize/ angular-sanitize.js',       'app/lib/angular-route/ angular-route.js',       'app/lib/angular-ui-router/ angular-ui-router.js',       'app/lib/angular-growl/ angular-growl.js',       'app/lib/angular-promise- tracker/promise-tracker.js',       'app/lib/angular-busy/ angular-busy.js',       'app/scripts/*.js',       'app/scripts/**/*.js',       'test/spec/**/*.js',       'test/test-main.js'     ],     // list of files / patterns to exclude     exclude: [],     // web server port     port: 7070,     // level of logging     // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG     logLevel: config.LOG_INFO,     // enable / disable watching file and executing tests whenever any file changes     autoWatch: true,     // Start these browsers, currently available:     // - Chrome     // - ChromeCanary     // - Firefox     // - Opera     // - Safari (only Mac)     // - PhantomJS     // - IE (only Windows)     browsers: ['PhantomJS'],     // Continuous Integration mode     // if true, it capture browsers, run tests and exit     singleRun: false   }); 12 karma.conf.js
  13. 13. Unless otherwise indicated, these slides are © 2013-2014 Pivotal Software, Inc. and licensed under a Creative Commons Attribution-NonCommercial license: http://creativecommons.org/licenses/by-nc/3.0/ Can we simplify this mess? We must 13
  14. 14. Unless otherwise indicated, these slides are © 2013-2014 Pivotal Software, Inc. and licensed under a Creative Commons Attribution-NonCommercial license: http://creativecommons.org/licenses/by-nc/3.0/ RaveJS 14 ┏( ˆ◡ˆ)┛┗(ˆ◡ˆ )┓ https://github.com/RaveJS
  15. 15. Unless otherwise indicated, these slides are © 2013-2014 Pivotal Software, Inc. and licensed under a Creative Commons Attribution-NonCommercial license: http://creativecommons.org/licenses/by-nc/3.0/ NO NO NO NO NO  Rave JS is NOT a new framework • …but it integrates with most (if not all)  RaveJS is NOT another {AMD|script|ES6} loader • …but it is an ES6 loader extension with a built-in shim • Loads AMD, CommonJS, and (soon) ES6 • Loads other things via loader extensions  RaveJS is NOT ready for production, yet :( • …but it is ready to play with • Feedback and PRs welcome! 15
  16. 16. Unless otherwise indicated, these slides are © 2013-2014 Pivotal Software, Inc. and licensed under a Creative Commons Attribution-NonCommercial license: http://creativecommons.org/licenses/by-nc/3.0/ RaveJS's goals in 3 bullets  Provide a default, instantly-runnable configuration  Make it easy to become sophisticated  Make it easy to assert your opinion 16
  17. 17. Unless otherwise indicated, these slides are © 2013-2014 Pivotal Software, Inc. and licensed under a Creative Commons Attribution-NonCommercial license: http://creativecommons.org/licenses/by-nc/3.0/ Provide a default, instantly-runnable configuration  No machinery or configuration (just a static web server) 1. Download/install a Rave Starter (or start "from scratch") 2. Launch your favorite browser 3. Open your favorite editor or IDE  Run-time is responsive to environment 1.Write code 2.Reload 3.Repeat 17
  18. 18. Unless otherwise indicated, these slides are © 2013-2014 Pivotal Software, Inc. and licensed under a Creative Commons Attribution-NonCommercial license: http://creativecommons.org/licenses/by-nc/3.0/ Make it easy to become sophisticated  CLI one-liner to switch mode • Responsive / dev <--> Built / production • Easily switch back • Still zero configuration!  CLI one-liner to launch tests • Unit tests, integration tests, push to CI 18
  19. 19. Unless otherwise indicated, these slides are © 2013-2014 Pivotal Software, Inc. and licensed under a Creative Commons Attribution-NonCommercial license: http://creativecommons.org/licenses/by-nc/3.0/ Make it easy to assert your opinion  Add microlibs, frameworks, third-party integrations • e.g. Knockout-Backbone  Add capabilities: loader extensions, shims • e.g. JSON loader, WebComponents shim  Install build, deploy, and testing patterns (SPA is default) • Spring, JEE, Rails • Buster, Karma 19
  20. 20. Unless otherwise indicated, these slides are © 2013-2014 Pivotal Software, Inc. and licensed under a Creative Commons Attribution-NonCommercial license: http://creativecommons.org/licenses/by-nc/3.0/ How? Metadata 20
  21. 21. Unless otherwise indicated, these slides are © 2013-2014 Pivotal Software, Inc. and licensed under a Creative Commons Attribution-NonCommercial license: http://creativecommons.org/licenses/by-nc/3.0/ Metadata all the things!  Provide default, minimal metadata out of the box  Allow devs to generate metadata naturally • bower install --save • npm install --save  Allow third parties to provide metadata • Rave Integration Extensions • bower install --save awesome-third-party-integration-package • Rave Starters 21
  22. 22. Unless otherwise indicated, these slides are © 2013-2014 Pivotal Software, Inc. and licensed under a Creative Commons Attribution-NonCommercial license: http://creativecommons.org/licenses/by-nc/3.0/ Rave Extensions  Add microlibs, frameworks, third-party integrations • bower install --save rave-knockout-backbone  Add capabilities: loader extensions, shims • npm install --save rave-load-css • bower install --save rave-polymer  Install build, deploy, and test patterns (SPA is default) • bower install --save-dev rave-spring-boot • bower install --save-dev rave-buster 22
  23. 23. Unless otherwise indicated, these slides are © 2013-2014 Pivotal Software, Inc. and licensed under a Creative Commons Attribution-NonCommercial license: http://creativecommons.org/licenses/by-nc/3.0/ To CLI or not to CLI?  Rave CLI • rave install <package-on-bower-or-npm> • Finds best package for your app, invokes --save (biggest newb mistake) • rave unbuild • Uses grunt or gulp (or both!) automatically  Familiar, established CLIs • npm install --save <package> • npm test • gulp rave --unbuild • grunt rave --test 23
  24. 24. Demo Unless otherwise indicated, these slides are © 2013-2014 Pivotal Software, Inc. and licensed under a Creative Commons Attribution-NonCommercial license: http://creativecommons.org/licenses/by-nc/3.0/ Zero config!
  25. 25. Unless otherwise indicated, these slides are © 2013-2014 Pivotal Software, Inc. and licensed under a Creative Commons Attribution-NonCommercial license: http://creativecommons.org/licenses/by-nc/3.0/ 25 Rave is a Work in Progress ASAP Summer 2014 Fall 2014 • Testing patterns • Even more extensions and patterns • Showcase/directory of community Rave Extensions? • ES6 module syntax** • IE8+ compatibility** • AngularJS 1.3 extensions and patterns** • Spring, JEE patterns • Minification • Bower*, npm* • AMD*, node* • Text*, CSS*, JSON* • cujoJS extensions* • Default build & deploy patterns** *done! **in progress
  26. 26. Unless otherwise indicated, these slides are © 2013-2014 Pivotal Software, Inc. and licensed under a Creative Commons Attribution-NonCommercial license: http://creativecommons.org/licenses/by-nc/3.0/ Questions? John Hann, JavaScript Barbarian, Pivotal @unscriptable

×