├── .gitignore ├── .npmignore ├── LICENSE ├── Makefile ├── README.md ├── examples ├── bigger-version-of-houses.jpg ├── examples.cjsx ├── houses.jpg ├── index.cjsx ├── index.html ├── ocean.jpg ├── ocean@2x.jpg ├── path.jpg ├── tower.jpg └── tower@2x.jpg ├── package.json ├── src └── index.cjsx ├── webpack.config.js └── webpack.production.config.js /.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 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | dist/ 30 | examples/bundle.js 31 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 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 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | src/ 30 | examples/bundle.js 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Kyle Mathews 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 | 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BIN = ./node_modules/.bin 2 | 3 | release-patch: 4 | @$(call release,patch) 5 | 6 | release-minor: 7 | @$(call release,minor) 8 | 9 | release-major: 10 | @$(call release,major) 11 | 12 | build: 13 | @$(BIN)/cjsx -cb -o dist src/index.cjsx 14 | @$(BIN)/webpack 15 | 16 | publish: 17 | git push --tags origin HEAD:master 18 | @$(BIN)/cjsx -cb -o dist src/index.cjsx 19 | npm publish 20 | 21 | publish-gh-pages: 22 | git checkout gh-pages 23 | git merge master 24 | @$(BIN)/webpack --config webpack.production.config.js 25 | cp examples/* . 26 | git add --all . 27 | git commit -m "New release" 28 | git push origin gh-pages 29 | git checkout master 30 | 31 | define release 32 | npm version $(1) 33 | endef 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | react-retina-image 2 | ================== 3 | 4 | React component for serving high-resolution images to devices with retina displays 5 | 6 | ## Demo 7 | http://kyleamathews.github.io/react-retina-image/ 8 | 9 | ## Install 10 | `npm install react-retina-image` 11 | 12 | ## Usage 13 | 14 | Available props: 15 | 16 | * `checkIfRetinaImgExists` — test if retina image exists before swapping 17 | it in. If you're sure there's a retina image available, it's safe to 18 | set this to false. Defaults to true. 19 | * `forceOriginalDimensions` — sets width/height of retina image to 20 | original image. Note, this doesn't work if `checkIfRetinaImgExists` is set to 21 | false as then the original image is never loaded. In this case you'll 22 | need to set the width manually either as a prop or using css. Defaults to true. 23 | * `retinaImageSuffix` — defaults to `@2x` but you can change this if you 24 | use a different naming convention. 25 | * `onLoad` — handle the image onLoad event. 26 | * `onError` — handle the image onError event. 27 | * `src` — string or array for the image srcs. [See the 28 | demo](http://kyleamathews.github.io/react-retina-image/) for examples 29 | of how to format your src string or array. 30 | 31 | ```javascript 32 | var React = require('react'); 33 | var RetinaImage = require('react-retina-image'); 34 | 35 | React.createClass({ 36 | render: function () { 37 | 38 | } 39 | }); 40 | 41 | // Can also pass in array of srcs. 42 | React.createClass({ 43 | render: function () { 44 | 46 | } 47 | }); 48 | ``` 49 | 50 | ## Attribution 51 | This component is largely a port of 52 | [retina.js](http://imulus.github.io/retinajs/) to React.js 53 | -------------------------------------------------------------------------------- /examples/bigger-version-of-houses.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KyleAMathews/react-retina-image/1703a0b946a53f1aaa1d6e2293b76b0dff2f9205/examples/bigger-version-of-houses.jpg -------------------------------------------------------------------------------- /examples/examples.cjsx: -------------------------------------------------------------------------------- 1 | React = require('react') 2 | RetinaImage = require '../src/index' 3 | isRetina = require 'is-retina' 4 | _ = require 'underscore' 5 | 6 | module.exports = class Examples extends React.Component 7 | constructor: (props) -> 8 | super props 9 | @state = 10 | picsArray: [ 11 | './tower.jpg' 12 | './path.jpg' 13 | './ocean.jpg' 14 | ] 15 | picIndex: 0 16 | 17 | render: -> 18 |
19 |

React-retina-image

20 | Code on Github 21 |
22 |
23 | {if isRetina() then

Your screen is retina!

else

Your screen is not retina

} 24 | 25 | {if isRetina() 26 |

This image loaded its retina version after checking if it exists

27 | else 28 |

This image won't load its retina version

29 | } 30 |

31 |       {"""
32 |       
33 |         """}
34 |       
35 | 36 | 37 |

This image doesn't have a @2x version so stays at its lower resolution version

38 |

39 |       {"""
40 |       
41 |         """}
42 |       
43 | 44 | 45 |

If you know there's a retina image available, you can disable the check.

46 |

47 |       {"""
48 |       
52 |         """}
53 |       
54 | 55 | 56 |

If you don't have predictable names for the retina and non-retina 57 | versions of images, you can simply pass in an array of images as src where 58 | the first src is the non-retina version and the second is the retina version. 59 |

60 |

61 |       {"""
62 |       
64 |         """}
65 |       
66 | 70 | 71 |

For testing updates. Click on the image and it'll cycle forward 72 | through pictures

73 |

74 |       {"""
75 |       
79 |         """}
80 |       
81 | 85 | 86 |
87 | 88 | cyclePics: => 89 | newIndex = @state.picIndex + 1 90 | if newIndex > 2 91 | newIndex = 0 92 | 93 | @setState picIndex: newIndex 94 | -------------------------------------------------------------------------------- /examples/houses.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KyleAMathews/react-retina-image/1703a0b946a53f1aaa1d6e2293b76b0dff2f9205/examples/houses.jpg -------------------------------------------------------------------------------- /examples/index.cjsx: -------------------------------------------------------------------------------- 1 | React = require 'react' 2 | ReactDOM = require 'react-dom' 3 | Examples = require './examples' 4 | 5 | # Rendering components directly into document.body is discouraged, 6 | # since its children are often manipulated by third-party scripts and browser extensions. 7 | # This may lead to subtle reconciliation issues. 8 | containerEl = document.createElement('DIV') 9 | document.body.appendChild(containerEl) 10 | ReactDOM.render(, containerEl) 11 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | React Retina Image 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/ocean.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KyleAMathews/react-retina-image/1703a0b946a53f1aaa1d6e2293b76b0dff2f9205/examples/ocean.jpg -------------------------------------------------------------------------------- /examples/ocean@2x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KyleAMathews/react-retina-image/1703a0b946a53f1aaa1d6e2293b76b0dff2f9205/examples/ocean@2x.jpg -------------------------------------------------------------------------------- /examples/path.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KyleAMathews/react-retina-image/1703a0b946a53f1aaa1d6e2293b76b0dff2f9205/examples/path.jpg -------------------------------------------------------------------------------- /examples/tower.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KyleAMathews/react-retina-image/1703a0b946a53f1aaa1d6e2293b76b0dff2f9205/examples/tower.jpg -------------------------------------------------------------------------------- /examples/tower@2x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KyleAMathews/react-retina-image/1703a0b946a53f1aaa1d6e2293b76b0dff2f9205/examples/tower@2x.jpg -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-retina-image", 3 | "description": "React component for serving high-resolution images to devices with retina displays", 4 | "version": "2.0.5", 5 | "author": "Kyle Mathews ", 6 | "bugs": { 7 | "url": "https://github.com/KyleAMathews/react-retina-image/issues" 8 | }, 9 | "dependencies": { 10 | "array-equal": "^1.0.0", 11 | "image-exists": "^1.1.0", 12 | "is-retina": "^1.0.3", 13 | "object-assign": "^4.1.0", 14 | "prop-types": "^15.5.6" 15 | }, 16 | "devDependencies": { 17 | "cjsx-loader": "^3.0.0", 18 | "coffee-loader": "^0.7.2", 19 | "coffee-react": "^5.0.0", 20 | "coffee-script": "^1.11.0", 21 | "css-loader": "^0.25.0", 22 | "gulp": "^3.9.1", 23 | "react": "^15.5.6 || ^16.0.0", 24 | "react-dom": "^15.5.6 || ^16.0.0", 25 | "style-loader": "^0.13.1", 26 | "underscore": "^1.8.3", 27 | "webpack": "^1.13.2", 28 | "webpack-dev-server": "^1.16.1" 29 | }, 30 | "peerDependencies": { 31 | "react": "^15.5.6 || ^16.0.0", 32 | "react-dom": "^15.5.6 || ^16.0.0" 33 | }, 34 | "directories": { 35 | "example": "examples" 36 | }, 37 | "homepage": "https://github.com/KyleAMathews/react-retina-image", 38 | "keywords": [ 39 | "image", 40 | "react", 41 | "react-component", 42 | "retina" 43 | ], 44 | "license": "MIT", 45 | "main": "dist/index.js", 46 | "repository": { 47 | "type": "git", 48 | "url": "https://github.com/KyleAMathews/react-retina-image.git" 49 | }, 50 | "scripts": { 51 | "test": "echo \"Error: no test specified\" && exit 1", 52 | "watch": "./node_modules/.bin/webpack-dev-server" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/index.cjsx: -------------------------------------------------------------------------------- 1 | React = require 'react' 2 | PropTypes = require 'prop-types' 3 | isRetina = require 'is-retina' 4 | imageExists = require 'image-exists' 5 | path = require 'path' 6 | assign = require 'object-assign' 7 | arrayEqual = require 'array-equal' 8 | 9 | module.exports = class RetinaImage extends React.Component 10 | @propTypes: 11 | src: PropTypes.oneOfType([ 12 | PropTypes.string 13 | PropTypes.array 14 | ]).isRequired 15 | checkIfRetinaImgExists: PropTypes.bool 16 | forceOriginalDimensions: PropTypes.bool 17 | retinaImageSuffix: PropTypes.string 18 | handleOnLoad: PropTypes.func # Deprecated. 19 | onLoad: PropTypes.func 20 | onError: PropTypes.func 21 | 22 | @defaultProps: 23 | checkIfRetinaImgExists: true 24 | forceOriginalDimensions: true 25 | retinaImageSuffix: '@2x' 26 | onError: -> 27 | 28 | constructor: (props) -> 29 | super props 30 | @state = @wrangleProps() 31 | 32 | componentWillReceiveProps: (nextProps) -> 33 | isEqual = true 34 | if Array.isArray(@props.src) and Array.isArray(nextProps.src) 35 | isEqual = arrayEqual(@props.src, nextProps.src) 36 | else 37 | isEqual = @props.src is nextProps.src 38 | 39 | unless isEqual 40 | @setState assign @wrangleProps(nextProps), { 41 | width: null 42 | height: null 43 | imgLoaded: null 44 | retinaImgExists: null 45 | retinaCheckComplete: null 46 | } 47 | 48 | componentDidMount: -> 49 | @checkForRetina() 50 | @checkLoaded() 51 | 52 | componentDidUpdate: -> 53 | @checkForRetina() 54 | 55 | render: -> 56 | # Propagate only the props that `` supports, avoid React `Unknown props` warning. https://fb.me/react-unknown-prop 57 | # CoffeeScript does not support splat `...` for object destructuring so using `assign` and `delete`. http://stackoverflow.com/a/20298038 58 | imgProps = assign {}, @props 59 | delete imgProps.src 60 | delete imgProps.checkIfRetinaImgExists 61 | delete imgProps.forceOriginalDimensions 62 | delete imgProps.retinaImageSuffix 63 | delete imgProps.handleOnLoad 64 | delete imgProps.onLoad 65 | delete imgProps.onError 66 | 67 | # Override some of the props for ``. 68 | imgProps.src = @state.src 69 | imgProps.onLoad = @handleOnLoad 70 | imgProps.onError = @props.onError 71 | 72 | if @state.width >= 0 73 | imgProps.width = @state.width 74 | 75 | if @state.height >= 0 76 | imgProps.height = @state.height 77 | 78 | 81 | 82 | # src can be a href or an array of hrefs. 83 | wrangleProps: (props=@props) -> 84 | if Array.isArray(props.src) 85 | return { 86 | src: props.src[0] 87 | srcIsArray: true 88 | } 89 | else 90 | return { 91 | src: props.src 92 | srcIsArray: false 93 | } 94 | 95 | checkForRetina: -> 96 | if @state.retinaCheckComplete then return 97 | 98 | if isRetina() and @props.checkIfRetinaImgExists 99 | imageExists @getRetinaPath(), (exists) => 100 | # If original image has loaded already (we have to wait so we know 101 | # the original image dimensions), then set the retina image path. 102 | if exists and @state?.imgLoaded 103 | @setState src: @getRetinaPath() 104 | else if exists 105 | @setState retinaImgExists: true 106 | 107 | @setState retinaCheckComplete: true 108 | 109 | # If the check isn't needed, immediately swap in the retina path 110 | else if isRetina() and not @props.checkIfRetinaImgExists 111 | @setState src: @getRetinaPath() 112 | 113 | @setState retinaCheckComplete: true 114 | 115 | # For server-rendered code, sometimes images will already be loaded by the time 116 | # this module mounts. 117 | # http://stackoverflow.com/a/1977898 118 | checkLoaded: -> 119 | el = @refs.img 120 | 121 | unless el.complete 122 | return false 123 | 124 | if el.naturalWidth is 0 125 | return false 126 | 127 | # No other way to disprove it's loaded so we'll assume it's ok. 128 | @handleOnLoad() 129 | 130 | 131 | handleOnLoad: (e) => 132 | # Customers of component might care when the image loads. 133 | if @props.onLoad? 134 | @props.onLoad(e) 135 | # handleOnLoad was in an earlier version (wrong name) and will be removed 136 | # at the next major release. 137 | if @props.handleOnLoad? 138 | @props.handleOnLoad(e) 139 | 140 | if @props.forceOriginalDimensions 141 | @setState { 142 | width: @refs.img.clientWidth 143 | height: @refs.img.clientHeight 144 | } 145 | 146 | @setState imgLoaded: true 147 | 148 | # If the retina image check has already finished, set the 2x path. 149 | if @state?.retinaImgExists or not @props.checkIfRetinaImgExists 150 | @setState src: @getRetinaPath() 151 | 152 | getRetinaPath: -> 153 | if @state.srcIsArray 154 | return @props.src[1] 155 | else 156 | basename = path.basename(@props.src, path.extname(@props.src)) 157 | basename = basename + @props.retinaImageSuffix + path.extname(@props.src) 158 | src = @props.src.replace(path.basename(@props.src), basename) 159 | return src 160 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | entry: [ 6 | "webpack-dev-server/client?http://0.0.0.0:8080", 7 | './examples/index' 8 | ], 9 | devServer: { 10 | contentBase: './examples/' 11 | }, 12 | devtool: "eval", 13 | debug: true, 14 | output: { 15 | path: path.join(__dirname, 'examples'), 16 | filename: 'bundle.js', 17 | }, 18 | resolveLoader: { 19 | modulesDirectories: ['node_modules'] 20 | }, 21 | plugins: [ 22 | new webpack.NoErrorsPlugin(), 23 | new webpack.IgnorePlugin(/un~$/) 24 | ], 25 | resolve: { 26 | extensions: ['', '.js', '.cjsx', '.coffee'] 27 | }, 28 | module: { 29 | loaders: [ 30 | { test: /\.css$/, loaders: ['style', 'css']}, 31 | { test: /\.cjsx$/, loaders: ['coffee', 'cjsx']}, 32 | { test: /\.coffee$/, loader: 'coffee' } 33 | ] 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /webpack.production.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | 5 | module.exports = { 6 | entry: [ 7 | './examples/index' 8 | ], 9 | output: { 10 | path: path.join(__dirname, 'examples'), 11 | filename: 'bundle.js', 12 | }, 13 | resolveLoader: { 14 | modulesDirectories: ['node_modules'] 15 | }, 16 | plugins: [ 17 | new webpack.DefinePlugin({ 18 | "process.env": { 19 | NODE_ENV: JSON.stringify("production") 20 | } 21 | }), 22 | new webpack.optimize.DedupePlugin(), 23 | new webpack.optimize.UglifyJsPlugin() 24 | ], 25 | resolve: { 26 | extensions: ['', '.js', '.cjsx', '.coffee'] 27 | }, 28 | module: { 29 | loaders: [ 30 | { test: /\.css$/, loaders: ['style', 'css']}, 31 | { test: /\.cjsx$/, loaders: ['coffee', 'cjsx']}, 32 | { test: /\.coffee$/, loader: 'coffee' } 33 | ] 34 | } 35 | }; 36 | --------------------------------------------------------------------------------