├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitattributes ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── dist ├── index.js └── utils │ ├── event.js │ ├── scrollParent.js │ ├── splice.js │ └── trim.js ├── example ├── app.js ├── assets │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ ├── glyphicons-halflings-regular.woff2 │ │ ├── icomoon.eot │ │ ├── icomoon.svg │ │ ├── icomoon.ttf │ │ └── icomoon.woff │ ├── images │ │ ├── grumpy.gif │ │ ├── wow-1.gif │ │ ├── wow-1.jpg │ │ ├── wow-10.gif │ │ ├── wow-11.gif │ │ ├── wow-12.gif │ │ ├── wow-2.gif │ │ ├── wow-2.jpg │ │ ├── wow-3.gif │ │ ├── wow-3.jpg │ │ ├── wow-4.gif │ │ ├── wow-4.jpg │ │ ├── wow-5.gif │ │ ├── wow-6.gif │ │ ├── wow-7.gif │ │ ├── wow-8.gif │ │ └── wow-9.gif │ └── styles │ │ ├── animate.css │ │ ├── bootstrap.css │ │ ├── exmaple.css │ │ ├── main.css │ │ └── theme.css ├── components │ └── content.js ├── containers │ ├── empty.js │ ├── main.js │ └── notfound.js ├── favicon.ico ├── index.dev.html ├── index.html └── routes │ └── index.js ├── package-lock.json ├── package.json ├── src ├── index.js └── utils │ ├── event.js │ ├── scrollParent.js │ ├── splice.js │ └── trim.js ├── test ├── Test.component.js ├── karma.conf.js └── specs │ └── index.specs.js ├── webpack.config.babel.js ├── webpack.dll.config.babel.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "stage-2", 5 | "react" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/** 2 | build/** 3 | example/assets/** 4 | webpack.config*.js 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | * 环境定义了预定义的全局变量 4 | */ 5 | "env": { 6 | "browser": true, 7 | "node": true, 8 | "es6": true 9 | }, 10 | /** 11 | * 解释器 babel-eslint 12 | */ 13 | "parser": "babel-eslint", 14 | /** 15 | * 启用 standard 规则 16 | */ 17 | "extends": [ 18 | "standard", 19 | "standard-react" 20 | ], 21 | /** 22 | * "off" 或 0 - 关闭规则 23 | * "warn" 或 1 - 开启规则,使用警告级别的错误:warn (不会导致程序退出), 24 | * "error" 或 2 - 开启规则,使用错误级别的错误:error (当被触发的时候,程序会退出) 25 | */ 26 | "rules": { 27 | // 强制使用一致的缩进 28 | "indent": [2, 4], 29 | // 文件末尾强制换行 30 | "eol-last": 2, 31 | // 双峰驼命名格式 32 | "camelcase": 0, 33 | // 数组和对象键值对最后一个逗号,多行模式必须带逗号,单行模式不能带逗号 34 | "comma-dangle": [2, "always-multiline"], 35 | // 要求使用 let 或 const 而不是 var 36 | "no-var": 2, 37 | // 要求使用 const 声明那些声明后不再被修改的变量 38 | "prefer-const": 2, 39 | // 要求使用模板字面量而非字符串连接 40 | "prefer-template": 2, 41 | // 强制一致地使用函数表达式 42 | "func-style": [2, "expression"], 43 | // 要求箭头函数的参数使用圆括号 44 | "arrow-parens": 2, 45 | // 禁止在构造函数中,在调用 super() 之前使用 this 或 super 46 | "no-this-before-super": 2, 47 | // 强制在大括号中使用一致的空格 48 | "object-curly-spacing": [2, "always"], 49 | // 要求或禁止对象字面量中方法和属性使用简写语法 50 | "object-shorthand": [2, "always"], 51 | "react/prop-types": 0, 52 | "react/jsx-curly-spacing": [2, "never"], 53 | "react/jsx-indent": [0, 4], 54 | "react/jsx-indent-props": [0, 4], 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.js linguist-language=JavaScript 2 | *.css linguist-language=JavaScript 3 | *.html linguist-language=JavaScript 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################################################ 2 | ############### .gitignore ################## 3 | ################################################ 4 | # 5 | # This file is only relevant if you are using git. 6 | # 7 | # Files which match the splat patterns below will 8 | # be ignored by git. This keeps random crap and 9 | # sensitive credentials from being uploaded to 10 | # your repository. It allows you to configure your 11 | # app for your machine without accidentally 12 | # committing settings which will smash the local 13 | # settings of other developers on your team. 14 | # 15 | # Some reasonable defaults are included below, 16 | # but, of course, you should modify/extend/prune 17 | # to fit your needs! 18 | ################################################ 19 | 20 | 21 | 22 | 23 | ################################################ 24 | # Local Configuration 25 | # 26 | # Explicitly ignore files which contain: 27 | # 28 | # 1. Sensitive information you'd rather not push to 29 | # your git repository. 30 | # e.g., your personal API keys or passwords. 31 | # 32 | # 2. Environment-specific configuration 33 | # Basically, anything that would be annoying 34 | # to have to change every time you do a 35 | # `git pull` 36 | # e.g., your local development database, or 37 | # the S3 bucket you're using for file uploads 38 | # development. 39 | # 40 | ################################################ 41 | 42 | coverage/** 43 | build/** 44 | example/assets/js/** 45 | example/assets/css/** 46 | .happypack/** 47 | manifest.json 48 | 49 | 50 | 51 | 52 | ################################################ 53 | # Dependencies 54 | # 55 | # When releasing a production app, you may 56 | # consider including your node_modules and 57 | # bower_components directory in your git repo, 58 | # but during development, its best to exclude it, 59 | # since different developers may be working on 60 | # different kernels, where dependencies would 61 | # need to be recompiled anyway. 62 | # 63 | # More on that here about node_modules dir: 64 | # http://www.futurealoof.com/posts/nodemodules-in-git.html 65 | # (credit Mikeal Rogers, @mikeal) 66 | # 67 | # About bower_components dir, you can see this: 68 | # http://addyosmani.com/blog/checking-in-front-end-dependencies/ 69 | # (credit Addy Osmani, @addyosmani) 70 | # 71 | ################################################ 72 | 73 | node_modules 74 | bower_components 75 | 76 | 77 | 78 | 79 | 80 | ################################################ 81 | # Node.js / NPM 82 | # 83 | # Common files generated by Node, NPM, and the 84 | # related ecosystem. 85 | ################################################ 86 | lib-cov 87 | *.seed 88 | *.log 89 | *.out 90 | *.pid 91 | npm-debug.log 92 | 93 | 94 | 95 | 96 | 97 | ################################################ 98 | # Miscellaneous 99 | # 100 | # Common files generated by text editors, 101 | # operating systems, file systems, etc. 102 | ################################################ 103 | 104 | *~ 105 | *# 106 | .DS_STORE 107 | .netbeans 108 | nbproject 109 | .idea 110 | .node_history 111 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | before_script: 5 | - export CHROME_BIN=chromium-browser 6 | - export DISPLAY=:99.0 7 | - "sh -e /etc/init.d/xvfb start" 8 | - sleep 3 9 | addons: 10 | chrome: stable 11 | script: "./node_modules/karma/bin/karma start test/karma.conf.js --browsers Chrome_travis_ci --single-run --no-auto-watch --capture-timeout 300000" 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | 'Software'), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React WOW [![Build Status](https://travis-ci.org/skyvow/react-wow.svg?branch=master)](https://travis-ci.org/skyvow/react-wow) [![npm version](https://img.shields.io/npm/v/react-wow.svg)](https://www.npmjs.org/package/react-wow) [![Coverage Status](https://coveralls.io/repos/github/skyvow/react-wow/badge.svg?branch=master)](https://coveralls.io/github/skyvow/react-wow?branch=master) 2 | 3 | Using CSS animation in your react components. 4 | 5 | [Demo](https://skyvow.github.io/react-wow) 6 | 7 | ## 依赖 8 | 9 | - [animate.css](https://github.com/daneden/animate.css) 10 | 11 | ## 安装 12 | 13 | ``` 14 | $ npm install --save react-wow 15 | ``` 16 | 17 | ## 示例 18 | 19 | ```js 20 | 21 | import React from 'react' 22 | import ReactDOM from 'react-dom' 23 | import ReactWOW from 'react-wow' 24 | 25 | const App = () => 26 | 27 | ReactDOM.render(, document.getElementById('app')) 28 | 29 | ``` 30 | 31 | ## 使用方法 32 | 33 | ```sh 34 | $ git clone https://github.com/skyvow/react-wow.git 35 | $ cd react-wow 36 | $ npm install 37 | $ npm start 38 | ``` 39 | 40 | |`npm run 17 | 18 | 19 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React-WOW - Example 8 | 9 | 10 | 11 | 12 | Fork me on GitHub 13 | 14 |
15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /example/routes/index.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | path: '/', 4 | getComponent (nextState, cb) { 5 | require.ensure([], (require) => { 6 | cb(null, require('../containers/main').default) 7 | }, 'index') 8 | }, 9 | }, 10 | { 11 | path: '404', 12 | getComponent (nextState, cb) { 13 | require.ensure([], (require) => { 14 | cb(null, require('../containers/notfound').default) 15 | }, 'notfound') 16 | }, 17 | }, 18 | { 19 | path: '*', 20 | indexRoute: { 21 | onEnter (nextState, replace) { 22 | replace('/404') 23 | }, 24 | }, 25 | }, 26 | ] 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-wow", 3 | "version": "1.0.0", 4 | "description": "Using CSS animation in your react components.", 5 | "main": "./dist/index.js", 6 | "dependencies": { 7 | "classnames": "^2.2.5", 8 | "prop-types": "^15.5.10", 9 | "react": "^16.0.0", 10 | "react-addons-css-transition-group": "^15.5.2", 11 | "react-dom": "^16.0.0", 12 | "react-router": "^3.0.5" 13 | }, 14 | "devDependencies": { 15 | "autoprefixer": "^6.7.7", 16 | "babel": "^6.23.0", 17 | "babel-cli": "^6.24.1", 18 | "babel-core": "^6.25.0", 19 | "babel-eslint": "^7.2.3", 20 | "babel-loader": "^6.4.1", 21 | "babel-preset-es2015": "^6.24.1", 22 | "babel-preset-react": "^6.24.1", 23 | "babel-preset-stage-2": "^6.24.1", 24 | "chai": "^3.5.0", 25 | "clean-webpack-plugin": "^0.1.16", 26 | "compression-webpack-plugin": "^0.4.0", 27 | "copy-webpack-plugin": "^4.0.1", 28 | "cross-env": "^1.0.8", 29 | "css-loader": "^0.23.1", 30 | "eslint": "^3.19.0", 31 | "eslint-config-standard": "^10.2.1", 32 | "eslint-config-standard-react": "^5.0.0", 33 | "eslint-plugin-import": "^2.7.0", 34 | "eslint-plugin-node": "^5.1.0", 35 | "eslint-plugin-promise": "^3.5.0", 36 | "eslint-plugin-react": "^7.1.0", 37 | "eslint-plugin-standard": "^3.0.1", 38 | "extract-text-webpack-plugin": "^1.0.1", 39 | "file-loader": "^0.8.5", 40 | "gh-pages": "^1.1.0", 41 | "happypack": "^3.1.0", 42 | "html-webpack-plugin": "^2.28.0", 43 | "istanbul": "~0.4.5", 44 | "istanbul-instrumenter-loader": "^0.2.0", 45 | "karma": "^0.13.22", 46 | "karma-chai": "^0.1.0", 47 | "karma-chrome-launcher": "^2.2.0", 48 | "karma-coverage": "^0.5.5", 49 | "karma-coveralls": "^1.1.2", 50 | "karma-firefox-launcher": "^1.0.1", 51 | "karma-mocha": "^0.2.2", 52 | "karma-sourcemap-loader": "^0.3.7", 53 | "karma-webpack": "^1.7.0", 54 | "lodash": "^4.17.4", 55 | "mocha": "^5.1.1", 56 | "open-browser-webpack-plugin": "^0.0.1", 57 | "os": "^0.1.1", 58 | "postcss-atroot": "^0.1.3", 59 | "postcss-conditionals": "^2.1.0", 60 | "postcss-custom-media": "^5.0.1", 61 | "postcss-each": "^0.9.3", 62 | "postcss-extend": "^1.0.5", 63 | "postcss-for": "^2.1.1", 64 | "postcss-import": "^8.2.0", 65 | "postcss-loader": "^0.8.2", 66 | "postcss-mixins": "^4.0.2", 67 | "postcss-nested": "^1.0.1", 68 | "postcss-simple-vars": "^2.0.0", 69 | "postcss-sprites": "^3.3.0", 70 | "raw-loader": "^0.5.1", 71 | "style-loader": "^0.13.2", 72 | "uglify-loader": "^1.4.0", 73 | "url-loader": "^0.5.8", 74 | "webpack": "^1.15.0", 75 | "webpack-dev-server": "^1.16.5" 76 | }, 77 | "scripts": { 78 | "start": "cross-env NODE_ENV=development webpack-dev-server --progress --colors", 79 | "lint": "eslint --fix --ext .js example", 80 | "dll": "webpack --config ./webpack.dll.config.babel.js", 81 | "build": "cross-env NODE_ENV=production webpack --progress --colors", 82 | "dist": "babel src/ --out-dir dist/", 83 | "test": "karma start test/karma.conf.js", 84 | "ghpages": "gh-pages -d ./build/demo" 85 | }, 86 | "repository": { 87 | "type": "git", 88 | "url": "git+https://github.com/skyvow/react-wow.git" 89 | }, 90 | "keywords": [ 91 | "react", 92 | "react-component", 93 | "animation" 94 | ], 95 | "author": "skyvow", 96 | "license": "MIT", 97 | "bugs": { 98 | "url": "https://github.com/skyvow/react-wow/issues" 99 | }, 100 | "homepage": "https://github.com/skyvow/react-wow#readme" 101 | } 102 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDom from 'react-dom' 3 | import { node, oneOfType, string, bool, number, arrayOf, func } from 'prop-types' 4 | import { on, off } from './utils/event' 5 | import splice from './utils/splice' 6 | import trim from './utils/trim' 7 | import scrollParent from './utils/scrollParent' 8 | 9 | const LISTEN_FLAG = 'data-react-wow' 10 | const defaultBoundingClientRect = { top: 0, right: 0, bottom: 0, left: 0, width: 0, height: 0 } 11 | const caches = { listeners: [], pending: [] } 12 | 13 | // try to handle passive events 14 | let passiveEventSupported = false 15 | try { 16 | const opts = Object.defineProperty({}, 'passive', { 17 | get () { 18 | passiveEventSupported = true 19 | }, 20 | }) 21 | window.addEventListener('test', null, opts) 22 | } catch (e) { } 23 | // if they are supported, setup the optional params 24 | // IMPORTANT: FALSE doubles as the default CAPTURE value! 25 | const passiveEvent = passiveEventSupported ? { capture: false, passive: true } : false 26 | 27 | /** 28 | * Check if `component` is visible in overflow container `parent` 29 | * @param {node} component React component 30 | * @param {node} parent component's scroll parent 31 | * @return {bool} 32 | */ 33 | const checkOverflowVisible = (component, parent) => { 34 | const node = ReactDom.findDOMNode(component) 35 | 36 | let parentTop 37 | let parentHeight 38 | 39 | try { 40 | ({ top: parentTop, height: parentHeight } = parent.getBoundingClientRect()) 41 | } catch (e) { 42 | ({ top: parentTop, height: parentHeight } = defaultBoundingClientRect) 43 | } 44 | 45 | const windowInnerHeight = window.innerHeight || document.documentElement.clientHeight 46 | 47 | // calculate top and height of the intersection of the element's scrollParent and viewport 48 | const intersectionTop = Math.max(parentTop, 0) // intersection's top relative to viewport 49 | const intersectionHeight = Math.min(windowInnerHeight, parentTop + parentHeight) - intersectionTop // height 50 | 51 | // check whether the element is visible in the intersection 52 | let top 53 | let height 54 | 55 | try { 56 | ({ top, height } = node.getBoundingClientRect()) 57 | } catch (e) { 58 | ({ top, height } = defaultBoundingClientRect) 59 | } 60 | 61 | const offsetTop = top - intersectionTop // element's top relative to intersection 62 | 63 | const offsets = Array.isArray(component.props.offset) ? component.props.offset : [component.props.offset, component.props.offset] // Be compatible with previous API 64 | 65 | return (offsetTop - offsets[0] <= intersectionHeight) && (offsetTop + height + offsets[1] >= 0) 66 | } 67 | 68 | /** 69 | * Check if `component` is visible in document 70 | * @param {node} component React component 71 | * @return {bool} 72 | */ 73 | const checkNormalVisible = (component) => { 74 | const node = ReactDom.findDOMNode(component) 75 | 76 | // If this element is hidden by css rules somehow, it's definitely invisible 77 | if (!(node.offsetWidth || node.offsetHeight || node.getClientRects().length)) return false 78 | 79 | let top 80 | let elementHeight 81 | 82 | try { 83 | ({ top, height: elementHeight } = node.getBoundingClientRect()) 84 | } catch (e) { 85 | ({ top, height: elementHeight } = defaultBoundingClientRect) 86 | } 87 | 88 | const windowInnerHeight = window.innerHeight || document.documentElement.clientHeight 89 | 90 | const offsets = Array.isArray(component.props.offset) ? component.props.offset : [component.props.offset, component.props.offset] // Be compatible with previous API 91 | 92 | return (top - offsets[0] <= windowInnerHeight) && (top + elementHeight + offsets[1] >= 0) 93 | } 94 | 95 | /** 96 | * Detect if element is visible in viewport, if so, set `visible` state to true. 97 | * If `once` prop is provided true, remove component as listener after checkVisible 98 | * 99 | * @param {React} component React component that respond to scroll and resize 100 | */ 101 | const checkVisible = (component) => { 102 | const node = ReactDom.findDOMNode(component) 103 | if (!node) { 104 | return 105 | } 106 | const parent = scrollParent(node) 107 | const isOverflow = component.props.overflow && parent !== node.ownerDocument && parent !== document && parent !== document.documentElement 108 | const visible = isOverflow ? checkOverflowVisible(component, parent) : checkNormalVisible(component) 109 | if (visible) { 110 | if (!component.visible) { 111 | caches.pending.push(component) 112 | 113 | component.visible = true 114 | component.setState({ 115 | stopped: false, 116 | }) 117 | component.forceUpdate() 118 | } 119 | } 120 | } 121 | const purgePending = () => { 122 | caches.pending.forEach((component) => splice(caches.listeners, component)) 123 | caches.pending = [] 124 | } 125 | const scrollHandler = () => { 126 | caches.listeners.forEach((component) => checkVisible(component)) 127 | 128 | // Remove `once` component in listeners 129 | purgePending() 130 | } 131 | 132 | class ReactWOW extends React.Component { 133 | constructor (props) { 134 | super(props) 135 | this.visible = false 136 | this.state = { 137 | stopped: false, 138 | } 139 | } 140 | 141 | componentDidMount () { 142 | if (this.props.disabled) { 143 | return false 144 | } 145 | 146 | if (this.props.overflow) { 147 | const parent = scrollParent(ReactDom.findDOMNode(this)) 148 | if (parent && typeof parent.getAttribute === 'function') { 149 | const listenerCount = 1 + (+parent.getAttribute(LISTEN_FLAG)) 150 | if (listenerCount === 1) { 151 | on(parent, 'scroll', scrollHandler, passiveEvent) 152 | } 153 | parent.setAttribute(LISTEN_FLAG, listenerCount) 154 | } 155 | } else if (!caches.listeners.length) { 156 | const { scroll, resize } = this.props 157 | 158 | if (scroll) { 159 | on(window, 'scroll', scrollHandler, passiveEvent) 160 | } 161 | 162 | if (resize) { 163 | on(window, 'resize', scrollHandler, passiveEvent) 164 | } 165 | } 166 | 167 | caches.listeners.push(this) 168 | checkVisible(this) 169 | } 170 | 171 | shouldComponentUpdate () { 172 | return this.visible 173 | } 174 | 175 | componentWillUnmount () { 176 | if (this.props.disabled) { 177 | return false 178 | } 179 | 180 | if (this.props.overflow) { 181 | const parent = scrollParent(ReactDom.findDOMNode(this)) 182 | if (parent && typeof parent.getAttribute === 'function') { 183 | const listenerCount = (+parent.getAttribute(LISTEN_FLAG)) - 1 184 | if (listenerCount === 0) { 185 | off(parent, 'scroll', scrollHandler, passiveEvent) 186 | parent.removeAttribute(LISTEN_FLAG) 187 | } else { 188 | parent.setAttribute(LISTEN_FLAG, listenerCount) 189 | } 190 | } 191 | } 192 | 193 | splice(caches.listeners, this) 194 | 195 | if (!caches.listeners.length) { 196 | off(window, 'scroll', scrollHandler, passiveEvent) 197 | off(window, 'resize', scrollHandler, passiveEvent) 198 | } 199 | } 200 | 201 | /** 202 | * Custom style 203 | * @param {boolean} hidden 204 | */ 205 | customStyle = (hidden) => { 206 | const { duration, delay, iteration, animation, disabled } = this.props 207 | const style = { 208 | animationName: hidden ? 'none' : animation, 209 | visibility: hidden && !disabled ? 'hidden' : 'visible', 210 | } 211 | 212 | if (duration) { 213 | style.animationDuration = duration 214 | } 215 | 216 | if (delay) { 217 | style.animationDelay = delay 218 | } 219 | 220 | if (iteration) { 221 | style.animationIterationCount = iteration 222 | } 223 | 224 | return style 225 | } 226 | 227 | /** 228 | * Reset animation 229 | * @param {object} e 230 | */ 231 | resetAnimation = (e) => { 232 | if (e.type.toLowerCase().indexOf('animationend') !== -1) { 233 | this.setState({ 234 | stopped: true, 235 | }, () => { 236 | const { callback } = this.props 237 | 238 | if (typeof callback === 'function') { 239 | callback.call(this, ReactDom.findDOMNode(this)) 240 | } 241 | }) 242 | } 243 | } 244 | 245 | /** 246 | * Merge props 247 | * @param {object} props 248 | */ 249 | mergeProps (props) { 250 | const { animation, animateClass } = this.props 251 | const style = this.customStyle(!this.visible) 252 | const className = this.visible ? `${animation} ${!this.state.stopped ? animateClass : ''}` : animation 253 | 254 | return { 255 | ...props, 256 | style: { 257 | ...props.style, 258 | ...style, 259 | }, 260 | className: trim(`${props.className ? props.className : ''} ${className}`), 261 | onAnimationEnd: this.resetAnimation, 262 | } 263 | } 264 | 265 | render () { 266 | const { children, disabled } = this.props 267 | 268 | return disabled ? children : children ? React.Children.map(children, (child) => React.cloneElement(child, this.mergeProps(child.props))) : null 269 | } 270 | } 271 | 272 | ReactWOW.propTypes = { 273 | duration: string, 274 | delay: string, 275 | iteration: string, 276 | animation: string, 277 | children: node, 278 | scroll: bool, 279 | resize: bool, 280 | animateClass: string, 281 | offset: oneOfType([number, arrayOf(number)]), 282 | overflow: bool, 283 | callback: func, 284 | } 285 | 286 | ReactWOW.defaultProps = { 287 | duration: '', // Animation duration 288 | delay: '', // Animation delay 289 | iteration: '', // Animation iteration count 290 | animation: '', // Animation name 291 | scroll: true, // Listen and react to scroll event 292 | resize: true, // Listen and react to resize event 293 | animateClass: 'animated', // Animation css class 294 | offset: 0, // Distance to the element when triggering the animation 295 | overflow: false, // If your components inside a overflow container, set this to true 296 | disabled: false, // Disable the animation 297 | callback: () => {}, // The callback is fired every time an animation is stoped 298 | } 299 | 300 | export default ReactWOW 301 | -------------------------------------------------------------------------------- /src/utils/event.js: -------------------------------------------------------------------------------- 1 | export const on = (el, eventName, callback, opts = false) => { 2 | if (el.addEventListener) { 3 | el.addEventListener(eventName, callback, opts) 4 | } else if (el.attachEvent) { 5 | el.attachEvent(`on${eventName}`, (e) => { 6 | callback.call(el, e || window.event) 7 | }) 8 | } 9 | } 10 | 11 | export const off = (el, eventName, callback, opts = false) => { 12 | if (el.removeEventListener) { 13 | el.removeEventListener(eventName, callback, opts) 14 | } else if (el.detachEvent) { 15 | el.detachEvent(`on${eventName}`, callback) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/utils/scrollParent.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview Find scroll parent 3 | */ 4 | 5 | export default (node) => { 6 | if (!node) { 7 | return document.documentElement 8 | } 9 | 10 | const excludeStaticParent = node.style.position === 'absolute' 11 | const overflowRegex = /(scroll|auto)/ 12 | let parent = node 13 | 14 | while (parent) { 15 | if (!parent.parentNode) { 16 | return node.ownerDocument || document.documentElement 17 | } 18 | 19 | const style = window.getComputedStyle(parent) 20 | const position = style.position 21 | const overflow = style.overflow 22 | const overflowX = style['overflow-x'] 23 | const overflowY = style['overflow-y'] 24 | 25 | if (position === 'static' && excludeStaticParent) { 26 | parent = parent.parentNode 27 | continue 28 | } 29 | 30 | if (overflowRegex.test(overflow) && overflowRegex.test(overflowX) && overflowRegex.test(overflowY)) { 31 | return parent 32 | } 33 | 34 | parent = parent.parentNode 35 | } 36 | 37 | return node.ownerDocument || node.documentElement || document.documentElement 38 | } 39 | -------------------------------------------------------------------------------- /src/utils/splice.js: -------------------------------------------------------------------------------- 1 | export default (arr = [], item) => { 2 | const index = arr.indexOf(item) 3 | 4 | if (index !== -1) { 5 | arr.splice(index, 1) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/trim.js: -------------------------------------------------------------------------------- 1 | export default (str = '') => { 2 | const arr = str.trim().split(' ') 3 | 4 | return arr.filter((n, i) => !!n && arr.indexOf(n) === i).join(' ') 5 | } 6 | -------------------------------------------------------------------------------- /test/Test.component.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default class Test extends React.Component { 4 | constructor() { 5 | super() 6 | this.state = { 7 | timestamp: Date.now(), 8 | } 9 | } 10 | 11 | componentWillReceiveProps() { 12 | this.setState({ 13 | timestamp: Date.now(), 14 | }) 15 | } 16 | 17 | render() { 18 | const { timestamp } = this.state 19 | const { height, style, className } = this.props 20 | 21 | return ( 22 |
23 | ReactWOW {timestamp} 24 | {this.props.children} 25 |
26 | ) 27 | } 28 | } 29 | 30 | Test.defaultProps = { 31 | height: 200, 32 | className: 'react-wow', 33 | } 34 | -------------------------------------------------------------------------------- /test/karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config) { 2 | config.set({ 3 | 4 | // base path that will be used to resolve all patterns (eg. files, exclude) 5 | basePath: '../', 6 | 7 | 8 | // frameworks to use 9 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 10 | frameworks: ['mocha', 'chai'], 11 | 12 | // list of files / patterns to load in the browser 13 | files: [ 14 | { pattern: 'test/specs/*.js', included: true, watched: false }, 15 | ], 16 | 17 | 18 | // list of files to exclude 19 | exclude: [ 20 | 'coverage/**', 21 | 'dist/**', 22 | 'node_modules/' 23 | ], 24 | 25 | 26 | // preprocess matching files before serving them to the browser 27 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 28 | preprocessors: { 29 | 'test/**/*.js': ['webpack', 'sourcemap'], 30 | }, 31 | 32 | webpack: { 33 | devtool: 'inline-source-map', 34 | module: { 35 | loaders: [{ 36 | test: /\.js$/, 37 | include: /src|test/, 38 | query: { 39 | presets: ['es2015', 'stage-2', 'react'], 40 | }, 41 | loader: 'babel' 42 | }], 43 | postLoaders: [{ 44 | test: /\.js$/, 45 | include: /src/, 46 | loader: 'istanbul-instrumenter' 47 | }] 48 | }, 49 | resolve: { 50 | extensions: ['', '.js', '.jsx'] 51 | } 52 | }, 53 | 54 | 55 | plugins: [ 56 | 'karma-webpack', 57 | 'karma-mocha', 58 | 'karma-coverage', 59 | 'karma-chai', 60 | 'karma-sourcemap-loader', 61 | 'karma-chrome-launcher', 62 | 'istanbul-instrumenter-loader', 63 | 'karma-coveralls' 64 | ], 65 | 66 | 67 | // test results reporter to use 68 | // possible values: 'dots', 'progress' 69 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 70 | reporters: ['progress', 'coverage', 'coveralls'], 71 | 72 | coverageReporter: { 73 | dir: './', 74 | reporters: [{ 75 | type: 'html', 76 | subdir: 'coverage' 77 | }, { 78 | type: 'text', 79 | }, { 80 | type: 'lcov', 81 | subdir: 'coverage' 82 | }] 83 | }, 84 | 85 | // web server port 86 | port: 9876, 87 | 88 | 89 | // enable / disable colors in the output (reporters and logs) 90 | colors: true, 91 | 92 | 93 | // level of logging 94 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 95 | logLevel: config.LOG_DEBUG, 96 | 97 | 98 | // enable / disable watching file and executing tests whenever any file changes 99 | autoWatch: true, 100 | 101 | customLaunchers: { 102 | Chrome_travis_ci: { 103 | base: 'Chrome', 104 | flags: ['--no-sandbox'] 105 | } 106 | }, 107 | 108 | // start these browsers 109 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 110 | browsers: ['Chrome'], 111 | 112 | browserNoActivityTimeout: 300000, 113 | browserDisconnectTimeout: 300000, 114 | 115 | // Continuous Integration mode 116 | // if true, Karma captures browsers, runs the tests and exits 117 | singleRun: false, 118 | processKillTimeout: 300000, 119 | }); 120 | }; 121 | -------------------------------------------------------------------------------- /test/specs/index.specs.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { expect } from 'chai' 4 | import ReactWOW from '../../src/index' 5 | import Test from '../Test.component' 6 | 7 | describe('', () => { 8 | let div 9 | 10 | before(() => { 11 | document.body.style.margin = 0 12 | document.body.style.padding = 0 13 | }) 14 | 15 | beforeEach(() => { 16 | div = document.createElement('div') 17 | document.body.appendChild(div) 18 | }) 19 | 20 | afterEach(() => { 21 | ReactDOM.unmountComponentAtNode(div) 22 | div.parentNode.removeChild(div) 23 | window.scrollTo(0, 0) 24 | }) 25 | 26 | describe(' render children', () => { 27 | it('should render null', () => { 28 | ReactDOM.render(, div) 29 | expect(document.querySelector('fadeIn')).to.not.exist 30 | }) 31 | 32 | it('should render children when `animation`', () => { 33 | ReactDOM.render(, div) 34 | expect(document.querySelector('.react-wow.fadeIn')).to.exist 35 | }) 36 | 37 | it('should render children when `disabled`', () => { 38 | ReactDOM.render(, div) 39 | expect(document.querySelector('.react-wow')).to.exist 40 | expect(document.querySelector('.react-wow.fadeIn')).to.not.exist 41 | expect(document.querySelector('.fadeIn')).to.not.exist 42 | }) 43 | 44 | it('should render children when `duration`', () => { 45 | ReactDOM.render(, div) 46 | expect(document.querySelector('.react-wow.fadeIn')).to.exist 47 | expect(document.querySelector('.react-wow.fadeIn').style.animationDuration).to.equal('0.3s') 48 | }) 49 | 50 | it('should render children when `delay`', () => { 51 | ReactDOM.render(, div) 52 | expect(document.querySelector('.react-wow.fadeIn')).to.exist 53 | expect(document.querySelector('.react-wow.fadeIn').style.animationDelay).to.equal('0.3s') 54 | }) 55 | 56 | it('should render children when `iteration`', () => { 57 | ReactDOM.render(, div) 58 | expect(document.querySelector('.react-wow.fadeIn')).to.exist 59 | expect(document.querySelector('.react-wow.fadeIn').style.animationIterationCount).to.equal('1') 60 | }) 61 | 62 | it('should render children when `offset`', (done) => { 63 | const height = window.innerHeight + 200 64 | 65 | ReactDOM.render(, div) 66 | 67 | expect(document.querySelector('.react-wow.fadeIn').style.visibility).to.equal('hidden') 68 | 69 | window.scrollTo(0, 100) 70 | 71 | setTimeout(() => { 72 | expect(document.querySelector('.react-wow.fadeIn').style.visibility).to.equal('visible') 73 | done() 74 | }, 1000) 75 | }) 76 | 77 | it('should render children when scrolled visible', (done) => { 78 | const height = window.innerHeight + 100 79 | 80 | ReactDOM.render( 81 |
82 | 83 | 84 |
85 | , div) 86 | 87 | expect(document.querySelector('.react-wow.fadeIn')).to.exist 88 | expect(document.querySelector('.react-wow.fadeOut').style.visibility).to.equal('hidden') 89 | 90 | window.scrollTo(0, 200) 91 | 92 | setTimeout(() => { 93 | expect(document.querySelector('.react-wow.fadeOut').style.visibility).to.equal('visible') 94 | done() 95 | }, 1000) 96 | }) 97 | 98 | it('should render children when `overflow`', (done) => { 99 | ReactDOM.render( 100 |
101 | 102 | 103 |
104 | , div) 105 | 106 | const container = document.querySelector('.container') 107 | 108 | container.scrollTop = 200 109 | 110 | // expect(document.querySelector('.react-wow.fadeIn').style.visibility).to.equal('hidden') 111 | 112 | window.scrollTo(0, 100) 113 | 114 | setTimeout(() => { 115 | expect(document.querySelector('.react-wow.fadeIn').style.visibility).to.equal('visible') 116 | done() 117 | }, 1000) 118 | }) 119 | }) 120 | }) 121 | -------------------------------------------------------------------------------- /webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | // 引入依赖 2 | import webpack from 'webpack' 3 | import path from 'path' 4 | import autoprefixer from 'autoprefixer' 5 | import cssImport from 'postcss-import' 6 | import cssMixins from 'postcss-mixins' 7 | import cssExtend from 'postcss-extend' 8 | import conditionals from 'postcss-conditionals' 9 | import cssEach from 'postcss-each' 10 | import cssFor from 'postcss-for' 11 | import nested from 'postcss-nested' 12 | import cssSimpleVars from 'postcss-simple-vars' 13 | import customMedia from 'postcss-custom-media' 14 | import cssAtroot from 'postcss-atroot' 15 | import sprites from 'postcss-sprites' 16 | import HtmlWebpackPlugin from 'html-webpack-plugin' 17 | import ExtractTextPlugin from 'extract-text-webpack-plugin' 18 | import OpenBrowserPlugin from 'open-browser-webpack-plugin' 19 | import CopyWebpackPlugin from 'copy-webpack-plugin' 20 | import CleanWebpackPlugin from 'clean-webpack-plugin' 21 | import CompressionWebpackPlugin from 'compression-webpack-plugin' 22 | import HappyPack from 'happypack' 23 | import os from 'os' 24 | import lodash from 'lodash' 25 | import pkg from './package.json' 26 | import manifest from './manifest.json' 27 | 28 | // 配置环境 29 | const ENV = process.env.NODE_ENV 30 | const isDev = ENV === 'development' || ENV === 'dev' 31 | const isProd = ENV === 'production' || ENV === 'prod' 32 | 33 | // 公共模块 34 | const deps = lodash.uniq(Object.keys(pkg.dependencies)) 35 | const vendor = lodash.pullAll(deps, []) 36 | const jsSourcePath = path.join(__dirname, 'example') 37 | const buildPath = path.join(__dirname, 'build/demo') 38 | const sourcePath = path.join(__dirname, 'example') 39 | 40 | // 开启 happypack 多进程的模式加速编译 41 | const happyPackHandle = { 42 | threadPool: HappyPack.ThreadPool({ 43 | size: os.cpus().length, 44 | }), 45 | cacheLoaders: {}, 46 | cachePlugins: [], 47 | createPlugin(id, loaders) { 48 | this.cacheLoaders[id] = { 49 | loaders, 50 | happypack: `happypack/loader?id=${id}`, 51 | } 52 | this.cachePlugins.push(new HappyPack({ 53 | id, 54 | loaders, 55 | threadPool: this.threadPool, 56 | cache: true, 57 | verbose: true, 58 | })) 59 | return this 60 | }, 61 | getLoaders(id) { 62 | const { loaders, happypack } = this.cacheLoaders[id] 63 | return isProd ? loaders[0] : happypack 64 | }, 65 | } 66 | 67 | // 创建 happypack 实例对象 68 | happyPackHandle 69 | .createPlugin('js', ['babel']) 70 | .createPlugin('css', ['css!postcss']) 71 | 72 | // 配置参数 73 | const config = { 74 | context: jsSourcePath, 75 | entry: { 76 | app: [ 77 | './app.js', 78 | ], 79 | vendor, 80 | }, 81 | output: { 82 | path: buildPath, 83 | publicPath: '', 84 | filename: 'assets/js/app.js', 85 | chunkFilename: 'chunk/[name].chunk.js', 86 | }, 87 | module: { 88 | loaders: [ 89 | { 90 | test: /\.js$/, 91 | loader: happyPackHandle.getLoaders('js'), 92 | exclude: [path.resolve(__dirname, 'node_modules')], 93 | }, 94 | { 95 | test: /\.css/, 96 | loader: ExtractTextPlugin.extract('style', happyPackHandle.getLoaders('css'), { 97 | publicPath: '../../', 98 | }), 99 | }, 100 | { 101 | test: /\.(gif|jpg|png)\??.*$/, 102 | loader: 'url-loader?limit=8192&name=assets/images/[hash].[ext]', 103 | }, 104 | { 105 | test: /\.(woff|svg|eot|ttf)\??.*$/, 106 | loader: 'url-loader?limit=8192&name=assets/fonts/[hash].[ext]', 107 | }, 108 | ], 109 | }, 110 | postcss: webpack => { 111 | const dependent = { 112 | addDependencyTo: webpack, 113 | } 114 | const processors = [ 115 | cssImport(dependent), 116 | cssMixins, 117 | cssExtend, 118 | conditionals, 119 | cssEach, 120 | cssFor, 121 | nested, 122 | cssSimpleVars, 123 | customMedia, 124 | cssAtroot, 125 | autoprefixer, 126 | ] 127 | return processors 128 | }, 129 | plugins: [ 130 | // 分析和优先考虑使用最多的模块,并为它们分配最小的 ID 131 | new webpack.optimize.OccurenceOrderPlugin(), 132 | // 删除重复的依赖 133 | new webpack.optimize.DedupePlugin(), 134 | // 跳过编译时出错的代码并记录,使编译后运行时的包不会发生错误 135 | new webpack.NoErrorsPlugin(), 136 | // 复制静态文件 137 | new CopyWebpackPlugin([{ 138 | from: './assets/', 139 | to: './assets/', 140 | }]), 141 | // 合并所有的 CSS 文件 142 | new ExtractTextPlugin('assets/css/app.css', { 143 | allChunks: true, 144 | }), 145 | // 自动生成页面 146 | new HtmlWebpackPlugin({ 147 | template: path.join(sourcePath, isDev ? 'index.dev.html' : 'index.html'), 148 | path: buildPath, 149 | filename: 'index.html', 150 | }), 151 | ], 152 | resolve: { 153 | extensions: [ 154 | '', 155 | '.js', 156 | '.json', 157 | '.css', 158 | ], 159 | root: [ 160 | path.resolve(__dirname), 161 | jsSourcePath, 162 | ], 163 | }, 164 | devServer: { 165 | contentBase: isProd ? buildPath : sourcePath, 166 | // historyApiFallback: true, 167 | port: 3000, 168 | // compress: isProd, 169 | hot: !isProd, 170 | inline: !isProd, 171 | // disableHostCheck: true, 172 | host: '0.0.0.0', 173 | }, 174 | } 175 | 176 | // 开发环境 177 | if (isDev) { 178 | Object.assign(config, { 179 | plugins: [ 180 | ...config.plugins, 181 | // 抽离并打包变动不频繁的模块 182 | new webpack.DllReferencePlugin({ 183 | context: __dirname, 184 | manifest, 185 | }), 186 | // 配置全局变量 187 | new webpack.DefinePlugin({ 188 | __DEBUG__: true, 189 | }), 190 | // happypack 插件 191 | ...happyPackHandle.cachePlugins, 192 | // 开启全局的模块热替换(HMR) 193 | new webpack.HotModuleReplacementPlugin(), 194 | // 当模块热替换(HMR)时在浏览器控制台输出对用户更友好的模块名字信息 195 | new webpack.NamedModulesPlugin() 196 | // 打开浏览器 197 | // new OpenBrowserPlugin({ 198 | // url: 'http://localhost:3000', 199 | // }), 200 | ], 201 | entry: Object.assign({}, config.entry, { 202 | app: [ 203 | // 开发环境全局变量配置 204 | // path.resolve(__dirname, 'dev.config.js'), 205 | ...config.entry.app, 206 | ], 207 | }), 208 | // 产生.map文件,方便调试 209 | devtool: 'source-map', 210 | }) 211 | } 212 | 213 | // 生产环境 214 | if (isProd) { 215 | Object.assign(config, { 216 | plugins: [ 217 | ...config.plugins, 218 | // 清除文件 219 | new CleanWebpackPlugin(['build'], { 220 | root: '', 221 | verbose: true, 222 | dry: false, 223 | }), 224 | // 提取公共模块单独打包,进而减小 rebuild 时的性能消耗 225 | new webpack.optimize.CommonsChunkPlugin('vendor', 'assets/js/vendor.js'), 226 | // 打包压缩 JS 文件 227 | new webpack.optimize.UglifyJsPlugin({ 228 | compress: { 229 | warnings: false, 230 | drop_console: true, 231 | } 232 | }), 233 | // 压缩成 gzip 格式 234 | new CompressionWebpackPlugin({ 235 | asset: "[path].gz[query]", 236 | algorithm: "gzip", 237 | test: /\.js$|\.css$|\.html$/, 238 | threshold: 10240, 239 | minRatio: 0, 240 | }), 241 | ], 242 | entry: Object.assign({}, config.entry, { 243 | app: [ 244 | // 生成环境全局变量配置 245 | // path.resolve(__dirname, 'prod.config.js'), 246 | ...config.entry.app, 247 | ], 248 | }), 249 | }) 250 | } 251 | 252 | export default config 253 | -------------------------------------------------------------------------------- /webpack.dll.config.babel.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack' 2 | import path from 'path' 3 | import lodash from 'lodash' 4 | import pkg from './package.json' 5 | 6 | // 公共模块 7 | const deps = lodash.uniq(Object.keys(pkg.dependencies)) 8 | const vendor = lodash.pullAll(deps, []) 9 | const jsSourcePath = path.join(__dirname, 'example') 10 | // const buildPath = path.join(__dirname, 'build/demo') 11 | 12 | export default { 13 | context: jsSourcePath, 14 | entry: { 15 | vendor, 16 | }, 17 | output: { 18 | path: 'example/assets/js', 19 | filename: '[name].js', 20 | library: '[name]', 21 | }, 22 | plugins: [ 23 | // 动态链接库,预编译资源模块 24 | new webpack.DllPlugin({ 25 | path: 'manifest.json', 26 | name: '[name]', 27 | context: __dirname, 28 | }), 29 | ], 30 | } 31 | --------------------------------------------------------------------------------