├── .babelrc ├── .gitignore ├── LICENSE ├── README.md ├── build └── reactBlurImageLoader.js ├── index.jsx ├── package.json ├── samples ├── 1.gif ├── sample-large.jpg └── sample-preview.jpg ├── src ├── BlurImageLoader.jsx ├── DefaultLoader.jsx └── ImageContainer.jsx ├── styles.css └── webpack.build.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "react", 4 | "es2015" 5 | ], 6 | "ignore": [ 7 | "node_modules" 8 | ] 9 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Marko Cen 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-blur-image-loader 2 | 3 | Progressive Image Loading for React. 4 | 5 | ![](https://cdn.rawgit.com/MarkoCen/react-blur-image-loader/3cf93750/samples/1.gif) 6 | 7 | ### Setup 8 | 1. Install 9 | ``` 10 | npm install react-blur-image-loader react react-dom -S 11 | ``` 12 | 2. Usage 13 | 14 | ```javascript 15 | import {render} from 'react-dom'; 16 | import BlurImageLoader from 'react-blur-image-loader'; 17 | 18 | render(, document.getElementById("root")); 23 | ``` 24 | 3. Props 25 | 26 | PropName | Type | Default Value | Description 27 | --- | --- | --- | --- 28 | `src` | string | Null | This field is REQUIRED, it specifies the final image url 29 | `preview` | string | "" | Specifies a preview image url, a preview image should be a tiny optimized image, it will be present until final image loaded, if `preview` is not defined, a spin loader would be present until final image loaded. 30 | `fullCover` | bool | false | Specifies whether image should cover its parent area or contain inside. 31 | `maxBlurLevel` | number | 10 | Specifies the level of Gaussian blur, refer to [stdDeviation](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stdDeviation). 32 | `transitionTime` | number | 400 | Specifies the transition time from blur to clear, default value is 400ms. 33 | `loader` | React.Element | <DefaultLoader /> | Specifies the preview loader 34 | -------------------------------------------------------------------------------- /build/reactBlurImageLoader.js: -------------------------------------------------------------------------------- 1 | !function(root,factory){"object"==typeof exports&&"object"==typeof module?module.exports=factory(require("react")):"function"==typeof define&&define.amd?define("reactBlurImageLoader",["react"],factory):"object"==typeof exports?exports.reactBlurImageLoader=factory(require("react")):root.reactBlurImageLoader=factory(root.react)}(this,function(__WEBPACK_EXTERNAL_MODULE_0__){return function(modules){function __webpack_require__(moduleId){if(installedModules[moduleId])return installedModules[moduleId].exports;var module=installedModules[moduleId]={i:moduleId,l:!1,exports:{}};return modules[moduleId].call(module.exports,module,module.exports,__webpack_require__),module.l=!0,module.exports}var installedModules={};return __webpack_require__.m=modules,__webpack_require__.c=installedModules,__webpack_require__.i=function(value){return value},__webpack_require__.d=function(exports,name,getter){__webpack_require__.o(exports,name)||Object.defineProperty(exports,name,{configurable:!1,enumerable:!0,get:getter})},__webpack_require__.n=function(module){var getter=module&&module.__esModule?function(){return module.default}:function(){return module};return __webpack_require__.d(getter,"a",getter),getter},__webpack_require__.o=function(object,property){return Object.prototype.hasOwnProperty.call(object,property)},__webpack_require__.p="",__webpack_require__(__webpack_require__.s=3)}([function(module,exports){module.exports=require("react")},function(module,exports,__webpack_require__){"use strict";function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(self,call){if(!self)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!call||"object"!=typeof call&&"function"!=typeof call?self:call}function _inherits(subClass,superClass){if("function"!=typeof superClass&&null!==superClass)throw new TypeError("Super expression must either be null or a function, not "+typeof superClass);subClass.prototype=Object.create(superClass&&superClass.prototype,{constructor:{value:subClass,enumerable:!1,writable:!0,configurable:!0}}),superClass&&(Object.setPrototypeOf?Object.setPrototypeOf(subClass,superClass):subClass.__proto__=superClass)}Object.defineProperty(exports,"__esModule",{value:!0});var _createClass=function(){function defineProperties(target,props){for(var i=0;i=0&&styleElementsInsertedAtTop.splice(idx,1)}function createStyleElement(options){var styleElement=document.createElement("style");return options.attrs.type="text/css",attachTagAttrs(styleElement,options.attrs),insertStyleElement(options,styleElement),styleElement}function createLinkElement(options){var linkElement=document.createElement("link");return options.attrs.type="text/css",options.attrs.rel="stylesheet",attachTagAttrs(linkElement,options.attrs),insertStyleElement(options,linkElement),linkElement}function attachTagAttrs(element,attrs){Object.keys(attrs).forEach(function(key){element.setAttribute(key,attrs[key])})}function addStyle(obj,options){var styleElement,update,remove;if(options.singleton){var styleIndex=singletonCounter++;styleElement=singletonElement||(singletonElement=createStyleElement(options)),update=applyToSingletonTag.bind(null,styleElement,styleIndex,!1),remove=applyToSingletonTag.bind(null,styleElement,styleIndex,!0)}else obj.sourceMap&&"function"==typeof URL&&"function"==typeof URL.createObjectURL&&"function"==typeof URL.revokeObjectURL&&"function"==typeof Blob&&"function"==typeof btoa?(styleElement=createLinkElement(options),update=updateLink.bind(null,styleElement,options),remove=function(){removeStyleElement(styleElement),styleElement.href&&URL.revokeObjectURL(styleElement.href)}):(styleElement=createStyleElement(options),update=applyToTag.bind(null,styleElement),remove=function(){removeStyleElement(styleElement)});return update(obj),function(newObj){if(newObj){if(newObj.css===obj.css&&newObj.media===obj.media&&newObj.sourceMap===obj.sourceMap)return;update(obj=newObj)}else remove()}}function applyToSingletonTag(styleElement,index,remove,obj){var css=remove?"":obj.css;if(styleElement.styleSheet)styleElement.styleSheet.cssText=replaceText(index,css);else{var cssNode=document.createTextNode(css),childNodes=styleElement.childNodes;childNodes[index]&&styleElement.removeChild(childNodes[index]),childNodes.length?styleElement.insertBefore(cssNode,childNodes[index]):styleElement.appendChild(cssNode)}}function applyToTag(styleElement,obj){var css=obj.css,media=obj.media;if(media&&styleElement.setAttribute("media",media),styleElement.styleSheet)styleElement.styleSheet.cssText=css;else{for(;styleElement.firstChild;)styleElement.removeChild(styleElement.firstChild);styleElement.appendChild(document.createTextNode(css))}}function updateLink(linkElement,options,obj){var css=obj.css,sourceMap=obj.sourceMap,autoFixUrls=void 0===options.convertToAbsoluteUrls&&sourceMap;(options.convertToAbsoluteUrls||autoFixUrls)&&(css=fixUrls(css)),sourceMap&&(css+="\n/*# sourceMappingURL=data:application/json;base64,"+btoa(unescape(encodeURIComponent(JSON.stringify(sourceMap))))+" */");var blob=new Blob([css],{type:"text/css"}),oldSrc=linkElement.href;linkElement.href=URL.createObjectURL(blob),oldSrc&&URL.revokeObjectURL(oldSrc)}var stylesInDom={},isOldIE=function(fn){var memo;return function(){return void 0===memo&&(memo=fn.apply(this,arguments)),memo}}(function(){return window&&document&&document.all&&!window.atob}),getElement=function(fn){var memo={};return function(selector){return void 0===memo[selector]&&(memo[selector]=fn.call(this,selector)),memo[selector]}}(function(styleTarget){return document.querySelector(styleTarget)}),singletonElement=null,singletonCounter=0,styleElementsInsertedAtTop=[],fixUrls=__webpack_require__(5);module.exports=function(list,options){if("undefined"!=typeof DEBUG&&DEBUG&&"object"!=typeof document)throw new Error("The style-loader cannot be used in a non-browser environment");options=options||{},options.attrs="object"==typeof options.attrs?options.attrs:{},void 0===options.singleton&&(options.singleton=isOldIE()),void 0===options.insertInto&&(options.insertInto="head"),void 0===options.insertAt&&(options.insertAt="bottom");var styles=listToStyles(list);return addStylesToDom(styles,options),function(newList){for(var mayRemove=[],i=0;i{ 30 | if(callback){ 31 | callback.call(this); 32 | } 33 | } 34 | this.image.src = src; 35 | } 36 | } 37 | 38 | loadOriginalImage(){ 39 | this.loadImage(this.props.src, ()=>{ 40 | this.setState({ 41 | showPreview: false, 42 | showLoader: false, 43 | showImage: true 44 | }) 45 | }) 46 | } 47 | 48 | loadPreviewImage(){ 49 | this.loadImage(this.props.preview, ()=>{ 50 | this.setState({ 51 | showPreview: !this.state.showImage, 52 | showLoader: false 53 | }) 54 | }) 55 | } 56 | 57 | componentWillReceiveProps({src,preview},{oldSrc}){ 58 | if(src !== oldSrc){ 59 | this.resetState(); 60 | this.loadOriginalImage(); 61 | this.loadPreviewImage(); 62 | } 63 | } 64 | 65 | componentWillMount(){ 66 | this.resetState(); 67 | this.loadOriginalImage(); 68 | this.loadPreviewImage(); 69 | } 70 | 71 | componentWillUnmount(){ 72 | this.image.src = ''; 73 | } 74 | 75 | renderLoader(){ 76 | if(React.isValidElement(this.props.loader)){ 77 | return this.props.loader; 78 | }else{ 79 | return 80 | } 81 | } 82 | 83 | render(){ 84 | return
85 | { this.state.showImage ? 89 | : null } 90 | { this.state.showPreview ? 93 | : null } 94 | { this.state.showLoader ? this.renderLoader() : null } 95 |
96 | } 97 | } 98 | 99 | BlurImageLoader.propTypes = { 100 | src: React.PropTypes.string.isRequired, 101 | preview: React.PropTypes.string, 102 | fullCover: React.PropTypes.bool, 103 | maxBlurLevel: React.PropTypes.number, 104 | transitionTime: React.PropTypes.number, 105 | loader: React.PropTypes.element 106 | } 107 | 108 | export default BlurImageLoader; -------------------------------------------------------------------------------- /src/DefaultLoader.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | class DefaultLoader extends React.Component{ 4 | render(){ 5 | return
6 | } 7 | } 8 | 9 | export default DefaultLoader; -------------------------------------------------------------------------------- /src/ImageContainer.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | const DEFAULT_BLUR_LEVEL = 10; 4 | const DEFAULT_STEP_TIME = 10; 5 | const DEFAULT_TRANSITION_DELAY = 300; 6 | 7 | class ImageContainer extends React.Component{ 8 | 9 | constructor(props){ 10 | super(); 11 | this.state = { 12 | blurLevel: props.maxBlurLevel || DEFAULT_BLUR_LEVEL, 13 | blurImageOpacity: 1 14 | } 15 | } 16 | 17 | componentDidMount(){ 18 | if(this.props.transitionTime){ 19 | let stepTime = DEFAULT_STEP_TIME; 20 | let remainTime = this.props.transitionTime; 21 | this.transitionTimeout = setTimeout(()=>{ 22 | this.timer = setInterval(()=>{ 23 | if(remainTime <= 0){ 24 | window.clearInterval(this.timer); 25 | }else{ 26 | let percent = Math.max(0, ((remainTime - stepTime) / remainTime)); 27 | let currentBlurLevel = this.state.blurImageOpacity * percent; 28 | remainTime = remainTime - stepTime; 29 | this.setState({ 30 | blurImageOpacity: currentBlurLevel 31 | }) 32 | } 33 | }, stepTime) 34 | }, DEFAULT_TRANSITION_DELAY) 35 | } 36 | } 37 | 38 | componentWillUnmount(){ 39 | clearTimeout(this.transitionTimeout); 40 | clearInterval(this.timer); 41 | } 42 | 43 | render(){ 44 | return 45 | 46 | 47 | 48 | 49 | 56 | 57 | { this.props.transitionTime ? 63 | : null} 64 | 65 | } 66 | } 67 | 68 | ImageContainer.propTypes = { 69 | src: React.PropTypes.string.isRequired, 70 | preview: React.PropTypes.string, 71 | fullCover: React.PropTypes.bool, 72 | maxBlurLevel: React.PropTypes.number, 73 | transitionTime: React.PropTypes.number 74 | } 75 | 76 | export default ImageContainer; -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | .bil-image{ 2 | display: -ms-flexbox; 3 | display: -webkit-flex; 4 | display: flex; 5 | 6 | -ms-flex-align: center; 7 | -webkit-align-items: center; 8 | -webkit-box-align: center; 9 | 10 | align-items: center; 11 | justify-content: center; 12 | 13 | height: 100%; 14 | width: 100%; 15 | } 16 | 17 | .bil-default-loader{ 18 | border: 2px solid #f3f3f3; 19 | border-top: 2px solid #555; 20 | border-radius: 50%; 21 | width: 30px; 22 | height: 30px; 23 | animation: bil-default-spin 1s linear infinite; 24 | } 25 | 26 | @keyframes bil-default-spin { 27 | 0% { transform: rotate(0deg); } 28 | 100% { transform: rotate(360deg); } 29 | } -------------------------------------------------------------------------------- /webpack.build.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var path = require('path'); 3 | var nodeExternals = require('webpack-node-externals'); 4 | 5 | module.exports = { 6 | entry: './index.jsx', 7 | target: 'node', 8 | output: { 9 | path: path.resolve(__dirname, './build'), 10 | filename: 'reactBlurImageLoader.js', 11 | library: 'reactBlurImageLoader', 12 | libraryTarget: 'umd', 13 | umdNamedDefine: true 14 | }, 15 | resolve: { 16 | extensions: ['.js', '.jsx', '.css'] 17 | }, 18 | externals: [nodeExternals()], 19 | module: { 20 | loaders: [ 21 | { test: /\.jsx?$/, loader: 'babel-loader'}, 22 | { test: /\.css$/, loader: 'style-loader!css-loader'} 23 | ] 24 | }, 25 | plugins: [ 26 | new webpack.optimize.UglifyJsPlugin({ 27 | mangle: false 28 | }) 29 | ] 30 | } --------------------------------------------------------------------------------