4

I have a nice little React drag-drop library that works for mouse- and touch systems. For touch, it grabs the touch location via clientX and clientY (e.targetTouches[0].clientX, e.targetTouches[0].clientY). It uses those coordinates to place the dragged element, which has position: fixed.

HOWEVER it turns out that at least on IOS Safari (v.11.x), when you zoom the display, the coordinate system for position:fixed no longer matches the window coordinate system. So the dragged element displays in the wrong place on the page.

Picture the zoomed-in browser window as a small rectangular view onto a larger rectangle containing the un-zoomed content. The location:fixed coordinate system uses the larger rectangle. The window coordinate system uses the small one.

As you scroll, the window pans around the larger rectangle in a way that's difficult to describe, with the result that the offset between 0,0 in position-fixed-land and 0,0 in the browser window is always changing.

How can I get the offset between the browser window and the "position:fixed" coordinate systems? (I can then add that offset to the position of the dragged element to position it correctly.)

Peter Hollingsworth
  • 1,870
  • 1
  • 18
  • 18

2 Answers2

1

Stick an element at 0,0, position:fixed.

Get its x/y offset from the browser window using getBoundingClientRect().

Then delete the element.

function getFixedOffset() {
    let fixedElem = document.createElement('div');
    fixedElem.style.cssText = 'position:fixed; top: 0; left: 0';
    document.body.appendChild(fixedElem);
    const rect = fixedElem.getBoundingClientRect();
    document.body.removeChild(fixedElem);
    return [rect.left, rect.top]
}

This works (yay!) but feels pretty kludgey, creating and then destroying a DOM element every time the user drags-and-drops. Other suggestions are welcome.

Peter Hollingsworth
  • 1,870
  • 1
  • 18
  • 18
  • I tried using with drag items inside a container that has an overflow:scroll, it will offset somewhat off the screen when zoomed in and scrolled down a bit, no matter if I use position:fixed or position:absolute, because the updated viewport is not taken into account. I was using essentially the same workaround, a div named #overlay_touch placed at the bottom but designed to be an overlay. This is on Safari for iOS 14, iPhone 11 Pro Max. – John Ernest Nov 19 '20 at 22:35
  • window.visualViewport will at least give scale and left/top info, if and only if you add the meta viewport tag to the head of your page, but I'm unsure of the adjustment math needed that would need to be applied to the results of getBoundingClientRect(). – John Ernest Nov 19 '20 at 23:00
  • getBoundingClientRect() will give incorrect results when using zoom in safari due to bug https://bugs.webkit.org/show_bug.cgi?id=77998 . I have tested in IOS, it returns incorrect top value when using zoom. – Dr. DS Jul 01 '21 at 14:52
0

I came across this in my search for a solution. I have a position:absolute element that I update the position on when the window is resized, and iOS triggers this on zooming in/out.

I discovered window.visualViewport which has the key information needed to offset this!

My solution for setting the position/size of an element with the zoom offset:

positionNavHighlight(link: HTMLElement) {
  const rect = link.getBoundingClientRect();

  const offsetTop = window.visualViewport.offsetTop;
  const offsetLeft = window.visualViewport.offsetLeft;

  navActiveBg.style.top = `${rect.top + offsetTop}px`;
  navActiveBg.style.left = `${rect.left + offsetLeft}px`;
  navActiveBg.style.height = `${rect.height}px`;
  navActiveBg.style.width = `${rect.width}px`;
}
Chris Barr
  • 29,851
  • 23
  • 95
  • 135