20

Is it possible to simply add event listeners to certain elements to detect if their height or width have been modified? I'd like do this without using something intensive like:

$(window).resize(function() { ... });

Ideally, I'd like to bind to specific elements:

$("#primaryContent p").resize(function() { ... });

It seems like using a resize handler on the window is the only solution, but this feels like overkill. It also doesn't account for situations where an element's dimensions are modified programatically.

Jim Jeffers
  • 17,572
  • 4
  • 41
  • 49
  • You could read the height of such element on load and the compare it to the new size on window resize. Would that work? – elclanrs Feb 13 '12 at 03:24
  • @Jim Jeffers - check out the new solution I posted, it's totally event-based, works IE9+, but that's just because I used ES5 array methods in the code, the mechanism itself actually works down to IE6 I believe ;) – csuwldcat Mar 22 '13 at 14:05

3 Answers3

5

I just came up with a purely event-based way to detect element resize for any element that can contain children, I've pasted the code from the solution below.

See also the original blog post, which has some historical details. Previous versions of this answer were based on a previous version of the blog post.


The following is the JavaScript you’ll need to enable resize event listening.

(function(){
  var attachEvent = document.attachEvent;
  var isIE = navigator.userAgent.match(/Trident/);
  var requestFrame = (function(){
    var raf = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame ||
        function(fn){ return window.setTimeout(fn, 20); };
    return function(fn){ return raf(fn); };
  })();

  var cancelFrame = (function(){
    var cancel = window.cancelAnimationFrame || window.mozCancelAnimationFrame || window.webkitCancelAnimationFrame ||
           window.clearTimeout;
    return function(id){ return cancel(id); };
  })();

  function resizeListener(e){
    var win = e.target || e.srcElement;
    if (win.__resizeRAF__) cancelFrame(win.__resizeRAF__);
    win.__resizeRAF__ = requestFrame(function(){
      var trigger = win.__resizeTrigger__;
      trigger.__resizeListeners__.forEach(function(fn){
        fn.call(trigger, e);
      });
    });
  }

  function objectLoad(e){
    this.contentDocument.defaultView.__resizeTrigger__ = this.__resizeElement__;
    this.contentDocument.defaultView.addEventListener('resize', resizeListener);
  }

  window.addResizeListener = function(element, fn){
    if (!element.__resizeListeners__) {
      element.__resizeListeners__ = [];
      if (attachEvent) {
        element.__resizeTrigger__ = element;
        element.attachEvent('onresize', resizeListener);
      }
      else {
        if (getComputedStyle(element).position == 'static') element.style.position = 'relative';
        var obj = element.__resizeTrigger__ = document.createElement('object'); 
        obj.setAttribute('style', 'display: block; position: absolute; top: 0; left: 0; height: 100%; width: 100%; overflow: hidden; pointer-events: none; z-index: -1;');
        obj.__resizeElement__ = element;
        obj.onload = objectLoad;
        obj.type = 'text/html';
        if (isIE) element.appendChild(obj);
        obj.data = 'about:blank';
        if (!isIE) element.appendChild(obj);
      }
    }
    element.__resizeListeners__.push(fn);
  };

  window.removeResizeListener = function(element, fn){
    element.__resizeListeners__.splice(element.__resizeListeners__.indexOf(fn), 1);
    if (!element.__resizeListeners__.length) {
      if (attachEvent) element.detachEvent('onresize', resizeListener);
      else {
        element.__resizeTrigger__.contentDocument.defaultView.removeEventListener('resize', resizeListener);
        element.__resizeTrigger__ = !element.removeChild(element.__resizeTrigger__);
      }
    }
  }
})();

Usage

Here’s a pseudo code usage of this solution:

var myElement = document.getElementById('my_element'),
    myResizeFn = function(){
        /* do something on resize */
    };
addResizeListener(myElement, myResizeFn);
removeResizeListener(myElement, myResizeFn);

Demo

http://www.backalleycoder.com/resize-demo.html

csuwldcat
  • 8,021
  • 2
  • 37
  • 32
  • 1
    Just in case a mod see's I've used this answer twice, do notice that the other question was about the CSS -moz-resize property - but the solution worked for his concern as well. This question is different, wasn't sure that duping was the appropriate action. – csuwldcat Mar 22 '13 at 00:10
  • 2
    Be careful when posting copy and paste boilerplate/verbatim answers to multiple questions, these tend to be flagged as "spammy" by the community. If you're doing this then it usually means the questions are duplicates so flag them as such instead. – Kev Mar 22 '13 at 00:39
  • 1
    @Kev - What about when the questions are very different, but share a common solution? – csuwldcat Mar 22 '13 at 14:00
  • You need to make sure your answer addresses the "specifics" of the question, so write it up that way....or flag as a dupe. If the solution is the same then the basic problem the OP's are solving must be more or less the same. – Kev Mar 22 '13 at 14:06
  • 2
    @Kev - I see that you've duped the answer here: http://stackoverflow.com/questions/8082729/how-to-detect-css3-resize-events. Now how do you figure that the question "How to detect CSS3 resize events" is the same as "Detect if an element has been resized via javascript?" - they are not the same. They share a common solution, but the askers need arises from completely different areas of the web stack, one is from CSS, the other JS. – csuwldcat Mar 22 '13 at 16:18
  • Sorry, the one you duped was titled "-moz-resize event", I pasted in the wrong title - same issue though, curious as to what the answer is there? Additionally, if they are the same, why did you not dupe the other one? – csuwldcat Mar 22 '13 at 16:26
  • Very nice workaround! Seems to work in Opera, too (tested in version 16). – stofl Oct 04 '13 at 10:31
4

Here is a jQuery plugin with watch and unwatch methods that can watch particular properties of an element. It is invoked as a method of a jQuery object. It uses built-in functionality in browsers that return events when the DOM changes, and uses setTimeout() for browsers that do not support these events.

The general syntax of the watch function is below:

$("selector here").watch(props, func, interval, id);
  • props is a comma-separated string of the properties you wish to watch (such as "width,height").
  • func is a callback function, passed the parameters watchData, index, where watchData refers to an object of the form { id: itId, props: [], func: func, vals: [] }, and index is the index of the changed property. this refers to the changed element.
  • interval is the interval, in milliseconds, for setInterval() in browsers that do not support property watching in the DOM.
  • id is an optional id that identifies this watcher, and is used to remove a particular watcher from a jQuery object.

The general syntax of the unwatch function is below:

$("selector here").unwatch(id);
  • id is an optional id that identifies this watcher to be removed. If id is not specified, all watchers from the object will be removed.

For those who are curious, the code of the plugin is reproduced below:

$.fn.watch = function(props, func, interval, id) {
    /// <summary>
    /// Allows you to monitor changes in a specific
    /// CSS property of an element by polling the value.
    /// when the value changes a function is called.
    /// The function called is called in the context
    /// of the selected element (ie. this)
    /// </summary>    
    /// <param name="prop" type="String">CSS Property to watch. If not specified (null) code is called on interval</param>    
    /// <param name="func" type="Function">
    /// Function called when the value has changed.
    /// </param>    
    /// <param name="func" type="Function">
    /// optional id that identifies this watch instance. Use if
    /// if you have multiple properties you're watching.
    /// </param>
    /// <param name="id" type="String">A unique ID that identifies this watch instance on this element</param>  
    /// <returns type="jQuery" /> 
    if (!interval)
        interval = 200;
    if (!id)
        id = "_watcher";

    return this.each(function() {
        var _t = this;
        var el = $(this);
        var fnc = function() { __watcher.call(_t, id) };
        var itId = null;

        if (typeof (this.onpropertychange) == "object")
            el.bind("propertychange." + id, fnc);
        else if ($.browser.mozilla)
            el.bind("DOMAttrModified." + id, fnc);
        else
            itId = setInterval(fnc, interval);

        var data = { id: itId,
            props: props.split(","),
            func: func,
            vals: []
        };
        $.each(data.props, function(i) { data.vals[i] = el.css(data.props[i]); });
        el.data(id, data);
    });

    function __watcher(id) {
        var el = $(this);
        var w = el.data(id);

        var changed = false;
        var i = 0;
        for (i; i < w.props.length; i++) {
            var newVal = el.css(w.props[i]);
            if (w.vals[i] != newVal) {
                w.vals[i] = newVal;
                changed = true;
                break;
            }
        }
        if (changed && w.func) {
            var _t = this;
            w.func.call(_t, w, i)
        }
    }
}
$.fn.unwatch = function(id) {
    this.each(function() {
        var w = $(this).data(id);
        var el = $(this);
        el.removeData();

        if (typeof (this.onpropertychange) == "object")
            el.unbind("propertychange." + id, fnc);
        else if ($.browser.mozilla)
            el.unbind("DOMAttrModified." + id, fnc);
        else
            clearInterval(w.id);
    });
    return this;
}
Kevin Ji
  • 10,479
  • 4
  • 40
  • 63
  • Very cool! So in this situation if I have a div that changes from 50% to 70% programmatically, I would be able to detect that via the DOM event listener. BUT if it changes its width due to the window being resized, I'd have to listen for changes on the $(window).resize() in addition to utilizing this solution. Correct? – Jim Jeffers Feb 13 '12 at 03:49
  • @JimJeffers Yes, although I should point out that this function resorts to `setTimeout()` if a browser doesn't return DOM modified events, which may or may not be acceptable to your application. – Kevin Ji Feb 13 '12 at 04:11
  • Right that would not be acceptable, but that is only a fall back for the propertychange and DOMAttrModified events which I was not aware of before. – Jim Jeffers Feb 13 '12 at 04:17
  • I would just like to note that this post is over two years old; `DOMAttrModified` has since been [deprecated](https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Mutation_events) and is slated for removal, and `onpropertychange` is [IE-only](http://msdn.microsoft.com/en-us/library/ie/ms536956(v=vs.85).aspx). In other words, in most cases this will fall back to polling, which might or might not be okay for you. – Thomas Sep 05 '14 at 12:25
0

Yes it is possible. You will have to track all of the elements on load and store it. You can try out the demo here. In it, you don't have to use any libraries, but I used jQuery just to be faster.

First thing first - Store their initial size

You can do that by using this method:

var state = [];           //Create an public (not necessary) array to store sizes.

$(window).load(function() {
    $("*").each(function() {
        var arr = [];
        arr[0] = this
        arr[1] = this.offsetWidth;
        arr[2] = this.offsetHeight;

        state[state.length] = arr;    //Store all elements' initial size
    });
});

Again, I used jQuery just to be fast.

Second - Check!

Of course you will need to check if it has been changed:

function checksize(ele) {
    for (var i = 0; i < state.length; i++) {       //Search through your "database"
        if (state[i][0] == ele) {
            if (state[i][1] == ele.offsetWidth && state[i][2] == ele.offsetHeight) {
                return false
            } else {
                return true
            }
        }
    }
}

Simply it will return false if it has not been change, true if it has been change.

Hope this helps you out!

Demo: http://jsfiddle.net/DerekL/6Evk6/

Derek 朕會功夫
  • 92,235
  • 44
  • 185
  • 247
  • this is a solid attempt but I think the watch plugin posted in a separate answer is more efficient. Mainly because it uses the DOM and I wanted to be able to subscribe to an event in order to react to the changes. – Jim Jeffers Feb 13 '12 at 03:52
  • I think that this answer is not correct because OP asked for a trigger / event that would get fired when the size changes. Your solution works only when checking manually. – Memet Olsen Jun 25 '14 at 08:04