diff --git a/.gitignore b/.gitignore index b7e395980c..090d7c3aee 100644 --- a/.gitignore +++ b/.gitignore @@ -6,11 +6,13 @@ static .grunt _SpecRunner.html build/ +.module-cache *.gem docs/code docs/_site docs/.sass-cache docs/css/react.css +docs/js/.module-cache docs/js/JSXTransformer.js docs/js/react.min.js docs/js/docs.js diff --git a/bin/jsx b/bin/jsx index ccdb19e668..2a6437d535 100755 --- a/bin/jsx +++ b/bin/jsx @@ -3,7 +3,7 @@ var visitors = require('../vendor/fbtransform/visitors').transformVisitors; var transform = require('../vendor/fbtransform/lib/transform').transform; -var debranch = require("../vendor/woodchipper").debranch; +var propagate = require("../vendor/constants").propagate; require("commoner").resolve(function(id) { var context = this; @@ -25,18 +25,12 @@ require("commoner").resolve(function(id) { }).process(function(id, source) { var context = this; + var constants = context.config.constants || {}; // This is where JSX, ES6, etc. desugaring happens. source = transform(visitors.react, source).code; - return context.makePromise(function(callback) { - var constants = context.config.constants || {}; - - // Debranching means removing any obviously dead code after - // replacing constant conditional expressions with literal - // (boolean) values. - debranch(constants, source, function(source) { - callback(null, source); - }); - }); + // Constant propagation means removing any obviously dead code after + // replacing constant expressions with literal (boolean) values. + return propagate(constants, source); }); diff --git a/docs/_includes/nav_docs.html b/docs/_includes/nav_docs.html index d6d9e945a9..1dbd3b5c91 100644 --- a/docs/_includes/nav_docs.html +++ b/docs/_includes/nav_docs.html @@ -21,4 +21,11 @@
  • API
  • + + diff --git a/docs/_layouts/docs.html b/docs/_layouts/docs.html index 8df307908b..ff0b835ac3 100644 --- a/docs/_layouts/docs.html +++ b/docs/_layouts/docs.html @@ -20,6 +20,6 @@ sectionid: docs {% endif %} -
    +
    diff --git a/docs/_layouts/post.html b/docs/_layouts/post.html index 656085dcb6..9aa96b3565 100644 --- a/docs/_layouts/post.html +++ b/docs/_layouts/post.html @@ -16,6 +16,6 @@ sectionid: blog
    -
    +
    diff --git a/docs/_posts/2013-06-12-community-roundup.md b/docs/_posts/2013-06-12-community-roundup.md index f5e63a098b..d37e3b1351 100644 --- a/docs/_posts/2013-06-12-community-roundup.md +++ b/docs/_posts/2013-06-12-community-roundup.md @@ -8,14 +8,14 @@ React was open sourced two weeks ago and it's time for a little round-up of what ## Khan Academy Question Editor -It looks like [Ben Alpert](http://benalpert.com/) is the first person outside of Facebook and Instagram to push React code to production. We are very grateful for his contributions in form of pull requests, bug reports and presence on IRC ([#reactjs on Freenode](irc://chat.freenode.net/reactjs)). Ben wrote about his experience using React on Quora: +It looks like [Ben Alpert](http://benalpert.com/) is the first person outside of Facebook and Instagram to push React code to production. We are very grateful for his contributions in form of pull requests, bug reports and presence on IRC ([#reactjs on Freenode](irc://chat.freenode.net/reactjs)). Ben wrote about his experience using React: > I just rewrote a 2000-line project in React and have now made a handful of pull requests to React. Everything about React I've seen so far seems really well thought-out and I'm proud to be the first non-FB/IG production user of React. > > The project that I rewrote in React (and am continuing to improve) is the Khan Academy question editor which content creators can use to enter questions and hints that will be presented to students: ->
    [![](/react/img/blog/khan-academy-editor.png)](http://www.quora.com/React-JS-Library/How-is-Facebooks-React-JavaScript-library/answer/Ben-Alpert)
    +>
    [![](/react/img/blog/khan-academy-editor.png)](http://benalpert.com/2013/06/09/using-react-to-speed-up-khan-academy.html)
    > -> [Read the full post...](http://www.quora.com/React-JS-Library/How-is-Facebooks-React-JavaScript-library/answer/Ben-Alpert) +> [Read the full post...](http://benalpert.com/2013/06/09/using-react-to-speed-up-khan-academy.html) ## Pimp my Backbone.View (by replacing it with React) diff --git a/docs/_posts/2013-06-19-community-roundup-2.md b/docs/_posts/2013-06-19-community-roundup-2.md new file mode 100644 index 0000000000..57b322e1cc --- /dev/null +++ b/docs/_posts/2013-06-19-community-roundup-2.md @@ -0,0 +1,71 @@ +--- +title: "Community Round-up #2" +layout: post +author: Vjeux +--- + +Since the launch we have received a lot of feedback and are actively working on React 0.4. In the meantime, here are the highlights of this week. + +## Some quick thoughts on React + +[Andrew Greig](http://www.andrewgreig.com/) made a blog post that gives a high level description of what React is. + +> I have been using Facebooks recently released Javascript framework called React.js for the last few days and have managed to obtain a rather high level understanding of how it works and formed a good perspective on how it fits in to the entire javascript framework ecosystem. +> +> Basically, React is not an MVC framework. It is not a replacement for Backbone or Knockout or Angular, instead it is designed to work with existing frameworks and help extend their functionality. +> +> It is designed for building big UIs. The type where you have lots of reusable components that are handling events and presenting and changing some backend data. In a traditional MVC app, React fulfils the role of the View. So you would still need to handle the Model and Controller on your own. +> +> I found the best way to utilise React was to pair it with Backbone, with React replacing the Backbone View, or to write your own Model/Data object and have React communicate with that. +> +> [Read the full post...](http://www.andrewgreig.com/637/) + +## React and Socket.IO Chat Application + +[Danial Khosravi](http://danialk.github.io/) made a real-time chat application that interacts with the back-end using Socket.IO. + +> A week ago I was playing with AngularJS and [this little chat application](https://github.com/btford/angular-socket-io-im) which uses socket.io and nodejs for realtime communication. Yesterday I saw a post about ReactJS in [EchoJS](http://www.echojs.com/) and started playing with this UI library. After playing a bit with React, I decided to write and chat application using React and I used Bran Ford's Backend for server side of this little app. +>
    [![](/react/img/blog/chatapp.png)](http://danialk.github.io/blog/2013/06/16/reactjs-and-socket-dot-io-chat-application/)
    +> +> [Read the full post...](http://danialk.github.io/blog/2013/06/16/reactjs-and-socket-dot-io-chat-application/) + +## React and Other Frameworks + +[Pete Hunt](http://www.petehunt.net/blog/) wrote an answer on Quora comparing React and Angular directives. At the end, he explains how you can make an Angular directive that is in fact being rendered with React. + +> To set the record straight: React components are far more powerful than Angular templates; they should be compared with Angular's directives instead. So I took the first Google hit for "AngularJS directive tutorial" (AngularJS Directives Tutorial - Fundoo Solutions), rewrote it in React and compared them. [...] +> +> We've designed React from the beginning to work well with other libraries. Angular is no exception. Let's take the original Angular example and use React to implement the fundoo-rating directive. +> +> [Read the full post...](http://www.quora.com/Pete-Hunt/Posts/Facebooks-React-vs-AngularJS-A-Closer-Look) + +In the same vein, [Markov Twain](https://twitter.com/markov_twain/status/345702941845499906) re-implemented the examples on the front-page [with Ember](http://jsbin.com/azihiw/2/edit) and [Vlad Yazhbin](https://twitter.com/vla) re-implemented the tutorial [with Angular](http://jsfiddle.net/vla/Cdrse/). + +## Web Components: React & x-tags + +Mozilla and Google are actively working on Web Components. [Vjeux](http://blog.vjeux.com/) wrote a proof of concept that shows how to implement them using React. + +> Using [x-tags](http://www.x-tags.org/) from Mozilla, we can write custom tags within the DOM. This is a great opportunity to be able to write reusable components without being tied to a particular library. I wrote [x-react](https://github.com/vjeux/react-xtags/) to have them being rendered in React. +>
    [![](/react/img/blog/xreact.png)](http://blog.vjeux.com/2013/javascript/custom-components-react-x-tags.html)
    +> +> [Read the full post...](http://blog.vjeux.com/2013/javascript/custom-components-react-x-tags.html) + +## React TodoMVC Example + +[TodoMVC.com](http://todomvc.com/) is a website that collects various implementations of the same basic Todo app. [Pete Hunt](http://www.petehunt.net/blog/) wrote an idiomatic React version. + +> Developers these days are spoiled with choice when it comes to selecting an MV* framework for structuring and organizing their JavaScript web apps. +> +> To help solve this problem, we created TodoMVC - a project which offers the same Todo application implemented using MV* concepts in most of the popular JavaScript MV* frameworks of today. +>
    [![](/react/img/blog/todomvc.png)](http://todomvc.com/labs/architecture-examples/react/)
    +> +> [Read the source code...](https://github.com/tastejs/todomvc/tree/gh-pages/labs/architecture-examples/react) + +## JSX is not HTML + +Many of you pointed out differences between JSX and HTML. In order to clear up some confusion, we have added some documentation that covers the four main differences: + + - [Whitespace removal](http://facebook.github.io/react/docs/jsx-is-not-html.html) + - [HTML Entities](http://facebook.github.io/react/docs/jsx-is-not-html.html) + - [Comments](http://facebook.github.io/react/docs/jsx-is-not-html.html) + - [Custom HTML Attributes](http://facebook.github.io/react/docs/jsx-is-not-html.html) diff --git a/docs/_posts/2013-06-21-react-v0-3-3.md b/docs/_posts/2013-06-21-react-v0-3-3.md new file mode 100644 index 0000000000..668c83de1a --- /dev/null +++ b/docs/_posts/2013-06-21-react-v0-3-3.md @@ -0,0 +1,24 @@ +--- +title: "React v0.3.3" +layout: post +author: Paul O'Shannessy +--- + +We have a ton of great stuff coming in v0.4, but in the meantime we're releasing v0.3.3. This release addresses some small issues people were having and simplifies our tools to make them easier to use. + + +## react-tools + +* Upgrade Commoner so `require` statements are no longer relativized when passing through the transformer. This was a feature needed when building React, but doesn't translate well for other consumers of `bin/jsx`. +* Upgraded our dependencies on Commoner and Recast so they use a different directory for their cache. +* Freeze our esprima dependency. + + +## React + +* Allow reusing the same DOM node to render different components. e.g. `React.renderComponent(
    , domNode); React.renderComponent(, domNode);` will work now. + + +## JSXTransformer + +* Improved the in-browser transformer so that transformed scripts will execute in the expected scope. The allows components to be defined and used from separate files. diff --git a/docs/_posts/2013-06-27-community-roundup-3.md b/docs/_posts/2013-06-27-community-roundup-3.md new file mode 100644 index 0000000000..50fed07978 --- /dev/null +++ b/docs/_posts/2013-06-27-community-roundup-3.md @@ -0,0 +1,91 @@ +--- +title: "Community Round-up #3" +layout: post +author: Vjeux +--- + +The highlight of this week is that an interaction-heavy app has been ported to React. React components are solving issues they had with nested views. + +## Moving From Backbone To React + +[Clay Allsopp](http://twitter.com/clayallsopp) successfuly ported [Propeller](http://usepropeller.com/blog/posts/from-backbone-to-react/), a fairly big, interaction-heavy JavaScript app, to React. + +> [](http://usepropeller.com/blog/posts/from-backbone-to-react/)Subviews involve a lot of easy-to-forget boilerplate that Backbone (by design) doesn't automate. Libraries like Backbone.Marionette offer more abstractions to make view nesting easier, but they're all limited by the fact that Backbone delegates how and went view-document attachment occurs to the application code. +> +> React, on the other hand, manages the DOM and only exposes real nodes at select points in its API. The "elements" you code in React are actually objects which wrap DOM nodes, not the actual objects which get inserted into the DOM. Internally, React converts those abstractions into actual DOMElements and fills out the document accordingly. [...] +> +> We moved about 20 different Backbone view classes to React over the past few weeks, including the live-preview pane that you see in our little iOS demo. Most importantly, it's allowed us to put energy into making each component work great on its own, instead of spending extra cycles to ensure they function in unison. For that reason, we think React is a more scalable way to build view-intensive apps than Backbone alone, and it doesn't require you to drop-everything-and-refactor like a move to Ember or Angular would demand. +> +> [Read the full post...](http://usepropeller.com/blog/posts/from-backbone-to-react/) + +## Grunt Task for JSX + +[Eric Clemmons](http://ericclemmons.github.io/) wrote a task for [Grunt](http://gruntjs.com/) that applies the JSX transformation to your Javascript files. It also works with [Browserify](http://browserify.org/) if you want all your files to be concatenated and minified together. + +> Grunt task for compiling Facebook React's .jsx templates into .js +> +> ```javascript +grunt.initConfig({ + react: { + app: { + options: { extension: 'js' }, + files: { 'path/to/output/dir': 'path/to/jsx/templates/dir' } +``` +> +> It also works great with `grunt-browserify`! +> +> ```javascript +browserify: { + options: { + transform: [ require('grunt-react').browserify ] + }, + app: { + src: 'path/to/source/main.js', + dest: 'path/to/target/output.js' +``` +> +> [Check out the project ...](https://github.com/ericclemmons/grunt-react) + +## Backbone/Handlebars Nested Views + +[Joel Burget](http://joelburget.com/) wrote a blog post talking about the way we would write React-like components in Backbone and Handlebars. + +> The problem here is that we're trying to maniplate a tree, but there's a textual layer we have to go through. Our views are represented as a tree - the subviews are children of CommentCollectionView - and they end up as part of a tree in the DOM. But there's a Handlebars layer in the middle (which deals in flat strings), so the hierarchy must be destructed and rebuilt when we render. +> +> What does it take to render a collection view? In the Backbone/Handlebars view of the world you have to render the template (with stubs), render each subview which replaces a stub, and keep a reference to each subview (or anything within the view that could change in the future). +> +> So while our view is conceptually hierarchical, due to the fact that it has to go through a flat textual representation, we need to do a lot of extra work to reassemble that structure after rendering. +> +> [Read the full post...](http://joelburget.com/react/) + +## JSRomandie Meetup + +[Renault John Lecoultre](https://twitter.com/renajohn/) from [BugBuster](http://www.bugbuster.com) did a React introduction talk at a JS meetup called [JS Romandie](https://twitter.com/jsromandie) last week. + + + +## CoffeeScript integration + +[Vjeux](http://blog.vjeux.com/) used the fact that JSX is just a syntactic sugar on-top of regular JS to rewrite the React front-page examples in CoffeeScript. + +> Multiple people asked what's the story about JSX and CoffeeScript. There is no JSX pre-processor for CoffeeScript and I'm not aware of anyone working on it. Fortunately, CoffeeScript is pretty expressive and we can play around the syntax to come up with something that is usable. +> +> ```javascript +{div, h3, textarea} = React.DOM +(div {className: 'MarkdownEditor'}, [ + (h3 {}, 'Input'), + (textarea {onKeyUp: @handleKeyUp, ref: 'textarea'}, + @state.value + ) +]) +``` +> +> [Read the full post...](http://blog.vjeux.com/2013/javascript/react-coffeescript.html) + +## Tutorial in Plain Javascript + +We've seen a lot of people comparing React with various frameworks. [Ricardo Tomasi](http://ricardo.cc/) decided to re-implement the tutorial without any framework, just plain Javascript. + +> Facebook & Instagram launched the React framework and an accompanying tutorial. Developer Vlad Yazhbin decided to rewrite that using AngularJS. The end result is pretty neat, but if you're like me you will not actually appreciate the HTML speaking for itself and doing all the hard work. So let's see what that looks like in plain javascript. +> +> [Read the full post...](http://ricardo.cc/2013/06/07/react-tutorial-rewritten-in-plain-javascript.html) diff --git a/docs/docs/api.md b/docs/docs/api.md index 1ae5e97b37..337ae86901 100644 --- a/docs/docs/api.md +++ b/docs/docs/api.md @@ -3,6 +3,7 @@ id: docs-api title: React API layout: docs prev: mixins.html +next: jsx-is-not-html.html --- ## React diff --git a/docs/docs/component-lifecycle.md b/docs/docs/component-lifecycle.md index 1998f1806e..8861c1877f 100644 --- a/docs/docs/component-lifecycle.md +++ b/docs/docs/component-lifecycle.md @@ -22,7 +22,7 @@ In this one simple line, we have accomplished the following: - The component is **mounted** into `document.body`. **Mounting** is the process of initializing a React component by creating its -DOM nodes and inserting the them into a supplied container node. +DOM nodes and inserting them into a supplied container node. At this point, the entire page consists of a single `
    ` with "Hello, world!". diff --git a/docs/docs/getting-started.md b/docs/docs/getting-started.md index 5b732c032a..1e62818ee9 100644 --- a/docs/docs/getting-started.md +++ b/docs/docs/getting-started.md @@ -88,6 +88,10 @@ React.renderComponent( ); ``` +> Note: +> +> The comment parser is very strict right now, in order for it to pick up the `@jsx` modifier, two conditions are required. The `@jsx` comment block must be the first comment on the file. The comment must start with `/**` (`/*` and `//` will not work). If the parser can't find the `@jsx` comment, it will output the file without transforming it. + Update your HTML file as below: ```html{6,10} diff --git a/docs/docs/jsx-is-not-html.md b/docs/docs/jsx-is-not-html.md new file mode 100644 index 0000000000..42013e9196 --- /dev/null +++ b/docs/docs/jsx-is-not-html.md @@ -0,0 +1,95 @@ +--- +id: docs-jsx-is-not-html +title: JSX is not HTML +description: Differences between JSX and HTML. +layout: docs +prev: api.html +--- + +JSX looks like HTML but there are some important differences you may run into. + +## Whitespace removal + +JSX doesn't follow the same whitespace elimination rules as HTML. JSX removes all whitespace between two curly braces expressions. If you want to have whitespace, simply add `{' '}`. + +```javascript +
    {this.props.name} {' '} {this.props.surname}
    +``` + +Follow [Issue #65](https://github.com/facebook/react/issues/65) for discussion on this behavior. + +## HTML Entities + +You can insert HTML entities within literal text in JSX: + +```javascript +
    First · Second
    +``` + +If you want to display an HTML entity within dynamic content, you will run into double escaping issues as React escapes all the strings you are displaying in order to prevent a wide range of XSS attacks by default. + +```javascript +// Bad: It displays "First · Second" +
    {'First · Second'}
    +``` + +There are various ways to work-around this issue. The easiest one is to write unicode character directly in Javascript. You need to make sure that the file is saved as UTF-8 and that the proper UTF-8 directives are set so the browser will display it correctly. + +```javascript +
    {'First ยท Second'}
    +``` + +A safer alternative is to find the [unicode number corresponding to the entity](http://www.fileformat.info/info/unicode/char/b7/index.htm) and use it inside of a JavaScript string. + +```javascript +
    {'First \u00b7 Second'}
    +
    {'First ' + String.fromCharCode(183) + ' Second'}
    +``` + +You can use mixed arrays with strings and JSX elements. + +```javascript +
    {['First ', ·, ' Second']}
    +``` + +As a last resort, you always have the ability to insert raw HTML. + +```javascript +
    +``` + +## Comments + +JSX supports both single-line and multi-line JavaScript comments within a tag declaration: + +```javascript +
    +``` + +As of React 0.3, there is no good way to insert comments within the children section. [Issue #82](https://github.com/facebook/react/issues/82) is tracking progress to enable the following: + +```javascript +// Note: This is not implemented yet! +
    + {/* This is a comment */} +
    +``` + +## Custom HTML Attributes + +If you pass properties to native HTML elements that do not exist in the HTML specification, React will not render them. If you want to use a custom attribute, you should prefix it with `data-`. + +```javascript +
    +``` + +[Web Accessibility](http://www.w3.org/WAI/intro/aria) attributes starting with `aria-` will be rendered properly. + +```javascript +
    +``` diff --git a/docs/docs/syntax.md b/docs/docs/syntax.md index e4b099bd23..bb477e9a07 100644 --- a/docs/docs/syntax.md +++ b/docs/docs/syntax.md @@ -53,7 +53,7 @@ var app = Nav({color:'blue'}, Profile({}, 'click')); ``` Use the [JSX Compiler](/react/jsx-compiler.html) to try out JSX and see how it -desguars into native JavaScript. +desugars into native JavaScript. If you want to use JSX, the [Getting Started](getting-started.html) guide shows how to setup compilation. diff --git a/docs/docs/tutorial.md b/docs/docs/tutorial.md index 8117b2baaa..021ff5057a 100644 --- a/docs/docs/tutorial.md +++ b/docs/docs/tutorial.md @@ -262,8 +262,8 @@ So far we've been inserting the comments directly in the source code. Instead, l ```javascript // tutorial8.js var data = [ - {author: 'Pete Hunt', text: 'This is one comment'}, - {author: 'Jordan Walke', text: 'This is *another* comment'} + {author: "Pete Hunt", text: "This is one comment"}, + {author: "Jordan Walke", text: "This is *another* comment"} ]; ``` @@ -357,8 +357,8 @@ When the component is first created, we want to GET some JSON from the server an ```javascript // tutorial13.json [ - {'author': 'Pete Hunt', 'text': 'This is one comment'}, - {'author': 'Jordan Walke', 'text': 'This is *another* comment'} + {"author": "Pete Hunt", "text": "This is one comment"}, + {"author": "Jordan Walke", "text": "This is *another* comment"} ] ``` @@ -435,7 +435,7 @@ React.renderComponent( ``` -All we have done here is move the AJAX call to a separate method and call it when the component is first loaded and every 60 seconds after that. Try running this in your browser and changing the `comments.json` file; within 5 seconds, the changes will show! +All we have done here is move the AJAX call to a separate method and call it when the component is first loaded and every 5 seconds after that. Try running this in your browser and changing the `comments.json` file; within 5 seconds, the changes will show! ## Adding new comments diff --git a/docs/downloads.md b/docs/downloads.md index d35ff4dbd2..2b2a9ad556 100644 --- a/docs/downloads.md +++ b/docs/downloads.md @@ -49,6 +49,8 @@ $ npm install -g react-tools ## Release Notes +**0.3.3** Upgrade `react-tools` dependencies, improve in-browser JSX transformer, make `React.renderComponent` more versatile. [Details...](blog/2013/06/20/react-v0-3-3.html) + **0.3.2** Improve compatibility of JSX Transformer; make `react-tools` compatible with [browserify](https://github.com/substack/node-browserify) **0.3.1** Fix `react-tools` module diff --git a/docs/img/blog/chatapp.png b/docs/img/blog/chatapp.png new file mode 100644 index 0000000000..55edf29dd0 Binary files /dev/null and b/docs/img/blog/chatapp.png differ diff --git a/docs/img/blog/propeller-logo.png b/docs/img/blog/propeller-logo.png new file mode 100644 index 0000000000..1573d715b2 Binary files /dev/null and b/docs/img/blog/propeller-logo.png differ diff --git a/docs/img/blog/todomvc.png b/docs/img/blog/todomvc.png new file mode 100644 index 0000000000..9c03bc0f62 Binary files /dev/null and b/docs/img/blog/todomvc.png differ diff --git a/docs/img/blog/xreact.png b/docs/img/blog/xreact.png new file mode 100644 index 0000000000..59fbb6f22a Binary files /dev/null and b/docs/img/blog/xreact.png differ diff --git a/grunt/config/compare_size.js b/grunt/config/compare_size.js index e785fe5281..fb59d94cdb 100644 --- a/grunt/config/compare_size.js +++ b/grunt/config/compare_size.js @@ -12,6 +12,6 @@ module.exports = { return gzip.zip(contents, {}).length; } }, - cache: "build/.sizecache.json" + cache: ".grunt/sizecache.json" } }; diff --git a/grunt/tasks/jsx.js b/grunt/tasks/jsx.js index 08a84ab4f0..290816711f 100644 --- a/grunt/tasks/jsx.js +++ b/grunt/tasks/jsx.js @@ -1,16 +1,17 @@ 'use strict'; -var exec = require("child_process").exec; -var expand = require("grunt").file.expand; +var grunt = require("grunt"); +var expand = grunt.file.expand; +var spawn = grunt.util.spawn; module.exports = function() { var done = this.async(); var config = this.data; var args = [ - "bin/jsx", "--cache-dir", ".module-cache", "--relativize", + "--follow-requires", config.sourceDir, config.outputDir ]; @@ -25,5 +26,18 @@ module.exports = function() { args.push.apply(args, rootIDs); args.push("--config", config.configFile); - exec(args.join(" "), done); + var child = spawn({ + cmd: "bin/jsx", + args: args + }, function(error, result, code) { + if (error) { + grunt.log.error(error); + done(false); + } else { + done(); + } + }); + + child.stdout.pipe(process.stdout); + child.stderr.pipe(process.stderr); }; diff --git a/grunt/tasks/npm.js b/grunt/tasks/npm.js index becc96a894..8fe6b7ad32 100644 --- a/grunt/tasks/npm.js +++ b/grunt/tasks/npm.js @@ -2,13 +2,11 @@ var assert = require("assert"); var path = require("path"); -var fs = require("fs"); var tmp = require("tmp"); var grunt = require("grunt"); var spawn = grunt.util.spawn; module.exports = function() { - var config = this.data; var done = this.async(); function run(cmd, args, opts, callback) { @@ -25,7 +23,8 @@ module.exports = function() { grunt.log.writeln("> " + cmd + " " + args.join(" ")); - var proc = spawn({ + // var proc = + spawn({ cmd: cmd, args: args, opts: opts diff --git a/grunt/tasks/phantom.js b/grunt/tasks/phantom.js index ff71f683c6..d5c66eb258 100644 --- a/grunt/tasks/phantom.js +++ b/grunt/tasks/phantom.js @@ -38,7 +38,7 @@ function run(config, done) { } args.push("--tests"); - var tests = grunt.file.expand({ + grunt.file.expand({ nonull: true, cwd: "src" }, config.tests || []).forEach(function(file) { diff --git a/package.json b/package.json index 0333bb02cd..7eb7e57801 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-tools", - "version": "0.3.2", + "version": "0.4.0a", "keywords": [ "react", "jsx", @@ -21,7 +21,7 @@ "bin/jsx", "build/modules/", "vendor/fbtransform/", - "vendor/woodchipper.js" + "vendor/constants.js" ], "main": "main.js", "bin": { @@ -36,9 +36,9 @@ }, "dependencies": { "base62": "~0.1.1", - "commoner": "~0.7.0", - "esprima": "git://github.com/facebook/esprima#fb-harmony", - "recast": "~0.4.5", + "commoner": "~0.8.0", + "esprima": "https://github.com/facebook/esprima/tarball/a3e0ea3979eb8d54d8bfade220c272903f928b1e", + "recast": "~0.4.8", "source-map": "~0.1.22" }, "devDependencies": { @@ -58,5 +58,8 @@ "tmp": "~0.0.18", "grunt-contrib-compress": "~0.5.1" }, + "engines": { + "node": ">=0.10.0" + }, "preferGlobal": true } diff --git a/src/core/React.js b/src/core/React.js index 1a9e61fbb8..7bdb3b39bd 100644 --- a/src/core/React.js +++ b/src/core/React.js @@ -31,7 +31,7 @@ ReactDefaultInjection.inject(); var React = { DOM: ReactDOM, - Props: ReactProps, + Props: ReactProps, initializeTouchEvents: function(shouldUseTouch) { ReactMount.useTouchEvents = shouldUseTouch; }, diff --git a/src/core/ReactComponent.js b/src/core/ReactComponent.js index 3353310b1f..e4f3ad4678 100644 --- a/src/core/ReactComponent.js +++ b/src/core/ReactComponent.js @@ -16,11 +16,13 @@ * @providesModule ReactComponent */ +/*jslint evil: true */ + "use strict"; -var ExecutionEnvironment = require('ExecutionEnvironment'); var ReactCurrentOwner = require('ReactCurrentOwner'); var ReactDOMIDOperations = require('ReactDOMIDOperations'); +var ReactID = require('ReactID'); var ReactMount = require('ReactMount'); var ReactOwner = require('ReactOwner'); var ReactReconcileTransaction = require('ReactReconcileTransaction'); @@ -35,6 +37,12 @@ var merge = require('merge'); */ var OWNER = '{owner}'; +/** + * Props key that determines if a component's key was already validated. + * @private + */ +var IS_KEY_VALIDATED = '{is.key.validated}'; + /** * Every React component is in one of these life cycles. */ @@ -55,52 +63,70 @@ var ComponentLifeCycle = keyMirror({ * This allows us to keep track of children between updates. */ -var CHILD_HAS_NO_IDENTITY = - 'You are passing a dynamic array of children. You should set the ' + - 'property "key" to a string that uniquely identifies each child.'; - -var HAS_WARNED = false; +var ownerHasWarned = {}; /** - * Helpers for flattening child arguments onto a new array or use an existing - * one. + * Warn if the component doesn't have an explicit key assigned to it. + * This component is in an array. The array could grow and shrink or be + * reordered. All children, that hasn't already been validated, are required to + * have a "key" property assigned to it. + * + * @internal + * @param {ReactComponent} component Component that requires a key. */ +function validateExplicitKey(component) { + if (component[IS_KEY_VALIDATED] || component.props.key != null) { + return; + } + component[IS_KEY_VALIDATED] = true; -function isEmptyChild(child) { - return child == null || typeof child === 'boolean'; + // We can't provide friendly warnings for top level components. + if (!ReactCurrentOwner.current) { + return; + } + + // Name of the component whose render method tried to pass children. + var currentName = ReactCurrentOwner.current.constructor.displayName; + if (ownerHasWarned.hasOwnProperty(currentName)) { + return; + } + ownerHasWarned[currentName] = true; + + var message = 'Each child in an array should have a unique "key" prop. ' + + 'Check the render method of ' + currentName + '.'; + if (!component.isOwnedBy(ReactCurrentOwner.current)) { + // Name of the component that originally created this child. + var childOwnerName = + component.props[OWNER] && component.props[OWNER].constructor.displayName; + + // Usually the current owner is the offender, but if it accepts + // children as a property, it may be the creator of the child that's + // responsible for assigning it a key. + message += ' It was passed a child from ' + childOwnerName + '.'; + } + + global.console && console.warn && console.warn(message); } -function assignKey(setKey, child, index) { - if (ReactComponent.isValidComponent(child)) { - var key = child.props.key; - if (__DEV__) { - if (!HAS_WARNED && !key) { - HAS_WARNED = true; - console && console.warn && console.warn(CHILD_HAS_NO_IDENTITY); +/** + * Ensure that every component either is passed in a static location or, if + * if it's passed in an array, has an explicit key property defined. + * + * @internal + * @param {*} component Statically passed child of any type. + * @return {boolean} + */ +function validateChildKeys(component) { + if (Array.isArray(component)) { + for (var i = 0; i < component.length; i++) { + var child = component[i]; + if (ReactComponent.isValidComponent(child)) { + validateExplicitKey(child); } } - child._key = (setKey ? setKey + ':' : '') + (key || ('' + index)); - } -} - -function tryToReuseArray(children) { - for (var i = 0; i < children.length; i++) { - var child = children[i]; - if (isEmptyChild(child)) return false; - assignKey('0', child, i); - } - return true; -} - -function appendNestedChildren(parentKey, sourceArray, targetArray) { - for (var i = 0; i < sourceArray.length; i++) { - var child = sourceArray[i]; - if (isEmptyChild(child)) continue; - assignKey(parentKey, child, i); - // TODO: Invalid components like strings could possibly need - // keys assigned to them here. Usually they're not stateful but - // CSS transitions and special events could make them stateful. - targetArray.push(child); + } else if (ReactComponent.isValidComponent(component)) { + // This component was passed in a valid location. + component[IS_KEY_VALIDATED] = true; } } @@ -144,6 +170,23 @@ var ReactComponent = { ); }, + /** + * Generate a key string that identifies a component within a set. + * + * @param {*} component A component that could contain a manual key. + * @param {number} index Index that is used if a manual key is not provided. + * @return {string} + * @internal + */ + getKey: function(component, index) { + if (component && component.props && component.props.key != null) { + // Explicit key + return '' + component.props.key; + } + // Implicit key determined by the index in the set + return '' + index; + }, + /** * @internal */ @@ -202,29 +245,16 @@ var ReactComponent = { /** * Returns the DOM node rendered by this component. * - * @return {?DOMElement} The root node of this component. + * @return {DOMElement} The root node of this component. * @final * @protected */ getDOMNode: function() { - invariant( - ExecutionEnvironment.canUseDOM, - 'getDOMNode(): The DOM is not supported in the current environment.' - ); invariant( this.isMounted(), 'getDOMNode(): A component must be mounted to have a DOM node.' ); - var rootNode = this._rootNode; - if (!rootNode) { - rootNode = document.getElementById(this._rootNodeID); - if (!rootNode) { - // TODO: Log the frequency that we reach this path. - rootNode = ReactMount.findReactRenderedDOMNodeSlow(this._rootNodeID); - } - this._rootNode = rootNode; - } - return rootNode; + return ReactID.getNode(this._rootNodeID); }, /** @@ -280,53 +310,23 @@ var ReactComponent = { // All components start unmounted. this._lifeCycleState = ComponentLifeCycle.UNMOUNTED; - // Children can be either an array or more than one argument - if (arguments.length < 2) { - return; - } - - if (arguments.length === 2) { - - // A single string or number child is treated as content, not an array. - var type = typeof children; - if (children == null || type === 'string' || type === 'number') { - this.props.children = children; - return; + // Children can be more than one argument + var childrenLength = arguments.length - 1; + if (childrenLength === 1) { + if (__DEV__) { + validateChildKeys(children); } - - // A single array can be reused if it's already flat - if (Array.isArray(children) && tryToReuseArray(children)) { - this.props.children = children; - return; - } - - } - - // Subsequent arguments are rolled into one child array. Array arguments - // are flattened onto it. This is inlined to avoid extra heap allocation. - var targetArray = null; - for (var i = 1; i < arguments.length; i++) { - var child = arguments[i]; - if (Array.isArray(child)) { - if (child.length === 0) continue; - - if (targetArray === null) targetArray = []; - appendNestedChildren('' + (i - 1), child, targetArray); - - } else if (!isEmptyChild(child)) { - - if (ReactComponent.isValidComponent(child)) { - // This is a static node and therefore safe to key by index. - // No warning necessary. - child._key = child.props.key || ('' + (i - 1)); + this.props.children = children; + } else if (childrenLength > 1) { + var childArray = Array(childrenLength); + for (var i = 0; i < childrenLength; i++) { + if (__DEV__) { + validateChildKeys(arguments[i + 1]); } - - if (targetArray === null) targetArray = []; - targetArray.push(child); - + childArray[i] = arguments[i + 1]; } + this.props.children = childArray; } - this.props.children = targetArray; }, /** @@ -376,7 +376,7 @@ var ReactComponent = { if (props.ref != null) { ReactOwner.removeComponentAsRefFrom(this, props.ref, props[OWNER]); } - this._rootNode = null; + ReactID.purgeID(this._rootNodeID); this._rootNodeID = null; this._lifeCycleState = ComponentLifeCycle.UNMOUNTED; }, @@ -447,6 +447,10 @@ var ReactComponent = { container, transaction, shouldReuseMarkup) { + invariant( + container && container.nodeType === 1, + 'mountComponentIntoNode(...): Target container is not a DOM element.' + ); var renderStart = Date.now(); var markup = this.mountComponent(rootID, transaction); ReactMount.totalInstantiationTime += (Date.now() - renderStart); @@ -500,6 +504,22 @@ var ReactComponent = { */ isOwnedBy: function(owner) { return this.props[OWNER] === owner; + }, + + /** + * Gets another component, that shares the same owner as this one, by ref. + * + * @param {string} ref of a sibling Component. + * @return {?ReactComponent} the actual sibling Component. + * @final + * @internal + */ + getSiblingByRef: function(ref) { + var owner = this.props[OWNER]; + if (!owner || !owner.refs) { + return null; + } + return owner.refs[ref]; } } diff --git a/src/core/ReactComponentWithPureRenderMixin.js b/src/core/ReactComponentWithPureRenderMixin.js index 7f46ed42fb..1ac4c23aad 100644 --- a/src/core/ReactComponentWithPureRenderMixin.js +++ b/src/core/ReactComponentWithPureRenderMixin.js @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. * -* @providesModule ReactComponentWithPureRender +* @providesModule ReactComponentWithPureRenderMixin */ "use strict"; @@ -27,9 +27,10 @@ * * Example: * - * var ReactComponentWithPureRender = require('ReactComponentWithPureRender'); + * var ReactComponentWithPureRender = + * require('ReactComponentWithPureRenderMixin'); * React.createClass({ - * mixins: [ReactComponentWithPureRender], + * mixins: [ReactComponentWithPureRenderMixin], * * render: function() { * return
    foo
    ; @@ -41,7 +42,7 @@ * differences. Only mixin to components which have simple props and state, or * use `forceUpdate()` when you know deep data structures have changed. */ -var ReactComponentWithPureRender = { +var ReactComponentWithPureRenderMixin = { shouldComponentUpdate: function(nextProps, nextState) { return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState); @@ -74,4 +75,4 @@ function shallowEqual(objA, objB) { return true; } -module.exports = ReactComponentWithPureRender; +module.exports = ReactComponentWithPureRenderMixin; diff --git a/src/core/ReactCompositeComponent.js b/src/core/ReactCompositeComponent.js index 78041c2a05..524c828c13 100644 --- a/src/core/ReactCompositeComponent.js +++ b/src/core/ReactCompositeComponent.js @@ -28,6 +28,19 @@ var keyMirror = require('keyMirror'); var merge = require('merge'); var mixInto = require('mixInto'); +function invokeWithWarning(func) { + return function() { + console && console.warn && console.warn( + 'You have invoked a method that is automatically bound, before the ' + + 'instance has been mounted. There is nothing conceptually wrong with ' + + 'this, but since this method will be replaced with a new version once' + + ' the component is mounted - you should be aware of the fact that ' + + 'this method will soon be replaced.' + ); + return func.apply(this, arguments); + }; +} + /** * Policies that describe methods in `ReactCompositeComponentInterface`. */ @@ -92,9 +105,8 @@ var ReactCompositeComponentInterface = { // ==== Definition methods ==== /** - * Invoked when the component is mounted and whenever new props are received. - * Values in the returned mapping will be set on `this.props` if that prop is - * not specified (i.e. using an `in` check). + * Invoked when the component is mounted. Values in the mapping will be set on + * `this.props` if that prop is not specified (i.e. using an `in` check). * * This method is invoked before `getInitialState` and therefore cannot rely * on `this.state` or use `this.setState`. @@ -283,6 +295,48 @@ var RESERVED_SPEC_KEYS = { } }; +function validateMethodOverride(proto, name) { + var specPolicy = ReactCompositeComponentInterface[name]; + + // Disallow overriding of base class methods unless explicitly allowed. + if (ReactCompositeComponentMixin.hasOwnProperty(name)) { + invariant( + specPolicy === SpecPolicy.OVERRIDE_BASE, + 'ReactCompositeComponentInterface: You are attempting to override ' + + '`%s` from your class specification. Ensure that your method names ' + + 'do not overlap with React methods.', + name + ); + } + + // Disallow defining methods more than once unless explicitly allowed. + if (proto.hasOwnProperty(name)) { + invariant( + specPolicy === SpecPolicy.DEFINE_MANY, + 'ReactCompositeComponentInterface: You are attempting to define ' + + '`%s` on your component more than once. This conflict may be due ' + + 'to a mixin.', + name + ); + } +} + + +function validateLifeCycleOnReplaceState(instance) { + var compositeLifeCycleState = instance._compositeLifeCycleState; + invariant( + instance.isMounted() || + compositeLifeCycleState === CompositeLifeCycle.MOUNTING, + 'replaceState(...): Can only update a mounted or mounting component.' + ); + invariant( + compositeLifeCycleState !== CompositeLifeCycle.RECEIVING_STATE && + compositeLifeCycleState !== CompositeLifeCycle.UNMOUNTING, + 'replaceState(...): Cannot update while unmounting component or during ' + + 'an existing state transition (such as within `render`).' + ); +} + /** * Custom version of `mixInto` which handles policy validation and reserved * specification keys when building `ReactCompositeComponent` classses. @@ -290,58 +344,47 @@ var RESERVED_SPEC_KEYS = { function mixSpecIntoComponent(Constructor, spec) { var proto = Constructor.prototype; for (var name in spec) { - if (!spec.hasOwnProperty(name)) { + var property = spec[name]; + if (!spec.hasOwnProperty(name) || !property) { continue; } - var property = spec[name]; - var specPolicy = ReactCompositeComponentInterface[name]; - - // Disallow overriding of base class methods unless explicitly allowed. - if (ReactCompositeComponentMixin.hasOwnProperty(name)) { - invariant( - specPolicy === SpecPolicy.OVERRIDE_BASE, - 'ReactCompositeComponentInterface: You are attempting to override ' + - '`%s` from your class specification. Ensure that your method names ' + - 'do not overlap with React methods.', - name - ); - } - - // Disallow using `React.autoBind` on internal methods. - if (specPolicy != null) { - invariant( - !property || !property.__reactAutoBind, - 'ReactCompositeComponentInterface: You are attempting to use ' + - '`React.autoBind` on `%s`, a method that is internal to React.' + - 'Internal methods are called with the component as the context.', - name - ); - } - - // Disallow defining methods more than once unless explicitly allowed. - if (proto.hasOwnProperty(name)) { - invariant( - specPolicy === SpecPolicy.DEFINE_MANY, - 'ReactCompositeComponentInterface: You are attempting to define ' + - '`%s` on your component more than once. This conflict may be due ' + - 'to a mixin.', - name - ); - } + validateMethodOverride(proto, name); if (RESERVED_SPEC_KEYS.hasOwnProperty(name)) { RESERVED_SPEC_KEYS[name](Constructor, property); - } else if (property && property.__reactAutoBind) { - if (!proto.__reactAutoBindMap) { - proto.__reactAutoBindMap = {}; - } - proto.__reactAutoBindMap[name] = property.__reactAutoBind; - } else if (proto.hasOwnProperty(name)) { - // For methods which are defined more than once, call the existing methods - // before calling the new property. - proto[name] = createChainedFunction(proto[name], property); } else { - proto[name] = property; + // Setup methods on prototype: + // The following member methods should not be automatically bound: + // 1. Expected ReactCompositeComponent methods (in the "interface"). + // 2. Overridden methods (that were mixed in). + var isCompositeComponentMethod = name in ReactCompositeComponentInterface; + var isInherited = name in proto; + var markedDontBind = property.__reactDontBind; + var isFunction = typeof property === 'function'; + var shouldAutoBind = + isFunction && + !isCompositeComponentMethod && + !isInherited && + !markedDontBind; + + if (shouldAutoBind) { + if (!proto.__reactAutoBindMap) { + proto.__reactAutoBindMap = {}; + } + proto.__reactAutoBindMap[name] = property; + proto[name] = property; + if (__DEV__) { + proto[name] = invokeWithWarning(property); + } + } else { + if (isInherited) { + // For methods which are defined more than once, call the existing + // methods before calling the new property. + proto[name] = createChainedFunction(proto[name], property); + } else { + proto[name] = property; + } + } } } } @@ -355,13 +398,9 @@ function mixSpecIntoComponent(Constructor, spec) { * @private */ function createChainedFunction(one, two) { - return function chainedFunction(a, b, c, d, e, tooMany) { - invariant( - typeof tooMany === 'undefined', - 'Chained function can only take a maximum of 5 arguments.' - ); - one.call(this, a, b, c, d, e); - two.call(this, a, b, c, d, e); + return function chainedFunction() { + one.apply(this, arguments); + two.apply(this, arguments); }; } @@ -370,7 +409,26 @@ function createChainedFunction(one, two) { * `this._compositeLifeCycleState` (which can be null). * * This is different from the life cycle state maintained by `ReactComponent` in - * `this._lifeCycleState`. + * `this._lifeCycleState`. The following diagram shows how the states overlap in + * time. There are times when the CompositeLifeCycle is null - at those times it + * is only meaningful to look at ComponentLifeCycle alone. + * + * Top Row: ReactComponent.ComponentLifeCycle + * Low Row: ReactComponent.CompositeLifeCycle + * + * +-------+------------------------------------------------------+--------+ + * | UN | MOUNTED | UN | + * |MOUNTED| | MOUNTED| + * +-------+------------------------------------------------------+--------+ + * | ^--------+ +------+ +------+ +------+ +--------^ | + * | | | | | | | | | | | | + * | 0--|MOUNTING|-0-|RECEIV|-0-|RECEIV|-0-|RECEIV|-0-| UN |--->0 | + * | | | |PROPS | | PROPS| | STATE| |MOUNTING| | + * | | | | | | | | | | | | + * | | | | | | | | | | | | + * | +--------+ +------+ +------+ +------+ +--------+ | + * | | | | + * +-------+------------------------------------------------------+--------+ */ var CompositeLifeCycle = keyMirror({ /** @@ -416,6 +474,17 @@ var ReactCompositeComponentMixin = { this._compositeLifeCycleState = null; }, + /** + * Checks whether or not this composite component is mounted. + * @return {boolean} True if mounted, false otherwise. + * @protected + * @final + */ + isMounted: function() { + return ReactComponent.Mixin.isMounted.call(this) && + this._compositeLifeCycleState !== CompositeLifeCycle.MOUNTING; + }, + /** * Initializes the component, renders markup, and registers event listeners. * @@ -427,11 +496,9 @@ var ReactCompositeComponentMixin = { */ mountComponent: function(rootID, transaction) { ReactComponent.Mixin.mountComponent.call(this, rootID, transaction); - - // Unset `this._lifeCycleState` until after this method is finished. - this._lifeCycleState = ReactComponent.LifeCycle.UNMOUNTED; this._compositeLifeCycleState = CompositeLifeCycle.MOUNTING; + this._defaultProps = this.getDefaultProps ? this.getDefaultProps() : null; this._processProps(this.props); if (this.__reactAutoBindMap) { @@ -455,15 +522,11 @@ var ReactCompositeComponentMixin = { // Done with mounting, `setState` will now trigger UI changes. this._compositeLifeCycleState = null; - this._lifeCycleState = ReactComponent.LifeCycle.MOUNTED; - - var html = this._renderedComponent.mountComponent(rootID, transaction); - + var markup = this._renderedComponent.mountComponent(rootID, transaction); if (this.componentDidMount) { transaction.getReactOnDOMReady().enqueue(this, this.componentDidMount); } - - return html; + return markup; }, /** @@ -479,6 +542,8 @@ var ReactCompositeComponentMixin = { } this._compositeLifeCycleState = null; + this._defaultProps = null; + ReactComponent.Mixin.unmountComponent.call(this); this._renderedComponent.unmountComponent(); this._renderedComponent = null; @@ -554,18 +619,7 @@ var ReactCompositeComponentMixin = { */ replaceState: function(completeState, callback) { var compositeLifeCycleState = this._compositeLifeCycleState; - invariant( - this.isMounted() || - compositeLifeCycleState === CompositeLifeCycle.MOUNTING, - 'replaceState(...): Can only update a mounted (or mounting) component.' - ); - invariant( - compositeLifeCycleState !== CompositeLifeCycle.RECEIVING_STATE && - compositeLifeCycleState !== CompositeLifeCycle.UNMOUNTING, - 'replaceState(...): Cannot update while unmounting component or during ' + - 'an existing state transition (such as within `render`).' - ); - + validateLifeCycleOnReplaceState.call(null, this); this._pendingState = completeState; // Do not trigger a state transition if we are in the middle of mounting or @@ -586,7 +640,6 @@ var ReactCompositeComponentMixin = { transaction ); ReactComponent.ReactReconcileTransaction.release(transaction); - this._compositeLifeCycleState = null; } @@ -603,12 +656,10 @@ var ReactCompositeComponentMixin = { */ _processProps: function(props) { var propName; - if (this.getDefaultProps) { - var defaultProps = this.getDefaultProps(); - for (propName in defaultProps) { - if (!(propName in props)) { - props[propName] = defaultProps[propName]; - } + var defaultProps = this._defaultProps; + for (propName in defaultProps) { + if (!(propName in props)) { + props[propName] = defaultProps[propName]; } } var propDeclarations = this.constructor.propDeclarations; @@ -721,8 +772,10 @@ var ReactCompositeComponentMixin = { forceUpdate: function(callback) { var compositeLifeCycleState = this._compositeLifeCycleState; invariant( - this.isMounted(), - 'forceUpdate(...): Can only force an update on mounted components.' + this.isMounted() || + compositeLifeCycleState === CompositeLifeCycle.MOUNTING, + 'forceUpdate(...): Can only force an update on mounted or mounting ' + + 'components.' ); invariant( compositeLifeCycleState !== CompositeLifeCycle.RECEIVING_STATE && @@ -786,27 +839,9 @@ var ReactCompositeComponentMixin = { */ _bindAutoBindMethod: function(method) { var component = this; - var hasWarned = false; - function autoBound(a, b, c, d, e, tooMany) { - invariant( - typeof tooMany === 'undefined', - 'React.autoBind(...): Methods can only take a maximum of 5 arguments.' - ); - if (component._lifeCycleState === ReactComponent.LifeCycle.MOUNTED) { - return method.call(component, a, b, c, d, e); - } else if (!hasWarned) { - hasWarned = true; - if (__DEV__) { - console.warn( - 'React.autoBind(...): Attempted to invoke an auto-bound method ' + - 'on an unmounted instance of `%s`. You either have a memory leak ' + - 'or an event handler that is being run after unmounting.', - component.constructor.displayName || 'ReactCompositeComponent' - ); - } - } - } - return autoBound; + return function() { + return method.apply(component, arguments); + }; } }; @@ -859,33 +894,16 @@ var ReactCompositeComponent = { }, /** - * Marks the provided method to be automatically bound to the component. - * This means the method's context will always be the component. - * - * React.createClass({ - * handleClick: React.autoBind(function() { - * this.setState({jumping: true}); - * }), - * render: function() { - * return Jump; - * } - * }); + * TODO: Delete this when all callers have been updated to rely on this + * behavior being the default. * + * Backwards compatible stub for what is now the default behavior. * @param {function} method Method to be bound. * @public */ autoBind: function(method) { - function unbound() { - invariant( - false, - 'React.autoBind(...): Attempted to invoke an auto-bound method that ' + - 'was not correctly defined on the class specification.' - ); - } - unbound.__reactAutoBind = method; - return unbound; + return method; } - }; module.exports = ReactCompositeComponent; diff --git a/src/core/ReactCurrentOwner.js b/src/core/ReactCurrentOwner.js index 819071ff75..ad137cdf2e 100644 --- a/src/core/ReactCurrentOwner.js +++ b/src/core/ReactCurrentOwner.js @@ -23,6 +23,8 @@ * * The current owner is the component who should own any components that are * currently being constructed. + * + * The depth indicate how many composite components are above this render level. */ var ReactCurrentOwner = { diff --git a/src/core/ReactDOM.js b/src/core/ReactDOM.js index 2fe78e99a7..2a9749c9be 100644 --- a/src/core/ReactDOM.js +++ b/src/core/ReactDOM.js @@ -14,7 +14,7 @@ * limitations under the License. * * @providesModule ReactDOM - * @typechecks + * @typechecks static-only */ "use strict"; @@ -41,15 +41,16 @@ var objMapKeyVal = require('objMapKeyVal'); */ function createDOMComponentClass(tag, omitClose) { var Constructor = function() {}; - Constructor.prototype = new ReactNativeComponent(tag, omitClose); Constructor.prototype.constructor = Constructor; - return function(props, children) { + var ConvenienceConstructor = function(props, children) { var instance = new Constructor(); instance.construct.apply(instance, arguments); return instance; }; + ConvenienceConstructor.componentConstructor = Constructor; + return ConvenienceConstructor; } /** @@ -79,8 +80,7 @@ var ReactDOM = objMapKeyVal({ embed: true, fieldset: false, footer: false, - // Danger: this gets monkeypatched! See ReactDOMForm for more info. - form: false, + form: false, // NOTE: Injected, see `ReactDOMForm`. h1: false, h2: false, h3: false, @@ -115,7 +115,7 @@ var ReactDOM = objMapKeyVal({ table: false, tbody: false, td: false, - textarea: false, + textarea: false, // NOTE: Injected, see `ReactDOMTextarea`. tfoot: false, th: false, thead: false, diff --git a/src/core/ReactDOMIDOperations.js b/src/core/ReactDOMIDOperations.js index b51ffbd019..1b110cc248 100644 --- a/src/core/ReactDOMIDOperations.js +++ b/src/core/ReactDOMIDOperations.js @@ -14,7 +14,7 @@ * limitations under the License. * * @providesModule ReactDOMIDOperations - * @typechecks + * @typechecks static-only */ /*jslint evil: true */ @@ -24,7 +24,7 @@ var CSSPropertyOperations = require('CSSPropertyOperations'); var DOMChildrenOperations = require('DOMChildrenOperations'); var DOMPropertyOperations = require('DOMPropertyOperations'); -var ReactDOMNodeCache = require('ReactDOMNodeCache'); +var ReactID = require('ReactID'); var getTextContentAccessor = require('getTextContentAccessor'); var invariant = require('invariant'); @@ -36,7 +36,6 @@ var invariant = require('invariant'); * @private */ var INVALID_PROPERTY_ERRORS = { - content: '`content` must be set using `updateTextContentByID()`.', dangerouslySetInnerHTML: '`dangerouslySetInnerHTML` must be set using `updateInnerHTMLByID()`.', style: '`style` must be set using `updateStylesByID()`.' @@ -66,7 +65,7 @@ var ReactDOMIDOperations = { * @internal */ updatePropertyByID: function(id, name, value) { - var node = ReactDOMNodeCache.getCachedNodeByID(id); + var node = ReactID.getNode(id); invariant( !INVALID_PROPERTY_ERRORS.hasOwnProperty(name), 'updatePropertyByID(...): %s', @@ -84,7 +83,7 @@ var ReactDOMIDOperations = { * @internal */ deletePropertyByID: function(id, name, value) { - var node = ReactDOMNodeCache.getCachedNodeByID(id); + var node = ReactID.getNode(id); invariant( !INVALID_PROPERTY_ERRORS.hasOwnProperty(name), 'updatePropertyByID(...): %s', @@ -121,7 +120,7 @@ var ReactDOMIDOperations = { * @internal */ updateStylesByID: function(id, styles) { - var node = ReactDOMNodeCache.getCachedNodeByID(id); + var node = ReactID.getNode(id); CSSPropertyOperations.setValueForStyles(node, styles); }, @@ -133,7 +132,7 @@ var ReactDOMIDOperations = { * @internal */ updateInnerHTMLByID: function(id, html) { - var node = ReactDOMNodeCache.getCachedNodeByID(id); + var node = ReactID.getNode(id); // HACK: IE8- normalize whitespace in innerHTML, removing leading spaces. // @see quirksmode.org/bugreports/archives/2004/11/innerhtml_and_t.html node.innerHTML = (html && html.__html || '').replace(/^ /g, ' '); @@ -147,7 +146,7 @@ var ReactDOMIDOperations = { * @internal */ updateTextContentByID: function(id, content) { - var node = ReactDOMNodeCache.getCachedNodeByID(id); + var node = ReactID.getNode(id); node[textContentAccessor] = content; }, @@ -160,9 +159,9 @@ var ReactDOMIDOperations = { * @see {Danger.dangerouslyReplaceNodeWithMarkup} */ dangerouslyReplaceNodeWithMarkupByID: function(id, markup) { - var node = ReactDOMNodeCache.getCachedNodeByID(id); + var node = ReactID.getNode(id); DOMChildrenOperations.dangerouslyReplaceNodeWithMarkup(node, markup); - ReactDOMNodeCache.purgeEntireCache(); + ReactID.purgeEntireCache(); }, /** @@ -170,13 +169,13 @@ var ReactDOMIDOperations = { * Detect if any elements were removed instead of blindly purging. */ manageChildrenByParentID: function(parentID, domOperations) { - var parent = ReactDOMNodeCache.getCachedNodeByID(parentID); + var parent = ReactID.getNode(parentID); DOMChildrenOperations.manageChildren(parent, domOperations); - ReactDOMNodeCache.purgeEntireCache(); + ReactID.purgeEntireCache(); }, setTextNodeValueAtIndexByParentID: function(parentID, index, value) { - var parent = ReactDOMNodeCache.getCachedNodeByID(parentID); + var parent = ReactID.getNode(parentID); DOMChildrenOperations.setTextNodeValueAtIndex(parent, index, value); } diff --git a/src/core/ReactDOMNodeCache.js b/src/core/ReactDOMNodeCache.js deleted file mode 100644 index 385ad5a27d..0000000000 --- a/src/core/ReactDOMNodeCache.js +++ /dev/null @@ -1,52 +0,0 @@ -/** - * 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 ReactDOMNodeCache - */ - -"use strict"; - -var ReactMount = require('ReactMount'); - -var nodeCache = {}; - -/** - * DOM node cache only intended for use by React. Placed into a shared module so - * that both read and write utilities may benefit from a shared cache. - */ -var ReactDOMNodeCache = { - /** - * Releases fast id lookups (node/style cache). This implementation is - * aggressive with purging because the bookkeeping associated with doing fine - * grained deleted from the cache may outweight the benefits of the cache. The - * heuristic that should be used to purge is 'any time anything is deleted'. - * Typically this means that a large amount of content is being replaced and - * several elements would need purging regardless. It's also a time when an - * application is likely not in the middle of a "smooth operation" (such as - * animating/scrolling). - */ - purgeEntireCache: function() { - nodeCache = {}; - return nodeCache; - }, - getCachedNodeByID: function(id) { - return nodeCache[id] || - (nodeCache[id] = - document.getElementById(id) || - ReactMount.findReactRenderedDOMNodeSlow(id)); - } -}; - -module.exports = ReactDOMNodeCache; diff --git a/src/core/ReactDefaultInjection.js b/src/core/ReactDefaultInjection.js index 27fb2afba0..33577220f2 100644 --- a/src/core/ReactDefaultInjection.js +++ b/src/core/ReactDefaultInjection.js @@ -20,10 +20,11 @@ var ReactDOM = require('ReactDOM'); var ReactDOMForm = require('ReactDOMForm'); +var ReactDOMTextarea = require('ReactDOMTextarea'); var DefaultEventPluginOrder = require('DefaultEventPluginOrder'); var EnterLeaveEventPlugin = require('EnterLeaveEventPlugin'); -var TextChangeEventPlugin = require('TextChangeEventPlugin'); +var ChangeEventPlugin = require('ChangeEventPlugin'); var EventPluginHub = require('EventPluginHub'); var ReactInstanceHandles = require('ReactInstanceHandles'); var SimpleEventPlugin = require('SimpleEventPlugin'); @@ -42,17 +43,19 @@ function inject() { EventPluginHub.injection.injectEventPluginsByName({ 'SimpleEventPlugin': SimpleEventPlugin, 'EnterLeaveEventPlugin': EnterLeaveEventPlugin, - 'TextChangeEventPlugin': TextChangeEventPlugin + 'ChangeEventPlugin': ChangeEventPlugin }); - /* - * This is a bit of a hack. We need to override the
    element - * to be a composite component because IE8 does not bubble or capture - * submit to the top level. In order to make this work with our - * dependency graph we need to inject it here. + /** + * This is a bit of a hack. We need to override the element to be a + * composite component because IE8 does not bubble or capture submit to the + * top level. In order to make this work with our dependency graph we need to + * inject it here. */ ReactDOM.injection.injectComponentClasses({ - form: ReactDOMForm + form: ReactDOMForm, + // TODO: Inject `ReactDOMInput`. + textarea: ReactDOMTextarea }); } diff --git a/src/core/ReactDoNotBindDeprecated.js b/src/core/ReactDoNotBindDeprecated.js new file mode 100644 index 0000000000..11b6c00166 --- /dev/null +++ b/src/core/ReactDoNotBindDeprecated.js @@ -0,0 +1,48 @@ +/** + * 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 ReactDoNotBindDeprecated + */ + +var ReactDoNotBindDeprecated = { + /** + * Marks the method for not being automatically bound on component mounting. A + * couple of reasons you might want to use this: + * + * - Automatically supporting the previous behavior in components that were + * built with previous versions of React. + * - Tuning performance, by avoiding binding on initial render for methods + * that are always invoked while being preceded by `this.`. Such binds are + * unnecessary. + * + * React.createClass({ + * handleClick: ReactDoNotBindDeprecated.doNotBind(function() { + * alert(this.setState); // undefined! + * }), + * render: function() { + * return Jump; + * } + * }); + * + * @param {function} method Method to avoid automatically binding. + * @public + */ + doNotBind: function(method) { + method.__reactDontBind = true; // Mutating + return method; + } +}; + +module.exports = ReactDoNotBindDeprecated; diff --git a/src/core/ReactEventEmitter.js b/src/core/ReactEventEmitter.js index ed552fc4d9..aa6a81e236 100644 --- a/src/core/ReactEventEmitter.js +++ b/src/core/ReactEventEmitter.js @@ -14,16 +14,16 @@ * limitations under the License. * * @providesModule ReactEventEmitter - * @typechecks + * @typechecks static-only */ "use strict"; -var BrowserEnv = require('BrowserEnv'); var EventConstants = require('EventConstants'); var EventListener = require('EventListener'); var EventPluginHub = require('EventPluginHub'); var ExecutionEnvironment = require('ExecutionEnvironment'); +var ViewportMetrics = require('ViewportMetrics'); var invariant = require('invariant'); var isEventSupported = require('isEventSupported'); @@ -126,7 +126,7 @@ function trapCapturedEvent(topLevelType, handlerBaseName, element) { * @see http://www.quirksmode.org/dom/events/scroll.html */ function registerScrollValueMonitoring() { - var refresh = BrowserEnv.refreshAuthoritativeScrollValues; + var refresh = ViewportMetrics.refreshScrollValues; EventListener.listen(window, 'scroll', refresh); EventListener.listen(window, 'resize', refresh); } diff --git a/src/core/ReactEventTopLevelCallback.js b/src/core/ReactEventTopLevelCallback.js index fde97acc15..215d23da23 100644 --- a/src/core/ReactEventTopLevelCallback.js +++ b/src/core/ReactEventTopLevelCallback.js @@ -14,16 +14,16 @@ * limitations under the License. * * @providesModule ReactEventTopLevelCallback - * @typechecks + * @typechecks static-only */ "use strict"; var ExecutionEnvironment = require('ExecutionEnvironment'); var ReactEventEmitter = require('ReactEventEmitter'); +var ReactID = require('ReactID'); var ReactInstanceHandles = require('ReactInstanceHandles'); -var getDOMNodeID = require('getDOMNodeID'); var getEventTarget = require('getEventTarget'); /** @@ -76,7 +76,7 @@ var ReactEventTopLevelCallback = { var topLevelTarget = ReactInstanceHandles.getFirstReactDOM( getEventTarget(nativeEvent) ) || ExecutionEnvironment.global; - var topLevelTargetID = getDOMNodeID(topLevelTarget) || ''; + var topLevelTargetID = ReactID.getID(topLevelTarget) || ''; ReactEventEmitter.handleTopLevel( topLevelType, topLevelTarget, diff --git a/src/core/ReactID.js b/src/core/ReactID.js new file mode 100644 index 0000000000..d677b14e8a --- /dev/null +++ b/src/core/ReactID.js @@ -0,0 +1,173 @@ +/** + * 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 ReactID + * @typechecks static-only + */ + +"use strict"; + +var invariant = require('invariant'); +var ReactMount = require('ReactMount'); +var ATTR_NAME = 'data-reactid'; +var nodeCache = {}; + +/** + * Accessing node[ATTR_NAME] or calling getAttribute(ATTR_NAME) on a form + * element can return its control whose name or ID equals ATTR_NAME. All + * DOM nodes support `getAttributeNode` but this can also get called on + * other objects so just return '' if we're given something other than a + * DOM node (such as window). + * + * @param {?DOMElement|DOMWindow|DOMDocument|DOMTextNode} node DOM node. + * @return {string} ID of the supplied `domNode`. + */ +function getID(node) { + var id = internalGetID(node); + if (id) { + if (nodeCache.hasOwnProperty(id)) { + var cached = nodeCache[id]; + if (cached !== node) { + invariant( + !isValid(cached, id), + 'ReactID: Two valid but unequal nodes with the same `%s`: %s', + ATTR_NAME, id + ); + + nodeCache[id] = node; + } + } else { + nodeCache[id] = node; + } + } + + return id; +} + +function internalGetID(node) { + if (node && node.getAttributeNode) { + var attributeNode = node.getAttributeNode(ATTR_NAME); + if (attributeNode) { + return attributeNode.value || ''; + } + } + return ''; +} + +/** + * Sets the React-specific ID of the given node. + * + * @param {DOMElement} node The DOM node whose ID will be set. + * @param {string} id The value of the ID attribute. + */ +function setID(node, id) { + var oldID = internalGetID(node); + if (oldID !== id) { + delete nodeCache[oldID]; + } + node.setAttribute(ATTR_NAME, id); + nodeCache[id] = node; +} + +/** + * Finds the node with the supplied React-generated DOM ID. + * + * @param {string} id A React-generated DOM ID. + * @return {DOMElement} DOM node with the suppled `id`. + * @internal + */ +function getNode(id) { + if (!nodeCache.hasOwnProperty(id) || !isValid(nodeCache[id], id)) { + nodeCache[id] = ReactMount.findReactNodeByID(id); + } + return nodeCache[id]; +} + +/** + * A node is "valid" if it is contained by a currently mounted container. + * + * This means that the node does not have to be contained by a document in + * order to be considered valid. + * + * @param {?DOMElement} node The candidate DOM node. + * @param {string} id The expected ID of the node. + * @return {boolean} Whether the node is contained by a mounted container. + */ +function isValid(node, id) { + if (node) { + invariant( + internalGetID(node) === id, + 'ReactID: Unexpected modification of `%s`', + ATTR_NAME + ); + + var container = ReactMount.findReactContainerForID(id); + if (container && contains(container, node)) { + return true; + } + } + + return false; +} + +function contains(ancestor, descendant) { + if (ancestor.contains) { + // Supported natively in virtually all browsers, but not in jsdom. + return ancestor.contains(descendant); + } + + if (descendant === ancestor) { + return true; + } + + if (descendant.nodeType === 3) { + // If descendant is a text node, start from descendant.parentNode + // instead, so that we can assume all ancestors worth considering are + // element nodes with nodeType === 1. + descendant = descendant.parentNode; + } + + while (descendant && descendant.nodeType === 1) { + if (descendant === ancestor) { + return true; + } + descendant = descendant.parentNode; + } + + return false; +} + +/** + * Causes the cache to forget about one React-specific ID. + * + * @param {string} id The ID to forget. + */ +function purgeID(id) { + delete nodeCache[id]; +} + +/** + * Clears the entire cache. + */ +function purgeEntireCache() { + nodeCache = {}; +} + +exports.ATTR_NAME = ATTR_NAME; +exports.getID = getID; +exports.setID = setID; +exports.getNode = getNode; +exports.purgeID = purgeID; +exports.purgeEntireCache = purgeEntireCache; diff --git a/src/core/ReactInputSelection.js b/src/core/ReactInputSelection.js index 2abd7ab6f0..056db55119 100644 --- a/src/core/ReactInputSelection.js +++ b/src/core/ReactInputSelection.js @@ -27,6 +27,10 @@ function getActiveElement() { } } +function isInDocument(node) { + return document.documentElement.contains(node); +} + /** * @ReactInputSelection: React input selection module. Based on Selection.js, * but modified to be suitable for react and has a couple of bug fixes (doesn't @@ -64,7 +68,7 @@ var ReactInputSelection = { var priorFocusedElem = priorSelectionInformation.focusedElem; var priorSelectionRange = priorSelectionInformation.selectionRange; if (curFocusedElem !== priorFocusedElem && - document.getElementById(priorFocusedElem.id)) { + isInDocument(priorFocusedElem)) { if (ReactInputSelection.hasSelectionCapabilities(priorFocusedElem)) { ReactInputSelection.setSelection( priorFocusedElem, diff --git a/src/core/ReactInstanceHandles.js b/src/core/ReactInstanceHandles.js index 8f26212864..02ea765e0a 100644 --- a/src/core/ReactInstanceHandles.js +++ b/src/core/ReactInstanceHandles.js @@ -14,12 +14,13 @@ * limitations under the License. * * @providesModule ReactInstanceHandles - * @typechecks + * @typechecks static-only */ "use strict"; -var getDOMNodeID = require('getDOMNodeID'); +var ReactID = require('ReactID'); + var invariant = require('invariant'); var SEPARATOR = '.'; @@ -47,7 +48,7 @@ var GLOBAL_MOUNT_POINT_MAX = 9999999; * @internal */ function getReactRootIDString(index) { - return '.reactRoot[' + index + ']'; + return SEPARATOR + 'r[' + index.toString(36) + ']'; } /** @@ -250,7 +251,7 @@ var ReactInstanceHandles = { // Not a DOMElement, therefore not a React component return false; } - var id = getDOMNodeID(node); + var id = ReactID.getID(node); return id ? id.charAt(0) === SEPARATOR : false; }, @@ -279,20 +280,34 @@ var ReactInstanceHandles = { * * @param {DOMEventTarget} ancestorNode Search from this root. * @pararm {string} id ID of the DOM representation of the component. - * @return {?DOMEventTarget} DOM node with the supplied `id`, if one exists. + * @return {DOMEventTarget} DOM node with the supplied `id`. * @internal */ findComponentRoot: function(ancestorNode, id) { var child = ancestorNode.firstChild; while (child) { - if (id === child.id) { - return child; - } else if (isAncestorIDOf(child.id, id)) { - return ReactInstanceHandles.findComponentRoot(child, id); + var childID = ReactID.getID(child); + if (childID) { + if (id === childID) { + return child; + } else if (isAncestorIDOf(childID, id)) { + return ReactInstanceHandles.findComponentRoot(child, id); + } } child = child.nextSibling; } - // Effectively: return null; + global.console && console.error && console.error( + 'Error while invoking `findComponentRoot` with the following ' + + 'ancestor node:', + ancestorNode + ); + invariant( + false, + 'findComponentRoot(..., %s): Unable to find element. This probably ' + + 'means the DOM was unexpectedly mutated (e.g. by the browser).', + id, + ReactID.getID(ancestorNode) + ); }, /** @@ -304,7 +319,7 @@ var ReactInstanceHandles = { * @internal */ getReactRootIDFromNodeID: function(id) { - var regexResult = /\.reactRoot\[[^\]]+\]/.exec(id); + var regexResult = /\.r\[[^\]]+\]/.exec(id); return regexResult && regexResult[0]; }, diff --git a/src/core/ReactMount.js b/src/core/ReactMount.js index 6eaf42cfd6..14f93055b9 100644 --- a/src/core/ReactMount.js +++ b/src/core/ReactMount.js @@ -21,6 +21,7 @@ var ReactEventEmitter = require('ReactEventEmitter'); var ReactInstanceHandles = require('ReactInstanceHandles'); var ReactEventTopLevelCallback = require('ReactEventTopLevelCallback'); +var ReactID = require('ReactID'); var $ = require('$'); @@ -35,7 +36,7 @@ var containersByReactRootID = {}; * @return {?*} DOM element that may have the reactRoot ID, or null. */ function getReactRootElementInContainer(container) { - return container.firstChild; + return container && container.firstChild; } /** @@ -44,7 +45,7 @@ function getReactRootElementInContainer(container) { */ function getReactRootID(container) { var rootElement = getReactRootElementInContainer(container); - return rootElement && rootElement.id; + return rootElement && ReactID.getID(rootElement); } /** @@ -54,9 +55,9 @@ function getReactRootID(container) { * * ReactMount.renderComponent(component, $('container')); * - *
    <-- Supplied `container`. - *
    <-- Rendered reactRoot of React component. - * // ... + *
    <-- Supplied `container`. + *
    <-- Rendered reactRoot of React + * // ... component. *
    *
    * @@ -268,13 +269,12 @@ var ReactMount = { }, /** - * Given the ID of a DOM node rendered by a React component, finds the root - * DOM node of the React component. + * Finds an element rendered by React with the supplied ID. * * @param {string} id ID of a DOM node in the React component. - * @return {?DOMElement} Root DOM node of the React component. + * @return {DOMElement} Root DOM node of the React component. */ - findReactRenderedDOMNodeSlow: function(id) { + findReactNodeByID: function(id) { var reactRoot = ReactMount.findReactContainerForID(id); return ReactInstanceHandles.findComponentRoot(reactRoot, id); } diff --git a/src/core/ReactNativeComponent.js b/src/core/ReactNativeComponent.js index 07371b2c8f..ef073dff68 100644 --- a/src/core/ReactNativeComponent.js +++ b/src/core/ReactNativeComponent.js @@ -14,7 +14,7 @@ * limitations under the License. * * @providesModule ReactNativeComponent - * @typechecks + * @typechecks static-only */ "use strict"; @@ -24,6 +24,7 @@ var DOMPropertyOperations = require('DOMPropertyOperations'); var ReactComponent = require('ReactComponent'); var ReactEventEmitter = require('ReactEventEmitter'); var ReactMultiChild = require('ReactMultiChild'); +var ReactID = require('ReactID'); var escapeTextForBrowser = require('escapeTextForBrowser'); var flattenChildren = require('flattenChildren'); @@ -39,7 +40,6 @@ var registrationNames = ReactEventEmitter.registrationNames; // For quickly matching children type, to test if can be treated as content. var CONTENT_TYPES = {'string': true, 'number': true}; -var CONTENT = keyOf({content: null}); var DANGEROUSLY_SET_INNER_HTML = keyOf({dangerouslySetInnerHTML: null}); var STYLE = keyOf({style: null}); @@ -50,14 +50,10 @@ function assertValidProps(props) { if (!props) { return; } - // Note the use of `!=` which checks for null or undefined. - var hasChildren = props.children != null ? 1 : 0; - var hasContent = props.content != null ? 1 : 0; - var hasInnerHTML = props.dangerouslySetInnerHTML != null ? 1 : 0; + // Note the use of `==` which checks for null or undefined. invariant( - hasChildren + hasContent + hasInnerHTML <= 1, - 'Can only set one of `children`, `props.content`, or ' + - '`props.dangerouslySetInnerHTML`.' + props.children == null || props.dangerouslySetInnerHTML == null, + 'Can only set one of `children` or `props.dangerouslySetInnerHTML`.' ); invariant( props.style == null || typeof props.style === 'object', @@ -138,7 +134,8 @@ ReactNativeComponent.Mixin = { } } - return ret + ' id="' + this._rootNodeID + '">'; + var escapedID = escapeTextForBrowser(this._rootNodeID); + return ret + ' ' + ReactID.ATTR_NAME + '="' + escapedID + '">'; }, /** @@ -156,7 +153,7 @@ ReactNativeComponent.Mixin = { return innerHTML.__html; } } else { - var contentToUse = this.props.content != null ? this.props.content : + var contentToUse = CONTENT_TYPES[typeof this.props.children] ? this.props.children : null; var childrenToUse = contentToUse != null ? null : this.props.children; if (contentToUse != null) { @@ -219,8 +216,8 @@ ReactNativeComponent.Mixin = { styleUpdates[styleName] = ''; } } - } else if (propKey === DANGEROUSLY_SET_INNER_HTML || - propKey === CONTENT) { + } else if (propKey === DANGEROUSLY_SET_INNER_HTML) { + // http://jsperf.com/emptying-speed ReactComponent.DOMIDOperations.updateTextContentByID( this._rootNodeID, '' @@ -274,11 +271,6 @@ ReactNativeComponent.Mixin = { nextProp ); } - } else if (propKey === CONTENT) { - ReactComponent.DOMIDOperations.updateTextContentByID( - this._rootNodeID, - '' + nextProp - ); } else if (registrationNames[propKey]) { putListener(this._rootNodeID, propKey, nextProp); } else { @@ -305,17 +297,10 @@ ReactNativeComponent.Mixin = { * @param {ReactReconcileTransaction} transaction */ _updateDOMChildren: function(nextProps, transaction) { - var thisPropsContentType = typeof this.props.content; - var thisPropsContentEmpty = - this.props.content == null || thisPropsContentType === 'boolean'; - var nextPropsContentType = typeof nextProps.content; - var nextPropsContentEmpty = - nextProps.content == null || nextPropsContentType === 'boolean'; - - var lastUsedContent = !thisPropsContentEmpty ? this.props.content : + var lastUsedContent = CONTENT_TYPES[typeof this.props.children] ? this.props.children : null; - var contentToUse = !nextPropsContentEmpty ? nextProps.content : + var contentToUse = CONTENT_TYPES[typeof nextProps.children] ? nextProps.children : null; // Note the use of `!=` which checks for null or undefined. diff --git a/src/core/ReactPropTransferer.js b/src/core/ReactPropTransferer.js index c1f697af4e..691ed58fc8 100644 --- a/src/core/ReactPropTransferer.js +++ b/src/core/ReactPropTransferer.js @@ -44,13 +44,17 @@ function createTransferStrategy(mergeStrategy) { */ var TransferStrategies = { /** - * Never transfer the `ref` prop. + * Never transfer `children`. */ - ref: emptyFunction, + children: emptyFunction, /** * Transfer the `className` prop by merging them. */ className: createTransferStrategy(joinClasses), + /** + * Never transfer the `ref` prop. + */ + ref: emptyFunction, /** * Transfer the `style` prop (which is an object) by merging them. */ diff --git a/src/core/ReactReconcileTransaction.js b/src/core/ReactReconcileTransaction.js index a44214dcb8..904df5e719 100644 --- a/src/core/ReactReconcileTransaction.js +++ b/src/core/ReactReconcileTransaction.js @@ -14,7 +14,7 @@ * limitations under the License. * * @providesModule ReactReconcileTransaction - * @typechecks + * @typechecks static-only */ "use strict"; diff --git a/src/core/ReactTextComponent.js b/src/core/ReactTextComponent.js index 4b7aa495ac..1d0af59858 100644 --- a/src/core/ReactTextComponent.js +++ b/src/core/ReactTextComponent.js @@ -14,12 +14,13 @@ * limitations under the License. * * @providesModule ReactTextComponent - * @typechecks + * @typechecks static-only */ "use strict"; var ReactComponent = require('ReactComponent'); +var ReactID = require('ReactID'); var escapeTextForBrowser = require('escapeTextForBrowser'); var mixInto = require('mixInto'); @@ -57,7 +58,7 @@ mixInto(ReactTextComponent, { mountComponent: function(rootID) { ReactComponent.Mixin.mountComponent.call(this, rootID); return ( - '' + + '' + escapeTextForBrowser(this.props.text) + '' ); diff --git a/src/core/__tests__/ReactBind-test.js b/src/core/__tests__/ReactBind-test.js index 2fa5f77c59..47e77bb9d8 100644 --- a/src/core/__tests__/ReactBind-test.js +++ b/src/core/__tests__/ReactBind-test.js @@ -21,9 +21,11 @@ var mocks = require('mocks'); var React = require('React'); +var ReactDoNotBindDeprecated = require('ReactDoNotBindDeprecated'); var ReactTestUtils = require('ReactTestUtils'); var reactComponentExpect = require('reactComponentExpect'); +// TODO: Test render and all stock methods. describe('React.autoBind', function() { it('Holds reference to instance', function() { @@ -31,16 +33,20 @@ describe('React.autoBind', function() { var mouseDidEnter = mocks.getMockFunction(); var mouseDidLeave = mocks.getMockFunction(); var mouseDidClick = mocks.getMockFunction(); - var didBadIdea = mocks.getMockFunction(); var TestBindComponent = React.createClass({ - onMouseEnter: mouseDidEnter, - onMouseLeave: mouseDidLeave, - onClick: React.autoBind(mouseDidClick), + getInitialState: function() { + return {something: 'hi'}; + }, + onMouseEnter: ReactDoNotBindDeprecated.doNotBind(mouseDidEnter), + onMouseLeave: ReactDoNotBindDeprecated.doNotBind(mouseDidLeave), + onClick: mouseDidClick, - // autoBind needs to be on the top-level spec. + // auto binding only occurs on top level functions in class defs. badIdeas: { - badBind: React.autoBind(didBadIdea) + badBind: function() { + this.state.something; + } }, render: function() { @@ -48,7 +54,8 @@ describe('React.autoBind', function() {
    + onClick={this.onClick} + /> ); } }); @@ -103,7 +110,7 @@ describe('React.autoBind', function() { var mouseDidClick = mocks.getMockFunction(); var TestMixin = { - onClick: React.autoBind(mouseDidClick) + onClick: mouseDidClick }; var TestBindComponent = React.createClass({ diff --git a/src/core/__tests__/ReactComponent-test.js b/src/core/__tests__/ReactComponent-test.js index a753ce0c89..af242c350d 100644 --- a/src/core/__tests__/ReactComponent-test.js +++ b/src/core/__tests__/ReactComponent-test.js @@ -31,6 +31,24 @@ describe('ReactComponent', function() { reactComponentExpect = require('reactComponentExpect'); }); + it('should throw on invalid render targets', function() { + var container = document.createElement('div'); + // jQuery objects are basically arrays; people often pass them in by mistake + expect(function() { + React.renderComponent(
    , [container]); + }).toThrow( + 'Invariant Violation: mountComponentIntoNode(...): Target container is ' + + 'not a DOM element.' + ); + + expect(function() { + React.renderComponent(
    , null); + }).toThrow( + 'Invariant Violation: mountComponentIntoNode(...): Target container is ' + + 'not a DOM element.' + ); + }); + it('should throw when supplying a ref outside of render method', function() { var instance =
    ; expect(function() { @@ -95,4 +113,23 @@ describe('ReactComponent', function() { ReactTestUtils.renderIntoDocument(instance); }); + it('should correctly determine if a component is mounted', function() { + var Component = React.createClass({ + componentWillMount: function() { + expect(this.isMounted()).toBeFalsy(); + }, + componentDidMount: function() { + expect(this.isMounted()).toBeTruthy(); + }, + render: function() { + return
    ; + } + }); + + var instance = ; + + expect(instance.isMounted()).toBeFalsy(); + ReactTestUtils.renderIntoDocument(instance); + expect(instance.isMounted()).toBeTruthy(); + }); }); diff --git a/src/core/__tests__/ReactComponentFlattenChildren-test.js b/src/core/__tests__/ReactComponentFlattenChildren-test.js deleted file mode 100644 index 972172c541..0000000000 --- a/src/core/__tests__/ReactComponentFlattenChildren-test.js +++ /dev/null @@ -1,159 +0,0 @@ -/** - * 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. - * - * @jsx React.DOM - * @emails react-core - */ - -"use strict"; - -var React; - -var Fake; - -var singleChild = function(children) { - return ({children}).props.children; -}; - -var multiChild = function(child1, child2, child3, child4) { - return ({child1} {child2} {child3}{child4}).props.children; -}; - - -describe('ReactComponentFlatten', function() { - beforeEach(function() { - require('mock-modules').dumpCache(); - React = require('React'); - var ReactComponent = require('ReactComponent'); - var copyProperties = require('copyProperties'); - var Constructor = function() {}; - copyProperties(Constructor.prototype, ReactComponent.Mixin); - Fake = function() { - var instance = new Constructor(); - instance.construct.apply(instance, arguments); - return instance; - }; - }); - - it("should reuse a flat array", function() { - var children = [ -
    , -
    - ]; - - var flat = singleChild(children); - - expect(flat).toBe(children); - expect(flat[0]._key).toEqual('0:foo'); - expect(flat[1]._key).toEqual('0:bar'); - }); - - it("should collapse a sparse array", function() { - var children = [ - null, -
    , -
    - ]; - - var flat = singleChild(children); - - expect(flat).not.toBe(children); - expect(flat.length).toEqual(2); - expect(flat[0]._key).toEqual('0:foo'); - expect(flat[1]._key).toEqual('0:bar'); - }); - - it("should collapse a nested array into a flat array", function() { - var flat = multiChild( - null, -
    , - [
    , null] - ); - - expect(flat.length).toEqual(2); - expect(flat[0]._key).toEqual('foo'); - expect(flat[1]._key).toEqual('2:bar'); - }); - - it("should use retain key index despite static empty values", function() { - // TODO: Wrap in another single child (currently breaks) - - var before = multiChild( -
    , -
    , - [
    ], - [
    ] - ); - - var after = multiChild( - null, -
    , - null, - [
    ] - ); - - expect(before.length).toEqual(4); - expect(after.length).toEqual(2); - expect(before[1]._key).toEqual(after[0]._key); - expect(before[3]._key).toEqual(after[1]._key); - }); - - it("should assign idempotent keys for extra flattening layers", function() { - var flat = multiChild( - null, - [null,
    ], - false - ); - - var preFlat = multiChild( - null, - singleChild([null,
    ]), - false - ); - - expect(flat.length).toBe(1); - expect(preFlat.length).toBe(1); - expect(preFlat[0]._key).toBe(flat[0]._key); - }); - - it("should assign idempotent strings through flattening", function() { - var children = [ - 'FOO', - 'BAR' - ]; - var flat = singleChild(children); - var wrappedFlat = multiChild(null, flat); - expect(flat).toBe(children); - expect(wrappedFlat.length).toBe(2); - expect(wrappedFlat[0]).toBe(flat[0]); - expect(wrappedFlat[1]).toBe(flat[1]); - }); - - it("cannot keep keys unique when children are unboxed", function() { - // This is a case that difficult to solve and should not actually be solved. - var children1 = [
    ]; - var children2 = [
    ]; - var flat1 = singleChild(children1); - var flat2 = singleChild(children2); - // There's no way to tell flat1[0] and flat2[0] apart. - var mergedChildren = [flat1[0], flat2[0]]; - var newChildren = multiChild(mergedChildren); - expect(newChildren.length).toBe(2); - expect(newChildren[0]._key).toBe(newChildren[1]._key); - }); - -}); - - diff --git a/src/core/__tests__/ReactComponentLifeCycle-test.js b/src/core/__tests__/ReactComponentLifeCycle-test.js index 4f66eb10e2..3f2df5e82e 100644 --- a/src/core/__tests__/ReactComponentLifeCycle-test.js +++ b/src/core/__tests__/ReactComponentLifeCycle-test.js @@ -286,6 +286,8 @@ describe('ReactComponentLifeCycle', function() { if (isInitialRender) { this._testJournal.stateInInitialRender = clone(this.state); this._testJournal.lifeCycleInInitialRender = this._lifeCycleState; + this._testJournal.compositeLifeCycleInInitialRender = + this._compositeLifeCycleState; } else { this._testJournal.stateInLaterRender = clone(this.state); this._testJournal.lifeCycleInLaterRender = this._lifeCycleState; @@ -319,7 +321,7 @@ describe('ReactComponentLifeCycle', function() { GET_INIT_STATE_RETURN_VAL ); expect(instance._testJournal.lifeCycleAtStartOfGetInitialState) - .toBe(ComponentLifeCycle.UNMOUNTED); + .toBe(ComponentLifeCycle.MOUNTED); expect(instance._testJournal.compositeLifeCycleAtStartOfGetInitialState) .toBe(CompositeComponentLifeCycle.MOUNTING); @@ -328,7 +330,7 @@ describe('ReactComponentLifeCycle', function() { instance._testJournal.returnedFromGetInitialState ); expect(instance._testJournal.lifeCycleAtStartOfWillMount) - .toBe(ComponentLifeCycle.UNMOUNTED); + .toBe(ComponentLifeCycle.MOUNTED); expect(instance._testJournal.compositeLifeCycleAtStartOfWillMount) .toBe(CompositeComponentLifeCycle.MOUNTING); @@ -343,7 +345,10 @@ describe('ReactComponentLifeCycle', function() { expect(instance._testJournal.stateInInitialRender) .toEqual(INIT_RENDER_STATE); expect(instance._testJournal.lifeCycleInInitialRender).toBe( - ComponentLifeCycle.UNMOUNTED + ComponentLifeCycle.MOUNTED + ); + expect(instance._testJournal.compositeLifeCycleInInitialRender).toBe( + CompositeComponentLifeCycle.MOUNTING ); expect(instance._lifeCycleState).toBe(ComponentLifeCycle.MOUNTED); @@ -493,6 +498,5 @@ describe('ReactComponentLifeCycle', function() { 'inner componentWillUnmount' ]); }); - }); diff --git a/src/core/__tests__/ReactCompositeComponent-test.js b/src/core/__tests__/ReactCompositeComponent-test.js index 178edaee1d..abbd7cf17f 100644 --- a/src/core/__tests__/ReactCompositeComponent-test.js +++ b/src/core/__tests__/ReactCompositeComponent-test.js @@ -26,6 +26,8 @@ var React; var ReactCurrentOwner; var ReactProps; var ReactTestUtils; +var ReactID; +var ReactDoNotBindDeprecated; var cx; var reactComponentExpect; @@ -37,8 +39,10 @@ describe('ReactCompositeComponent', function() { reactComponentExpect = require('reactComponentExpect'); React = require('React'); ReactCurrentOwner = require('ReactCurrentOwner'); + ReactDoNotBindDeprecated = require('ReactDoNotBindDeprecated'); ReactProps = require('ReactProps'); ReactTestUtils = require('ReactTestUtils'); + ReactID = require('ReactID'); MorphingComponent = React.createClass({ getInitialState: function() { @@ -50,7 +54,7 @@ describe('ReactCompositeComponent', function() { }, render: function() { - var toggleActivatedState = this._toggleActivatedState.bind(this); + var toggleActivatedState = this._toggleActivatedState; return !this.state.activated ? : ; @@ -62,9 +66,9 @@ describe('ReactCompositeComponent', function() { return {activated: false}; }, - _toggleActivatedState: React.autoBind(function() { + _toggleActivatedState:function() { this.setState({activated: !this.state.activated}); - }), + }, render: function() { return !this.state.activated ? @@ -160,20 +164,23 @@ describe('ReactCompositeComponent', function() { // rerender instance.setProps({renderAnchor: true, anchorClassOn: false}); var anchorID = instance.getAnchorID(); - var actualDOMAnchorNode = document.getElementById(anchorID); + var actualDOMAnchorNode = ReactID.getNode(anchorID); expect(actualDOMAnchorNode.className).toBe(''); }); it('should auto bind methods and values correctly', function() { - var RETURN_VALUE_AFTER_MOUNT = 'returnValue'; var ComponentClass = React.createClass({ getInitialState: function() { - return { - valueToReturn: RETURN_VALUE_AFTER_MOUNT - }; + return {valueToReturn: 'hi'}; }, - methodBoundOnMount: React.autoBind(function() { - return this.state.valueToReturn; + methodToBeExplicitlyBound: function() { + return this; + }, + methodAutoBound: function() { + return this; + }, + methodExplicitlyNotBound: ReactDoNotBindDeprecated.doNotBind(function() { + return this; }), render: function() { return
    ; @@ -181,19 +188,34 @@ describe('ReactCompositeComponent', function() { }); var instance = ; - // Autobound methods will throw before mounting. + // These are controversial assertions for now, they just exist + // because existing code depends on these assumptions. expect(function() { - instance.methodBoundOnMount(); - }).toThrow(); + instance.methodToBeExplicitlyBound.bind(instance)(); + }).not.toThrow(); + expect(function() { + instance.methodAutoBound(); + }).not.toThrow(); + expect(function() { + instance.methodExplicitlyNotBound(); + }).not.toThrow(); // Next, prove that once mounted, the scope is bound correctly to the actual // component. ReactTestUtils.renderIntoDocument(instance); - var retValAfterMount = instance.methodBoundOnMount(); - expect(retValAfterMount).toBe(RETURN_VALUE_AFTER_MOUNT); - var retValAfterMountWithCrazyScope = - instance.methodBoundOnMount.call({thisIsACrazyScope:null}); - expect(retValAfterMountWithCrazyScope).toBe(RETURN_VALUE_AFTER_MOUNT); + var explicitlyBound = instance.methodToBeExplicitlyBound.bind(instance); + var autoBound = instance.methodAutoBound; + var explicitlyNotBound = instance.methodExplicitlyNotBound; + + var context = {}; + expect(explicitlyBound.call(context)).toBe(instance); + expect(autoBound.call(context)).toBe(instance); + expect(explicitlyNotBound.call(context)).toBe(context); + + expect(explicitlyBound.call(instance)).toBe(instance); + expect(autoBound.call(instance)).toBe(instance); + expect(explicitlyNotBound.call(instance)).toBe(instance); + }); it('should normalize props with default values', function() { @@ -287,7 +309,7 @@ describe('ReactCompositeComponent', function() { instance.forceUpdate(); }).toThrow( 'Invariant Violation: forceUpdate(...): Can only force an update on ' + - 'mounted components.' + 'mounted or mounting components.' ); React.renderComponent(instance, container); @@ -300,7 +322,7 @@ describe('ReactCompositeComponent', function() { instance.forceUpdate(); }).toThrow( 'Invariant Violation: forceUpdate(...): Can only force an update on ' + - 'mounted components.' + 'mounted or mounting components.' ); }); diff --git a/src/core/__tests__/ReactCompositeComponentTransferProps-test.js b/src/core/__tests__/ReactCompositeComponentTransferProps-test.js index e32660801f..70c7e27532 100644 --- a/src/core/__tests__/ReactCompositeComponentTransferProps-test.js +++ b/src/core/__tests__/ReactCompositeComponentTransferProps-test.js @@ -51,8 +51,8 @@ describe('ReactCompositeComponent-transferProps', function() { ReactTestUtils.renderIntoDocument(instance); reactComponentExpect(instance) - .expectRenderedChild(instance) - .toBeDOMComponentWithTag('input') + .expectRenderedChild() + .toBeComponentOfType(React.DOM.input) .scalarPropsEqual({ className: 'textinput', style: {display: 'block'}, @@ -66,8 +66,8 @@ describe('ReactCompositeComponent-transferProps', function() { ReactTestUtils.renderIntoDocument(instance); reactComponentExpect(instance) - .expectRenderedChild(instance) - .toBeDOMComponentWithTag('input') + .expectRenderedChild() + .toBeComponentOfType(React.DOM.input) .scalarPropsEqual({placeholder: 'Type here...'}); }); @@ -80,8 +80,8 @@ describe('ReactCompositeComponent-transferProps', function() { ReactTestUtils.renderIntoDocument(instance); reactComponentExpect(instance) - .expectRenderedChild(instance) - .toBeDOMComponentWithTag('input') + .expectRenderedChild() + .toBeComponentOfType(React.DOM.input) .scalarPropsEqual({ className: 'textinput hidden_elem', style: { @@ -91,6 +91,25 @@ describe('ReactCompositeComponent-transferProps', function() { }); }); + it('should not transfer children', function() { + var ChildrenTestComponent = React.createClass({ + render: function() { + return this.transferPropsTo(
    ); + } + }); + + var instance = + + Hello! + ; + + ReactTestUtils.renderIntoDocument(instance); + reactComponentExpect(instance) + .expectRenderedChild() + .toBeDOMComponentWithTag('div') + .toBeDOMComponentWithNoChildren(); + }); + it('should not transfer ref', function() { var RefTestComponent = React.createClass({ render: function() { diff --git a/src/core/__tests__/ReactDOM-test.js b/src/core/__tests__/ReactDOM-test.js index f327b147b9..6068a81327 100644 --- a/src/core/__tests__/ReactDOM-test.js +++ b/src/core/__tests__/ReactDOM-test.js @@ -17,12 +17,15 @@ * @emails react-core */ +/*jslint evil: true */ + "use strict"; var React = require('React'); var ReactDOM = require('ReactDOM'); var ReactTestUtils = require('ReactTestUtils'); var React = require('React'); +var ReactID = require('ReactID'); describe('ref swapping', function() { // TODO: uncomment this test once we can run in phantom, which @@ -58,7 +61,7 @@ describe('ref swapping', function() { var argDiv = ReactTestUtils.renderIntoDocument( ReactDOM.div(null, 'child') ); - var argNode = document.getElementById(argDiv._rootNodeID); + var argNode = ReactID.getNode(argDiv._rootNodeID); expect(argNode.innerHTML).toBe('child'); }); @@ -66,7 +69,7 @@ describe('ref swapping', function() { var conflictDiv = ReactTestUtils.renderIntoDocument( ReactDOM.div({children: 'fakechild'}, 'child') ); - var conflictNode = document.getElementById(conflictDiv._rootNodeID); + var conflictNode = ReactID.getNode(conflictDiv._rootNodeID); expect(conflictNode.innerHTML).toBe('child'); }); @@ -81,31 +84,34 @@ describe('ref swapping', function() { theBird:
    }}
    ); + // Warm the cache with theDog myDiv.setProps({ children: { - theDog:
    , // Warm the cache with theDog + theDog:
    , theBird:
    } }); - myDiv.setProps({ - children: { // This better purge that cache - theBird:
    - } - }); - // Now, put the dog back. + // Remove theDog - this should purge the cache myDiv.setProps({ children: { - theDog:
    , // This is a different node than before theBird:
    } }); + // Now, put theDog back. It's now a different DOM node. myDiv.setProps({ - children: { // className changed to bigdog. - theDog:
    , // but will it use the proper element + children: { + theDog:
    , theBird:
    } }); - var root = document.getElementById(myDiv._rootNodeID); + // Change the className of theDog. It will use the same element + myDiv.setProps({ + children: { + theDog:
    , + theBird:
    + } + }); + var root = ReactID.getNode(myDiv._rootNodeID); var dog = root.childNodes[0]; expect(dog.className).toBe('bigdog'); }); diff --git a/src/core/__tests__/ReactDOMIDOperations-test.js b/src/core/__tests__/ReactDOMIDOperations-test.js index c4d4bc8ca0..77a64475c6 100644 --- a/src/core/__tests__/ReactDOMIDOperations-test.js +++ b/src/core/__tests__/ReactDOMIDOperations-test.js @@ -23,23 +23,23 @@ describe('ReactDOMIDOperations', function() { var DOMPropertyOperations = require('DOMPropertyOperations'); var ReactDOMIDOperations = require('ReactDOMIDOperations'); - var ReactDOMNodeCache = require('ReactDOMNodeCache'); + var ReactID = require('ReactID'); var keyOf = require('keyOf'); it('should disallow updating special properties', function() { - spyOn(ReactDOMNodeCache, "getCachedNodeByID"); + spyOn(ReactID, "getNode"); spyOn(DOMPropertyOperations, "setValueForProperty"); expect(function() { ReactDOMIDOperations.updatePropertyByID( 'testID', - keyOf({content: null}), - 'testContent' + keyOf({dangerouslySetInnerHTML: null}), + {__html: 'testContent'} ); }).toThrow(); expect( - ReactDOMNodeCache.getCachedNodeByID.argsForCall[0][0] + ReactID.getNode.argsForCall[0][0] ).toBe('testID'); expect( @@ -49,7 +49,7 @@ describe('ReactDOMIDOperations', function() { it('should update innerHTML and special-case whitespace', function() { var stubNode = document.createElement('div'); - spyOn(ReactDOMNodeCache, "getCachedNodeByID").andReturn(stubNode); + spyOn(ReactID, "getNode").andReturn(stubNode); ReactDOMIDOperations.updateInnerHTMLByID( 'testID', @@ -57,7 +57,7 @@ describe('ReactDOMIDOperations', function() { ); expect( - ReactDOMNodeCache.getCachedNodeByID.argsForCall[0][0] + ReactID.getNode.argsForCall[0][0] ).toBe('testID'); expect(stubNode.innerHTML).toBe(' testContent'); diff --git a/src/core/__tests__/ReactEvent-test.js b/src/core/__tests__/ReactEventEmitter-test.js similarity index 71% rename from src/core/__tests__/ReactEvent-test.js rename to src/core/__tests__/ReactEventEmitter-test.js index 9fb5fe9eb6..79bf8f2390 100644 --- a/src/core/__tests__/ReactEvent-test.js +++ b/src/core/__tests__/ReactEventEmitter-test.js @@ -22,6 +22,7 @@ require('mock-modules') .dontMock('BrowserScroll') .dontMock('CallbackRegistry') .dontMock('EventPluginHub') + .dontMock('ReactID') .dontMock('ReactEventEmitter') .dontMock('ReactInstanceHandles') .dontMock('EventPluginHub') @@ -34,6 +35,9 @@ var keyOf = require('keyOf'); var mocks = require('mocks'); var EventPluginHub; +var ReactID = require('ReactID'); +var getID = ReactID.getID; +var setID = ReactID.setID; var ReactEventEmitter; var ReactEventTopLevelCallback; var ReactTestUtils; @@ -68,15 +72,15 @@ var ON_TOUCH_TAP_KEY = keyOf({onTouchTap: null}); var CHILD = document.createElement('div'); var PARENT = document.createElement('div'); var GRANDPARENT = document.createElement('div'); -CHILD.id = '.reactRoot.[0].[0].[0]'; -PARENT.id = '.reactRoot.[0].[0]'; -GRANDPARENT.id = '.reactRoot.[0]'; +setID(CHILD, '.reactRoot.[0].[0].[0]'); +setID(PARENT, '.reactRoot.[0].[0]'); +setID(GRANDPARENT, '.reactRoot.[0]'); function registerSimpleTestHandler() { - ReactEventEmitter.putListener(CHILD.id, ON_CLICK_KEY, LISTENER); - var listener = ReactEventEmitter.getListener(CHILD.id, ON_CLICK_KEY); + ReactEventEmitter.putListener(getID(CHILD), ON_CLICK_KEY, LISTENER); + var listener = ReactEventEmitter.getListener(getID(CHILD), ON_CLICK_KEY); expect(listener).toEqual(LISTENER); - return ReactEventEmitter.getListener(CHILD.id, ON_CLICK_KEY); + return ReactEventEmitter.getListener(getID(CHILD), ON_CLICK_KEY); } @@ -85,6 +89,9 @@ describe('ReactEventEmitter', function() { require('mock-modules').dumpCache(); EventPluginHub = require('EventPluginHub'); TapEventPlugin = require('TapEventPlugin'); + ReactID = require('ReactID'); + getID = ReactID.getID; + setID = ReactID.setID; ReactEventEmitter = require('ReactEventEmitter'); ReactTestUtils = require('ReactTestUtils'); ReactEventTopLevelCallback = require('ReactEventTopLevelCallback'); @@ -98,20 +105,20 @@ describe('ReactEventEmitter', function() { it('should store a listener correctly', function() { registerSimpleTestHandler(); - var listener = ReactEventEmitter.getListener(CHILD.id, ON_CLICK_KEY); + var listener = ReactEventEmitter.getListener(getID(CHILD), ON_CLICK_KEY); expect(listener).toBe(LISTENER); }); it('should retrieve a listener correctly', function() { registerSimpleTestHandler(); - var listener = ReactEventEmitter.getListener(CHILD.id, ON_CLICK_KEY); + var listener = ReactEventEmitter.getListener(getID(CHILD), ON_CLICK_KEY); expect(listener).toEqual(LISTENER); }); it('should clear all handlers when asked to', function() { registerSimpleTestHandler(); - ReactEventEmitter.deleteAllListeners(CHILD.id); - var listener = ReactEventEmitter.getListener(CHILD.id, ON_CLICK_KEY); + ReactEventEmitter.deleteAllListeners(getID(CHILD)); + var listener = ReactEventEmitter.getListener(getID(CHILD), ON_CLICK_KEY); expect(listener).toBe(undefined); }); @@ -133,89 +140,89 @@ describe('ReactEventEmitter', function() { it('should bubble simply', function() { ReactEventEmitter.putListener( - CHILD.id, + getID(CHILD), ON_CLICK_KEY, - recordID.bind(null, CHILD.id) + recordID.bind(null, getID(CHILD)) ); ReactEventEmitter.putListener( - PARENT.id, + getID(PARENT), ON_CLICK_KEY, - recordID.bind(null, PARENT.id) + recordID.bind(null, getID(PARENT)) ); ReactEventEmitter.putListener( - GRANDPARENT.id, + getID(GRANDPARENT), ON_CLICK_KEY, - recordID.bind(null, GRANDPARENT.id) + recordID.bind(null, getID(GRANDPARENT)) ); ReactTestUtils.Simulate.click(CHILD); expect(idCallOrder.length).toBe(3); - expect(idCallOrder[0]).toBe(CHILD.id); - expect(idCallOrder[1]).toBe(PARENT.id); - expect(idCallOrder[2]).toBe(GRANDPARENT.id); + expect(idCallOrder[0]).toBe(getID(CHILD)); + expect(idCallOrder[1]).toBe(getID(PARENT)); + expect(idCallOrder[2]).toBe(getID(GRANDPARENT)); }); it('should support stopPropagation()', function() { ReactEventEmitter.putListener( - CHILD.id, + getID(CHILD), ON_CLICK_KEY, - recordID.bind(null, CHILD.id) + recordID.bind(null, getID(CHILD)) ); ReactEventEmitter.putListener( - PARENT.id, + getID(PARENT), ON_CLICK_KEY, - recordIDAndStopPropagation.bind(null, PARENT.id) + recordIDAndStopPropagation.bind(null, getID(PARENT)) ); ReactEventEmitter.putListener( - GRANDPARENT.id, + getID(GRANDPARENT), ON_CLICK_KEY, - recordID.bind(null, GRANDPARENT.id) + recordID.bind(null, getID(GRANDPARENT)) ); ReactTestUtils.Simulate.click(CHILD); expect(idCallOrder.length).toBe(2); - expect(idCallOrder[0]).toBe(CHILD.id); - expect(idCallOrder[1]).toBe(PARENT.id); + expect(idCallOrder[0]).toBe(getID(CHILD)); + expect(idCallOrder[1]).toBe(getID(PARENT)); }); it('should stop after first dispatch if stopPropagation', function() { ReactEventEmitter.putListener( - CHILD.id, + getID(CHILD), ON_CLICK_KEY, - recordIDAndStopPropagation.bind(null, CHILD.id) + recordIDAndStopPropagation.bind(null, getID(CHILD)) ); ReactEventEmitter.putListener( - PARENT.id, + getID(PARENT), ON_CLICK_KEY, - recordID.bind(null, PARENT.id) + recordID.bind(null, getID(PARENT)) ); ReactEventEmitter.putListener( - GRANDPARENT.id, + getID(GRANDPARENT), ON_CLICK_KEY, - recordID.bind(null, GRANDPARENT.id) + recordID.bind(null, getID(GRANDPARENT)) ); ReactTestUtils.Simulate.click(CHILD); expect(idCallOrder.length).toBe(1); - expect(idCallOrder[0]).toBe(CHILD.id); + expect(idCallOrder[0]).toBe(getID(CHILD)); }); it('should stopPropagation if false is returned', function() { ReactEventEmitter.putListener( - CHILD.id, + getID(CHILD), ON_CLICK_KEY, - recordIDAndReturnFalse.bind(null, CHILD.id) + recordIDAndReturnFalse.bind(null, getID(CHILD)) ); ReactEventEmitter.putListener( - PARENT.id, + getID(PARENT), ON_CLICK_KEY, - recordID.bind(null, PARENT.id) + recordID.bind(null, getID(PARENT)) ); ReactEventEmitter.putListener( - GRANDPARENT.id, + getID(GRANDPARENT), ON_CLICK_KEY, - recordID.bind(null, GRANDPARENT.id) + recordID.bind(null, getID(GRANDPARENT)) ); ReactTestUtils.Simulate.click(CHILD); expect(idCallOrder.length).toBe(1); - expect(idCallOrder[0]).toBe(CHILD.id); + expect(idCallOrder[0]).toBe(getID(CHILD)); }); /** @@ -229,30 +236,38 @@ describe('ReactEventEmitter', function() { it('should invoke handlers that were removed while bubbling', function() { var handleParentClick = mocks.getMockFunction(); - var handleChildClick = function(abstractEvent) { - ReactEventEmitter.deleteAllListeners(PARENT.id); + var handleChildClick = function(event) { + ReactEventEmitter.deleteAllListeners(getID(PARENT)); }; - ReactEventEmitter.putListener(CHILD.id, ON_CLICK_KEY, handleChildClick); - ReactEventEmitter.putListener(PARENT.id, ON_CLICK_KEY, handleParentClick); + ReactEventEmitter.putListener(getID(CHILD), ON_CLICK_KEY, handleChildClick); + ReactEventEmitter.putListener( + getID(PARENT), + ON_CLICK_KEY, + handleParentClick + ); ReactTestUtils.Simulate.click(CHILD); expect(handleParentClick.mock.calls.length).toBe(1); }); it('should not invoke newly inserted handlers while bubbling', function() { var handleParentClick = mocks.getMockFunction(); - var handleChildClick = function(abstractEvent) { - ReactEventEmitter.putListener(PARENT.id, ON_CLICK_KEY, handleParentClick); + var handleChildClick = function(event) { + ReactEventEmitter.putListener( + getID(PARENT), + ON_CLICK_KEY, + handleParentClick + ); }; - ReactEventEmitter.putListener(CHILD.id, ON_CLICK_KEY, handleChildClick); + ReactEventEmitter.putListener(getID(CHILD), ON_CLICK_KEY, handleChildClick); ReactTestUtils.Simulate.click(CHILD); expect(handleParentClick.mock.calls.length).toBe(0); }); it('should infer onTouchTap from a touchStart/End', function() { ReactEventEmitter.putListener( - CHILD.id, + getID(CHILD), ON_TOUCH_TAP_KEY, - recordID.bind(null, CHILD.id) + recordID.bind(null, getID(CHILD)) ); ReactTestUtils.Simulate.touchStart( CHILD, @@ -263,14 +278,14 @@ describe('ReactEventEmitter', function() { ReactTestUtils.nativeTouchData(0, 0) ); expect(idCallOrder.length).toBe(1); - expect(idCallOrder[0]).toBe(CHILD.id); + expect(idCallOrder[0]).toBe(getID(CHILD)); }); it('should infer onTouchTap from when dragging below threshold', function() { ReactEventEmitter.putListener( - CHILD.id, + getID(CHILD), ON_TOUCH_TAP_KEY, - recordID.bind(null, CHILD.id) + recordID.bind(null, getID(CHILD)) ); ReactTestUtils.Simulate.touchStart( CHILD, @@ -281,14 +296,14 @@ describe('ReactEventEmitter', function() { ReactTestUtils.nativeTouchData(0, tapMoveThreshold - 1) ); expect(idCallOrder.length).toBe(1); - expect(idCallOrder[0]).toBe(CHILD.id); + expect(idCallOrder[0]).toBe(getID(CHILD)); }); it('should not onTouchTap from when dragging beyond threshold', function() { ReactEventEmitter.putListener( - CHILD.id, + getID(CHILD), ON_TOUCH_TAP_KEY, - recordID.bind(null, CHILD.id) + recordID.bind(null, getID(CHILD)) ); ReactTestUtils.Simulate.touchStart( CHILD, @@ -304,19 +319,19 @@ describe('ReactEventEmitter', function() { it('should bubble onTouchTap', function() { ReactEventEmitter.putListener( - CHILD.id, + getID(CHILD), ON_TOUCH_TAP_KEY, - recordID.bind(null, CHILD.id) + recordID.bind(null, getID(CHILD)) ); ReactEventEmitter.putListener( - PARENT.id, + getID(PARENT), ON_TOUCH_TAP_KEY, - recordID.bind(null, PARENT.id) + recordID.bind(null, getID(PARENT)) ); ReactEventEmitter.putListener( - GRANDPARENT.id, + getID(GRANDPARENT), ON_TOUCH_TAP_KEY, - recordID.bind(null, GRANDPARENT.id) + recordID.bind(null, getID(GRANDPARENT)) ); ReactTestUtils.Simulate.touchStart( CHILD, @@ -327,9 +342,9 @@ describe('ReactEventEmitter', function() { ReactTestUtils.nativeTouchData(0, 0) ); expect(idCallOrder.length).toBe(3); - expect(idCallOrder[0]).toBe(CHILD.id); - expect(idCallOrder[1]).toBe(PARENT.id); - expect(idCallOrder[2]).toBe(GRANDPARENT.id); + expect(idCallOrder[0]).toBe(getID(CHILD)); + expect(idCallOrder[1]).toBe(getID(PARENT)); + expect(idCallOrder[2]).toBe(getID(GRANDPARENT)); }); }); diff --git a/src/core/__tests__/ReactIdentity-test.js b/src/core/__tests__/ReactIdentity-test.js index ee3f88487f..fac5cde23d 100644 --- a/src/core/__tests__/ReactIdentity-test.js +++ b/src/core/__tests__/ReactIdentity-test.js @@ -22,6 +22,7 @@ var React; var ReactTestUtils; var reactComponentExpect; +var ReactID; describe('ReactIdentity', function() { @@ -30,11 +31,12 @@ describe('ReactIdentity', function() { React = require('React'); ReactTestUtils = require('ReactTestUtils'); reactComponentExpect = require('reactComponentExpect'); + ReactID = require('ReactID'); }); - var idExp = /^\.reactRoot\[\d+\](.*)$/; + var idExp = /^\.r\[.+?\](.*)$/; function checkId(child, expectedId) { - var actual = idExp.exec(child.id); + var actual = idExp.exec(ReactID.getID(child)); var expected = idExp.exec(expectedId); expect(actual).toBeTruthy(); expect(expected).toBeTruthy(); @@ -53,8 +55,8 @@ describe('ReactIdentity', function() { React.renderComponent(instance, document.createElement('div')); var node = instance.getDOMNode(); reactComponentExpect(instance).toBeDOMComponentWithChildCount(2); - checkId(node.childNodes[0], '.reactRoot[0].[0]{first}'); - checkId(node.childNodes[1], '.reactRoot[0].[0]{second}'); + checkId(node.childNodes[0], '.r[0].{first}'); + checkId(node.childNodes[1], '.r[0].{second}'); }); it('should allow key property to express identity', function() { @@ -62,13 +64,17 @@ describe('ReactIdentity', function() {
    +
    +
    ; React.renderComponent(instance, document.createElement('div')); var node = instance.getDOMNode(); - reactComponentExpect(instance).toBeDOMComponentWithChildCount(2); - checkId(node.childNodes[0], '.reactRoot[0].[apple]'); - checkId(node.childNodes[1], '.reactRoot[0].[banana]'); + reactComponentExpect(instance).toBeDOMComponentWithChildCount(4); + checkId(node.childNodes[0], '.r[0].[apple]'); + checkId(node.childNodes[1], '.r[0].[banana]'); + checkId(node.childNodes[2], '.r[0].[0]'); + checkId(node.childNodes[3], '.r[0].[123]'); }); it('should use instance identity', function() { @@ -89,37 +95,157 @@ describe('ReactIdentity', function() { React.renderComponent(instance, document.createElement('div')); var node = instance.getDOMNode(); reactComponentExpect(instance).toBeDOMComponentWithChildCount(3); - checkId(node.childNodes[0], '.reactRoot[0].[wrap1]'); - checkId( - node.childNodes[0].firstChild, - '.reactRoot[0].[wrap1].[0:squirrel]' - ); - checkId(node.childNodes[1], '.reactRoot[0].[wrap2]'); - checkId(node.childNodes[1].firstChild, '.reactRoot[0].[wrap2].[0:bunny]'); - checkId(node.childNodes[2], '.reactRoot[0].[2]'); - checkId(node.childNodes[2].firstChild, '.reactRoot[0].[2].[0:chipmunk]'); + + checkId(node.childNodes[0], '.r[0].[wrap1]'); + checkId(node.childNodes[0].firstChild, '.r[0].[wrap1].[squirrel]'); + checkId(node.childNodes[1], '.r[0].[wrap2]'); + checkId(node.childNodes[1].firstChild, '.r[0].[wrap2].[bunny]'); + checkId(node.childNodes[2], '.r[0].[2]'); + checkId(node.childNodes[2].firstChild, '.r[0].[2].[chipmunk]'); + }); + + function renderAComponentWithKeyIntoContainer(key, container) { + var span1 = ; + var span2 = ; + + var map = {}; + map[key] = span2; + + React.renderComponent(
    {[span1, map]}
    , container); + + expect(span1.getDOMNode()).not.toBe(null); + expect(span2.getDOMNode()).not.toBe(null); + + checkId(span1.getDOMNode(), '.r[0].[' + key + ']'); + checkId(span2.getDOMNode(), '.r[0].[1]{' + key + '}'); + } + + it('should allow any character as a key, in a detached parent', function() { + var detachedContainer = document.createElement('div'); + renderAComponentWithKeyIntoContainer("<'WEIRD/&\\key'>", detachedContainer); + }); + + it('should allow any character as a key, in an attached parent', function() { + // This test exists to protect against implementation details that + // incorrectly query escaped IDs using DOM tools like getElementById. + var attachedContainer = document.createElement('div'); + document.body.appendChild(attachedContainer); + + renderAComponentWithKeyIntoContainer("<'WEIRD/&\\key'>", attachedContainer); + + document.body.removeChild(attachedContainer); + }); + + it('should not allow scripts in keys to execute', function() { + var h4x0rKey = '">
    ); - return d; -}; var renderSingleTextChild = function(text) { var d = ReactTestUtils.renderIntoDocument(
    {text}
    ); return d; @@ -141,7 +138,7 @@ describe('ReactMultiChildText', function() { assertMultiChild(d, 'hello', 'goodbye'); }); - it('should render zero string as text node then switch to spans', function() { + it('should render zero string as text node then switch to spans', function() { var d = renderSingleTextChild('0'); // false should act exactly as a null child assertNodeText(d, '0'); @@ -149,7 +146,7 @@ describe('ReactMultiChildText', function() { assertMultiChild(d, 'hello', 'goodbye'); }); - it('should render zero number as text node then switch to spans', function() { + it('should render zero number as text node then switch to spans', function() { var d = renderSingleTextChild('0'); // false should act exactly as a null child assertNodeText(d, 0); @@ -157,11 +154,6 @@ describe('ReactMultiChildText', function() { assertMultiChild(d, 'hello', 'goodbye'); }); - it('should render content to single text node', function() { - var d = renderSingleContentChild('hello'); - assertNodeText(d, 'hello'); - }); - it('should render a single text child to a single text node', function() { var d = renderSingleTextChild('hello'); assertNodeText(d, 'hello'); @@ -233,12 +225,6 @@ describe('ReactMultiChildText', function() { assertNodeText(d, '0'); }); - it('should render content number zero as text node', function() { - var d = renderSingleContentChild(0); - // false should act exactly as a null child - assertNodeText(d, '0'); - }); - it('should render zero string as string child', function() { var d = renderMultipleTextChildren('0', 234.2); // false should act exactly as a null child @@ -249,7 +235,7 @@ describe('ReactMultiChildText', function() { var d = renderMultipleTextChildren('0', 234.2); // false should act exactly as a null child assertMultiChild(d, '0', '234.2'); - d.replaceProps({content: '0'}); + d.replaceProps({children: '0'}); assertNodeText(d, '0'); }); @@ -257,7 +243,7 @@ describe('ReactMultiChildText', function() { var d = renderMultipleTextChildren(0, 234.2); // false should act exactly as a null child assertMultiChild(d, '0', '234.2'); - d.replaceProps({content: 0}); + d.replaceProps({children: 0}); // BELOW REVEALS A BUG IN JSDOM // assertNodeText(d, '0'); // This works in the browser. }); @@ -265,7 +251,7 @@ describe('ReactMultiChildText', function() { it('should render multiple children then switch to inline', function() { var d = renderMultipleTextChildren('hello', 'goodbye'); assertMultiChild(d, 'hello', 'goodbye'); - d.replaceProps({content: 'hello'}); + d.replaceProps({children: 'hello'}); assertNodeText(d, 'hello'); }); @@ -285,13 +271,6 @@ describe('ReactMultiChildText', function() { assertMultiChild(d, 'hello', 'goodbye'); }); - it('should render content, then switch to text components ', function() { - var d = renderSingleContentChild('hello'); - assertNodeText(d, 'hello'); - d.replaceProps({children: ['hello', 'goodbye']}); - assertMultiChild(d, 'hello', 'goodbye'); - }); - it('should render inline child, then switch to composite', function() { var d = renderSingleTextChild('hello'); assertNodeText(d, 'hello'); @@ -301,9 +280,11 @@ describe('ReactMultiChildText', function() { .toBeCompositeComponentWithType(TestCompositeComponent); }); - it('should throw if rendering both content and children', function() { + it('should throw if rendering both HTML and children', function() { expect(function() { - ReactTestUtils.renderIntoDocument(
    ghjkl
    ); + ReactTestUtils.renderIntoDocument( +
    ghjkl
    + ); }).toThrow(); }); }); diff --git a/src/core/__tests__/ReactNativeComponent-test.js b/src/core/__tests__/ReactNativeComponent-test.js index 55e5f13ec6..9b6ab66df5 100644 --- a/src/core/__tests__/ReactNativeComponent-test.js +++ b/src/core/__tests__/ReactNativeComponent-test.js @@ -292,25 +292,11 @@ describe('ReactNativeComponent', function() { }); it("should validate against multiple children props", function() { - expect(function() { - mountComponent({ content: '', children: '' }); - }).toThrow( - 'Invariant Violation: Can only set one of `children`, ' + - '`props.content`, or `props.dangerouslySetInnerHTML`.' - ); - - expect(function() { - mountComponent({ content: '', dangerouslySetInnerHTML: '' }); - }).toThrow( - 'Invariant Violation: Can only set one of `children`, ' + - '`props.content`, or `props.dangerouslySetInnerHTML`.' - ); - expect(function() { mountComponent({ children: '', dangerouslySetInnerHTML: '' }); }).toThrow( - 'Invariant Violation: Can only set one of `children`, ' + - '`props.content`, or `props.dangerouslySetInnerHTML`.' + 'Invariant Violation: Can only set one of `children` or ' + + '`props.dangerouslySetInnerHTML`.' ); }); @@ -328,6 +314,7 @@ describe('ReactNativeComponent', function() { it("should clean up listeners", function() { var React = require('React'); var ReactEventEmitter = require('ReactEventEmitter'); + var ReactID = require('ReactID'); var container = document.createElement('div'); document.documentElement.appendChild(container); @@ -337,7 +324,7 @@ describe('ReactNativeComponent', function() { React.renderComponent(instance, container); var rootNode = instance.getDOMNode(); - var rootNodeID = rootNode.id; + var rootNodeID = ReactID.getID(rootNode); expect( ReactEventEmitter.getListener(rootNodeID, 'onClick') ).toBe(callback); diff --git a/src/core/__tests__/ReactProps-test.js b/src/core/__tests__/ReactProps-test.js index 82cb58ebee..df9080ae3e 100644 --- a/src/core/__tests__/ReactProps-test.js +++ b/src/core/__tests__/ReactProps-test.js @@ -26,7 +26,7 @@ function typeCheck(declaration, value) { if (arguments.length > 1) { props.testProp = value; } - return declaration.bind(this, props, 'testProp', 'testComponent'); + return declaration.bind(null, props, 'testProp', 'testComponent'); } describe('Primitive Types', function() { diff --git a/src/dom/CSSPropertyOperations.js b/src/dom/CSSPropertyOperations.js index ab46485f36..e664ad432e 100644 --- a/src/dom/CSSPropertyOperations.js +++ b/src/dom/CSSPropertyOperations.js @@ -14,7 +14,7 @@ * limitations under the License. * * @providesModule CSSPropertyOperations - * @typechecks + * @typechecks static-only */ "use strict"; diff --git a/src/dom/DOMChildrenOperations.js b/src/dom/DOMChildrenOperations.js index f1fd2fc85f..b1562e8e79 100644 --- a/src/dom/DOMChildrenOperations.js +++ b/src/dom/DOMChildrenOperations.js @@ -16,6 +16,9 @@ * @providesModule DOMChildrenOperations */ +// Empty blocks improve readability so disable that warning +// jshint -W035 + "use strict"; var Danger = require('Danger'); @@ -44,7 +47,7 @@ var REMOVE_AT = keyOf({removeAt: null}); * * @param {Element} parent Parent DOM node. * @param {Object} childOperations Description of child operations. - * @returns {Array?} Sparse array containing elements by their current index in + * @return {Array?} Sparse array containing elements by their current index in * the DOM. */ var _getNodesByOriginalIndex = function(parent, childOperations) { diff --git a/src/dom/DOMProperty.js b/src/dom/DOMProperty.js index 52c4fcd07e..0d763036a7 100644 --- a/src/dom/DOMProperty.js +++ b/src/dom/DOMProperty.js @@ -14,7 +14,7 @@ * limitations under the License. * * @providesModule DOMProperty - * @typechecks + * @typechecks static-only */ /*jslint bitwise: true */ diff --git a/src/dom/DOMPropertyOperations.js b/src/dom/DOMPropertyOperations.js index b582bf914c..cb5d55ad4c 100644 --- a/src/dom/DOMPropertyOperations.js +++ b/src/dom/DOMPropertyOperations.js @@ -14,7 +14,7 @@ * limitations under the License. * * @providesModule DOMPropertyOperations - * @typechecks + * @typechecks static-only */ "use strict"; diff --git a/src/dom/Danger.js b/src/dom/Danger.js index 0117e2ef5e..21fd331fa6 100644 --- a/src/dom/Danger.js +++ b/src/dom/Danger.js @@ -16,6 +16,8 @@ * @providesModule Danger */ +/*jslint evil: true */ + "use strict"; var ExecutionEnvironment = require('ExecutionEnvironment'); @@ -63,7 +65,7 @@ function getParentDummy(parent) { * @param {Element} elem Parent element. * @param {Element} insert Element to insert. * @param {Element} after Element to insert after. - * @returns {Element} Element that was inserted. + * @return {Element} Element that was inserted. */ function insertNodeAfterNode(elem, insert, after) { if (__DEV__) { diff --git a/src/core/BrowserEnv.js b/src/dom/ViewportMetrics.js similarity index 73% rename from src/core/BrowserEnv.js rename to src/dom/ViewportMetrics.js index 53937a4ada..e9f2d3d1e0 100644 --- a/src/core/BrowserEnv.js +++ b/src/dom/ViewportMetrics.js @@ -13,24 +13,24 @@ * See the License for the specific language governing permissions and * limitations under the License. * - * @providesModule BrowserEnv + * @providesModule ViewportMetrics */ "use strict"; -/** - * A place to share/cache browser/chrome level computations. - */ -var BrowserEnv = { +var ViewportMetrics = { + currentScrollLeft: 0, + currentScrollTop: 0, - browserInfo: null, - refreshAuthoritativeScrollValues: function() { - BrowserEnv.currentScrollLeft = + + refreshScrollValues: function() { + ViewportMetrics.currentScrollLeft = document.body.scrollLeft + document.documentElement.scrollLeft; - BrowserEnv.currentScrollTop = + ViewportMetrics.currentScrollTop = document.body.scrollTop + document.documentElement.scrollTop; } + }; -module.exports = BrowserEnv; +module.exports = ViewportMetrics; diff --git a/src/dom/__tests__/CSSPropertyOperations-test.js b/src/dom/__tests__/CSSPropertyOperations-test.js index 3918d801af..2164c38e9f 100644 --- a/src/dom/__tests__/CSSPropertyOperations-test.js +++ b/src/dom/__tests__/CSSPropertyOperations-test.js @@ -17,6 +17,8 @@ * @emails react-core */ +/*jslint evil: true */ + "use strict"; var React = require('React'); diff --git a/src/core/ReactDOMForm.js b/src/dom/components/ReactDOMForm.js similarity index 100% rename from src/core/ReactDOMForm.js rename to src/dom/components/ReactDOMForm.js diff --git a/src/dom/components/ReactDOMInput.js b/src/dom/components/ReactDOMInput.js new file mode 100644 index 0000000000..2d552fc923 --- /dev/null +++ b/src/dom/components/ReactDOMInput.js @@ -0,0 +1,112 @@ +/** + * 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 ReactDOMInput + */ + +"use strict"; + +var DOMPropertyOperations = require('DOMPropertyOperations'); +var ReactCompositeComponent = require('ReactCompositeComponent'); +var ReactDOM = require('ReactDOM'); + +var merge = require('merge'); + +// Store a reference to the `ReactNativeComponent`. +var input = ReactDOM.input; + +/** + * Implements an native component that allows setting these optional + * props: `checked`, `value`, `defaultChecked`, and `defaultValue`. + * + * If `checked` or `value` are not supplied (or null/undefined), user actions + * that affect the checked state or value will trigger updates to the element. + * + * If they are supplied (and not null/undefined), the rendered element will not + * trigger updates to the element. Instead, the props must change in order for + * the rendered element to be updated. + * + * The rendered element will be initialized as unchecked (or `defaultChecked`) + * with an empty value (or `defaultValue`). + * + * @see http://www.w3.org/TR/2012/WD-html5-20121025/the-input-element.html + */ +var ReactDOMInput = ReactCompositeComponent.createClass({ + + getInitialState: function() { + return { + checked: this.props.defaultChecked || false, + value: this.props.defaultValue || '' + }; + }, + + shouldComponentUpdate: function() { + // Defer any updates to this component during the `onChange` handler. + return !this._isChanging; + }, + + getChecked: function() { + return this.props.checked != null ? this.props.checked : this.state.checked; + }, + + getValue: function() { + return this.props.value != null ? this.props.value : this.state.value; + }, + + render: function() { + // Clone `this.props` so we don't mutate the input. + var props = merge(this.props); + + props.checked = this.getChecked(); + props.value = this.getValue(); + props.onChange = this.handleChange; + + return input(props, this.props.children); + }, + + componentDidUpdate: function(prevProps, prevState, rootNode) { + if (this.props.checked != null) { + DOMPropertyOperations.setValueForProperty( + rootNode, + 'checked', + this.props.checked || false + ); + } + if (this.props.value != null) { + DOMPropertyOperations.setValueForProperty( + rootNode, + 'value', + this.props.value || '' + ); + } + }, + + handleChange: ReactCompositeComponent.autoBind(function(event) { + var returnValue; + if (this.props.onChange) { + this._isChanging = true; + returnValue = this.props.onChange(event); + this._isChanging = false; + } + this.setState({ + checked: event.target.checked, + value: event.target.value + }); + return returnValue; + }) + +}); + +module.exports = ReactDOMInput; diff --git a/src/dom/components/ReactDOMTextarea.js b/src/dom/components/ReactDOMTextarea.js new file mode 100644 index 0000000000..84e2132a90 --- /dev/null +++ b/src/dom/components/ReactDOMTextarea.js @@ -0,0 +1,136 @@ +/** + * 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 ReactDOMTextarea + */ + +"use strict"; + +var DOMPropertyOperations = require('DOMPropertyOperations'); +var ReactCompositeComponent = require('ReactCompositeComponent'); +var ReactDOM = require('ReactDOM'); + +var invariant = require('invariant'); +var merge = require('merge'); + +// Store a reference to the ; + var node = renderTextarea(stub); + + expect(node.value).toBe('giraffe'); + + // Changing children should do nothing, it functions like `defaultValue`. + stub.replaceProps({children: 'gorilla'}); + expect(node.value).toEqual('giraffe'); + }); + + it('should allow numbers as children', function() { + var node = renderTextarea(); + expect(node.value).toBe('17'); + }); + + it("should throw with multiple or invalid children", function() { + expect(function() { + ReactTestUtils.renderIntoDocument( + + ); + }).toThrow(); + + expect(function() { + ReactTestUtils.renderIntoDocument( + + ); + }).toThrow(); + }); +}); diff --git a/src/dom/dangerousStyleValue.js b/src/dom/dangerousStyleValue.js index 9c12b12ab3..216396563e 100644 --- a/src/dom/dangerousStyleValue.js +++ b/src/dom/dangerousStyleValue.js @@ -14,7 +14,7 @@ * limitations under the License. * * @providesModule dangerousStyleValue - * @typechecks + * @typechecks static-only */ "use strict"; diff --git a/src/dom/getDOMNodeID.js b/src/dom/getDOMNodeID.js deleted file mode 100644 index f9632d8de5..0000000000 --- a/src/dom/getDOMNodeID.js +++ /dev/null @@ -1,40 +0,0 @@ -/** - * 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 getDOMNodeID - * @typechecks - */ - -"use strict"; - -/** - * Accessing "id" or calling getAttribute('id') on a form element can return its - * control whose name or ID is "id". All DOM nodes support `getAttributeNode` - * but this can also get called on other objects so just return '' if we're - * given something other than a DOM node (such as window). - * - * @param {DOMElement|DOMWindow|DOMDocument} domNode DOM node. - * @returns {string} ID of the supplied `domNode`. - */ -function getDOMNodeID(domNode) { - if (domNode.getAttributeNode) { - var attributeNode = domNode.getAttributeNode('id'); - return attributeNode && attributeNode.value || ''; - } else { - return ''; - } -} - -module.exports = getDOMNodeID; diff --git a/src/dom/getEventTarget.js b/src/dom/getEventTarget.js index 9529e3932f..e1b10aebc4 100644 --- a/src/dom/getEventTarget.js +++ b/src/dom/getEventTarget.js @@ -14,9 +14,11 @@ * limitations under the License. * * @providesModule getEventTarget - * @typechecks + * @typechecks static-only */ +"use strict"; + var ExecutionEnvironment = require('ExecutionEnvironment'); /** diff --git a/src/environment/ReactServerRendering.js b/src/environment/ReactServerRendering.js index b7c43d13ec..0a0b598927 100644 --- a/src/environment/ReactServerRendering.js +++ b/src/environment/ReactServerRendering.js @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. * - * @typechecks + * @typechecks static-only * @providesModule ReactServerRendering */ "use strict"; diff --git a/src/environment/__tests__/ReactServerRendering-test.js b/src/environment/__tests__/ReactServerRendering-test.js index 779730a68f..c8cc58ebe5 100644 --- a/src/environment/__tests__/ReactServerRendering-test.js +++ b/src/environment/__tests__/ReactServerRendering-test.js @@ -17,15 +17,19 @@ * @emails react-core */ +/*jslint evil: true */ + "use strict"; require('mock-modules') .dontMock('ExecutionEnvironment') .dontMock('React') + .dontMock('ReactID') .dontMock('ReactServerRendering') .dontMock('ReactTestUtils'); var React; +var ReactID; var ReactTestUtils; var ReactServerRendering; var ExecutionEnvironment; @@ -34,6 +38,7 @@ describe('ReactServerRendering', function() { beforeEach(function() { require('mock-modules').dumpCache(); React = require('React'); + ReactID = require('ReactID'); ReactTestUtils = require('ReactTestUtils'); ExecutionEnvironment = require('ExecutionEnvironment'); ExecutionEnvironment.canUseDOM = false; @@ -49,7 +54,7 @@ describe('ReactServerRendering', function() { } ); expect(response).toMatch( - 'hello world' + 'hello world' ); }); @@ -72,8 +77,12 @@ describe('ReactServerRendering', function() { } ); expect(response).toMatch( - '
    My name is ' + - 'child
    ' + '
    ' + + '' + + 'My name is ' + + 'child' + + '' + + '
    ' ); }); @@ -121,8 +130,10 @@ describe('ReactServerRendering', function() { ); expect(response).toMatch( - 'Component name: ' + - 'TestComponent' + '' + + 'Component name: ' + + 'TestComponent' + + '' ); expect(lifecycle).toEqual( ['getInitialState', 'componentWillMount', 'render'] @@ -147,7 +158,7 @@ describe('ReactServerRendering', function() { return ( Name: {this.props.name} ); - }, + } }); var element = document.createElement('div'); diff --git a/src/event/AbstractEvent.js b/src/event/AbstractEvent.js deleted file mode 100644 index ad14378a69..0000000000 --- a/src/event/AbstractEvent.js +++ /dev/null @@ -1,253 +0,0 @@ -/** - * 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 AbstractEvent - */ - -"use strict"; - -var BrowserEnv = require('BrowserEnv'); -var PooledClass = require('PooledClass'); -var TouchEventUtils = require('TouchEventUtils'); - -var emptyFunction = require('emptyFunction'); - -var MAX_POOL_SIZE = 20; - -/** - * AbstractEvent copy constructor. @see `PooledClass`. Provides a single place - * to define all cross browser normalization of DOM events. Does not attempt to - * extend a native event, rather creates a completely new object that has a - * reference to the nativeEvent through .nativeEvent member. The property .data - * should hold all data that is extracted from the event in a cross browser - * manner. Application code should use the data field when possible, not the - * unreliable native event. - */ -function AbstractEvent( - reactEventType, - reactTargetID, // Allows the abstract target to differ from native. - nativeEvent, - data) { - this.reactEventType = reactEventType; - this.reactTargetID = reactTargetID || ''; - this.nativeEvent = nativeEvent; - this.data = data; - // TODO: Deprecate storing target - doesn't always make sense for some types - this.target = nativeEvent && nativeEvent.target; - - /** - * As a performance optimization, we tag the existing event with the listeners - * (or listener [singular] if only one). This avoids having to package up an - * abstract event along with the set of listeners into a wrapping "dispatch" - * object. No one should ever read this property except event system and - * plugin/dispatcher code. We also tag the abstract event with a parallel - * ID array. _dispatchListeners[i] is being dispatched to a DOM node at ID - * _dispatchIDs[i]. The lengths should never, ever, ever be different. - */ - this._dispatchListeners = null; - this._dispatchIDs = null; - - this.isPropagationStopped = false; - this.isPersistent = emptyFunction.thatReturnsFalse; -} - -/** `PooledClass` looks for this. */ -AbstractEvent.poolSize = MAX_POOL_SIZE; - -/** - * `PooledClass` looks for `destructor` on each instance it releases. We need to - * ensure that we remove all references to listeners which could trap large - * amounts of memory in their closures. - */ -AbstractEvent.prototype.destructor = function() { - this.target = null; - this._dispatchListeners = null; - this._dispatchIDs = null; -}; - -/** - * Enhance the `AbstractEvent` class to have pooling abilities. We instruct - * `PooledClass` that our copy constructor accepts five arguments (this is just - * a performance optimization). These objects are instantiated frequently. - */ -PooledClass.addPoolingTo(AbstractEvent, PooledClass.fiveArgumentPooler); - -AbstractEvent.prototype.stopPropagation = function() { - this.isPropagationStopped = true; - if (this.nativeEvent.stopPropagation) { - this.nativeEvent.stopPropagation(); - } - // IE8 only understands cancelBubble, not stopPropagation(). - this.nativeEvent.cancelBubble = true; -}; - -AbstractEvent.prototype.preventDefault = function() { - AbstractEvent.preventDefaultOnNativeEvent(this.nativeEvent); -}; - -/** - * We clear out all dispatched `AbstractEvent`s after each event loop, adding - * them back into the pool. This allows a way to hold onto a reference that - * won't be added back into the pool. - */ -AbstractEvent.prototype.persist = function() { - this.isPersistent = emptyFunction.thatReturnsTrue; -}; - -/** - * Utility function for preventing default in cross browser manner. - */ -AbstractEvent.preventDefaultOnNativeEvent = function(nativeEvent) { - if (nativeEvent.preventDefault) { - nativeEvent.preventDefault(); - } else { - nativeEvent.returnValue = false; - } -}; - -/** - * @param {Element} target The target element. - */ -AbstractEvent.normalizeScrollDataFromTarget = function(target) { - return { - scrollTop: target.scrollTop, - scrollLeft: target.scrollLeft, - clientWidth: target.clientWidth, - clientHeight: target.clientHeight, - scrollHeight: target.scrollHeight, - scrollWidth: target.scrollWidth - }; -}; - -/* - * There are some normalizations that need to happen for various browsers. In - * addition to replacing the general event fixing with a framework such as - * jquery, we need to normalize mouse events here. Code below is mostly borrowed - * from: jScrollPane/script/jquery.mousewheel.js - */ -AbstractEvent.normalizeMouseWheelData = function(nativeEvent) { - var delta = 0; - var deltaX = 0; - var deltaY = 0; - - /* traditional scroll wheel data */ - if ( nativeEvent.wheelDelta ) { delta = nativeEvent.wheelDelta/120; } - if ( nativeEvent.detail ) { delta = -nativeEvent.detail/3; } - - /* Multidimensional scroll (touchpads) with deltas */ - deltaY = delta; - - /* Gecko based browsers */ - if (nativeEvent.axis !== undefined && - nativeEvent.axis === nativeEvent.HORIZONTAL_AXIS ) { - deltaY = 0; - deltaX = -delta; - } - - /* Webkit based browsers */ - if (nativeEvent.wheelDeltaY !== undefined ) { - deltaY = nativeEvent.wheelDeltaY/120; - } - if (nativeEvent.wheelDeltaX !== undefined ) { - deltaX = -nativeEvent.wheelDeltaX/120; - } - - return { delta: delta, deltaX: deltaX, deltaY: deltaY }; -}; - -/** - * I <3 Quirksmode.org: - * http://www.quirksmode.org/js/events_properties.html - */ -AbstractEvent.isNativeClickEventRightClick = function(nativeEvent) { - return nativeEvent.which ? nativeEvent.which === 3 : - nativeEvent.button ? nativeEvent.button === 2 : - false; -}; - -AbstractEvent.normalizePointerData = function(nativeEvent) { - return { - globalX: AbstractEvent.eventPageX(nativeEvent), - globalY: AbstractEvent.eventPageY(nativeEvent), - rightMouseButton: - AbstractEvent.isNativeClickEventRightClick(nativeEvent) - }; -}; - -AbstractEvent.normalizeDragEventData = - function(nativeEvent, globalX, globalY, startX, startY) { - return { - globalX: globalX, - globalY: globalY, - startX: startX, - startY: startY - }; - }; - -/** - * Warning: It is possible to move your finger on a touch surface, yet not - * effect the `eventPageX/Y` because the touch had caused a scroll that - * compensated for your movement. To track movements across the page, prevent - * default to avoid scrolling, and control scrolling in javascript. - */ - -/** - * Gets the exact position of a touch/mouse event on the page with respect to - * the document body. The only reason why this method is needed instead of using - * `TouchEventUtils.extractSingleTouch` is to support IE8-. Mouse events in all - * browsers except IE8- contain a pageY. IE8 and below require clientY - * computation: - * - * @param {Event} nativeEvent Native event, possibly touch or mouse. - * @return {number} Coordinate with respect to document body. - */ -AbstractEvent.eventPageY = function(nativeEvent) { - var singleTouch = TouchEventUtils.extractSingleTouch(nativeEvent); - if (singleTouch) { - return singleTouch.pageY; - } else if (typeof nativeEvent.pageY !== 'undefined') { - return nativeEvent.pageY; - } else { - return nativeEvent.clientY + BrowserEnv.currentPageScrollTop; - } -}; - -/** - * @see `AbstractEvent.eventPageY`. - * - * @param {Event} nativeEvent Native event, possibly touch or mouse. - * @return {number} Coordinate with respect to document body. - */ -AbstractEvent.eventPageX = function(nativeEvent) { - var singleTouch = TouchEventUtils.extractSingleTouch(nativeEvent); - if (singleTouch) { - return singleTouch.pageX; - } else if (typeof nativeEvent.pageX !== 'undefined') { - return nativeEvent.pageX; - } else { - return nativeEvent.clientX + BrowserEnv.currentPageScrollLeft; - } -}; - -/** - * @deprecated - */ -AbstractEvent.persistentCloneOf = function(abstractEvent) { - abstractEvent.persist(); - return abstractEvent; -}; - -module.exports = AbstractEvent; - diff --git a/src/event/CallbackRegistry.js b/src/event/CallbackRegistry.js index a0c2b5aca5..e0cf9a5c82 100644 --- a/src/event/CallbackRegistry.js +++ b/src/event/CallbackRegistry.js @@ -14,7 +14,7 @@ * limitations under the License. * * @providesModule CallbackRegistry - * @typechecks + * @typechecks static-only */ "use strict"; diff --git a/src/event/EventPluginHub.js b/src/event/EventPluginHub.js index 03133e4906..ff0be9d8a1 100644 --- a/src/event/EventPluginHub.js +++ b/src/event/EventPluginHub.js @@ -65,7 +65,7 @@ var executeDispatchesAndRelease = function(event) { * Required. When a top-level event is fired, this method is expected to * extract synthetic events that will in turn be queued and dispatched. * - * `abstractEventTypes` {object} + * `eventTypes` {object} * Optional, plugins that fire events must publish a mapping of registration * names that are used to register listeners. Values of this mapping must * be objects that contain `registrationName` or `phasedRegistrationNames`. diff --git a/src/event/EventPluginRegistry.js b/src/event/EventPluginRegistry.js index f36ca3c3a6..14a2fcb024 100644 --- a/src/event/EventPluginRegistry.js +++ b/src/event/EventPluginRegistry.js @@ -1,6 +1,20 @@ /** + * 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 EventPluginRegistry - * @typechecks + * @typechecks static-only */ "use strict"; @@ -46,7 +60,7 @@ function recomputePluginOrdering() { pluginName ); EventPluginRegistry.plugins[pluginIndex] = PluginModule; - var publishedEvents = PluginModule.abstractEventTypes; + var publishedEvents = PluginModule.eventTypes; for (var eventName in publishedEvents) { invariant( publishEventForPlugin(publishedEvents[eventName], PluginModule), @@ -184,7 +198,7 @@ var EventPluginRegistry = { * @internal */ getPluginModuleForEvent: function(event) { - var dispatchConfig = event.reactEventType; + var dispatchConfig = event.dispatchConfig; if (dispatchConfig.registrationName) { return EventPluginRegistry.registrationNames[ dispatchConfig.registrationName diff --git a/src/event/EventPluginUtils.js b/src/event/EventPluginUtils.js index 219090e77a..c9d063f620 100644 --- a/src/event/EventPluginUtils.js +++ b/src/event/EventPluginUtils.js @@ -19,7 +19,6 @@ "use strict"; var EventConstants = require('EventConstants'); -var AbstractEvent = require('AbstractEvent'); var invariant = require('invariant'); @@ -40,27 +39,11 @@ function isStartish(topLevelType) { topLevelType === topLevelTypes.topTouchStart; } -function storePageCoordsIn(obj, nativeEvent) { - var pageX = AbstractEvent.eventPageX(nativeEvent); - var pageY = AbstractEvent.eventPageY(nativeEvent); - obj.pageX = pageX; - obj.pageY = pageY; -} - -function eventDistance(coords, nativeEvent) { - var pageX = AbstractEvent.eventPageX(nativeEvent); - var pageY = AbstractEvent.eventPageY(nativeEvent); - return Math.pow( - Math.pow(pageX - coords.pageX, 2) + Math.pow(pageY - coords.pageY, 2), - 0.5 - ); -} - var validateEventDispatches; if (__DEV__) { - validateEventDispatches = function(abstractEvent) { - var dispatchListeners = abstractEvent._dispatchListeners; - var dispatchIDs = abstractEvent._dispatchIDs; + validateEventDispatches = function(event) { + var dispatchListeners = event._dispatchListeners; + var dispatchIDs = event._dispatchIDs; var listenersIsArr = Array.isArray(dispatchListeners); var idsIsArr = Array.isArray(dispatchIDs); @@ -71,81 +54,79 @@ if (__DEV__) { invariant( idsIsArr === listenersIsArr && IDsLen === listenersLen, - 'EventPluginUtils: Invalid `abstractEvent`.' + 'EventPluginUtils: Invalid `event`.' ); }; } /** - * Invokes `cb(abstractEvent, listener, id)`. Avoids using call if no scope is + * Invokes `cb(event, listener, id)`. Avoids using call if no scope is * provided. The `(listener,id)` pair effectively forms the "dispatch" but are * kept separate to conserve memory. */ -function forEachEventDispatch(abstractEvent, cb) { - var dispatchListeners = abstractEvent._dispatchListeners; - var dispatchIDs = abstractEvent._dispatchIDs; +function forEachEventDispatch(event, cb) { + var dispatchListeners = event._dispatchListeners; + var dispatchIDs = event._dispatchIDs; if (__DEV__) { - validateEventDispatches(abstractEvent); + validateEventDispatches(event); } if (Array.isArray(dispatchListeners)) { - var i; - for ( - i = 0; - i < dispatchListeners.length && !abstractEvent.isPropagationStopped; - i++) { + for (var i = 0; i < dispatchListeners.length; i++) { + if (event.isPropagationStopped()) { + break; + } // Listeners and IDs are two parallel arrays that are always in sync. - cb(abstractEvent, dispatchListeners[i], dispatchIDs[i]); + cb(event, dispatchListeners[i], dispatchIDs[i]); } } else if (dispatchListeners) { - cb(abstractEvent, dispatchListeners, dispatchIDs); + cb(event, dispatchListeners, dispatchIDs); } } /** * Default implementation of PluginModule.executeDispatch(). - * @param {AbstractEvent} AbstractEvent to handle + * @param {SyntheticEvent} SyntheticEvent to handle * @param {function} Application-level callback * @param {string} domID DOM id to pass to the callback. */ -function executeDispatch(abstractEvent, listener, domID) { - listener(abstractEvent, domID); +function executeDispatch(event, listener, domID) { + listener(event, domID); } /** * Standard/simple iteration through an event's collected dispatches. */ -function executeDispatchesInOrder(abstractEvent, executeDispatch) { - forEachEventDispatch(abstractEvent, executeDispatch); - abstractEvent._dispatchListeners = null; - abstractEvent._dispatchIDs = null; +function executeDispatchesInOrder(event, executeDispatch) { + forEachEventDispatch(event, executeDispatch); + event._dispatchListeners = null; + event._dispatchIDs = null; } /** * Standard/simple iteration through an event's collected dispatches, but stops * at the first dispatch execution returning true, and returns that id. * - * @returns id of the first dispatch execution who's listener returns true, or + * @return id of the first dispatch execution who's listener returns true, or * null if no listener returned true. */ -function executeDispatchesInOrderStopAtTrue(abstractEvent) { - var dispatchListeners = abstractEvent._dispatchListeners; - var dispatchIDs = abstractEvent._dispatchIDs; +function executeDispatchesInOrderStopAtTrue(event) { + var dispatchListeners = event._dispatchListeners; + var dispatchIDs = event._dispatchIDs; if (__DEV__) { - validateEventDispatches(abstractEvent); + validateEventDispatches(event); } if (Array.isArray(dispatchListeners)) { - var i; - for ( - i = 0; - i < dispatchListeners.length && !abstractEvent.isPropagationStopped; - i++) { + for (var i = 0; i < dispatchListeners.length; i++) { + if (event.isPropagationStopped()) { + break; + } // Listeners and IDs are two parallel arrays that are always in sync. - if (dispatchListeners[i](abstractEvent, dispatchIDs[i])) { + if (dispatchListeners[i](event, dispatchIDs[i])) { return dispatchIDs[i]; } } } else if (dispatchListeners) { - if (dispatchListeners(abstractEvent, dispatchIDs)) { + if (dispatchListeners(event, dispatchIDs)) { return dispatchIDs; } } @@ -159,32 +140,32 @@ function executeDispatchesInOrderStopAtTrue(abstractEvent) { * return values at each dispatch execution, but it does tend to make sense when * dealing with "direct" dispatches. * - * @returns The return value of executing the single dispatch. + * @return The return value of executing the single dispatch. */ -function executeDirectDispatch(abstractEvent) { +function executeDirectDispatch(event) { if (__DEV__) { - validateEventDispatches(abstractEvent); + validateEventDispatches(event); } - var dispatchListener = abstractEvent._dispatchListeners; - var dispatchID = abstractEvent._dispatchIDs; + var dispatchListener = event._dispatchListeners; + var dispatchID = event._dispatchIDs; invariant( !Array.isArray(dispatchListener), - 'executeDirectDispatch(...): Invalid `abstractEvent`.' + 'executeDirectDispatch(...): Invalid `event`.' ); var res = dispatchListener ? - dispatchListener(abstractEvent, dispatchID) : + dispatchListener(event, dispatchID) : null; - abstractEvent._dispatchListeners = null; - abstractEvent._dispatchIDs = null; + event._dispatchListeners = null; + event._dispatchIDs = null; return res; } /** - * @param {AbstractEvent} abstractEvent - * @returns {bool} True iff number of dispatches accumulated is greater than 0. + * @param {SyntheticEvent} event + * @return {bool} True iff number of dispatches accumulated is greater than 0. */ -function hasDispatches(abstractEvent) { - return !!abstractEvent._dispatchListeners; +function hasDispatches(event) { + return !!event._dispatchListeners; } /** @@ -194,8 +175,6 @@ var EventPluginUtils = { isEndish: isEndish, isMoveish: isMoveish, isStartish: isStartish, - storePageCoordsIn: storePageCoordsIn, - eventDistance: eventDistance, executeDispatchesInOrder: executeDispatchesInOrder, executeDispatchesInOrderStopAtTrue: executeDispatchesInOrderStopAtTrue, executeDirectDispatch: executeDirectDispatch, diff --git a/src/event/EventPropagators.js b/src/event/EventPropagators.js index e7534f7a2d..26127b5d00 100644 --- a/src/event/EventPropagators.js +++ b/src/event/EventPropagators.js @@ -56,19 +56,19 @@ var injection = { * Some event types have a notion of different registration names for different * "phases" of propagation. This finds listeners by a given phase. */ -function listenerAtPhase(id, abstractEvent, propagationPhase) { +function listenerAtPhase(id, event, propagationPhase) { var registrationName = - abstractEvent.reactEventType.phasedRegistrationNames[propagationPhase]; + event.dispatchConfig.phasedRegistrationNames[propagationPhase]; return getListener(id, registrationName); } /** - * Tags an `AbstractEvent` with dispatched listeners. Creating this function + * Tags a `SyntheticEvent` with dispatched listeners. Creating this function * here, allows us to not have to bind or create functions for each event. * Mutating the event's members allows us to not have to create a wrapping * "dispatch" object that pairs the event with the listener. */ -function accumulateDirectionalDispatches(domID, upwards, abstractEvent) { +function accumulateDirectionalDispatches(domID, upwards, event) { if (__DEV__) { if (!domID) { throw new Error('Dispatching id must not be null'); @@ -76,11 +76,10 @@ function accumulateDirectionalDispatches(domID, upwards, abstractEvent) { injection.validate(); } var phase = upwards ? PropagationPhases.bubbled : PropagationPhases.captured; - var listener = listenerAtPhase(domID, abstractEvent, phase); + var listener = listenerAtPhase(domID, event, phase); if (listener) { - abstractEvent._dispatchListeners = - accumulate(abstractEvent._dispatchListeners, listener); - abstractEvent._dispatchIDs = accumulate(abstractEvent._dispatchIDs, domID); + event._dispatchListeners = accumulate(event._dispatchListeners, listener); + event._dispatchIDs = accumulate(event._dispatchIDs, domID); } } @@ -91,12 +90,12 @@ function accumulateDirectionalDispatches(domID, upwards, abstractEvent) { * single traversal for the entire collection of events because each event may * have a different target. */ -function accumulateTwoPhaseDispatchesSingle(abstractEvent) { - if (abstractEvent && abstractEvent.reactEventType.phasedRegistrationNames) { +function accumulateTwoPhaseDispatchesSingle(event) { + if (event && event.dispatchConfig.phasedRegistrationNames) { injection.InstanceHandle.traverseTwoPhase( - abstractEvent.reactTargetID, + event.dispatchMarker, accumulateDirectionalDispatches, - abstractEvent + event ); } } @@ -105,36 +104,35 @@ function accumulateTwoPhaseDispatchesSingle(abstractEvent) { /** * Accumulates without regard to direction, does not look for phased * registration names. Same as `accumulateDirectDispatchesSingle` but without - * requiring that the `reactTargetID` be the same as the dispatched ID. + * requiring that the `dispatchMarker` be the same as the dispatched ID. */ -function accumulateDispatches(id, ignoredDirection, abstractEvent) { - if (abstractEvent && abstractEvent.reactEventType.registrationName) { - var registrationName = abstractEvent.reactEventType.registrationName; +function accumulateDispatches(id, ignoredDirection, event) { + if (event && event.dispatchConfig.registrationName) { + var registrationName = event.dispatchConfig.registrationName; var listener = getListener(id, registrationName); if (listener) { - abstractEvent._dispatchListeners = - accumulate(abstractEvent._dispatchListeners, listener); - abstractEvent._dispatchIDs = accumulate(abstractEvent._dispatchIDs, id); + event._dispatchListeners = accumulate(event._dispatchListeners, listener); + event._dispatchIDs = accumulate(event._dispatchIDs, id); } } } /** - * Accumulates dispatches on an `AbstractEvent`, but only for the - * `reactTargetID`. - * @param {AbstractEvent} abstractEvent + * Accumulates dispatches on an `SyntheticEvent`, but only for the + * `dispatchMarker`. + * @param {SyntheticEvent} event */ -function accumulateDirectDispatchesSingle(abstractEvent) { - if (abstractEvent && abstractEvent.reactEventType.registrationName) { - accumulateDispatches(abstractEvent.reactTargetID, null, abstractEvent); +function accumulateDirectDispatchesSingle(event) { + if (event && event.dispatchConfig.registrationName) { + accumulateDispatches(event.dispatchMarker, null, event); } } -function accumulateTwoPhaseDispatches(abstractEvents) { +function accumulateTwoPhaseDispatches(events) { if (__DEV__) { injection.validate(); } - forEachAccumulated(abstractEvents, accumulateTwoPhaseDispatchesSingle); + forEachAccumulated(events, accumulateTwoPhaseDispatchesSingle); } function accumulateEnterLeaveDispatches(leave, enter, fromID, toID) { @@ -151,11 +149,11 @@ function accumulateEnterLeaveDispatches(leave, enter, fromID, toID) { } -function accumulateDirectDispatches(abstractEvents) { +function accumulateDirectDispatches(events) { if (__DEV__) { injection.validate(); } - forEachAccumulated(abstractEvents, accumulateDirectDispatchesSingle); + forEachAccumulated(events, accumulateDirectDispatchesSingle); } diff --git a/src/event/__tests__/EventPluginRegistry-test.js b/src/event/__tests__/EventPluginRegistry-test.js index c8755c8b38..f890a58169 100644 --- a/src/event/__tests__/EventPluginRegistry-test.js +++ b/src/event/__tests__/EventPluginRegistry-test.js @@ -1,4 +1,18 @@ /** + * 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. + * * @emails react-core */ @@ -141,13 +155,13 @@ describe('EventPluginRegistry', function() { it('should publish registration names of injected plugins', function() { var OnePlugin = createPlugin({ - abstractEventTypes: { + eventTypes: { click: {registrationName: 'onClick'}, focus: {registrationName: 'onFocus'} } }); var TwoPlugin = createPlugin({ - abstractEventTypes: { + eventTypes: { magic: { phasedRegistrationNames: { bubbled: 'onMagicBubble', @@ -175,12 +189,12 @@ describe('EventPluginRegistry', function() { it('should throw if multiple registration names collide', function() { var OnePlugin = createPlugin({ - abstractEventTypes: { + eventTypes: { photoCapture: {registrationName: 'onPhotoCapture'} } }); var TwoPlugin = createPlugin({ - abstractEventTypes: { + eventTypes: { photo: { phasedRegistrationNames: { bubbled: 'onPhotoBubble', @@ -205,7 +219,7 @@ describe('EventPluginRegistry', function() { it('should throw if an invalid event is published', function() { var OnePlugin = createPlugin({ - abstractEventTypes: { + eventTypes: { badEvent: {/* missing configuration */} } }); @@ -232,14 +246,14 @@ describe('EventPluginRegistry', function() { }; var OnePlugin = createPlugin({ - abstractEventTypes: { + eventTypes: { click: clickDispatchConfig, magic: magicDispatchConfig } }); - var clickEvent = {reactEventType: clickDispatchConfig}; - var magicEvent = {reactEventType: magicDispatchConfig}; + var clickEvent = {dispatchConfig: clickDispatchConfig}; + var magicEvent = {dispatchConfig: magicDispatchConfig}; expect(EventPluginRegistry.getPluginModuleForEvent(clickEvent)).toBe(null); expect(EventPluginRegistry.getPluginModuleForEvent(magicEvent)).toBe(null); diff --git a/src/event/synthetic/SyntheticEvent.js b/src/event/synthetic/SyntheticEvent.js index c72275f679..2370528851 100644 --- a/src/event/synthetic/SyntheticEvent.js +++ b/src/event/synthetic/SyntheticEvent.js @@ -14,9 +14,11 @@ * limitations under the License. * * @providesModule SyntheticEvent - * @typechecks + * @typechecks static-only */ +"use strict"; + var PooledClass = require('PooledClass'); var emptyFunction = require('emptyFunction'); diff --git a/src/event/synthetic/SyntheticFocusEvent.js b/src/event/synthetic/SyntheticFocusEvent.js index 8ba8e73c5c..cf17be6bf0 100644 --- a/src/event/synthetic/SyntheticFocusEvent.js +++ b/src/event/synthetic/SyntheticFocusEvent.js @@ -14,9 +14,11 @@ * limitations under the License. * * @providesModule SyntheticFocusEvent - * @typechecks + * @typechecks static-only */ +"use strict"; + var SyntheticUIEvent = require('SyntheticUIEvent'); /** diff --git a/src/event/synthetic/SyntheticKeyboardEvent.js b/src/event/synthetic/SyntheticKeyboardEvent.js index 64448c91f9..c7c64e21bb 100644 --- a/src/event/synthetic/SyntheticKeyboardEvent.js +++ b/src/event/synthetic/SyntheticKeyboardEvent.js @@ -14,9 +14,11 @@ * limitations under the License. * * @providesModule SyntheticKeyboardEvent - * @typechecks + * @typechecks static-only */ +"use strict"; + var SyntheticUIEvent = require('SyntheticUIEvent'); /** diff --git a/src/event/synthetic/SyntheticMouseEvent.js b/src/event/synthetic/SyntheticMouseEvent.js index fa3fed1c23..1e894ae938 100644 --- a/src/event/synthetic/SyntheticMouseEvent.js +++ b/src/event/synthetic/SyntheticMouseEvent.js @@ -14,11 +14,13 @@ * limitations under the License. * * @providesModule SyntheticMouseEvent - * @typechecks + * @typechecks static-only */ -var BrowserEnv = require('BrowserEnv'); +"use strict"; + var SyntheticUIEvent = require('SyntheticUIEvent'); +var ViewportMetrics = require('ViewportMetrics'); /** * @interface MouseEvent @@ -59,12 +61,12 @@ var MouseEventInterface = { pageX: function(event) { return 'pageX' in event ? event.pageX : - event.clientX + BrowserEnv.currentPageScrollLeft; + event.clientX + ViewportMetrics.currentScrollLeft; }, pageY: function(event) { return 'pageY' in event ? event.pageY : - event.clientY + BrowserEnv.currentPageScrollTop; + event.clientY + ViewportMetrics.currentScrollTop; } }; diff --git a/src/event/synthetic/SyntheticMutationEvent.js b/src/event/synthetic/SyntheticMutationEvent.js index 84fcbf187b..44bbc0d3c7 100644 --- a/src/event/synthetic/SyntheticMutationEvent.js +++ b/src/event/synthetic/SyntheticMutationEvent.js @@ -14,9 +14,11 @@ * limitations under the License. * * @providesModule SyntheticMutationEvent - * @typechecks + * @typechecks static-only */ +"use strict"; + var SyntheticEvent = require('SyntheticEvent'); /** diff --git a/src/event/synthetic/SyntheticTouchEvent.js b/src/event/synthetic/SyntheticTouchEvent.js index ccc4fe89d1..3c3348bfbf 100644 --- a/src/event/synthetic/SyntheticTouchEvent.js +++ b/src/event/synthetic/SyntheticTouchEvent.js @@ -14,9 +14,11 @@ * limitations under the License. * * @providesModule SyntheticTouchEvent - * @typechecks + * @typechecks static-only */ +"use strict"; + var SyntheticUIEvent = require('SyntheticUIEvent'); /** diff --git a/src/event/synthetic/SyntheticUIEvent.js b/src/event/synthetic/SyntheticUIEvent.js index 314d9d81b1..5d95ea1417 100644 --- a/src/event/synthetic/SyntheticUIEvent.js +++ b/src/event/synthetic/SyntheticUIEvent.js @@ -14,9 +14,11 @@ * limitations under the License. * * @providesModule SyntheticUIEvent - * @typechecks + * @typechecks static-only */ +"use strict"; + var SyntheticEvent = require('SyntheticEvent'); /** diff --git a/src/event/synthetic/SyntheticWheelEvent.js b/src/event/synthetic/SyntheticWheelEvent.js index 857c8eaa9f..5d2320f9ae 100644 --- a/src/event/synthetic/SyntheticWheelEvent.js +++ b/src/event/synthetic/SyntheticWheelEvent.js @@ -14,9 +14,11 @@ * limitations under the License. * * @providesModule SyntheticWheelEvent - * @typechecks + * @typechecks static-only */ +"use strict"; + var SyntheticMouseEvent = require('SyntheticMouseEvent'); /** diff --git a/src/eventPlugins/AnalyticsEventPluginFactory.js b/src/eventPlugins/AnalyticsEventPluginFactory.js index f10a77804a..c2cf1e6ff5 100644 --- a/src/eventPlugins/AnalyticsEventPluginFactory.js +++ b/src/eventPlugins/AnalyticsEventPluginFactory.js @@ -133,14 +133,14 @@ if (__DEV__) { } /** - * This plugin does not really extract any abstract events. Rather it just looks - * at the top level event and bumps up counters as appropriate + * This plugin does not really extract any synthetic events. Rather it just + * looks at the top-level event and bumps up counters as appropriate * * @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 `AbstractEvent`s. + * @return {*} An accumulation of synthetic events. * @see {EventPluginHub.extractEvents} */ function extractEvents( diff --git a/src/eventPlugins/ChangeEventPlugin.js b/src/eventPlugins/ChangeEventPlugin.js new file mode 100644 index 0000000000..b3338fdb09 --- /dev/null +++ b/src/eventPlugins/ChangeEventPlugin.js @@ -0,0 +1,383 @@ +/** + * 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 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 + ); +} + + +/** + * @see http://www.whatwg.org/specs/web-apps/current-work/multipage/the-input-element.html#input-type-attr-summary + */ +var supportedInputTypes = { + 'color': true, + 'date': true, + 'datetime': true, + 'datetime-local': true, + 'email': true, + 'month': true, + 'number': true, + 'password': true, + 'range': true, + 'search': true, + 'tel': true, + 'text': true, + 'time': true, + 'url': true, + 'week': true +}; + +function shouldUseInputEvent(elem) { + return ( + (elem.nodeName === 'INPUT' && supportedInputTypes[elem.type]) || + elem.nodeName === 'TEXTAREA' + ); +} + +/** + * (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) { + 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(); + } else 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 `supportedInputTypes`) + * - 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 (shouldUseInputEvent(topLevelTarget)) { + if (isInputEventSupported) { + getTargetIDFunc = getTargetIDForInputEvent; + } else { + 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; + } + } else if (handleEventFunc) { + handleEventFunc( + topLevelType, + topLevelTarget, + topLevelTargetID + ); + } + } + +}; + +module.exports = ChangeEventPlugin; diff --git a/src/eventPlugins/DefaultEventPluginOrder.js b/src/eventPlugins/DefaultEventPluginOrder.js index ab7ee4448d..4374d967d4 100644 --- a/src/eventPlugins/DefaultEventPluginOrder.js +++ b/src/eventPlugins/DefaultEventPluginOrder.js @@ -34,7 +34,7 @@ var DefaultEventPluginOrder = [ keyOf({SimpleEventPlugin: null}), keyOf({TapEventPlugin: null}), keyOf({EnterLeaveEventPlugin: null}), - keyOf({TextChangeEventPlugin: null}), + keyOf({ChangeEventPlugin: null}), keyOf({AnalyticsEventPlugin: null}) ]; diff --git a/src/eventPlugins/EnterLeaveEventPlugin.js b/src/eventPlugins/EnterLeaveEventPlugin.js index 6e149b33c3..f7a385ba18 100644 --- a/src/eventPlugins/EnterLeaveEventPlugin.js +++ b/src/eventPlugins/EnterLeaveEventPlugin.js @@ -14,95 +14,95 @@ * limitations under the License. * * @providesModule EnterLeaveEventPlugin - * @typechecks + * @typechecks static-only */ "use strict"; +var EventConstants = require('EventConstants'); var EventPropagators = require('EventPropagators'); var ExecutionEnvironment = require('ExecutionEnvironment'); -var AbstractEvent = require('AbstractEvent'); -var EventConstants = require('EventConstants'); var ReactInstanceHandles = require('ReactInstanceHandles'); +var SyntheticMouseEvent = require('SyntheticMouseEvent'); -var getDOMNodeID = require('getDOMNodeID'); +var ReactID = require('ReactID'); var keyOf = require('keyOf'); var topLevelTypes = EventConstants.topLevelTypes; var getFirstReactDOM = ReactInstanceHandles.getFirstReactDOM; -var abstractEventTypes = { +var eventTypes = { mouseEnter: {registrationName: keyOf({onMouseEnter: null})}, mouseLeave: {registrationName: keyOf({onMouseLeave: null})} }; -/** - * For almost every interaction we care about, there will be a top-level - * `mouseover` and `mouseout` event that occurs so only pay attention to one of - * the two (to avoid duplicate events). We use the `mouseout` event. - * - * However, there's one interaction where there will be no `mouseout` event to - * rely on - mousing from outside the browser *into* the chrome. We detect this - * scenario and only in that case, we use the `mouseover` event. - * - * @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 `AbstractEvent`s. - * @see {EventPluginHub.extractEvents} - */ -var extractEvents = function( - topLevelType, - topLevelTarget, - topLevelTargetID, - nativeEvent) { - if (topLevelType === topLevelTypes.topMouseOver && - (nativeEvent.relatedTarget || nativeEvent.fromElement)) { - return null; - } - if (topLevelType !== topLevelTypes.topMouseOut && - topLevelType !== topLevelTypes.topMouseOver) { - return null; // Must not be a mouse in or mouse out - ignoring. - } - - var to, from; - if (topLevelType === topLevelTypes.topMouseOut) { - to = getFirstReactDOM(nativeEvent.relatedTarget || nativeEvent.toElement) || - ExecutionEnvironment.global; - from = topLevelTarget; - } else { - to = topLevelTarget; - from = ExecutionEnvironment.global; - } - - // Nothing pertains to our managed components. - if (from === to) { - return null; - } - - var fromID = from ? getDOMNodeID(from) : ''; - var toID = to ? getDOMNodeID(to) : ''; - - var leave = AbstractEvent.getPooled( - abstractEventTypes.mouseLeave, - fromID, - nativeEvent - ); - var enter = AbstractEvent.getPooled( - abstractEventTypes.mouseEnter, - toID, - nativeEvent - ); - - EventPropagators.accumulateEnterLeaveDispatches(leave, enter, fromID, toID); - - return [leave, enter]; -}; - var EnterLeaveEventPlugin = { - abstractEventTypes: abstractEventTypes, - extractEvents: extractEvents + + eventTypes: eventTypes, + + /** + * For almost every interaction we care about, there will be both a top-level + * `mouseover` and `mouseout` event that occurs. Only use `mouseout` so that + * we do not extract duplicate events. However, moving the mouse into the + * browser from outside will not fire a `mouseout` event. In this case, we use + * the `mouseover` top-level event. + * + * @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) { + if (topLevelType === topLevelTypes.topMouseOver && + (nativeEvent.relatedTarget || nativeEvent.fromElement)) { + return null; + } + if (topLevelType !== topLevelTypes.topMouseOut && + topLevelType !== topLevelTypes.topMouseOver) { + // Must not be a mouse in or mouse out - ignoring. + return null; + } + + var from, to; + if (topLevelType === topLevelTypes.topMouseOut) { + from = topLevelTarget; + to = + getFirstReactDOM(nativeEvent.relatedTarget || nativeEvent.toElement) || + ExecutionEnvironment.global; + } else { + from = ExecutionEnvironment.global; + to = topLevelTarget; + } + + if (from === to) { + // Nothing pertains to our managed components. + return null; + } + + var fromID = from ? ReactID.getID(from) : ''; + var toID = to ? ReactID.getID(to) : ''; + + var leave = SyntheticMouseEvent.getPooled( + eventTypes.mouseLeave, + fromID, + nativeEvent + ); + var enter = SyntheticMouseEvent.getPooled( + eventTypes.mouseEnter, + toID, + nativeEvent + ); + + EventPropagators.accumulateEnterLeaveDispatches(leave, enter, fromID, toID); + return [leave, enter]; + } + }; module.exports = EnterLeaveEventPlugin; diff --git a/src/eventPlugins/ResponderEventPlugin.js b/src/eventPlugins/ResponderEventPlugin.js index 235b381211..ec55663ca4 100644 --- a/src/eventPlugins/ResponderEventPlugin.js +++ b/src/eventPlugins/ResponderEventPlugin.js @@ -18,10 +18,10 @@ "use strict"; -var AbstractEvent = require('AbstractEvent'); var EventConstants = require('EventConstants'); var EventPluginUtils = require('EventPluginUtils'); var EventPropagators = require('EventPropagators'); +var SyntheticEvent = require('SyntheticEvent'); var accumulate = require('accumulate'); var keyOf = require('keyOf'); @@ -41,11 +41,7 @@ var executeDispatchesInOrderStopAtTrue = var responderID = null; var isPressing = false; -var getResponderID = function() { - return responderID; -}; - -var abstractEventTypes = { +var eventTypes = { /** * On a `touchStart`/`mouseDown`, is it desired that this element become the * responder? @@ -116,22 +112,22 @@ var abstractEventTypes = { * `extractEvents()`. * - These events that are returned from `extractEvents` are "deferred * dispatched events". - * - When returned from `extractEvents`, deferred dispatched events - * contain an "accumulation" of deferred dispatches. - * -- These deferred dispatches are accumulated/collected before they are - * returned, but processed at a later time by the `EventPluginHub` (hence the - * name deferred). + * - When returned from `extractEvents`, deferred-dispatched events contain an + * "accumulation" of deferred dispatches. + * - These deferred dispatches are accumulated/collected before they are + * returned, but processed at a later time by the `EventPluginHub` (hence the + * name deferred). * - * In the process of returning their deferred dispatched events, event plugins + * In the process of returning their deferred-dispatched events, event plugins * themselves can dispatch events on-demand without returning them from - * `extractEvents`. Plugins might want to do this, so that they can use - * event dispatching as a tool that helps them decide which events should be - * extracted in the first place. + * `extractEvents`. Plugins might want to do this, so that they can use event + * dispatching as a tool that helps them decide which events should be extracted + * in the first place. * * "On-Demand-Dispatched Events": * - * - On-demand dispatched are not returned from `extractEvents`. - * - On-demand dispatched events are dispatched during the process of returning + * - On-demand-dispatched events are not returned from `extractEvents`. + * - On-demand-dispatched events are dispatched during the process of returning * the deferred-dispatched events. * - They should not have side effects. * - They should be avoided, and/or eventually be replaced with another @@ -158,81 +154,81 @@ var abstractEventTypes = { * - `touchStart` (`EventPluginHub` dispatches as usual) * - `responderGrant/Reject` (`EventPluginHub` dispatches as usual) * - * @returns {Accumulation} - */ - -/** * @param {string} topLevelType Record from `EventConstants`. - * @param {string} renderedTargetID ID of deepest React rendered element. + * @param {string} topLevelTargetID ID of deepest React rendered element. * @param {object} nativeEvent Native browser event. - * @return {*} An accumulation of extracted `AbstractEvent`s. + * @return {*} An accumulation of synthetic events. */ -var setResponderAndExtractTransfer = - function(topLevelType, renderedTargetID, nativeEvent) { - var type; - var shouldSetEventType = - isStartish(topLevelType) ? abstractEventTypes.startShouldSetResponder : - isMoveish(topLevelType) ? abstractEventTypes.moveShouldSetResponder : - abstractEventTypes.scrollShouldSetResponder; +function setResponderAndExtractTransfer( + topLevelType, + topLevelTargetID, + nativeEvent) { + var shouldSetEventType = + isStartish(topLevelType) ? eventTypes.startShouldSetResponder : + isMoveish(topLevelType) ? eventTypes.moveShouldSetResponder : + eventTypes.scrollShouldSetResponder; - var bubbleShouldSetFrom = responderID || renderedTargetID; - var shouldSetEvent = AbstractEvent.getPooled( - shouldSetEventType, - bubbleShouldSetFrom, - topLevelType, - nativeEvent, - AbstractEvent.normalizePointerData(nativeEvent) - ); - EventPropagators.accumulateTwoPhaseDispatches(shouldSetEvent); - var wantsResponderID = executeDispatchesInOrderStopAtTrue(shouldSetEvent); - AbstractEvent.release(shouldSetEvent); + var bubbleShouldSetFrom = responderID || topLevelTargetID; + var shouldSetEvent = SyntheticEvent.getPooled( + shouldSetEventType, + bubbleShouldSetFrom, + nativeEvent + ); + EventPropagators.accumulateTwoPhaseDispatches(shouldSetEvent); + var wantsResponderID = executeDispatchesInOrderStopAtTrue(shouldSetEvent); + if (!shouldSetEvent.isPersistent()) { + shouldSetEvent.constructor.release(shouldSetEvent); + } - if (!wantsResponderID || wantsResponderID === responderID) { - return null; - } - var extracted; - var grantEvent = AbstractEvent.getPooled( - abstractEventTypes.responderGrant, - wantsResponderID, - topLevelType, + if (!wantsResponderID || wantsResponderID === responderID) { + return null; + } + var extracted; + var grantEvent = SyntheticEvent.getPooled( + eventTypes.responderGrant, + wantsResponderID, + nativeEvent + ); + + EventPropagators.accumulateDirectDispatches(grantEvent); + if (responderID) { + var terminationRequestEvent = SyntheticEvent.getPooled( + eventTypes.responderTerminationRequest, + responderID, nativeEvent ); - - EventPropagators.accumulateDirectDispatches(grantEvent); - if (responderID) { - type = abstractEventTypes.responderTerminationRequest; - var terminationRequestEvent = AbstractEvent.getPooled(type, responderID); - EventPropagators.accumulateDirectDispatches(terminationRequestEvent); - var shouldSwitch = !hasDispatches(terminationRequestEvent) || - executeDirectDispatch(terminationRequestEvent); - AbstractEvent.release(terminationRequestEvent); - if (shouldSwitch) { - var terminateType = abstractEventTypes.responderTerminate; - var terminateEvent = AbstractEvent.getPooled( - terminateType, - responderID, - topLevelType, - nativeEvent - ); - EventPropagators.accumulateDirectDispatches(terminateEvent); - extracted = accumulate(extracted, [grantEvent, terminateEvent]); - responderID = wantsResponderID; - } else { - var rejectEvent = AbstractEvent.getPooled( - abstractEventTypes.responderReject, - wantsResponderID, - topLevelType, - nativeEvent - ); - EventPropagators.accumulateDirectDispatches(rejectEvent); - extracted = accumulate(extracted, rejectEvent); - } - } else { - extracted = accumulate(extracted, grantEvent); - responderID = wantsResponderID; + EventPropagators.accumulateDirectDispatches(terminationRequestEvent); + var shouldSwitch = !hasDispatches(terminationRequestEvent) || + executeDirectDispatch(terminationRequestEvent); + if (!terminationRequestEvent.isPersistent()) { + terminationRequestEvent.constructor.release(terminationRequestEvent); } - return extracted; - }; + + if (shouldSwitch) { + var terminateType = eventTypes.responderTerminate; + var terminateEvent = SyntheticEvent.getPooled( + terminateType, + responderID, + nativeEvent + ); + EventPropagators.accumulateDirectDispatches(terminateEvent); + extracted = accumulate(extracted, [grantEvent, terminateEvent]); + responderID = wantsResponderID; + } else { + var rejectEvent = SyntheticEvent.getPooled( + eventTypes.responderReject, + wantsResponderID, + nativeEvent + ); + EventPropagators.accumulateDirectDispatches(rejectEvent); + extracted = accumulate(extracted, rejectEvent); + } + } else { + extracted = accumulate(extracted, grantEvent); + responderID = wantsResponderID; + } + return extracted; +} /** * A transfer is a negotiation between a currently set responder and the next @@ -241,9 +237,8 @@ var setResponderAndExtractTransfer = * currently a responder set (in other words as long as the user is pressing * down). * - * @param {EventConstants.topLevelTypes} topLevelType - * @return {boolean} Whether or not a transfer of responder could possibly - * occur. + * @param {string} topLevelType Record from `EventConstants`. + * @return {boolean} True if a transfer of responder could possibly occur. */ function canTriggerTransfer(topLevelType) { return topLevelType === EventConstants.topLevelTypes.topScroll || @@ -251,69 +246,71 @@ function canTriggerTransfer(topLevelType) { (isPressing && isMoveish(topLevelType)); } -/** - * @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 `AbstractEvent`s. - * @see {EventPluginHub.extractEvents} - */ -var extractEvents = function( - topLevelType, - topLevelTarget, - topLevelTargetID, - nativeEvent) { - var extracted; - // Must have missed an end event - reset the state here. - if (responderID && isStartish(topLevelType)) { - responderID = null; - } - if (isStartish(topLevelType)) { - isPressing = true; - } else if (isEndish(topLevelType)) { - isPressing = false; - } - if (canTriggerTransfer(topLevelType)) { - var transfer = setResponderAndExtractTransfer( - topLevelType, - topLevelTargetID, - nativeEvent - ); - if (transfer) { - extracted = accumulate(extracted, transfer); - } - } - // Now that we know the responder is set correctly, we can dispatch - // responder type events (directly to the responder). - var type = isMoveish(topLevelType) ? abstractEventTypes.responderMove : - isEndish(topLevelType) ? abstractEventTypes.responderRelease : - isStartish(topLevelType) ? abstractEventTypes.responderStart : null; - if (type) { - var data = AbstractEvent.normalizePointerData(nativeEvent); - var gesture = AbstractEvent.getPooled( - type, - responderID, - nativeEvent, - data - ); - EventPropagators.accumulateDirectDispatches(gesture); - extracted = accumulate(extracted, gesture); - } - if (type === abstractEventTypes.responderRelease) { - responderID = null; - } - return extracted; -}; - /** * Event plugin for formalizing the negotiation between claiming locks on * receiving touches. */ var ResponderEventPlugin = { - abstractEventTypes: abstractEventTypes, - extractEvents: extractEvents, - getResponderID: getResponderID + + getResponderID: function() { + return responderID; + }, + + 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 extracted; + // Must have missed an end event - reset the state here. + if (responderID && isStartish(topLevelType)) { + responderID = null; + } + if (isStartish(topLevelType)) { + isPressing = true; + } else if (isEndish(topLevelType)) { + isPressing = false; + } + if (canTriggerTransfer(topLevelType)) { + var transfer = setResponderAndExtractTransfer( + topLevelType, + topLevelTargetID, + nativeEvent + ); + if (transfer) { + extracted = accumulate(extracted, transfer); + } + } + // Now that we know the responder is set correctly, we can dispatch + // responder type events (directly to the responder). + var type = isMoveish(topLevelType) ? eventTypes.responderMove : + isEndish(topLevelType) ? eventTypes.responderRelease : + isStartish(topLevelType) ? eventTypes.responderStart : null; + if (type) { + var gesture = SyntheticEvent.getPooled( + type, + responderID || '', + nativeEvent + ); + EventPropagators.accumulateDirectDispatches(gesture); + extracted = accumulate(extracted, gesture); + } + if (type === eventTypes.responderRelease) { + responderID = null; + } + return extracted; + } + }; module.exports = ResponderEventPlugin; diff --git a/src/eventPlugins/SimpleEventPlugin.js b/src/eventPlugins/SimpleEventPlugin.js index 490dce2172..b11de71145 100644 --- a/src/eventPlugins/SimpleEventPlugin.js +++ b/src/eventPlugins/SimpleEventPlugin.js @@ -18,201 +18,236 @@ "use strict"; -var AbstractEvent = require('AbstractEvent'); var EventConstants = require('EventConstants'); var EventPropagators = require('EventPropagators'); +var SyntheticEvent = require('SyntheticEvent'); +var SyntheticFocusEvent = require('SyntheticFocusEvent'); +var SyntheticKeyboardEvent = require('SyntheticKeyboardEvent'); +var SyntheticMouseEvent = require('SyntheticMouseEvent'); +var SyntheticMutationEvent = require('SyntheticMutationEvent'); +var SyntheticTouchEvent = require('SyntheticTouchEvent'); +var SyntheticUIEvent = require('SyntheticUIEvent'); +var SyntheticWheelEvent = require('SyntheticWheelEvent'); +var invariant = require('invariant'); var keyOf = require('keyOf'); var topLevelTypes = EventConstants.topLevelTypes; -var SimpleEventPlugin = { - abstractEventTypes: { - // Note: We do not allow listening to mouseOver events. Instead, use the - // onMouseEnter/onMouseLeave created by `EnterLeaveEventPlugin`. - mouseDown: { - phasedRegistrationNames: { - bubbled: keyOf({onMouseDown: true}), - captured: keyOf({onMouseDownCapture: true}) - } - }, - mouseUp: { - phasedRegistrationNames: { - bubbled: keyOf({onMouseUp: true}), - captured: keyOf({onMouseUpCapture: true}) - } - }, - mouseMove: { - phasedRegistrationNames: { - bubbled: keyOf({onMouseMove: true}), - captured: keyOf({onMouseMoveCapture: true}) - } - }, - doubleClick: { - phasedRegistrationNames: { - bubbled: keyOf({onDoubleClick: true}), - captured: keyOf({onDoubleClickCapture: true}) - } - }, - click: { - phasedRegistrationNames: { - bubbled: keyOf({onClick: true}), - captured: keyOf({onClickCapture: true}) - } - }, - wheel: { - phasedRegistrationNames: { - bubbled: keyOf({onWheel: true}), - captured: keyOf({onWheelCapture: true}) - } - }, - touchStart: { - phasedRegistrationNames: { - bubbled: keyOf({onTouchStart: true}), - captured: keyOf({onTouchStartCapture: true}) - } - }, - touchEnd: { - phasedRegistrationNames: { - bubbled: keyOf({onTouchEnd: true}), - captured: keyOf({onTouchEndCapture: true}) - } - }, - touchCancel: { - phasedRegistrationNames: { - bubbled: keyOf({onTouchCancel: true}), - captured: keyOf({onTouchCancelCapture: true}) - } - }, - touchMove: { - phasedRegistrationNames: { - bubbled: keyOf({onTouchMove: true}), - captured: keyOf({onTouchMoveCapture: true}) - } - }, - keyUp: { - phasedRegistrationNames: { - bubbled: keyOf({onKeyUp: true}), - captured: keyOf({onKeyUpCapture: true}) - } - }, - keyPress: { - phasedRegistrationNames: { - bubbled: keyOf({onKeyPress: true}), - captured: keyOf({onKeyPressCapture: true}) - } - }, - keyDown: { - phasedRegistrationNames: { - bubbled: keyOf({onKeyDown: true}), - captured: keyOf({onKeyDownCapture: true}) - } - }, - input: { - phasedRegistrationNames: { - bubbled: keyOf({onInput: true}), - captured: keyOf({onInputCapture: true}) - } - }, - focus: { - phasedRegistrationNames: { - bubbled: keyOf({onFocus: true}), - captured: keyOf({onFocusCapture: true}) - } - }, - blur: { - phasedRegistrationNames: { - bubbled: keyOf({onBlur: true}), - captured: keyOf({onBlurCapture: true}) - } - }, - scroll: { - phasedRegistrationNames: { - bubbled: keyOf({onScroll: true}), - captured: keyOf({onScrollCapture: true}) - } - }, - change: { - phasedRegistrationNames: { - bubbled: keyOf({onChange: true}), - captured: keyOf({onChangeCapture: true}) - } - }, - submit: { - phasedRegistrationNames: { - bubbled: keyOf({onSubmit: true}), - captured: keyOf({onSubmitCapture: true}) - } - }, - DOMCharacterDataModified: { - phasedRegistrationNames: { - bubbled: keyOf({onDOMCharacterDataModified: true}), - captured: keyOf({onDOMCharacterDataModifiedCapture: true}) - } - }, - drag: { - phasedRegistrationNames: { - bubbled: keyOf({onDrag: true}), - captured: keyOf({onDragCapture: true}) - } - }, - dragEnd: { - phasedRegistrationNames: { - bubbled: keyOf({onDragEnd: true}), - captured: keyOf({onDragEndCapture: true}) - } - }, - dragEnter: { - phasedRegistrationNames: { - bubbled: keyOf({onDragEnter: true}), - captured: keyOf({onDragEnterCapture: true}) - } - }, - dragExit: { - phasedRegistrationNames: { - bubbled: keyOf({onDragExit: true}), - captured: keyOf({onDragExitCapture: true}) - } - }, - dragLeave: { - phasedRegistrationNames: { - bubbled: keyOf({onDragLeave: true}), - captured: keyOf({onDragLeaveCapture: true}) - } - }, - dragOver: { - phasedRegistrationNames: { - bubbled: keyOf({onDragOver: true}), - captured: keyOf({onDragOverCapture: true}) - } - }, - dragStart: { - phasedRegistrationNames: { - bubbled: keyOf({onDragStart: true}), - captured: keyOf({onDragStartCapture: true}) - } - }, - drop: { - phasedRegistrationNames: { - bubbled: keyOf({onDrop: true}), - captured: keyOf({onDropCapture: true}) - } +var eventTypes = { + blur: { + phasedRegistrationNames: { + bubbled: keyOf({onBlur: true}), + captured: keyOf({onBlurCapture: true}) } }, + click: { + phasedRegistrationNames: { + bubbled: keyOf({onClick: true}), + captured: keyOf({onClickCapture: true}) + } + }, + doubleClick: { + phasedRegistrationNames: { + bubbled: keyOf({onDoubleClick: true}), + captured: keyOf({onDoubleClickCapture: true}) + } + }, + drag: { + phasedRegistrationNames: { + bubbled: keyOf({onDrag: true}), + captured: keyOf({onDragCapture: true}) + } + }, + dragEnd: { + phasedRegistrationNames: { + bubbled: keyOf({onDragEnd: true}), + captured: keyOf({onDragEndCapture: true}) + } + }, + dragEnter: { + phasedRegistrationNames: { + bubbled: keyOf({onDragEnter: true}), + captured: keyOf({onDragEnterCapture: true}) + } + }, + dragExit: { + phasedRegistrationNames: { + bubbled: keyOf({onDragExit: true}), + captured: keyOf({onDragExitCapture: true}) + } + }, + dragLeave: { + phasedRegistrationNames: { + bubbled: keyOf({onDragLeave: true}), + captured: keyOf({onDragLeaveCapture: true}) + } + }, + dragOver: { + phasedRegistrationNames: { + bubbled: keyOf({onDragOver: true}), + captured: keyOf({onDragOverCapture: true}) + } + }, + dragStart: { + phasedRegistrationNames: { + bubbled: keyOf({onDragStart: true}), + captured: keyOf({onDragStartCapture: true}) + } + }, + drop: { + phasedRegistrationNames: { + bubbled: keyOf({onDrop: true}), + captured: keyOf({onDropCapture: true}) + } + }, + DOMCharacterDataModified: { + phasedRegistrationNames: { + bubbled: keyOf({onDOMCharacterDataModified: true}), + captured: keyOf({onDOMCharacterDataModifiedCapture: true}) + } + }, + focus: { + phasedRegistrationNames: { + bubbled: keyOf({onFocus: true}), + captured: keyOf({onFocusCapture: true}) + } + }, + input: { + phasedRegistrationNames: { + bubbled: keyOf({onInput: true}), + captured: keyOf({onInputCapture: true}) + } + }, + keyDown: { + phasedRegistrationNames: { + bubbled: keyOf({onKeyDown: true}), + captured: keyOf({onKeyDownCapture: true}) + } + }, + keyPress: { + phasedRegistrationNames: { + bubbled: keyOf({onKeyPress: true}), + captured: keyOf({onKeyPressCapture: true}) + } + }, + keyUp: { + phasedRegistrationNames: { + bubbled: keyOf({onKeyUp: true}), + captured: keyOf({onKeyUpCapture: true}) + } + }, + // Note: We do not allow listening to mouseOver events. Instead, use the + // onMouseEnter/onMouseLeave created by `EnterLeaveEventPlugin`. + mouseDown: { + phasedRegistrationNames: { + bubbled: keyOf({onMouseDown: true}), + captured: keyOf({onMouseDownCapture: true}) + } + }, + mouseMove: { + phasedRegistrationNames: { + bubbled: keyOf({onMouseMove: true}), + captured: keyOf({onMouseMoveCapture: true}) + } + }, + mouseUp: { + phasedRegistrationNames: { + bubbled: keyOf({onMouseUp: true}), + captured: keyOf({onMouseUpCapture: true}) + } + }, + scroll: { + phasedRegistrationNames: { + bubbled: keyOf({onScroll: true}), + captured: keyOf({onScrollCapture: true}) + } + }, + submit: { + phasedRegistrationNames: { + bubbled: keyOf({onSubmit: true}), + captured: keyOf({onSubmitCapture: true}) + } + }, + touchCancel: { + phasedRegistrationNames: { + bubbled: keyOf({onTouchCancel: true}), + captured: keyOf({onTouchCancelCapture: true}) + } + }, + touchEnd: { + phasedRegistrationNames: { + bubbled: keyOf({onTouchEnd: true}), + captured: keyOf({onTouchEndCapture: true}) + } + }, + touchMove: { + phasedRegistrationNames: { + bubbled: keyOf({onTouchMove: true}), + captured: keyOf({onTouchMoveCapture: true}) + } + }, + touchStart: { + phasedRegistrationNames: { + bubbled: keyOf({onTouchStart: true}), + captured: keyOf({onTouchStartCapture: true}) + } + }, + wheel: { + phasedRegistrationNames: { + bubbled: keyOf({onWheel: true}), + captured: keyOf({onWheelCapture: true}) + } + } +}; + +var topLevelEventsToDispatchConfig = { + topBlur: eventTypes.blur, + topClick: eventTypes.click, + topDoubleClick: eventTypes.doubleClick, + topDOMCharacterDataModified: eventTypes.DOMCharacterDataModified, + topDrag: eventTypes.drag, + topDragEnd: eventTypes.dragEnd, + topDragEnter: eventTypes.dragEnter, + topDragExit: eventTypes.dragExit, + topDragLeave: eventTypes.dragLeave, + topDragOver: eventTypes.dragOver, + topDragStart: eventTypes.dragStart, + topDrop: eventTypes.drop, + topFocus: eventTypes.focus, + topInput: eventTypes.input, + topKeyDown: eventTypes.keyDown, + topKeyPress: eventTypes.keyPress, + topKeyUp: eventTypes.keyUp, + topMouseDown: eventTypes.mouseDown, + topMouseMove: eventTypes.mouseMove, + topMouseUp: eventTypes.mouseUp, + topScroll: eventTypes.scroll, + topSubmit: eventTypes.submit, + topTouchCancel: eventTypes.touchCancel, + topTouchEnd: eventTypes.touchEnd, + topTouchMove: eventTypes.touchMove, + topTouchStart: eventTypes.touchStart, + topWheel: eventTypes.wheel +}; + +var SimpleEventPlugin = { + + eventTypes: eventTypes, /** * Same as the default implementation, except cancels the event when return * value is false. * - * @param {AbstractEvent} AbstractEvent to handle - * @param {function} Application-level callback - * @param {string} domID DOM id to pass to the callback. + * @param {object} Event to be dispatched. + * @param {function} Application-level callback. + * @param {string} domID DOM ID to pass to the callback. */ - executeDispatch: function(abstractEvent, listener, domID) { - var returnValue = listener(abstractEvent, domID); + executeDispatch: function(event, listener, domID) { + var returnValue = listener(event, domID); if (returnValue === false) { - abstractEvent.stopPropagation(); - abstractEvent.preventDefault(); + event.stopPropagation(); + event.preventDefault(); } }, @@ -221,7 +256,7 @@ var SimpleEventPlugin = { * @param {DOMEventTarget} topLevelTarget The listening component root node. * @param {string} topLevelTargetID ID of `topLevelTarget`. * @param {object} nativeEvent Native browser event. - * @return {*} An accumulation of `AbstractEvent`s. + * @return {*} An accumulation of synthetic events. * @see {EventPluginHub.extractEvents} */ extractEvents: function( @@ -229,29 +264,29 @@ var SimpleEventPlugin = { topLevelTarget, topLevelTargetID, nativeEvent) { - var data; - var abstractEventType = - SimpleEventPlugin.topLevelTypesToAbstract[topLevelType]; - if (!abstractEventType) { + var dispatchConfig = topLevelEventsToDispatchConfig[topLevelType]; + if (!dispatchConfig) { return null; } + var EventConstructor; switch(topLevelType) { - case topLevelTypes.topWheel: - data = AbstractEvent.normalizeMouseWheelData(nativeEvent); + case topLevelTypes.topInput: + case topLevelTypes.topSubmit: + // HTML Events + // @see http://www.w3.org/TR/html5/index.html#events-0 + EventConstructor = SyntheticEvent; break; - case topLevelTypes.topScroll: - data = AbstractEvent.normalizeScrollDataFromTarget(topLevelTarget); + case topLevelTypes.topKeyDown: + case topLevelTypes.topKeyPress: + case topLevelTypes.topKeyUp: + EventConstructor = SyntheticKeyboardEvent; + break; + case topLevelTypes.topBlur: + case topLevelTypes.topFocus: + EventConstructor = SyntheticFocusEvent; break; case topLevelTypes.topClick: case topLevelTypes.topDoubleClick: - case topLevelTypes.topChange: - case topLevelTypes.topDOMCharacterDataModified: - case topLevelTypes.topMouseDown: - case topLevelTypes.topMouseUp: - case topLevelTypes.topMouseMove: - case topLevelTypes.topTouchMove: - case topLevelTypes.topTouchStart: - case topLevelTypes.topTouchEnd: case topLevelTypes.topDrag: case topLevelTypes.topDragEnd: case topLevelTypes.topDragEnter: @@ -260,53 +295,41 @@ var SimpleEventPlugin = { case topLevelTypes.topDragOver: case topLevelTypes.topDragStart: case topLevelTypes.topDrop: - data = AbstractEvent.normalizePointerData(nativeEvent); - // todo: Use AbstractEvent.normalizeDragEventData for drag/drop? + case topLevelTypes.topMouseDown: + case topLevelTypes.topMouseMove: + case topLevelTypes.topMouseUp: + EventConstructor = SyntheticMouseEvent; + break; + case topLevelTypes.topDOMCharacterDataModified: + EventConstructor = SyntheticMutationEvent; + break; + case topLevelTypes.topTouchCancel: + case topLevelTypes.topTouchEnd: + case topLevelTypes.topTouchMove: + case topLevelTypes.topTouchStart: + EventConstructor = SyntheticTouchEvent; + break; + case topLevelTypes.topScroll: + EventConstructor = SyntheticUIEvent; + break; + case topLevelTypes.topWheel: + EventConstructor = SyntheticWheelEvent; break; - default: - data = null; } - var abstractEvent = AbstractEvent.getPooled( - abstractEventType, - topLevelTargetID, - nativeEvent, - data + invariant( + EventConstructor, + 'SimpleEventPlugin: Unhandled event type, `%s`.', + topLevelType ); - EventPropagators.accumulateTwoPhaseDispatches(abstractEvent); - return abstractEvent; + var event = EventConstructor.getPooled( + dispatchConfig, + topLevelTargetID, + nativeEvent + ); + EventPropagators.accumulateTwoPhaseDispatches(event); + return event; } -}; -SimpleEventPlugin.topLevelTypesToAbstract = { - topMouseDown: SimpleEventPlugin.abstractEventTypes.mouseDown, - topMouseUp: SimpleEventPlugin.abstractEventTypes.mouseUp, - topMouseMove: SimpleEventPlugin.abstractEventTypes.mouseMove, - topClick: SimpleEventPlugin.abstractEventTypes.click, - topDoubleClick: SimpleEventPlugin.abstractEventTypes.doubleClick, - topWheel: SimpleEventPlugin.abstractEventTypes.wheel, - topTouchStart: SimpleEventPlugin.abstractEventTypes.touchStart, - topTouchEnd: SimpleEventPlugin.abstractEventTypes.touchEnd, - topTouchMove: SimpleEventPlugin.abstractEventTypes.touchMove, - topTouchCancel: SimpleEventPlugin.abstractEventTypes.touchCancel, - topKeyUp: SimpleEventPlugin.abstractEventTypes.keyUp, - topKeyPress: SimpleEventPlugin.abstractEventTypes.keyPress, - topKeyDown: SimpleEventPlugin.abstractEventTypes.keyDown, - topInput: SimpleEventPlugin.abstractEventTypes.input, - topFocus: SimpleEventPlugin.abstractEventTypes.focus, - topBlur: SimpleEventPlugin.abstractEventTypes.blur, - topScroll: SimpleEventPlugin.abstractEventTypes.scroll, - topChange: SimpleEventPlugin.abstractEventTypes.change, - topSubmit: SimpleEventPlugin.abstractEventTypes.submit, - topDOMCharacterDataModified: - SimpleEventPlugin.abstractEventTypes.DOMCharacterDataModified, - topDrag: SimpleEventPlugin.abstractEventTypes.drag, - topDragEnd: SimpleEventPlugin.abstractEventTypes.dragEnd, - topDragEnter: SimpleEventPlugin.abstractEventTypes.dragEnter, - topDragExit: SimpleEventPlugin.abstractEventTypes.dragExit, - topDragLeave: SimpleEventPlugin.abstractEventTypes.dragLeave, - topDragOver: SimpleEventPlugin.abstractEventTypes.dragOver, - topDragStart: SimpleEventPlugin.abstractEventTypes.dragStart, - topDrop: SimpleEventPlugin.abstractEventTypes.drop }; module.exports = SimpleEventPlugin; diff --git a/src/eventPlugins/TapEventPlugin.js b/src/eventPlugins/TapEventPlugin.js index c36732881a..c748da79e7 100644 --- a/src/eventPlugins/TapEventPlugin.js +++ b/src/eventPlugins/TapEventPlugin.js @@ -14,30 +14,54 @@ * limitations under the License. * * @providesModule TapEventPlugin - * @typechecks + * @typechecks static-only */ "use strict"; -var AbstractEvent = require('AbstractEvent'); var EventPluginUtils = require('EventPluginUtils'); var EventPropagators = require('EventPropagators'); +var SyntheticUIEvent = require('SyntheticUIEvent'); +var TouchEventUtils = require('TouchEventUtils'); +var ViewportMetrics = require('ViewportMetrics'); var keyOf = require('keyOf'); var isStartish = EventPluginUtils.isStartish; var isEndish = EventPluginUtils.isEndish; -var storePageCoordsIn = EventPluginUtils.storePageCoordsIn; -var eventDistance = EventPluginUtils.eventDistance; /** - * The number of pixels that are tolerated in between a touchStart and - * touchEnd in order to still be considered a 'tap' event. + * Number of pixels that are tolerated in between a `touchStart` and `touchEnd` + * in order to still be considered a 'tap' event. */ var tapMoveThreshold = 10; var startCoords = {x: null, y: null}; -var abstractEventTypes = { +var Axis = { + x: {page: 'pageX', client: 'clientX', envScroll: 'currentPageScrollLeft'}, + y: {page: 'pageY', client: 'clientY', envScroll: 'currentPageScrollTop'} +}; + +function getAxisCoordOfEvent(axis, nativeEvent) { + var singleTouch = TouchEventUtils.extractSingleTouch(nativeEvent); + if (singleTouch) { + return singleTouch[axis.page]; + } + return axis.page in nativeEvent ? + nativeEvent[axis.page] : + nativeEvent[axis.client] + ViewportMetrics[axis.envScroll]; +} + +function getDistance(coords, nativeEvent) { + var pageX = getAxisCoordOfEvent(Axis.x, nativeEvent); + var pageY = getAxisCoordOfEvent(Axis.y, nativeEvent); + return Math.pow( + Math.pow(pageX - coords.x, 2) + Math.pow(pageY - coords.y, 2), + 0.5 + ); +} + +var eventTypes = { touchTap: { phasedRegistrationNames: { bubbled: keyOf({onTouchTap: null}), @@ -46,46 +70,48 @@ var abstractEventTypes = { } }; -/** - * @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 `AbstractEvent`s. - * @see {EventPluginHub.extractEvents} - */ -var extractEvents = function( - topLevelType, - topLevelTarget, - topLevelTargetID, - nativeEvent) { - if (!isStartish(topLevelType) && !isEndish(topLevelType)) { - return; - } - var abstractEvent; - var dist = eventDistance(startCoords, nativeEvent); - if (isEndish(topLevelType) && dist < tapMoveThreshold) { - abstractEvent = AbstractEvent.getPooled( - abstractEventTypes.touchTap, - topLevelTargetID, - nativeEvent - ); - } - if (isStartish(topLevelType)) { - storePageCoordsIn(startCoords, nativeEvent); - } else if (isEndish(topLevelType)) { - startCoords.x = 0; - startCoords.y = 0; - } - EventPropagators.accumulateTwoPhaseDispatches(abstractEvent); - return abstractEvent; -}; - var TapEventPlugin = { + tapMoveThreshold: tapMoveThreshold, - startCoords: startCoords, - abstractEventTypes: abstractEventTypes, - extractEvents: extractEvents + + 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) { + if (!isStartish(topLevelType) && !isEndish(topLevelType)) { + return null; + } + var event = null; + var distance = getDistance(startCoords, nativeEvent); + if (isEndish(topLevelType) && distance < tapMoveThreshold) { + event = SyntheticUIEvent.getPooled( + eventTypes.touchTap, + topLevelTargetID, + nativeEvent + ); + } + if (isStartish(topLevelType)) { + startCoords.x = getAxisCoordOfEvent(Axis.x, nativeEvent); + startCoords.y = getAxisCoordOfEvent(Axis.y, nativeEvent); + } else if (isEndish(topLevelType)) { + startCoords.x = 0; + startCoords.y = 0; + } + EventPropagators.accumulateTwoPhaseDispatches(event); + return event; + } + }; module.exports = TapEventPlugin; diff --git a/src/eventPlugins/TextChangeEventPlugin.js b/src/eventPlugins/TextChangeEventPlugin.js deleted file mode 100644 index ced2eb1093..0000000000 --- a/src/eventPlugins/TextChangeEventPlugin.js +++ /dev/null @@ -1,253 +0,0 @@ -/** - * 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 TextChangeEventPlugin - */ - -"use strict"; - -var AbstractEvent = require('AbstractEvent'); -var EventConstants = require('EventConstants'); -var EventPluginHub = require('EventPluginHub'); -var EventPropagators = require('EventPropagators'); -var ExecutionEnvironment = require('ExecutionEnvironment'); - -var isEventSupported = require('isEventSupported'); -var keyOf = require('keyOf'); - -var topLevelTypes = EventConstants.topLevelTypes; - -var abstractEventTypes = { - textChange: { - phasedRegistrationNames: { - bubbled: keyOf({onTextChange: null}), - captured: keyOf({onTextChangeCapture: null}) - } - } -}; - -var isInputSupported; -if (ExecutionEnvironment.canUseDOM) { - // IE9 claims to support the input event but fails to trigger it when - // deleting text, so we ignore its input events - isInputSupported = isEventSupported('input') && ( - !("documentMode" in document) || document.documentMode > 9 - ); -} - -var hasInputCapabilities = function(elem) { - // The HTML5 spec lists many more types than `text` and `password` on which - // the input event is triggered but none of them exist in old IE, so we don't - // check them here. - // TODO: