Представляем визуальный порт просмотра

Что, если я скажу вам, что существует более одного окна просмотра.

БРРРРААААААМММММММММММММММММММММММММММММММММММММММММММММММММММ

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

БРРРРААААААМММММММММММММММММММММММММММММММММММММММММММММММММММ

И иногда данные, которые дает вам DOM, относятся к одному из этих окон просмотра, а не к другому.

БРРРРААААМ… подождите, что?

Это правда, посмотрите:

Окно просмотра макета и окно визуального просмотра

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

При обычной прокрутке все происходит довольно просто. Зеленая область представляет собой область просмотра макета , к которой относятся position: fixed элементы.

Ситуация становится странной, когда вводится масштабирование. Красный прямоугольник представляет собой визуальную область просмотра , то есть ту часть страницы, которую мы действительно можем видеть. Этот видовой экран может перемещаться во время position: fixed элементы остаются там, где они были, прикрепленные к видовому экрану макета. Если мы панорамируем границу видового экрана макета, он перетаскивает видовой экран макета вместе с ним.

Улучшение совместимости

К сожалению, веб-API несовместимы с точки зрения того, к какому экрану просмотра они относятся, а также они несовместимы в разных браузерах.

Например, element.getBoundingClientRect().y возвращает смещение в области просмотра макета . Это круто, но нам часто нужна позиция на странице, поэтому пишем:

element.getBoundingClientRect().y + window.scrollY

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

Chrome 61 изменяет window.scrollY чтобы вместо этого он ссылался на область просмотра макета, что означает, что приведенный выше код работает даже при масштабировании. Фактически, браузеры постепенно меняют все позиционные свойства, чтобы они ссылались на область просмотра макета.

За исключением одного нового объекта…

Предоставление визуального окна просмотра сценарию

Новый API предоставляет визуальную область просмотра как window.visualViewport . Это черновой вариант спецификации , одобренный всеми браузерами , и он появится в Chrome 61.

console.log(window.visualViewport.width);

Вот что нам дает window.visualViewport :

Свойства visualViewport
offsetLeft Расстояние между левым краем визуального окна просмотра и окном просмотра макета в CSS-пикселях.
offsetTop Расстояние между верхним краем визуального окна просмотра и окном просмотра макета в пикселях CSS.
pageLeft Расстояние между левым краем области визуального просмотра и левой границей документа в CSS-пикселях.
pageTop Расстояние между верхним краем области визуального просмотра и верхней границей документа в CSS-пикселях.
width Ширина области визуального просмотра в CSS-пикселях.
height Высота области визуального просмотра в CSS-пикселях.
scale Масштаб, примененный с помощью масштабирования. Если содержимое в два раза больше из-за масштабирования, это вернет 2 . devicePixelRatio на это не влияет.

Также есть пара событий:

window.visualViewport.addEventListener('resize', listener);
События visualViewport
resize Вызывается при изменении width , height или scale .
scroll Вызывается при изменении offsetLeft или offsetTop .

Демо

Видео в начале этой статьи было создано с помощью visualViewport , посмотрите его в Chrome 61+ . Он использует visualViewport , чтобы мини-карта прикреплялась к правому верхнему углу визуального окна просмотра, и применяет инверсный масштаб, чтобы она всегда отображалась одного и того же размера, несмотря на масштабирование.

Ошибки

События срабатывают только при изменении визуального окна просмотра.

Кажется, это очевидная вещь, но это зацепило меня, когда я впервые поигрался с visualViewport .

Если размер области просмотра макета изменяется, а область просмотра — нет, вы не получаете событие resize . Однако изменение размера области просмотра макета без изменения ширины/высоты визуальной области просмотра является необычным.

Настоящая ошибка — это прокрутка. Если происходит прокрутка, но область визуального просмотра остается статичной относительно области просмотра макета , вы не получаете событие scroll на visualViewport , и это действительно распространенная ситуация. Во время обычной прокрутки документа область визуального просмотра остается заблокированной в левом верхнем углу области просмотра макета, поэтому scroll не срабатывает в visualViewport .

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

visualViewport.addEventListener('scroll', update);
visualViewport.addEventListener('resize', update);
window.addEventListener('scroll', update);

Избегайте дублирования работы с несколькими слушателями

Подобно прослушиванию scroll и resize окна, в результате вы, вероятно, вызовете какую-то функцию «обновления». Однако многие из этих событий обычно происходят одновременно. Если пользователь изменит размер окна, это вызовет resize , но довольно часто также scroll . Чтобы повысить производительность, избегайте многократной обработки изменений:

// Add listeners
visualViewport.addEventListener('scroll', update);
visualViewport.addEventListener('resize', update);
addEventListener('scroll', update);

let pendingUpdate = false;

function update() {
    // If we're already going to handle an update, return
    if (pendingUpdate) return;

    pendingUpdate = true;

    // Use requestAnimationFrame so the update happens before next render
    requestAnimationFrame(() => {
    pendingUpdate = false;

    // Handle update here
    });
}

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

Обработчики событий не работают

Из-за ошибки Chrome это не работает :

Не

Багги – использует обработчик событий

visualViewport.onscroll = () => console.log('scroll!');

Вместо:

Делать

Работает – использует прослушиватель событий

visualViewport.addEventListener('scroll', () => console.log('scroll'));

Значения смещения округлены.

Я думаю (ну надеюсь), что это очередная ошибка Chrome .

offsetLeft и offsetTop округлены, что довольно неточно, если пользователь увеличил масштаб. Вы можете увидеть проблемы с этим во время демонстрации — если пользователь увеличивает масштаб и медленно панорамирует, мини-карта замыкается между неувеличенными пикселями .

Скорость событий низкая

Как и другие события resize и scroll , они не запускают каждый кадр, особенно на мобильных устройствах. Вы можете увидеть это во время демо : как только вы увеличите масштаб, мини-карта не сможет оставаться привязанной к окну просмотра.

Доступность

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

visualViewport можно использовать для улучшения доступности. Например, если пользователь увеличивает масштаб, вы можете скрыть декоративное position: fixed элементы, чтобы убрать их с дороги пользователя. Но опять же, будьте осторожны: вы не скрываете то, что пользователь пытается рассмотреть поближе.

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

visualViewport.addEventListener('resize', () => {
    if (visualViewport.scale > 1) {
    // Post data to analytics service
    }
});

Вот и все! visualViewport — это приятный небольшой API, который попутно решает проблемы совместимости.