'Wait until scrollTo is complete before running a command

I've got an AngularJS app, and I have a smooth scrolling directive to force the page to scroll to the bottom. I want the command to only run after the scrolling has finished. You can see that at the moment I run the scrolling function and then I run $('#comment-input').focus(); to focus on an element. I want to change it so this is only ran after the scrolling. I know I need to implement a callback but I can't figure out where to implement it.

(function() {

    var app = angular.module('myApp');

    app.service('anchorSmoothScroll', function(){

        this.scrollTo = function(eID) {

            // This scrolling function 
            // is from http://www.itnewb.com/tutorial/Creating-the-Smooth-Scroll-Effect-with-JavaScript

            var startY = currentYPosition();
            var stopY = elmYPosition(eID);
            var distance = stopY > startY ? stopY - startY : startY - stopY;
            if (distance < 100) {
                scrollTo(0, stopY); return;
            }
            var speed = Math.round(distance / 100);
            if (speed >= 20) speed = 20;
            var step = Math.round(distance / 25);
            var leapY = stopY > startY ? startY + step : startY - step;
            var timer = 0;
            if (stopY > startY) {
                for ( var i=startY; i<stopY; i+=step ) {
                    setTimeout("window.scrollTo(0, "+leapY+")", timer * speed);
                    leapY += step; if (leapY > stopY) leapY = stopY; timer++;
                } return;
            }
            for ( var i=startY; i>stopY; i-=step ) {
                setTimeout("window.scrollTo(0, "+leapY+")", timer * speed);
                leapY -= step; if (leapY < stopY) leapY = stopY; timer++;
            }

            function currentYPosition() {
                // Firefox, Chrome, Opera, Safari
                if (self.pageYOffset) return self.pageYOffset;
                // Internet Explorer 6 - standards mode
                if (document.documentElement && document.documentElement.scrollTop)
                    return document.documentElement.scrollTop;
                // Internet Explorer 6, 7 and 8
                if (document.body.scrollTop) return document.body.scrollTop;
                return 0;
            }

            function elmYPosition(eID) {
                var elm = document.getElementById(eID);
                var y = elm.offsetTop;
                var node = elm;
                while (node.offsetParent && node.offsetParent != document.body) {
                    node = node.offsetParent;
                    y += node.offsetTop;
                } return y;
            }

        };

    });

    app.controller('TextareaController', ['$scope','$location', 'anchorSmoothScroll',
    function($scope, $location, anchorSmoothScroll) {

        $scope.gotoElement = function (eID){
          // set the location.hash to the id of
          // the element you wish to scroll to.
          $location.hash('bottom-div');

          // call $anchorScroll()
          anchorSmoothScroll.scrollTo(eID);

          $('#comment-input').focus();

        };

    }]);

}());


Solution 1:[1]

Get anchorSmoothScroll.scrollTo to return a promise (using $q) and then focus once the promise is fullfilled. Read here for more info on $q.

anchorSmoothScroll.scrollTo(eID)
     .then(function() {
          $('#comment-input').focus();
     })

EDIT: Your code was failing because you are doing scrollTo inside setTimeout and that's where you would have to resolve your promise. Now, next problem is that there are multiple setTimeouts (as it is inside a for loop), so there would be lots of promises, and that's where $q.all will help.

I have setup a plunker for you, where I have updated one of the execution paths... Have a look here. In the console window, you will see that focusing is printer after scrolling has finished. To make it obvious, I have hard-coded the setTimeout interval to 2 seconds. I hope that helps.

Solution 2:[2]

I'd suggest creating a function that returns a promise and avoid the loops/timers. Then you can access the function like this:

    smoothScroll(element).then(() => {
      //Execute something when scrolling has finished
    });

The smoothScroll function can be defined like this, without using a lot of timers (the timer that actually is defined is just to reject() the promise if scrolling failed for some reason, such as user interaction):

function smoothScroll(elem, offset = 0) {
  const rect = elem.getBoundingClientRect();
  let targetPosition = Math.floor(rect.top + self.pageYOffset + offset);
  window.scrollTo({
    top: targetPosition,
    behavior: 'smooth'
  });

  return new Promise((resolve, reject) => {
    const failed = setTimeout(() => {
      reject();
    }, 2000);

    const scrollHandler = () => {
      if (self.pageYOffset === targetPosition) {
        window.removeEventListener("scroll", scrollHandler);
        clearTimeout(failed);
        resolve();
      }
    };
    if (self.pageYOffset === targetPosition) {
      clearTimeout(failed);
      resolve();
    } else {
      window.addEventListener("scroll", scrollHandler);
      elem.getBoundingClientRect();
    }
  });
}

I have also made a working example in this pen: https://codepen.io/familjenpersson/pen/bQeEBX

Solution 3:[3]

You can eventually try to bind a listener to notice when you reach bottom using

elm.on('scroll', function() {
      if(elm[0].scrollTop === elm[0].scrollHeight)
       $('#comment-input').focus();
    });

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2
Solution 3 Ismapro