12

Still trying to answer this question, and I think I finally found a solution, but it runs too slow.

var $div = $('<div>')
    .css({ 'border': '1px solid red', 'position': 'absolute', 'z-index': '65535' })
    .appendTo('body');

$('body *').live('mousemove', function(e) {
    var topElement = null;
    $('body *').each(function() {
        if(this == $div[0]) return true;
        var $elem = $(this);
        var pos = $elem.offset();
        var width = $elem.width();
        var height = $elem.height();
        if(e.pageX > pos.left && e.pageY > pos.top
            && e.pageX < (pos.left + width) && e.pageY < (pos.top + height)) {
            var zIndex = document.defaultView.getComputedStyle(this, null).getPropertyValue('z-index');
            if(zIndex == 'auto') zIndex = $elem.parents().length;
            if(topElement == null || zIndex > topElement.zIndex) {
                topElement = {
                    'node': $elem,
                    'zIndex': zIndex
                };
            }

        }
    });
    if(topElement != null ) {
        var $elem = topElement.node;
        $div.offset($elem.offset()).width($elem.width()).height($elem.height());
    }
});

It basically loops through all the elements on the page and finds the top-most element beneath the cursor.

Is there maybe some way I could use a quad-tree or something and segment the page so the loop runs faster?

Community
  • 1
  • 1
mpen
  • 272,448
  • 266
  • 850
  • 1,236
  • `$(this).closest('body>*')` should give you the top-most ancestor of `this`, is this what you want? or is your layout such that elements are not always inside their ancestor? – tobyodavies Jan 17 '11 at 07:55
  • 2
    Is there any reason that you can't just use `e.currentTarget` instead of looping to find the element? – Guffa Jan 17 '11 at 08:01
  • @toby: It's for a Chrome extension. Could be run on any page. Don't know what the markup will look like. By "top-most" I mean has the highest z-index... there could be less deeply nested elements that are styled to appear above others. – mpen Jan 17 '11 at 08:02
  • @Guffa: Yes, the `currentTarget` will always be the `$div` because I'm moving it overtop of the element to highlight it, but then it steals all the mouse events. – mpen Jan 17 '11 at 08:03
  • 1
    The biggest bottleneck isn't the looping part, it's computing the offset and dimensions. Focus your attention there. One small optimisation is to not bother calculating the width and height if the offset is greater than the current mouse position. – David Tang Jan 17 '11 at 08:41
  • @Ralph - you can use the `event.target`, which is `this` in a jQuery event handler. See my answer. – gblazex Jan 17 '11 at 08:53
  • i think in this case you would save a lot of rendering time using pubsub events... https://github.com/phiggins42/bloody-jquery-plugins/blob/master/pubsub.js – meo Jan 17 '11 at 09:45
  • @meo: Err...there isn't much of a description on that page, but it looks completely unrelated. – mpen Jan 23 '11 at 01:17
  • possible duplicate of [jQuery: Highlight element under mouse cursor?](http://stackoverflow.com/questions/4698259/jquery-highlight-element-under-mouse-cursor) – kapa Sep 18 '12 at 10:35

2 Answers2

18

Is there maybe some way I could use a quad-tree or something and segment the page so the loop runs faster?

Just step back a bit, realize how small the problem is, and that the harder your try the more complicated answer you will use.

Now what you need to do is to create 4 elements for the highlighting. They will form an empty square, and so your mouse events are free to fire. This is similar to this overlay example I've made.

The difference is that you only need the four elements (no resize markers), and that the size and position of the 4 boxes are a bit different (to mimick the red border). Then you can use event.target in your event handler, because it gets the real topmost element by default.

Another approach is to hide the exra element, get elementFromPoint, calculate then put it back.

They're faster than light, I can tell you. Even Einstein would agree :)

1.) elementFromPoint overlay/borders - [Demo1] FF needs v3.0+

var box = $("<div class='outer' />").css({
  display: "none", position: "absolute", 
  zIndex: 65000, background:"rgba(255, 0, 0, .3)"
}).appendTo("body");

var mouseX, mouseY, target, lastTarget;

// in case you need to support older browsers use a requestAnimationFrame polyfill
// e.g: https://gist.github.com/paulirish/1579671
window.requestAnimationFrame(function frame() {
  window.requestAnimationFrame(frame);
    if (target && target.className === "outer") {
        box.hide();
        target = document.elementFromPoint(mouseX, mouseY);
    }
    box.show();   

    if (target === lastTarget) return;

    lastTarget = target;
    var $target = $(target);
    var offset = $target.offset();
    box.css({
        width:  $target.outerWidth()  - 1, 
        height: $target.outerHeight() - 1, 
        left:   offset.left, 
        top:    offset.top 
    });
});

$("body").mousemove(function (e) {
    mouseX = e.clientX;
    mouseY = e.clientY;
    target = e.target;
});

2.) mouseover borders - [Demo2]

var box = new Overlay();

$("body").mouseover(function(e){
  var el = $(e.target);
  var offset = el.offset();
  box.render(el.outerWidth(), el.outerHeight(), offset.left, offset.top);
});​

/**
 * This object encapsulates the elements and actions of the overlay.
 */
function Overlay(width, height, left, top) {

    this.width = this.height = this.left = this.top = 0;

    // outer parent
    var outer = $("<div class='outer' />").appendTo("body");

    // red lines (boxes)
    var topbox    = $("<div />").css("height", 1).appendTo(outer);
    var bottombox = $("<div />").css("height", 1).appendTo(outer);  
    var leftbox   = $("<div />").css("width",  1).appendTo(outer);
    var rightbox  = $("<div />").css("width",  1).appendTo(outer);

    // don't count it as a real element
    outer.mouseover(function(){ 
        outer.hide(); 
    });    

    /**
     * Public interface
     */

    this.resize = function resize(width, height, left, top) {
      if (width != null)
        this.width = width;
      if (height != null)
        this.height = height;
      if (left != null)
        this.left = left;
      if (top != null)
        this.top = top;      
    };

    this.show = function show() {
       outer.show();
    };

    this.hide = function hide() {
       outer.hide();
    };     

    this.render = function render(width, height, left, top) {

        this.resize(width, height, left, top);

        topbox.css({
          top:   this.top,
          left:  this.left,
          width: this.width
        });
        bottombox.css({
          top:   this.top + this.height - 1,
          left:  this.left,
          width: this.width
        });
        leftbox.css({
          top:    this.top, 
          left:   this.left, 
          height: this.height
        });
        rightbox.css({
          top:    this.top, 
          left:   this.left + this.width - 1, 
          height: this.height  
        });

        this.show();
    };      

    // initial rendering [optional]
    // this.render(width, height, left, top);
}
gblazex
  • 49,155
  • 12
  • 98
  • 91
  • That's what someone suggested in the other thread too. I actually wanted a semi-transparent red rectangle overlayed... must have changed the styling before I posted this. But anyway, wouldn't this glitch up as you mouse over the borders still? – mpen Jan 17 '11 at 17:50
  • Just remove/hide the empty square `onmouseover`, then the next mouse movement triggers the correct element. It will be unnoticable for the user. Let me do a demo for you... – gblazex Jan 17 '11 at 18:06
  • I've updated the first version to support semi-transparent red overlay. Now it's exactly how you've wanted. – gblazex Jan 18 '11 at 10:15
  • Didn't know there was an `elementFromPoint` function... that makes things a lot easier. Looks like this works pretty well, thank you :) – mpen Jan 23 '11 at 01:20
  • 1
    What's the + before `new` do though? Never seen that before. – mpen Jan 23 '11 at 18:42
  • 2
    @Mark, I was curious too: [What does the plus sign do in 'return +new date'?](http://stackoverflow.com/questions/221539/what-does-the-plus-sign-do-in-return-new-date) – John Leehey Apr 17 '12 at 23:23
  • @John: Thanks! In the year and a bit since I posted that, I still never found out =) – mpen Apr 18 '12 at 01:54
  • Haha, I'm sorry you had to wait so long. You can find the answer in another answer by me about common shorthand notations in JS: http://stackoverflow.com/questions/3899495/is-there-a-good-js-shorthand-reference-out-there/3899587#3899587 – gblazex Apr 18 '12 at 22:33
  • `+new Date` is basically creating a new Date object and then converts it to a number which yields a timestamp in *ms*. It is a short version for `(new Date()).getTime()` – gblazex Apr 18 '12 at 22:34
  • 1
    Generally speaking the **prefix operator `+` converts to number**. Check `alert(typeof "52")` vs `alert(typeof +"52")` – gblazex Apr 18 '12 at 22:38
1

First off, i don't think doing $('body *').live is a very good idea, it seems very expensive (think about the kind of calculation the browser has to do every time you move your mouse)

That said, here is an optimized version that ignores that aspect

var $div = $('<div>')
    .css({ 'border': '1px solid red', 'position': 'absolute', 'z-index': '65535' })
    .appendTo('body');

$('body *').live('mousemove', function(e) {
    var topElement = null;
    var $bodyStar = $('body *');
    for(var i=0,elem;elem=$bodyStar[i];i++) {
        if(elem == $div[0]) continue;
        var $elem = $(elem);
        var pos = $elem.offset();
        var width = $elem.width();
        var height = $elem.height();
        if(e.pageX > pos.left && e.pageY > pos.top && e.pageX < (pos.left + width) && e.pageY < (pos.top + height)) {
            var zIndex = document.defaultView.getComputedStyle(this, null).getPropertyValue('z-index');
            if(zIndex == 'auto') zIndex = $elem.parents().length;
            if(topElement == null || zIndex > topElement.zIndex) {
                topElement = {
                    'node': $elem,
                    'zIndex': zIndex
                };
            }

        }
    }
    if(topElement != null) {
        var $elem = topElement.node;
        $div.offset($elem.offset()).width($elem.width()).height($elem.height());
    }
});

For future reference, never use jQuerys looping mechanisms if you need performance. They are all build around function calls for every iteration, which is very slow compared to a normal loop, since the call stack initiation that happens when you do a function call is a huge overhead for most iteration operations you need to do.

Code updated to fix errors, and allow for dynamically inserted elements.

Martin Jespersen
  • 25,743
  • 8
  • 56
  • 68
  • First of all, it's better to use `addClass()` than `.css()`. Second, may I ask when will your `for` loop end looping? – Reigel Gallarde Jan 17 '11 at 08:10
  • 2
    It will end when $bodystar[i] retruns undefined, which it will do when i reaches the length of the array. It is the most efficient way to loop over an array you know doesn't contain values that will evaluate to false – Martin Jespersen Jan 17 '11 at 08:11
  • actually using while(i--) is faster but thats a whole different ballgame :) – Martin Jespersen Jan 17 '11 at 08:12
  • Pretty sure `$bodyStar[i]` returns the DOM element, not the jQuery object. `$('body *').live()` lets all the events bubble up to the top and provides only *one* handler for everything rather than attaching an event to every single event on the page... I'm not convinced that would be any cheaper, *plus*, it won't work newly created elements, like I want. – mpen Jan 17 '11 at 08:15
  • 1
    Also, I don't *think* pulling `bodyStar` out like that helps any. The loop condition *might* be a slight improvement over `.each` (if implemented correctly), but... ultimately we're still looping over every element on the page, and I think that's the killer. Maybe caching some of the computed properties would help.... – mpen Jan 17 '11 at 08:20
  • 1
    The loop condition is a huge improvement, over each - run some test and you will see :) .each/filter/map etc possible the single slowest aspect of jquery. Do some testing on the subject and you will see, i have done tons of performancetest in browsers from ie7 and up, including chrome,safari,opera &firefox and the difference between using regular loops and function based loops is simply staggering... but hey, you can always disregard the advice, it is free after all :) – Martin Jespersen Jan 17 '11 at 08:26
  • This answer provides some good advice that is unfortunately misunderstood. Counter-voting the downvote. Although I think you mean to use `continue` instead of `break`. – David Tang Jan 17 '11 at 08:34
  • @Martin: Well I did try running it to see if it was an actual improvement, but it didn't do anything. Oh.. I see you've made some edits to fix it. I'll try it again later, thanks. (+1) – mpen Jan 17 '11 at 17:55
  • It's dog slow even on my fast computer with Chrome 8 XD Sorry dude `algorithm > minor tweaking` – gblazex Jan 17 '11 at 18:36
  • @galambalaz: you are probably right, i never really thought of my solution as something that would make the entire implementation work. as i read the question, it was about optimizing the loop, so thats what i did, whether or not it will make the entire thing performant enough was always doubtfull - fullpage delegation of mousemove will never work imo, which is what i was trying to say with the very first sentance of my answer. – Martin Jespersen Jan 17 '11 at 21:24
  • @Martin - Why wouldn't a full delegation work? The question was about *"it runs too slow."*. And the reason for that is a slow algorithm. Tweaking that won't help much, one has to rethink the fundamentals to achive greater performance :) – gblazex Jan 17 '11 at 22:05
  • Mousemove is the most cpu intensive event in any browser, nothing else comes close. Pair that up with dom traversal (which delegation is all about) and you have the all time cpu-killer-combo. I cannot count the amount of projects i ahve been on where people have wanted this and failed to make a useable implementation. The closest i ahve ever come of doing it myself, was when i coded my own dumbed-down-only-able-to-delegate-on-class-and-tagname super-optimized browser native delegator method. Using sizzle and jQuery will just never work, it is way to heavy. – Martin Jespersen Jan 17 '11 at 22:12
  • All it takes is some careful design. You can see `mouseover` used in my second answer. It's crazy fast. As for `mousemove` my first answer shows a way to delay action for a small amount of time (e.g. 25ms) so that it's much **less** CPU intensive, with still superb UX. – gblazex Jan 18 '11 at 09:56