├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── LICENSE ├── README.md ├── dist ├── react-fixed.js └── react-fixed.min.js ├── gulpfile.js ├── lib └── Fixed.js ├── package.json └── src └── Fixed.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | root = true 4 | 5 | [*] 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | indent_style = tab 11 | 12 | [*.json] 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [*.yml] 17 | indent_style = space 18 | indent_size = 2 19 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | lib/* 2 | dist/* 3 | node_modules/* 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "keystone" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Jed Watson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-fixed 2 | 3 | A lightweight component that sticks to the bottom of the window while scrolling. Neat for toolbars. 4 | 5 | Docs coming soon. 6 | -------------------------------------------------------------------------------- /dist/react-fixed.js: -------------------------------------------------------------------------------- 1 | (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.Fixed = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o maxY && (sizeChanged || this.mode !== 'inline')) { 91 | this.mode = 'inline'; 92 | newState.top = 0; 93 | newState.position = 'absolute'; 94 | this.setState(newState); 95 | } else if (viewY <= maxY && (sizeChanged || this.mode !== 'fixed')) { 96 | this.mode = 'fixed'; 97 | newState.top = window.innerHeight - this.fixedSize.y; 98 | newState.position = 'fixed'; 99 | this.setState(newState); 100 | } 101 | }, 102 | 103 | render: function render() { 104 | var wrapperStyle = { 105 | position: 'relative', 106 | height: this.state.height 107 | }; 108 | var fixedProps = blacklist(this.props, 'children', 'style'); 109 | var fixedStyle = xtend(this.props.style || {}, { 110 | position: this.state.position, 111 | top: this.state.top, 112 | width: this.state.width, 113 | height: this.state.height 114 | }); 115 | return React.createElement( 116 | 'div', 117 | { ref: 'wrapper', style: wrapperStyle }, 118 | React.createElement( 119 | 'div', 120 | _extends({ ref: 'fixed', style: fixedStyle }, fixedProps), 121 | this.props.children 122 | ) 123 | ); 124 | } 125 | }); 126 | 127 | module.exports = Fixed; 128 | 129 | }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) 130 | },{"blacklist":undefined,"xtend":undefined}]},{},[1])(1) 131 | }); -------------------------------------------------------------------------------- /dist/react-fixed.min.js: -------------------------------------------------------------------------------- 1 | !function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var i;i="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,i.Fixed=e()}}(function(){return function e(i,t,n){function o(s,d){if(!t[s]){if(!i[s]){var f="function"==typeof require&&require;if(!d&&f)return f(s,!0);if(r)return r(s,!0);var a=new Error("Cannot find module '"+s+"'");throw a.code="MODULE_NOT_FOUND",a}var h=t[s]={exports:{}};i[s][0].call(h.exports,function(e){var t=i[s][1][e];return o(t?t:e)},h,h.exports,e,i,t,n)}return t[s].exports}for(var r="function"==typeof require&&require,s=0;sn&&(s||"inline"!==this.mode)?(this.mode="inline",d.top=0,d.position="absolute",this.setState(d)):n>=o&&(s||"fixed"!==this.mode)&&(this.mode="fixed",d.top=window.innerHeight-this.fixedSize.y,d.position="fixed",this.setState(d))},render:function(){var e={position:"relative",height:this.state.height},i=s(this.props,"children","style"),t=r(this.props.style||{},{position:this.state.position,top:this.state.top,width:this.state.width,height:this.state.height});return o.createElement("div",{ref:"wrapper",style:e},o.createElement("div",n({ref:"fixed",style:t},i),this.props.children))}});i.exports=d}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{blacklist:void 0,xtend:void 0}]},{},[1])(1)}); -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var initGulpTasks = require('react-component-gulp-tasks'); 3 | 4 | var taskConfig = { 5 | component: { 6 | name: 'Fixed', 7 | }, 8 | }; 9 | 10 | initGulpTasks(gulp, taskConfig); 11 | -------------------------------------------------------------------------------- /lib/Fixed.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 4 | 5 | var React = require('react'); 6 | var xtend = require('xtend'); 7 | var blacklist = require('blacklist'); 8 | 9 | var Fixed = React.createClass({ 10 | 11 | displayName: 'Fixed', 12 | 13 | propTypes: { 14 | children: React.PropTypes.node, 15 | style: React.PropTypes.object 16 | }, 17 | 18 | getInitialState: function getInitialState() { 19 | return { 20 | position: 'relative', 21 | width: 'auto', 22 | height: 'auto', 23 | top: 0 24 | }; 25 | }, 26 | 27 | componentDidMount: function componentDidMount() { 28 | 29 | // Bail in IE8 because React doesn't support the onScroll event in that browser 30 | // Conveniently (!) IE8 doesn't have window.getComputedStyle which we also use here 31 | if (!window.getComputedStyle) return; 32 | 33 | var fixed = this.refs.fixed; 34 | 35 | this.windowSize = this.getWindowSize(); 36 | 37 | var fixedStyle = window.getComputedStyle(fixed); 38 | 39 | this.fixedSize = { 40 | x: fixed.offsetWidth, 41 | y: fixed.offsetHeight + parseInt(fixedStyle.marginTop || '0') 42 | }; 43 | 44 | window.addEventListener('scroll', this.recalcPosition, false); 45 | window.addEventListener('resize', this.recalcPosition, false); 46 | 47 | this.recalcPosition(); 48 | }, 49 | 50 | componentWillUnmount: function componentWillUnmount() { 51 | window.removeEventListener('scroll', this.recalcPosition, false); 52 | window.removeEventListener('resize', this.recalcPosition, false); 53 | }, 54 | 55 | getWindowSize: function getWindowSize() { 56 | return { 57 | x: window.innerWidth, 58 | y: window.innerHeight 59 | }; 60 | }, 61 | 62 | recalcPosition: function recalcPosition() { 63 | var wrapper = this.refs.wrapper; 64 | var fixed = this.refs.fixed; 65 | 66 | this.fixedSize.x = wrapper.offsetWidth; 67 | 68 | var offsetTop = 0; 69 | var offsetEl = wrapper; 70 | 71 | while (offsetEl) { 72 | offsetTop += offsetEl.offsetTop; 73 | offsetEl = offsetEl.offsetParent; 74 | } 75 | 76 | var maxY = offsetTop + this.fixedSize.y; 77 | var viewY = window.scrollY + window.innerHeight; 78 | 79 | var newSize = this.getWindowSize(); 80 | var sizeChanged = newSize.x !== this.windowSize.x || newSize.y !== this.windowSize.y; 81 | this.windowSize = newSize; 82 | 83 | var newState = { 84 | width: this.fixedSize.x, 85 | height: this.fixedSize.y 86 | }; 87 | 88 | if (viewY > maxY && (sizeChanged || this.mode !== 'inline')) { 89 | this.mode = 'inline'; 90 | newState.top = 0; 91 | newState.position = 'absolute'; 92 | this.setState(newState); 93 | } else if (viewY <= maxY && (sizeChanged || this.mode !== 'fixed')) { 94 | this.mode = 'fixed'; 95 | newState.top = window.innerHeight - this.fixedSize.y; 96 | newState.position = 'fixed'; 97 | this.setState(newState); 98 | } 99 | }, 100 | 101 | render: function render() { 102 | var wrapperStyle = { 103 | position: 'relative', 104 | height: this.state.height 105 | }; 106 | var fixedProps = blacklist(this.props, 'children', 'style'); 107 | var fixedStyle = xtend(this.props.style || {}, { 108 | position: this.state.position, 109 | top: this.state.top, 110 | width: this.state.width, 111 | height: this.state.height 112 | }); 113 | return React.createElement( 114 | 'div', 115 | { ref: 'wrapper', style: wrapperStyle }, 116 | React.createElement( 117 | 'div', 118 | _extends({ ref: 'fixed', style: fixedStyle }, fixedProps), 119 | this.props.children 120 | ) 121 | ); 122 | } 123 | }); 124 | 125 | module.exports = Fixed; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-fixed", 3 | "version": "1.0.2", 4 | "description": "A lightweight component that sticks to the bottom of the window while scrolling. Neat for toolbars.", 5 | "main": "lib/Fixed.js", 6 | "author": "Jed Watson", 7 | "license": "MIT", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/JedWatson/react-fixed.git" 11 | }, 12 | "dependencies": { 13 | "blacklist": "^1.1.2", 14 | "xtend": "^4.0.1" 15 | }, 16 | "devDependencies": { 17 | "eslint": "^2.10.2", 18 | "eslint-plugin-react": "^5.1.1", 19 | "gulp": "^3.9.1", 20 | "react": "^15.1.0", 21 | "react-component-gulp-tasks": "^0.7.7" 22 | }, 23 | "peerDependencies": { 24 | "react": "^0.14.0 || ^15.0.0" 25 | }, 26 | "browserify-shim": { 27 | "react": "global:React" 28 | }, 29 | "scripts": { 30 | "build": "gulp clean && NODE_ENV=production gulp build", 31 | "lint": "eslint .", 32 | "test": "npm run lint", 33 | "watch": "gulp watch:lib" 34 | }, 35 | "keywords": [ 36 | "react", 37 | "react-component", 38 | "sticky", 39 | "fixed", 40 | "scroll", 41 | "toolbar" 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /src/Fixed.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var xtend = require('xtend'); 3 | var blacklist = require('blacklist'); 4 | 5 | var Fixed = React.createClass({ 6 | 7 | displayName: 'Fixed', 8 | 9 | propTypes: { 10 | children: React.PropTypes.node, 11 | style: React.PropTypes.object, 12 | }, 13 | 14 | getInitialState: function () { 15 | return { 16 | position: 'relative', 17 | width: 'auto', 18 | height: 'auto', 19 | top: 0, 20 | }; 21 | }, 22 | 23 | componentDidMount: function () { 24 | 25 | // Bail in IE8 because React doesn't support the onScroll event in that browser 26 | // Conveniently (!) IE8 doesn't have window.getComputedStyle which we also use here 27 | if (!window.getComputedStyle) return; 28 | 29 | var fixed = this.refs.fixed; 30 | 31 | this.windowSize = this.getWindowSize(); 32 | 33 | var fixedStyle = window.getComputedStyle(fixed); 34 | 35 | this.fixedSize = { 36 | x: fixed.offsetWidth, 37 | y: fixed.offsetHeight + parseInt(fixedStyle.marginTop || '0'), 38 | }; 39 | 40 | window.addEventListener('scroll', this.recalcPosition, false); 41 | window.addEventListener('resize', this.recalcPosition, false); 42 | 43 | this.recalcPosition(); 44 | }, 45 | 46 | componentWillUnmount: function () { 47 | window.removeEventListener('scroll', this.recalcPosition, false); 48 | window.removeEventListener('resize', this.recalcPosition, false); 49 | }, 50 | 51 | getWindowSize: function () { 52 | return { 53 | x: window.innerWidth, 54 | y: window.innerHeight, 55 | }; 56 | }, 57 | 58 | recalcPosition: function () { 59 | var wrapper = this.refs.wrapper; 60 | 61 | this.fixedSize.x = wrapper.offsetWidth; 62 | 63 | var offsetTop = 0; 64 | var offsetEl = wrapper; 65 | 66 | while (offsetEl) { 67 | offsetTop += offsetEl.offsetTop; 68 | offsetEl = offsetEl.offsetParent; 69 | } 70 | 71 | var maxY = offsetTop + this.fixedSize.y; 72 | var viewY = window.scrollY + window.innerHeight; 73 | 74 | var newSize = this.getWindowSize(); 75 | var sizeChanged = (newSize.x !== this.windowSize.x || newSize.y !== this.windowSize.y); 76 | this.windowSize = newSize; 77 | 78 | var newState = { 79 | width: this.fixedSize.x, 80 | height: this.fixedSize.y, 81 | }; 82 | 83 | if (viewY > maxY && (sizeChanged || this.mode !== 'inline')) { 84 | this.mode = 'inline'; 85 | newState.top = 0; 86 | newState.position = 'absolute'; 87 | this.setState(newState); 88 | } else if (viewY <= maxY && (sizeChanged || this.mode !== 'fixed')) { 89 | this.mode = 'fixed'; 90 | newState.top = window.innerHeight - this.fixedSize.y; 91 | newState.position = 'fixed'; 92 | this.setState(newState); 93 | } 94 | }, 95 | 96 | render: function () { 97 | var wrapperStyle = { 98 | position: 'relative', 99 | height: this.state.height, 100 | }; 101 | var fixedProps = blacklist(this.props, 'children', 'style'); 102 | var fixedStyle = xtend(this.props.style || {}, { 103 | position: this.state.position, 104 | top: this.state.top, 105 | width: this.state.width, 106 | height: this.state.height, 107 | }); 108 | return ( 109 |
110 |
{this.props.children}
111 |
112 | ); 113 | }, 114 | }); 115 | 116 | module.exports = Fixed; 117 | --------------------------------------------------------------------------------