Демо: https://lucamug.github.io/elm-scroll-resize-events/

Это небольшой пример о событиях прокрутки и изменения размера в Elm через порты.

Я создал два порта в Elm, один для получения данных о позиции прокрутки, высоте страницы и размере области просмотра. Все это целые числа, как описано в этом определении типа.

type alias ScreenData =
    { scrollTop : Int
    , pageHeight : Int
    , viewportHeight : Int
    , viewportWidth : Int
    }

Порт просто определяется как

port scrollOrResize : (ScreenData -> msg) -> Sub msg

Основной скрипт подписывается на этот порт с помощью

subscriptions : Model -> Sub Msg
subscriptions model =
    Sub.batch
        [ scrollOrResize OnScroll
        ]

Итак, теперь каждый раз, когда Javascript отправляет данные через этот порт, OnScroll сообщение отправляется функции update.

Затем функция обновления просто обновит модель данными:

        OnScroll data ->
            ( { model | screenData = Just data }, Cmd.none )

В Javascript есть небольшая функция для дросселирования и подавления событий scroll и resize. Без дросселирования и устранения неполадок эти события запускаются много раз каждую секунду и могут ухудшить взаимодействие с пользователем.

Функция

var scrollTimer = null;
var lastScrollFireTime = 0;
var minScrollTime = 200;
var scrolledOrResized = function() {
    if (scrollTimer) {} else {
        var now = new Date().getTime();
        if (now - lastScrollFireTime > (3 * minScrollTime)) {
            processScrollOrResize();
            lastScrollFireTime = now;
        }
        scrollTimer = setTimeout(function() {
            scrollTimer = null;
            lastScrollFireTime = new Date().getTime();
            processScrollOrResize();
        }, minScrollTime);
    }
};

Первое условие if if (scrollTimer) проверяет наличие инициализированного таймера (setTimeout). В этом случае функция просто выйдет.

Второе условие if if (now — lastScrollFireTime > (3 * minScrollTime)) проверяет, прошло ли много времени с момента последнего вызова функции. Если это так, вероятно, это начальный момент нового действия прокрутки, поэтому мы хотим сразу же отправить данные в Elm, а затем установить таймер.

Последний раздел - установка таймера. Это сработает через определенный период времени, и в течение этого периода функция не будет предпринимать никаких действий из-за первого условия if.

Для связи с Elm используется следующая функция:

var processScrollOrResize = function() {
    var screenData = {
        scrollTop: parseInt(_window.pageYOffset || _html.scrollTop || _body.scrollTop || 0),
        pageHeight: parseInt(Math.max(_body.scrollHeight, _body.offsetHeight, _html.clientHeight, _html.scrollHeight, _html.offsetHeight)),
        viewportHeight: parseInt(_html.clientHeight),
        viewportWidth: parseInt(_html.clientWidth),
    };
    app.ports.scrollOrResize.send(screenData);
}

Эта функция использует некоторые уловки для получения данных из разных браузеров. Затем отправьте эти данные через порт scrollOrResize.

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

В Elm есть еще один порт, который используется для прокрутки страницы вверх.

port scrollTop : Int -> Cmd msg

Javascript подписывается на этот порт с помощью

app.ports.scrollTop.subscribe(elmScrollTop);

Это просто выполнило бы эту функцию

var elmScrollTop = function(position) {
    _window.scroll({
        top: position,
        left: 0,
        behavior: 'smooth'
    });
};

window.scroll находится на стадии Рабочий проект, поэтому я загружаю для него полифилл.

Чтобы запустить демонстрацию локально, вы можете выполнить следующие команды:

$ git clone https://github.com/lucamug/elm-scroll-resize-events
$ cd elm-scroll-resize-events
$ elm-live --output=elm.js src/Main.elm --open --debug