commit b1562cd74c8b7332c82bbdeaea0dbd2df8e33705 Author: Simon Bailey Date: Tue Jan 7 20:00:49 2014 +0000 Basic project generator setup TODO: - App tests - Generator tests - Component generator Signed-off-by: Simon Bailey diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c308ed0 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +# http://editorconfig.org +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..176a458 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2d1ef45 --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +### SublimeText ### +*.sublime-workspace + +### OSX ### +.DS_Store +.AppleDouble +.LSOverride +Icon + +# Thumbnails +._* + +# Files that might appear on external disk +.Spotlight-V100 +.Trashes + +### Windows ### +# Windows image file caches +Thumbs.db +ehthumbs.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# App specific + +node_modules/ +temp/ diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..da64b6e --- /dev/null +++ b/.jshintrc @@ -0,0 +1,21 @@ +{ + "node": true, + "esnext": true, + "bitwise": true, + "camelcase": true, + "curly": true, + "eqeqeq": true, + "immed": true, + "indent": 2, + "latedef": true, + "newcap": true, + "noarg": true, + "quotmark": "single", + "regexp": true, + "undef": true, + "unused": true, + "strict": true, + "trailing": true, + "smarttabs": true, + "white": true +} diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..3f16ce8 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,8 @@ +language: node_js +node_js: + - '0.8' + - '0.10' +before_install: + - currentfolder=${PWD##*/} + - if [ "$currentfolder" != 'generator-react-webpack' ]; then cd .. && eval "mv $currentfolder generator-react-webpack" && cd generator-react-webpack; fi + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a5e8019 --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +Copyright 2014 Simon Bailey + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e83623c --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# generator-react-webpack [![Build Status](https://secure.travis-ci.org/newtriks/generator-react-webpack.png?branch=master)](https://travis-ci.org/newtriks/generator-react-webpack) + +A generator for [Yeoman](http://yeoman.io). + + +## Getting Started + +### What is Yeoman? + +Trick question. It's not a thing. It's this guy: + +![](http://i.imgur.com/JHaAlBJ.png) + +Basically, he wears a top hat, lives in your computer, and waits for you to tell him what kind of application you wish to create. + +Not every new computer comes with a Yeoman pre-installed. He lives in the [npm](https://npmjs.org) package repository. You only have to ask for him once, then he packs up and moves into your hard drive. *Make sure you clean up, he likes new and shiny things.* + +``` +$ npm install -g yo +``` + +### Yeoman Generators + +Yeoman travels light. He didn't pack any generators when he moved in. You can think of a generator like a plug-in. You get to choose what type of application you wish to create, such as a Backbone application or even a Chrome extension. + +To install generator-react-webpack from npm, run: + +``` +$ npm install -g generator-react-webpack +``` + +Finally, initiate the generator: + +``` +$ yo react-webpack +``` + +### Getting To Know Yeoman + +Yeoman has a heart of gold. He's a person with feelings and opinions, but he's very easy to work with. If you think he's too opinionated, he can be easily convinced. + +If you'd like to get to know Yeoman better and meet some of his friends, [Grunt](http://gruntjs.com) and [Bower](http://bower.io), check out the complete [Getting Started Guide](https://github.com/yeoman/yeoman/wiki/Getting-Started). + + +## License + +[MIT License](http://en.wikipedia.org/wiki/MIT_License) diff --git a/app/index.js b/app/index.js new file mode 100644 index 0000000..740dc80 --- /dev/null +++ b/app/index.js @@ -0,0 +1,74 @@ +'use strict'; +var util = require('util'); +var path = require('path'); +var yeoman = require('yeoman-generator'); +var generalUtils = require('../util.js'); + +var ReactWebpackGenerator = module.exports = function ReactWebpackGenerator(args, options, config) { + yeoman.generators.Base.apply(this, arguments); + + this.argument('appname', { type: String, required: false }); + this.appname = this.appname || path.basename(process.cwd()); + this.appname = this._.camelize(this._.slugify(this._.humanize(this.appname))); + this.scriptAppName = this._.capitalize(this.appname) + generalUtils.appName(this); + + args = ['main']; + + if (typeof this.env.options.appPath === 'undefined') { + this.env.options.appPath = this.env.options.appPath || 'src'; + } + + this.appPath = this.env.options.appPath; + + this.hookFor('react-webpack:common', { + args: args + }); + + this.hookFor('react-webpack:main', { + args: args + }); + + this.on('end', function () { + this.installDependencies({ skipInstall: options['skip-install'] }); + }); + + this.pkg = JSON.parse(this.readFileAsString(path.join(__dirname, '../package.json'))); +}; + +util.inherits(ReactWebpackGenerator, yeoman.generators.Base); + +ReactWebpackGenerator.prototype.welcome = function welcome() { + // welcome message + if (!this.options['skip-welcome-message']) { + console.log(this.yeoman); + console.log( + 'Out of the box I include Webpack and some default React components.\n' + ); + } +}; + +ReactWebpackGenerator.prototype.readIndex = function readIndex() { + this.indexFile = this.engine(this.read('../../templates/common/index.html'), this); +}; + +ReactWebpackGenerator.prototype.createIndexHtml = function createIndexHtml() { + this.indexFile = this.indexFile.replace(/'/g, "'"); + this.write(path.join(this.appPath, 'index.html'), this.indexFile); +}; + +ReactWebpackGenerator.prototype.packageFiles = function () { + this.template('../../templates/common/_package.json', 'package.json'); + this.copy('../../templates/common/Gruntfile.js', 'Gruntfile.js'); +}; + +ReactWebpackGenerator.prototype.styleFiles = function styleFiles() { + var mainFile = 'main.css'; + var resetFile = 'reset.css'; + this.copy('styles/' + mainFile, 'src/styles/' + mainFile); + this.copy('styles/' + resetFile, 'src/styles/' + resetFile); +}; + +ReactWebpackGenerator.prototype.imageFiles = function () { + this.sourceRoot(path.join(__dirname, 'templates')); + this.directory('images', 'src/images', true); +}; diff --git a/app/templates/images/yeoman.png b/app/templates/images/yeoman.png new file mode 100644 index 0000000..92497ad Binary files /dev/null and b/app/templates/images/yeoman.png differ diff --git a/app/templates/styles/main.css b/app/templates/styles/main.css new file mode 100644 index 0000000..321e9a1 --- /dev/null +++ b/app/templates/styles/main.css @@ -0,0 +1,41 @@ +/* Stiziles */ + +html, body { + background: #222222; +} + +/* main */ + +.main { + width: 100%; + height: 100%; + background: #222222; + color: #fff; +} + +.main img { + width: 103px; + height: 89px; + margin-bottom: 10px; + text-align: center; +} + +/* transitions */ + +.fade-enter { + opacity: 0.01; + transition: opacity .5s ease-in; +} + +.fade-enter.fade-enter-active { + opacity: 1; +} + +.fade-leave { + opacity: 1; + transition: opacity .5s ease-in; +} + +.fade-leave.fade-leave-active { + opacity: 0.01; +} diff --git a/app/templates/styles/reset.css b/app/templates/styles/reset.css new file mode 100644 index 0000000..af94440 --- /dev/null +++ b/app/templates/styles/reset.css @@ -0,0 +1,48 @@ +/* http://meyerweb.com/eric/tools/css/reset/ + v2.0 | 20110126 + License: none (public domain) +*/ + +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, embed, +figure, figcaption, footer, header, hgroup, +menu, nav, output, ruby, section, summary, +time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} +/* HTML5 display-role reset for older browsers */ +article, aside, details, figcaption, figure, +footer, header, hgroup, menu, nav, section { + display: block; +} +body { + line-height: 1; +} +ol, ul { + list-style: none; +} +blockquote, q { + quotes: none; +} +blockquote:before, blockquote:after, +q:before, q:after { + content: ''; + content: none; +} +table { + border-collapse: collapse; + border-spacing: 0; +} \ No newline at end of file diff --git a/common/index.js b/common/index.js new file mode 100644 index 0000000..0452f1a --- /dev/null +++ b/common/index.js @@ -0,0 +1,17 @@ +'use strict'; +var path = require('path'); +var util = require('util'); +var yeoman = require('yeoman-generator'); + +var CommonGenerator = module.exports = function CommonGenerator(args, options, config) { + yeoman.generators.NamedBase.apply(this, arguments); +}; + +util.inherits(CommonGenerator, yeoman.generators.NamedBase); + +CommonGenerator.prototype.setupEnv = function setupEnv() { + // Copies the contents of the generator `templates` + // directory into your users new application path + this.sourceRoot(path.join(__dirname, '../templates/common')); + this.directory('root', '.', true); +}; diff --git a/main/index.js b/main/index.js new file mode 100644 index 0000000..3b39a36 --- /dev/null +++ b/main/index.js @@ -0,0 +1,13 @@ +'use strict'; +var util = require('util'); +var ScriptBase = require('../script-base.js'); + +var MainGenerator = module.exports = function MainGenerator(args, options, config) { + ScriptBase.apply(this, arguments); +}; + +util.inherits(MainGenerator, ScriptBase); + +MainGenerator.prototype.createAppFile = function createAppFile() { + this.appTemplate('app', 'scripts/components/'+this.scriptAppName); +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..fbe5ca8 --- /dev/null +++ b/package.json @@ -0,0 +1,41 @@ +{ + "name": "generator-react-webpack", + "version": "0.0.0", + "description": "A generator for Yeoman", + "keywords": [ + "yeoman-generator" + ], + "homepage": "https://github.com/newtriks/generator-react-webpack", + "bugs": "https://github.com/newtriks/generator-react-webpack/issues", + "author": { + "name": "Simon Bailey", + "email": "simon@newtriks.com", + "url": "https://github.com/newtriks" + }, + "main": "app/index.js", + "repository": { + "type": "git", + "url": "git://github.com/newtriks/generator-react-webpack.git" + }, + "scripts": { + "test": "mocha" + }, + "dependencies": { + "yeoman-generator": "~0.14.0" + }, + "devDependencies": { + "mocha": "~1.14.0" + }, + "peerDependencies": { + "yo": ">=1.0.0" + }, + "engines": { + "node": ">=0.8.0", + "npm": ">=1.2.10" + }, + "licenses": [ + { + "type": "MIT" + } + ] +} diff --git a/script-base.js b/script-base.js new file mode 100644 index 0000000..261047c --- /dev/null +++ b/script-base.js @@ -0,0 +1,61 @@ +'use strict'; +var util = require('util'); +var path = require('path'); +var yeoman = require('yeoman-generator'); +var generalUtils = require('./util.js'); + +var Generator = module.exports = function Generator() { + yeoman.generators.NamedBase.apply(this, arguments); + + // Add capitalize mixin + this._.mixin({ 'capitalize': generalUtils.capitalize }); + + this.appname = path.basename(process.cwd()); + + this.appname = this._.slugify(this._.humanize(this.appname)); + this.scriptAppName = this._.capitalize(this.appname) + generalUtils.appName(this); + + if (typeof this.env.options.appPath === 'undefined') { + this.env.options.appPath = this.env.options.appPath || 'src'; + } + + if (typeof this.env.options.testPath === 'undefined') { + this.env.options.testPath = this.env.options.testPath || 'test/spec'; + } + + var sourceRoot = '/templates/javascript'; + this.scriptSuffix = '.js'; + + this.sourceRoot(path.join(__dirname, sourceRoot)); +}; + +util.inherits(Generator, yeoman.generators.NamedBase); + +Generator.prototype.appTemplate = function (src, dest) { + yeoman.generators.Base.prototype.template.apply(this, [ + src + this.scriptSuffix, + path.join(this.env.options.appPath, dest) + this.scriptSuffix + ]); +}; + +Generator.prototype.testTemplate = function (src, dest) { + yeoman.generators.Base.prototype.template.apply(this, [ + src + this.scriptSuffix, + path.join(this.env.options.testPath, dest) + this.scriptSuffix + ]); +}; + +Generator.prototype.htmlTemplate = function (src, dest) { + yeoman.generators.Base.prototype.template.apply(this, [ + src, + path.join(this.env.options.appPath, dest.toLowerCase()) + ]); +}; + +Generator.prototype.generateSourceAndTest = function (appTemplate, testTemplate, targetDirectory, skipAdd) { + this.appTemplate(appTemplate, path.join('scripts', targetDirectory, this.name)); + this.testTemplate(testTemplate, path.join(targetDirectory, this.name)); + if (!skipAdd) { + this.addScriptToIndex(path.join(targetDirectory, this.name)); + } +}; \ No newline at end of file diff --git a/templates/common/Gruntfile.js b/templates/common/Gruntfile.js new file mode 100644 index 0000000..3b10070 --- /dev/null +++ b/templates/common/Gruntfile.js @@ -0,0 +1,137 @@ +'use strict'; +var LIVERELOAD_PORT = 35729; +var lrSnippet = require('connect-livereload')({ + port: LIVERELOAD_PORT +}); +var mountFolder = function (connect, dir) { + return connect.static(require('path').resolve(dir)); +}; + +module.exports = function (grunt) { + // Let *load-grunt-tasks* require everything + require('load-grunt-tasks')(grunt); + // Read configuration from package.json + var pkgConfig = grunt.file.readJSON('package.json'); + var jshintConfig = grunt.file.readJSON('.jshintrc'); + + var loaders = [{ + test: /\.css$/, + loader: 'style!css' + }, { + test: /\.gif/, + loader: 'url-loader?limit=10000&minetype=image/gif' + }, { + test: /\.jpg/, + loader: 'url-loader?limit=10000&minetype=image/jpg' + }, { + test: /\.png/, + loader: 'url-loader?limit=10000&minetype=image/png' + }, { + test: /\.js$/, + loader: 'jsx-loader' + }]; + + grunt.initConfig({ + pkg: pkgConfig, + webpack: { + development: { + entry: './<%= pkg.src %>/scripts/components/<%= pkg.mainInput %>.js', + output: { + path: '<%= pkg.src %>/scripts/', + filename: '<%= pkg.mainOutput %>.js' + }, + debug: true, + cache: true, + stats: { + colors: true, + reasons: true + }, + jshint: grunt.util._.merge(jshintConfig, { + emitErrors: false, + failOnHint: false + }), + module: { + preLoaders: [{ + test: '\\.js$', + exclude: 'node_modules', + loader: 'jshint' + }], + loaders: loaders + } + } + }, + watch: { + webpack: { + files: ['<%= pkg.src %>/scripts/{,*/}*.js', + '<%= pkg.src %>/styles/{,*/}*.css', + '!<%= pkg.src %>/scripts/<%= pkg.mainOutput %>.js' + ], + tasks: ['webpack:development'] + }, + livereload: { + options: { + livereload: LIVERELOAD_PORT + }, + files: [ + '<%= pkg.src %>/{,*/}*.html', + '<%= pkg.src %>/scripts/<%= pkg.mainOutput %>.js' + ] + } + }, + connect: { + options: { + port: 8000, + // Change this to '0.0.0.0' to access the server from outside. + hostname: 'localhost' + }, + livereload: { + options: { + middleware: function (connect) { + return [ + lrSnippet, + mountFolder(connect, pkgConfig.src) + ]; + } + } + }, + dist: { + options: { + middleware: function (connect) { + return [ + mountFolder(connect, pkgConfig.dist) + ]; + } + } + } + }, + open: { + server: { + url: 'http://localhost:<%= connect.options.port %>' + } + }, + karma: { + unit: { + configFile: 'karma.conf.js' + } + } + }); + + grunt.registerTask('server', function (target) { + if (target === 'dist') { + return grunt.task.run(['build', 'open', 'connect:dist:keepalive']); + } + + grunt.task.run([ + 'connect:livereload', + 'webpack:development', + 'open', + 'watch' + ]); + }); + + grunt.registerTask('test', ['karma']); + + grunt.registerTask('build', []); + + grunt.registerTask('default', []); +}; diff --git a/templates/common/_package.json b/templates/common/_package.json new file mode 100644 index 0000000..7a0a648 --- /dev/null +++ b/templates/common/_package.json @@ -0,0 +1,36 @@ +{ + "name": "<%= _.slugify(appname) %>", + "version": "0.0.0", + "src": "src", + "test": "test", + "dist": "dist", + "mainInput": "<%= scriptAppName %>", + "mainOutput": "main", + "dependencies": { + "react": "~0.8.0" + }, + "devDependencies": { + "grunt": "~0.4.2", + "load-grunt-tasks": "~0.2.1", + "grunt-contrib-watch": "~0.5.3", + "grunt-contrib-connect": "~0.6.0", + "connect-livereload": "~0.3.1", + "webpack": "~1.0.0-beta4", + "jsx-loader": "petehunt/jsx-loader", + "grunt-webpack": "~1.0.0", + "style-loader": "~0.6.0", + "url-loader": "~0.5.2", + "css-loader": "~0.6.6", + "karma-script-launcher": "~0.1.0", + "karma-chrome-launcher": "~0.1.2", + "karma-firefox-launcher": "~0.1.3", + "karma-jasmine": "~0.1.5", + "karma-phantomjs-launcher": "~0.1.1", + "karma": "~0.10.9", + "grunt-karma": "~0.6.2", + "karma-webpack-plugin": "~1.0.0", + "webpack-dev-server": "~1.0.2", + "grunt-open": "~0.2.2", + "jshint-loader": "~0.6.0" + } +} diff --git a/templates/common/gitignore b/templates/common/gitignore new file mode 100644 index 0000000..cfb1a09 --- /dev/null +++ b/templates/common/gitignore @@ -0,0 +1,32 @@ +### SublimeText ### +*.sublime-workspace + +### OSX ### +.DS_Store +.AppleDouble +.LSOverride +Icon + +# Thumbnails +._* + +# Files that might appear on external disk +.Spotlight-V100 +.Trashes + +### Windows ### +# Windows image file caches +Thumbs.db +ehthumbs.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# App specific + +node_modules/ +.tmp +/src/scripts/main.js diff --git a/templates/common/index.html b/templates/common/index.html new file mode 100644 index 0000000..71fa2a1 --- /dev/null +++ b/templates/common/index.html @@ -0,0 +1,19 @@ + + + + + + + + + + + +
+

If you can see this, something is broken (or JS is not enabled).

+
+ + + diff --git a/templates/common/root/.editorconfig b/templates/common/root/.editorconfig new file mode 100644 index 0000000..c308ed0 --- /dev/null +++ b/templates/common/root/.editorconfig @@ -0,0 +1,13 @@ +# http://editorconfig.org +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false diff --git a/templates/common/root/.jshintrc b/templates/common/root/.jshintrc new file mode 100644 index 0000000..b5f6ae1 --- /dev/null +++ b/templates/common/root/.jshintrc @@ -0,0 +1,27 @@ +{ + "node": true, + "browser": true, + "esnext": true, + "bitwise": true, + "camelcase": false, + "curly": true, + "eqeqeq": true, + "immed": true, + "indent": 2, + "latedef": true, + "newcap": true, + "noarg": true, + "quotmark": "single", + "regexp": true, + "undef": true, + "unused": false, + "strict": true, + "trailing": true, + "smarttabs": true, + "white": true, + "newcap": false, + "globals": { + "React": true + } +} + diff --git a/templates/common/root/src/favicon.ico b/templates/common/root/src/favicon.ico new file mode 100644 index 0000000..6527905 Binary files /dev/null and b/templates/common/root/src/favicon.ico differ diff --git a/templates/common/root/test/.jshintrc b/templates/common/root/test/.jshintrc new file mode 100644 index 0000000..6d4b5b7 --- /dev/null +++ b/templates/common/root/test/.jshintrc @@ -0,0 +1,40 @@ +{ + "node": true, + "browser": true, + "esnext": true, + "bitwise": true, + "camelcase": false, + "curly": true, + "eqeqeq": true, + "immed": true, + "indent": 2, + "latedef": true, + "newcap": true, + "noarg": true, + "quotmark": "single", + "regexp": true, + "undef": true, + "unused": false, + "strict": true, + "trailing": true, + "smarttabs": true, + "white": true, + "newcap": false, + "globals": { + "after": false, + "afterEach": false, + "react": false, + "before": false, + "beforeEach": false, + "browser": false, + "describe": false, + "expect": false, + "inject": false, + "it": false, + "spyOn": false, + "jasmine": false, + "spyOnConstructor": false, + "React": true + } +} + diff --git a/templates/common/root/test/helpers/phantomjs-shims.js b/templates/common/root/test/helpers/phantomjs-shims.js new file mode 100644 index 0000000..7da307d --- /dev/null +++ b/templates/common/root/test/helpers/phantomjs-shims.js @@ -0,0 +1,34 @@ +(function() { + +var Ap = Array.prototype; +var slice = Ap.slice; +var Fp = Function.prototype; + +if (!Fp.bind) { + // PhantomJS doesn't support Function.prototype.bind natively, so + // polyfill it whenever this module is required. + Fp.bind = function(context) { + var func = this; + var args = slice.call(arguments, 1); + + function bound() { + var invokedAsConstructor = func.prototype && (this instanceof func); + return func.apply( + // Ignore the context parameter when invoking the bound function + // as a constructor. Note that this includes not only constructor + // invocations using the new keyword but also calls to base class + // constructors such as BaseClass.call(this, ...) or super(...). + !invokedAsConstructor && context || this, + args.concat(slice.call(arguments)) + ); + } + + // The bound function must share the .prototype of the unbound + // function so that any object created by one constructor will count + // as an instance of both constructors. + bound.prototype = func.prototype; + + return bound; + }; +} +})(); diff --git a/templates/common/root/test/helpers/react/addons.js b/templates/common/root/test/helpers/react/addons.js new file mode 100755 index 0000000..0bf44fe --- /dev/null +++ b/templates/common/root/test/helpers/react/addons.js @@ -0,0 +1,16336 @@ +/** + * React (with addons) v0.9.0-alpha + */ +!function(e){"object"==typeof exports?module.exports=e():"function"==typeof define&&define.amd?define(e):"undefined"!=typeof window?window.React=e():"undefined"!=typeof global?global.React=e():"undefined"!=typeof self&&(self.React=e())}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o -1; +} + +var CSSCore = { + + /** + * Adds the class passed in to the element if it doesn't already have it. + * + * @param {DOMElement} element the element to set the class on + * @param {string} className the CSS className + * @return {DOMElement} the element passed in + */ + addClass: function(element, className) { + ("production" !== "development" ? invariant( + !/\s/.test(className), + 'CSSCore.addClass takes only a single class name. "%s" contains ' + + 'multiple classes.', className + ) : invariant(!/\s/.test(className))); + + if (className) { + if (element.classList) { + element.classList.add(className); + } else if (!hasClass(element, className)) { + element.className = element.className + ' ' + className; + } + } + return element; + }, + + /** + * Removes the class passed in from the element + * + * @param {DOMElement} element the element to set the class on + * @param {string} className the CSS className + * @return {DOMElement} the element passed in + */ + removeClass: function(element, className) { + ("production" !== "development" ? invariant( + !/\s/.test(className), + 'CSSCore.removeClass takes only a single class name. "%s" contains ' + + 'multiple classes.', className + ) : invariant(!/\s/.test(className))); + + if (className) { + if (element.classList) { + element.classList.remove(className); + } else if (hasClass(element, className)) { + element.className = element.className + .replace(new RegExp('(^|\\s)' + className + '(?:\\s|$)', 'g'), '$1') + .replace(/\s+/g, ' ') // multiple spaces to one + .replace(/^\s*|\s*$/g, ''); // trim the ends + } + } + return element; + }, + + /** + * Helper to add or remove a class from an element based on a condition. + * + * @param {DOMElement} element the element to set the class on + * @param {string} className the CSS className + * @param {*} bool condition to whether to add or remove the class + * @return {DOMElement} the element passed in + */ + conditionClass: function(element, className, bool) { + return (bool ? CSSCore.addClass : CSSCore.removeClass)(element, className); + } +}; + +module.exports = CSSCore; + +},{"./invariant":113}],3:[function(require,module,exports){ +/** + * Copyright 2013 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @providesModule CSSProperty + */ + +"use strict"; + +/** + * CSS properties which accept numbers but are not in units of "px". + */ +var isUnitlessNumber = { + fillOpacity: true, + fontWeight: true, + lineHeight: true, + opacity: true, + orphans: true, + zIndex: true, + zoom: true +}; + +/** + * Most style properties can be unset by doing .style[prop] = '' but IE8 + * doesn't like doing that with shorthand properties so for the properties that + * IE8 breaks on, which are listed here, we instead unset each of the + * individual properties. See http://bugs.jquery.com/ticket/12385. + * The 4-value 'clock' properties like margin, padding, border-width seem to + * behave without any problems. Curiously, list-style works too without any + * special prodding. + */ +var shorthandPropertyExpansions = { + background: { + backgroundImage: true, + backgroundPosition: true, + backgroundRepeat: true, + backgroundColor: true + }, + border: { + borderWidth: true, + borderStyle: true, + borderColor: true + }, + borderBottom: { + borderBottomWidth: true, + borderBottomStyle: true, + borderBottomColor: true + }, + borderLeft: { + borderLeftWidth: true, + borderLeftStyle: true, + borderLeftColor: true + }, + borderRight: { + borderRightWidth: true, + borderRightStyle: true, + borderRightColor: true + }, + borderTop: { + borderTopWidth: true, + borderTopStyle: true, + borderTopColor: true + }, + font: { + fontStyle: true, + fontVariant: true, + fontWeight: true, + fontSize: true, + lineHeight: true, + fontFamily: true + } +}; + +var CSSProperty = { + isUnitlessNumber: isUnitlessNumber, + shorthandPropertyExpansions: shorthandPropertyExpansions +}; + +module.exports = CSSProperty; + +},{}],4:[function(require,module,exports){ +/** + * Copyright 2013 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @providesModule CSSPropertyOperations + * @typechecks static-only + */ + +"use strict"; + +var CSSProperty = require("./CSSProperty"); + +var dangerousStyleValue = require("./dangerousStyleValue"); +var escapeTextForBrowser = require("./escapeTextForBrowser"); +var hyphenate = require("./hyphenate"); +var memoizeStringOnly = require("./memoizeStringOnly"); + +var processStyleName = memoizeStringOnly(function(styleName) { + return escapeTextForBrowser(hyphenate(styleName)); +}); + +/** + * Operations for dealing with CSS properties. + */ +var CSSPropertyOperations = { + + /** + * Serializes a mapping of style properties for use as inline styles: + * + * > createMarkupForStyles({width: '200px', height: 0}) + * "width:200px;height:0;" + * + * Undefined values are ignored so that declarative programming is easier. + * + * @param {object} styles + * @return {?string} + */ + createMarkupForStyles: function(styles) { + var serialized = ''; + for (var styleName in styles) { + if (!styles.hasOwnProperty(styleName)) { + continue; + } + var styleValue = styles[styleName]; + if (styleValue != null) { + serialized += processStyleName(styleName) + ':'; + serialized += dangerousStyleValue(styleName, styleValue) + ';'; + } + } + return serialized || null; + }, + + /** + * Sets the value for multiple styles on a node. If a value is specified as + * '' (empty string), the corresponding style property will be unset. + * + * @param {DOMElement} node + * @param {object} styles + */ + setValueForStyles: function(node, styles) { + var style = node.style; + for (var styleName in styles) { + if (!styles.hasOwnProperty(styleName)) { + continue; + } + var styleValue = dangerousStyleValue(styleName, styles[styleName]); + if (styleValue) { + style[styleName] = styleValue; + } else { + var expansion = CSSProperty.shorthandPropertyExpansions[styleName]; + if (expansion) { + // Shorthand property that IE8 won't like unsetting, so unset each + // component to placate it + for (var individualStyleName in expansion) { + style[individualStyleName] = ''; + } + } else { + style[styleName] = ''; + } + } + } + } + +}; + +module.exports = CSSPropertyOperations; + +},{"./CSSProperty":3,"./dangerousStyleValue":97,"./escapeTextForBrowser":99,"./hyphenate":112,"./memoizeStringOnly":121}],5:[function(require,module,exports){ +/** + * Copyright 2013 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @providesModule CallbackRegistry + * @typechecks static-only + */ + +"use strict"; + +var listenerBank = {}; + +/** + * Stores "listeners" by `registrationName`/`id`. There should be at most one + * "listener" per `registrationName`/`id` in the `listenerBank`. + * + * Access listeners via `listenerBank[registrationName][id]`. + * + * @class CallbackRegistry + * @internal + */ +var CallbackRegistry = { + + /** + * Stores `listener` at `listenerBank[registrationName][id]`. Is idempotent. + * + * @param {string} id ID of the DOM element. + * @param {string} registrationName Name of listener (e.g. `onClick`). + * @param {?function} listener The callback to store. + */ + putListener: function(id, registrationName, listener) { + var bankForRegistrationName = + listenerBank[registrationName] || (listenerBank[registrationName] = {}); + bankForRegistrationName[id] = listener; + }, + + /** + * @param {string} id ID of the DOM element. + * @param {string} registrationName Name of listener (e.g. `onClick`). + * @return {?function} The stored callback. + */ + getListener: function(id, registrationName) { + var bankForRegistrationName = listenerBank[registrationName]; + return bankForRegistrationName && bankForRegistrationName[id]; + }, + + /** + * Deletes a listener from the registration bank. + * + * @param {string} id ID of the DOM element. + * @param {string} registrationName Name of listener (e.g. `onClick`). + */ + deleteListener: function(id, registrationName) { + var bankForRegistrationName = listenerBank[registrationName]; + if (bankForRegistrationName) { + delete bankForRegistrationName[id]; + } + }, + + /** + * Deletes all listeners for the DOM element with the supplied ID. + * + * @param {string} id ID of the DOM element. + */ + deleteAllListeners: function(id) { + for (var registrationName in listenerBank) { + delete listenerBank[registrationName][id]; + } + }, + + /** + * This is needed for tests only. Do not use! + */ + __purge: function() { + listenerBank = {}; + } + +}; + +module.exports = CallbackRegistry; + +},{}],6:[function(require,module,exports){ +/** + * Copyright 2013 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @providesModule ChangeEventPlugin + */ + +"use strict"; + +var EventConstants = require("./EventConstants"); +var EventPluginHub = require("./EventPluginHub"); +var EventPropagators = require("./EventPropagators"); +var ExecutionEnvironment = require("./ExecutionEnvironment"); +var SyntheticEvent = require("./SyntheticEvent"); + +var isEventSupported = require("./isEventSupported"); +var isTextInputElement = require("./isTextInputElement"); +var keyOf = require("./keyOf"); + +var topLevelTypes = EventConstants.topLevelTypes; + +var eventTypes = { + change: { + phasedRegistrationNames: { + bubbled: keyOf({onChange: null}), + captured: keyOf({onChangeCapture: null}) + } + } +}; + +/** + * For IE shims + */ +var activeElement = null; +var activeElementID = null; +var activeElementValue = null; +var activeElementValueProp = null; + +/** + * SECTION: handle `change` event + */ +function shouldUseChangeEvent(elem) { + return ( + elem.nodeName === 'SELECT' || + (elem.nodeName === 'INPUT' && elem.type === 'file') + ); +} + +var doesChangeEventBubble = false; +if (ExecutionEnvironment.canUseDOM) { + // See `handleChange` comment below + doesChangeEventBubble = isEventSupported('change') && ( + !('documentMode' in document) || document.documentMode > 8 + ); +} + +function manualDispatchChangeEvent(nativeEvent) { + var event = SyntheticEvent.getPooled( + eventTypes.change, + activeElementID, + nativeEvent + ); + EventPropagators.accumulateTwoPhaseDispatches(event); + + // If change bubbled, we'd just bind to it like all the other events + // and have it go through ReactEventTopLevelCallback. Since it doesn't, we + // manually listen for the change event and so we have to enqueue and + // process the abstract event manually. + EventPluginHub.enqueueEvents(event); + EventPluginHub.processEventQueue(); +} + +function startWatchingForChangeEventIE8(target, targetID) { + activeElement = target; + activeElementID = targetID; + activeElement.attachEvent('onchange', manualDispatchChangeEvent); +} + +function stopWatchingForChangeEventIE8() { + if (!activeElement) { + return; + } + activeElement.detachEvent('onchange', manualDispatchChangeEvent); + activeElement = null; + activeElementID = null; +} + +function getTargetIDForChangeEvent( + topLevelType, + topLevelTarget, + topLevelTargetID) { + if (topLevelType === topLevelTypes.topChange) { + return topLevelTargetID; + } +} +function handleEventsForChangeEventIE8( + topLevelType, + topLevelTarget, + topLevelTargetID) { + if (topLevelType === topLevelTypes.topFocus) { + // stopWatching() should be a noop here but we call it just in case we + // missed a blur event somehow. + stopWatchingForChangeEventIE8(); + startWatchingForChangeEventIE8(topLevelTarget, topLevelTargetID); + } else if (topLevelType === topLevelTypes.topBlur) { + stopWatchingForChangeEventIE8(); + } +} + + +/** + * SECTION: handle `input` event + */ +var isInputEventSupported = false; +if (ExecutionEnvironment.canUseDOM) { + // IE9 claims to support the input event but fails to trigger it when + // deleting text, so we ignore its input events + isInputEventSupported = isEventSupported('input') && ( + !('documentMode' in document) || document.documentMode > 9 + ); +} + +/** + * (For old IE.) Replacement getter/setter for the `value` property that gets + * set on the active element. + */ +var newValueProp = { + get: function() { + return activeElementValueProp.get.call(this); + }, + set: function(val) { + // Cast to a string so we can do equality checks. + activeElementValue = '' + val; + activeElementValueProp.set.call(this, val); + } +}; + +/** + * (For old IE.) Starts tracking propertychange events on the passed-in element + * and override the value property so that we can distinguish user events from + * value changes in JS. + */ +function startWatchingForValueChange(target, targetID) { + activeElement = target; + activeElementID = targetID; + activeElementValue = target.value; + activeElementValueProp = Object.getOwnPropertyDescriptor( + target.constructor.prototype, + 'value' + ); + + Object.defineProperty(activeElement, 'value', newValueProp); + activeElement.attachEvent('onpropertychange', handlePropertyChange); +} + +/** + * (For old IE.) Removes the event listeners from the currently-tracked element, + * if any exists. + */ +function stopWatchingForValueChange() { + if (!activeElement) { + return; + } + + // delete restores the original property definition + delete activeElement.value; + activeElement.detachEvent('onpropertychange', handlePropertyChange); + + activeElement = null; + activeElementID = null; + activeElementValue = null; + activeElementValueProp = null; +} + +/** + * (For old IE.) Handles a propertychange event, sending a `change` event if + * the value of the active element has changed. + */ +function handlePropertyChange(nativeEvent) { + if (nativeEvent.propertyName !== 'value') { + return; + } + var value = nativeEvent.srcElement.value; + if (value === activeElementValue) { + return; + } + activeElementValue = value; + + manualDispatchChangeEvent(nativeEvent); +} + +/** + * If a `change` event should be fired, returns the target's ID. + */ +function getTargetIDForInputEvent( + topLevelType, + topLevelTarget, + topLevelTargetID) { + if (topLevelType === topLevelTypes.topInput) { + // In modern browsers (i.e., not IE8 or IE9), the input event is exactly + // what we want so fall through here and trigger an abstract event + return topLevelTargetID; + } +} + +// For IE8 and IE9. +function handleEventsForInputEventIE( + topLevelType, + topLevelTarget, + topLevelTargetID) { + if (topLevelType === topLevelTypes.topFocus) { + // In IE8, we can capture almost all .value changes by adding a + // propertychange handler and looking for events with propertyName + // equal to 'value' + // In IE9, propertychange fires for most input events but is buggy and + // doesn't fire when text is deleted, but conveniently, selectionchange + // appears to fire in all of the remaining cases so we catch those and + // forward the event if the value has changed + // In either case, we don't want to call the event handler if the value + // is changed from JS so we redefine a setter for `.value` that updates + // our activeElementValue variable, allowing us to ignore those changes + // + // stopWatching() should be a noop here but we call it just in case we + // missed a blur event somehow. + stopWatchingForValueChange(); + startWatchingForValueChange(topLevelTarget, topLevelTargetID); + } else if (topLevelType === topLevelTypes.topBlur) { + stopWatchingForValueChange(); + } +} + +// For IE8 and IE9. +function getTargetIDForInputEventIE( + topLevelType, + topLevelTarget, + topLevelTargetID) { + if (topLevelType === topLevelTypes.topSelectionChange || + topLevelType === topLevelTypes.topKeyUp || + topLevelType === topLevelTypes.topKeyDown) { + // On the selectionchange event, the target is just document which isn't + // helpful for us so just check activeElement instead. + // + // 99% of the time, keydown and keyup aren't necessary. IE8 fails to fire + // propertychange on the first input event after setting `value` from a + // script and fires only keydown, keypress, keyup. Catching keyup usually + // gets it and catching keydown lets us fire an event for the first + // keystroke if user does a key repeat (it'll be a little delayed: right + // before the second keystroke). Other input methods (e.g., paste) seem to + // fire selectionchange normally. + if (activeElement && activeElement.value !== activeElementValue) { + activeElementValue = activeElement.value; + return activeElementID; + } + } +} + + +/** + * SECTION: handle `click` event + */ +function shouldUseClickEvent(elem) { + // Use the `click` event to detect changes to checkbox and radio inputs. + // This approach works across all browsers, whereas `change` does not fire + // until `blur` in IE8. + return ( + elem.nodeName === 'INPUT' && + (elem.type === 'checkbox' || elem.type === 'radio') + ); +} + +function getTargetIDForClickEvent( + topLevelType, + topLevelTarget, + topLevelTargetID) { + if (topLevelType === topLevelTypes.topClick) { + return topLevelTargetID; + } +} + +/** + * This plugin creates an `onChange` event that normalizes change events + * across form elements. This event fires at a time when it's possible to + * change the element's value without seeing a flicker. + * + * Supported elements are: + * - input (see `isTextInputElement`) + * - textarea + * - select + */ +var ChangeEventPlugin = { + + eventTypes: eventTypes, + + /** + * @param {string} topLevelType Record from `EventConstants`. + * @param {DOMEventTarget} topLevelTarget The listening component root node. + * @param {string} topLevelTargetID ID of `topLevelTarget`. + * @param {object} nativeEvent Native browser event. + * @return {*} An accumulation of synthetic events. + * @see {EventPluginHub.extractEvents} + */ + extractEvents: function( + topLevelType, + topLevelTarget, + topLevelTargetID, + nativeEvent) { + + var getTargetIDFunc, handleEventFunc; + if (shouldUseChangeEvent(topLevelTarget)) { + if (doesChangeEventBubble) { + getTargetIDFunc = getTargetIDForChangeEvent; + } else { + handleEventFunc = handleEventsForChangeEventIE8; + } + } else if (isTextInputElement(topLevelTarget)) { + if (isInputEventSupported) { + getTargetIDFunc = getTargetIDForInputEvent; + } else { + getTargetIDFunc = getTargetIDForInputEventIE; + handleEventFunc = handleEventsForInputEventIE; + } + } else if (shouldUseClickEvent(topLevelTarget)) { + getTargetIDFunc = getTargetIDForClickEvent; + } + + if (getTargetIDFunc) { + var targetID = getTargetIDFunc( + topLevelType, + topLevelTarget, + topLevelTargetID + ); + if (targetID) { + var event = SyntheticEvent.getPooled( + eventTypes.change, + targetID, + nativeEvent + ); + EventPropagators.accumulateTwoPhaseDispatches(event); + return event; + } + } + + if (handleEventFunc) { + handleEventFunc( + topLevelType, + topLevelTarget, + topLevelTargetID + ); + } + } + +}; + +module.exports = ChangeEventPlugin; + +},{"./EventConstants":15,"./EventPluginHub":17,"./EventPropagators":20,"./ExecutionEnvironment":21,"./SyntheticEvent":80,"./isEventSupported":114,"./isTextInputElement":116,"./keyOf":120}],7:[function(require,module,exports){ +/** + * Copyright 2013 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @providesModule CompositionEventPlugin + * @typechecks static-only + */ + +"use strict"; + +var EventConstants = require("./EventConstants"); +var EventPropagators = require("./EventPropagators"); +var ExecutionEnvironment = require("./ExecutionEnvironment"); +var ReactInputSelection = require("./ReactInputSelection"); +var SyntheticCompositionEvent = require("./SyntheticCompositionEvent"); + +var getTextContentAccessor = require("./getTextContentAccessor"); +var keyOf = require("./keyOf"); + +var END_KEYCODES = [9, 13, 27, 32]; // Tab, Return, Esc, Space +var START_KEYCODE = 229; + +var useCompositionEvent = ExecutionEnvironment.canUseDOM && + 'CompositionEvent' in window; +var topLevelTypes = EventConstants.topLevelTypes; +var currentComposition = null; + +// Events and their corresponding property names. +var eventTypes = { + compositionEnd: { + phasedRegistrationNames: { + bubbled: keyOf({onCompositionEnd: null}), + captured: keyOf({onCompositionEndCapture: null}) + } + }, + compositionStart: { + phasedRegistrationNames: { + bubbled: keyOf({onCompositionStart: null}), + captured: keyOf({onCompositionStartCapture: null}) + } + }, + compositionUpdate: { + phasedRegistrationNames: { + bubbled: keyOf({onCompositionUpdate: null}), + captured: keyOf({onCompositionUpdateCapture: null}) + } + } +}; + +/** + * Translate native top level events into event types. + * + * @param {string} topLevelType + * @return {object} + */ +function getCompositionEventType(topLevelType) { + switch (topLevelType) { + case topLevelTypes.topCompositionStart: + return eventTypes.compositionStart; + case topLevelTypes.topCompositionEnd: + return eventTypes.compositionEnd; + case topLevelTypes.topCompositionUpdate: + return eventTypes.compositionUpdate; + } +} + +/** + * Does our fallback best-guess model think this event signifies that + * composition has begun? + * + * @param {string} topLevelType + * @param {object} nativeEvent + * @return {boolean} + */ +function isFallbackStart(topLevelType, nativeEvent) { + return ( + topLevelType === topLevelTypes.topKeyDown && + nativeEvent.keyCode === START_KEYCODE + ); +} + +/** + * Does our fallback mode think that this event is the end of composition? + * + * @param {string} topLevelType + * @param {object} nativeEvent + * @return {boolean} + */ +function isFallbackEnd(topLevelType, nativeEvent) { + switch (topLevelType) { + case topLevelTypes.topKeyUp: + // Command keys insert or clear IME input. + return (END_KEYCODES.indexOf(nativeEvent.keyCode) !== -1); + case topLevelTypes.topKeyDown: + // Expect IME keyCode on each keydown. If we get any other + // code we must have exited earlier. + return (nativeEvent.keyCode !== START_KEYCODE); + case topLevelTypes.topKeyPress: + case topLevelTypes.topMouseDown: + case topLevelTypes.topBlur: + // Events are not possible without cancelling IME. + return true; + default: + return false; + } +} + +/** + * Helper class stores information about selection and document state + * so we can figure out what changed at a later date. + * + * @param {DOMEventTarget} root + */ +function FallbackCompositionState(root) { + this.root = root; + this.startSelection = ReactInputSelection.getSelection(root); + this.startValue = this.getText(); +} + +/** + * Get current text of input. + * + * @return {string} + */ +FallbackCompositionState.prototype.getText = function() { + return this.root.value || this.root[getTextContentAccessor()]; +}; + +/** + * Text that has changed since the start of composition. + * + * @return {string} + */ +FallbackCompositionState.prototype.getData = function() { + var endValue = this.getText(); + var prefixLength = this.startSelection.start; + var suffixLength = this.startValue.length - this.startSelection.end; + + return endValue.substr( + prefixLength, + endValue.length - suffixLength - prefixLength + ); +}; + +/** + * This plugin creates `onCompositionStart`, `onCompositionUpdate` and + * `onCompositionEnd` events on inputs, textareas and contentEditable + * nodes. + */ +var CompositionEventPlugin = { + + eventTypes: eventTypes, + + /** + * @param {string} topLevelType Record from `EventConstants`. + * @param {DOMEventTarget} topLevelTarget The listening component root node. + * @param {string} topLevelTargetID ID of `topLevelTarget`. + * @param {object} nativeEvent Native browser event. + * @return {*} An accumulation of synthetic events. + * @see {EventPluginHub.extractEvents} + */ + extractEvents: function( + topLevelType, + topLevelTarget, + topLevelTargetID, + nativeEvent) { + + var eventType; + var data; + + if (useCompositionEvent) { + eventType = getCompositionEventType(topLevelType); + } else if (!currentComposition) { + if (isFallbackStart(topLevelType, nativeEvent)) { + eventType = eventTypes.start; + currentComposition = new FallbackCompositionState(topLevelTarget); + } + } else if (isFallbackEnd(topLevelType, nativeEvent)) { + eventType = eventTypes.compositionEnd; + data = currentComposition.getData(); + currentComposition = null; + } + + if (eventType) { + var event = SyntheticCompositionEvent.getPooled( + eventType, + topLevelTargetID, + nativeEvent + ); + if (data) { + // Inject data generated from fallback path into the synthetic event. + // This matches the property of native CompositionEventInterface. + event.data = data; + } + EventPropagators.accumulateTwoPhaseDispatches(event); + return event; + } + } +}; + +module.exports = CompositionEventPlugin; + +},{"./EventConstants":15,"./EventPropagators":20,"./ExecutionEnvironment":21,"./ReactInputSelection":51,"./SyntheticCompositionEvent":79,"./getTextContentAccessor":110,"./keyOf":120}],8:[function(require,module,exports){ +/** + * Copyright 2013 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @providesModule DOMChildrenOperations + * @typechecks static-only + */ + +"use strict"; + +var Danger = require("./Danger"); +var ReactMultiChildUpdateTypes = require("./ReactMultiChildUpdateTypes"); + +var getTextContentAccessor = require("./getTextContentAccessor"); + +/** + * The DOM property to use when setting text content. + * + * @type {string} + * @private + */ +var textContentAccessor = getTextContentAccessor() || 'NA'; + +/** + * Inserts `childNode` as a child of `parentNode` at the `index`. + * + * @param {DOMElement} parentNode Parent node in which to insert. + * @param {DOMElement} childNode Child node to insert. + * @param {number} index Index at which to insert the child. + * @internal + */ +function insertChildAt(parentNode, childNode, index) { + var childNodes = parentNode.childNodes; + if (childNodes[index] === childNode) { + return; + } + // If `childNode` is already a child of `parentNode`, remove it so that + // computing `childNodes[index]` takes into account the removal. + if (childNode.parentNode === parentNode) { + parentNode.removeChild(childNode); + } + if (index >= childNodes.length) { + parentNode.appendChild(childNode); + } else { + parentNode.insertBefore(childNode, childNodes[index]); + } +} + +/** + * Operations for updating with DOM children. + */ +var DOMChildrenOperations = { + + dangerouslyReplaceNodeWithMarkup: Danger.dangerouslyReplaceNodeWithMarkup, + + /** + * Updates a component's children by processing a series of updates. The + * update configurations are each expected to have a `parentNode` property. + * + * @param {array} updates List of update configurations. + * @param {array} markupList List of markup strings. + * @internal + */ + processUpdates: function(updates, markupList) { + var update; + // Mapping from parent IDs to initial child orderings. + var initialChildren = null; + // List of children that will be moved or removed. + var updatedChildren = null; + + for (var i = 0; update = updates[i]; i++) { + if (update.type === ReactMultiChildUpdateTypes.MOVE_EXISTING || + update.type === ReactMultiChildUpdateTypes.REMOVE_NODE) { + var updatedIndex = update.fromIndex; + var updatedChild = update.parentNode.childNodes[updatedIndex]; + var parentID = update.parentID; + + initialChildren = initialChildren || {}; + initialChildren[parentID] = initialChildren[parentID] || []; + initialChildren[parentID][updatedIndex] = updatedChild; + + updatedChildren = updatedChildren || []; + updatedChildren.push(updatedChild); + } + } + + var renderedMarkup = Danger.dangerouslyRenderMarkup(markupList); + + // Remove updated children first so that `toIndex` is consistent. + if (updatedChildren) { + for (var j = 0; j < updatedChildren.length; j++) { + updatedChildren[j].parentNode.removeChild(updatedChildren[j]); + } + } + + for (var k = 0; update = updates[k]; k++) { + switch (update.type) { + case ReactMultiChildUpdateTypes.INSERT_MARKUP: + insertChildAt( + update.parentNode, + renderedMarkup[update.markupIndex], + update.toIndex + ); + break; + case ReactMultiChildUpdateTypes.MOVE_EXISTING: + insertChildAt( + update.parentNode, + initialChildren[update.parentID][update.fromIndex], + update.toIndex + ); + break; + case ReactMultiChildUpdateTypes.TEXT_CONTENT: + update.parentNode[textContentAccessor] = update.textContent; + break; + case ReactMultiChildUpdateTypes.REMOVE_NODE: + // Already removed by the for-loop above. + break; + } + } + } + +}; + +module.exports = DOMChildrenOperations; + +},{"./Danger":11,"./ReactMultiChildUpdateTypes":58,"./getTextContentAccessor":110}],9:[function(require,module,exports){ +/** + * Copyright 2013 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @providesModule DOMProperty + * @typechecks static-only + */ + +/*jslint bitwise: true */ + +"use strict"; + +var invariant = require("./invariant"); + +var DOMPropertyInjection = { + /** + * Mapping from normalized, camelcased property names to a configuration that + * specifies how the associated DOM property should be accessed or rendered. + */ + MUST_USE_ATTRIBUTE: 0x1, + MUST_USE_PROPERTY: 0x2, + HAS_SIDE_EFFECTS: 0x4, + HAS_BOOLEAN_VALUE: 0x8, + HAS_POSITIVE_NUMERIC_VALUE: 0x10, + + /** + * Inject some specialized knowledge about the DOM. This takes a config object + * with the following properties: + * + * isCustomAttribute: function that given an attribute name will return true + * if it can be inserted into the DOM verbatim. Useful for data-* or aria-* + * attributes where it's impossible to enumerate all of the possible + * attribute names, + * + * Properties: object mapping DOM property name to one of the + * DOMPropertyInjection constants or null. If your attribute isn't in here, + * it won't get written to the DOM. + * + * DOMAttributeNames: object mapping React attribute name to the DOM + * attribute name. Attribute names not specified use the **lowercase** + * normalized name. + * + * DOMPropertyNames: similar to DOMAttributeNames but for DOM properties. + * Property names not specified use the normalized name. + * + * DOMMutationMethods: Properties that require special mutation methods. If + * `value` is undefined, the mutation method should unset the property. + * + * @param {object} domPropertyConfig the config as described above. + */ + injectDOMPropertyConfig: function(domPropertyConfig) { + var Properties = domPropertyConfig.Properties || {}; + var DOMAttributeNames = domPropertyConfig.DOMAttributeNames || {}; + var DOMPropertyNames = domPropertyConfig.DOMPropertyNames || {}; + var DOMMutationMethods = domPropertyConfig.DOMMutationMethods || {}; + + if (domPropertyConfig.isCustomAttribute) { + DOMProperty._isCustomAttributeFunctions.push( + domPropertyConfig.isCustomAttribute + ); + } + + for (var propName in Properties) { + ("production" !== "development" ? invariant( + !DOMProperty.isStandardName[propName], + 'injectDOMPropertyConfig(...): You\'re trying to inject DOM property ' + + '\'%s\' which has already been injected. You may be accidentally ' + + 'injecting the same DOM property config twice, or you may be ' + + 'injecting two configs that have conflicting property names.', + propName + ) : invariant(!DOMProperty.isStandardName[propName])); + + DOMProperty.isStandardName[propName] = true; + + var lowerCased = propName.toLowerCase(); + DOMProperty.getPossibleStandardName[lowerCased] = propName; + + var attributeName = DOMAttributeNames[propName]; + if (attributeName) { + DOMProperty.getPossibleStandardName[attributeName] = propName; + } + + DOMProperty.getAttributeName[propName] = attributeName || lowerCased; + + DOMProperty.getPropertyName[propName] = + DOMPropertyNames[propName] || propName; + + var mutationMethod = DOMMutationMethods[propName]; + if (mutationMethod) { + DOMProperty.getMutationMethod[propName] = mutationMethod; + } + + var propConfig = Properties[propName]; + DOMProperty.mustUseAttribute[propName] = + propConfig & DOMPropertyInjection.MUST_USE_ATTRIBUTE; + DOMProperty.mustUseProperty[propName] = + propConfig & DOMPropertyInjection.MUST_USE_PROPERTY; + DOMProperty.hasSideEffects[propName] = + propConfig & DOMPropertyInjection.HAS_SIDE_EFFECTS; + DOMProperty.hasBooleanValue[propName] = + propConfig & DOMPropertyInjection.HAS_BOOLEAN_VALUE; + DOMProperty.hasPositiveNumericValue[propName] = + propConfig & DOMPropertyInjection.HAS_POSITIVE_NUMERIC_VALUE; + + ("production" !== "development" ? invariant( + !DOMProperty.mustUseAttribute[propName] || + !DOMProperty.mustUseProperty[propName], + 'DOMProperty: Cannot require using both attribute and property: %s', + propName + ) : invariant(!DOMProperty.mustUseAttribute[propName] || + !DOMProperty.mustUseProperty[propName])); + ("production" !== "development" ? invariant( + DOMProperty.mustUseProperty[propName] || + !DOMProperty.hasSideEffects[propName], + 'DOMProperty: Properties that have side effects must use property: %s', + propName + ) : invariant(DOMProperty.mustUseProperty[propName] || + !DOMProperty.hasSideEffects[propName])); + ("production" !== "development" ? invariant( + !DOMProperty.hasBooleanValue[propName] || + !DOMProperty.hasPositiveNumericValue[propName], + 'DOMProperty: Cannot have both boolean and positive numeric value: %s', + propName + ) : invariant(!DOMProperty.hasBooleanValue[propName] || + !DOMProperty.hasPositiveNumericValue[propName])); + } + } +}; +var defaultValueCache = {}; + +/** + * DOMProperty exports lookup objects that can be used like functions: + * + * > DOMProperty.isValid['id'] + * true + * > DOMProperty.isValid['foobar'] + * undefined + * + * Although this may be confusing, it performs better in general. + * + * @see http://jsperf.com/key-exists + * @see http://jsperf.com/key-missing + */ +var DOMProperty = { + + /** + * Checks whether a property name is a standard property. + * @type {Object} + */ + isStandardName: {}, + + /** + * Mapping from lowercase property names to the properly cased version, used + * to warn in the case of missing properties. + * @type {Object} + */ + getPossibleStandardName: {}, + + /** + * Mapping from normalized names to attribute names that differ. Attribute + * names are used when rendering markup or with `*Attribute()`. + * @type {Object} + */ + getAttributeName: {}, + + /** + * Mapping from normalized names to properties on DOM node instances. + * (This includes properties that mutate due to external factors.) + * @type {Object} + */ + getPropertyName: {}, + + /** + * Mapping from normalized names to mutation methods. This will only exist if + * mutation cannot be set simply by the property or `setAttribute()`. + * @type {Object} + */ + getMutationMethod: {}, + + /** + * Whether the property must be accessed and mutated as an object property. + * @type {Object} + */ + mustUseAttribute: {}, + + /** + * Whether the property must be accessed and mutated using `*Attribute()`. + * (This includes anything that fails ` in `.) + * @type {Object} + */ + mustUseProperty: {}, + + /** + * Whether or not setting a value causes side effects such as triggering + * resources to be loaded or text selection changes. We must ensure that + * the value is only set if it has changed. + * @type {Object} + */ + hasSideEffects: {}, + + /** + * Whether the property should be removed when set to a falsey value. + * @type {Object} + */ + hasBooleanValue: {}, + + /** + * Whether the property must be positive numeric or parse as a positive + * numeric and should be removed when set to a falsey value. + * @type {Object} + */ + hasPositiveNumericValue: {}, + + /** + * All of the isCustomAttribute() functions that have been injected. + */ + _isCustomAttributeFunctions: [], + + /** + * Checks whether a property name is a custom attribute. + * @method + */ + isCustomAttribute: function(attributeName) { + return DOMProperty._isCustomAttributeFunctions.some( + function(isCustomAttributeFn) { + return isCustomAttributeFn.call(null, attributeName); + } + ); + }, + + /** + * Returns the default property value for a DOM property (i.e., not an + * attribute). Most default values are '' or false, but not all. Worse yet, + * some (in particular, `type`) vary depending on the type of element. + * + * TODO: Is it better to grab all the possible properties when creating an + * element to avoid having to create the same element twice? + */ + getDefaultValueForProperty: function(nodeName, prop) { + var nodeDefaults = defaultValueCache[nodeName]; + var testElement; + if (!nodeDefaults) { + defaultValueCache[nodeName] = nodeDefaults = {}; + } + if (!(prop in nodeDefaults)) { + testElement = document.createElement(nodeName); + nodeDefaults[prop] = testElement[prop]; + } + return nodeDefaults[prop]; + }, + + injection: DOMPropertyInjection +}; + +module.exports = DOMProperty; + +},{"./invariant":113}],10:[function(require,module,exports){ +/** + * Copyright 2013 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @providesModule DOMPropertyOperations + * @typechecks static-only + */ + +"use strict"; + +var DOMProperty = require("./DOMProperty"); + +var escapeTextForBrowser = require("./escapeTextForBrowser"); +var memoizeStringOnly = require("./memoizeStringOnly"); + +function shouldIgnoreValue(name, value) { + return value == null || + DOMProperty.hasBooleanValue[name] && !value || + DOMProperty.hasPositiveNumericValue[name] && (isNaN(value) || value < 1); +} + +var processAttributeNameAndPrefix = memoizeStringOnly(function(name) { + return escapeTextForBrowser(name) + '="'; +}); + +if ("production" !== "development") { + var reactProps = { + children: true, + dangerouslySetInnerHTML: true, + key: true, + ref: true + }; + var warnedProperties = {}; + + var warnUnknownProperty = function(name) { + if (reactProps[name] || warnedProperties[name]) { + return; + } + + warnedProperties[name] = true; + var lowerCasedName = name.toLowerCase(); + + // data-* attributes should be lowercase; suggest the lowercase version + var standardName = DOMProperty.isCustomAttribute(lowerCasedName) ? + lowerCasedName : DOMProperty.getPossibleStandardName[lowerCasedName]; + + // For now, only warn when we have a suggested correction. This prevents + // logging too much when using transferPropsTo. + if (standardName != null) { + console.warn( + 'Unknown DOM property ' + name + '. Did you mean ' + standardName + '?' + ); + } + + }; +} + +/** + * Operations for dealing with DOM properties. + */ +var DOMPropertyOperations = { + + /** + * Creates markup for a property. + * + * @param {string} name + * @param {*} value + * @return {?string} Markup string, or null if the property was invalid. + */ + createMarkupForProperty: function(name, value) { + if (DOMProperty.isStandardName[name]) { + if (shouldIgnoreValue(name, value)) { + return ''; + } + var attributeName = DOMProperty.getAttributeName[name]; + return processAttributeNameAndPrefix(attributeName) + + escapeTextForBrowser(value) + '"'; + } else if (DOMProperty.isCustomAttribute(name)) { + if (value == null) { + return ''; + } + return processAttributeNameAndPrefix(name) + + escapeTextForBrowser(value) + '"'; + } else if ("production" !== "development") { + warnUnknownProperty(name); + } + return null; + }, + + /** + * Sets the value for a property on a node. + * + * @param {DOMElement} node + * @param {string} name + * @param {*} value + */ + setValueForProperty: function(node, name, value) { + if (DOMProperty.isStandardName[name]) { + var mutationMethod = DOMProperty.getMutationMethod[name]; + if (mutationMethod) { + mutationMethod(node, value); + } else if (shouldIgnoreValue(name, value)) { + this.deleteValueForProperty(node, name); + } else if (DOMProperty.mustUseAttribute[name]) { + node.setAttribute(DOMProperty.getAttributeName[name], '' + value); + } else { + var propName = DOMProperty.getPropertyName[name]; + if (!DOMProperty.hasSideEffects[name] || node[propName] !== value) { + node[propName] = value; + } + } + } else if (DOMProperty.isCustomAttribute(name)) { + if (value == null) { + node.removeAttribute(DOMProperty.getAttributeName[name]); + } else { + node.setAttribute(name, '' + value); + } + } else if ("production" !== "development") { + warnUnknownProperty(name); + } + }, + + /** + * Deletes the value for a property on a node. + * + * @param {DOMElement} node + * @param {string} name + */ + deleteValueForProperty: function(node, name) { + if (DOMProperty.isStandardName[name]) { + var mutationMethod = DOMProperty.getMutationMethod[name]; + if (mutationMethod) { + mutationMethod(node, undefined); + } else if (DOMProperty.mustUseAttribute[name]) { + node.removeAttribute(DOMProperty.getAttributeName[name]); + } else { + var propName = DOMProperty.getPropertyName[name]; + var defaultValue = DOMProperty.getDefaultValueForProperty( + node.nodeName, + name + ); + if (!DOMProperty.hasSideEffects[name] || + node[propName] !== defaultValue) { + node[propName] = defaultValue; + } + } + } else if (DOMProperty.isCustomAttribute(name)) { + node.removeAttribute(name); + } else if ("production" !== "development") { + warnUnknownProperty(name); + } + } + +}; + +module.exports = DOMPropertyOperations; + +},{"./DOMProperty":9,"./escapeTextForBrowser":99,"./memoizeStringOnly":121}],11:[function(require,module,exports){ +/** + * Copyright 2013 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @providesModule Danger + * @typechecks static-only + */ + +/*jslint evil: true, sub: true */ + +"use strict"; + +var ExecutionEnvironment = require("./ExecutionEnvironment"); + +var createNodesFromMarkup = require("./createNodesFromMarkup"); +var emptyFunction = require("./emptyFunction"); +var getMarkupWrap = require("./getMarkupWrap"); +var invariant = require("./invariant"); +var mutateHTMLNodeWithMarkup = require("./mutateHTMLNodeWithMarkup"); + +var OPEN_TAG_NAME_EXP = /^(<[^ \/>]+)/; +var RESULT_INDEX_ATTR = 'data-danger-index'; + +/** + * Extracts the `nodeName` from a string of markup. + * + * NOTE: Extracting the `nodeName` does not require a regular expression match + * because we make assumptions about React-generated markup (i.e. there are no + * spaces surrounding the opening tag and there is at least one attribute). + * + * @param {string} markup String of markup. + * @return {string} Node name of the supplied markup. + * @see http://jsperf.com/extract-nodename + */ +function getNodeName(markup) { + return markup.substring(1, markup.indexOf(' ')); +} + +var Danger = { + + /** + * Renders markup into an array of nodes. The markup is expected to render + * into a list of root nodes. Also, the length of `resultList` and + * `markupList` should be the same. + * + * @param {array} markupList List of markup strings to render. + * @return {array} List of rendered nodes. + * @internal + */ + dangerouslyRenderMarkup: function(markupList) { + ("production" !== "development" ? invariant( + ExecutionEnvironment.canUseDOM, + 'dangerouslyRenderMarkup(...): Cannot render markup in a Worker ' + + 'thread. This is likely a bug in the framework. Please report ' + + 'immediately.' + ) : invariant(ExecutionEnvironment.canUseDOM)); + var nodeName; + var markupByNodeName = {}; + // Group markup by `nodeName` if a wrap is necessary, else by '*'. + for (var i = 0; i < markupList.length; i++) { + ("production" !== "development" ? invariant( + markupList[i], + 'dangerouslyRenderMarkup(...): Missing markup.' + ) : invariant(markupList[i])); + nodeName = getNodeName(markupList[i]); + nodeName = getMarkupWrap(nodeName) ? nodeName : '*'; + markupByNodeName[nodeName] = markupByNodeName[nodeName] || []; + markupByNodeName[nodeName][i] = markupList[i]; + } + var resultList = []; + var resultListAssignmentCount = 0; + for (nodeName in markupByNodeName) { + if (!markupByNodeName.hasOwnProperty(nodeName)) { + continue; + } + var markupListByNodeName = markupByNodeName[nodeName]; + + // This for-in loop skips the holes of the sparse array. The order of + // iteration should follow the order of assignment, which happens to match + // numerical index order, but we don't rely on that. + for (var resultIndex in markupListByNodeName) { + if (markupListByNodeName.hasOwnProperty(resultIndex)) { + var markup = markupListByNodeName[resultIndex]; + + // Push the requested markup with an additional RESULT_INDEX_ATTR + // attribute. If the markup does not start with a < character, it + // will be discarded below (with an appropriate console.error). + markupListByNodeName[resultIndex] = markup.replace( + OPEN_TAG_NAME_EXP, + // This index will be parsed back out below. + '$1 ' + RESULT_INDEX_ATTR + '="' + resultIndex + '" ' + ); + } + } + + // Render each group of markup with similar wrapping `nodeName`. + var renderNodes = createNodesFromMarkup( + markupListByNodeName.join(''), + emptyFunction // Do nothing special with