0

After spending hours on end trying to determine what is happening and why my code will not work, I've decided that maybe someone else will see something I'm not.

This is my program, cut down about as much as possible:

<!DOCTYPE html>
<html>
<head>
    <script>
        var ASML = function(content){

            var isDef = function(prm){ return typeof prm !== 'undefined'; };

            var loadContent = function(){
                asml.viewPort = (function(){
                    var self = this;
                    var pvar = p(self);

                    this.size = function(){
                        if(affectedChange[0]){
                            affectedChange[0].ifThis.push([window, "resize"]);
                        }
                        return {
                            x: function(){ return pvar.element.offsetWidth; },
                            y: function(){ return pvar.element.offsetHeight; },
                        }
                    }
                    this.parent = function(){
                        return null;
                    }

                    return this;
                }).apply(new create (document.createElement('div') ));

                content.apply(asml, [function(prm){ return new create(document.createElement('div'), prm); }] );

                var ev = document.createEvent('CustomEvent');
                ev.initCustomEvent("resize", false, false, null);
                window.dispatchEvent(ev);
            };

            var p = function(obj){ return private[obj.ID]; };

            var private = []
            var asml = this;
            var affectedChange = [];

            var create = function(element, prm){

                this.ID = private.length;

                private.push({
                    change: {OffsetL:0, OffsetR:0, OffsetB:0, OffsetT:0, Children:0, Parent:0},
                    element: element,
                    parent: null,
                    children: [],
                });

                var self = this;
                var pvar = p(self);

                var handleParam = function(prm, changeName, setAttr){
                    var attrChange = pvar.change[changeName];
                    // Defined "prm" means there is a new value to set, causing a change event
                    // Undefined "prm" means 
                    if(isDef(prm)){
                        // Remove prior listeners from the change event
                        for(var i = 0; i < attrChange.ifThis.length; i++){
                            var arg = attrChange.ifThis[i];
                            arg[0].removeEventListener(arg[1], attrChange.doThis, false);
                        }

                        attrChange.ifThis.splice(0, attrChange.ifThis.length);
                        
                        // Set latest affected change to this change
                        affectedChange.splice(0, 0, attrChange);
                        // Run the task to find dependent change events to listen to
                        setAttr( (typeof prm == "function") ? prm.apply(self) : prm );
                        // Remove this change from top of the "affected change" list
                        affectedChange.splice(0, 1);                
                        // Alert event listeners that this attributes value has changed
                        var ev = document.createEvent('CustomEvent');
                        ev.initCustomEvent("change"+changeName, false, false, null);
                        element.dispatchEvent(ev);

                        // The listener task must be run through handleParam again to catch listeners for its next change event
                        attrChange.doThis = function(event){ handleParam(prm, changeName, setAttr); };

                        // Assign new listeners under the new change task
                        for(var i = 0; i < attrChange.ifThis.length; i++){
                            var arg = attrChange.ifThis[i];
                            arg[0].addEventListener(arg[1], attrChange.doThis, false);
                        }
                        
                        return true;

                    } else {
                        if(affectedChange[0] && affectedChange[0] != attrChange){
                            affectedChange[0].ifThis.push([element, "change" + changeName]);
                        }
                        return false;
                    }
                };
                this.offset = function(prm){
                    if(isDef(prm)){
                        switch(typeof prm){
                            case "object":
                                for(attr in prm){
                                    self.offset()[ attr ]( prm[ attr ] );
                                }
                                break;
                        }

                        return self;
                    } else {
                        var doStandard = function(prm, side, abbr){
                            var setAttr = function(prm){
                                element.style[side] = (self.parent().offset()[abbr]() + prm) + "px";
                            };

                            if(handleParam(prm, "Offset" + abbr.toUpperCase(), setAttr)){
                                return self;
                            } else {
                                return parseFloat(element.style[side]);
                            }
                        };

                        return {
                            l: function(prm){ return doStandard(prm, "left", "l"); },
                            r: function(prm){ return doStandard(prm, "right", "r"); },
                            b: function(prm){ return doStandard(prm, "bottom", "b"); },
                            t: function(prm){ return doStandard(prm, "top", "t"); },
                        };
                    }
                };
                this.parent = function(prm){
                    var setAttr = function(prm){
                        // only occurs if parent() is called before children()
                        var index;
                        if(pvar.parent != null && (index = p(pvar.parent).children.indexOf(self)) != -1){
                            pvar.parent.children(index, 1, []);
                        }

                        pvar.parent = prm;

                        if(pvar.parent != null && p(pvar.parent).children.indexOf(self) == -1){
                            pvar.parent.children(-1, 0, [self]);
                        }
                    }

                    if(handleParam(prm, "Parent", setAttr)){
                        return self;
                    } else {
                        if(pvar.parent != null){
                            return pvar.parent;
                        } else {
                            return {
                                offset: function(){
                                    return {
                                        l: function(){ return 0; },
                                        r: function(){ return 0; },
                                        b: function(){ return 0; },
                                        t: function(){ return 0; },
                                    };
                                },
                                size: function(){
                                    return asml.viewPort.size();
                                },
                            };
                        }
                    }
                };
                this.children = function(index, remove, insert){
                    // "prm" remains undefined unless a child is removed or inserted
                    var prm;
                    if( isDef(remove) ){
                        if(!isDef(insert)){
                            insert = [];
                        }
                        prm = [index, remove, insert];
                    }

                    var setAttr = function(prm){
                        var remove = pvar.children.slice(index, prm[1] + index);
                        var insert = prm[2];

                        pvar.children.splice.apply(pvar.children, [prm[0], prm[1]].concat(insert));

                        for(var i = 0; i < remove.length; i++){
                            if(p(remove[i]).parent != null){
                                remove[i].parent(null);
                            }
                        }
                        for(var i = 0; i < insert.length; i++){
                            if(p(insert[i]).parent != self){
                                insert[i].parent(self);
                            }
                        }
                    }

                    if(handleParam(prm, "Children", setAttr)){
                        return self;
                    } else {
                        if(!isDef(index)){
                            return pvar.children;
                        } else {
                            return pvar.children[index];
                        }
                    }
                };

                this.size = function(){
                    return {
                        x: function(){ return asml.viewPort.size().x() - self.offset().l() - self.offset().r(); },
                        y: function(){ return asml.viewPort.size().y() - self.offset().b() - self.offset().t(); }
                    };
                };

                for(i in pvar.change){
                    pvar.change[i] = {
                        doThis: null,
                        ifThis: []
                    };
                }
                // Default styling and attributes are set
                var es = element.style
                es.position = "absolute";
                es.overflow = "hidden";
                es.border = "1px solid black";
                self.offset({ l: 0, r: 0, b: 0, t: 0 });

                document.body.appendChild(element);
            };

            if(document.body){
                loadContent();
            } else {
                window.addEventListener('load', loadContent, false);
            }
        };
    </script>
    <script>
        var testEl, testEl2;
        new ASML(function(e){
            var asml = this;

            function size(s, a, b, c){
                if(b){
                    return function(){
                        return asml.viewPort.size()[a]() - this.offset()[c]() - this.parent().offset()[b]() - s;
                    };
                } else {
                    return function(){
                        return (this.parent().size()[a]() - s) / 2;
                    };
                }
            }

                    testEl = e()
                        .offset({
                            t: size(200, 'y', 't', 'b'),
                            r: 10,
                            l: size(200, 'x', 'l', 'r'),
                            b: 10,
                        })

            testEl2 = e()
                .offset({ l: 10, r: 10, b: 10, t: 50 })
                .children(0,0,[
                    testEl
                ])
        });
    </script>
</head>
</html>

It is, in essence, meant to mimic a sort of alternative-to-CSS in JavaScript where attributes, such as the box offsets, are dependent upon other attributes, such as the offsets of box's parent box.

In the offset method for one of these boxes, like testEl or testEl2, you can set the left, right, bottom, and top offset as follows:

box.offset({
    l: 10,
    r: 10,
    b: 10,
    t: 10,
});

You can also place functions in place of numerical values, which will allow the value of the offset to be reassessed everytime and "affecting" attribute is changed. For instance, if I say:

box1.offset({
    l: function(){ return box2.offset().r() * .5; }
});

box2.offset({ r: 20 });

Then the offset of box1 will reevaluate to match it's left offset to half the size of box2's right offset.

That all said, it would appear that, with certain configurations of assiging attribute values and the use of these "affecting" attributes, a strange thing occurs where one of the offsets (in the code example above, the bottom offset), isn't evaluated correctly and doesn't change when the parent bottom offset changes.

You may have to put it in your browser to understand, but you'll see that, in Safari and Chrome, at least, the bottom offset of testEl remains at 10 even after it becomes of child of testEl2 and should be reevaluated to 20. For some reason, though, it appears to work just fine in Firefox, so I'm wondering if maybe Firefox uses an event system that compensates for something unusual in my code.

If anyone has any thoughts on how I might improve my code and could enlighten me as to why I'm getting such weird results, your response would be greatly appreciated. Thanks.



Update January 31, 2014 (on jfriend00's suggestion):

To see if Chrome and Safari were dropping even listeners because they were trying to cut down the amount of processing taken up by the large number of layout changes, I made a few changes. My original code for the doStandard function in my offset function was...

var doStandard = function(prm, side, abbr){
    var setAttr = function(prm){
        element.style[side] = (self.parent().offset()[abbr]() + prm) + "px";
    };

    if(handleParam(prm, "Offset" + abbr.toUpperCase(), setAttr)){
        return self;
    } else {
        return parseFloat(element.style[side]);
    }
};

I changed this to

var doStandard = function(prm, side, abbr){
    var setAttr = function(prm){
        console.log(self.ID, "child of", self.parent().ID, abbr + ":", prm);
        offset[abbr] = (self.parent().offset()[abbr]() + prm);
    };

    if(handleParam(prm, "Offset" + abbr.toUpperCase(), setAttr)){
        return self;
    } else {
        return parseFloat(offset[abbr]);
    }
};

keeping the layout from being affected and tracking all changes in a persistent variable inside each object, instead. I also had to do a few other things to make sure I got all the numbers I was supposed and added in a console logging expression to track what offsets were changed and in what order.

I tried this out on Chrome and Firefox. Here is what was logged for Chrome:

0 "child of" undefined "l:" 0 
0 "child of" undefined "r:" 0 
0 "child of" undefined "b:" 0 
0 "child of" undefined "t:" 0 
1 "child of" undefined "l:" 0 
1 "child of" undefined "r:" 0 
1 "child of" undefined "b:" 0 
1 "child of" undefined "t:" 0 
1 "child of" undefined "t:" 466 
1 "child of" undefined "r:" 10 
1 "child of" undefined "l:" 1069 
1 "child of" undefined "b:" 10 
1 "child of" undefined "t:" 456 
2 "child of" undefined "l:" 0 
2 "child of" undefined "r:" 0 
2 "child of" undefined "b:" 0 
2 "child of" undefined "t:" 0 
2 "child of" undefined "l:" 10 
2 "child of" undefined "r:" 10 
2 "child of" undefined "b:" 10 
2 "child of" undefined "t:" 50 
1 "child of" 2 "r:" 10 
1 "child of" 2 "l:" 1049 
1 "child of" 2 "t:" 406 
1 "child of" 2 "l:" 1049 

and for Firefox

 0 child of undefined l: 0
 0 child of undefined r: 0
 0 child of undefined b: 0
 0 child of undefined t: 0
 1 child of undefined l: 0
 1 child of undefined r: 0
 1 child of undefined b: 0
 1 child of undefined t: 0
 1 child of undefined t: 159
 1 child of undefined r: 10
 1 child of undefined l: 1066
 1 child of undefined b: 10
 1 child of undefined t: 149
 2 child of undefined l: 0
 2 child of undefined r: 0
 2 child of undefined b: 0
 2 child of undefined t: 0
 2 child of undefined l: 10
 2 child of undefined r: 10
 2 child of undefined b: 10
 2 child of undefined t: 50
 1 child of 2 r: 10
 1 child of 2 l: 1046
 1 child of 2 t: 99
 1 child of 2 b: 10
 1 child of 2 t: 89
 1 child of 2 l: 1046
 1 child of 2 t: 89 

ID "0" is unimportant, ID "1" corresponds to testEl and ID "2" to testEl2. As you can see, Firefox dispatch some event tasks towards the end that Chrome doesn't. These are the event listener tasks that Chrome and Safari seem to be dropping (or not getting):

 1 child of 2 b: 10

Even when not altering the layout in every event, Chrome still doesn't execute this bottom offset shift. Could it be that just altering a variable value this much and this quickly sets off an alarm for Safari and Chrome? And, if so, how do I get around that?

Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129

2 Answers2

1

When you make changes to the DOM that require some new layout to be calculated, browsers will attempt to defer that layout until you are done making all your changes so they can just do the layout once. This is because layout can be an expensive option. Thus, just because you've made a change to the DOM does not mean that the browser has yet relaid everything out and put everything in it's new position. When your javascript finishes executing and the browser gets back in its event loop, it will relayout things that need layout and then repaint the screen, but it tries to wait until you're done making changes before doing any of this.

What this means is that some querying of properties may not be entirely accurate until the layout occurs. Browsers try to be "smart" about this and when you request certain properties, they may realize that this property won't be accurate until after a layout and they "may" force a layout. But, as far as I know, this type of behavior is not defined by a standard and there are performance tradeoffs involved so it would not surprise me if different browsers had slightly different behavior in this regard.

Here are some references on "forcing a layout":

Can I use javascript to force the browser to "flush" any pending layout changes?

http://www.phpied.com/rendering-repaint-reflowrelayout-restyle/

Note: there are actually more articles written on how to prevent intermediate layout because allowing a bunch of DOM changes to defer layout until you're done can be a huge performance increase.

Community
  • 1
  • 1
jfriend00
  • 683,504
  • 96
  • 985
  • 979
0

After finally deciding that the problem was unsolvable unless I had access to viewing the event listeners of every event before it was broadcast, I created my own "event system" and used that to identify the issues. It's kind of hard to explain, and I have some doubts anyone will find this useful, but, out of forum etiquette, I'll post my final code.

<!DOCTYPE html>
<html>
<head>
    <script>
        var ASML = function(content){

            var isDef = function(prm){ return typeof prm !== 'undefined'; };

            var loadContent = function(){
                asml.viewPort = (function(){
                    var self = this;
                    var pvar = p(self);
                    pvar.change.Size = { effectees: [] };

                    this.size = function(){
                        if(affectedChange[0]){
                            affectedChange[0].effectors.splice(-1, 0, pvar.change.Size);
                            pvar.change.Size.effectees.splice(-1, 0, affectedChange[0]);
                        }
                        return {
                            x: function(){ return pvar.element.offsetWidth; },
                            y: function(){ return pvar.element.offsetHeight; },
                        }
                    }
                    this.parent = function(){
                        return null;
                    }

                    window.addEventListener('resize', function(){
                        for(var i = 0; i < pvar.change.Size.effectees.length; i++){
                            pvar.change.Size.effectees[i].doThis();
                        }
                    }, false);

                    return this;
                }).apply(new create (document.createElement('div') ));

                content.apply(asml, [function(prm){ return new create(document.createElement('div'), prm); }] );

                var ev = document.createEvent('CustomEvent');
                ev.initCustomEvent("resize", false, false, null);
                window.dispatchEvent(ev);
            };

            var p = function(obj){ return private[obj.ID]; };

            var private = []
            var asml = this;
            var affectedChange = [];

            var create = function(element, prm){

                this.ID = private.length;

                private.push({
                    change: {OffsetL:0, OffsetR:0, OffsetB:0, OffsetT:0, Children:0, Parent:0},
                    element: element,
                    parent: null,
                    children: [],
                });

                var self = this;
                var pvar = p(self);

                var handleParam = function(prm, changeName, setAttr){
                    var attrChange = pvar.change[changeName];
                    // Defined "prm" means there is a new value to set, causing a change event
                    // Undefined "prm" means 
                    if(isDef(prm)){
                        // Remove previous effectors before attaching new ones
                        for(var i = 0; i < attrChange.effectors.length; i++){
                            var eff = attrChange.effectors[i].effectees;
                            var index;
                            while((index = eff.indexOf(attrChange)) != -1){
                                eff.splice(index, 1);
                            }
                        }

                        attrChange.effectors.splice(0, attrChange.effectors.length);

                        // Set latest "affected change" to this change
                        affectedChange.splice(0, 0, attrChange);
                        // Look for new effectors
                        setAttr( (typeof prm == "function") ? prm.apply(self) : prm );
                        // Remove this change from top of the "affected change" list
                        affectedChange.splice(0, 1);

                        // The listener task must be run through handleParam again to catch listeners for its next change event
                        attrChange.doThis = function(){ handleParam(prm, changeName, setAttr); };

                        // Alert existing effectees
                        attrChange.effectees.slice(0,attrChange.effectees.length).forEach(function(effectee, i){
                            console.log(i);
                            console.log({ dothis: effectee.doThis });
                            effectee.doThis();
                            console.log(i);
                        });

                        // Alert event listeners that this attributes value has changed
                        var ev = document.createEvent('CustomEvent');
                        ev.initCustomEvent("change"+changeName, false, false, null);
                        element.dispatchEvent(ev);

                        return true;

                    } else {
                        if(affectedChange[0] && affectedChange[0] != attrChange){
                            affectedChange[0].effectors.splice(-1, 0, attrChange);
                            attrChange.effectees.splice(-1, 0, affectedChange[0]);
                        }
                        return false;
                    }
                };
                this.offset = function(prm){
                    if(isDef(prm)){
                        switch(typeof prm){
                            case "object":
                                for(attr in prm){
                                    self.offset()[ attr ]( prm[ attr ] );
                                }
                                break;
                        }

                        return self;
                    } else {
                        var doStandard = function(prm, side, abbr){
                            var setAttr = function(prm){
                                element.style[side] = (self.parent().offset()[abbr]() + prm) + "px";
                            };

                            if(handleParam(prm, "Offset" + abbr.toUpperCase(), setAttr)){
                                return self;
                            } else {
                                return parseFloat(element.style[side]);
                            }
                        };

                        return {
                            l: function(prm){ return doStandard(prm, "left", "l"); },
                            r: function(prm){ return doStandard(prm, "right", "r"); },
                            b: function(prm){ return doStandard(prm, "bottom", "b"); },
                            t: function(prm){ return doStandard(prm, "top", "t"); },
                        };
                    }
                };
                this.parent = function(prm){
                    var setAttr = function(prm){
                        // only occurs if parent() is called before children()
                        var index;
                        if(pvar.parent != null && (index = p(pvar.parent).children.indexOf(self)) != -1){
                            pvar.parent.children(index, 1, []);
                        }

                        pvar.parent = prm;

                        if(pvar.parent != null && p(pvar.parent).children.indexOf(self) == -1){
                            pvar.parent.children(-1, 0, [self]);
                        }
                    }

                    if(handleParam(prm, "Parent", setAttr)){
                        return self;
                    } else {
                        if(pvar.parent != null){
                            return pvar.parent;
                        } else {
                            return {
                                offset: function(){
                                    return {
                                        l: function(){ return 0; },
                                        r: function(){ return 0; },
                                        b: function(){ return 0; },
                                        t: function(){ return 0; },
                                    };
                                },
                                size: function(){
                                    return asml.viewPort.size();
                                },
                            };
                        }
                    }
                };
                this.children = function(index, remove, insert){
                    // "prm" remains undefined unless a child is removed or inserted
                    var prm;
                    if( isDef(remove) ){
                        if(!isDef(insert)){
                            insert = [];
                        }
                        prm = [index, remove, insert];
                    }

                    var setAttr = function(prm){
                        var remove = pvar.children.slice(index, prm[1] + index);
                        var insert = prm[2];

                        pvar.children.splice.apply(pvar.children, [prm[0], prm[1]].concat(insert));

                        for(var i = 0; i < remove.length; i++){
                            if(p(remove[i]).parent != null){
                                remove[i].parent(null);
                            }
                        }
                        for(var i = 0; i < insert.length; i++){
                            if(p(insert[i]).parent != self){
                                insert[i].parent(self);
                            }
                        }
                    }

                    if(handleParam(prm, "Children", setAttr)){
                        return self;
                    } else {
                        if(!isDef(index)){
                            return pvar.children;
                        } else {
                            return pvar.children[index];
                        }
                    }
                };

                this.size = function(){
                    return {
                        x: function(){ return asml.viewPort.size().x() - self.offset().l() - self.offset().r(); },
                        y: function(){ return asml.viewPort.size().y() - self.offset().b() - self.offset().t(); }
                    };
                };

                for(i in pvar.change){
                    pvar.change[i] = {
                        doThis: null,
                        effectors: [],
                        effectees: [],
                    };
                }


                // Default styling and attributes are set
                var es = element.style
                es.position = "absolute";
                es.overflow = "hidden";
                es.border = "1px solid black";
                self.offset({ l: 0, r: 0, b: 0, t: 0 });

                element.id = "ASML_" + self.ID;

                document.body.appendChild(element);
            };

            if(document.body){
                loadContent();
            } else {
                window.addEventListener('load', loadContent, false);
            }
        };
    </script>
    <script>
        var testEl, testEl2;
        new ASML(function(e){
            var asml = this;

            function size(s, a, b, c){
                if(b){
                    return function(){
                        return asml.viewPort.size()[a]() - this.offset()[c]() - this.parent().offset()[b]() - s;
                    };
                } else {
                    return function(){
                        return (this.parent().size()[a]() - s) / 2;
                    };
                }
            }

                    testEl = e()
                        .offset({
                            t: size(200, 'y', 't', 'b'),
                            r: 10,
                            l: size(200, 'x', 'l', 'r'),
                            b: 10,
                        })

            testEl2 = e()
                .offset({ l: 10, r: 10, b: 10, t: 50 })
                .children(0,0,[
                    testEl
                ])

        });

    </script>
</head>
</html>

The main thing I had to change was the handleParam function, but there were a lot of little tweaks that had to be made so that I could make my code work independent of how a browser's event listeners were called, which was the key.

I think the issue was that, as event listeners were being called, new ones were being added to the list. Firefox and, as it turns out, IE had a way of dealing with this (they made a copy of the list and then ran calls from that so that, as the original changed, the temporary one stayed the same) but Chrome and Safari didn't. Once I had engineered a system more like FF and IE, everything started working perfectly.

Thanks to everyone for their help and suggestions.

  • 1
    The procedure is to wait 48 hours before marking your answer as accepted, not to put a Solved label in your title. – BoltClock Feb 02 '14 at 05:40
  • Oh, OK. Good to know :) I guess I had always seen [[Solved]] in other titles and assumed that's what I should do. – user1864983 Feb 09 '14 at 23:39