├── .editorconfig ├── .gitignore ├── .npmignore ├── .travis.yml ├── HISTORY.md ├── README.md ├── examples ├── alert.html ├── alert.js ├── hide-todo.html ├── hide-todo.js ├── todo.html └── todo.js ├── index.js ├── package.json ├── src ├── CSSCore.jsx ├── CSSTransitionGroup.jsx ├── CSSTransitionGroupChild.jsx ├── ReactTransitionChildMapping.jsx ├── ReactTransitionEvents.jsx └── index.js └── tests ├── CSSTransitionGroup.spec.js ├── index.spec.css ├── index.spec.js └── runner.html /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | # Unix-style newlines with a newline ending every file 5 | [*.{js,css}] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 2 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | *.log 3 | .idea/ 4 | .ipr 5 | .iws 6 | *~ 7 | ~* 8 | *.diff 9 | *.patch 10 | *.bak 11 | .DS_Store 12 | Thumbs.db 13 | .project 14 | .*proj 15 | .svn/ 16 | *.swp 17 | *.swo 18 | *.pyc 19 | *.pyo 20 | .build 21 | node_modules 22 | .cache 23 | dist 24 | assets/**/*.css 25 | build 26 | lib -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | build/ 2 | *.cfg 3 | nohup.out 4 | *.iml 5 | .idea/ 6 | .ipr 7 | .iws 8 | *~ 9 | ~* 10 | *.diff 11 | *.log 12 | *.patch 13 | *.bak 14 | .DS_Store 15 | Thumbs.db 16 | .project 17 | .*proj 18 | .svn/ 19 | *.swp 20 | out/ 21 | .build 22 | node_modules 23 | .cache 24 | examples 25 | tests 26 | src 27 | /index.js 28 | .* 29 | assets/**/*.less -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | notifications: 4 | email: 5 | - yiminghe@gmail.com 6 | 7 | node_js: 8 | - 0.12 9 | 10 | before_install: 11 | - | 12 | if ! git diff --name-only $TRAVIS_COMMIT_RANGE | grep -qvE '(\.md$)|(^(docs|examples))/' 13 | then 14 | echo "Only docs were updated, stopping build process." 15 | exit 16 | fi 17 | npm install mocha-phantomjs -g 18 | phantomjs --version 19 | 20 | script: 21 | - | 22 | if [ "$TEST_TYPE" = test ]; then 23 | npm test 24 | else 25 | npm run $TEST_TYPE 26 | fi 27 | 28 | env: 29 | matrix: 30 | - TEST_TYPE=lint 31 | - TEST_TYPE=browser-test 32 | - TEST_TYPE=browser-test-cover 33 | - TEST_TYPE=saucelabs 34 | global: 35 | - secure: IUflgQ8L65l4Xfn/ra5j/kH/nyxmZuz7Pf/qAqZl32da6OMzt3iCofpcM0RNpZtJcJoGkcE3yZpc5wDEEKUa8l+mUQxC7oeKUrJO0bit4KqO/2Jaa2QMyt0xd3agPs0vdnsz0ZHkNuB3iUjwY8sEDOdliVLw+3gTbI1mmPBEA4w= 36 | - secure: e/j2zwyPTIXZFU2DW3WEj53Twem8XV90chOnoqfNAk+7+xMW/d5xQY2CKueuBEMKL9QdIcaZDOWf1WHd702tkoTQd/EGiJhsN8UcRvF3i9wXxOxuayU5xyB/34F7jjo1v+THgZGLNqykz60bUqj+Z5vmB4xPs6uWGzlLgic1cQQ= 37 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # History 2 | ---- 3 | 4 | ## 2.1.0 / 2015-05-19 5 | 6 | `new` add exclusive prop 7 | 8 | ## 2.0.0 / 2015-05-19 9 | 10 | `new` [#2](https://github.com/react-component/css-transition-group/issues/2) add showProp prop -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is deprecated in favor of https://github.com/react-component/animate 2 | -------------------------------------------------------------------------------- /examples/alert.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-component/css-transition-group/c654070ebf14a2625255e4e96b86b1690f5da996/examples/alert.html -------------------------------------------------------------------------------- /examples/alert.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var style = '.alert-outer{\ 4 | position: fixed;\ 5 | width:100%;\ 6 | top: 50px;\ 7 | z-index: 9999;\ 8 | }\ 9 | \ 10 | .alert-outer .alert {\ 11 | background:yellow;\ 12 | width: 600px;\ 13 | padding:20px;\ 14 | margin-left:auto;\ 15 | margin-right:auto;\ 16 | }\ 17 | \ 18 | .alert-outer p{\ 19 | padding: 15px;\ 20 | }\ 21 | \ 22 | .alert-anim-enter {\ 23 | opacity: 0.01;\ 24 | transition: opacity 1s ease-in;\ 25 | -webkit-transition: opacity 1s ease-in;\ 26 | }\ 27 | \ 28 | .alert-anim-enter.alert-anim-enter-active {\ 29 | opacity: 1;\ 30 | }\ 31 | \ 32 | .alert-anim-leave {\ 33 | opacity: 1;\ 34 | transition: opacity 1s ease-in;\ 35 | -webkit-transition: opacity 1s ease-in;\ 36 | }\ 37 | \ 38 | .alert-anim-leave.alert-anim-leave-active {\ 39 | opacity: 0.01;\ 40 | }'; 41 | 42 | var React = require('react'); 43 | var CSSTransitionGroup = require('rc-css-transition-group'); 44 | var seed = 0; 45 | 46 | var Alert = React.createClass({ 47 | protoTypes: { 48 | time: React.PropTypes.number, 49 | type: React.PropTypes.number, 50 | str: React.PropTypes.string, 51 | onEnd: React.PropTypes.func 52 | }, 53 | 54 | getDefaultProps: function () { 55 | return { 56 | onEnd: function () { 57 | }, 58 | time: 2000, 59 | type: 'success' 60 | } 61 | }, 62 | 63 | componentDidMount: function () { 64 | var props = this.props; 65 | setTimeout(function () { 66 | props.onEnd(); 67 | }, props.time); 68 | }, 69 | 70 | render: function () { 71 | var props = this.props; 72 | return
{props.str}
; 73 | } 74 | }); 75 | 76 | 77 | var AlertGroup = React.createClass({ 78 | getInitialState: function () { 79 | return { 80 | alerts: [] 81 | } 82 | }, 83 | addAlert: function (a) { 84 | this.setState({ 85 | alerts: this.state.alerts.concat(a) 86 | }); 87 | }, 88 | onEnd: function (key) { 89 | var alerts = this.state.alerts; 90 | var ret = []; 91 | var target; 92 | alerts.forEach(function (a) { 93 | if (a.key === key) { 94 | target = a; 95 | } else { 96 | ret.push(a); 97 | } 98 | }); 99 | if (target) { 100 | this.setState({ 101 | alerts: ret 102 | }, function () { 103 | if (target.callback) { 104 | target.callback(); 105 | } 106 | }) 107 | } 108 | }, 109 | render: function () { 110 | var alerts = this.state.alerts; 111 | var self = this; 112 | var children = alerts.map(function (a) { 113 | if (!a.key) { 114 | seed++; 115 | a.key = seed + ''; 116 | } 117 | return 118 | }); 119 | return
120 | {children} 121 |
; 122 | } 123 | }); 124 | 125 | var alertGroup; 126 | 127 | function alert(str, time, type, callback) { 128 | if (!alertGroup) { 129 | var div = document.createElement('div'); 130 | document.body.appendChild(div); 131 | alertGroup = React.render(, div); 132 | } 133 | alertGroup.addAlert({ 134 | str: str, 135 | time: time, 136 | type: type, 137 | callback: callback 138 | }); 139 | } 140 | 141 | function onClick() { 142 | for (var i = 0; i < 4; i++) { 143 | (function (i) { 144 | setTimeout(function () { 145 | alert(i); 146 | }, 1000 * i); 147 | })(i); 148 | } 149 | } 150 | 151 | React.render(
152 |

notification

153 | 154 | 155 |
, 156 | document.getElementById('__react-content')); 157 | -------------------------------------------------------------------------------- /examples/hide-todo.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-component/css-transition-group/c654070ebf14a2625255e4e96b86b1690f5da996/examples/hide-todo.html -------------------------------------------------------------------------------- /examples/hide-todo.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var style = '.example-enter {\ 4 | opacity: 0.01;\ 5 | transition: opacity 1s ease-in;\ 6 | }\ 7 | \ 8 | .example-enter.example-enter-active {\ 9 | opacity: 1;\ 10 | }\ 11 | \ 12 | .example-leave {\ 13 | opacity: 1;\ 14 | transition: opacity 1s ease-in;\ 15 | }\ 16 | \ 17 | .example-leave.example-leave-active {\ 18 | opacity: 0.01;\ 19 | }\ 20 | \ 21 | .item {\ 22 | width:100px;\ 23 | border:1px solid red;\ 24 | padding:10px;\ 25 | margin:10px;\ 26 | }'; 27 | 28 | var CSSTransitionGroup = require('rc-css-transition-group'); 29 | var React = require('react'); 30 | var assign = require('object-assign'); 31 | 32 | var Todo = React.createClass({ 33 | getDefaultProps: function () { 34 | return { 35 | visible: true, 36 | end: function () { 37 | } 38 | } 39 | }, 40 | componentWillUnmount: function () { 41 | console.log('componentWillUnmount'); 42 | console.log(this.props.children); 43 | this.props.end(); 44 | }, 45 | render: function () { 46 | var props = this.props; 47 | return
50 | {props.children} 51 |
; 52 | } 53 | }); 54 | var TodoList = React.createClass({ 55 | getInitialState: function () { 56 | return { 57 | items: [ 58 | {content: 'hello', visible: true}, 59 | {content: 'world', visible: true}, 60 | {content: 'click', visible: true}, 61 | {content: 'me', visible: true}] 62 | }; 63 | }, 64 | handleHide: function (i, item) { 65 | var newItems = this.state.items.concat([]); 66 | newItems.forEach((n, index)=> { 67 | newItems[index] = assign({}, n, { 68 | visible: true 69 | }); 70 | }); 71 | newItems[i] = assign({}, item, { 72 | visible: false 73 | }); 74 | this.setState({items: newItems}); 75 | }, 76 | render: function () { 77 | var items = this.state.items.map(function (item, i) { 78 | return ( 79 | 82 | {item.content} 83 | 84 | ); 85 | }.bind(this)); 86 | return ( 87 |
88 | 91 | {items} 92 | 93 |
94 | ); 95 | } 96 | }); 97 | 98 | React.render(
99 |

Hide Todo

100 | 101 | 102 |
, document.getElementById('__react-content')); 103 | -------------------------------------------------------------------------------- /examples/todo.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-component/css-transition-group/c654070ebf14a2625255e4e96b86b1690f5da996/examples/todo.html -------------------------------------------------------------------------------- /examples/todo.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var style = '.example-enter {\ 4 | opacity: 0.01;\ 5 | transition: opacity 1s ease-in;\ 6 | }\ 7 | \ 8 | .example-enter.example-enter-active {\ 9 | opacity: 1;\ 10 | }\ 11 | \ 12 | .example-leave {\ 13 | opacity: 1;\ 14 | transition: opacity 1s ease-in;\ 15 | }\ 16 | \ 17 | .example-leave.example-leave-active {\ 18 | opacity: 0.01;\ 19 | }\ 20 | \ 21 | .item {\ 22 | width:100px;\ 23 | border:1px solid red;\ 24 | padding:10px;\ 25 | margin:10px;\ 26 | }'; 27 | 28 | var CSSTransitionGroup = require('rc-css-transition-group'); 29 | var React = require('react'); 30 | var Todo = React.createClass({ 31 | getDefaultProps: function () { 32 | return { 33 | end: function () { 34 | } 35 | } 36 | }, 37 | componentWillUnmount: function () { 38 | console.log('componentWillUnmount'); 39 | console.log(this.props.children); 40 | this.props.end(); 41 | }, 42 | render: function () { 43 | var props = this.props; 44 | return
45 | {props.children} 46 |
; 47 | } 48 | }); 49 | var TodoList = React.createClass({ 50 | getInitialState: function () { 51 | return {items: ['hello', 'world', 'click', 'me']}; 52 | }, 53 | handleAdd: function () { 54 | var newItems = 55 | this.state.items.concat([prompt('Enter some text')]); 56 | this.setState({items: newItems}); 57 | }, 58 | handleRemove: function (i) { 59 | var newItems = this.state.items; 60 | newItems.splice(i, 1); 61 | this.setState({items: newItems}); 62 | }, 63 | render: function () { 64 | var items = this.state.items.map(function (item, i) { 65 | return ( 66 | 67 | {item} 68 | 69 | ); 70 | }.bind(this)); 71 | return ( 72 |
73 | 74 | 75 | {items} 76 | 77 |
78 | ); 79 | } 80 | }); 81 | 82 | React.render(
83 |

Todo

84 | 85 | 86 |
, document.getElementById('__react-content')); 87 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('./src/'); 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rc-css-transition-group", 3 | "version": "2.1.4", 4 | "description": "css-transition-group ui component for react", 5 | "keywords": [ 6 | "react", 7 | "react-component", 8 | "react-css-transition-group", 9 | "css-transition-group" 10 | ], 11 | "main": "./lib/index", 12 | "homepage": "http://github.com/react-component/css-transition-group", 13 | "author": "", 14 | "repository": { 15 | "type": "git", 16 | "url": "git@github.com:react-component/css-transition-group.git" 17 | }, 18 | "bugs": { 19 | "url": "http://github.com/react-component/css-transition-group/issues" 20 | }, 21 | "licenses": "MIT", 22 | "config": { 23 | "port": 8010 24 | }, 25 | "scripts": { 26 | "build": "rc-tools run build", 27 | "precommit": "rc-tools run precommit", 28 | "less": "rc-tools run less", 29 | "gh-pages": "rc-tools run gh-pages", 30 | "history": "rc-tools run history", 31 | "start": "node --harmony node_modules/.bin/rc-server", 32 | "publish": "rc-tools run tag", 33 | "lint": "rc-tools run lint", 34 | "saucelabs": "node --harmony node_modules/.bin/rc-tools run saucelabs", 35 | "browser-test": "node --harmony node_modules/.bin/rc-tools run browser-test", 36 | "browser-test-cover": "node --harmony node_modules/.bin/rc-tools run browser-test-cover" 37 | }, 38 | "devDependencies": { 39 | "expect.js": "~0.3.1", 40 | "object-assign": "~2.0.0", 41 | "precommit-hook": "^1.0.7", 42 | "rc-server": "3.x", 43 | "rc-tools": "3.x", 44 | "react": "~0.13.0" 45 | }, 46 | "precommit": [ 47 | "precommit" 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /src/CSSCore.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var SPACE = ' '; 4 | var RE_CLASS = /[\n\t\r]/g; 5 | 6 | var norm = function (elemClass) { 7 | return (SPACE + elemClass + SPACE).replace(RE_CLASS, SPACE); 8 | }; 9 | 10 | module.exports = { 11 | addClass(elem, className) { 12 | elem.className += ' ' + className; 13 | }, 14 | 15 | removeClass(elem, needle) { 16 | var elemClass = elem.className.trim(); 17 | var className = norm(elemClass); 18 | needle = needle.trim(); 19 | needle = SPACE + needle + SPACE; 20 | // 一个 cls 有可能多次出现:'link link2 link link3 link' 21 | while (className.indexOf(needle) >= 0) { 22 | className = className.replace(needle, SPACE); 23 | } 24 | elem.className = className.trim(); 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /src/CSSTransitionGroup.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react'); 4 | var ReactTransitionChildMapping = require('./ReactTransitionChildMapping'); 5 | var CSSTransitionGroupChild = require('./CSSTransitionGroupChild'); 6 | 7 | var CSSTransitionGroup = React.createClass({ 8 | protoTypes: { 9 | component: React.PropTypes.any, 10 | transitionName: React.PropTypes.string.isRequired, 11 | transitionEnter: React.PropTypes.bool, 12 | transitionLeave: React.PropTypes.bool 13 | }, 14 | 15 | getDefaultProps() { 16 | return { 17 | component: 'span', 18 | transitionEnter: true, 19 | transitionLeave: true 20 | }; 21 | }, 22 | 23 | getInitialState() { 24 | var ret = []; 25 | React.Children.forEach(this.props.children, (c)=> { 26 | ret.push(c); 27 | }); 28 | return { 29 | children: ret 30 | }; 31 | }, 32 | 33 | componentWillMount() { 34 | this.currentlyTransitioningKeys = {}; 35 | this.keysToEnter = []; 36 | this.keysToLeave = []; 37 | }, 38 | 39 | componentWillReceiveProps(nextProps) { 40 | var nextChildMapping = []; 41 | var showProp = this.props.showProp; 42 | var exclusive = this.props.exclusive; 43 | 44 | React.Children.forEach(nextProps.children, (c)=> { 45 | nextChildMapping.push(c); 46 | }); 47 | 48 | // // last props children if exclusive 49 | var prevChildMapping = exclusive ? this.props.children : this.state.children; 50 | 51 | var newChildren = ReactTransitionChildMapping.mergeChildMappings( 52 | prevChildMapping, 53 | nextChildMapping 54 | ); 55 | 56 | if (showProp) { 57 | newChildren = newChildren.map((c)=> { 58 | if (!c.props[showProp] && ReactTransitionChildMapping.isShownInChildren(prevChildMapping, c, showProp)) { 59 | var newProps = {}; 60 | newProps[showProp] = true; 61 | c = React.cloneElement(c, newProps); 62 | } 63 | return c; 64 | }); 65 | } 66 | 67 | if (exclusive) { 68 | // make middle state children invalid 69 | // restore to last props children 70 | newChildren.forEach((c)=> { 71 | this.stop(c.key); 72 | }); 73 | } 74 | 75 | this.setState({ 76 | children: newChildren 77 | }); 78 | 79 | nextChildMapping.forEach((c)=> { 80 | var key = c.key; 81 | var hasPrev = prevChildMapping && ReactTransitionChildMapping.inChildren(prevChildMapping, c); 82 | if (showProp) { 83 | if (hasPrev) { 84 | var showInPrev = ReactTransitionChildMapping.isShownInChildren(prevChildMapping, c, showProp); 85 | var showInNow = c.props[showProp]; 86 | if (!showInPrev && showInNow && !this.currentlyTransitioningKeys[key]) { 87 | this.keysToEnter.push(key); 88 | } 89 | } 90 | } else if (!hasPrev && !this.currentlyTransitioningKeys[key]) { 91 | this.keysToEnter.push(key); 92 | } 93 | }); 94 | 95 | prevChildMapping.forEach((c)=> { 96 | var key = c.key; 97 | var hasNext = nextChildMapping && ReactTransitionChildMapping.inChildren(nextChildMapping, c); 98 | if (showProp) { 99 | if (hasNext) { 100 | var showInNext = ReactTransitionChildMapping.isShownInChildren(nextChildMapping, c, showProp); 101 | var showInNow = c.props[showProp]; 102 | if (!showInNext && showInNow && !this.currentlyTransitioningKeys[key]) { 103 | this.keysToLeave.push(key); 104 | } 105 | } 106 | } else if (!hasNext && !this.currentlyTransitioningKeys[key]) { 107 | this.keysToLeave.push(key); 108 | } 109 | }); 110 | }, 111 | 112 | performEnter(key) { 113 | this.currentlyTransitioningKeys[key] = true; 114 | var component = this.refs[key]; 115 | if (component.componentWillEnter) { 116 | component.componentWillEnter( 117 | this._handleDoneEntering.bind(this, key) 118 | ); 119 | } else { 120 | this._handleDoneEntering(key); 121 | } 122 | }, 123 | 124 | _handleDoneEntering(key) { 125 | //console.log('_handleDoneEntering, ', key); 126 | delete this.currentlyTransitioningKeys[key]; 127 | var currentChildMapping = this.props.children; 128 | var showProp = this.props.showProp; 129 | if (!currentChildMapping || ( 130 | !showProp && !ReactTransitionChildMapping.inChildrenByKey(currentChildMapping, key) 131 | ) || ( 132 | showProp && !ReactTransitionChildMapping.isShownInChildrenByKey(currentChildMapping, key, showProp) 133 | )) { 134 | // This was removed before it had fully entered. Remove it. 135 | //console.log('releave ',key); 136 | this.performLeave(key); 137 | } else { 138 | this.setState({children: currentChildMapping}); 139 | } 140 | }, 141 | 142 | stop(key) { 143 | delete this.currentlyTransitioningKeys[key]; 144 | var component = this.refs[key]; 145 | if (component) { 146 | component.stop(); 147 | } 148 | }, 149 | 150 | performLeave(key) { 151 | this.currentlyTransitioningKeys[key] = true; 152 | 153 | var component = this.refs[key]; 154 | if (component.componentWillLeave) { 155 | component.componentWillLeave(this._handleDoneLeaving.bind(this, key)); 156 | } else { 157 | // Note that this is somewhat dangerous b/c it calls setState() 158 | // again, effectively mutating the component before all the work 159 | // is done. 160 | this._handleDoneLeaving(key); 161 | } 162 | }, 163 | 164 | _handleDoneLeaving(key) { 165 | //console.log('_handleDoneLeaving, ', key); 166 | delete this.currentlyTransitioningKeys[key]; 167 | var showProp = this.props.showProp; 168 | var currentChildMapping = this.props.children; 169 | if (showProp && currentChildMapping && 170 | ReactTransitionChildMapping.isShownInChildrenByKey(currentChildMapping, key, showProp)) { 171 | this.performEnter(key); 172 | } else if (!showProp && currentChildMapping && ReactTransitionChildMapping.inChildrenByKey(currentChildMapping, key)) { 173 | // This entered again before it fully left. Add it again. 174 | //console.log('reenter ',key); 175 | this.performEnter(key); 176 | } else { 177 | this.setState({children: currentChildMapping}); 178 | } 179 | }, 180 | 181 | componentDidUpdate() { 182 | var keysToEnter = this.keysToEnter; 183 | this.keysToEnter = []; 184 | keysToEnter.forEach(this.performEnter); 185 | var keysToLeave = this.keysToLeave; 186 | this.keysToLeave = []; 187 | keysToLeave.forEach(this.performLeave); 188 | }, 189 | 190 | render() { 191 | var props = this.props; 192 | var children = this.state.children.map((child) => { 193 | return {child}; 199 | }); 200 | var Component = this.props.component; 201 | return {children}; 202 | } 203 | }); 204 | module.exports = CSSTransitionGroup; 205 | -------------------------------------------------------------------------------- /src/CSSTransitionGroupChild.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013-2014, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @typechecks 10 | * @providesModule ReactCSSTransitionGroupChild 11 | */ 12 | 13 | 'use strict'; 14 | 15 | var React = require('react'); 16 | 17 | var CSSCore = require('./CSSCore'); 18 | var ReactTransitionEvents = require('./ReactTransitionEvents'); 19 | 20 | var TICK = 17; 21 | 22 | var ReactCSSTransitionGroupChild = React.createClass({ 23 | transition(animationType, finishCallback) { 24 | var node = this.getDOMNode(); 25 | var className = this.props.name + '-' + animationType; 26 | var activeClassName = className + '-active'; 27 | 28 | if (this.endListener) { 29 | this.endListener(); 30 | } 31 | 32 | this.endListener = (e) => { 33 | if (e && e.target !== node) { 34 | return; 35 | } 36 | 37 | CSSCore.removeClass(node, className); 38 | CSSCore.removeClass(node, activeClassName); 39 | 40 | ReactTransitionEvents.removeEndEventListener(node, this.endListener); 41 | this.endListener = null; 42 | 43 | // Usually this optional callback is used for informing an owner of 44 | // a leave animation and telling it to remove the child. 45 | if (finishCallback) { 46 | finishCallback(); 47 | } 48 | }; 49 | 50 | ReactTransitionEvents.addEndEventListener(node, this.endListener); 51 | 52 | CSSCore.addClass(node, className); 53 | 54 | // Need to do this to actually trigger a transition. 55 | this.queueClass(activeClassName); 56 | }, 57 | 58 | queueClass(className) { 59 | this.classNameQueue.push(className); 60 | 61 | if (!this.timeout) { 62 | this.timeout = setTimeout(this.flushClassNameQueue, TICK); 63 | } 64 | }, 65 | 66 | stop() { 67 | //console.log('force stop') 68 | if (this.timeout) { 69 | clearTimeout(this.timeout); 70 | this.classNameQueue.length = 0; 71 | this.timeout = null; 72 | } 73 | if (this.endListener) { 74 | this.endListener(); 75 | } 76 | }, 77 | 78 | flushClassNameQueue() { 79 | if (this.isMounted()) { 80 | this.classNameQueue.forEach( 81 | CSSCore.addClass.bind(CSSCore, this.getDOMNode()) 82 | ); 83 | } 84 | this.classNameQueue.length = 0; 85 | this.timeout = null; 86 | }, 87 | 88 | componentWillMount() { 89 | this.classNameQueue = []; 90 | }, 91 | 92 | componentWillUnmount() { 93 | if (this.timeout) { 94 | clearTimeout(this.timeout); 95 | } 96 | }, 97 | 98 | componentWillEnter(done) { 99 | if (this.props.enter) { 100 | this.transition('enter', done); 101 | } else { 102 | done(); 103 | } 104 | }, 105 | 106 | componentWillLeave(done) { 107 | if (this.props.leave) { 108 | this.transition('leave', done); 109 | } else { 110 | done(); 111 | } 112 | }, 113 | 114 | render() { 115 | return this.props.children; 116 | } 117 | }); 118 | 119 | module.exports = ReactCSSTransitionGroupChild; 120 | -------------------------------------------------------------------------------- /src/ReactTransitionChildMapping.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function inChildren(children, child) { 4 | var found = 0; 5 | children.forEach(function (c) { 6 | if (found) { 7 | return; 8 | } 9 | found = c.key === child.key; 10 | }); 11 | return found; 12 | } 13 | 14 | module.exports = { 15 | inChildren: inChildren, 16 | 17 | isShownInChildren(children, child, showProp) { 18 | var found = 0; 19 | children.forEach(function (c) { 20 | if (found) { 21 | return; 22 | } 23 | found = (c.key === child.key && c.props[showProp]); 24 | }); 25 | return found; 26 | }, 27 | 28 | inChildrenByKey(children, key) { 29 | var found = 0; 30 | children.forEach(function (c) { 31 | if (found) { 32 | return; 33 | } 34 | found = c.key === key; 35 | }); 36 | return found; 37 | }, 38 | 39 | isShownInChildrenByKey(children, key, showProp) { 40 | var found = 0; 41 | children.forEach(function (c) { 42 | if (found) { 43 | return; 44 | } 45 | found = c.key === key && c.props[showProp]; 46 | }); 47 | return found; 48 | }, 49 | 50 | mergeChildMappings(prev, next) { 51 | var ret = []; 52 | 53 | // For each key of `next`, the list of keys to insert before that key in 54 | // the combined list 55 | var nextChildrenPending = {}; 56 | var pendingChildren = []; 57 | prev.forEach(function (c) { 58 | if (inChildren(next, c)) { 59 | if (pendingChildren.length) { 60 | nextChildrenPending[c.key] = pendingChildren; 61 | pendingChildren = []; 62 | } 63 | } else { 64 | pendingChildren.push(c); 65 | } 66 | }); 67 | 68 | next.forEach(function (c) { 69 | if (nextChildrenPending.hasOwnProperty(c.key)) { 70 | ret = ret.concat(nextChildrenPending[c.key]); 71 | } 72 | ret.push(c); 73 | }); 74 | 75 | ret = ret.concat(pendingChildren); 76 | 77 | return ret; 78 | } 79 | }; 80 | -------------------------------------------------------------------------------- /src/ReactTransitionEvents.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013-2014, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @providesModule ReactTransitionEvents 10 | */ 11 | 12 | 'use strict'; 13 | /** 14 | * EVENT_NAME_MAP is used to determine which event fired when a 15 | * transition/animation ends, based on the style property used to 16 | * define that event. 17 | */ 18 | var EVENT_NAME_MAP = { 19 | transitionend: { 20 | transition: 'transitionend', 21 | WebkitTransition: 'webkitTransitionEnd', 22 | MozTransition: 'mozTransitionEnd', 23 | OTransition: 'oTransitionEnd', 24 | msTransition: 'MSTransitionEnd' 25 | }, 26 | 27 | animationend: { 28 | animation: 'animationend', 29 | WebkitAnimation: 'webkitAnimationEnd', 30 | MozAnimation: 'mozAnimationEnd', 31 | OAnimation: 'oAnimationEnd', 32 | msAnimation: 'MSAnimationEnd' 33 | } 34 | }; 35 | 36 | var endEvents = []; 37 | 38 | function detectEvents() { 39 | var testEl = document.createElement('div'); 40 | var style = testEl.style; 41 | 42 | // On some platforms, in particular some releases of Android 4.x, 43 | // the un-prefixed "animation" and "transition" properties are defined on the 44 | // style object but the events that fire will still be prefixed, so we need 45 | // to check if the un-prefixed events are useable, and if not remove them 46 | // from the map 47 | if (!('AnimationEvent' in window)) { 48 | delete EVENT_NAME_MAP.animationend.animation; 49 | } 50 | 51 | if (!('TransitionEvent' in window)) { 52 | delete EVENT_NAME_MAP.transitionend.transition; 53 | } 54 | 55 | for (var baseEventName in EVENT_NAME_MAP) { 56 | var baseEvents = EVENT_NAME_MAP[baseEventName]; 57 | for (var styleName in baseEvents) { 58 | if (styleName in style) { 59 | endEvents.push(baseEvents[styleName]); 60 | break; 61 | } 62 | } 63 | } 64 | } 65 | 66 | if (typeof window !== 'undefined') { 67 | detectEvents(); 68 | } 69 | 70 | // We use the raw {add|remove}EventListener() call because EventListener 71 | // does not know how to remove event listeners and we really should 72 | // clean up. Also, these events are not triggered in older browsers 73 | // so we should be A-OK here. 74 | 75 | function addEventListener(node, eventName, eventListener) { 76 | node.addEventListener(eventName, eventListener, false); 77 | } 78 | 79 | function removeEventListener(node, eventName, eventListener) { 80 | node.removeEventListener(eventName, eventListener, false); 81 | } 82 | 83 | var ReactTransitionEvents = { 84 | addEndEventListener(node, eventListener) { 85 | if (endEvents.length === 0) { 86 | // If CSS transitions are not supported, trigger an "end animation" 87 | // event immediately. 88 | window.setTimeout(eventListener, 0); 89 | return; 90 | } 91 | endEvents.forEach(function (endEvent) { 92 | addEventListener(node, endEvent, eventListener); 93 | }); 94 | }, 95 | 96 | endEvents: endEvents, 97 | 98 | removeEndEventListener(node, eventListener) { 99 | if (endEvents.length === 0) { 100 | return; 101 | } 102 | endEvents.forEach(function (endEvent) { 103 | removeEventListener(node, endEvent, eventListener); 104 | }); 105 | } 106 | }; 107 | 108 | module.exports = ReactTransitionEvents; 109 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('./CSSTransitionGroup'); 4 | -------------------------------------------------------------------------------- /tests/CSSTransitionGroup.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | // cao not run in phantomjs, fail! 5 | var CSSTransitionGroup = require('../'); 6 | var React = require('react/addons'); 7 | var TestUtils = React.addons.TestUtils; 8 | var Simulate = TestUtils.Simulate; 9 | var expect = require('expect.js'); 10 | require('./index.spec.css'); 11 | 12 | var Todo = React.createClass({ 13 | getDefaultProps: function () { 14 | return { 15 | end: function () { 16 | } 17 | } 18 | }, 19 | 20 | componentWillUnmount: function () { 21 | this.props.end(); 22 | }, 23 | 24 | render: function () { 25 | var props = this.props; 26 | return
27 | {props.children} 28 |
; 29 | } 30 | }); 31 | var TodoList = React.createClass({ 32 | getInitialState: function () { 33 | return {items: ['hello', 'world', 'click', 'me']}; 34 | }, 35 | 36 | handleAdd: function (item) { 37 | var newItems = 38 | this.state.items.concat(item); 39 | this.setState({items: newItems}); 40 | }, 41 | 42 | handleRemove: function (i) { 43 | var newItems = this.state.items; 44 | newItems.splice(i, 1); 45 | this.setState({items: newItems}); 46 | }, 47 | 48 | render: function () { 49 | var items = this.state.items.map(function (item, i) { 50 | return ( 51 | 52 | {item} 53 | 54 | ); 55 | }.bind(this)); 56 | return ( 57 |
58 | 59 | {items} 60 | 61 |
62 | ); 63 | } 64 | }); 65 | 66 | describe('CSSTransitionGroup', function () { 67 | var list; 68 | var container = document.createElement('div'); 69 | document.body.appendChild(container); 70 | 71 | beforeEach(function (done) { 72 | React.render(, container, function () { 73 | list = this; 74 | done(); 75 | }); 76 | }); 77 | 78 | afterEach(function () { 79 | React.unmountComponentAtNode(container); 80 | }); 81 | 82 | it('create works', function () { 83 | expect(TestUtils.scryRenderedDOMComponentsWithClass(list, 'item').length).to.be(4); 84 | }); 85 | 86 | var ReactTransitionEvents = require('../src/ReactTransitionEvents'); 87 | if (!ReactTransitionEvents.endEvents.length) { 88 | return; 89 | } 90 | 91 | it('transitionLeave works', function (done) { 92 | this.timeout(5999); 93 | list.handleRemove(0); 94 | setTimeout(function () { 95 | expect(TestUtils.scryRenderedDOMComponentsWithClass(list, 'item').length).to.be(4); 96 | if (!window.callPhantom) { 97 | expect(TestUtils.scryRenderedDOMComponentsWithClass(list, 'item')[0].getDOMNode().className) 98 | .to.contain('example-leave'); 99 | expect(TestUtils.scryRenderedDOMComponentsWithClass(list, 'item')[0].getDOMNode().className) 100 | .to.contain('example-leave-active'); 101 | } 102 | }, 100); 103 | setTimeout(function () { 104 | if (!window.callPhantom) { 105 | expect(TestUtils.scryRenderedDOMComponentsWithClass(list, 'item').length).to.be(3); 106 | } 107 | done(); 108 | }, 1400); 109 | }); 110 | 111 | it('transitionLeave works', function (done) { 112 | this.timeout(5999); 113 | list.handleAdd(Date.now()); 114 | setTimeout(function () { 115 | expect(TestUtils.scryRenderedDOMComponentsWithClass(list, 'item').length).to.be(5); 116 | if (!window.callPhantom) { 117 | expect(TestUtils.scryRenderedDOMComponentsWithClass(list, 'item')[4].getDOMNode().className) 118 | .to.contain('example-enter'); 119 | expect(TestUtils.scryRenderedDOMComponentsWithClass(list, 'item')[4].getDOMNode().className) 120 | .to.contain('example-enter-active'); 121 | } 122 | }, 100); 123 | setTimeout(function () { 124 | if (!window.callPhantom) { 125 | expect(TestUtils.scryRenderedDOMComponentsWithClass(list, 'item').length).to.be(5); 126 | expect(TestUtils.scryRenderedDOMComponentsWithClass(list, 'item')[4].getDOMNode().className) 127 | .not.to.contain('example-enter'); 128 | expect(TestUtils.scryRenderedDOMComponentsWithClass(list, 'item')[4].getDOMNode().className) 129 | .not.to.contain('example-enter-active'); 130 | } 131 | done(); 132 | }, 1400); 133 | }); 134 | }); 135 | -------------------------------------------------------------------------------- /tests/index.spec.css: -------------------------------------------------------------------------------- 1 | .example-enter { 2 | opacity: 0.01; 3 | transition: opacity 1s ease-in; 4 | } 5 | 6 | .example-enter.example-enter-active { 7 | opacity: 1; 8 | } 9 | 10 | .example-leave { 11 | opacity: 1; 12 | transition: opacity 1s ease-in; 13 | } 14 | 15 | .example-leave.example-leave-active { 16 | opacity: 0.01; 17 | } 18 | 19 | .item { 20 | width:100px; 21 | border:1px solid red; 22 | padding:10px; 23 | margin:10px; 24 | } 25 | -------------------------------------------------------------------------------- /tests/index.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./CSSTransitionGroup.spec'); 4 | -------------------------------------------------------------------------------- /tests/runner.html: -------------------------------------------------------------------------------- 1 | stub --------------------------------------------------------------------------------