Выполнить только одно из одновременных событий

Описание

У меня есть некая функция (для ясности назовем foo), которая должна выполнятся если пользователь опустил курсор на элемент. Поскольку мне неизвестно какое устройство у пользователя я прикрепляю все возможные обработчики:

element.addEventListener(`mousedown`, (event) => {
    foo();
});
element.addEventListener(`pointerdown`, (event) => {
    foo();
});
element.addEventListener(`touchstart`, (event) => {
    foo();
});

Теперь мне надо делать так, чтобы когда в тот же момент заработает один из обработчиков, остальные молчали, чтобы функция выполнилась 1 раз, а не 3. Делаю я это вот так:

const controller = new AbortController();
await new Promise((resolve) => {
    element.addEventListener(`mousedown`, (event) => {
        resolve();
    }, { signal: controller.signal });
    element.addEventListener(`pointerdown`, (event) => {
        resolve();
    }, { signal: controller.signal });
    element.addEventListener(`touchstart`, (event) => {
        resolve();
    }, { signal: controller.signal });
});
foo();
controller.abort();

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

Проблема

Ну решение, как решение работает. Но если считать что кроме mousedown, pointerdown, touchstart ещё и добавить mousemove, pointermove, touchmove и ещё куча таких же обработчиков, то, по-моему, выглядит нудно.

Вопрос

Есть альтернативное, более хорошее решение?
Или же можно упростить эту конструкцию?


ЧаВо

— Почему не использовать только pointer... слушатели? Они же универсальные. — Они иногда неправильно работают. К примеру, pointermove останавливается 24-30 кадров после опускания мыши при использовании на мобильных устройствах.

— Почему не использовать для каждого устройства свои правильные обработчики? — Я бы с радостью. Но как на 100% верно узнать эту "правильность" обработчиков? Обработчики везде присутствуют, но не реагируют. navigator.userAgent дает неправильной информации. Какие ещё способы мне попробовать?


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

Автор решения: Stanislav Volodarskiy

NB Это была хорошая попытка, но, скорее всего, не заработает как надо. Опора на какие-то интервалы времени, надеясь что всё работает достаточно быстро - это все не надёжно. Я сперва думал что двух миллисекуд хватит. А оказалось что на мощном железе, на пустой странице интервал может быть и больше. Увеличил до десяти миллисекунд. Смотрите сами. Я бы сделал идемпотентный обработчик, хоть это и много работы.

Попробуйте троттлинг в десять миллисекунд:

const throttle = (f, delay) => {
    let waitFor = 0;
    return (...args) => {
        const now = new Date().getTime();
        if (now > waitFor) {
            waitFor = now + delay;
            // setTimeout(() => f(...args));
            f(...args);
        }
    };
};

const addEventListeners = (element, types, listener) => {
    const handler = throttle(listener, 10);
    types.forEach(type => element.addEventListener(type, handler));
};

...

    addEventListeners(
        element,
        ['mousedown', 'pointerdown', 'touchstart'],
        event => foo()
    );

P.S. Интервал выбран в десять миллисекунд потому что:

  1. Обработка может производится до 100 раз в секунду, пользователь не заметит пропуска разных событий так как обычный темп обработки не более 60 раз в секунду.

  2. Разные логические события произошедшие от одного физического события будут вызываться с малым интервалом по времени.

P.P.S. Хочется написать setTimeout(() => f(...args));. Но тогда перестанет работать event.preventDefault(); в теле f, а это часто нужно. С другой стороны f должен исполняться как можно скорее, иначе троттлинг просто не сработает. Идеально если f выполнит event.preventDefault(); а всю остальную обработку передаст в вызов setTimeout(...);.

P.P.P.S. С другой стороны, если вам нужен preventDefault, то его надо делать без троттлинга во всех обработчиках. Тогда схема другая:

const addEventListeners = (element, types, listener, preventDefault = false) => {
    const handler = throttle(listener, 10);
    let h = handler;
    if (preventDefault) {
        h = event => {
            event.preventDefault();
            handler(event);
        };
    }
    types.forEach(type => element.addEventListener(type, h));
};

...

    addEventListeners(
        element,
        ['mousedown', 'pointerdown', 'touchstart'],
        event => foo(),
        preventDefault = true
    );

Короче, не было печали, купила баба порося...

→ Ссылка
Автор решения: Arbery

Наверху есть решение с замыканием и созданием всего списка событий из массива. По итогу, лично у меня постоянно срабатывает pointerdown (к которому у автора есть претензии), независимо от порядка. Ну и фактически создаются все 2-3 варианта события, если они имеются в браузере.

Мое решение - проверять событие из списка на наличие в объекте window и вешать обработчик первое не равное undefined. Из плюсов - можно задавать любой порядок, не создает лишних событий. Из минусов завязано на проверку наличия событий в объекте window.

ВАЖНО!! ключевой момент порядок обработчиков в массиве, на мой взгляд, оптимальный touch, mouse, pointer - но этот момент уточняется на практике

const addEventListeners = (element, types, listener) => {
  for(let i = 0; i < types.length; i++) {
     if (window[`on${types[i]}`] !== undefined) {
       element.addEventListener(types[i], e => listener(e));
       return;
    } 
  }
};

const foo = e => {
  console.log(e.type, 'event');
  result.insertAdjacentHTML('beforeend', `<div>${e.type}, event</div>`);
}
const element = document.querySelector('.button');
const result = document.querySelector('.result');

addEventListeners(
  element,
  ['touchstart','mousedown', 'pointerdown'],
  event => foo(event)
);
<button class='button'>кнопка</button>
<div class='result'></div>

Идея отсюда Существование события в браузере , и еще можно здесь посмотреть Как ваш браузер обрабатывает прикосновения к экрану телефона (js touch events).

→ Ссылка
Автор решения: Stanislav Volodarskiy

Первое полученное событие замыкает обработчик на себя. Он перестаёт реагировать на события других типов:

const makeGate = f => {
    let password = undefined;
    return (key, event) => {
        if (password === key) {
            f(event);
        } else if (password === undefined) {
            password = key;
            f(event);
        }
    };
};

const addEventListeners = (element, types, listener) => {
    const gate = makeGate(listener);
    types.forEach(
        type => element.addEventListener(type, event => gate(type, event))
    );
};

...

    addEventListeners(
        element,
        ['mousedown', 'pointerdown', 'touchstart'],
        event => foo()
    );
→ Ссылка