elementFromPoint() under iOS 5

本文讨论了在iOS5版本中JavaScript库elementFromPoint()函数的行为变化,从旧版iOS的文档坐标系统转变为视口坐标系统,并提供了转换函数和检测方法来确保跨不同iOS版本的兼容性。
 

The JavaScript call elementFromPoint(x,y) can be used to find the element of a web page at a certain coordinate. If you have used this call in the past on a web page or in a native iOS App which was developed for iOS 4.x or older, you’ll notice that your web page or App might fail when used under iOS 5. Under iOS 5 the elementFromPoint(x,y) call finds different elements or even returns null instead of an element. It looks like the call is now broken. But this is not the case, in fact, under iOS 5 it works correct the first time, it was broken before.

elementFromPoint(x,y) was defined to return the element at the given coordinates within the view port (the visible area) of the web page, or null (if the coordinates are outside of the viewport). The coordinates are measured relative to the origin of the view port. This is how iOS 5 finally works.

Before (iOS 4.x and older), elementFromPoint(x,y) completely ignored the view port. It measured the coordinates relative to the origin of the document. And even elements outside of the visible area could be found. This behavior seems to make much more sense than the new iOS 5 behavior, but according to the official JavaScript specification, it’s not the correct behavior.

The different behavior between iOS 5 and older iOS versions can cause some serious problems. The coordinate systems are no longer compatible, so when the web page or App has to run under old an new iOS versions, it is necessary to find out the correct coordinate system that is used.

In a native iOS App you could simply check the iOS version, and based on its value you can decide which coordinate system you need to use when calling elementFromPoint(x,y). But when writing a web page, this is not so easy: the iOS version is not exposed to the web page (it might be part of the UserAgent information, but because almost all browser do allow to use a fake userAgent information, this information is not reliably at all). Also on the Mac and on other platforms different WebKit releases might be used which do use different coordinate systems for the elementFromPoint(x,y) call as well. Therefore it makes sense to find a way to identify the coordinate system independent of the iOS version, and if necessary correct the coordinates.

At first when we compare the two coordinate systems, we notice that the coordinates are offset by the scroll location. If we scroll the web page so that the top left corner of the page is visible, both coordinate systems are the same. The origin of the view port is identical to the origin of the web page. And the scroll offset is also 0 in both directions. If you scroll down 100 px, the origin (the coordinate (0,0)) of the viewport is located at the coordinate (0,100) of the web page. So the scroll offset is exactly the offset between the two coordinate systems. Therefore, transforming one coordinate system into the other is very easy. We only need to add or subtract the actual scroll offsets.

function documentCoordinateToViewportCoordinate(x,y) {
  var coord = new Object();
  coord.x = x - window.pageXOffset;
  coord.y = y - window.pageYOffset;
  return coord;
}

function viewportCoordinateToDocumentCoordinate(x,y) {
  var coord = new Object();
  coord.x = x + window.pageXOffset;
  coord.y = y + window.pageYOffset;
  return coord;
}

These JavaScript functions take a coordinate of one system and transform them into a coordinate of the other system.

But in order find out if and which of the functions we need to use, we have to find out, in which coordinate system the call elementFromPoint(x,y) expects the coordinates. To do this we use the fact that elementFromPoint() returns null when the coordinates are outside of the view port, when it expects coordinates measured relative to the viewport (as noted above, when the coordinates are relative to the origin of the document, elementFromPoint() will always return an element, even when outside of the visible area, so we can distinguish between the two cases).
Good test coordinates would be (0, window.pageYOffset + window.innerHeight -1) and (window.pageXOffset + window.innerWidth -1, 0), for vertical scrolling and horizontal scrolling. As noted above, when no scrolling is done, both coordinate systems are identical, and we don’t need to take care about anything. But if the page is scrolled, we need to check which system is used. The test coordinates take the actual scroll offset and add the width or height of the visible area (this is the innerWidth and innerHeight of the “window” object) and subtract 1. This makes sure that the coordinate addresses the very last pixel line or column of the visible area measured relative to the document origin. This is always a valid document-based coordinate which lies within the document boundaries (a coordinate outside of the document boundaries would return null even with the elementFromPoint() call for the document-based coordinate system). If the page is scrolled by at least one single pixel, the test coordinates would lie outside of the viewport, when interpreted as relative to the viewport, so elementFromPoint() would return null. When elementFromPoint() would interpret them relative to the document, these coordinates are always valid and would always return an element. And this is how we can easily detect, which coordinate system elementFromPoint() is using.

function elementFromPointIsUsingViewPortCoordinates() {
  if (window.pageYOffset > 0) {     // page scrolled down
    return (window.document.elementFromPoint(0, window.pageYOffset + window.innerHeight -1) == null);
  } else if (window.pageXOffset > 0) {   // page scrolled to the right
    return (window.document.elementFromPoint(window.pageXOffset + window.innerWidth -1, 0) == null);
  }
  return false; // no scrolling, don't care
}

We can combine this to one custom elementFromPoint() function that is using a document-based coordinate system as input and will internally do all the magic for us:

function elementFromDocumentPoint(x,y) {
  if (elementFromPointIsUsingViewPortCoordinates()) {
    var coord = documentCoordinateToViewportCoordinate(x,y);
    return window.document.elementFromPoint(coord.x,coord.y);
  } else {
    return window.document.elementFromPoint(x,y);
  }
}

And the counterpart for viewport-based coordinates:

function elementFromViewportPoint(x,y) {
  if (elementFromPointIsUsingViewPortCoordinates()) {
    return window.document.elementFromPoint(x,y);
  } else {
    var coord = viewportCoordinateToDocumentCoordinate(x,y);
    return window.document.elementFromPoint(coord.x,coord.y);
  }
}

So instead of using elementFromPoint() directly, you simply use elementFromViewportPoint() or elementFromDocumentPoint() instead, depending of the coordinates you have to deal with. It will then work correct in old and new WebKit releases.

Please note: if you use the code of my older blog post “Customize the contextual menu of UIWebView” in your projects, you need to update this as well, because it also uses the elementFromPoint() call. But this should be really easy to do.

Posted in iPhone & iPod Touch, Programming, Tips & tricks, Web Technology.

Tagged with Development, iOS, JavaScript, UIWebView, WebKit.

在 Vue 中使用 `@touchend` 结合 `elementFromPoint` 时,如果遇到 `contains` 方法报错(如 `TypeError: target.contains is not a function`),通常是因为 `document.elementFromPoint()` 返回的是 `null` 或未正确获取到目标元素。以下是解决方案和优化代码: --- ### **问题原因** 1. **`elementFromPoint` 返回 `null`** - 当触摸点不在任何 DOM 元素上时(如快速滑动离开屏幕),`document.elementFromPoint()` 可能返回 `null`,导致 `contains` 方法调用失败。 2. **未正确引用目标元素** - 如果 `this.$refs.targetElement` 未正确绑定或元素未渲染,`target` 可能是 `undefined`。 --- ### **修复后的代码** ```vue <template> <div ref="targetElement" @touchend="handleTouchEnd" > 指定元素内容 </div> </template> <script> export default { methods: { handleTouchEnd(event) { const target = this.$refs.targetElement; if (!target) return; // 确保目标元素存在 const touch = event.changedTouches[0]; const elementAtPoint = document.elementFromPoint(touch.clientX, touch.clientY); // 检查元素是否存在且是否在目标元素内 if (!elementAtPoint || !target.contains(elementAtPoint)) { console.log('用户离开了指定元素'); this.$emit('leave'); // 触发自定义事件 } } } }; </script> ``` --- ### **关键修复点** 1. **检查 `elementFromPoint` 返回值** - 添加 `if (!elementAtPoint)` 判断,避免对 `null` 调用 `contains`。 2. **确保目标元素存在** - 通过 `if (!target) return` 防止 `this.$refs.targetElement` 未绑定时出错。 3. **使用 `changedTouches`** - `event.changedTouches[0]` 获取触摸结束时的坐标,而非 `event.touches`(后者可能为空)。 --- ### **替代方案:使用 `getBoundingClientRect`** 如果 `elementFromPoint` 仍不稳定,可以通过坐标判断是否离开元素边界: ```javascript handleTouchEnd(event) { const target = this.$refs.targetElement; if (!target) return; const touch = event.changedTouches[0]; const rect = target.getBoundingClientRect(); // 检查触摸点是否在元素矩形范围内 const isInside = ( touch.clientX >= rect.left && touch.clientX <= rect.right && touch.clientY >= rect.top && touch.clientY <= rect.bottom ); if (!isInside) { console.log('用户离开了指定元素'); } } ``` --- ### **其他注意事项** 1. **快速滑动问题** - 某些设备在快速滑动时可能不会触发 `touchend`,可结合 `touchcancel` 事件处理异常情况。 2. **Vue 3 组合式 API** - 在 Vue 3 中,使用 `ref` 和 `onMounted` 获取 DOM 引用: ```javascript import { ref, onMounted } from 'vue'; export default { setup() { const targetElement = ref(null); const handleTouchEnd = (event) => { if (!targetElement.value) return; // 其余逻辑... }; return { targetElement, handleTouchEnd }; } }; ``` ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值