问题 通过scrollOffset查找最接近的锚点href


我有一个 UIWebView 完整加载HTML页面。该 UIWebView 有一个320 x 480的框架并水平滚动。我可以获得用户当前所在的当前偏移量。我想使用XY偏移找到最近的锚点,这样我就可以“跳转到”锚定位置。这是可能吗?有人能指点我使用Javascript中的资源吗?

<a id="p-1">Text Text Text Text Text Text Text Text Text<a id="p-2">Text Text Text Text Text Text Text Text Text ... 

更新

我超级难过的JS代码:

function posForElement(e)
{
    var totalOffsetY = 0;

    do
    {
        totalOffsetY += e.offsetTop;
    } while(e = e.offsetParent)

    return totalOffsetY;
}

function getClosestAnchor(locationX, locationY)
{
    var a = document.getElementsByTagName('a');

    var currentAnchor;
    for (var idx = 0; idx < a.length; ++idx)
    {
        if(a[idx].getAttribute('id') && a[idx+1])
        {
            if(posForElement(a[idx]) <= locationX && locationX <= posForElement(a[idx+1])) 
            {
                currentAnchor = a[idx];
                break;
            }
            else
            {
                currentAnchor = a[0];
            }
        }
    }

    return currentAnchor.getAttribute('id');
}

Objective-C的

float pageOffset = 320.0f;

NSString *path = [[NSBundle mainBundle] pathForResource:@"GetAnchorPos" ofType:@"js"];
NSString *jsCode = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
[webView stringByEvaluatingJavaScriptFromString:jsCode];

NSString *execute = [NSString stringWithFormat:@"getClosestAnchor('%f', '0')", pageOffset];
NSString *anchorID = [webView stringByEvaluatingJavaScriptFromString:execute];

12634
2018-05-17 19:53


起源

你真的想通过scrollOffset吗? - user1365010
你想要从锚的左上角或从中心或其他地方获得最近的锚吗? - Mageek
给定当前XY偏移量,我想要从文档的顶部/左侧得到最近的锚点。 - Oh Danny Boy
Danny Boy,你没有评论太多,你有没有尝试过我们的建议?这个问题出了点问题...我觉得有点震惊,你甚至找不到最有用的答案。 - Denis
我无法在iOS应用程序中工作。我已经无数次尝试并修改了每个解决方案。我现在正在尝试,以便我可以奖励最接近的解决方案。 - Oh Danny Boy


答案:


[UPDATE] 我重写了代码以匹配所有具有id的锚点,并简化了我的向量的范数的比较 sortByDistance 功能。

检查我的尝试 在jsFiddle上 (前一个是 这里 )。

javascript部分:

// findPos : courtesy of @ppk - see http://www.quirksmode.org/js/findpos.html
var findPos = function(obj) {
    var curleft = 0,
        curtop = 0;
    if (obj.offsetParent) {
        curleft = obj.offsetLeft;
        curtop = obj.offsetTop;
        while ((obj = obj.offsetParent)) {
            curleft += obj.offsetLeft;
            curtop += obj.offsetTop;
        }
    }
    return [curleft, curtop];
};

var findClosestAnchor = function (anchors) {

    var sortByDistance = function(element1, element2) {

        var pos1 = findPos( element1 ),
            pos2 = findPos( element2 );

        // vect1 & vect2 represent 2d vectors going from the top left extremity of each element to the point positionned at the scrolled offset of the window
        var vect1 = [
                window.scrollX - pos1[0],
                window.scrollY - pos1[1]
            ],
            vect2 = [
                window.scrollX - pos2[0],
                window.scrollY - pos2[1]
            ];

        // we compare the length of the vectors using only the sum of their components squared
        // no need to find the magnitude of each (this was inspired by Mageek’s answer)
        var sqDist1 = vect1[0] * vect1[0] + vect1[1] * vect1[1],
            sqDist2 = vect2[0] * vect2[0] + vect2[1] * vect2[1];

        if ( sqDist1 <  sqDist2 ) return -1;
        else if ( sqDist1 >  sqDist2 ) return 1;
        else return 0;
    };

    // Convert the nodelist to an array, then returns the first item of the elements sorted by distance
    return Array.prototype.slice.call( anchors ).sort( sortByDistance )[0];
};

当dom准备就绪时,您可以像这样检索和缓存锚点: var anchors = document.body.querySelectorAll('a[id]');

我还没有在智能手机上测试它,但我没有看到为什么它不起作用的任何原因。 这里 这就是我使用的原因 var foo = function() {}; 形式(更多的javascript模式)。

return Array.prototype.slice.call( anchors ).sort( sortByDistance )[0]; 线实际上有点棘手。

document.body.querySelectorAll('a['id']') 返回一个NodeList,其中包含当前页面正文中具有属性“id”的所有锚点。 遗憾的是,NodeList对象没有“排序”方法,也无法使用 sort Array原型的方法, 与其他一些方法一样,例如过滤器或地图 (NodeList.prototype.sort = Array.prototype.sort 本来真的很棒)。

本文 更好地解释了我可以使用的原因 Array.prototype.slice.call 把我的NodeList变成一个数组。

最后,我用了 Array.prototype.sort 方法(以及自定义 sortByDistance 函数)比较NodeList的每个元素,并且我只返回第一个项目,即最接近的项目。

要查找使用固定位置的元素的位置,可以使用此更新版本 findPos : http://www.greywyvern.com/?post=331

我的答案可能不是更有效(drdigit必须比我的更多)但我更喜欢简单而不是效率,我认为这是最容易维持的。

[再次更新]

这是一个经过大量修改的findPos版本,可与webkit css列一起使用(没有间隙):

// Also adapted from PPK - this guy is everywhere ! - check http://www.quirksmode.org/dom/getstyles.html
var getStyle = function(el,styleProp)
{
    if (el.currentStyle)
        var y = el.currentStyle[styleProp];
    else if (window.getComputedStyle)
        var y = document.defaultView.getComputedStyle(el,null).getPropertyValue(styleProp);
    return y;
}

// findPos : original by @ppk - see http://www.quirksmode.org/js/findpos.html
// made recursive and transformed to returns the corect position when css columns are used

var findPos = function( obj, childCoords ) {
   if ( typeof childCoords == 'undefined'  ) {
       childCoords = [0, 0];
   }

   var parentColumnWidth,
       parentHeight;

   var curleft, curtop;

   if( obj.offsetParent && ( parentColumnWidth = parseInt( getStyle( obj.offsetParent, '-webkit-column-width' ) ) ) ) {
       parentHeight = parseInt( getStyle( obj.offsetParent, 'height' ) );
       curtop = obj.offsetTop;
       column = Math.ceil( curtop / parentHeight );
       curleft = ( ( column - 1 ) * parentColumnWidth ) + ( obj.offsetLeft % parentColumnWidth );
       curtop %= parentHeight;
   }
   else {
       curleft = obj.offsetLeft;
       curtop = obj.offsetTop;
   }

   curleft += childCoords[0];
   curtop += childCoords[1];

   if( obj.offsetParent ) {
       var coords = findPos( obj.offsetParent, [curleft, curtop] );
       curleft = coords[0];
       curtop = coords[1];
   }
   return [curleft, curtop];
}

9
2018-05-18 06:47



你的jsfiddle在我的iPod上不起作用。 - Mageek
没有必要得到所有的指责。如果您认为某些事情不对,请指示主持人注意并与之交谈 我们 而不是在这里开始参数。 - BoltClock♦
当窗口缩小到480 x 320并且滚动时,JSFiddle不起作用。它报告的数字不正确。 - Oh Danny Boy
@Denis这适用于您提供的演示(链接在评论中的评论)。但是在我的UIWebView上,文档为2000(宽度)和480高度,每次都返回相同的锚点。值得注意的是,使用其他实现并注销每个锚点scrollTop返回一个大于文档的数字。注销文档高度会返回正确的高度。 - Oh Danny Boy
@Denis尝试使用-webkit-column-width制作此固定高度:320px和-webkit-column-gap:0并且它不起作用。它仅在文档垂直流动时才有效。 - Oh Danny Boy


我找到了一种方法来实现它,而不使用scrollOffset。这有点复杂,所以如果你有任何问题要理解它只是评论。

HTML:

<body>
<br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br />
<a />Text Text Text Text Text Text Text Text Text
<br /><br /><br /><br /><br />
<a />Text Text Text Text Text Text Text Text Text
</body>

CSS:

body
{
    height:3000px;
}

JS:

var tempY;

function getClosestAnchor(e)
{
    if((window.event?event.keyCode:e.which)!=97)return;
    var allAnchors=document.getElementsByTagName("a");
    var allDiff=[];
    for(var a=0;a<allAnchors.length;a++)allDiff[a]=margeY(allAnchors[a])-tempY;
    var smallest=allDiff[0];
    for(var a=1;a<allDiff.length;a++)
    {
        if(Math.abs(smallest)>Math.abs(allDiff[a]))
        {
            smallest=allDiff[a];
        }
    }
    window.scrollBy(0,smallest);
}

function margeY(obj)
{
    var posY=0;
    if(!obj.offsetParent)return;
    do posY+=obj.offsetTop;
    while(obj=obj.offsetParent);
    return posY;
}

function update(e)
{
    if(e.pageY)tempY=e.pageY;
    else tempY=e.clientY+(document.documentElement.scrollTop||document.body.scrollTop)-document.documentElement.clientTop;
}


window.onkeypress=getClosestAnchor;
window.onmousemove=update;

这是一个小提琴演示: http://jsfiddle.net/jswuC/

奖金: 您不必为所有锚指定id。


3
2018-05-24 20:55



感谢您抽出宝贵时间作出回应。我似乎得到了allAnchors的一个空数组,并且我试图弄清楚这是否是一个问题,因为UIWebView组件。代码很清楚,我知道它的作用。我会尝试适应并回到这里。 - Oh Danny Boy
阵列 allAnchors 是空的 ???很奇怪 !如果您希望我尝试了解原因,您可以发布所有代码。 PS:你明白要滚动到最近的锚,你必须按下 A 关键? - Mageek
allAnchors不再是NULL,但跳转仍然不起作用。 - Oh Danny Boy
@OhDannyBoy,查看修改后的内容 的jsfiddle 只改变了内容。在开始之前,例如在Chrome中,您必须单击空的白色HTML示例区域以绘制焦点/激活部分。然后在示例之间悬停并按字母A滚动到最近的锚点。 - arttronics
@Mageek,+1用于巧妙地使用距离算法解释代码的核心。另外,我不知道使用Square Root是电源性能问题,并会记住这一点。 - arttronics


唷!我完成了!

JS:

var x=0,y=0;//Here are the given X and Y, you can change them
var idClosest;//Id of the nearest anchor
var smallestIndex;
var couplesXY=[];
var allAnchors;
var html=document.getElementsByTagName("html")[0];
html.style.width="3000px";//You can change 3000, it's to make the possibility of horizontal scroll
html.style.height="3000px";//Here too

function random(min,max)
{
    var nb=min+(max+1-min)*Math.random();
    return Math.floor(nb);
}
function left(obj)//A remixed function of this site http://www.quirksmode.org/js/findpos.html
{
    if(obj.style.position=="absolute")return parseInt(obj.style.left);
    var posX=0;
    if(!obj.offsetParent)return;
    do posX+=obj.offsetLeft;
    while(obj=obj.offsetParent);
    return posX;
}
function top(obj)
{
    if(obj.style.position=="absolute")return parseInt(obj.style.top);
    var posY=0;
    if(!obj.offsetParent)return;
    do posY+=obj.offsetTop;
    while(obj=obj.offsetParent);
    return posY;
}

function generateRandomAnchors()//Just for the exemple, you can delete the function if you have already anchors
{
    for(var a=0;a<50;a++)//You can change 50
    {
        var anchor=document.createElement("a");
        anchor.style.position="absolute";
        anchor.style.width=random(0,100)+"px";//You can change 100
        anchor.style.height=random(0,100)+"px";//You can change 100
        anchor.style.left=random(0,3000-parseInt(anchor.style.width))+"px";//If you changed 3000 from
        anchor.style.top=random(0,3000-parseInt(anchor.style.height))+"px";//the top, change it here
        anchor.style.backgroundColor="black";
        anchor.id="Anchor"+a;
        document.body.appendChild(anchor);
    }
}
function getAllAnchors()
{
    allAnchors=document.getElementsByTagName("a");
    for(var a=0;a<allAnchors.length;a++)
    {
        couplesXY[a]=[];
        couplesXY[a][0]=left(allAnchors[a]);
        couplesXY[a][1]=top(allAnchors[a]);
    }
}
function findClosestAnchor()
{
    var distances=[];
    for(var a=0;a<couplesXY.length;a++)distances.push(Math.pow((x-couplesXY[a][0]),2)+Math.pow((y-couplesXY[a][1]),2));//Math formula to get the distance from A to B (http://euler.ac-versailles.fr/baseeuler/lexique/notion.jsp?id=122). I removed the square root not to slow down the calculations
    var smallest=distances[0];
    smallestIndex=0;
    for(var a=1;a<distances.length;a++)if(smallest>distances[a])
    {
        smallest=distances[a];
        smallestIndex=a;
    }
    idClosest=allAnchors[smallestIndex].id;
    alert(idClosest);
}
function jumpToIt()
{
    window.scrollTo(couplesXY[smallestIndex][0],couplesXY[smallestIndex][1]);
    allAnchors[smallestIndex].style.backgroundColor="red";//Color it to see it
}

generateRandomAnchors();
getAllAnchors();
findClosestAnchor();
jumpToIt();

小提琴: http://jsfiddle.net/W8LBs/2

PS:如果你在智能手机上打开这个小提琴,它不起作用(我不知道为什么),但如果你在智能手机上的样本中复制此代码,它可以工作(但你必须指定 <html> 和 <body> 部分)。


1
2018-05-28 20:08



我无法访问智能手机,但是 的jsfiddle 可以通过访问设置为不同的HTML Doctype 信息 上 左窗格。默认设置是 HTML5。此外,如果你改变空体部分以包含简单的html和body标签,它可能会在智能手机上工作。 - arttronics
在仔细查看代码之后,看起来你试图适应自己的使用了 findPos 我也使用了彼得 - 保罗科赫的功能,但是你做得不好。在你的 left 和 top 函数,这是不确定的 obj 变量来自哪里? - Denis
谢谢!我纠正了!我已经对它进行了调整,即使位置设置为绝对位置也会给出位置。 - Mageek
如果你从别人那里拿东西,首先说出它来自哪里通常是件好事。这就是我拒绝你的答案的原因,并补充说你的代码需要严格的重构。 - Denis
这只返回第一个元素。 - Oh Danny Boy


这个答案没有得到足够的重视。

完整样本,快速(二分搜索),缓存位置。

固定高度和宽度,最近的锚和scrollto的id

<!DOCTYPE html>
<html lang="en">
<head>
<meta>
<title>Offset 2</title>
<style>
body { font-family:helvetica,arial; font-size:12px; }
</style>
<script>
var ui = reqX = reqY = null, etop = eleft = 0, ref, cache;
function createAnchors()
{
    if (!ui)
    {
        ui = document.getElementById('UIWebView');
        reqX = document.getElementById('reqX');
        reqY = document.getElementById('reqY');
        var h=[], i=0;
        while (i < 1000)
            h.push('<a>fake anchor ... ',i,'</a> <a href=#>text for anchor <b>',(i++),'</b></a> ');
        ui.innerHTML = '<div style="padding:10px;width:700px">' + h.join('') + '</div>';
        cache = [];
        ref = Array.prototype.slice.call(ui.getElementsByTagName('a'));
        i = ref.length;
        while (--i >= 0)
        if (ref[i].href.length == 0)
            ref.splice(i,1);
    }
}
function pos(i)
{
    if (!cache[i])
    {
        etop = eleft = 0;
        var e=ref[i];
        if (e.offsetParent)
        {
            do
            {
                etop += e.offsetTop;
                eleft += e.offsetLeft;
            } while ((e = e.offsetParent) && e != ui)
        }
        cache[i] = [etop, eleft];       
    }
    else
    {
        etop = cache[i][0];
        eleft = cache[i][1];
    }
}
function find()
{
    createAnchors();
    if (!/^\d+$/.test(reqX.value))
    {
        alert ('I need a number for X');
        return;
    }   
    if (!/^\d+$/.test(reqY.value))
    {
        alert ('I need a number for Y');
        return;
    }
    var
        x = reqX.value,
        y = reqY.value,
        low = 0,
        hi = ref.length + 1, 
        med,
        limit = (ui.scrollHeight > ui.offsetHeight) ? ui.scrollHeight - ui.offsetHeight : ui.offsetHeight - ui.scrollHeight;
    if (y > limit)
        y = limit;
    if (x > ui.scrollWidth)
        x = (ui.scrollWidth > ui.offsetWidth) ? ui.scrollWidth : ui.offsetWidth;
    while (low < hi)
    {
        med = (low + ((hi - low) >> 1));
        pos(med);
        if (etop == y)
        {
            low = med;
            break;
        }
        if (etop < y)
            low = med + 1;
        else
            hi = med - 1;
    }
    var ctop = etop;
    if (eleft != x)
    {
        if (eleft > x)
            while (low > 0)
            {
                pos(--low);
                if (etop < ctop || eleft < x)
                {
                    pos(++low);
                    break;
                }
            }
        else
        {
            hi = ref.length;
            while (low < hi)
            {
                pos(++low);
                if (etop > ctop || eleft > x)
                {
                    pos(--low);
                    break;
                }
            }
        }
    }
    ui.scrollTop = etop - ui.offsetTop;
    ui.scrollLeft = eleft - ui.offsetLeft;
    ref[low].style.backgroundColor = '#ff0';
    alert(
        'Requested position: ' + x + ', ' + y + 
        '\nScrollTo position: ' + ui.scrollLeft + ', '+ ui.scrollTop + 
        '\nClosest anchor id: ' + low
        );
}
</script>
</head>
<body>
<div id=UIWebView style="width:320px;height:480px;overflow:auto;border:solid 1px #000"></div>
<label for="req">X: <input id=reqX type=text size=5 maxlength=5 value=200></label>
<label for="req">Y: <input id=reqY type=text size=5 maxlength=5 value=300></label>
<input type=button value="Find closest anchor" onclick="find()">
</body>
</html>

0



二元搜索部分很棒,很荣幸。毫无疑问,它比我简单的Array.sort更有效。但有一些事情你没有考虑到。首先,您不是在寻找元素的真实坐标。 element.offsetTop 返回元素相对于其父元素的顶部。您无法避免遍历DOM树以添加每个元素的父级offsetTop。此外,具有锚点的元素不一定是链接。而且你忘记了原始海报在给出XY偏移的情况下要求最近的锚点,而你只检查每个元素的Y偏移量。 - Denis
拼接没有href属性的锚点数组元素(如果需要)并使用while循环更改“top = a [med] .offsetTop”。最后对于X部分,保持低Y位置,通过设置hi = low + 1,low = 0进行第二次二分搜索,并将y,顶部和高度参考改为x,左边和宽度,并保留早期保持的Y位置打破。
所以你建议在项目列表上执行两次二进制搜索?我认为它比必要的更复杂。请记住,“过早优化是所有邪恶的根源”。不过,二进制搜索并不是一个坏主意。如果性能是这里的关键点,我想我仍然使用我建议的技术,但是我将创建并使用NodeList.prototype.binarySearch方法而不是Array.prototype.sort。基于矢量从其左上端到窗口左上端的最小范数,元素仍然会被比较一次。 - Denis
也, document.body.querySelectorAll('[id]') (要么 document.body.querySelectorAll('a[id]') 如果它需要链接)工作正常找到锚点元素。 - Denis
丹尼斯检查第二个样本(更新)