Как узнать, что выделение элемента сделано в Javascript?

Я использую метод Javascript Element.scrollIntoView()
https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView

Могу ли я узнать, когда закончится свиток? Скажи, что была анимация, или я установил {behavior: smooth},

Я предполагаю, что прокрутка асинхронна, и я хочу знать, есть ли какой-нибудь механизм, похожий на обратный вызов.

10 ответов

Здесь нет scrollEnd событие, но вы можете слушать scroll событие и проверьте, прокручивает ли оно окно:

var scrollTimeout;
addEventListener('scroll', function(e) {
    clearTimeout(scrollTimeout);
    scrollTimeout = setTimeout(function() {
        console.log('Scroll ended');
    }, 100);
});

Для этого "плавного" поведения все спецификации говорят, что

Когда пользовательский агент должен выполнить плавную прокрутку прокручиваемого окна окна до позиции, он должен обновить положение прокрутки окна в режиме, определяемом пользовательским агентом, в течение определенного пользователем времени.

(подчеркивает мой)

Таким образом, не только не существует единственного события, которое сработает после его завершения, но мы даже не можем предполагать какое-либо стабилизированное поведение между различными браузерами.

И действительно, нынешние Firefox и Chrome уже отличаются своим поведением:

  • Firefox, похоже, имеет фиксированную продолжительность, и независимо от расстояния прокрутки, он будет делать это в течение этой фиксированной продолжительности ( ~500 мс).
  • Chrome, с другой стороны, будет использовать скорость, то есть продолжительность операции будет зависеть от расстояния для прокрутки с жестким ограничением в 3 секунды.

Таким образом, это уже дисквалифицирует все решения этой проблемы, основанные на тайм-аутах.

Теперь в одном из ответов здесь предлагается использовать MutationObserver, что является неплохим решением, но не слишком переносимым и не требует inline а также block варианты во внимание.

Так что лучше всего будет регулярно проверять, прекратили ли мы прокрутку. Чтобы сделать это неинвазивным способом, мы можем начатьrequestAnimationFrame петля с питанием, поэтому наши проверки выполняются только один раз за кадр.

Вот одна такая реализация, которая вернет обещание, которое будет разрешено после завершения операции прокрутки.
Примечание. В этом коде отсутствует способ проверки успешности операции, поскольку, если на странице происходит другая операция прокрутки, все текущие отменяются, но я позволю этому упражнению для читателя.

const buttons = [ ...document.querySelectorAll( 'button' ) ];

document.addEventListener( 'click', ({ target }) => {
  // handle delegated event
  target = target.closest('button');
  if( !target ) { return; }
  // find where to go next
  const next_index =  (buttons.indexOf(target) + 1) % buttons.length;
  const next_btn = buttons[next_index];
  const block_type = target.dataset.block;

  // make it red
  document.body.classList.add( 'scrolling' );
  
  smoothScroll( next_btn, { block: block_type })
    .then( () => {
      // remove the red
      document.body.classList.remove( 'scrolling' );
    } )
});


/* 
 *
 * Promised based scrollIntoView( { behavior: 'smooth' } )
 * @param { Element } elem
 **  ::An Element on which we'll call scrollIntoView
 * @param { object } [options]
 **  ::An optional scrollIntoViewOptions dictionary
 * @return { Promise } (void)
 **  ::Resolves when the scrolling ends
 *
 */
function smoothScroll( elem, options ) {
  return new Promise( (resolve) => {
    if( !( elem instanceof Element ) ) {
      throw new TypeError( 'Argument 1 must be an Element' );
    }
    let same = 0; // a counter
    let lastPos = null; // last known Y position
    // pass the user defined options along with our default
    const scrollOptions = Object.assign( { behavior: 'smooth' }, options );

    // let's begin
    elem.scrollIntoView( scrollOptions );
    requestAnimationFrame( check );
    
    // this function will be called every painting frame
    // for the duration of the smooth scroll operation
    function check() {
      // check our current position
      const newPos = elem.getBoundingClientRect().top;
      
      if( newPos === lastPos ) { // same as previous
        if(same ++ > 2) { // if it's more than two frames
/* @todo: verify it succeeded
 * if(isAtCorrectPosition(elem, options) {
 *   resolve();
 * } else {
 *   reject();
 * }
 * return;
 */
          return resolve(); // we've come to an halt
        }
      }
      else {
        same = 0; // reset our counter
        lastPos = newPos; // remember our current position
      }
      // check again next painting frame
      requestAnimationFrame(check);
    }
  });
}
p {
  height: 400vh;
  width: 5px;
  background: repeat 0 0 / 5px 10px
    linear-gradient(to bottom, black 50%, white 50%);
}
body.scrolling {
  background: red;
}
<button data-block="center">scroll to next button <code>block:center</code></button>
<p></p>
<button data-block="start">scroll to next button <code>block:start</code></button>
<p></p>
<button data-block="nearest">scroll to next button <code>block:nearest</code></button>
<p></p>
<button>scroll to top</button>

Ты можешь использовать IntersectionObserverпроверьте, если элемент .isIntersecting в IntersectionObserver функция обратного вызова

const element = document.getElementById("box");

const intersectionObserver = new IntersectionObserver((entries) => {
  let [entry] = entries;
  if (entry.isIntersecting) {
    setTimeout(() => alert(`${entry.target.id} is visible`), 100)
  }
});
// start observing
intersectionObserver.observe(box);

element.scrollIntoView({behavior: "smooth"});
body {
  height: calc(100vh * 2);
}

#box {
  position: relative;
  top:500px;
}
<div id="box">
box
</div>

Я наткнулся на этот вопрос, так как хотел сфокусировать конкретный ввод после завершения прокрутки (чтобы сохранить плавную прокрутку).

Если у вас такой же сценарий использования, как у меня, вам на самом деле не нужно ждать завершения прокрутки, чтобы сфокусировать свой ввод, вы можете просто отключить прокрутку фокуса .

Вот как это делается:

      window.scrollTo({ top: 0, behavior: "smooth" });
myInput.focus({ preventScroll: true });

cf: https://github.com/w3c/csswg-drafts/issues/3744#issuecomment-685683932

Кстати, эта конкретная проблема (ожидания завершения прокрутки перед выполнением действия) обсуждается в CSSWG GitHub здесь: https://github.com/w3c/csswg-drafts/issues/3744

Решение, которое работает для меня с rxjs

lang: Машинопись

scrollToElementRef(
    element: HTMLElement,
    options?: ScrollIntoViewOptions,
    emitFinish = false,
  ): void | Promise<boolean> {
    element.scrollIntoView(options);
    if (emitFinish) {
      return fromEvent(window, 'scroll')
        .pipe(debounceTime(100), first(), mapTo(true)).toPromise();
    }
  }

Применение:

const element = document.getElementById('ELEM_ID');
scrollToElementRef(elment, {behavior: 'smooth'}, true).then(() => {
  // scroll finished do something
})

Принятый ответ великолепен, но я почти не использовал его из-за его многословия. Вот более простая версия vanillajs, которая должна говорить сама за себя:

          scrollTarget.scrollIntoView({
        behavior: "smooth",
        block: "center",
    });
    let lastPos = null;
    requestAnimationFrame(checkPos);
    function checkPos() {
        const newPos = scrollTarget.getBoundingClientRect().top;
        if (newPos === lastPos) {
            console.log('scroll finished on', scrollTarget);
        } else {
            lastPos = newPos;
            requestAnimationFrame(checkPos);
        }
    }

Я пропустил проверку, в которой ОП беспокоился, что раф выстрелит дважды подряд без изменения свитка; возможно, это обоснованное опасение, но я не сталкивался с такой проблемой.

В приведенных выше ответах обработчик событий остается на месте даже после выполнения прокрутки (так что, если пользователь прокручивает, их метод продолжает вызываться). Они также не уведомляют вас, если прокрутка не требуется. Вот ответ чуть получше:

$("#mybtn").click(function() {
    $('html, body').animate({
        scrollTop: $("div").offset().top
    }, 2000);

    $("div").html("Scrolling...");

    callWhenScrollCompleted(() => {
        $("div").html("Scrolling is completed!");
    });
});

// Wait for scrolling to stop.
function callWhenScrollCompleted(callback, checkTimeout = 200, parentElement = $(window)) {
  const scrollTimeoutFunction = () => {
    // Scrolling is complete
    parentElement.off("scroll");
    callback();
  };
  let scrollTimeout = setTimeout(scrollTimeoutFunction, checkTimeout);

  parentElement.on("scroll", () => {
    clearTimeout(scrollTimeout);
    scrollTimeout = setTimeout(scrollTimeoutFunction, checkTimeout);
  });
}
body { height: 2000px; }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

<button id="mybtn">Scroll to Text</button>
<br><br><br><br><br><br><br><br>
<div>example text</div>

Я не эксперт в JavaScript, но я сделал это с помощью JQuery. Я надеюсь, что это помогает

$("#mybtn").click(function() {
    $('html, body').animate({
        scrollTop: $("div").offset().top
    }, 2000);
});

$( window ).scroll(function() {
  $("div").html("scrolling");
  if($(window).scrollTop() == $("div").offset().top) {
    $("div").html("Ended");
  }
})
body { height: 2000px; }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

<button id="mybtn">Scroll to Text</button>
<br><br><br><br><br><br><br><br>
<div>example text</div>

Недавно мне понадобился метод обратного вызова element.scrollIntoView(). Поэтому попытался использовать ответ Кшиштофа Подляски . Но я не мог использовать его как есть. Я немного модифицировал.

      import { fromEvent, lastValueFrom } from 'rxjs';
import { debounceTime, first, mapTo } from 'rxjs/operators';

/**
 * This function allows to get a callback for the scrolling end
 */
const scrollToElementRef = (parentEle, childEle, options) => {
  // If parentEle.scrollTop is 0, the parentEle element does not emit 'scroll' event. So below is needed.
  if (parentEle.scrollTop === 0) return Promise.resolve(1);
  childEle.scrollIntoView(options);
  return lastValueFrom(
    fromEvent(parentEle, 'scroll').pipe(
      debounceTime(100),
      first(),
      mapTo(true)
    )
  );
};

Как использовать

      scrollToElementRef(
  scrollableContainerEle, 
  childrenEle, 
  {
    behavior: 'smooth',
    block: 'end',
    inline: 'nearest',
  }
).then(() => {
  // Do whatever you want ;)
});

Если кто-то ищет способ распознать событие ScrollEnd в Angular с помощью директивы:

      /**
 * As soon as the current scroll animation ends
 * (triggered by scrollElementIntoView({behavior: 'smooth'})),
 * this method resolves the returned Promise.
 */
@Directive({
    selector : '[scrollEndRecognizer]'
})
export class ScrollEndDirective {

    @Output() scrollEnd: EventEmitter<void> = new EventEmitter();

    private scrollTimeoutId: number;

    //*****************************************************************************
    //  Events
    //****************************************************************************/
    @HostListener('scroll', [])
    public emitScrollEndEvent() {
        // On each new scroll event, clear the timeout.
        window.clearTimeout(this.scrollTimeoutId);

        // Only after scrolling has ended, the timeout executes and emits an event.
        this.scrollTimeoutId = window.setTimeout(() => {
            this.scrollEnd.emit();
            this.scrollTimeoutId = null;
        }, 100);
    }

    /////////////////////////////////////////////////////////////////////////////*/
    //  END Events
    /////////////////////////////////////////////////////////////////////////////*/
}
Другие вопросы по тегам