3

I have a page and on it are some widgets. There are 2 columns. Widgets will auto-resize themselves if there is no widget to their left (or right).

I'm using document.elementFromPoint (after getting the widget's offset) to detect if there is a widget next to it.

There is a problem with document.elementFromPoint though.

If the specified point is outside the visible bounds of the document... the result is null.

It only works if the element is in the viewport. How can I detect what element is at a certain position even if it's not visible?

EDIT: Here's an example:

Crappy Mockup

The red line is the viewport, and the grey box (with the red writing) is Widget D's original position (the other Widget D is where it is when the page is done loading). When the page loads, it checks Widgets B, C, and D. It gets their offsets, and then uses document.elementFromPoint to see if Widget A is to their right. If Widget A is to their right, the Widget is half width, if not it's full width.

Widget D is full width because document.elementFromPoint returns null because it's outside the viewport. How can I detect that Widget A is to the right of Widget D even though it's outside the viewport?

gen_Eric
  • 223,194
  • 41
  • 299
  • 337
  • If your code is not often executed, you can move the `html` or `body` element to the left/right/top/bottom, so that the element is temporary within the viewport. Another option: If you know which elements have to be compared, use `$('selector').offset()` (`.top` and `.left`), plus `.height()`/`.outerHeight()` and `.width()`/`.outerWidth()` to calculate the relative position of the elements. – Rob W Jan 25 '12 at 23:02
  • 1
    @RobW: I tried to explain my issue with a crappy mockup. – gen_Eric Jan 25 '12 at 23:20
  • So, you know which elements have to be resized relative to each other? – Rob W Jan 25 '12 at 23:23
  • @RobW: Basically. This page is customizable. When it loads, each widget checks whether it should be half width or full width based on what is to its right. – gen_Eric Jan 25 '12 at 23:26
  • 2
    By saying "each widget", you imply that there's a fixed number of elements which have to be checked. You can loop through each element, and combine the methods from my first comment to check whether there's an element right/left of the current element. – Rob W Jan 25 '12 at 23:29
  • @RobW: That's my problem. I can't figure out how to check whether there's an element to right/left if it's outside the viewport (`document.elementFromPoint` returns `null`). Think you could whip up a quick answer to elaborate a bit on your suggestion? – gen_Eric Jan 25 '12 at 23:31

1 Answers1

4

The following code saves the boundaries of the elements in two lists: x and y positions. Then, you can use a for-loop to start at a given offset, and walk through the list. At the bottom of the answer, an implementation is shown.

// These arrays are going to contain many empty elements ;)
var xAxis = [];
var yAxis = [];

// Our storage. Each element looks like: [$element, top, right, bottom, left]
// Beware of cross-references! Deleted elements cannot be GCed if they're here!
var allElements = [];

/*
 * @param   $elem jQuery object
 * @returns Object containing the jQuery object and the floored border offsets
 */
function getBorders($elem) {
    $elem = $elem instanceof jQuery ? $elem.first() : $($elem);
    var offset = $elem.offset();           // Properties: top, left

    var width = $elem.outerWidth();        // Includes padding, border.
    var leftBorder = offset.left;          // Position of the left border ->|
    var rightBorder = leftBorder + width;  // Position of the right border  |<-

    var height = $elem.outerHeight();      // Includes padding, border
    var topBorder = offset.top;            // Position of the top border    _v_
    var bottomBorder = offset.top + height;// Position of the bottom border  ^

    // Turn all numbers in integers, so that they can be used as indexes
    //   for arrays
    // See also: http://stackoverflow.com/a/8112802/938089
    return {
        $elem: $elem,
        top: ~~topBorder,
        right: ~~rightBorder,
        bottom: ~~bottomBorder,
        left: ~~leftBorder
    };
}

$('.widget').each(function() {
    var $this = $(this);

    var info = getBorders($this);
    allElements.push(info);
    // The positions are floored, so that they can be used as a quick reference
    xAxis[info.left] = xAxis[info.right] = info;
    yAxis[info.top] = yAxis[info.bottom] = info;
});

The created list can now be used to check whether there's any element which collides. For example, consider the following function:

function doesItCollide($elem) {
    // WeakMaps are useful. For compatibility, I don't use them.
    var info$elem = getBorders($elem);
    // info$elem properties: $elem, top, right, bottom, left
    // Y-oriented
    for (var y=info$elem.top; y<yAxis.length; y++) {
        var candidate = yAxis[y];
        // Not undefined, and not iself
        if (candidate && candidate.$elem[0] !== info$elem.$elem[0]) {
            // Check whether the element is inside the range
            if (candidate.top >= info$elem.bottom &&
                candidate.bottom >= info$elem.bottom) continue;
            // It's here, so the top / bottom are possibly in the same range
            // Check whether the x-positions are also colliding
            if (candidate.left >= info$elem.left && /* Left within range? */
                candidate.left <= info$elem.right || /* OR */
                candidate.right <= info$elem.right && /* Right within range? */
                candidate.right >= info$elem.left) {
                return candidate;
            }
        }
    }
    return null;
}

Example (Demo: http://jsfiddle.net/LtJsM/):

$('.widget').each(function() {
    var $this = $(this);
    var overlapping = doesItCollide($this);
    if (overlapping) {
        alert('Current element:  '+this.className +
           '\nColliding element: ' + overlapping.$elem[0].className);
    }
});
Rob W
  • 341,306
  • 83
  • 791
  • 678
  • Thanks for this, I'm gonna need to tweak it a bit, but an array of element positions is a great idea :-) – gen_Eric Jan 26 '12 at 16:32