-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: klimashkin <[email protected]>
- Loading branch information
1 parent
8755825
commit 67c6de4
Showing
13 changed files
with
1,159 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
root = true | ||
|
||
[*] | ||
charset = utf-8 | ||
|
||
indent_style = space | ||
indent_size = 2 | ||
|
||
end_of_line = lf | ||
insert_final_newline = false | ||
trim_trailing_whitespace = true |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
dist/**/*.js |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
package-lock=false |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
## Change Log | ||
|
||
### 1.0.0 (2018-03-15) | ||
- Initial release (@klimashkin) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,2 @@ | ||
# react-responsive-container | ||
Leverage ResizeObserver to add breakpoints to your component | ||
# react-size-watcher | ||
Leverage ResizeObserver by adding breakpoints to your component |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
{ | ||
"name": "react-size-watcher", | ||
"version": "1.0.0", | ||
"author": "Pavel Klimashkin", | ||
"description": "Leverage ResizeObserver to add breakpoints to your component and track its size", | ||
"homepage": "https://github.com/klimashkin/react-size-watcher", | ||
"repository": { | ||
"type": "git", | ||
"url": "git+https://github.com/klimashkin/react-size-watcher.git" | ||
}, | ||
"license": "MIT", | ||
"bugs": { | ||
"url": "https://github.com/klimashkin/react-size-watcher/issues" | ||
}, | ||
"keywords": [ | ||
"react", | ||
"responsive", | ||
"resize", | ||
"resizeobserver", | ||
"ResizeObserver", | ||
"react-size-watcher", | ||
"react-size-observer", | ||
"media query" | ||
], | ||
"main": "./dist/react-size-watcher.js", | ||
"es2015": "./dist/es2015/react-size-watcher.js", | ||
"es2017": "./dist/es2017/react-size-watcher.js", | ||
"files": [ | ||
"dist", | ||
"src" | ||
], | ||
"peerDependencies": { | ||
"react": "^16.2.0" | ||
}, | ||
"dependencies": { | ||
"prop-types": "^15.6.1" | ||
}, | ||
"devDependencies": { | ||
"babel-cli": "^6.26.0", | ||
"babel-core": "^6.26.0", | ||
"babel-eslint": "^8.2.2", | ||
"babel-loader": "^7.1.4", | ||
"babel-plugin-syntax-async-functions": "6.13.0", | ||
"babel-plugin-syntax-trailing-function-commas": "6.22.0", | ||
"babel-plugin-transform-async-to-generator": "6.24.1", | ||
"babel-plugin-transform-class-properties": "^6.24.1", | ||
"babel-plugin-transform-object-rest-spread": "^6.26.0", | ||
"babel-plugin-transform-react-remove-prop-types": "0.4.13", | ||
"babel-preset-env": "^1.6.1", | ||
"babel-preset-react": "^6.24.1", | ||
"cross-env": "^5.1.4", | ||
"eslint": "^4.18.2", | ||
"eslint-plugin-babel": "^4.1.2", | ||
"eslint-plugin-import": "^2.9.0", | ||
"eslint-plugin-react": "^7.7.0", | ||
"github-changes": "^1.1.2", | ||
"null-loader": "0.1.1", | ||
"react": "^16.2.0", | ||
"react-dom": "^16.2.0", | ||
"rimraf": "2.6.2", | ||
"uglifyjs-webpack-plugin": "1.2.3", | ||
"webpack": "^4.1.1", | ||
"webpack-cli": "2.0.12" | ||
}, | ||
"scripts": { | ||
"preversion": "npm run lint && npm run clean && npm run build", | ||
"postversion": "npm run changelog", | ||
"build:umd": "cross-env BUILD_MODE=umd webpack", | ||
"build:umd-min": "cross-env BUILD_MODE=umd-min webpack", | ||
"build:es6": "cross-env BUILD_MODE=es2015 webpack", | ||
"build:es6-min": "cross-env BUILD_MODE=es2015-min webpack", | ||
"build:es8": "cross-env BUILD_MODE=es2017 webpack", | ||
"build:es8-min": "cross-env BUILD_MODE=es2017-min webpack", | ||
"build": "npm run build:umd && npm run build:umd-min && npm run build:es6 && npm run build:es6-min && npm run build:es8 && npm run build:es8-min", | ||
"clean": "rimraf dist", | ||
"lint": "eslint ./", | ||
"changelog": "github-changes -o klimashkin -r react-size-watcher -b master -f ./CHANGELOG.md --order-semver --use-commit-body" | ||
}, | ||
"engines": { | ||
"node": ">=6.0.0" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,245 @@ | ||
import PropTypes from 'prop-types'; | ||
import {Component, createElement} from 'react'; | ||
import {contextTypes} from './SizeWatcherTypes'; | ||
|
||
const defaultBreakpoint = {props: {}, minWidth: 0, maxWidth: Infinity, minHeight: 0, maxHeight: Infinity}; | ||
|
||
export default class SizeWatcher extends Component { | ||
static contextTypes = contextTypes; | ||
|
||
static propTypes = { | ||
// Array of breakpoints | ||
breakpoints: PropTypes.arrayOf(PropTypes.shape({ | ||
// Dimensions if breakpoint | ||
minWidth: PropTypes.number, | ||
maxWidth: PropTypes.number, | ||
minHeight: PropTypes.number, | ||
maxHeight: PropTypes.number, | ||
// Props that are mixed into container props | ||
props: PropTypes.object, | ||
// Property that contains any custom data useful in child render function | ||
data: PropTypes.any, | ||
})).isRequired, | ||
|
||
// Breakpoint match strategy | ||
// 'order' (default) - match by breakpoints order, like in css @media-queries, when the last match wins | ||
// 'breakpointArea' - match by breakpoints area, when the smallest area is considered to be more specific | ||
// 'intersectionArea' - match by area of breakpoint/size intersection, | ||
// when the biggest intersection area is considered to be more specific | ||
matchBy: PropTypes.oneOf(['order', 'breakpointArea', 'intersectionArea']), | ||
|
||
// By default only container will be rendered on first render() call (because usually it's a block element) | ||
// to compute its width and decide what breakpoint pass down to render function. | ||
// But if container is a part of flex, or grow according to its content, or breakpoint depend on height, | ||
// content should be rendered with visibility:hidden before breakpoint computation. | ||
renderContentOnInit: PropTypes.bool, // By default - false | ||
// If renderContentOnInit is true, pass this flag to show content on first render before computation | ||
showOnInit: PropTypes.bool, // By default - false | ||
|
||
// Container can mimic any html element ('div', 'h2' etc) or custom component (constructors like Link, Button etc) | ||
type: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), | ||
|
||
// Render function, that is called on breakpoint change | ||
children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired, | ||
|
||
// Optional callback on each size change | ||
onSizeChange: PropTypes.func, | ||
}; | ||
|
||
static defaultProps = { | ||
renderContentOnInit: false, | ||
showOnInit: false, | ||
matchBy: 'order', | ||
type: 'div', | ||
}; | ||
|
||
constructor(props, context) { | ||
super(props, context); | ||
|
||
this.processBreakpoints(props.breakpoints); | ||
this.state = { | ||
breakpoint: props.renderContentOnInit ? this.findBreakpoint() : null, | ||
}; | ||
|
||
this.saveRef = this.saveRef.bind(this); | ||
} | ||
|
||
componentWillReceiveProps(nextProps) { | ||
const {props} = this; | ||
|
||
if (nextProps.breakpoints !== props.breakpoints || nextProps.matchBy !== props.matchBy) { | ||
this.processBreakpoints(nextProps.breakpoints); | ||
|
||
const breakpoint = this.findBreakpoint(this.size); | ||
|
||
if (breakpoint !== this.state.breakpoint) { | ||
this.setState({breakpoint}); | ||
} | ||
} | ||
} | ||
|
||
componentWillUnmount() { | ||
this.checkOut(); | ||
} | ||
|
||
saveRef(dom) { | ||
if (dom) { | ||
// If type props contains custom react components, ref returns instance of that component | ||
// Try taking dom by calling method getWrappedInstance on it | ||
while (typeof dom.getWrappedInstance === 'function') { | ||
dom = dom.getWrappedInstance(); | ||
} | ||
|
||
this.checkIn(dom); | ||
} else { | ||
this.checkOut(); | ||
} | ||
} | ||
|
||
checkIn(dom) { | ||
this.dom = dom; | ||
this.context.sizeWatcher.checkIn(this, dom); | ||
} | ||
|
||
checkOut() { | ||
if (this.dom) { | ||
this.context.sizeWatcher.checkOut(this.dom); | ||
this.dom = null; | ||
} | ||
} | ||
|
||
updateSize(size) { | ||
if (this.size === undefined || size.width !== this.size.width || size.height !== this.size.height) { | ||
const breakpointNeedCheck = | ||
this.size === undefined || | ||
this.sensitiveToWidth && size.width !== this.size.width || | ||
this.sensitiveToHeight && size.height !== this.size.height; | ||
|
||
const currentBreakpoint = this.state.breakpoint; | ||
const newBreakpoint = breakpointNeedCheck ? this.findBreakpoint(size) : currentBreakpoint; | ||
|
||
this.size = size; | ||
|
||
if (typeof this.props.onSizeChange === 'function') { | ||
this.props.onSizeChange(size, currentBreakpoint, newBreakpoint); | ||
} | ||
|
||
if (newBreakpoint !== currentBreakpoint) { | ||
this.setState({breakpoint: newBreakpoint}); | ||
} | ||
} | ||
} | ||
|
||
processBreakpoints(breakpoints) { | ||
this.sensitiveToWidth = false; | ||
this.sensitiveToHeight = false; | ||
this.breakpoints = breakpoints; | ||
|
||
for (const {minWidth, maxWidth, minHeight, maxHeight} of breakpoints) { | ||
// Validate maxWidth/minWidth in development | ||
const maxWidthType = typeof maxWidth; | ||
const minWidthType = typeof minWidth; | ||
const maxHeightType = typeof maxHeight; | ||
const minHeightType = typeof minHeight; | ||
|
||
if (maxWidthType !== 'undefined' && (maxWidthType !== 'number' || maxWidth < 0) || | ||
minWidthType !== 'undefined' && (minWidthType !== 'number' || minWidth < 0 || maxWidth && minWidth > maxWidth)) { | ||
throw Error(`Wrong min ${JSON.stringify(minWidth)} or max ${JSON.stringify(maxWidth)} width in breakpoint`); | ||
} | ||
|
||
if (maxHeightType !== 'undefined' && (maxHeightType !== 'number' || maxHeight < 0) || | ||
minHeightType !== 'undefined' && (minHeightType !== 'number' || minHeight < 0 || maxHeight && minHeight > maxHeight)) { | ||
throw Error(`Wrong min ${JSON.stringify(minHeight)} or max ${JSON.stringify(maxHeight)} height in breakpoint`); | ||
} | ||
|
||
if (!this.sensitiveToWidth && (minWidth || maxWidth)) { | ||
this.sensitiveToWidth = true; | ||
} | ||
|
||
if (!this.sensitiveToHeight && (minHeight || maxHeight)) { | ||
this.sensitiveToHeight = true; | ||
} | ||
} | ||
|
||
if (this.props.matchBy === 'order') { | ||
this.findBreakpoint = this.findBreakpointByOrder; | ||
} else if (this.props.matchBy === 'breakpointArea') { | ||
this.findBreakpoint = this.findBreakpointByArea; | ||
} else if (this.props.matchBy === 'intersectionArea') { | ||
this.findBreakpoint = this.findBreakpointByIntersection; | ||
} | ||
} | ||
|
||
// Find the last breakpoint that matches given size | ||
findBreakpointByOrder({width = Infinity, height = Infinity} = {}) { | ||
for (let i = this.breakpoints.length; i--;) { | ||
const breakpoint = this.breakpoints[i]; | ||
const {minWidth = 0, maxWidth = Infinity, minHeight = 0, maxHeight = Infinity} = breakpoint; | ||
|
||
if (width >= minWidth && width <= maxWidth && height >= minHeight && height <= maxHeight) { | ||
return breakpoint; | ||
} | ||
} | ||
|
||
return defaultBreakpoint; | ||
} | ||
|
||
// Find breakpoint with the smallest area | ||
findBreakpointByArea({width = 1e6, height = 1e6} = {}) { | ||
return this.breakpoints.reduce((result, breakpoint) => { | ||
const {minWidth = 0, maxWidth = 1e6, minHeight = 0, maxHeight = 1e6} = breakpoint; | ||
|
||
if (width >= minWidth && width <= maxWidth && height >= minHeight && height <= maxHeight) { | ||
const area = (maxWidth - minWidth) * (maxHeight - minHeight); | ||
|
||
if (area <= result.area) { | ||
result.area = area; | ||
result.breakpoint = breakpoint; | ||
} | ||
} | ||
|
||
return result; | ||
}, {area: Infinity, breakpoint: defaultBreakpoint}).breakpoint; | ||
} | ||
|
||
// Find breakpoint with the biggest breakpoint/size intersection area | ||
findBreakpointByIntersection({width = 1e6, height = 1e6} = {}) { | ||
return this.breakpoints.reduce((result, breakpoint) => { | ||
const {minWidth = 0, maxWidth = 1e6, minHeight = 0, maxHeight = 1e6} = breakpoint; | ||
const area = Math.max(0, Math.min(maxWidth, width) - minWidth) * Math.max(0, Math.min(maxHeight, height) - minHeight); | ||
|
||
if (area >= result.area) { | ||
result.area = area; | ||
result.breakpoint = breakpoint; | ||
} | ||
|
||
return result; | ||
}, {area: 0, breakpoint: defaultBreakpoint}).breakpoint; | ||
} | ||
|
||
render() { | ||
const { | ||
props: { | ||
type, breakpoints, matchBy, renderContentOnInit, showOnInit, children, onSizeChange, | ||
...elementProps | ||
}, | ||
state: {breakpoint}, | ||
} = this; | ||
|
||
let renderedChildren; | ||
|
||
if (breakpoint !== null) { | ||
renderedChildren = typeof children === 'function' ? children(breakpoint, this.size) : children; | ||
|
||
Object.assign(elementProps, breakpoint.props); | ||
|
||
if (this.size === undefined && !showOnInit) { | ||
elementProps.style = {...elementProps.style, visibility: 'hidden'}; | ||
} | ||
} | ||
|
||
elementProps.ref = this.saveRef; | ||
|
||
return createElement(type, elementProps, renderedChildren); | ||
} | ||
} |
Oops, something went wrong.