From 4db8c79549732cdbbac59075a510591b997e23cd Mon Sep 17 00:00:00 2001 From: Michael Romanenko Date: Sun, 13 Dec 2015 20:56:33 +0600 Subject: [PATCH 1/4] =?UTF-8?q?Use=20babel=20from=20`node=5Fmodules`=20?= =?UTF-8?q?=E2=80=94=20needed=20to=20make=20tests=20work?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + Makefile | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 8f028e4..ed9021e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /node_modules/ /lib/ +npm-debug.log diff --git a/Makefile b/Makefile index 346daae..1578e19 100644 --- a/Makefile +++ b/Makefile @@ -1,16 +1,16 @@ node: - babel --stage=0 src --out-dir lib + ./node_modules/.bin/babel --stage=0 src --out-dir lib browser: - babel-node ./node_modules/.bin/webpack --config ./webpack.config.js + ./node_modules/.bin/babel-node ./node_modules/.bin/webpack --config ./webpack.config.js build: node browser test: - babel-node ./test/server.js + ./node_modules/.bin/babel-node ./test/server.js dev: - babel-node ./test/server.js --open + ./node_modules/.bin/babel-node ./test/server.js --open major: mversion major From eafcea118a0939f1a44f3c44fb50628982ed7149 Mon Sep 17 00:00:00 2001 From: Michael Romanenko Date: Sun, 13 Dec 2015 20:57:51 +0600 Subject: [PATCH 2/4] XMLHttpRequest Level 2 spec way to track progress --- src/index.js | 32 +++++++++++++++++++++++++++++--- standalone/react-imageloader.js | 2 +- test/tests.js | 5 +++++ 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/index.js b/src/index.js index 2d67bd5..69ded4f 100644 --- a/src/index.js +++ b/src/index.js @@ -29,7 +29,10 @@ export default class ImageLoader extends React.Component { constructor(props) { super(props); - this.state = {status: props.src ? Status.LOADING : Status.PENDING}; + this.state = { + status: props.src ? Status.LOADING : Status.PENDING, + progress: 0, + }; } componentDidMount() { @@ -67,6 +70,9 @@ export default class ImageLoader extends React.Component { this.img = new Image(); this.img.onload = ::this.handleLoad; + this.img.onloadstart = ::this.handleProgressStart; + this.img.onprogress = ::this.handleProgress; + this.img.onloadend = ::this.handleProgressEnd; this.img.onerror = ::this.handleError; this.img.src = this.props.src; } @@ -74,6 +80,9 @@ export default class ImageLoader extends React.Component { destroyLoader() { if (this.img) { this.img.onload = null; + this.img.onloadstart = null; + this.img.onprogress = null; + this.img.onloadend = null; this.img.onerror = null; this.img = null; } @@ -81,11 +90,28 @@ export default class ImageLoader extends React.Component { handleLoad(event) { this.destroyLoader(); - this.setState({status: Status.LOADED}); + this.setState({status: Status.LOADED, progress: 1}); if (this.props.onLoad) this.props.onLoad(event); } + handleProgress(event) { + if (event.lengthComputable) { + return; + } + const progress = (event.loaded / event.total).toFixed(2); + + this.setState({progress}); + } + + handleProgressStart() { + this.setState({progress: 0}); + } + + handleProgressEnd() { + this.setState({progress: 1}); + } + handleError(error) { this.destroyLoader(); this.setState({status: Status.FAILED}); @@ -127,7 +153,7 @@ export default class ImageLoader extends React.Component { break; default: - if (this.props.preloader) wrapperArgs.push(this.props.preloader()); + if (this.props.preloader) wrapperArgs.push(this.props.preloader(this.state.progress)); break; } diff --git a/standalone/react-imageloader.js b/standalone/react-imageloader.js index e3886c3..97211a9 100644 --- a/standalone/react-imageloader.js +++ b/standalone/react-imageloader.js @@ -1 +1 @@ -!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("React")):"function"==typeof define&&define.amd?define(["React"],t):"object"==typeof exports?exports.ReactImageLoader=t(require("React")):e.ReactImageLoader=t(e.React)}(this,function(e){return function(e){function t(o){if(r[o])return r[o].exports;var s=r[o]={exports:{},id:o,loaded:!1};return e[o].call(s.exports,s,s.exports,t),s.loaded=!0,s.exports}var r={};return t.m=e,t.c=r,t.p="",t(0)}([function(e,t,r){"use strict";function o(e){return e&&e.__esModule?e:{"default":e}}function s(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function n(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(t,"__esModule",{value:!0});var a=function(){function e(e,t){for(var r=0;r=0||(r[o]=this.props[o]));return p["default"].createElement("img",r)}},{key:"render",value:function(){var e,t={className:this.getClassName()};this.props.style&&(t.style=this.props.style);var r=[t];switch(this.state.status){case d.LOADED:r.push(this.renderImg());break;case d.FAILED:this.props.children&&r.push(this.props.children);break;default:this.props.preloader&&r.push(this.props.preloader())}return(e=this.props).wrapper.apply(e,r)}}]),t}(p["default"].Component);t["default"]=f,e.exports=t["default"]},function(t,r){t.exports=e}])}); \ No newline at end of file +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("React")):"function"==typeof define&&define.amd?define(["React"],t):"object"==typeof exports?exports.ReactImageLoader=t(require("React")):e.ReactImageLoader=t(e.React)}(this,function(e){return function(e){function t(o){if(r[o])return r[o].exports;var s=r[o]={exports:{},id:o,loaded:!1};return e[o].call(s.exports,s,s.exports,t),s.loaded=!0,s.exports}var r={};return t.m=e,t.c=r,t.p="",t(0)}([function(e,t,r){"use strict";function o(e){return e&&e.__esModule?e:{"default":e}}function s(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function n(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(t,"__esModule",{value:!0});var a=function(){function e(e,t){for(var r=0;r { assert(TestUtils.findRenderedDOMComponentWithTag(loader, 'div')); }); + it('calls progress function with progress value as first argument if provided', async function() { + const preloader = (progress) => { assert.equal(progress, 0, 'Expected progress value as an argument'); }; + await loadImage({src: nocache('tiger.svg'), preloader}); + }); + it('removes a preloader when load completes', async function() { const loader = await loadImage({src: nocache('tiger.svg'), preloader: React.DOM.div}); assert.throws(() => { TestUtils.findRenderedDOMComponentWithTag(loader, 'div'); }); From 2f237cc8434f93d201367ea2d5fb4735e5a9f56c Mon Sep 17 00:00:00 2001 From: Michael Romanenko Date: Mon, 14 Dec 2015 02:03:43 +0600 Subject: [PATCH 3/4] Fix wrong case for `react` CommonJS module name --- webpack.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webpack.config.js b/webpack.config.js index dabe02c..bf3c53b 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -10,7 +10,7 @@ export default { libraryTarget: 'umd', target: 'web', }, - externals: ['React', {react: 'React'}], + externals: ['react', {react: 'React'}], module: { loaders: [ {test: /\.js$/, exclude: /node_modules/, loader: 'babel?stage=0'}, From 9deb63332fefba93f75ef706d35133f65e9dd0cb Mon Sep 17 00:00:00 2001 From: Michael Romanenko Date: Mon, 14 Dec 2015 02:04:20 +0600 Subject: [PATCH 4/4] XMLHttpRequest() approach to load image and receive progress events --- src/index.js | 54 +++++++++++++++++++-------------- standalone/react-imageloader.js | 2 +- test/tests.js | 2 +- 3 files changed, 34 insertions(+), 24 deletions(-) diff --git a/src/index.js b/src/index.js index 69ded4f..2932ed3 100644 --- a/src/index.js +++ b/src/index.js @@ -31,6 +31,7 @@ export default class ImageLoader extends React.Component { super(props); this.state = { status: props.src ? Status.LOADING : Status.PENDING, + imageSrc: props.src, progress: 0, }; } @@ -50,7 +51,7 @@ export default class ImageLoader extends React.Component { } componentDidUpdate() { - if (this.state.status === Status.LOADING && !this.img) { + if (this.state.status === Status.LOADING && !this.request) { this.createLoader(); } } @@ -68,38 +69,46 @@ export default class ImageLoader extends React.Component { createLoader() { this.destroyLoader(); // We can only have one loader at a time. - this.img = new Image(); - this.img.onload = ::this.handleLoad; - this.img.onloadstart = ::this.handleProgressStart; - this.img.onprogress = ::this.handleProgress; - this.img.onloadend = ::this.handleProgressEnd; - this.img.onerror = ::this.handleError; - this.img.src = this.props.src; + this.request = new XMLHttpRequest(); + this.request.onloadstart = ::this.handleProgressStart; + this.request.onprogress = ::this.handleProgress; + this.request.onloadend = ::this.handleProgressEnd; + this.request.onload = ::this.handleLoad; + this.request.open('GET', this.props.src, true); + this.request.withCredentials = true; + this.request.responseType = 'blob'; + this.request.send(null); } destroyLoader() { - if (this.img) { - this.img.onload = null; - this.img.onloadstart = null; - this.img.onprogress = null; - this.img.onloadend = null; - this.img.onerror = null; - this.img = null; + if (this.request) { + this.request.onloadstart = null; + this.request.onprogress = null; + this.request.onloadend = null; + this.request.onload = null; + this.request = null; } } handleLoad(event) { - this.destroyLoader(); - this.setState({status: Status.LOADED, progress: 1}); + const response = event.target; + const imageSrc = typeof window !== 'undefined' ? + window.URL.createObjectURL(this.request.response) : this.props.src; + + if (response.readyState !== 4 || response.status < 200 || response.status > 300) { + return this.handleError(response); + } + this.setState({status: Status.LOADED, progress: 1, imageSrc}); if (this.props.onLoad) this.props.onLoad(event); + this.destroyLoader(); } - handleProgress(event) { - if (event.lengthComputable) { + handleProgress({lengthComputable, loaded, total}) { + if (lengthComputable) { return; } - const progress = (event.loaded / event.total).toFixed(2); + const progress = (loaded / total).toFixed(2); this.setState({progress}); } @@ -120,8 +129,9 @@ export default class ImageLoader extends React.Component { } renderImg() { - const {src, imgProps} = this.props; - let props = {src}; + const {imgProps} = this.props; + const {imageSrc} = this.state; + let props = {src: imageSrc}; for (let k in imgProps) { if (imgProps.hasOwnProperty(k)) { diff --git a/standalone/react-imageloader.js b/standalone/react-imageloader.js index 97211a9..1db4024 100644 --- a/standalone/react-imageloader.js +++ b/standalone/react-imageloader.js @@ -1 +1 @@ -!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("React")):"function"==typeof define&&define.amd?define(["React"],t):"object"==typeof exports?exports.ReactImageLoader=t(require("React")):e.ReactImageLoader=t(e.React)}(this,function(e){return function(e){function t(o){if(r[o])return r[o].exports;var s=r[o]={exports:{},id:o,loaded:!1};return e[o].call(s.exports,s,s.exports,t),s.loaded=!0,s.exports}var r={};return t.m=e,t.c=r,t.p="",t(0)}([function(e,t,r){"use strict";function o(e){return e&&e.__esModule?e:{"default":e}}function s(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function n(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(t,"__esModule",{value:!0});var a=function(){function e(e,t){for(var r=0;r300?this.handleError(t):(this.setState({status:d.LOADED,progress:1,imageSrc:r}),this.props.onLoad&&this.props.onLoad(e),void this.destroyLoader())}},{key:"handleProgress",value:function(e){var t=e.lengthComputable,r=e.loaded,s=e.total;if(!t){var o=(r/s).toFixed(2);this.setState({progress:o})}}},{key:"handleProgressStart",value:function(){this.setState({progress:0})}},{key:"handleProgressEnd",value:function(){this.setState({progress:1})}},{key:"handleError",value:function(e){this.destroyLoader(),this.setState({status:d.FAILED}),this.props.onError&&this.props.onError(e)}},{key:"renderImg",value:function(){var e=this.props.imgProps,t=this.state.imageSrc,r={src:t};for(var s in e)e.hasOwnProperty(s)&&(r[s]=e[s]);return l["default"].createElement("img",r)}},{key:"render",value:function(){var e,t={className:this.getClassName()};this.props.style&&(t.style=this.props.style);var r=[t];switch(this.state.status){case d.LOADED:r.push(this.renderImg());break;case d.FAILED:this.props.children&&r.push(this.props.children);break;default:this.props.preloader&&r.push(this.props.preloader(this.state.progress))}return(e=this.props).wrapper.apply(e,r)}}]),t}(l["default"].Component);t["default"]=h,e.exports=t["default"]},function(t,r){t.exports=e}])}); \ No newline at end of file diff --git a/test/tests.js b/test/tests.js index e64631c..81d8aa7 100644 --- a/test/tests.js +++ b/test/tests.js @@ -147,7 +147,7 @@ describe('ReactImageLoader', () => { />, domEl); // Make sure that the image load isn't handled by ImageLoader. - loader.img.addEventListener('load', () => { + loader.request.addEventListener('load', () => { assert.throws(() => TestUtils.findRenderedDOMComponentWithTag(loader, 'img')); done(); });