├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── README.md ├── demo └── src │ ├── index.css │ └── index.js ├── nwb.config.js ├── package-lock.json ├── package.json ├── src ├── index.css ├── index.js └── utils │ ├── isModern.js │ ├── loadImage.js │ └── states.js └── tests ├── .eslintrc └── index-test.js /.gitignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | /demo/dist 3 | /es 4 | /lib 5 | /node_modules 6 | /umd 7 | npm-debug.log* 8 | **/.DS_Store 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: node_js 4 | node_js: 5 | - 8 6 | 7 | before_install: 8 | - npm install codecov.io coveralls 9 | 10 | after_success: 11 | - cat ./coverage/lcov.info | ./node_modules/codecov.io/bin/codecov.io.js 12 | - cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js 13 | 14 | branches: 15 | only: 16 | - master 17 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Prerequisites 2 | 3 | [Node.js](http://nodejs.org/) >= 6 must be installed. 4 | 5 | ## Installation 6 | 7 | - Running `npm install` in the component's root directory will install everything you need for development. 8 | 9 | ## Demo Development Server 10 | 11 | - `npm start` will run a development server with the component's demo app at [http://localhost:3000](http://localhost:3000) with hot module reloading. 12 | 13 | ## Running Tests 14 | 15 | - `npm test` will run the tests once. 16 | 17 | - `npm run test:coverage` will run the tests and produce a coverage report in `coverage/`. 18 | 19 | - `npm run test:watch` will run the tests on every change. 20 | 21 | ## Building 22 | 23 | - `npm run build` will build the component for publishing to npm and also bundle the demo app. 24 | 25 | - `npm run clean` will delete built resources. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Total Downloads 2 | Latest Release 3 | License 4 | 5 | # react-fitted-image 6 | 7 | Pictures are always difficult to handle in web page, especially in a responsive way. A well-known workaround is to style the img tag with something like this: 8 | 9 | ```css 10 | img { 11 | max-width: 100%; 12 | height: auto; 13 | } 14 | ``` 15 | 16 | This method works well, but there is a lot of cases where it's not sufficient. Fortunately CSS introduced some times ago the ["object-fit" property](https://developer.mozilla.org/fr/docs/Web/CSS/object-fit), improving the way we can handle image displaying. 17 | The current component provides an easy react binding to display images with object-fit property. 18 | 19 | In addition, you can provide a loader which will be displayed during the image processing by the browser. 20 | 21 | Object-fit is well supported by browsers but not all of them for now ( see [Caniuse](http://caniuse.com/#feat=object-fit) ). To work on stuffs like IE, a basic test is done with [CSS.supports](https://developer.mozilla.org/en/docs/Web/API/CSS/supports) to provide a fallback based on "background" CSS property. 22 | 23 | ## Demo 24 | 25 | http://alexjoffroy.github.io/react-fitted-image/example/ 26 | 27 | ## Usage 28 | 29 | ### Install 30 | 31 | ``` 32 | npm install --save react-fitted-image 33 | ``` 34 | 35 | ### Properties 36 | 37 | | Property | Type | Description | Default value | Required | 38 | | ---------- | -------- | ---------------------------------------------------------------------- | ------------- | -------- | 39 | | background | bool | Force the component to use the CSS bacground properties | false | no | 40 | | className | string | Custom classname for the component | \_ | no | 41 | | fit | string | Value of the object-fit property. Can be "auto", "contain", or "cover" | "auto" | no | 42 | | loader | element | Component to use as loader | \_ | no | 43 | | onLoad | function | Success callback for image loading | \_ | no | 44 | | onError | function | Error callback for image loading | \_ | no | 45 | | src | string | Image url to render | \_ | yes | 46 | | style | object | Custom styles | {} | no | 47 | 48 | ### Example 49 | 50 | ```javascript 51 | Loading} 54 | onLoad={(...args) => console.log(...args)} 55 | onError={(...args) => console.log(...args)} 56 | src="public/img.jpg" 57 | /> 58 | ``` 59 | -------------------------------------------------------------------------------- /demo/src/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | } 6 | .App { 7 | margin: auto; 8 | padding: 0.5rem 0; 9 | max-width: 50rem; 10 | height: 100vh; 11 | display: flex; 12 | flex-direction: column; 13 | } 14 | .Settings { 15 | margin-bottom: 0.5rem; 16 | display: flex; 17 | flex-direction: row; 18 | } 19 | .Settings > label { 20 | flex: 1; 21 | } 22 | .Settings > select { 23 | flex: 4; 24 | } 25 | .Wrapper { 26 | border: 1px solid #ddd; 27 | height: 90%; 28 | } 29 | .Loader { 30 | display: flex; 31 | justify-content: center; 32 | align-items: center; 33 | height: 100%; 34 | width: 100%; 35 | } 36 | -------------------------------------------------------------------------------- /demo/src/index.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | import {render} from 'react-dom' 3 | 4 | import FittedImage from '../../lib' 5 | 6 | import './index.css' 7 | 8 | const fitOptions = [ 9 | { value: 'auto', label: 'Auto' }, 10 | { value: 'contain', label: 'Contain' }, 11 | { value: 'cover', label: 'Cover' } 12 | ]; 13 | 14 | const sizeOptions = [ 15 | { value: '1200/700', label: '1200x700' }, 16 | { value: '500/800', label: '500x800' }, 17 | { value: '900/900', label: '900x900' } 18 | ]; 19 | 20 | class Demo extends Component { 21 | 22 | constructor(props) { 23 | super(props) 24 | 25 | this.state = { 26 | fit: 'contain', 27 | size: '1200/700' 28 | } 29 | } 30 | 31 | getFitOptions() { 32 | return fitOptions.map( (opt, key) => { 33 | return ; 34 | }); 35 | } 36 | 37 | getSizeOptions() { 38 | return sizeOptions.map( (opt, key) => { 39 | return ; 40 | }); 41 | } 42 | 43 | onPropChange(prop, e) { 44 | let state = {}; 45 | state[prop] = e.target.value; 46 | this.setState(state); 47 | } 48 | 49 | render() { 50 | return ( 51 |
52 |
53 | 54 | 57 |
58 |
59 | 60 | 63 |
64 |
65 | Loading...
} /> 69 |
70 | 71 | ) 72 | } 73 | 74 | } 75 | 76 | render(, document.querySelector('#demo')) 77 | -------------------------------------------------------------------------------- /nwb.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | type: 'react-component', 3 | npm: { 4 | esModules: true, 5 | umd: false 6 | }, 7 | devServer: { 8 | port: 1234 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-fitted-image", 3 | "version": "1.0.1", 4 | "description": "React component for image handling with object-fit", 5 | "main": "lib/index.js", 6 | "module": "es/index.js", 7 | "files": [ 8 | "css", 9 | "es", 10 | "lib", 11 | "umd" 12 | ], 13 | "scripts": { 14 | "build": "nwb build-react-component --copy-files", 15 | "clean": "nwb clean-module && nwb clean-demo", 16 | "prepublishOnly": "npm run build", 17 | "start": "nwb serve-react-demo", 18 | "test": "nwb test-react", 19 | "test:coverage": "nwb test-react --coverage", 20 | "test:watch": "nwb test-react --server" 21 | }, 22 | "dependencies": {}, 23 | "peerDependencies": { 24 | "react": "16.x" 25 | }, 26 | "devDependencies": { 27 | "babel-runtime": "^6.26.0", 28 | "enzyme": "^3.9.0", 29 | "enzyme-adapter-react-16": "^1.13.2", 30 | "nwb": "0.23.x", 31 | "react": "^16.8.6", 32 | "react-dom": "^16.8.6" 33 | }, 34 | "author": "alexjoffroy", 35 | "homepage": "https://github.com/alexjoffroy/react-fitted-image", 36 | "license": "MIT", 37 | "repository": { 38 | "type": "git", 39 | "url": "https://github.com/alexjoffroy/react-fitted-image.git" 40 | }, 41 | "bugs": { 42 | "url": "https://github.com/alexjoffroy/react-fitted-image/issues" 43 | }, 44 | "keywords": [ 45 | "react", 46 | "image", 47 | "picture", 48 | "load", 49 | "object-fit", 50 | "img", 51 | "react-component" 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | .FittedImage { 2 | width: 100%; 3 | height: 100%; 4 | } 5 | 6 | .FittedImage--contain { 7 | object-fit: contain; 8 | } 9 | 10 | .FittedImage--cover { 11 | object-fit: cover; 12 | } 13 | 14 | .FittedImage--background { 15 | background-repeat: no-repeat; 16 | background-position: center; 17 | } 18 | 19 | .FittedImage--background .FittedImage--contain { 20 | background-size: contain; 21 | } 22 | 23 | .FittedImage--background .FittedImage--cover { 24 | background-size: cover; 25 | } 26 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | import PropTypes from 'prop-types'; 3 | 4 | import isModern from './utils/isModern' 5 | import loadImage from './utils/loadImage' 6 | import states from './utils/states' 7 | 8 | import './index.css' 9 | 10 | class FittedImage extends Component { 11 | 12 | constructor(props) { 13 | super(props) 14 | 15 | this.state = { 16 | status: states.PENDING 17 | } 18 | } 19 | 20 | componentDidMount() { 21 | if (this.props.loader) { 22 | this._loadImage(); 23 | } 24 | } 25 | 26 | componentWillReceiveProps(props) { 27 | if (this.props.src !== props.src) { 28 | this._loadImage(); 29 | } 30 | } 31 | 32 | render() { 33 | if (this.props.loader && !this._isLoaded()) { 34 | return this.props.loader; 35 | } 36 | 37 | return this._getImage() 38 | } 39 | 40 | _getClassName(background) { 41 | return [ 42 | 'FittedImage', 43 | background ? 'FittedImage--background' : null, 44 | 'FittedImage--' + this.props.fit, 45 | this.props.className 46 | ].filter(item => !!item).join(' ') 47 | } 48 | 49 | _getImage() { 50 | /* eslint-disable no-unused-vars */ 51 | const { background, fit, src, loader, onLoad, onError, ...props } = this.props 52 | 53 | if ( !background && isModern ) { 54 | return 55 | } 56 | 57 | return ( 58 |
61 | ) 62 | } 63 | 64 | _isLoaded() { 65 | return states.LOADED === this.state.status 66 | } 67 | 68 | async _loadImage() { 69 | this.setState({ 70 | status: states.LOADING 71 | }) 72 | 73 | try { 74 | await loadImage(this.props.src) 75 | } catch(err) { 76 | this._onLoadError() 77 | } 78 | 79 | this._onLoadSuccess() 80 | } 81 | 82 | _onLoadSuccess() { 83 | this.setState({ 84 | status: states.LOADED 85 | }) 86 | 87 | this.props.onLoad() 88 | } 89 | 90 | _onLoadError() { 91 | this.setState({ 92 | status: states.DEAD 93 | }) 94 | 95 | this.props.onError() 96 | } 97 | 98 | } 99 | 100 | FittedImage.propTypes = { 101 | background: PropTypes.bool, 102 | className: PropTypes.string, 103 | fit: PropTypes.oneOf(['auto', 'contain', 'cover']), 104 | loader: PropTypes.element, 105 | src: PropTypes.string.isRequired, 106 | style: PropTypes.object, 107 | onLoad: PropTypes.func, 108 | onError: PropTypes.func 109 | } 110 | 111 | FittedImage.defaultProps = { 112 | background: false, 113 | fit: 'auto', 114 | style: {}, 115 | onLoad: () => {}, 116 | onError: () => {} 117 | } 118 | 119 | export default FittedImage -------------------------------------------------------------------------------- /src/utils/isModern.js: -------------------------------------------------------------------------------- 1 | const isModern = window.CSS && CSS.supports && CSS.supports('object-fit', 'cover') 2 | 3 | export default isModern -------------------------------------------------------------------------------- /src/utils/loadImage.js: -------------------------------------------------------------------------------- 1 | export default function loadImage(url) { 2 | return new Promise((resolve, reject) => { 3 | 4 | let image = new Image() 5 | 6 | image.onload = resolve 7 | 8 | image.onerror = () => reject(new Error('Error when loading ' + url)) 9 | 10 | image.src = url; 11 | 12 | }) 13 | } -------------------------------------------------------------------------------- /src/utils/states.js: -------------------------------------------------------------------------------- 1 | const states = { 2 | PENDING: 0, 3 | LOADING: 1, 4 | LOADED: 2, 5 | DEAD: 3 6 | } 7 | 8 | export default states -------------------------------------------------------------------------------- /tests/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /tests/index-test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import expect from 'expect' 3 | import { configure, mount } from 'enzyme'; 4 | import Adapter from 'enzyme-adapter-react-16'; 5 | import FittedImage from 'src/' 6 | 7 | configure({ adapter: new Adapter() }); 8 | 9 | describe('FittedImage', () => { 10 | 11 | it('should have the "FittedImage" and "FittedImage--auto" classes by default', () => { 12 | 13 | const wrapper = mount() 14 | 15 | expect(wrapper.children().length).toBe(1) 16 | expect(wrapper.find('.FittedImage.FittedImage--auto').length).toBe(1) 17 | 18 | }) 19 | 20 | it('should render a div tag with "FittedImage--background" when browser does not support object-fit', () => { 21 | 22 | const wrapper = mount() 23 | 24 | expect(wrapper.children().length).toBe(1) 25 | expect(wrapper.find('.FittedImage--background').length).toBe(1) 26 | 27 | }) 28 | 29 | it('should render a div tag with "FittedImage--background" when background=true', () => { 30 | 31 | const wrapper = mount() 32 | 33 | expect(wrapper.children().length).toBe(1) 34 | expect(wrapper.find('.FittedImage--background').length).toBe(1) 35 | 36 | }) 37 | 38 | it('should have the "FittedImage--contain" class when fit="contain"', () => { 39 | 40 | const wrapper = mount() 41 | 42 | expect(wrapper.children().length).toBe(1) 43 | expect(wrapper.find('.FittedImage--contain').length).toBe(1) 44 | 45 | }) 46 | 47 | it('should have the "FittedImage--contain" class when fit="cover"', () => { 48 | 49 | const wrapper = mount() 50 | 51 | expect(wrapper.children().length).toBe(1) 52 | expect(wrapper.find('.FittedImage--cover').length).toBe(1) 53 | 54 | }) 55 | 56 | it('should accept custom classes', () => { 57 | 58 | const wrapper = mount() 59 | 60 | expect(wrapper.children().length).toBe(1) 61 | expect(wrapper.find('.FittedImage.CustomClass.OtherOne').length).toBe(1) 62 | 63 | }) 64 | 65 | it('should accept custom styles', () => { 66 | 67 | const wrapper = mount() 68 | 69 | expect(wrapper.children().length).toBe(1) 70 | expect(wrapper.find('.FittedImage').length).toBe(1) 71 | expect(wrapper.find('.FittedImage').first().props().style.width).toBe('200px') 72 | 73 | }) 74 | 75 | }) 76 | --------------------------------------------------------------------------------