Files
bare/web/js/zoom.js
T
mayatnikovandClaude Opus 5 f9ac83bef8 Развести номера ADR: 064/065/066 этой ветки → 073/074/075
Ветка отведена от 522ba89, где последним был ADR-063, и заняла
064–072. Тем временем в main через #3 и #4 пришли свои 064
(строка ввода — div вместо form), 065 (стороны сообщений)
и 066 (серверного перца нет).

Переезжают три ADR этой ветки, 067–072 остаются на месте:
их номера свободны, а внутренние ссылки менять незачем.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QR8uS3zGkybzWRW4GEY1oz
2026-08-24 11:23:12 +03:00

70 lines
3.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Запрет масштабирования — страховка к строке viewport (ADR-075).
//
// Основное средство — `maximum-scale=1, user-scalable=no` в index.html
// и `touch-action: manipulation` в app.css. Safari вправе не послушаться:
// он и раньше игнорировал `user-scalable=no` целиком. Здесь то, что
// работает независимо от его настроения, — и ничего больше: прокрутка,
// свайпы и обычные нажатия проходят как проходили.
// Щипок в Safari — это gesturestart/gesturechange/gestureend. В других
// браузерах их не бывает вовсе, и подписка ничего не стоит.
const GESTURES = ["gesturestart", "gesturechange", "gestureend"];
// Два тапа подряд ближе этого срока и этого расстояния друг к другу —
// двойной тап, то есть масштабирование. Пороги браузерные: свои цифры
// здесь всё равно ничем не проверить.
const TAP_APART = 350;
const TAP_NEAR = 40;
// Прошлый тап. До первого касания его не было вовсе: иначе тап в углу
// в первые триста миллисекунд жизни страницы сошёл бы за второй.
const last = { at: -Infinity, x: 0, y: 0 };
// lock вешает страховку. Слушатели непассивные: пассивному
// preventDefault не даёт ничего.
export function lock() {
for (const name of GESTURES) {
document.addEventListener(name, stop, { passive: false });
}
document.addEventListener("touchend", tap, { passive: false });
}
function stop(event) {
event.preventDefault();
}
// tap гасит второй тап подряд — он и масштабирует. Первый доходит
// до страницы целиком, поэтому нажатия, свайпы и прокрутка не меняются.
function tap(event) {
// Второй палец на экране — это щипок или прокрутка двумя, не тап.
if (event.touches.length > 0 || event.changedTouches.length !== 1) {
last.at = -Infinity;
return;
}
const touch = event.changedTouches[0];
const quick = event.timeStamp - last.at <= TAP_APART;
const near = Math.abs(touch.clientX - last.x) <= TAP_NEAR
&& Math.abs(touch.clientY - last.y) <= TAP_NEAR;
last.at = event.timeStamp;
last.x = touch.clientX;
last.y = touch.clientY;
if (!quick || !near || control(event.target)) {
return;
}
event.preventDefault();
}
// control — элемент управления. Второй тап по нему не гасится:
// preventDefault на touchend уносит с собой click, а нажать кнопку
// дважды подряд — обычное дело. Цена мала: до элементов управления зум
// по двойному тапу не доходит и без страховки — `touch-action:
// manipulation` стоит на документе.
//
// Строка сообщения — редактируемый блок, а не textarea (ADR-069), и она
// в этом перечне: погашенный тап не доносит до неё ни click, ни фокус,
// то есть второй тап подряд не ставил бы курсор в поле ввода.
function control(target) {
return typeof target?.closest === "function"
&& target.closest("button, input, textarea, select, label, a, [contenteditable]") !== null;
}