有效地获得元素的可见区域坐标

时间:2014-11-26 10:12:42

标签: javascript jquery html css visibility

StackOverflow是loaded with questions,关于如何检查视口中是否真的可见元素,但它们都寻求布尔答案。我有兴趣获得元素的实际可见区域。

function getVisibleAreas(e) {
    ...
    return rectangleSet;
}

更正式地说 - 元素的可见区域是CSS坐标中的一组(最好是非重叠的)矩形,elementFromPoint(x, y)将返回元素(如果点)(x ,y)包含在集合中的(至少)一个矩形中。

在所有DOM元素(包括iframe)上调用此函数的结果应该是一组非重叠区域集,其中union是整个视口区域。

我的目标是创建某种视口“转储”数据结构,它可以有效地为视口中的给定点返回单个元素,反之亦然 - 对于转储中的给定元素,它将返回一组可见区域。 (数据结构将传递给远程客户端应用程序,因此当我需要查询视口结构时,我不一定能访问实际文档。)

实施要求:

  • 显然,实现应该考虑元素的hidden状态,z-index,标题&页脚等
  • 我正在寻找适用于所有常用浏览器的实现,尤其是移动设备 - Android的Chrome和iOS的Safari。
  • 最好不要使用外部库。

    当然,我可能天真并且为视口中的每个离散点调用elementFromPoint,但是性能是至关重要的,因为我遍历所有元素,并且会经常这样做。

    请指导我如何实现这一目标。

    免责声明:我对网络编程概念很不错,所以我可能使用了错误的技术术语。

    进度:

    我想出了一个实现。算法非常简单:

    1. 迭代所有元素,并将其垂直/水平线添加到坐标贴图(如果坐标位于视口内)。
    2. 为每个“矩形”中心位置调用`document.elementFromPoint`。矩形是来自步骤1的地图中的两个连续垂直坐标和两个连续水平坐标之间的区域。

    这会产生一组区域/矩形,每个区域都指向一个元素。

    我的实施问题是:

    1. 复杂页面效率低下(对于真正的大屏幕和gmail收件箱,最多可能需要2-4分钟)。
    2. 它为每个元素生成大量的矩形,这使得通过网络进行字符串化和发送效率低下,并且使用起来也不方便(我希望最终得到一个具有少量的集合每个元素尽可能的矩形)。

    据我所知,elementFromPoint调用是一个需要花费大量时间并使我的算法相对无用的调用...

    有人能提出更好的方法吗?

    这是我的实施:

    function AreaPortion(l, t, r, b, currentDoc) {
        if (!currentDoc) currentDoc = document;
        this._x = l;
        this._y = t;
        this._r = r;
        this._b = b;
        this._w = r - l;
        this._h = b - t;
    
        center = this.getCenter();
        this._elem = currentDoc.elementFromPoint(center[0], center[1]);
    }
    
    AreaPortion.prototype = {
        getName: function() {
            return "[x:" + this._x + ",y:" + this._y + ",w:" + this._w + ",h:" + this._h + "]";
        },
    
        getCenter: function() {
            return [this._x + (this._w / 2), this._y + (this._h / 2)];
        }
    }
    
    function getViewport() {
        var viewPortWidth;
        var viewPortHeight;
    
        // IE6 in standards compliant mode (i.e. with a valid doctype as the first line in the document)
        if (
                typeof document.documentElement != 'undefined' &&
                typeof document.documentElement.clientWidth != 'undefined' &&
                document.documentElement.clientWidth != 0) {
            viewPortWidth = document.documentElement.clientWidth,
            viewPortHeight = document.documentElement.clientHeight
        }
    
        // the more standards compliant browsers (mozilla/netscape/opera/IE7) use window.innerWidth and window.innerHeight
        else if (typeof window.innerWidth != 'undefined') {
            viewPortWidth = window.innerWidth,
            viewPortHeight = window.innerHeight
        }
    
        // older versions of IE
        else {
            viewPortWidth = document.getElementsByTagName('body')[0].clientWidth,
            viewPortHeight = document.getElementsByTagName('body')[0].clientHeight
        }
    
        return [viewPortWidth, viewPortHeight];
    }
    
    function getLines() {
        var onScreen = [];
        var viewPort = getViewport();
        // TODO: header & footer
        var all = document.getElementsByTagName("*");
    
        var vert = {};
        var horz = {};
    
        vert["0"] = 0;
        vert["" + viewPort[1]] = viewPort[1];
        horz["0"] = 0;
        horz["" + viewPort[0]] = viewPort[0];
        for (i = 0 ; i < all.length ; i++) {
            var e = all[i];
            // TODO: Get all client rectangles
            var rect = e.getBoundingClientRect();
            if (rect.width < 1 && rect.height < 1) continue;
    
            var left = Math.floor(rect.left);
            var top = Math.floor(rect.top);
            var right = Math.floor(rect.right);
            var bottom = Math.floor(rect.bottom);
    
            if (top > 0 && top < viewPort[1]) {
                vert["" + top] = top;
            }
            if (bottom > 0 && bottom < viewPort[1]) {
                vert["" + bottom] = bottom;
            }
            if (right > 0 && right < viewPort[0]) {
                horz["" + right] = right;
            }
            if (left > 0 && left < viewPort[0]) {
                horz["" + left] = left;
            }
        }
    
        hCoords = [];
        vCoords = [];
        //TODO: 
        for (var v in vert) {
            vCoords.push(vert[v]);
        }
    
        for (var h in horz) {
            hCoords.push(horz[h]);
        }
    
        return [hCoords, vCoords];
    }
    
    function getAreaPortions() {
        var portions = {}
        var lines = getLines();
    
        var hCoords = lines[0];
        var vCoords = lines[1];
    
        for (i = 1 ; i < hCoords.length ; i++) {
            for (j = 1 ; j < vCoords.length ; j++) {
                var portion = new AreaPortion(hCoords[i - 1], vCoords[j - 1], hCoords[i], vCoords[j]);
                portions[portion.getName()] = portion;
            }
        }
    
        return portions;
    }
    
  • 3 个答案:

    答案 0 :(得分:1)

    尝试

    var res = [];
    $("body *").each(function (i, el) {
        if ((el.getBoundingClientRect().bottom <= window.innerHeight 
            || el.getBoundingClientRect().top <= window.innerHeight)
            && el.getBoundingClientRect().right <= window.innerWidth) {
                res.push([el.tagName.toLowerCase(), el.getBoundingClientRect()]);
        };
    });
    

    jsfiddle http://jsfiddle.net/guest271314/ueum30g5/

    请参阅Element.getBoundingClientRect()

    &#13;
    &#13;
    $.each(new Array(180), function () {
        $("body").append(
        $("<img>"))
    });
    
    $.each(new Array(180), function () {
    $("body").append(
    $("<img>"))
    });
    
    var res = [];
    $("body *").each(function (i, el) {
    if ((el.getBoundingClientRect().bottom <= window.innerHeight || el.getBoundingClientRect().top <= window.innerHeight)
        && el.getBoundingClientRect().right <= window.innerWidth) {
        res.push(
        [el.tagName.toLowerCase(),
        el.getBoundingClientRect()]);
        $(el).css(
            "outline", "0.15em solid red");
        $("body").append(JSON.stringify(res, null, 4));
        console.log(res)
    };
    });
    &#13;
    body {
        width : 1000px;
        height : 1000px;
    }
    img {
        width : 50px;
        height : 50px;
        background : navy;
    }
    &#13;
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
    &#13;
    &#13;
    &#13;

    答案 1 :(得分:1)

    我不知道性能是否足够(特别是在移动设备上),并且结果不是您所要求的矩形集,但您是否考虑使用位图来存储结果?

    注意某些元素可能具有3d css变换(例如,倾斜,旋转),某些元素可能具有边框半径,并且某些元素可能具有不可见背景 - 如果您想要包含这些特征以及“像素中的元素”函数然后矩形集无法帮助你 - 但位图可以容纳所有的视觉特征。

    生成位图的解决方案相当简单(我想......没有经过测试):

    1. 创建一个可见屏幕大小的画布。
    2. 递归迭代所有元素,按z顺序排序,忽略隐藏
    3. 对于每个元素在画布中绘制一个矩形,矩形的颜色是元素的标识符(例如,可以是增量计数器)。如果需要,可以根据元素的视觉特征(倾斜,旋转,边框半径等)修改矩形。
    4. 将画布保存为无损格式,例如png not jpg
    5. 将位图作为元素的元数据发送到屏幕上
    6. 要查询哪个元素位于点(x,y),您可以在像素(x,y)处检查位图的颜色,颜色将告诉您元素是什么。

    答案 2 :(得分:0)

    如果你可以放弃IE,这里有一个简单的:

    function getElementVisibleRect(el) {
      return new Promise((resolve, reject) => {
        el.style.overflow = "hidden";
        requestAnimationFrame((timeStamp) => {
          var br = el.getBoundingClientRect();
          el.style.overflow = "";
          resolve(br);
        });
      });
    }
    

    即便如此,Promise也很容易被填充,而且requestAnimationFrame()可以追溯到IE 8。到2016年,你唯一不愿意在老IE上给任何可怜的灵魂提供一个清晰易懂的体验。