javascript

JavaScript: заметка об Invoker Commands API

  • понедельник, 22 декабря 2025 г. в 00:00:06
https://habr.com/ru/companies/timeweb/articles/972668/

Привет, друзья!

В этой небольшой статье я хочу рассказать вам о новом Web API — Invoker Commands.

Invoker Commands API позволяет декларативно управлять поведением некоторых интерактивных элементов с помощью кнопок. «Декларативно» означает, что управления элементами осуществляется только с помощью HTML, без JavaScript.

На сегодняшний день этот API поддерживается всеми основными браузерами (в Safari пока только в качестве экспериментальной возможности):

❯ Как это работает

В настоящее время Invoker Commands API позволяет управлять двумя элементами:

  • dialog

  • поповером — элементом с атрибутом popover (еще один новый Web API - Popover)

У кнопок появилось два новых атрибута:

  • commandfor — идентификатор управляемого элемента

  • command — команда для выполнения управляемым элементом, определенным с помощью атрибута commandfor

На сегодняшний день доступны следующие команды (со временем список будет расширяться):

  • show-modal — отображает dialog в виде модального окна. Если dialog уже отображен в виде модалки, ничего не происходит. Является декларативным эквивалентом вызова метода HTMLDialogElement.showModal()

  • close — закрывает dialog. Если dialog уже закрыт, ничего не происходит. Является декларативным эквивалентом вызова метода HTMLDialogElement.close()

  • request-close — кнопка сначала вызывает событие cancel на dialog и только потом — событие close. Вызов preventDefault() на событии cancel отменяет закрытие dialog (в этом отличие этой команды от команды close, которая сразу вызывает событие close). Если dialog уже закрыт, ничего не происходит. Является декларативным эквивалентом вызова метода HTMLDialogElement.requestClose()

  • show-popover — отображает скрытый поповер. Если поповер уже отображен, ничего не происходит. Является эквивалентом установки значения show атрибута popovertargetaction, а также декларативным эквивалентом вызова метода HTMLElement.showPopover()

  • hide-popover — скрывает отображаемый поповер. Если поповер уже скрыт, ничего не происходит. Является эквивалентом установки значения hide атрибута popovertargetaction, а также декларативным эквивалентом вызова метода HTMLElement.hidePopover()

  • toggle-popover — переключает состояние видимости поповера. Является эквивалентом установки значения toggle атрибута popovertargetaction, а также декларативным эквивалентом вызова метода HTMLElement.togglePopover()

  • кастомное значение — строка, начинающаяся с -- (как переменные CSS). Нажатие кнопки с такой командой вызывает событие command на управляемом элементе. Эта команда открывает широкие возможности по управлению интерактивными элементами

Таким образом, для использования Invoker Commands API достаточно определить dialog или поповер с уникальным идентификатором и кнопку с атрибутами commandfor и command.

❯ Примеры

Рассмотрим парочку примеров использования Invoker Commands API.

Отображение/скрытие поповера и dialog

Кнопка отображения/скрытия поповера:

<button commandfor='my-popover' command='toggle-popover'>
  Toggle popover
</button>

Поповер, управляемый этой кнопкой:

<!-- Значение атрибута `id` этого поповера должно совпадать со значением атрибута `commandfor` кнопки -->
<div id='my-popover' popover>
  <!-- Кнопка для скрытия этого поповера -->
  <button commandfor='my-popover' command='hide-popover'>
    Close
  </button>
  <h3>Popover</h3>
</div>

Кнопка для отображения dialog в виде модального окна:

<button commandfor='my-dialog' command='show-modal'>
  Show modal dialog
</button>

dialog, управляемый этой кнопкой:

<dialog id='my-dialog'>
  <!-- Кнопка для скрытия этого `dialog` -->
  <button commandfor='my-dialog' command='close'>
    Close
  </button>
  <h3>Dialog</h3>
</dialog>

Вы не поверите, но это все (ну почти), что необходимо для реализации функционала отображения/скрытия поповера и dialog. Очень удобно, не правда ли?

Песочница:

Поиск нужного поповера или dialog для выполнения команды для браузера почти ничего не стоит с точки зрения производительности, поскольку элементы с атрибутом id являются свойствами глобального объекта window. Другими словами, время поиска таких элементов является константным (O(1)).

Ссылку на DOM-элемент всегда лучше получать с помощью метода getElementById etc., поскольку в некоторых средах выполнения window может отсутствовать.

Лайтбокс

Рассмотрим пример использования кастомной команды, реализовав функционал лайтбокса (lightbox).

Структурно наш лайтбокс будет состоять из кнопки, открывающей dialog в режиме модального окна, содержащий слайдер (slider) из трех изображений и три кнопки: для закрытия dialog и смены слайдов.

Кнопка для отображения dialog в виде модалки:

<button commandfor="lightbox" command="show-modal">Open lightbox</button>

dialog с тремя изображениями и тремя кнопками:

<dialog id="lightbox">
  <div class="lightbox-content">
    <!-- Кнопка закрытия `dialog` -->
    <button commandfor="lightbox" command="close" id="close-dialog-button">
      Close
    </button>
    <!-- Кнопка переключения предыдущего слайда -->
    <button commandfor="lightbox" command="--prev-slide">Prev</button>
    <div>
      <img src="https://cdn.pixabay.com/photo/2012/11/28/08/54/milky-way-67504_960_720.jpg" alt="" />
      <img src="https://cdn.pixabay.com/photo/2016/11/29/02/20/cosmos-1866820_960_720.jpg" alt="" />
      <img src="https://cdn.pixabay.com/photo/2016/09/08/12/00/stars-1654074_960_720.jpg" alt="" />
    </div>
    <!-- Кнопка переключения следующего слайда -->
    <button commandfor="lightbox" command="--next-slide">Next</button>
  </div>
</dialog>

Минимальные стили:

html,
body {
  height: 100%;
}

#close-dialog-button {
  position: absolute;
  top: 0;
  right: 0;
}

dialog {
  padding: 0;
  background: none;
  border: none;
  outline: none;

  &::backdrop {
    background-color: rgba(0, 0, 0, 0.5);
  }

  .lightbox-content {
    display: flex;
    align-items: center;
    gap: 1rem;
  }

  img {
    width: 480px;
    height: 320px;
    object-fit: cover;
    /* По умолчанию все изображения скрыты */
    display: none;
  }
}

При использовании кастомных команд без JS уже не обойтись:

// Индекс текущего изображения
let currentIndex = 0;

// `dialog`
const dialog = document.querySelector("#lightbox");
// Изображения
const images = [...dialog.querySelectorAll("img")];
// Показываем первое изображение
images[currentIndex].style.display = "block";

// Обрабатываем кастомные команды
dialog.addEventListener("command", (event) => {
  // Новое свойство `Event`
  switch (event.command) {
    // Переключаем предыдущий слайд
    case "--prev-slide": {
      // Скрываем текущее изображение
      images[currentIndex].style.display = "none";
      // Обновляем индекс
      currentIndex =
        currentIndex - 1 < 0 ? images.length - 1 : currentIndex - 1;
      // Показываем предыдущее изображение
      images[currentIndex].style.display = "block";
      break;
    }
    // Переключаем следующее изображение
    case "--next-slide": {
      images[currentIndex].style.display = "none";
      currentIndex =
        currentIndex + 1 > images.length - 1 ? 0 : currentIndex + 1;
      images[currentIndex].style.display = "block";
      break;
    }
  }
});

Песочница:

Управление с помощью клавиатуры

В нашем лайтбоксе из коробки (благодаря dialog) частично реализовано управление с помощью клавиатуры: захват фокуса, установка фокуса на первый интерактивный элемент (кнопка закрытия), переключение между кнопками с помощью Tab и Shift+Tab и закрытие dialog с помощью Escape.

Хотелось бы также иметь возможность переключать слайды с помощью стрелок. Сделать это проще простого:

// Получаем ссылки на кнопки переключения слайдов
// (не забудьте добавить кнопкам соответствующие `id`)
const prevSlideButton = dialog.querySelector("#prev-slide-button");
const nextSlideButton = dialog.querySelector("#next-slide-button");

// Обрабатываем нажатие стрелок
document.addEventListener("keydown", (event) => {
  if (!dialog.open) return;

  switch (event.key) {
    case "ArrowLeft": {
      prevSlideButton.focus();
      prevSlideButton.click();
      break;
    }
    case "ArrowRight": {
      nextSlideButton.focus();
      nextSlideButton.click();
      break;
    }
  }
});

Глядя на этот код, у вас может возникнуть два вопроса:

  1. Для чего нам фокусировка на кнопках (вызов метода focus())?

  2. Почему не использовать класс CustomEvent, что представляется более верным семантически?

Фокусировка на кнопках нужна для того, чтобы при нажатии Enter нажималась та же кнопка. Это важно, коль скоро мы говорим о полноценном управлении слайдером с помощью клавиатуры. Без этого при нажатии Enter будет нажиматься кнопка закрытия dialog.

Событие command на dialog действительно можно вызывать с помощью экземпляра CustomEvent, например:

dialog.dispatchEvent(
  new CustomEvent('command', { detail: '--prev-slide' }),
)

Но, во-первых, у кастомного события нет свойства command (возможно, появится в будущем), поэтому в обработчике команд нужна такая строка:

// `event.command` будет иметь значение `undefined` при `dispatchEvent()`
const command = event.command ?? event.detail

Во-вторых, остается проблема отсутствия фокусировки на активных кнопках.

Песочница:

Это все, чем я хотел поделиться с вами в этой небольшой заметке. Надеюсь, вы узнали что-то новое и, следовательно, не зря потратили время.

Happy coding!


Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud - в нашем Telegram-канале