From 810339b3a9859c97ad78d4b88f7000d897c29920 Mon Sep 17 00:00:00 2001 From: Shawn Date: Thu, 23 Jun 2016 12:24:12 -0400 Subject: [PATCH 1/2] Removed requestAnimationFrame dependecy and tweaks Since we already knew in the scroll event if the element had resized, the need to check via requestAnimationFrame is not needed, so it was removed along with the dirtyCheck and the polyfill. The elements old height and width are passed through the callback as an object with the properties `width` and `height`. Other minor tweaks were made for readability and linting purposes. --- src/ResizeSensor.js | 188 +++++++++++++++++++++----------------------- 1 file changed, 89 insertions(+), 99 deletions(-) diff --git a/src/ResizeSensor.js b/src/ResizeSensor.js index 648a713..842451c 100755 --- a/src/ResizeSensor.js +++ b/src/ResizeSensor.js @@ -4,25 +4,15 @@ * https://github.com/marcj/css-element-queries/blob/master/LICENSE. */ ; -(function (root, factory) { - if (typeof define === "function" && define.amd) { +(function ( root, factory ) { + if ( typeof define === "function" && define.amd ) { define(factory); - } else if (typeof exports === "object") { + } else if ( typeof exports === "object" ) { module.exports = factory(); } else { root.ResizeSensor = factory(); } -}(this, function () { - - // Only used for the dirty checking, so the event callback count is limted to max 1 call per fps per sensor. - // In combination with the event based resize sensor this saves cpu time, because the sensor is too fast and - // would generate too many unnecessary events. - var requestAnimationFrame = window.requestAnimationFrame || - window.mozRequestAnimationFrame || - window.webkitRequestAnimationFrame || - function (fn) { - return window.setTimeout(fn, 20); - }; +}( this, function () { /** * Iterate over each of the provided element(s). @@ -30,21 +20,22 @@ * @param {HTMLElement|HTMLElement[]} elements * @param {Function} callback */ - function forEachElement(elements, callback){ - var elementsType = Object.prototype.toString.call(elements); - var isCollectionTyped = ('[object Array]' === elementsType + function forEachElement( elements, callback ){ + var elementsType = Object.prototype.toString.call( elements ), + isCollectionTyped = ( '[object Array]' === elementsType || ('[object NodeList]' === elementsType) || ('[object HTMLCollection]' === elementsType) || ('undefined' !== typeof jQuery && elements instanceof jQuery) //jquery || ('undefined' !== typeof Elements && elements instanceof Elements) //mootools - ); - var i = 0, j = elements.length; - if (isCollectionTyped) { + ), + + i = 0, j = elements.length; + if ( isCollectionTyped ) { for (; i < j; i++) { - callback(elements[i]); + callback( elements[i] ); } } else { - callback(elements); + callback( elements ); } } @@ -56,35 +47,35 @@ * * @constructor */ - var ResizeSensor = function(element, callback) { + var ResizeSensor = function( element, callback ) { /** * * @constructor */ function EventQueue() { var q = []; - this.add = function(ev) { - q.push(ev); + this.add = function( ev ) { + q.push( ev ); }; var i, j; - this.call = function() { - for (i = 0, j = q.length; i < j; i++) { - q[i].call(); + this.call = function( context, data ) { + for ( i = 0, j = q.length; i < j; i++ ) { + q[i].call( context, data ); } }; - this.remove = function(ev) { + this.remove = function( ev ) { var newQueue = []; - for(i = 0, j = q.length; i < j; i++) { - if(q[i] !== ev) newQueue.push(q[i]); + for( i = 0, j = q.length; i < j; i++ ) { + if(q[i] !== ev) newQueue.push( q[i] ); } q = newQueue; - } + }; - this.length = function() { + this.length = function () { return q.length; - } + }; } /** @@ -92,11 +83,11 @@ * @param {String} prop * @returns {String|Number} */ - function getComputedStyle(element, prop) { - if (element.currentStyle) { + function getComputedStyle ( element, prop ) { + if ( element.currentStyle ) { return element.currentStyle[prop]; - } else if (window.getComputedStyle) { - return window.getComputedStyle(element, null).getPropertyValue(prop); + } else if ( window.getComputedStyle ) { + return window.getComputedStyle( element, null ).getPropertyValue( prop ); } else { return element.style[prop]; } @@ -107,19 +98,20 @@ * @param {HTMLElement} element * @param {Function} resized */ - function attachResizeEvent(element, resized) { + function attachResizeEvent( element, resized ) { if (!element.resizedAttached) { element.resizedAttached = new EventQueue(); - element.resizedAttached.add(resized); + element.resizedAttached.add( resized ); } else if (element.resizedAttached) { - element.resizedAttached.add(resized); + element.resizedAttached.add( resized ); return; } element.resizeSensor = document.createElement('div'); element.resizeSensor.className = 'resize-sensor'; - var style = 'position: absolute; left: 0; top: 0; right: 0; bottom: 0; overflow: hidden; z-index: -1; visibility: hidden;'; - var styleChild = 'position: absolute; left: 0; top: 0; transition: 0s;'; + + var style = 'position: absolute; left: 0; top: 0; right: 0; bottom: 0; overflow: hidden; z-index: -1; visibility: hidden;', + styleChild = 'position: absolute; left: 0; top: 0; transition: 0s;'; element.resizeSensor.style.cssText = style; element.resizeSensor.innerHTML = @@ -129,56 +121,52 @@ '
' + '
' + '
'; - element.appendChild(element.resizeSensor); + element.appendChild( element.resizeSensor ); - if (getComputedStyle(element, 'position') == 'static') { + if ( getComputedStyle(element, 'position') == 'static' ) { element.style.position = 'relative'; } - var expand = element.resizeSensor.childNodes[0]; - var expandChild = expand.childNodes[0]; - var shrink = element.resizeSensor.childNodes[1]; - - var reset = function() { - expandChild.style.width = 100000 + 'px'; - expandChild.style.height = 100000 + 'px'; - - expand.scrollLeft = 100000; - expand.scrollTop = 100000; - - shrink.scrollLeft = 100000; - shrink.scrollTop = 100000; - }; + var expand = element.resizeSensor.childNodes[0], + expandChild = expand.childNodes[0], + shrink = element.resizeSensor.childNodes[1], + shrinkChild = expand.childNodes[0], + + /** + * Resets the position of the resize-detection elements + */ + reset = function() { + expandChild.style.width = '100000px'; + expandChild.style.height = '100000px'; + + expand.scrollLeft = expandChild.offsetWidth; + expand.scrollTop = expandChild.offsetheight; + + shrink.scrollLeft = shrinkChild.offsetWidth; + shrink.scrollTop = shrinkChild.offsetheight; + }; reset(); - var dirty = false; - var dirtyChecking = function() { - if (!element.resizedAttached) return; - - if (dirty) { - element.resizedAttached.call(); - dirty = false; - } - - requestAnimationFrame(dirtyChecking); - }; - - requestAnimationFrame(dirtyChecking); - var lastWidth, lastHeight; - var cachedWidth, cachedHeight; //useful to not query offsetWidth twice - - var onScroll = function() { - if ((cachedWidth = element.offsetWidth) != lastWidth || (cachedHeight = element.offsetHeight) != lastHeight) { - dirty = true; - - lastWidth = cachedWidth; - lastHeight = cachedHeight; - } - reset(); - }; - - var addEvent = function(el, name, cb) { + var lastWidth = element.offsetWidth, lastHeight = element.offsetHeight, + cachedWidth, cachedHeight, //useful to not query offsetWidth twice + + onScroll = function() { + if ( (cachedWidth = element.offsetWidth) != lastWidth || (cachedHeight = element.offsetHeight) != lastHeight ) { + if ( !!element.resizedAttached ) { + element.resizedAttached.call( element, { + width: lastWidth, + height: lastHeight + }); + } + + lastWidth = cachedWidth; + lastHeight = cachedHeight; + } + reset(); + }; + + var addEvent = function( el, name, cb ) { if (el.attachEvent) { el.attachEvent('on' + name, cb); } else { @@ -186,27 +174,29 @@ } }; - addEvent(expand, 'scroll', onScroll); - addEvent(shrink, 'scroll', onScroll); + addEvent( expand, 'scroll', onScroll ); + addEvent( shrink, 'scroll', onScroll ); } - forEachElement(element, function(elem){ - attachResizeEvent(elem, callback); + forEachElement( element, function( elem ){ + attachResizeEvent( elem, callback ); }); - this.detach = function(ev) { - ResizeSensor.detach(element, ev); + this.detach = function( ev ) { + ResizeSensor.detach( element, ev ); }; }; - ResizeSensor.detach = function(element, ev) { - forEachElement(element, function(elem){ - if(elem.resizedAttached && typeof ev == "function"){ - elem.resizedAttached.remove(ev); - if(elem.resizedAttached.length()) return; + ResizeSensor.detach = function( element, ev ) { + forEachElement( element, function( elem ){ + if( elem.resizedAttached && typeof ev == "function" ){ + elem.resizedAttached.remove( ev ); + if( elem.resizedAttached.length() ) { + return; + } } - if (elem.resizeSensor) { - elem.removeChild(elem.resizeSensor); + if ( elem.resizeSensor ) { + elem.removeChild( elem.resizeSensor ); delete elem.resizeSensor; delete elem.resizedAttached; } From 9aef2cd137aa9e5bd31c1a7fdacac3ec9f18d503 Mon Sep 17 00:00:00 2001 From: Shawn G Date: Thu, 17 Nov 2016 18:57:56 -0500 Subject: [PATCH 2/2] Fixed some issues, improved performance and added real-time events. Fixed issues: - #81 - #72 Performance: - Events are no longer added to each detector. Only one event is attached to listen to all sensors. - No longer using timeout functions. - Only one EventQueue is created to manage all ResizeSensors - Minor refactoring New Features: - ResizeSensor now listens to dynamically added elements. You can create an element in javascript - EventQueue accepts and passes arguments to the event listener. - ResizeSensor passes extra data that may be useful like the difference of the height and the width from the last resize event --- src/ResizeSensor.js | 380 +++++++++++++++++++++++++++----------------- 1 file changed, 233 insertions(+), 147 deletions(-) diff --git a/src/ResizeSensor.js b/src/ResizeSensor.js index 842451c..5196def 100755 --- a/src/ResizeSensor.js +++ b/src/ResizeSensor.js @@ -14,20 +14,166 @@ } }( this, function () { + + var events = new EventQueue(); + + /** + * Class for dimension change detection. + * + * @param {Element|Element[]|Elements|jQuery} element + * @param {Function} callback + * + * @constructor + */ + var ResizeSensor = function ( element, callback ) { + var that = this; + + // Add class variables here + that.element = element; + that.callback = callback; + + forEachElement( element, function( elem ){ + if( !elem.resizeSensor ) { + appendDetectors( that, elem ); + } + + events.add( elem, callback ); + }); + }; + + ResizeSensor.prototype = { + constructor: ResizeSensor, + + detach: function ResizeSensorDetach ( ev, element ) { + ResizeSensor.detach( element || this.element, ev ); + }, + + reset: function ResizeSensorResize () { + forEachElement( element, function( elem ){ + if( elem.resizeSensor ) { + reset( that, elem ); + } + }); + } + }; + + /** + * Function for appending the detectors to the elements + */ + function appendDetectors ( that, element ) { + var style = 'display: block; position: absolute; left: 0; top: 0; right: 0; bottom: 0; overflow: scroll; z-index: -1; visibility: hidden;', + styleChild = 'position: absolute; left: 0; top: 0; transition: 0s;'; + + // Set the elements position to enable the ability to use top/right/bottom/left + if ( getComputedStyle(element, 'position') == 'static' ) { + element.style.position = 'relative'; + } + + // Create the sensor element and set the styles and innerHTML + var rSE = document.createElement( 'resize-sensor' ); + rSE.className = 'resize-sensor'; + + rSE.style.cssText = style; + rSE.innerHTML = + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + rSE.element = element; + + rSE.resetting = false; + + rSE.lastWidth = element.offsetWidth; + rSE.lastHeight = element.offsetHeight; + rSE.cacheWidth = null; + rSE.cacheHeight = null; + + rSE.expand = rSE.childNodes[0]; + rSE.expandChild = rSE.expand.childNodes[0]; + rSE.shrink = rSE.childNodes[1]; + rSE.shrinkChild = rSE.shrink.childNodes[0]; + + rSE.detach = function ResizeSensorDetach () { + that.detach( that.callback, element ); + }; + + rSE.reset = function ResizeSensorReset () { + reset( rSE ); + }; + + element.resizeSensor = rSE; + + element.appendChild( element.resizeSensor ); + reset( element.resizeSensor ); + } + + /** + * Class for event queueing and management + * + * @constructor + */ + function EventQueue () { + var that = this, + q = {}, + guid = 1; + this.add = function ( element, ev ) { + var guid = element.guid || 'rs-' + (guid++); + element.rsguid = guid; + q[guid] = q[guid] || []; + q[guid].push( ev ); + }; + + var i, j; + this.call = function ( element, context, arguments ) { + var guid = element.rsguid || ''; + if( !!q[guid] ) { + var evl = q[guid]; + for ( i = 0, j = evl.length; i < j; i++ ) { + evl[i].apply( context, arguments ); + } + } + }; + + this.remove = function ( element, ev ) { + var newQueue = []; + if( !!q[element.rsguid] ) { + var evl = q[element.rsguid]; + for( i = 0, j = evl.length; i < j; i++ ) { + if(evl[i] !== ev) { + newQueue.push( evl[i] ); + } + } + q[element.rsguid] = newQueue; + } + }; + + this.length = function ( element ) { + if( !!q[element.rsguid] ) { + return q[element.rsguid].length; + } + }; + } + + + var collectionTypes = ['[object Array]', '[object NodeList]', '[object HTMLCollection]']; /** * Iterate over each of the provided element(s). * * @param {HTMLElement|HTMLElement[]} elements * @param {Function} callback */ - function forEachElement( elements, callback ){ + function forEachElement ( elements, callback ){ + if( !elements ) { + return; + } var elementsType = Object.prototype.toString.call( elements ), - isCollectionTyped = ( '[object Array]' === elementsType - || ('[object NodeList]' === elementsType) - || ('[object HTMLCollection]' === elementsType) - || ('undefined' !== typeof jQuery && elements instanceof jQuery) //jquery - || ('undefined' !== typeof Elements && elements instanceof Elements) //mootools - ), + isVanillaCollection = collectionTypes.indexOf(elementsType) != -1, + isJQueryCollection = 'undefined' !== typeof Elements && elements instanceof Elements, + isMooToolsCollection = 'undefined' !== typeof Elements && elements instanceof Elements, + isCollectionTyped = isVanillaCollection || isJQueryCollection || isMooToolsCollection, i = 0, j = elements.length; if ( isCollectionTyped ) { @@ -40,169 +186,109 @@ } /** - * Class for dimension change detection. - * - * @param {Element|Element[]|Elements|jQuery} element - * @param {Function} callback - * - * @constructor + * @param {HTMLElement} element + * @param {String} prop + * @returns {String|Number} */ - var ResizeSensor = function( element, callback ) { - /** - * - * @constructor - */ - function EventQueue() { - var q = []; - this.add = function( ev ) { - q.push( ev ); - }; - - var i, j; - this.call = function( context, data ) { - for ( i = 0, j = q.length; i < j; i++ ) { - q[i].call( context, data ); - } - }; - - this.remove = function( ev ) { - var newQueue = []; - for( i = 0, j = q.length; i < j; i++ ) { - if(q[i] !== ev) newQueue.push( q[i] ); - } - q = newQueue; - }; - - this.length = function () { - return q.length; - }; - } - - /** - * @param {HTMLElement} element - * @param {String} prop - * @returns {String|Number} - */ - function getComputedStyle ( element, prop ) { - if ( element.currentStyle ) { - return element.currentStyle[prop]; - } else if ( window.getComputedStyle ) { - return window.getComputedStyle( element, null ).getPropertyValue( prop ); - } else { - return element.style[prop]; - } + function getComputedStyle ( element, prop ) { + if ( element.currentStyle ) { + return element.currentStyle[prop]; + } else if ( window.getComputedStyle ) { + return window.getComputedStyle( element, null ).getPropertyValue( prop ); + } else { + return element.style[prop]; } + } - /** - * - * @param {HTMLElement} element - * @param {Function} resized - */ - function attachResizeEvent( element, resized ) { - if (!element.resizedAttached) { - element.resizedAttached = new EventQueue(); - element.resizedAttached.add( resized ); - } else if (element.resizedAttached) { - element.resizedAttached.add( resized ); - return; - } - - element.resizeSensor = document.createElement('div'); - element.resizeSensor.className = 'resize-sensor'; + function hasClass ( element, className ) { + return (" "+ element.className +" ").replace(/[\n\t]/g, " ").indexOf(" "+ className +" ") > -1; + } - var style = 'position: absolute; left: 0; top: 0; right: 0; bottom: 0; overflow: hidden; z-index: -1; visibility: hidden;', - styleChild = 'position: absolute; left: 0; top: 0; transition: 0s;'; + function live (eventType, elementQuerySelector, cb) { + document.addEventListener( eventType, function (event) { + var qs = document.querySelectorAll(elementQuerySelector); - element.resizeSensor.style.cssText = style; - element.resizeSensor.innerHTML = - '
' + - '
' + - '
' + - '
' + - '
' + - '
'; - element.appendChild( element.resizeSensor ); + if (qs) { + var el = event.target, index = -1; + while (el && ((index = Array.prototype.indexOf.call(qs, el)) === -1)) { + el = el.parentElement; + } - if ( getComputedStyle(element, 'position') == 'static' ) { - element.style.position = 'relative'; + if (index > -1) { + cb.call(el, event); + } } + }, true ); + } + + /** + * Reset the size and scroll position of the resizeSensor element + * @param {[type]} rSE resizeSensor Element + */ + function reset ( rSE ) { + rSE.resetting = true; + rSE.expandChild.style.width = '100000px'; + rSE.expandChild.style.height = '100000px'; + + + rSE.expand.scrollLeft = 100000; + rSE.expand.scrollTop = 100000; + + rSE.shrink.scrollLeft = 100000; + rSE.shrink.scrollTop = 100000; + rSE.resetting = false; + } - var expand = element.resizeSensor.childNodes[0], - expandChild = expand.childNodes[0], - shrink = element.resizeSensor.childNodes[1], - shrinkChild = expand.childNodes[0], - - /** - * Resets the position of the resize-detection elements - */ - reset = function() { - expandChild.style.width = '100000px'; - expandChild.style.height = '100000px'; - - expand.scrollLeft = expandChild.offsetWidth; - expand.scrollTop = expandChild.offsetheight; - - shrink.scrollLeft = shrinkChild.offsetWidth; - shrink.scrollTop = shrinkChild.offsetheight; - }; - - reset(); - - var lastWidth = element.offsetWidth, lastHeight = element.offsetHeight, - cachedWidth, cachedHeight, //useful to not query offsetWidth twice - - onScroll = function() { - if ( (cachedWidth = element.offsetWidth) != lastWidth || (cachedHeight = element.offsetHeight) != lastHeight ) { - if ( !!element.resizedAttached ) { - element.resizedAttached.call( element, { - width: lastWidth, - height: lastHeight - }); - } - - lastWidth = cachedWidth; - lastHeight = cachedHeight; + function scrollHandler ( ev ) { + var + that = this, + detector = ev.target, + + type, + rSE = detector.parentNode, + elem = rSE.element; + if( elem && !rSE.resetting ) { + if ( + elem.offsetWidth != rSE.lastWidth + || elem.offsetHeight != rSE.lastHeight + ) { + events.call( elem, elem, [ + ev, + { + width: rSE.offsetWidth, + widthDifference: rSE.offsetWidth - rSE.lastWidth, + height: rSE.offsetHeight, + heightDifference: rSE.offsetHeight - rSE.lastHeight } - reset(); - }; - - var addEvent = function( el, name, cb ) { - if (el.attachEvent) { - el.attachEvent('on' + name, cb); - } else { - el.addEventListener(name, cb); - } - }; + ]); - addEvent( expand, 'scroll', onScroll ); - addEvent( shrink, 'scroll', onScroll ); + rSE.lastWidth = rSE.offsetWidth; + rSE.lastHeight = rSE.offsetHeight; + } + reset( rSE ); } - - forEachElement( element, function( elem ){ - attachResizeEvent( elem, callback ); - }); - - this.detach = function( ev ) { - ResizeSensor.detach( element, ev ); - }; - }; + } ResizeSensor.detach = function( element, ev ) { forEachElement( element, function( elem ){ - if( elem.resizedAttached && typeof ev == "function" ){ - elem.resizedAttached.remove( ev ); - if( elem.resizedAttached.length() ) { + if( typeof ev == "function" ){ + events.remove( elem, ev ); + if( elem.resizedAttached.length( elem ) ) { return; } } if ( elem.resizeSensor ) { - elem.removeChild( elem.resizeSensor ); + if(elem.resizeSensor.parentNode) { + elem.removeChild( elem.resizeSensor ); + } delete elem.resizeSensor; delete elem.resizedAttached; } }); }; + + // use only one event for all sensors + live( 'scroll', '.resize-sensor', scrollHandler ); return ResizeSensor; - }));