19

Possible Duplicate:
How to make this JavaScript much faster?

I'm trying to create an "element picker" in jQuery, like Firebug has. Basically, I want to highlight the element underneath the user's mouse. Here's what I've got so far, but it isn't working very well:

$('*').mouseover(function (event) {
    var $this = $(this);
    $div.offset($this.offset()).width($this.width()).height($this.height());
    return false;
});


var $div = $('<div>')
    .css({ 'background-color': 'rgba(255,0,0,.5)', 'position': 'absolute', 'z-index': '65535' })
    .appendTo('body');

Basically, I'm injecting a div into the DOM that has a semi-transparent background. Then I listen for the mouseover event on every element, then move the div so that it covers that element.

Right now, this just makes the whole page go red as soon as you move your mouse over the page. How can I get this to work nicer?

Edit: Pretty sure the problem is that as soon as my mouse touches the page, the body gets selected, and then as I move my mouse around, none of the moments get passed through the highligher because its overtop of everything.


Firebug

Digging through Firebug source code, I found this:

drawBoxModel: function(el)
{
    // avoid error when the element is not attached a document
    if (!el || !el.parentNode)
        return;

    var box = Firebug.browser.getElementBox(el);

    var windowSize = Firebug.browser.getWindowSize();
    var scrollPosition = Firebug.browser.getWindowScrollPosition();

    // element may be occluded by the chrome, when in frame mode
    var offsetHeight = Firebug.chrome.type == "frame" ? FirebugChrome.height : 0;

    // if element box is not inside the viewport, don't draw the box model
    if (box.top > scrollPosition.top + windowSize.height - offsetHeight ||
        box.left > scrollPosition.left + windowSize.width ||
        scrollPosition.top > box.top + box.height ||
        scrollPosition.left > box.left + box.width )
        return;

    var top = box.top;
    var left = box.left;
    var height = box.height;
    var width = box.width;

    var margin = Firebug.browser.getMeasurementBox(el, "margin");
    var padding = Firebug.browser.getMeasurementBox(el, "padding");
    var border = Firebug.browser.getMeasurementBox(el, "border");

    boxModelStyle.top = top - margin.top + "px";
    boxModelStyle.left = left - margin.left + "px";
    boxModelStyle.height = height + margin.top + margin.bottom + "px";
    boxModelStyle.width = width + margin.left + margin.right + "px";

    boxBorderStyle.top = margin.top + "px";
    boxBorderStyle.left = margin.left + "px";
    boxBorderStyle.height = height + "px";
    boxBorderStyle.width = width + "px";

    boxPaddingStyle.top = margin.top + border.top + "px";
    boxPaddingStyle.left = margin.left + border.left + "px";
    boxPaddingStyle.height = height - border.top - border.bottom + "px";
    boxPaddingStyle.width = width - border.left - border.right + "px";

    boxContentStyle.top = margin.top + border.top + padding.top + "px";
    boxContentStyle.left = margin.left + border.left + padding.left + "px";
    boxContentStyle.height = height - border.top - padding.top - padding.bottom - border.bottom + "px";
    boxContentStyle.width = width - border.left - padding.left - padding.right - border.right + "px";

    if (!boxModelVisible) this.showBoxModel();
},

hideBoxModel: function()
{
    if (!boxModelVisible) return;

    offlineFragment.appendChild(boxModel);
    boxModelVisible = false;
},

showBoxModel: function()
{
    if (boxModelVisible) return;

    if (outlineVisible) this.hideOutline();

    Firebug.browser.document.getElementsByTagName("body")[0].appendChild(boxModel);
    boxModelVisible = true;
}

Looks like they're using a standard div + css to draw it..... just have to figure out how they're handling the events now... (this file is 28K lines long)

There's also this snippet, which I guess retrieves the appropriate object.... although I can't figure out how. They're looking for a class "objectLink-element"... and I have no idea what this "repObject" is.

onMouseMove: function(event)
{
    var target = event.srcElement || event.target;

    var object = getAncestorByClass(target, "objectLink-element");
    object = object ? object.repObject : null;

    if(object && instanceOf(object, "Element") && object.nodeType == 1)
    {
        if(object != lastHighlightedObject)
        {
            Firebug.Inspector.drawBoxModel(object);
            object = lastHighlightedObject;
        }
    }
    else
        Firebug.Inspector.hideBoxModel();

},

I'm thinking that maybe when the mousemove or mouseover event fires for the highlighter node I can somehow pass it along instead? Maybe to node it's covering...?


If I position an invisible element above every element with a higher z-index than my highlighter, but give my highlighter a higher z-index than the actual elements... theoretically, it should work. The invisible elements will trip the mouse event, but the highlight effect will still look like its overtop of the actual elements.

This means I've just doubled the DOM elements though, and the positioning has to be correct. Unless maybe I only "lift" the elements that the highlighter is currently covering?? But that could still be every element >.< Someone help me!

Community
  • 1
  • 1
mpen
  • 272,448
  • 266
  • 850
  • 1,236
  • 3
    There's a nasty catch-22 when trying to do stuff like this. Basically, as soon as you draw something under the mouse, as a result of the mouse hovering over an element, the mouse actually leaves the original element and is now hovering over the _new_ one. – Matt Ball Jan 15 '11 at 06:16
  • @Matt: Well how do we get around this then? Does Firebug not manipulate the DOM to render those boxes, or does it have its own rendering engine? – mpen Jan 15 '11 at 06:31
  • 1
    Firebug highlights elements in a web page when you mouse over them in Firebug - which is **separate**. Or is that tired-think? When you use the "click to inspect" it only outlines the elements, which may be the crux of the matter - Firebug could draw 4 separate elements, one for each edge of the box, in which case, the catch-22 I mentioned isn't an issue at all. – Matt Ball Jan 15 '11 at 06:35
  • 2
    +1 for digging through the Firebug source code - I was going to suggest this. – Matt Ball Jan 16 '11 at 04:48

5 Answers5

26

All these answers are too complicated... Simple solution:

Javascript:

prevElement = null;
document.addEventListener('mousemove',
    function(e){
        var elem = e.target || e.srcElement;
        if (prevElement!= null) {prevElement.classList.remove("mouseOn");}
        elem.classList.add("mouseOn");
        prevElement = elem;
    },true);

Css:

.mouseOn{
  background-color: #bcd5eb !important;
  outline: 2px solid #5166bb !important;
}
mrBorna
  • 1,757
  • 16
  • 16
3

To amend David's answer so that it properly reverts the background color afterwords...

$('body *').live('mouseover mouseout', function(event) {
    if (event.type == 'mouseover') {
        $(this).data('bgcolor', $(this).css('background-color'));
        $(this).css('background-color','rgba(255,0,0,.5)');
    } else {
        $(this).css('background-color', $(this).data('bgcolor'));
    }
    return false;
});
mpen
  • 272,448
  • 266
  • 850
  • 1,236
2

Can I suggest an alternative approach?

How about simply assigning a background-color to all child elements of the page, and then, on hover(), adjust the background-color of that element to increase its contrast?

$('html').children().css('background-color','rgba(0,0,0,0.2)');
$('body').children().hover(
    function(){
        $(this).css('background-color','#fff');
    },
    function(){
        $(this).css('background-color','rgba(0,0,0,0.2)');
    });

JS Fiddle demo.

David Thomas
  • 249,100
  • 51
  • 377
  • 410
  • It's possible. I might have to go with this approach if I can't find a solution, but this is *really* bugging me because I know there has to be a way to do it. – mpen Jan 16 '11 at 01:47
1

The reason that the whole page goes red as soon as you mouse over is that your code is matching a mouseover event for the body element. You can stop that from happening by only selecting children of body.

$('body *').bind( //...

You'll hit performance problems on a page with a large number of elements, though since bind will attach a listener for every single matched element. Try taking a look into jQuery's event delegation api (.delegate(), http://api.jquery.com/delegate/) which allows you to listen for events propagating up to a root element and works for mouse events as well.

andrewle
  • 1,731
  • 13
  • 11
  • That doesn't really fix the problem (using `body *`), that just moves the problem to a slightly deeper element. See Matt's comment. Not too worried about performance just yet, I'll take anything that works. – mpen Jan 15 '11 at 06:30
  • Also, the reason I'm not using live() or delegate() is because then future elements would be included... namely, I don't want it to start selecting itself (which is why I appended it after I bound the event). – mpen Jan 15 '11 at 06:32
  • Have you tried checking the event object passed to your callback for the currentTarget? If the currentTarget is your highlighting div, then you could just do nothing. – andrewle Jan 15 '11 at 06:40
  • Won't matter if I do nothing, it still prevents the *real* element from receiving the event. (But yes, that would allow my to use `live()`... as long as I could fix the other issues) – mpen Jan 16 '11 at 00:46
1

► This is a bit less fidgety, but it can't detect moving from a parent element to one of its children without first moving the mouse completely off said parent.

var $div = $('<div id="highlighter">').css({
    'background-color': 'rgba(255,0,0,.5)',
    'position': 'absolute',
    'z-index': '65535'
}).hide().prependTo('body');

var $highlit;

$('*').live('mousemove', function(event) {
    if (this.nodeName === 'HTML' || this.nodeName === 'BODY')
    {
        $div.hide();
        return false;
    }
    var $this = this.id === 'highligher' ? $highlit : $(this),

        x = event.pageX,
        y = event.pageY,

        width = $this.width(),
        height = $this.height(),
        offset = $this.offset(),

        minX = offset.left,
        minY = offset.top,
        maxX = minX + width,
        maxY = minY + height;

    if (this.id === 'highlighter')
    {
        if (minX <= x && x <= maxX
            && minY <= y && y <= maxY)
        {
            // nada
        }
        else
        {
            $div.hide();
        }
    }
    else
    {
        $highlit = $this;
        $div.offset(offset).width($this.width()).height($this.height()).show();
    }
    return false;
});

Hopefully that helps get the ball rolling. You could probably tweak what I've written to use document.elementFromPoint(x, y) to check whether or not the mouse has moved into a child element of the element currently highlighted. I'm not awake enough to figure this out right now.

Alternately, if outlining is as good as highlighting to you, you could try the approach I mentioned in my comment to your original question. Draw 4 divs around the currently hovered element* - that's one div for each edge of the outline box. No more drawing elements between you and your actual content!


*I'd bet that document.elementFromPoint(x, y) will make getting the element under your mouse much easier here.

Matt Ball
  • 354,903
  • 100
  • 647
  • 710