From d9196e8cd860efb5a989377b54bca7326efd6984 Mon Sep 17 00:00:00 2001 From: Nick Stakenburg Date: Sun, 7 Jun 2015 00:13:01 +0200 Subject: [PATCH] 1.3.0: Added a check ensuring render after onload --- bower.json | 2 +- package.json | 2 +- src/imageready.js | 250 ++++++++++++++++++++++++++++++++------------- voila.pkgd.js | 252 +++++++++++++++++++++++++++++++++------------- voila.pkgd.min.js | 3 +- 5 files changed, 367 insertions(+), 142 deletions(-) diff --git a/bower.json b/bower.json index c275c24..1f3e4fb 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "voila", - "version": "1.2.1", + "version": "1.3.0", "description": "A jQuery plugin that provides callbacks for images, letting you know when they've loaded.", "keywords": [ "image", diff --git a/package.json b/package.json index 3120e33..1395944 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "voila", "title": "Voilà", - "version": "1.2.1", + "version": "1.3.0", "description": "A jQuery plugin that provides callbacks for images, letting you know when they've loaded.", "keywords": [ "image", diff --git a/src/imageready.js b/src/imageready.js index 30a7830..05a619b 100644 --- a/src/imageready.js +++ b/src/imageready.js @@ -2,10 +2,111 @@ * http://voila.nickstakenburg.com * MIT License */ -var ImageReady = function() { +var ImageReady = (function($) { + +var Poll = function() { return this.initialize.apply(this, Array.prototype.slice.call(arguments)); }; +$.extend(Poll.prototype, { + initialize: function() { + this.options = $.extend({ + test: function() {}, + success: function() {}, + timeout: function() {}, + callAt: false, + intervals: [ + [0, 0], + [1 * 1000, 10], + [2 * 1000, 50], + [4 * 1000, 100], + [20 * 1000, 500] + ] + }, arguments[0] || {}); + + this._test = this.options.test; + this._success = this.options.success; + this._timeout = this.options.timeout; + + this._ipos = 0; + this._time = 0; + this._delay = this.options.intervals[this._ipos][1]; + this._callTimeouts = []; + + this.poll(); + this._createCallsAt(); + }, + + poll: function() { + this._polling = setTimeout($.proxy(function() { + if (this._test()) { + this.success(); + return; + } + + // update time + this._time += this._delay; + + // next i within the interval + if (this._time >= this.options.intervals[this._ipos][0]) { + // timeout when no next interval + if (!this.options.intervals[this._ipos + 1]) { + if ($.type(this._timeout) == 'function') { + this._timeout(); + } + return; + } + + this._ipos++; + + // update to the new bracket + this._delay = this.options.intervals[this._ipos][1]; + } + + this.poll(); + }, this), this._delay); + }, + + success: function() { + this.abort(); + this._success(); + }, + + _createCallsAt: function() { + if (!this.options.callAt) return; + + // start a timer for each call + $.each(this.options.callAt, $.proxy(function(i, at) { + var time = at[0], fn = at[1]; + var timeout = setTimeout($.proxy(function() { + fn(); + }, this), time); + + this._callTimeouts.push(timeout); + }, this)); + }, + + _stopCallTimeouts: function() { + $.each(this._callTimeouts, function(i, timeout) { + clearTimeout(timeout); + }); + this._callTimeouts = []; + }, + + abort: function() { + this._stopCallTimeouts(); + + if (this._polling) { + clearTimeout(this._polling); + this._polling = null; + } + } +}); + + +var ImageReady = function() { + return this.initialize.apply(this, Array.prototype.slice.call(arguments)); +}; $.extend(ImageReady.prototype, { supports: { naturalWidth: (function() { @@ -31,20 +132,6 @@ $.extend(ImageReady.prototype, { return; } - this.intervals = [ - [1 * 1000, 10], - [2 * 1000, 50], - [4 * 1000, 100], - [20 * 1000, 500] - ]; - - // for testing, 2sec delay - //this.intervals = [[20 * 1000, 2000]]; - - this._ipos = 0; - this._time = 0; - this._delay = this.intervals[this._ipos][1]; - // start polling this.poll(); }, @@ -55,60 +142,50 @@ $.extend(ImageReady.prototype, { // was implemented). The spec even seems to be against // this, making polling unreliable in those cases. poll: function() { - this._polling = setTimeout($.proxy(function() { - if (this.img.naturalWidth > 0) { - this.success(); - return; - } - - // update time spend - this._time += this._delay; - - // use load() after waiting as a fallback - if (this.options.pollFallbackAfter && - this._time >= this.options.pollFallbackAfter && - !this._usedPollFallback) { - this._usedPollFallback = true; - this.load(); - } - - // next i within the interval - if (this._time > this.intervals[this._ipos][0]) { - // if there's no next interval, we assume - // the image errored out - if (!this.intervals[this._ipos + 1]) { - this.error(); - return; - } + this._poll = new Poll({ + test: $.proxy(function() { + return this.img.naturalWidth > 0; + }, this), - this._ipos++; - - // update to the new bracket - this._delay = this.intervals[this._ipos][1]; - } - - this.poll(); - }, this), this._delay); + success: $.proxy(function() { + this.success(); + }, this), + + timeout: $.proxy(function() { + // error on timeout + this.error(); + }, this), + + callAt: [ + [this.options.pollFallbackAfter, $.proxy(function() { + this.load(); + }, this)] + ] + }); }, load: function() { - var image = new Image(); - this._onloadImage = image; - - image.onload = $.proxy(function() { - image.onload = function() {}; - - if (!this.supports.naturalWidth) { - this.img.naturalWidth = image.width; - this.img.naturalHeight = image.height; - } + this._loading = setTimeout($.proxy(function() { + var image = new Image(); + this._onloadImage = image; + + image.onload = $.proxy(function() { + image.onload = function() {}; + + if (!this.supports.naturalWidth) { + this.img.naturalWidth = image.width; + this.img.naturalHeight = image.height; + image.naturalWidth = image.width; + image.naturalHeight = image.height; + } - this.success(); - }, this); + this.success(); + }, this); - image.onerror = $.proxy(this.error, this); + image.onerror = $.proxy(this.error, this); - image.src = this.img.src; + image.src = this.img.src; + }, this)); }, success: function() { @@ -120,7 +197,7 @@ $.extend(ImageReady.prototype, { this.abort(); // some time to allow layout updates, IE requires this! - this._successRenderTimeout = setTimeout($.proxy(function() { + this.waitForRender($.proxy(function() { this.isLoaded = true; this.successCallback(this); }, this)); @@ -134,6 +211,8 @@ $.extend(ImageReady.prototype, { // stop loading/polling this.abort(); + // don't wait for an actual render on error, just timeout + // to give the browser some time to render a broken image icon this._errorRenderTimeout = setTimeout($.proxy(function() { if (this.errorCallback) this.errorCallback(this); }, this)); @@ -146,23 +225,53 @@ $.extend(ImageReady.prototype, { }, stopPolling: function() { - if (this._polling) { - clearTimeout(this._polling); - this._polling = null; + if (this._poll) { + this._poll.abort(); + this._poll = null; } }, stopLoading: function() { + if (this._loading) { + clearTimeout(this._loading); + this._loading = null; + } + if (this._onloadImage) { this._onloadImage.onload = function() { }; this._onloadImage.onerror = function() { }; } }, + // used by success() only + waitForRender: function(callback) { + if (this._renderPoll) this._renderPoll.abort(); + + this._renderPoll = new Poll({ + test: $.proxy(function() { + // when using onload, the detached image node shoud have + // the same dimensions as the in the DOM to guarantee + // a complete render. + // IE 11 can have situations where the detached image is loaded + // but rendering hasn't completed on the with the same src, + // this guards against that. + if (this.options.method == 'onload') { + return this.img.naturalWidth == this._onloadImage.naturalWidth + && this.img.naturalHeight == this._onloadImage.naturalHeight; + } else { + // otherwise we used naturalWidth an might not have a detached + // image that rendered successfully. + return true; + } + }, this), + success: callback + }); + }, + stopWaitingForRender: function() { - if (this._successRenderTimeout) { - clearTimeout(this._successRenderTimeout); - this._successRenderTimeout = null; + if (this._renderPoll) { + this._renderPoll.abort(); + this._renderPoll = null; } if (this._errorRenderTimeout) { @@ -171,3 +280,6 @@ $.extend(ImageReady.prototype, { } } }); + +return ImageReady; +})(jQuery); diff --git a/voila.pkgd.js b/voila.pkgd.js index 2ccf740..606eb97 100644 --- a/voila.pkgd.js +++ b/voila.pkgd.js @@ -1,5 +1,5 @@ /*! - * Voilà - v1.2.1 + * Voilà - v1.3.0 * (c) 2015 Nick Stakenburg * * http://voila.nickstakenburg.com @@ -147,10 +147,111 @@ $.fn.voila = function() { * http://voila.nickstakenburg.com * MIT License */ -var ImageReady = function() { +var ImageReady = (function($) { + +var Poll = function() { return this.initialize.apply(this, Array.prototype.slice.call(arguments)); }; +$.extend(Poll.prototype, { + initialize: function() { + this.options = $.extend({ + test: function() {}, + success: function() {}, + timeout: function() {}, + callAt: false, + intervals: [ + [0, 0], + [1 * 1000, 10], + [2 * 1000, 50], + [4 * 1000, 100], + [20 * 1000, 500] + ] + }, arguments[0] || {}); + + this._test = this.options.test; + this._success = this.options.success; + this._timeout = this.options.timeout; + + this._ipos = 0; + this._time = 0; + this._delay = this.options.intervals[this._ipos][1]; + this._callTimeouts = []; + + this.poll(); + this._createCallsAt(); + }, + + poll: function() { + this._polling = setTimeout($.proxy(function() { + if (this._test()) { + this.success(); + return; + } + + // update time + this._time += this._delay; + + // next i within the interval + if (this._time >= this.options.intervals[this._ipos][0]) { + // timeout when no next interval + if (!this.options.intervals[this._ipos + 1]) { + if ($.type(this._timeout) == 'function') { + this._timeout(); + } + return; + } + + this._ipos++; + + // update to the new bracket + this._delay = this.options.intervals[this._ipos][1]; + } + + this.poll(); + }, this), this._delay); + }, + + success: function() { + this.abort(); + this._success(); + }, + + _createCallsAt: function() { + if (!this.options.callAt) return; + + // start a timer for each call + $.each(this.options.callAt, $.proxy(function(i, at) { + var time = at[0], fn = at[1]; + + var timeout = setTimeout($.proxy(function() { + fn(); + }, this), time); + + this._callTimeouts.push(timeout); + }, this)); + }, + + _stopCallTimeouts: function() { + $.each(this._callTimeouts, function(i, timeout) { + clearTimeout(timeout); + }); + this._callTimeouts = []; + }, + + abort: function() { + this._stopCallTimeouts(); + + if (this._polling) { + clearTimeout(this._polling); + this._polling = null; + } + } +}); + +var ImageReady = function() { + return this.initialize.apply(this, Array.prototype.slice.call(arguments)); +}; $.extend(ImageReady.prototype, { supports: { naturalWidth: (function() { @@ -176,20 +277,6 @@ $.extend(ImageReady.prototype, { return; } - this.intervals = [ - [1 * 1000, 10], - [2 * 1000, 50], - [4 * 1000, 100], - [20 * 1000, 500] - ]; - - // for testing, 2sec delay - //this.intervals = [[20 * 1000, 2000]]; - - this._ipos = 0; - this._time = 0; - this._delay = this.intervals[this._ipos][1]; - // start polling this.poll(); }, @@ -200,60 +287,50 @@ $.extend(ImageReady.prototype, { // was implemented). The spec even seems to be against // this, making polling unreliable in those cases. poll: function() { - this._polling = setTimeout($.proxy(function() { - if (this.img.naturalWidth > 0) { - this.success(); - return; - } - - // update time spend - this._time += this._delay; - - // use load() after waiting as a fallback - if (this.options.pollFallbackAfter && - this._time >= this.options.pollFallbackAfter && - !this._usedPollFallback) { - this._usedPollFallback = true; - this.load(); - } - - // next i within the interval - if (this._time > this.intervals[this._ipos][0]) { - // if there's no next interval, we assume - // the image errored out - if (!this.intervals[this._ipos + 1]) { - this.error(); - return; - } + this._poll = new Poll({ + test: $.proxy(function() { + return this.img.naturalWidth > 0; + }, this), - this._ipos++; - - // update to the new bracket - this._delay = this.intervals[this._ipos][1]; - } - - this.poll(); - }, this), this._delay); + success: $.proxy(function() { + this.success(); + }, this), + + timeout: $.proxy(function() { + // error on timeout + this.error(); + }, this), + + callAt: [ + [this.options.pollFallbackAfter, $.proxy(function() { + this.load(); + }, this)] + ] + }); }, load: function() { - var image = new Image(); - this._onloadImage = image; - - image.onload = $.proxy(function() { - image.onload = function() {}; - - if (!this.supports.naturalWidth) { - this.img.naturalWidth = image.width; - this.img.naturalHeight = image.height; - } + this._loading = setTimeout($.proxy(function() { + var image = new Image(); + this._onloadImage = image; + + image.onload = $.proxy(function() { + image.onload = function() {}; + + if (!this.supports.naturalWidth) { + this.img.naturalWidth = image.width; + this.img.naturalHeight = image.height; + image.naturalWidth = image.width; + image.naturalHeight = image.height; + } - this.success(); - }, this); + this.success(); + }, this); - image.onerror = $.proxy(this.error, this); + image.onerror = $.proxy(this.error, this); - image.src = this.img.src; + image.src = this.img.src; + }, this)); }, success: function() { @@ -265,7 +342,7 @@ $.extend(ImageReady.prototype, { this.abort(); // some time to allow layout updates, IE requires this! - this._successRenderTimeout = setTimeout($.proxy(function() { + this.waitForRender($.proxy(function() { this.isLoaded = true; this.successCallback(this); }, this)); @@ -279,6 +356,8 @@ $.extend(ImageReady.prototype, { // stop loading/polling this.abort(); + // don't wait for an actual render on error, just timeout + // to give the browser some time to render a broken image icon this._errorRenderTimeout = setTimeout($.proxy(function() { if (this.errorCallback) this.errorCallback(this); }, this)); @@ -291,23 +370,53 @@ $.extend(ImageReady.prototype, { }, stopPolling: function() { - if (this._polling) { - clearTimeout(this._polling); - this._polling = null; + if (this._poll) { + this._poll.abort(); + this._poll = null; } }, stopLoading: function() { + if (this._loading) { + clearTimeout(this._loading); + this._loading = null; + } + if (this._onloadImage) { this._onloadImage.onload = function() { }; this._onloadImage.onerror = function() { }; } }, + // used by success() only + waitForRender: function(callback) { + if (this._renderPoll) this._renderPoll.abort(); + + this._renderPoll = new Poll({ + test: $.proxy(function() { + // when using onload, the detached image node shoud have + // the same dimensions as the in the DOM to guarantee + // a complete render. + // IE 11 can have situations where the detached image is loaded + // but rendering hasn't completed on the with the same src, + // this guards against that. + if (this.options.method == 'onload') { + return this.img.naturalWidth == this._onloadImage.naturalWidth + && this.img.naturalHeight == this._onloadImage.naturalHeight; + } else { + // otherwise we used naturalWidth an might not have a detached + // image that rendered successfully. + return true; + } + }, this), + success: callback + }); + }, + stopWaitingForRender: function() { - if (this._successRenderTimeout) { - clearTimeout(this._successRenderTimeout); - this._successRenderTimeout = null; + if (this._renderPoll) { + this._renderPoll.abort(); + this._renderPoll = null; } if (this._errorRenderTimeout) { @@ -317,6 +426,9 @@ $.extend(ImageReady.prototype, { } }); +return ImageReady; +})(jQuery); + return Voila; })); diff --git a/voila.pkgd.min.js b/voila.pkgd.min.js index 9a754a2..ac2a387 100644 --- a/voila.pkgd.min.js +++ b/voila.pkgd.min.js @@ -1,8 +1,9 @@ /*! - * Voilà - v1.2.1 + * Voilà - v1.3.0 * (c) 2015 Nick Stakenburg * * http://voila.nickstakenburg.com * * MIT License */ +!function(a){"function"==typeof define&&define.amd?define(["jquery"],a):jQuery&&!window.Voila&&(window.Voila=a(jQuery))}(function(a){function b(c,d,e){if(!(this instanceof b))return new b(c,d,e);var f=a.type(arguments[1]),g="object"===f?arguments[1]:{},h="function"===f?arguments[1]:"function"===a.type(arguments[2])?arguments[2]:!1;return this.options=a.extend({method:"onload"},g),this.deferred=new jQuery.Deferred,h&&this.always(h),this._processed=0,this.images=[],this._add(c),this}a.extend(b.prototype,{_add:function(b){var d="string"==a.type(b)?a(b):b instanceof jQuery||b.length>0?b:[b];a.each(d,a.proxy(function(b,d){var e=a(),f=a(d);e=e.add(f.is("img")?f:f.find("img")),e.each(a.proxy(function(b,d){this.images.push(new c(d,a.proxy(function(a){this._progress(a)},this),a.proxy(function(a){this._progress(a)},this),this.options))},this))},this)),this.images.length<1&&setTimeout(a.proxy(function(){this._resolve()},this))},abort:function(){this._progress=this._notify=this._reject=this._resolve=function(){},a.each(this.images,function(a,b){b.abort()}),this.images=[]},_progress:function(a){this._processed++,a.isLoaded||(this._broken=!0),this._notify(a),this._processed==this.images.length&&this[this._broken?"_reject":"_resolve"]()},_notify:function(a){this.deferred.notify(this,a)},_reject:function(){this.deferred.reject(this)},_resolve:function(){this.deferred.resolve(this)},always:function(a){return this.deferred.always(a),this},done:function(a){return this.deferred.done(a),this},fail:function(a){return this.deferred.fail(a),this},progress:function(a){return this.deferred.progress(a),this}}),a.fn.voila=function(){return b.apply(b,[this].concat(Array.prototype.slice.call(arguments)))};var c=function(a){var b=function(){return this.initialize.apply(this,Array.prototype.slice.call(arguments))};a.extend(b.prototype,{initialize:function(){this.options=a.extend({test:function(){},success:function(){},timeout:function(){},callAt:!1,intervals:[[0,0],[1e3,10],[2e3,50],[4e3,100],[2e4,500]]},arguments[0]||{}),this._test=this.options.test,this._success=this.options.success,this._timeout=this.options.timeout,this._ipos=0,this._time=0,this._delay=this.options.intervals[this._ipos][1],this._callTimeouts=[],this.poll(),this._createCallsAt()},poll:function(){this._polling=setTimeout(a.proxy(function(){if(this._test())return void this.success();if(this._time+=this._delay,this._time>=this.options.intervals[this._ipos][0]){if(!this.options.intervals[this._ipos+1])return void("function"==a.type(this._timeout)&&this._timeout());this._ipos++,this._delay=this.options.intervals[this._ipos][1]}this.poll()},this),this._delay)},success:function(){this.abort(),this._success()},_createCallsAt:function(){this.options.callAt&&a.each(this.options.callAt,a.proxy(function(b,c){var d=c[0],e=c[1],f=setTimeout(a.proxy(function(){e()},this),d);this._callTimeouts.push(f)},this))},_stopCallTimeouts:function(){a.each(this._callTimeouts,function(a,b){clearTimeout(b)}),this._callTimeouts=[]},abort:function(){this._stopCallTimeouts(),this._polling&&(clearTimeout(this._polling),this._polling=null)}});var c=function(){return this.initialize.apply(this,Array.prototype.slice.call(arguments))};return a.extend(c.prototype,{supports:{naturalWidth:function(){return"naturalWidth"in new Image}()},initialize:function(b,c,d){return this.img=a(b)[0],this.successCallback=c,this.errorCallback=d,this.isLoaded=!1,this.options=a.extend({method:"onload",pollFallbackAfter:1e3},arguments[3]||{}),"onload"!=this.options.method&&this.supports.naturalWidth?void this.poll():void this.load()},poll:function(){this._poll=new b({test:a.proxy(function(){return this.img.naturalWidth>0},this),success:a.proxy(function(){this.success()},this),timeout:a.proxy(function(){this.error()},this),callAt:[[this.options.pollFallbackAfter,a.proxy(function(){this.load()},this)]]})},load:function(){this._loading=setTimeout(a.proxy(function(){var b=new Image;this._onloadImage=b,b.onload=a.proxy(function(){b.onload=function(){},this.supports.naturalWidth||(this.img.naturalWidth=b.width,this.img.naturalHeight=b.height,b.naturalWidth=b.width,b.naturalHeight=b.height),this.success()},this),b.onerror=a.proxy(this.error,this),b.src=this.img.src},this))},success:function(){this._calledSuccess||(this._calledSuccess=!0,this.abort(),this.waitForRender(a.proxy(function(){this.isLoaded=!0,this.successCallback(this)},this)))},error:function(){this._calledError||(this._calledError=!0,this.abort(),this._errorRenderTimeout=setTimeout(a.proxy(function(){this.errorCallback&&this.errorCallback(this)},this)))},abort:function(){this.stopLoading(),this.stopPolling(),this.stopWaitingForRender()},stopPolling:function(){this._poll&&(this._poll.abort(),this._poll=null)},stopLoading:function(){this._loading&&(clearTimeout(this._loading),this._loading=null),this._onloadImage&&(this._onloadImage.onload=function(){},this._onloadImage.onerror=function(){})},waitForRender:function(c){this._renderPoll&&this._renderPoll.abort(),this._renderPoll=new b({test:a.proxy(function(){return"onload"==this.options.method?this.img.naturalWidth==this._onloadImage.naturalWidth&&this.img.naturalHeight==this._onloadImage.naturalHeight:!0},this),success:c})},stopWaitingForRender:function(){this._renderPoll&&(this._renderPoll.abort(),this._renderPoll=null),this._errorRenderTimeout&&(clearTimeout(this._errorRenderTimeout),this._errorRenderTimeout=null)}}),c}(jQuery);return b}); \ No newline at end of file