Так-с, уважаемые коллеги, всех радостно приветствую! Это небольшая статейка как раз для тех людей, которые хотят по быстрому вот такой функционал:

  • обнаруживать какой тип взаимодействия с веб приложением у пользователя: touchscreen, мышка, либо же и то, и другое одновременно

  • обнаруживать какая ориентация на данный момент у пользователя

  • обнаруживать какой тип устройства имеет пользователь: desktop, tab, phone

    Звучит, конечно, не сложно, но для этого нужно собирать своего трансформера с разных форумов, я же предлагаю свое решение в готовом и компактном виде :)

Перейдем к главному: логика и код

В общем и целом, мне нужен был такой функционал, который бы при первом заходе пользователя показывает touch это или нет. А далее уже можно подхватить и разные другие события. Делается это все для реактивной адаптивности, т.к. на css сделать это просто невозможно.

Перейдем к коду. Создадим для начала все нужные перечисления (enum) для дальнейшего взаимодействия:

  • InteractionType.ts - enum, отвечающий за тип взаимодействия - мышка, тач, либо оба сразу

export namespace InteractionType
{
    export enum State {
        Unknown = 'Unknown',
        Mouse = 'Mouse',
        Touch = 'Touch',
        Both =  'Both'
    }
}
  • OrientationType.ts - enum, отвечающий за тип ориентации - вертикаль, горизонталь, в рамках css это портрет и лэндскейп

export namespace OrientationType
{
    export enum State {
        Unknown = 'Unknown',
        Portrait = 'Portrait',
        Landscape = 'Landscape',
    }
}
  • DeviceType.ts - enum, отвечающий за тип устройства - планшет, телефон, либо ПК

export namespace DeviceType
{
    export enum State {
        Unknown = 'Unknown',
        Desktop = 'Desktop',
        Phone = 'Phone',
        Tab = 'Tab'
    }
}

Теперь напишем сам класс, который будем подключать к компонентам. Начнем с самого простого - инициализация и объявление всех полей. Тут все по стандарту, ничего такого, разве что можно обратить внимание на параметр у addEventListener - once, т.к. нам не требуется отслеживать каждый раз что там у пользователя за тип, сработало раз - все, делаем отметку, что есть, например, тачскрин и больше этот обработчик не трогаем.
ВАЖНО!! - переменные должны быть реактивные, иначе у вас ничего не будет работать, можно использовать ref из vue, можно использовать Subject'ы из rxjs, но я советую в рамках vue использовать ref, т.к. vue умный и умеет удалять все ссылки, прослушки автоматически, а в rxjs же subscrib'ы очень непослушные и пока вы вручную не напишете unsubscribe, оно так и будет висеть. Также не забудем про инкапсуляцию и сделаем все переменные приватными, поэтому сделаем для них геттеры.

private p_interactionType: Ref<InteractionType.State>;
private p_orientationType: Ref<OrientationType.State>;
private p_deviceType: Ref<DeviceType.State>;

constructor() {
  this.p_interactionType = ref(InteractionType.State.Unknown);
  this.p_orientationType = ref(OrientationType.State.Unknown);
  this.p_deviceType = ref(DeviceType.State.Unknown);

  window.addEventListener("touchstart", () => this.activeTouchState(), { once: true });
  window.addEventListener("mousemove", () => this.activeDeskState(), { once: true });
}

  public get InteractionType() {
    return this.p_interactionType.value;
  }

  public get OrientationType() {
    return this.p_orientationType.value;
  }

  public get DeviceType() {
    return this.p_deviceType.value;
  }

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

public defineTouchscreen(): void {
    if (window.PointerEvent && "maxTouchPoints" in navigator) {
      // if Pointer Events are supported, just check maxTouchPoints
      if (navigator.maxTouchPoints > 0) {
        this.activeTouchState();
      }
    } else {
      // no Pointer Events...
      if (window.matchMedia && window.matchMedia("(any-pointer:coarse)").matches) {
        // check for any-pointer:coarse which mostly means touchscreen
        this.activeTouchState();
      } else if (window.TouchEvent || "ontouchstart" in window) {
        // last resort - check for exposed touch events API / event handler
        this.activeTouchState();
      }
    }
  }

Теперь добавим функции обработчики для eventListener'ов:

  private activeTouchState(): void {
    if (this.p_interactionType.value === InteractionType.State.Mouse) {
      this.p_interactionType.value = InteractionType.State.Both;
    } else {
      this.p_interactionType.value = InteractionType.State.Touch;
    }
  }

  private activeDeskState(): void {
    if (this.p_interactionType.value === InteractionType.State.Touch) {
      this.p_interactionType.value = InteractionType.State.Both;
    } else {
      this.p_interactionType.value = InteractionType.State.Mouse;
    }
  }

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

Теперь функция для ориентации устройства:

private defineDeviceType(): void {
    const ua = navigator.userAgent;
    if (/(tablet|ipad|playbook|silk)|(android(?!.*mobi))/i.test(ua)) {
      this.p_deviceType.value = DeviceType.State.Tab;
      return;
    }
    if (
      /Mobile|iP(hone|od)|Android|BlackBerry|IEMobile|Kindle|Silk-Accelerated|(hpw|web)OS|Opera M(obi|ini)/.test(
        ua
      )
    ) {
      this.p_deviceType.value = DeviceType.State.Phone;
      return;
    }
    this.p_deviceType.value = DeviceType.State.Desktop;
  }

Единственное, что тут нужно знать, так это то, что navigator.userAgent возвращает строку вида:

userAgent = appCodeName/appVersion number (Platform; Security; OS-or-CPU;
Localization; rv: revision-version-number) product/productSub
Application-Name Application-Name-version

А дальше мы уже можем проводить столько проверок, сколько захотим.
Ну и осталась функция для определения ориентации устройства:

private defineOrientationType(): void {
    if (window.matchMedia("(orientation: portrait)").matches) {
      this.p_orientationType.value = OrientationType.State.Portrait;
    } else if (window.matchMedia("(orientation: landscape)").matches) {
      this.p_orientationType.value = OrientationType.State.Landscape;
    }
  }

На этом код готов) Вообще тут можно много всего сделать, нужно уже самому тыкаться и смотреть, что да как прикручивать. Я его использовал примерно так:

const currentInteractionState = computed(() => deviceDetect.InteractionType);

const hasTouchScreen = computed(() => {
  return (
      currentInteractionState.value === InteractionType.State.Both ||
      currentInteractionState.value === InteractionType.State.Touch
  );
});

Все это дело я прикручивал к динамическим классам, от значения computed теперь меняются и стили) Класс!
Полный код как обычно на моем гитхабе

На этом желаю всем удачи! До встречи!

Комментарии (11)


  1. sanchezzzhak
    18.07.2023 18:40
    -1

    Надо ли говорить что определение типа устройства по ua это не точно.

    Многие планшеты имеют мобильные ua.

    Самый главный вопрос зачем тип устройства во frontend?


    1. GonnaMakeItBrah Автор
      18.07.2023 18:40

      По ua не точно, но он покрывает большую часть устройств, хотя в любом правиле есть исключения это да, надо будет подумать над универсальным решением. Зачем во фронтенде эта штука? - чтобы адаптив настраивать и вертеть интерфейсом так, как хочется)


      1. carkatau01
        18.07.2023 18:40

        Chrome в скором времени начнёт урезать UA и информации в нем будет все меньше

        Вместо UA нужно смотреть в сторону Client Hints


  1. deamondz
    18.07.2023 18:40

    три вопроса:

    1. при чём тут vue?

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

    3. если вы предполагаете, что девайсом будут пользоваться мышкой, тачем, мышкой и тачем - зачем это может быть нужно детектировать, если придётся и так и так поддерживать все способы?


    1. GonnaMakeItBrah Автор
      18.07.2023 18:40
      +2

      1. Я писал это для использования во vue компонентах, поэтому vue

      2. А есть способы получше? Я с радостью посмотрю на них)

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


  1. Dimava
    18.07.2023 18:40
    +2

    Код выглядит как будто написан лет пять назад.

    namespace и enum уже давно использовать не рекомендуется, вместо них пришли ES Module и Union types: export type InteractionType = 'unknown' | 'mouse' | 'touch' | 'both'

    Vue Class Component уже давно deprecated, Vue 2 заканчивается в декабре 2023. Сейчас все потихоньку переезжают на Vue 3 и Composition API
    У класса реактивности нету ("просто создать computed свойство" не сработает - сам инстанс не реактивный)
    listener в addEventListener не обёрнуты, поэтому код даже не "не сработает" - а сработает, добавив interactionType в window

    Вот хороший пример как надо - https://vueuse.org/core/useScreenOrientation (исходный код - https://github.com/vueuse/vueuse/blob/main/packages/core/useScreenOrientation/index.ts )

    И последнее. У репозитория нету лицензии. А значит использовать данный код нельзя вообще.


    1. GonnaMakeItBrah Автор
      18.07.2023 18:40

      О, а это хорошее предложение) Да вот проблемка, я работал в компании где была vue 2, поэтому немного привык к его синтаксису, спасибо за замечания! Насчет лицензии ничего не знаю, я больше в ознакомительных целях пишу статьи


    1. GonnaMakeItBrah Автор
      18.07.2023 18:40

      Действительно, реактивности не было, теперь предусмотрел и реактивность! Компутеды снова начнут работать)


    1. GonnaMakeItBrah Автор
      18.07.2023 18:40

      лицензию тоже прикрутил)


  1. SuperCat911
    18.07.2023 18:40

    Раньше сталкивался с проблемой определения IPad, так как ua врал как мог. Решение на заметку:

    /**  @returns {boolean} */
    function isIPad() {
        let ua = window.navigator.userAgent.toLowerCase();
        return ua.indexOf('ipad') > -1 || ua.indexOf('macintosh') > -1 && navigator.maxTouchPoints && navigator.maxTouchPoints > 2
            && navigator.platform !== "iPhone";
    }


    1. GonnaMakeItBrah Автор
      18.07.2023 18:40

      Не тестил на айпаде, но в любом случае спасибо!)