Monitoring Html Element CSS Changes in JavaScript

Posted by Rick Strahl on West-Wind See other posts from West-Wind or by Rick Strahl
Published on Tue, 15 Feb 2011 09:17:44 GMT Indexed on 2011/02/15 15:26 UTC
Read the original article Hit count: 412

Filed under:
|
|

[ updated Feb 15, 2011: Added event unbinding to avoid unintended recursion ]

Here's a scenario I've run into on a few occasions: I need to be able to monitor certain CSS properties on an HTML element and know when that CSS element changes. For example, I have a some HTML element behavior plugins like a drop shadow that attaches to any HTML element, but I then need to be able to automatically keep the shadow in sync with the window if the  element dragged around the window or moved via code.

Unfortunately there's no move event for HTML elements so you can't tell when it's location changes. So I've been looking around for some way to keep track of the element and a specific CSS property, but no luck. I suspect there's nothing native to do this so the only way I could think of is to use a timer and poll rather frequently for the property.

I ended up with a generic jQuery plugin that looks like this:

(function($){
$.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 Properties to watch sep. by commas</param>    
    /// <param name="func" type="Function">
    /// Function called when the value has changed.
    /// </param>    
    /// <param name="interval" type="Number">
    /// Optional interval for browsers that don't support DOMAttrModified or propertychange events.
    /// Determines the interval used for setInterval calls.
    /// </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;

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

        hookChange(el$, id, data.fnc);

    });

    function hookChange(el$, id, fnc) {
        el$.each(function () {
            var el = $(this);
            if (typeof (el.get(0).onpropertychange) == "object")
                el.bind("propertychange." + id, fnc);
            else if ($.browser.mozilla)
                el.bind("DOMAttrModified." + id, fnc);
            else
                itId = setInterval(fnc, interval);
        });
    }
    function __watcher(id) {
        var el$ = $(this);
        var w = el$.data(id);
        if (!w) return;
        var _t = this;

        if (!w.func)
            return;

        // must unbind or else unwanted recursion may occur
        el$.unwatch(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.call(_t, w, i);

        // rebind event
        hookChange(el$, id, w.fnc);
    }
}
$.fn.unwatch = function (id) {
    this.each(function () {
        var el = $(this);
        var fnc = el.data(id).fnc;
        try {
            if (typeof (this.onpropertychange) == "object")
                el.unbind("propertychange." + id, fnc);
            else if ($.browser.mozilla)
                el.unbind("DOMAttrModified." + id, fnc);
            else
                clearInterval(id);
        }
        // ignore if element was already unbound
        catch (e) { }
    });
    return this;
}
})(jQuery);

With this I can now monitor movement by monitoring say the top CSS property of the element. The following code creates a box and uses the draggable (jquery.ui) plugin and a couple of custom plugins that center and create a shadow. Here's how I can set this up with the watcher:

$("#box")
        .draggable()
        .centerInClient()
        .shadow()
        .watch("top", function() {
                    $(this).shadow();
               },70,"_shadow");
                
... 
$("#box")
.unwatch("_shadow")
.shadow("remove");

This code basically sets up the window to be draggable and initially centered and then a shadow is added. The .watch() call then assigns a CSS property to monitor (top in this case) and a function to call in response. The component now sets up a setInterval call and keeps on pinging this property every time. When the top value changes the supplied function is called.

While this works and I can now drag my window around with the shadow following suit it's not perfect by a long shot. The shadow move is delayed and so drags behind the window, but using a higher timer value is not appropriate either as the UI starts getting jumpy if the timer's set with too small of an increment.

This sort of monitor can be useful for other things as well where operations are maybe not quite as time critical as a UI operation taking place.

Can anybody see a better a better way of capturing movement of an element on the page?

© Rick Strahl, West Wind Technologies, 2005-2011
Posted in ASP.NET  JavaScript  jQuery  
kick it on DotNetKicks.com

© West-Wind or respective owner

Related posts about ASP.NET

Related posts about JavaScript