Перетаскивание блока по экрану, не срабатывает onmouseup

Когда только начинаю перемещать блок, он на пару пикселей смещаеся вправо и вниз относительно мышки. Таким образом, если курсор установить где-то очень близко к верхней левой границе блока, при самом начале перетаскивания блок ускользает из-под мышки на пару пикселей и у него перестаёт работать onmouseup.

Поясните, почему так происходит и откуда могут браться эти 3-4 пикселя?

На самом деле, почему-то в редакторе кода stackoverflow он съезжает на куда большее количество пикселей, поэтому лучше скопировать код в IDE для полного воспроизведения ошибки.

//Список окон потому, что в оригинальном коде их может быть больше одного.
wins = [...document.getElementsByClassName('window')];

function init() {

    wins.forEach(win => {

        win.onmousedown = function(event) { 
            
            //Смещение для координат, так как event даёт координаты относительно viewport, а задаём мы координаты относительно родительского блока.
            win.shiftX = event.clientX - win.getBoundingClientRect().left + win.parentElement.getBoundingClientRect().left;
            win.shiftY = event.clientY - win.getBoundingClientRect().top + win.parentElement.getBoundingClientRect().top;

            //Функция перемещения блока. Также проверяет на пересечение границ родительского блока, иначе по каким-то причинам onmouseup тоже не срабатывает. Также применяет смещение.
            function moveAt(pageX, pageY) {

                if(pageX >= win.parentElement.getBoundingClientRect().right 
                   || pageX <= win.parentElement.getBoundingClientRect().left
                   || pageY >= win.parentElement.getBoundingClientRect().bottom
                   || pageY <= win.parentElement.getBoundingClientRect().top) {
                    
                    document.removeEventListener('mousemove', win.onMouseMove);
                    return;
                } 
                
                //console.log(pageX, win.shiftX, pageX - win.shiftX);
                win.style.left = pageX - win.shiftX + 'px';
                win.style.top = pageY - win.shiftY + 'px';
            }

            win.onMouseMove = function(event) {
                moveAt(event.pageX, event.pageY);
            }

            document.addEventListener('mousemove', win.onMouseMove);

            win.onmouseup = function() {
                document.removeEventListener('mousemove', win.onMouseMove);
                win.onmouseup = null;
            };
        }

        //Чтобы браузерный drag-n-drop не мешался.
        win.ondragstart = function() {
            return false;
        };
    });
}

init();
.window {
    box-sizing: border-box;
    position: absolute;
    border: 1px solid red;
    background-color: white;
}

.surface {
    
    box-sizing: border-box;
    position: absolute;
    margin: 0 0 0 0;
    overflow: hidden;
    
    top: 100px;
    left: 100px;
    
    border: 5px solid green;
    width: 600px;
    height: 400px;
}

#id-1 {
    left: 100px;
    top: 100px;
    width: 100px;
    overflow: hidden;
}

.unselectable {
  -webkit-touch-callout: none; /* iOS Safari */
  -webkit-user-select: none;   /* Chrome/Safari/Opera */
  -khtml-user-select: none;    /* Konqueror */
  -moz-user-select: none;      /* Firefox */
  -ms-user-select: none;       /* Internet Explorer/Edge */
  user-select: none;           /* Non-prefixed version, currently
                                  not supported by any browser */
}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <link rel="stylesheet" type="text/css" href="style.css">

</head>
<body>
    
    <div class="surface">
        
        <div class="window unselectable" id="id-1">
            <p>Window</p>
        </div>
    
    </div>
    
    <script type="text/javascript" src="script.js"></script>
</body>
</html>


Ответы (1 шт):

Автор решения: Денис Степанов

Надеюсь тут более менее понятно будет, то что у вас отсчет position: absolute; top: ... идет со внутренней границы обводки.

А getBoundingClientRect наоборот по внешней определяет. И получается граница выпадает из ваших расчетов.

const box = document.querySelector('.container');
const rect = box.getBoundingClientRect();


console.log(rect.top); // равен отступу элемента с учетом границы. В данном случае равен margin-top.
* {
  margin: 0;
}

.container {
  width: 150px;

  height: 100px;
  border: 10px solid green;
  
  margin: 100px auto 0;
  
  box-sizing: border-box;
  position: relative;
}

.block {
  position: absolute;
  box-sizing: border-box;
  
  top: 0; /* вот тут не с верхней границы, а с нижней границы обводки. Стоит 0, но отступ равен ширине обводки*/
  
  width: 10px;
  height: 10px;
  
  background-color: red;
}
<div class="container">
  <div class="block"></div>
</div>

→ Ссылка